From 0f29c04d72660f0cb6284d2795cbb156a0fd08ae Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 4 Mar 2025 19:42:27 -0500 Subject: [PATCH 01/17] feat: Add initial banner for initialization --- package-lock.json | 290 +++++++++++++++++- package.json | 10 +- src/commands/init.ts | 71 +++++ src/orchestrators/destroy.ts | 2 +- src/orchestrators/import.ts | 3 +- src/orchestrators/initialize-plugins.ts | 100 ++++++ src/orchestrators/initialize.ts | 100 ------ src/orchestrators/plan.ts | 2 +- src/orchestrators/validate.ts | 14 +- src/ui/components/default-component.tsx | 6 + src/ui/components/init/InitBanner.tsx | 29 ++ src/ui/reporters/default-reporter.tsx | 4 + src/ui/reporters/reporter.ts | 2 + src/ui/store/index.ts | 1 + .../initialize/initialize.test.ts | 2 +- 15 files changed, 511 insertions(+), 125 deletions(-) create mode 100644 src/commands/init.ts create mode 100644 src/orchestrators/initialize-plugins.ts create mode 100644 src/ui/components/init/InitBanner.tsx diff --git a/package-lock.json b/package-lock.json index ac58d42d..d5e44831 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@inkjs/ui": "^2", "@oclif/core": "^4.0.8", + "@oclif/plugin-autocomplete": "^3.2.24", "@oclif/plugin-help": "^6.2.4", "@oclif/plugin-update": "^4.6.13", "ajv": "^8.12.0", @@ -23,6 +24,8 @@ "detect-indent": "^7.0.1", "diff": "^7.0.0", "ink": "^5.1.0", + "ink-big-text": "^2.0.0", + "ink-gradient": "^3.0.0", "ink-select-input": "^6.0.0", "jotai": "^2.11.1", "js-yaml": "^4.1.0", @@ -2976,6 +2979,20 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/@oclif/plugin-autocomplete": { + "version": "3.2.24", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.24.tgz", + "integrity": "sha512-KGz7ypHhahRJWy0sB+mNvLLJiMWEt4pgjCFIpcBhkZhc090H4e4QhGR6Xp2430rQgStPcnUa0BYcVZJPojAsDQ==", + "dependencies": { + "@oclif/core": "^4", + "ansis": "^3.16.0", + "debug": "^4.4.0", + "ejs": "^3.1.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@oclif/plugin-help": { "version": "6.2.18", "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.18.tgz", @@ -4258,6 +4275,14 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "node_modules/@types/gradient-string": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@types/gradient-string/-/gradient-string-1.1.6.tgz", + "integrity": "sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==", + "dependencies": { + "@types/tinycolor2": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -4346,6 +4371,11 @@ "strip-ansi": "*" } }, + "node_modules/@types/tinycolor2": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", + "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==" + }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", @@ -5102,11 +5132,11 @@ } }, "node_modules/ansis": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.3.2.tgz", - "integrity": "sha512-cFthbBlt+Oi0i9Pv/j6YdVWJh54CtjGACaMPCIrEV4Ha7HWsIjXDwseYV79TIL0B4+KfSwD5S70PeQDkPUd1rA==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", "engines": { - "node": ">=15" + "node": ">=14" } }, "node_modules/anymatch": { @@ -5669,6 +5699,35 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/cfonts": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/cfonts/-/cfonts-3.3.0.tgz", + "integrity": "sha512-RlVxeEw2FXWI5Bs9LD0/Ef3bsQIc9m6lK/DINN20HIW0Y0YHUO2jjy88cot9YKZITiRTCdWzTfLmTyx47HeSLA==", + "dependencies": { + "supports-color": "^8", + "window-size": "^1" + }, + "bin": { + "cfonts": "bin/index.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cfonts/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/chai": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", @@ -6233,9 +6292,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dependencies": { "ms": "^2.1.3" }, @@ -6358,6 +6417,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -8144,7 +8214,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8508,6 +8577,58 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/gradient-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-2.0.2.tgz", + "integrity": "sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==", + "dependencies": { + "chalk": "^4.1.2", + "tinygradient": "^1.1.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gradient-string/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/gradient-string/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/gradient-string/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -8592,7 +8713,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -8862,6 +8982,45 @@ } } }, + "node_modules/ink-big-text": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ink-big-text/-/ink-big-text-2.0.0.tgz", + "integrity": "sha512-Juzqv+rIOLGuhMJiE50VtS6dg6olWfzFdL7wsU/EARSL5Eaa5JNXMogMBm9AkjgzO2Y3UwWCOh87jbhSn8aNdw==", + "dependencies": { + "cfonts": "^3.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + }, + "peerDependencies": { + "ink": ">=4", + "react": ">=18" + } + }, + "node_modules/ink-gradient": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ink-gradient/-/ink-gradient-3.0.0.tgz", + "integrity": "sha512-OVyPBovBxE1tFcBhSamb+P1puqDP6pG3xFe2W9NiLgwUZd9RbcjBeR7twLbliUT9navrUstEf1ZcPKKvx71BsQ==", + "dependencies": { + "@types/gradient-string": "^1.1.2", + "gradient-string": "^2.0.2", + "prop-types": "^15.8.1", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + }, + "peerDependencies": { + "ink": ">=4" + } + }, "node_modules/ink-select-input": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/ink-select-input/-/ink-select-input-6.0.0.tgz", @@ -9079,6 +9238,17 @@ "node": ">= 12" } }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9163,6 +9333,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-builtin-module": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", @@ -9214,6 +9389,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/is-data-view": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", @@ -9247,6 +9433,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -9789,6 +9987,17 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/latest-semver": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/latest-semver/-/latest-semver-4.0.0.tgz", @@ -10298,6 +10507,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", @@ -10909,6 +11126,16 @@ "node": ">=0.4.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -11108,6 +11335,11 @@ } } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-reconciler": { "version": "0.29.2", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", @@ -12505,6 +12737,11 @@ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, "node_modules/tinyexec": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", @@ -12550,6 +12787,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinygradient": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", + "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", + "dependencies": { + "@types/tinycolor2": "^1.4.0", + "tinycolor2": "^1.0.0" + } + }, "node_modules/tinypool": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", @@ -13656,6 +13902,32 @@ "node": ">=8" } }, + "node_modules/window-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-1.1.1.tgz", + "integrity": "sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA==", + "dependencies": { + "define-property": "^1.0.0", + "is-number": "^3.0.0" + }, + "bin": { + "window-size": "cli.js" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/window-size/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 3fc6ea8c..84c095a9 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ }, "dependencies": { "@codifycli/ink-form": "0.0.11", - "ink-select-input": "^6.0.0", "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@inkjs/ui": "^2", "@oclif/core": "^4.0.8", + "@oclif/plugin-autocomplete": "^3.2.24", "@oclif/plugin-help": "^6.2.4", "@oclif/plugin-update": "^4.6.13", "ajv": "^8.12.0", @@ -19,6 +19,10 @@ "detect-indent": "^7.0.1", "diff": "^7.0.0", "ink": "^5.1.0", + "ink-big-text": "^2.0.0", + "ink-gradient": "^3.0.0", + "ink-select-input": "^6.0.0", + "@fforres/ink-quicksearch-input": "^1.0.1", "jotai": "^2.11.1", "js-yaml": "^4.1.0", "js-yaml-source-map": "^0.2.2", @@ -32,6 +36,7 @@ }, "description": "Codify allows users to configure settings, install new packages, and automate their systems using code instead of the GUI. Get set up on a new laptop in one click, maintain a Codify file within your project so anyone can get started and never lose your cool apps or favourite settings again.", "devDependencies": { + "@memlab/core": "^1.1.39", "@oclif/prettier-config": "^0.2.1", "@types/chalk": "^2.2.0", "@types/debug": "^4.1.12", @@ -44,7 +49,6 @@ "@types/strip-ansi": "^5.2.1", "@typescript-eslint/eslint-plugin": "^8.16.0", "codify-plugin-lib": "^1.0.151", - "react-devtools-core": "4.28.5", "esbuild": "^0.24.0", "esbuild-plugin-copy": "^2.1.1", "eslint": "^8.51.0", @@ -54,8 +58,8 @@ "ink-testing-library": "^4.0.0", "memfs": "^4.14.0", "mocha": "^10", - "@memlab/core": "^1.1.39", "oclif": "^4.15.29", + "react-devtools-core": "4.28.5", "shx": "^0.3.3", "strip-ansi": "^7.1.0", "tsx": "^4.7.3", diff --git a/src/commands/init.ts b/src/commands/init.ts new file mode 100644 index 00000000..9ea66b2e --- /dev/null +++ b/src/commands/init.ts @@ -0,0 +1,71 @@ +import fs from 'node:fs/promises'; + +import { BaseCommand } from '../common/base-command.js'; +import { ShellUtils } from '../utils/shell.js'; + +export default class Init extends BaseCommand { + static strict = false; + static override description = +`Generate codify configs from already installed packages. Use a list of space separated arguments to specify the resource types to import. Leave blank to import all resource in an existing *.codify.json file. + +Modes: +1. No args: if no args are specified and an *.codify.json already exists. Then codify will update the existing file with any new changes to the resources specified in the file/files. + +Command: codify import + +2. With args: specify specific resources to import using arguments. Wild card matching is supported using '*' and ? (Note: in zsh * expands to the current dir and needs to be escaped using \\* or '*'). A prompt will be shown if more information is required to complete the import. + +Example: codify import nvm asdf\\*, codify import \\* (for importing all supported resources) + +The results can then be saved: + a. To an existing *.codify.json file + b. To a new file + c. Or only printed to console + +Codify will try to smartly insert new configs by following existing spacing and formatting. +` + + static override examples = [ + '<%= config.bin %> <%= command.id %> homebrew nvm asdf\\*', + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> git-clone --path ../my/other/folder', + '<%= config.bin %> <%= command.id %> \\*' + ] + + public async run(): Promise { + const { raw, flags } = await this.parse(Init) + + this.reporter.displayInitBanner() + + // if (flags.path) { + // this.log(`Applying Codify from: ${flags.path}`); + // } + // + // const resolvedPath = path.resolve(flags.path ?? '.'); + // + // const args = raw + // .filter((r) => r.type === 'arg') + // .map((r) => r.input); + // + // const cleanedArgs = await this.cleanupZshStarExpansion(args); + // + // await ImportOrchestrator.run({ + // typeIds: cleanedArgs, + // path: resolvedPath, + // secureMode: flags.secure, + // }, this.reporter) + // + // process.exit(0) + } + + private async cleanupZshStarExpansion(args: string[]): Promise { + const combinedArgs = args.join(' '); + const zshStarExpansion = (await ShellUtils.isZshShell()) + ? (await fs.readdir(process.cwd())).filter((name) => !name.startsWith('.')).join(' ') + : '' + + return combinedArgs + .replaceAll(zshStarExpansion, '*') + .split(' ') + } +} diff --git a/src/orchestrators/destroy.ts b/src/orchestrators/destroy.ts index c60da0e3..380b24ee 100644 --- a/src/orchestrators/destroy.ts +++ b/src/orchestrators/destroy.ts @@ -5,7 +5,7 @@ import { ProcessName, SubProcessName, ctx } from '../events/context.js'; import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js'; import { Reporter } from '../ui/reporters/reporter.js'; import { getTypeAndNameFromId } from '../utils/index.js'; -import { InitializeOrchestrator } from './initialize.js'; +import { InitializeOrchestrator } from './initialize-plugins.js'; export interface DestroyArgs { ids: string[]; diff --git a/src/orchestrators/import.ts b/src/orchestrators/import.ts index 66b15e75..9b4a07cf 100644 --- a/src/orchestrators/import.ts +++ b/src/orchestrators/import.ts @@ -12,8 +12,7 @@ import { FileUtils } from '../utils/file.js'; import { FileModificationCalculator, ModificationType } from '../utils/file-modification-calculator.js'; import { groupBy, sleep } from '../utils/index.js'; import { wildCardMatch } from '../utils/wild-card-match.js'; -import { InitializationResult, InitializeOrchestrator } from './initialize.js'; -import { ValidateOrchestrator } from './validate.js'; +import { InitializationResult, InitializeOrchestrator } from './initialize-plugins.js'; export type ImportResult = { result: ResourceConfig[], errors: string[] } diff --git a/src/orchestrators/initialize-plugins.ts b/src/orchestrators/initialize-plugins.ts new file mode 100644 index 00000000..67804263 --- /dev/null +++ b/src/orchestrators/initialize-plugins.ts @@ -0,0 +1,100 @@ +import * as fs from 'node:fs/promises' +import * as os from 'node:os' +import * as path from 'node:path' + +import { Project } from '../entities/project.js'; +import { SubProcessName, ctx } from '../events/context.js'; +import { CODIFY_FILE_REGEX, CodifyParser } from '../parser/index.js'; +import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js'; +import { Reporter } from '../ui/reporters/reporter.js'; + +export interface InitializeArgs { + path?: string; + secure?: boolean; + transformProject?: (project: Project) => Project | Promise; + allowEmptyProject?: boolean; +} + +export interface InitializationResult { + typeIdsToDependenciesMap: DependencyMap + pluginManager: PluginManager, + project: Project, +} + +export class InitializeOrchestrator { + static async run( + args: InitializeArgs, + reporter: Reporter, + ): Promise { + let project = await InitializeOrchestrator.parse( + args.path, + args.allowEmptyProject ?? false, + reporter + ) + if (args.transformProject) { + project = await args.transformProject(project); + } + + ctx.subprocessStarted(SubProcessName.INITIALIZE_PLUGINS) + const pluginManager = new PluginManager(); + const typeIdsToDependenciesMap = await pluginManager.initialize(project, args.secure); + ctx.subprocessFinished(SubProcessName.INITIALIZE_PLUGINS) + + return { typeIdsToDependenciesMap, pluginManager, project }; + } + + private static async parse( + fileOrDir: string | undefined, + allowEmptyProject: boolean, + reporter: Reporter + ): Promise { + ctx.subprocessStarted(SubProcessName.PARSE); + + const pathToParse = (fileOrDir === undefined) + ? await InitializeOrchestrator.findCodifyJson() + : fileOrDir + + if (!pathToParse && !allowEmptyProject) { + ctx.subprocessFinished(SubProcessName.PARSE); + ctx.subprocessStarted(SubProcessName.CREATE_ROOT_FILE) + const createRootCodifyFile = await reporter.promptConfirmation('\nNo codify file found. Do you want to create a root file at ~/codify.json?'); + + if (createRootCodifyFile) { + await fs.writeFile( + path.resolve(os.homedir(), 'codify.json'), + '[]', + { encoding: 'utf8', flag: 'wx' } + ); // flag: 'wx' prevents overwrites if the file exists + } + + ctx.subprocessFinished(SubProcessName.CREATE_ROOT_FILE) + + console.log('Created ~/codify.json file') + process.exit(0); + } + + const project = pathToParse + ? await CodifyParser.parse(pathToParse) + : Project.empty() + + ctx.subprocessFinished(SubProcessName.PARSE); + + return project + } + + private static async findCodifyJson(dir?: string): Promise { + dir = dir ?? process.cwd(); + + const filesInDir = await fs.readdir(dir); + if (filesInDir.some((f) => CODIFY_FILE_REGEX.test(f))) { + return dir; + } + + if (dir.includes(os.homedir()) && dir !== os.homedir()) { + return this.findCodifyJson(path.dirname(dir)) + } + + return null; + } + +} diff --git a/src/orchestrators/initialize.ts b/src/orchestrators/initialize.ts index 67804263..e69de29b 100644 --- a/src/orchestrators/initialize.ts +++ b/src/orchestrators/initialize.ts @@ -1,100 +0,0 @@ -import * as fs from 'node:fs/promises' -import * as os from 'node:os' -import * as path from 'node:path' - -import { Project } from '../entities/project.js'; -import { SubProcessName, ctx } from '../events/context.js'; -import { CODIFY_FILE_REGEX, CodifyParser } from '../parser/index.js'; -import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js'; -import { Reporter } from '../ui/reporters/reporter.js'; - -export interface InitializeArgs { - path?: string; - secure?: boolean; - transformProject?: (project: Project) => Project | Promise; - allowEmptyProject?: boolean; -} - -export interface InitializationResult { - typeIdsToDependenciesMap: DependencyMap - pluginManager: PluginManager, - project: Project, -} - -export class InitializeOrchestrator { - static async run( - args: InitializeArgs, - reporter: Reporter, - ): Promise { - let project = await InitializeOrchestrator.parse( - args.path, - args.allowEmptyProject ?? false, - reporter - ) - if (args.transformProject) { - project = await args.transformProject(project); - } - - ctx.subprocessStarted(SubProcessName.INITIALIZE_PLUGINS) - const pluginManager = new PluginManager(); - const typeIdsToDependenciesMap = await pluginManager.initialize(project, args.secure); - ctx.subprocessFinished(SubProcessName.INITIALIZE_PLUGINS) - - return { typeIdsToDependenciesMap, pluginManager, project }; - } - - private static async parse( - fileOrDir: string | undefined, - allowEmptyProject: boolean, - reporter: Reporter - ): Promise { - ctx.subprocessStarted(SubProcessName.PARSE); - - const pathToParse = (fileOrDir === undefined) - ? await InitializeOrchestrator.findCodifyJson() - : fileOrDir - - if (!pathToParse && !allowEmptyProject) { - ctx.subprocessFinished(SubProcessName.PARSE); - ctx.subprocessStarted(SubProcessName.CREATE_ROOT_FILE) - const createRootCodifyFile = await reporter.promptConfirmation('\nNo codify file found. Do you want to create a root file at ~/codify.json?'); - - if (createRootCodifyFile) { - await fs.writeFile( - path.resolve(os.homedir(), 'codify.json'), - '[]', - { encoding: 'utf8', flag: 'wx' } - ); // flag: 'wx' prevents overwrites if the file exists - } - - ctx.subprocessFinished(SubProcessName.CREATE_ROOT_FILE) - - console.log('Created ~/codify.json file') - process.exit(0); - } - - const project = pathToParse - ? await CodifyParser.parse(pathToParse) - : Project.empty() - - ctx.subprocessFinished(SubProcessName.PARSE); - - return project - } - - private static async findCodifyJson(dir?: string): Promise { - dir = dir ?? process.cwd(); - - const filesInDir = await fs.readdir(dir); - if (filesInDir.some((f) => CODIFY_FILE_REGEX.test(f))) { - return dir; - } - - if (dir.includes(os.homedir()) && dir !== os.homedir()) { - return this.findCodifyJson(path.dirname(dir)) - } - - return null; - } - -} diff --git a/src/orchestrators/plan.ts b/src/orchestrators/plan.ts index 26bef889..71b5f2e7 100644 --- a/src/orchestrators/plan.ts +++ b/src/orchestrators/plan.ts @@ -4,7 +4,7 @@ import { ProcessName, SubProcessName, ctx } from '../events/context.js'; import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js'; import { Reporter } from '../ui/reporters/reporter.js'; import { createStartupShellScriptsIfNotExists } from '../utils/file.js'; -import { InitializeOrchestrator } from './initialize.js'; +import { InitializeOrchestrator } from './initialize-plugins.js'; import { ValidateOrchestrator } from './validate.js'; export interface PlanArgs { diff --git a/src/orchestrators/validate.ts b/src/orchestrators/validate.ts index d7cbb9c7..9cddef15 100644 --- a/src/orchestrators/validate.ts +++ b/src/orchestrators/validate.ts @@ -1,17 +1,15 @@ -import { ctx, SubProcessName } from '../events/context.js'; -import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js'; -import { Project } from '../entities/project.js'; -import { InitializationResult, InitializeOrchestrator } from './initialize.js'; +import { SubProcessName, ctx } from '../events/context.js'; import { Reporter } from '../ui/reporters/reporter.js'; +import { InitializationResult, InitializeOrchestrator } from './initialize-plugins.js'; export interface ValidateArgs { existing?: InitializationResult; path?: string; } -export class ValidateOrchestrator { +export const ValidateOrchestrator = { - static async run( + async run( args: ValidateArgs, reporter: Reporter ): Promise { @@ -36,5 +34,5 @@ export class ValidateOrchestrator { } else { ctx.processFinished(SubProcessName.VALIDATE) } - } -} + }, +}; diff --git a/src/ui/components/default-component.tsx b/src/ui/components/default-component.tsx index a3d7263d..00c44725 100644 --- a/src/ui/components/default-component.tsx +++ b/src/ui/components/default-component.tsx @@ -16,6 +16,7 @@ import { RenderStatus, store } from '../store/index.js'; import { FileModificationDisplay } from './file-modification/FileModification.js'; import { ImportResultComponent } from './import/import-result.js'; import { ImportWarning } from './import/import-warning.js'; +import { InitBanner } from './init/InitBanner.js'; import { PlanComponent } from './plan/plan.js'; import { ProgressDisplay } from './progress/progress-display.js'; @@ -130,5 +131,10 @@ export function DefaultComponent(props: { ) } + { + renderStatus === RenderStatus.DISPLAY_INIT_BANNER && ( + + ) + } } diff --git a/src/ui/components/init/InitBanner.tsx b/src/ui/components/init/InitBanner.tsx new file mode 100644 index 00000000..5c7efd80 --- /dev/null +++ b/src/ui/components/init/InitBanner.tsx @@ -0,0 +1,29 @@ +import { MultiSelect } from '@inkjs/ui'; +import { Box, Text } from 'ink'; +import BigText from 'ink-big-text'; +import Gradient from 'ink-gradient'; +import React from 'react'; + +export function InitBanner() { + return + + + + Use config + Use this init flow to get setup quickly with Codify. + + Select which resources to import: + + +} diff --git a/src/ui/reporters/default-reporter.tsx b/src/ui/reporters/default-reporter.tsx index 7dd1ec5f..11842d70 100644 --- a/src/ui/reporters/default-reporter.tsx +++ b/src/ui/reporters/default-reporter.tsx @@ -48,6 +48,10 @@ export class DefaultReporter implements Reporter { ctx.on(Event.SUB_PROCESS_FINISH, (name, additionalName) => this.onSubprocessFinishEvent(name, additionalName)); } + displayInitBanner(): void { + this.updateRenderState(RenderStatus.DISPLAY_INIT_BANNER); + } + async displayImportWarning(requiresParameters: string[], noParametersRequired: string[]): Promise { await this.updateStateAndAwaitEvent( () => this.updateRenderState(RenderStatus.IMPORT_PROMPT_WARNING, { requiresParameters, noParametersRequired }), diff --git a/src/ui/reporters/reporter.ts b/src/ui/reporters/reporter.ts index 29f0e3e6..1f1db715 100644 --- a/src/ui/reporters/reporter.ts +++ b/src/ui/reporters/reporter.ts @@ -44,6 +44,8 @@ export enum PromptType { export interface Reporter { displayPlan(plan: Plan): void + displayInitBanner(): void + promptConfirmation(message: string): Promise promptOptions(message: string, options: string[]): Promise; diff --git a/src/ui/store/index.ts b/src/ui/store/index.ts index ea7a8e20..3bd486a4 100644 --- a/src/ui/store/index.ts +++ b/src/ui/store/index.ts @@ -10,6 +10,7 @@ export interface RenderState { export enum RenderStatus { NOTHING, PROGRESS, + DISPLAY_INIT_BANNER, DISPLAY_PLAN, DISPLAY_IMPORT_RESULT, DISPLAY_FILE_MODIFICATION, diff --git a/test/orchestrator/initialize/initialize.test.ts b/test/orchestrator/initialize/initialize.test.ts index 6ab486df..22480db0 100644 --- a/test/orchestrator/initialize/initialize.test.ts +++ b/test/orchestrator/initialize/initialize.test.ts @@ -3,7 +3,7 @@ import * as fs from 'node:fs'; import os from 'node:os'; import { MockOs } from '../mocks/system'; -import { InitializeOrchestrator } from '../../../src/orchestrators/initialize'; +import { InitializeOrchestrator } from '../../../src/orchestrators/initialize-plugins'; import path from 'node:path'; import { MockReporter } from '../mocks/reporter'; import { MockResource, MockResourceConfig } from '../mocks/resource'; From 2a20b198065d03fa3a0b2e421f53dd40f7a04d5b Mon Sep 17 00:00:00 2001 From: kevinwang Date: Wed, 5 Mar 2025 10:33:56 -0500 Subject: [PATCH 02/17] feat: Added importing of resources and selection flow --- package-lock.json | 13 +- package.json | 3 +- src/commands/init.ts | 3 +- src/events/context.ts | 1 + src/orchestrators/destroy.ts | 4 +- src/orchestrators/import.ts | 4 +- src/orchestrators/initialize-plugins.ts | 6 +- src/orchestrators/initialize.ts | 37 +++++ src/orchestrators/plan.ts | 6 +- src/orchestrators/validate.ts | 4 +- src/plugins/plugin-manager.ts | 4 +- src/plugins/plugin.ts | 5 +- src/ui/components/default-component.tsx | 18 ++- src/ui/components/init/InitBanner.tsx | 37 ++--- src/ui/components/multi-select/checkbox.tsx | 12 ++ src/ui/components/multi-select/indicator.tsx | 14 ++ src/ui/components/multi-select/item.tsx | 10 ++ .../multi-select/multi-select.test.tsx | 0 .../components/multi-select/multi-select.tsx | 140 ++++++++++++++++++ src/ui/components/multi-select/utils.ts | 8 + src/ui/reporters/default-reporter.tsx | 23 ++- src/ui/reporters/reporter.ts | 8 +- src/ui/store/index.ts | 1 + .../initialize/initialize.test.ts | 6 +- 24 files changed, 318 insertions(+), 49 deletions(-) create mode 100644 src/ui/components/multi-select/checkbox.tsx create mode 100644 src/ui/components/multi-select/indicator.tsx create mode 100644 src/ui/components/multi-select/item.tsx create mode 100644 src/ui/components/multi-select/multi-select.test.tsx create mode 100644 src/ui/components/multi-select/multi-select.tsx create mode 100644 src/ui/components/multi-select/utils.ts diff --git a/package-lock.json b/package-lock.json index d5e44831..346f721c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "chalk": "^5.3.0", - "codify-schemas": "^1.0.73", + "codify-schemas": "^1.0.74", "debug": "^4.3.4", "detect-indent": "^7.0.1", "diff": "^7.0.0", @@ -6066,10 +6066,19 @@ } } }, - "node_modules/codify-schemas": { + "node_modules/codify-plugin-lib/node_modules/codify-schemas": { "version": "1.0.73", "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.73.tgz", "integrity": "sha512-rokit3Ayz/B32NBZm4fsCb3/0Ju0lqhWu/K/soDV5ls3BqbuHd8Cg35D3FGgTxNqoGWLfLt5odHOuvMqS4Q/KA==", + "dev": true, + "dependencies": { + "ajv": "^8.12.0" + } + }, + "node_modules/codify-schemas": { + "version": "1.0.74", + "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.74.tgz", + "integrity": "sha512-PqcG1JhsYmIUxf2RlSkJU1W+nchBs7lbWTNqoNUUZsZWhs42ZhdbA5rqW7+c0vjTd6wJOv3LDFTk/QxewrZTEw==", "dependencies": { "ajv": "^8.12.0" } diff --git a/package.json b/package.json index 84c095a9..a5711747 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "chalk": "^5.3.0", - "codify-schemas": "^1.0.73", + "codify-schemas": "^1.0.74", "debug": "^4.3.4", "detect-indent": "^7.0.1", "diff": "^7.0.0", @@ -22,7 +22,6 @@ "ink-big-text": "^2.0.0", "ink-gradient": "^3.0.0", "ink-select-input": "^6.0.0", - "@fforres/ink-quicksearch-input": "^1.0.1", "jotai": "^2.11.1", "js-yaml": "^4.1.0", "js-yaml-source-map": "^0.2.2", diff --git a/src/commands/init.ts b/src/commands/init.ts index 9ea66b2e..77ce1ed3 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,6 +1,7 @@ import fs from 'node:fs/promises'; import { BaseCommand } from '../common/base-command.js'; +import { InitializeOrchestrator } from '../orchestrators/initialize.js'; import { ShellUtils } from '../utils/shell.js'; export default class Init extends BaseCommand { @@ -35,7 +36,7 @@ Codify will try to smartly insert new configs by following existing spacing and public async run(): Promise { const { raw, flags } = await this.parse(Init) - this.reporter.displayInitBanner() + await InitializeOrchestrator.run(this.reporter); // if (flags.path) { // this.log(`Applying Codify from: ${flags.path}`); diff --git a/src/events/context.ts b/src/events/context.ts index 6f08a484..0d83a71e 100644 --- a/src/events/context.ts +++ b/src/events/context.ts @@ -22,6 +22,7 @@ export enum ProcessName { PLAN = 'plan', DESTROY = 'destroy', IMPORT = 'import', + INIT = 'init', } export enum SubProcessName { diff --git a/src/orchestrators/destroy.ts b/src/orchestrators/destroy.ts index 380b24ee..8343a115 100644 --- a/src/orchestrators/destroy.ts +++ b/src/orchestrators/destroy.ts @@ -5,7 +5,7 @@ import { ProcessName, SubProcessName, ctx } from '../events/context.js'; import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js'; import { Reporter } from '../ui/reporters/reporter.js'; import { getTypeAndNameFromId } from '../utils/index.js'; -import { InitializeOrchestrator } from './initialize-plugins.js'; +import { PluginInitOrchestrator } from './initialize-plugins.js'; export interface DestroyArgs { ids: string[]; @@ -23,7 +23,7 @@ export class DestroyOrchestrator { ctx.processStarted(ProcessName.DESTROY) - const { typeIdsToDependenciesMap, pluginManager, project } = await InitializeOrchestrator.run({ + const { typeIdsToDependenciesMap, pluginManager, project } = await PluginInitOrchestrator.run({ ...args, allowEmptyProject: true, transformProject(project) { diff --git a/src/orchestrators/import.ts b/src/orchestrators/import.ts index 9b4a07cf..66377112 100644 --- a/src/orchestrators/import.ts +++ b/src/orchestrators/import.ts @@ -12,7 +12,7 @@ import { FileUtils } from '../utils/file.js'; import { FileModificationCalculator, ModificationType } from '../utils/file-modification-calculator.js'; import { groupBy, sleep } from '../utils/index.js'; import { wildCardMatch } from '../utils/wild-card-match.js'; -import { InitializationResult, InitializeOrchestrator } from './initialize-plugins.js'; +import { InitializationResult, PluginInitOrchestrator } from './initialize-plugins.js'; export type ImportResult = { result: ResourceConfig[], errors: string[] } @@ -47,7 +47,7 @@ export class ImportOrchestrator { const typeIds = args.typeIds?.filter(Boolean) ctx.processStarted(ProcessName.IMPORT) - const initializationResult = await InitializeOrchestrator.run( + const initializationResult = await PluginInitOrchestrator.run( { ...args, allowEmptyProject: true }, reporter ); diff --git a/src/orchestrators/initialize-plugins.ts b/src/orchestrators/initialize-plugins.ts index 67804263..406b6d9c 100644 --- a/src/orchestrators/initialize-plugins.ts +++ b/src/orchestrators/initialize-plugins.ts @@ -21,12 +21,12 @@ export interface InitializationResult { project: Project, } -export class InitializeOrchestrator { +export class PluginInitOrchestrator { static async run( args: InitializeArgs, reporter: Reporter, ): Promise { - let project = await InitializeOrchestrator.parse( + let project = await PluginInitOrchestrator.parse( args.path, args.allowEmptyProject ?? false, reporter @@ -51,7 +51,7 @@ export class InitializeOrchestrator { ctx.subprocessStarted(SubProcessName.PARSE); const pathToParse = (fileOrDir === undefined) - ? await InitializeOrchestrator.findCodifyJson() + ? await PluginInitOrchestrator.findCodifyJson() : fileOrDir if (!pathToParse && !allowEmptyProject) { diff --git a/src/orchestrators/initialize.ts b/src/orchestrators/initialize.ts index e69de29b..8084f574 100644 --- a/src/orchestrators/initialize.ts +++ b/src/orchestrators/initialize.ts @@ -0,0 +1,37 @@ +import { ProcessName, ctx } from '../events/context.js'; +import { Reporter } from '../ui/reporters/reporter.js'; +import { PluginInitOrchestrator } from './initialize-plugins.js'; + +export const InitializeOrchestrator = { + + async run(reporter: Reporter) { + await reporter.displayInitBanner() + + ctx.processStarted(ProcessName.INIT) + await reporter.displayProgress(); + + + const { pluginManager, typeIdsToDependenciesMap } = await PluginInitOrchestrator.run({}, reporter); + + const importResults = await Promise.all([...typeIdsToDependenciesMap.keys()].map(async (typeId) => { + try { + return await pluginManager.importResource({ + core: { type: typeId }, + parameters: {} + }, true); + } catch { + return null; + } + })) + + const flattenedResults = importResults.filter(Boolean).flatMap(p => p?.result).filter(Boolean) + + const userSelectedTypes = await reporter.promptInitResultSelection([...new Set(flattenedResults.map((r) => r!.core.type))]) + + ctx.processFinished(ProcessName.INIT); + + console.log(JSON.stringify(flattenedResults, null, 2)); + }, + + +}; diff --git a/src/orchestrators/plan.ts b/src/orchestrators/plan.ts index 71b5f2e7..20fec249 100644 --- a/src/orchestrators/plan.ts +++ b/src/orchestrators/plan.ts @@ -1,10 +1,10 @@ import { Plan } from '../entities/plan.js'; import { Project } from '../entities/project.js'; import { ProcessName, SubProcessName, ctx } from '../events/context.js'; -import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js'; +import { PluginManager } from '../plugins/plugin-manager.js'; import { Reporter } from '../ui/reporters/reporter.js'; import { createStartupShellScriptsIfNotExists } from '../utils/file.js'; -import { InitializeOrchestrator } from './initialize-plugins.js'; +import { PluginInitOrchestrator } from './initialize-plugins.js'; import { ValidateOrchestrator } from './validate.js'; export interface PlanArgs { @@ -22,7 +22,7 @@ export class PlanOrchestrator { static async run(args: PlanArgs, reporter: Reporter): Promise { ctx.processStarted(ProcessName.PLAN) - const initializationResult = await InitializeOrchestrator.run({ + const initializationResult = await PluginInitOrchestrator.run({ ...args, }, reporter); const { typeIdsToDependenciesMap, pluginManager, project } = initializationResult; diff --git a/src/orchestrators/validate.ts b/src/orchestrators/validate.ts index 9cddef15..0e7a2414 100644 --- a/src/orchestrators/validate.ts +++ b/src/orchestrators/validate.ts @@ -1,6 +1,6 @@ import { SubProcessName, ctx } from '../events/context.js'; import { Reporter } from '../ui/reporters/reporter.js'; -import { InitializationResult, InitializeOrchestrator } from './initialize-plugins.js'; +import { InitializationResult, PluginInitOrchestrator } from './initialize-plugins.js'; export interface ValidateArgs { existing?: InitializationResult; @@ -17,7 +17,7 @@ export const ValidateOrchestrator = { project, typeIdsToDependenciesMap: dependencyMap, pluginManager, - } = args.existing ?? await InitializeOrchestrator.run(args, reporter) + } = args.existing ?? await PluginInitOrchestrator.run(args, reporter) if (args.existing) { ctx.subprocessStarted(SubProcessName.VALIDATE) diff --git a/src/plugins/plugin-manager.ts b/src/plugins/plugin-manager.ts index ce190ca1..cb69ef79 100644 --- a/src/plugins/plugin-manager.ts +++ b/src/plugins/plugin-manager.ts @@ -94,7 +94,7 @@ export class PluginManager { } - async importResource(config: ResourceJson): Promise { + async importResource(config: ResourceJson, autoImportAll = false): Promise { const pluginName = this.resourceToPluginMapping.get(config.core.type); if (!pluginName) { throw new Error(`Unable to find plugin for resource: ${config.core.type}`); @@ -105,7 +105,7 @@ export class PluginManager { throw new Error(`Unable to find plugin for resource ${config.core.type}`); } - return plugin.import(config); + return plugin.import(config, autoImportAll); } async plan(project: Project): Promise { diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index 82ac9fa3..eee0a56a 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -1,6 +1,7 @@ import { GetResourceInfoResponseData, GetResourceInfoResponseDataSchema, + ImportRequestData, ImportResponseData, ImportResponseDataSchema, InitializeResponseData, @@ -115,8 +116,8 @@ export class Plugin implements IPlugin { } - async import(config: ResourceJson): Promise { - const result = await this.process!.sendMessageForResult('import', config); + async import(config: ResourceJson, autoSearchAll = false): Promise { + const result = await this.process!.sendMessageForResult('import', { ...config, autoSearchAll }); if (!result.isSuccessful()) { throw new Error(`Unable import resource ${config.core.type} with plugin: "${this.name}" \n\n` + result.data); diff --git a/src/ui/components/default-component.tsx b/src/ui/components/default-component.tsx index 00c44725..fdf7c074 100644 --- a/src/ui/components/default-component.tsx +++ b/src/ui/components/default-component.tsx @@ -19,6 +19,7 @@ import { ImportWarning } from './import/import-warning.js'; import { InitBanner } from './init/InitBanner.js'; import { PlanComponent } from './plan/plan.js'; import { ProgressDisplay } from './progress/progress-display.js'; +import { MultiSelect } from './multi-select/multi-select.js'; const spinnerEmitter = new EventEmitter(); @@ -133,7 +134,22 @@ export function DefaultComponent(props: { } { renderStatus === RenderStatus.DISPLAY_INIT_BANNER && ( - + + ) + } + { + renderStatus === RenderStatus.PROMPT_INIT_RESULT_SELECTION && ( + + Codify found the following supported resorces on your system. + + Select which ones to import: + ({ label: o, value: o })).sort((a, b) => a.label.localeCompare(b.label))} + onSubmit={(result: unknown[]) => emitter.emit(RenderEvent.PROMPT_RESULT, result)} + defaultSelected={(renderData as string[]).map((o) => ({ label: o, value: o }))} + /> + ) } diff --git a/src/ui/components/init/InitBanner.tsx b/src/ui/components/init/InitBanner.tsx index 5c7efd80..9c592594 100644 --- a/src/ui/components/init/InitBanner.tsx +++ b/src/ui/components/init/InitBanner.tsx @@ -1,29 +1,24 @@ import { MultiSelect } from '@inkjs/ui'; -import { Box, Text } from 'ink'; +import { Box, Static, Text } from 'ink'; import BigText from 'ink-big-text'; import Gradient from 'ink-gradient'; +import EventEmitter from 'node:events'; import React from 'react'; +import { RenderEvent } from '../../reporters/reporter.js'; -export function InitBanner() { +export function InitBanner(props: { emitter: EventEmitter }) { return - - - - Use config - Use this init flow to get setup quickly with Codify. - - Select which resources to import: - + { + () => + + + + Codify is a configuration-as-code tool that helps you setup and manage your system. + Use this init flow to get started quickly with Codify. + + Codify will scan your system for any supported programs or settings and automatically generate configs for you. + + } + { props.emitter.emit(RenderEvent.PROMPT_RESULT); }}/> } diff --git a/src/ui/components/multi-select/checkbox.tsx b/src/ui/components/multi-select/checkbox.tsx new file mode 100644 index 00000000..cf4b7e5e --- /dev/null +++ b/src/ui/components/multi-select/checkbox.tsx @@ -0,0 +1,12 @@ +import { Box, Text } from 'ink'; +import React from 'react'; + +export function CheckBox(props: { isSelected: boolean }) { + const isSelected = props.isSelected ?? false; + + return + {isSelected ? '◉' : '◯'} + +} + +export default CheckBox; diff --git a/src/ui/components/multi-select/indicator.tsx b/src/ui/components/multi-select/indicator.tsx new file mode 100644 index 00000000..9d29a1bb --- /dev/null +++ b/src/ui/components/multi-select/indicator.tsx @@ -0,0 +1,14 @@ +import { Box, Text } from 'ink'; +import React from 'react'; + +export function Indicator(props: { isHighlighted: boolean }) { + const isHighlighted = props.isHighlighted ?? false; + + return + + {isHighlighted ? '❯' : ' '} + + +} + +export default Indicator; diff --git a/src/ui/components/multi-select/item.tsx b/src/ui/components/multi-select/item.tsx new file mode 100644 index 00000000..6dcac410 --- /dev/null +++ b/src/ui/components/multi-select/item.tsx @@ -0,0 +1,10 @@ +import { Text } from 'ink'; +import React from 'react'; + +export function Item(props: { isHighlighted: boolean, label: string }) { + const isHighlighted = props.isHighlighted ?? false; + + return + {props.label} + +} diff --git a/src/ui/components/multi-select/multi-select.test.tsx b/src/ui/components/multi-select/multi-select.test.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/ui/components/multi-select/multi-select.tsx b/src/ui/components/multi-select/multi-select.tsx new file mode 100644 index 00000000..6df6675c --- /dev/null +++ b/src/ui/components/multi-select/multi-select.tsx @@ -0,0 +1,140 @@ +import { Box, useInput, useStdin } from 'ink'; +import React, { useLayoutEffect, useState } from 'react'; + +import CheckBox from './checkbox.js'; +import Indicator from './indicator.js'; +import { Item } from './item.js'; +import { arrRotate } from './utils.js'; + +interface Item { + key?: string; + value: string; + label: string; +} + +interface Props { + items: Array; + selected: Array; + defaultSelected: Array; + defaultHighlightedIndex: number; + focus: boolean; + initialIndex: number; + limit: number + onSelect?: (item: Item) => void; + onUnselect?: (item: Item) => void; + onSubmit?: (result: Item[]) => void; + onHighlight?: (item: Item) => void; +} + + +export function MultiSelect(props: Props) { + const [rotateIndex, setRotateIndex] = useState(0); + const [highlightedIndex, setHighlightedIndex] = useState(props.defaultHighlightedIndex ?? 0); + const [selected, setSelected] = useState>(props.selected ?? props.defaultSelected ?? []); + const { setRawMode } = useStdin(); + + const isSelected = (value: string) => { + const newlySelected = props.selected || selected + + return newlySelected.map(({ value }) => value).includes( + value + ); + } + + useLayoutEffect(() => { + setRawMode(true); + + return () => { setRawMode(false) } + }, []); + + useInput((input, key) => { + const { items, onHighlight, onSubmit } = props; + const newlySelected = props.selected ?? selected; + + if (key.upArrow || input === 'k') { + const lastIndex = (hasLimit() ? limit() : items.length) - 1; + const atFirstIndex = highlightedIndex === 0; + const nextIndex = (hasLimit() ? highlightedIndex : lastIndex); + const nextRotateIndex = atFirstIndex ? rotateIndex + 1 : rotateIndex; + const nextHighlightedIndex = atFirstIndex ? nextIndex : highlightedIndex - 1; + + setRotateIndex(nextRotateIndex); + setHighlightedIndex(nextHighlightedIndex); + + const slicedItems = hasLimit() ? arrRotate(items, nextRotateIndex).slice(0, limit()) : items; + onHighlight?.(slicedItems[nextHighlightedIndex]); + } + + if (key.downArrow || input === 'j') { + const atLastIndex = highlightedIndex === (hasLimit() ? limit() : items.length) - 1; + const nextIndex = (hasLimit() ? highlightedIndex : 0); + const nextRotateIndex = atLastIndex ? rotateIndex - 1 : rotateIndex; + const nextHighlightedIndex = atLastIndex ? nextIndex : highlightedIndex + 1; + + setRotateIndex(nextRotateIndex); + setHighlightedIndex(nextHighlightedIndex); + + const slicedItems = hasLimit() ? arrRotate(items, nextRotateIndex).slice(0, limit()) : items; + onHighlight?.(slicedItems[nextHighlightedIndex]); + } + + if (input === ' ') { + const slicedItems = hasLimit() ? arrRotate(items, rotateIndex).slice(0, limit()) : items; + const selectedItem = slicedItems[highlightedIndex]; + + setSelected(selectItem(selectedItem)) + } + + if (key.return) { + onSubmit?.(newlySelected); + } + }) + + const hasLimit = () => { + const { limit, items } = props; + return items.length > limit; + } + + const limit = () => { + const { limit, items } = props; + + if (hasLimit()) { + return Math.min(limit, items.length); + } + + return items.length; + } + + const selectItem = (item: Item) => { + const { onSelect, onUnselect } = props; + const newSelected = props.selected ?? selected; + + if (isSelected(item.value)) { + onUnselect?.(item); + + return newSelected.filter(({ value }) => value !== item.value); + } + + onSelect?.(item); + return [...newSelected, item]; + } + + const slicedItems = hasLimit() ? arrRotate(props.items, rotateIndex).slice(0, limit()) : props.items; + + return ( + + {slicedItems.map((item, index) => { + const key = item.key ?? item.value; + const isHighlighted = index === highlightedIndex; + + return ( + + + + + + ); + })} + + ); +} diff --git a/src/ui/components/multi-select/utils.ts b/src/ui/components/multi-select/utils.ts new file mode 100644 index 00000000..b0ca8969 --- /dev/null +++ b/src/ui/components/multi-select/utils.ts @@ -0,0 +1,8 @@ +export function arrRotate(input: Array, n: number): Array { + if (!Array.isArray(input)) { + throw new TypeError(`Expected an array, got ${typeof input}`); + } + + const x = [...input]; + return x.splice(-n % x.length).concat(x); +} diff --git a/src/ui/reporters/default-reporter.tsx b/src/ui/reporters/default-reporter.tsx index 11842d70..ae5e92b6 100644 --- a/src/ui/reporters/default-reporter.tsx +++ b/src/ui/reporters/default-reporter.tsx @@ -23,6 +23,7 @@ const ProgressLabelMapping = { [ProcessName.PLAN]: 'Codify plan', [ProcessName.DESTROY]: 'Codify destroy', [ProcessName.IMPORT]: 'Codify import', + [ProcessName.INIT]: 'Codify init', [SubProcessName.APPLYING_RESOURCE]: 'Applying resource', [SubProcessName.GENERATE_PLAN]: 'Refresh states and generating plan', [SubProcessName.INITIALIZE_PLUGINS]: 'Initializing plugins', @@ -48,8 +49,19 @@ export class DefaultReporter implements Reporter { ctx.on(Event.SUB_PROCESS_FINISH, (name, additionalName) => this.onSubprocessFinishEvent(name, additionalName)); } - displayInitBanner(): void { - this.updateRenderState(RenderStatus.DISPLAY_INIT_BANNER); + async displayInitBanner(): Promise { + await this.updateStateAndAwaitEvent( + () => this.updateRenderState(RenderStatus.DISPLAY_INIT_BANNER), + RenderEvent.PROMPT_RESULT, + ) + } + + async displayProgress(): Promise { + this.updateRenderState(RenderStatus.PROGRESS); + } + + async hideProgress(): Promise { + this.updateRenderState(RenderStatus.NOTHING); } async displayImportWarning(requiresParameters: string[], noParametersRequired: string[]): Promise { @@ -142,6 +154,13 @@ export class DefaultReporter implements Reporter { this.updateRenderState(RenderStatus.DISPLAY_MESSAGE, message); } + async promptInitResultSelection(availableTypes: string[]): Promise { + return this.updateStateAndAwaitEvent( + () => this.updateRenderState(RenderStatus.PROMPT_INIT_RESULT_SELECTION, availableTypes), + RenderEvent.PROMPT_RESULT, + ) + } + async promptConfirmation(message: string): Promise { const result = await this.updateStateAndAwaitEvent( () => this.updateRenderState(RenderStatus.PROMPT_CONFIRMATION, message), diff --git a/src/ui/reporters/reporter.ts b/src/ui/reporters/reporter.ts index 1f1db715..154b8944 100644 --- a/src/ui/reporters/reporter.ts +++ b/src/ui/reporters/reporter.ts @@ -44,7 +44,13 @@ export enum PromptType { export interface Reporter { displayPlan(plan: Plan): void - displayInitBanner(): void + displayInitBanner(): Promise + + displayProgress(): Promise; + + hideProgress(): Promise; + + promptInitResultSelection(availableTypes: string[]): Promise; promptConfirmation(message: string): Promise diff --git a/src/ui/store/index.ts b/src/ui/store/index.ts index 3bd486a4..8e935cbe 100644 --- a/src/ui/store/index.ts +++ b/src/ui/store/index.ts @@ -14,6 +14,7 @@ export enum RenderStatus { DISPLAY_PLAN, DISPLAY_IMPORT_RESULT, DISPLAY_FILE_MODIFICATION, + PROMPT_INIT_RESULT_SELECTION, IMPORT_PROMPT, IMPORT_PROMPT_WARNING, PROMPT_CONFIRMATION, diff --git a/test/orchestrator/initialize/initialize.test.ts b/test/orchestrator/initialize/initialize.test.ts index 22480db0..16457bf0 100644 --- a/test/orchestrator/initialize/initialize.test.ts +++ b/test/orchestrator/initialize/initialize.test.ts @@ -3,7 +3,7 @@ import * as fs from 'node:fs'; import os from 'node:os'; import { MockOs } from '../mocks/system'; -import { InitializeOrchestrator } from '../../../src/orchestrators/initialize-plugins'; +import { PluginInitOrchestrator } from '../../../src/orchestrators/initialize-plugins'; import path from 'node:path'; import { MockReporter } from '../mocks/reporter'; import { MockResource, MockResourceConfig } from '../mocks/resource'; @@ -64,7 +64,7 @@ describe('Parser integration tests', () => { const cwdSpy = vi.spyOn(process, 'cwd'); cwdSpy.mockReturnValue(folder); - const { project, pluginManager, typeIdsToDependenciesMap } = await InitializeOrchestrator.run({}, reporter); + const { project, pluginManager, typeIdsToDependenciesMap } = await PluginInitOrchestrator.run({}, reporter); console.log(project); expect(project).toMatchObject({ @@ -109,7 +109,7 @@ describe('Parser integration tests', () => { const cwdSpy = vi.spyOn(process, 'cwd'); cwdSpy.mockReturnValue(innerFolder); - const { project, pluginManager, typeIdsToDependenciesMap } = await InitializeOrchestrator.run({}, reporter); + const { project, pluginManager, typeIdsToDependenciesMap } = await PluginInitOrchestrator.run({}, reporter); console.log(project); expect(project).toMatchObject({ From 0165234a290021b12755755feed18fd42c55b60d Mon Sep 17 00:00:00 2001 From: kevinwang Date: Wed, 5 Mar 2025 10:34:58 -0500 Subject: [PATCH 03/17] chore: updated tsx file names to capital --- src/ui/components/default-component.tsx | 2 +- .../multi-select/{checkbox.tsx => Checkbox.tsx} | 4 ++-- .../multi-select/{indicator.tsx => Indicator.tsx} | 0 src/ui/components/multi-select/{item.tsx => Item.tsx} | 0 .../{multi-select.test.tsx => MultiSelect.test.tsx} | 0 .../multi-select/{multi-select.tsx => MultiSelect.tsx} | 8 ++++---- 6 files changed, 7 insertions(+), 7 deletions(-) rename src/ui/components/multi-select/{checkbox.tsx => Checkbox.tsx} (71%) rename src/ui/components/multi-select/{indicator.tsx => Indicator.tsx} (100%) rename src/ui/components/multi-select/{item.tsx => Item.tsx} (100%) rename src/ui/components/multi-select/{multi-select.test.tsx => MultiSelect.test.tsx} (100%) rename src/ui/components/multi-select/{multi-select.tsx => MultiSelect.tsx} (95%) diff --git a/src/ui/components/default-component.tsx b/src/ui/components/default-component.tsx index fdf7c074..324796f0 100644 --- a/src/ui/components/default-component.tsx +++ b/src/ui/components/default-component.tsx @@ -19,7 +19,7 @@ import { ImportWarning } from './import/import-warning.js'; import { InitBanner } from './init/InitBanner.js'; import { PlanComponent } from './plan/plan.js'; import { ProgressDisplay } from './progress/progress-display.js'; -import { MultiSelect } from './multi-select/multi-select.js'; +import { MultiSelect } from './multi-select/MultiSelect.js'; const spinnerEmitter = new EventEmitter(); diff --git a/src/ui/components/multi-select/checkbox.tsx b/src/ui/components/multi-select/Checkbox.tsx similarity index 71% rename from src/ui/components/multi-select/checkbox.tsx rename to src/ui/components/multi-select/Checkbox.tsx index cf4b7e5e..494e5dba 100644 --- a/src/ui/components/multi-select/checkbox.tsx +++ b/src/ui/components/multi-select/Checkbox.tsx @@ -1,7 +1,7 @@ import { Box, Text } from 'ink'; import React from 'react'; -export function CheckBox(props: { isSelected: boolean }) { +export function Checkbox(props: { isSelected: boolean }) { const isSelected = props.isSelected ?? false; return @@ -9,4 +9,4 @@ export function CheckBox(props: { isSelected: boolean }) { } -export default CheckBox; +export default Checkbox; diff --git a/src/ui/components/multi-select/indicator.tsx b/src/ui/components/multi-select/Indicator.tsx similarity index 100% rename from src/ui/components/multi-select/indicator.tsx rename to src/ui/components/multi-select/Indicator.tsx diff --git a/src/ui/components/multi-select/item.tsx b/src/ui/components/multi-select/Item.tsx similarity index 100% rename from src/ui/components/multi-select/item.tsx rename to src/ui/components/multi-select/Item.tsx diff --git a/src/ui/components/multi-select/multi-select.test.tsx b/src/ui/components/multi-select/MultiSelect.test.tsx similarity index 100% rename from src/ui/components/multi-select/multi-select.test.tsx rename to src/ui/components/multi-select/MultiSelect.test.tsx diff --git a/src/ui/components/multi-select/multi-select.tsx b/src/ui/components/multi-select/MultiSelect.tsx similarity index 95% rename from src/ui/components/multi-select/multi-select.tsx rename to src/ui/components/multi-select/MultiSelect.tsx index 6df6675c..39b9d80b 100644 --- a/src/ui/components/multi-select/multi-select.tsx +++ b/src/ui/components/multi-select/MultiSelect.tsx @@ -1,9 +1,9 @@ import { Box, useInput, useStdin } from 'ink'; import React, { useLayoutEffect, useState } from 'react'; -import CheckBox from './checkbox.js'; -import Indicator from './indicator.js'; -import { Item } from './item.js'; +import Checkbox from './Checkbox.js'; +import Indicator from './Indicator.js'; +import { Item } from './Item.js'; import { arrRotate } from './utils.js'; interface Item { @@ -130,7 +130,7 @@ export function MultiSelect(props: Props) { return ( - + ); From 5656e4ffc960e22e3355878ddd128b8063749d03 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Fri, 7 Mar 2025 14:40:15 -0500 Subject: [PATCH 04/17] feature: Added improvements to multi-select and completed the init flow --- src/orchestrators/initialize.ts | 60 ++++++++++++++++++- src/ui/components/default-component.test.tsx | 39 ++++++++---- src/ui/components/default-component.tsx | 19 ++++-- .../multi-select/MultiSelect.test.tsx | 21 +++++++ .../components/multi-select/MultiSelect.tsx | 57 +++++++++++------- src/ui/reporters/default-reporter.tsx | 9 ++- src/ui/reporters/reporter.ts | 4 +- src/ui/store/index.ts | 1 + src/utils/index.ts | 12 ++++ 9 files changed, 178 insertions(+), 44 deletions(-) diff --git a/src/orchestrators/initialize.ts b/src/orchestrators/initialize.ts index 8084f574..cb8d409a 100644 --- a/src/orchestrators/initialize.ts +++ b/src/orchestrators/initialize.ts @@ -1,5 +1,11 @@ -import { ProcessName, ctx } from '../events/context.js'; +import chalk from 'chalk'; +import path from 'node:path'; + +import { ResourceConfig } from '../entities/resource-config.js'; +import { ProcessName, SubProcessName, ctx } from '../events/context.js'; import { Reporter } from '../ui/reporters/reporter.js'; +import { FileUtils } from '../utils/file.js'; +import { resolvePathWithVariables, untildify } from '../utils/index.js'; import { PluginInitOrchestrator } from './initialize-plugins.js'; export const InitializeOrchestrator = { @@ -13,6 +19,7 @@ export const InitializeOrchestrator = { const { pluginManager, typeIdsToDependenciesMap } = await PluginInitOrchestrator.run({}, reporter); + ctx.subprocessStarted(SubProcessName.IMPORT_RESOURCE) const importResults = await Promise.all([...typeIdsToDependenciesMap.keys()].map(async (typeId) => { try { return await pluginManager.importResource({ @@ -23,15 +30,62 @@ export const InitializeOrchestrator = { return null; } })) + ctx.subprocessFinished(SubProcessName.IMPORT_RESOURCE) const flattenedResults = importResults.filter(Boolean).flatMap(p => p?.result).filter(Boolean) const userSelectedTypes = await reporter.promptInitResultSelection([...new Set(flattenedResults.map((r) => r!.core.type))]) + ctx.log('Resource types were chosen to be imported.') - ctx.processFinished(ProcessName.INIT); + const locationToSave = await this.promptSaveLocation(reporter); + ctx.log(`Save results to ${locationToSave}`) + await reporter.hide(); + + const resourcesRaw = flattenedResults.filter((r) => userSelectedTypes.includes(r.core.type)) + .map((r) => ResourceConfig.fromJson(r!)) + .map((r) => r.raw); + + await FileUtils.writeFile(locationToSave, JSON.stringify(resourcesRaw, null, 2)); + ctx.log('File successfully saved'); - console.log(JSON.stringify(flattenedResults, null, 2)); + await reporter.displayMessage(` +🎉🎉 Codify successfully initialized. 🎉🎉 +The imported configs were written to: ${locationToSave} + +Use ${chalk.bgMagenta.bold(' codify plan ')} to compute changes and ${chalk.bgMagenta.bold(' codify apply ')} to apply them. +For more information visit: https://docs.codifycli.com. + +Enjoy! + `) + + ctx.processFinished(ProcessName.INIT); }, + async promptSaveLocation(reporter: Reporter): Promise { + let locationToSave = ''; + let input = ''; + let isValidSaveLocation = false; + let error = false; + + while (!isValidSaveLocation) { + input = (await reporter.promptInput( + `Where to save the new Codify configs? ${chalk.grey.dim('(leave blank for ~/codify.json)')}`, + error ? `Invalid location: ${input} already exists` : undefined) + ) + input = input ? input : '~/codify.json'; + + locationToSave = path.resolve(untildify(resolvePathWithVariables(input))); + + try { + isValidSaveLocation = !(await FileUtils.fileExists(locationToSave)); + error = !isValidSaveLocation; + } catch { + isValidSaveLocation = false; + error = true; + } + } + + return locationToSave; + } }; diff --git a/src/ui/components/default-component.test.tsx b/src/ui/components/default-component.test.tsx index 48eb061b..fd12dbd8 100644 --- a/src/ui/components/default-component.test.tsx +++ b/src/ui/components/default-component.test.tsx @@ -1,24 +1,26 @@ +import chalk from 'chalk'; import { cleanup, render } from 'ink-testing-library'; import { EventEmitter } from 'node:events'; import React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { DefaultReporter } from '../reporters/default-reporter.js'; import { RenderStatus, store } from '../store/index.js'; import { DefaultComponent } from './default-component.js'; // Mock dependent components -vi.mock('./progress/progress-display', () => ({ - ProgressDisplay: () =>
Mock Progress Display
-})); -vi.mock('./import/index', () => ({ - ImportParametersForm: () =>
Mock Import Parameters Form
-})); -vi.mock('./plan/plan', () => ({ - PlanComponent: () =>
Mock Plan Component
-})); -vi.mock('./import/import-result', () => ({ - ImportResultComponent: () =>
Mock Import Result Component
-})); +// vi.mock('./progress/progress-display', () => ({ +// ProgressDisplay: () =>
Mock Progress Display
+// })); +// vi.mock('./import/index', () => ({ +// ImportParametersForm: () =>
Mock Import Parameters Form
+// })); +// vi.mock('./plan/plan', () => ({ +// PlanComponent: () =>
Mock Plan Component
+// })); +// vi.mock('./import/import-result', () => ({ +// ImportResultComponent: () =>
Mock Import Result Component
+// })); describe('DefaultComponent', () => { let emitter: EventEmitter; @@ -33,6 +35,19 @@ describe('DefaultComponent', () => { emitter.removeAllListeners(); }); + it('Renders the init completed message', () => { + const reporter = new DefaultReporter(); + const locationToSave = '~/codify.json' + + reporter.displayMessage(` +🎉🎉 Codify successfully initialized. 🎉🎉 +The imported configs were written to: ${locationToSave} + +Use ${chalk.bgHex('#F0EAD6').bold(' codify plan ')} to futures compute changes and ${chalk.bgHex('#F0EAD6').bold(' codify apply ')} to apply them. +Visit the documentation for more info: https://docs.codifycli.com. + `) + }) + it('renders progress display when renderStatus is PROGRESS', () => { // TODO: Doesn't work on github actions for some reason. Will investigate later 02-13-2025 // store.set(store.renderState, { status: RenderStatus.PROGRESS }); diff --git a/src/ui/components/default-component.tsx b/src/ui/components/default-component.tsx index 324796f0..9c3ee70c 100644 --- a/src/ui/components/default-component.tsx +++ b/src/ui/components/default-component.tsx @@ -1,5 +1,5 @@ import { Form, FormProps } from '@codifycli/ink-form'; -import { PasswordInput } from '@inkjs/ui'; +import { PasswordInput, TextInput } from '@inkjs/ui'; import chalk from 'chalk'; import { Box, Static, Text } from 'ink'; import SelectInput from 'ink-select-input'; @@ -142,15 +142,24 @@ export function DefaultComponent(props: { Codify found the following supported resorces on your system. - Select which ones to import: + Select the resources to import: ({ label: o, value: o })).sort((a, b) => a.label.localeCompare(b.label))} - onSubmit={(result: unknown[]) => emitter.emit(RenderEvent.PROMPT_RESULT, result)} defaultSelected={(renderData as string[]).map((o) => ({ label: o, value: o }))} + items={(renderData as string[]).map((o) => ({ label: o, value: o })).sort((a, b) => a.label.localeCompare(b.label))} + limit={9} + onSubmit={(result: unknown[]) => emitter.emit(RenderEvent.PROMPT_RESULT, result.map((r: any) => r?.label))} /> ) } + { + renderStatus === RenderStatus.PROMPT_INPUT && ( + + {renderData.prompt} + { renderData.error && ({renderData.error}) } + emitter.emit(RenderEvent.PROMPT_RESULT, result)} /> + + ) + } } diff --git a/src/ui/components/multi-select/MultiSelect.test.tsx b/src/ui/components/multi-select/MultiSelect.test.tsx index e69de29b..0951a41b 100644 --- a/src/ui/components/multi-select/MultiSelect.test.tsx +++ b/src/ui/components/multi-select/MultiSelect.test.tsx @@ -0,0 +1,21 @@ +import { render } from 'ink'; +import React from 'react'; +import { describe } from 'vitest'; + +import { MultiSelect } from './MultiSelect.js'; + +render( ) diff --git a/src/ui/components/multi-select/MultiSelect.tsx b/src/ui/components/multi-select/MultiSelect.tsx index 39b9d80b..5e450834 100644 --- a/src/ui/components/multi-select/MultiSelect.tsx +++ b/src/ui/components/multi-select/MultiSelect.tsx @@ -1,4 +1,4 @@ -import { Box, useInput, useStdin } from 'ink'; +import { Box, Text, useInput, useStdin } from 'ink'; import React, { useLayoutEffect, useState } from 'react'; import Checkbox from './Checkbox.js'; @@ -14,12 +14,12 @@ interface Item { interface Props { items: Array; - selected: Array; - defaultSelected: Array; - defaultHighlightedIndex: number; - focus: boolean; - initialIndex: number; - limit: number + selected?: Array; + defaultSelected?: Array; + defaultHighlightedIndex?: number; + focus?: boolean; + initialIndex?: number; + limit?: number onSelect?: (item: Item) => void; onUnselect?: (item: Item) => void; onSubmit?: (result: Item[]) => void; @@ -88,18 +88,26 @@ export function MultiSelect(props: Props) { if (key.return) { onSubmit?.(newlySelected); } + + if (input === 'a') { + setSelected(items); + } + + if (input === 'd') { + setSelected([]); + } }) const hasLimit = () => { const { limit, items } = props; - return items.length > limit; + return items.length > (limit ?? items.length); } const limit = () => { const { limit, items } = props; if (hasLimit()) { - return Math.min(limit, items.length); + return Math.min(limit ?? items.length, items.length); } return items.length; @@ -122,19 +130,24 @@ export function MultiSelect(props: Props) { const slicedItems = hasLimit() ? arrRotate(props.items, rotateIndex).slice(0, limit()) : props.items; return ( - - {slicedItems.map((item, index) => { - const key = item.key ?? item.value; - const isHighlighted = index === highlightedIndex; - - return ( - - - - - - ); - })} + + + {slicedItems.map((item, index) => { + const key = item.key ?? item.value; + const isHighlighted = index === highlightedIndex; + + return ( + + + + + + ); + })} + + {'Use to select and to submit.'} + {'Use to select all items and to de-select all items.'} + ); } diff --git a/src/ui/reporters/default-reporter.tsx b/src/ui/reporters/default-reporter.tsx index ae5e92b6..aedb7cd7 100644 --- a/src/ui/reporters/default-reporter.tsx +++ b/src/ui/reporters/default-reporter.tsx @@ -56,11 +56,18 @@ export class DefaultReporter implements Reporter { ) } + async promptInput(prompt: string, error?: string, validation?: () => Promise, autoComplete?: (input: string) => string[]): Promise { + return this.updateStateAndAwaitEvent( + () => this.updateRenderState(RenderStatus.PROMPT_INPUT, { prompt, error }), + RenderEvent.PROMPT_RESULT, + ) + } + async displayProgress(): Promise { this.updateRenderState(RenderStatus.PROGRESS); } - async hideProgress(): Promise { + async hide(): Promise { this.updateRenderState(RenderStatus.NOTHING); } diff --git a/src/ui/reporters/reporter.ts b/src/ui/reporters/reporter.ts index 154b8944..3ba82086 100644 --- a/src/ui/reporters/reporter.ts +++ b/src/ui/reporters/reporter.ts @@ -48,10 +48,12 @@ export interface Reporter { displayProgress(): Promise; - hideProgress(): Promise; + hide(): Promise; promptInitResultSelection(availableTypes: string[]): Promise; + promptInput(prompt: string, error?: string, validation?: () => Promise, autoComplete?: (input: string) => string[]): Promise; + promptConfirmation(message: string): Promise promptOptions(message: string, options: string[]): Promise; diff --git a/src/ui/store/index.ts b/src/ui/store/index.ts index 8e935cbe..8fe5a355 100644 --- a/src/ui/store/index.ts +++ b/src/ui/store/index.ts @@ -19,6 +19,7 @@ export enum RenderStatus { IMPORT_PROMPT_WARNING, PROMPT_CONFIRMATION, PROMPT_OPTIONS, + PROMPT_INPUT, SUDO_PROMPT, DISPLAY_MESSAGE, } diff --git a/src/utils/index.ts b/src/utils/index.ts index f4753fc4..2122931e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ +import os from 'node:os'; + export function groupBy(arr: T[], grouper: (item: T) => string): Record { // eslint-disable-next-line unicorn/no-array-reduce return arr.reduce((result, curr) => { @@ -59,3 +61,13 @@ export function deepEqual(obj1: unknown, obj2: unknown): boolean { // If all checks pass, the objects are deep equal. return true; } + +export function resolvePathWithVariables(pathWithVariables: string) { + // @ts-expect-error Ignore this for now + return pathWithVariables.replace(/\$([A-Z_]+[A-Z0-9_]*)|\${([A-Z0-9_]*)}/ig, (_, a, b) => process.env[a || b]) +} + +export function untildify(pathWithTilde: string) { + const homeDirectory = os.homedir(); + return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde; +} From 6e7aba17ddb94eec82e635a64abbc9d81b371d5e Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 8 Mar 2025 12:18:30 -0500 Subject: [PATCH 05/17] feat: Add json reporter --- new.codify.json | 1 - src/commands/plan/index.ts | 1 - src/common/base-command.ts | 4 +- src/ui/components/init/InitBanner.tsx | 7 ++-- src/ui/reporters/json-reporter.ts | 54 +++++++++++++++++++++++++++ src/ui/reporters/reporter.ts | 3 +- 6 files changed, 61 insertions(+), 9 deletions(-) delete mode 100644 new.codify.json create mode 100644 src/ui/reporters/json-reporter.ts diff --git a/new.codify.json b/new.codify.json deleted file mode 100644 index fe51488c..00000000 --- a/new.codify.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/src/commands/plan/index.ts b/src/commands/plan/index.ts index 76528bfb..a5588645 100644 --- a/src/commands/plan/index.ts +++ b/src/commands/plan/index.ts @@ -13,7 +13,6 @@ export default class Plan extends BaseCommand { ] async init(): Promise { - console.log('Running Codify plan...') return super.init(); } diff --git a/src/common/base-command.ts b/src/common/base-command.ts index 72b9ae13..63bf4191 100644 --- a/src/common/base-command.ts +++ b/src/common/base-command.ts @@ -9,14 +9,12 @@ import { Reporter, ReporterFactory, ReporterType } from '../ui/reporters/reporte import { prettyPrintError } from './errors.js'; export abstract class BaseCommand extends Command { - static enableJsonFlag = true; - static baseFlags = { 'debug': Flags.boolean(), 'output': Flags.option({ char: 'o', default: 'default', - options: ['plain', 'default', 'debug', 'json'], + options: ['plain', 'default', 'json'], })(), 'secure': Flags.boolean({ char: 's', diff --git a/src/ui/components/init/InitBanner.tsx b/src/ui/components/init/InitBanner.tsx index 9c592594..2ce30022 100644 --- a/src/ui/components/init/InitBanner.tsx +++ b/src/ui/components/init/InitBanner.tsx @@ -1,9 +1,10 @@ -import { MultiSelect } from '@inkjs/ui'; +import { Select } from '@inkjs/ui'; import { Box, Static, Text } from 'ink'; import BigText from 'ink-big-text'; import Gradient from 'ink-gradient'; import EventEmitter from 'node:events'; import React from 'react'; + import { RenderEvent } from '../../reporters/reporter.js'; export function InitBanner(props: { emitter: EventEmitter }) { @@ -11,7 +12,7 @@ export function InitBanner(props: { emitter: EventEmitter }) { { () => - + Codify is a configuration-as-code tool that helps you setup and manage your system. Use this init flow to get started quickly with Codify. @@ -19,6 +20,6 @@ export function InitBanner(props: { emitter: EventEmitter }) { Codify will scan your system for any supported programs or settings and automatically generate configs for you. } - { props.emitter.emit(RenderEvent.PROMPT_RESULT); }}/> +