mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 14:15:56 +00:00
Merge pull request #35 from kaitranntt/feat/cli-ui-ux-enhancement
Merge feat/cli-ui-ux-enhancement into beta
This commit is contained in:
21 files changed
+2535
-1167
No files matched your search
+3
-1
@@ -29,4 +29,6 @@ dist/
|
||||
|
||||
# Package lock files (keep package-lock.json but ignore others)
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
pnpm-lock.yaml
|
||||
|
||||
.claude/active-plan
|
||||
@@ -4,7 +4,11 @@
|
||||
"": {
|
||||
"name": "@kaitranntt/ccs",
|
||||
"dependencies": {
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "^5.6.2",
|
||||
"cli-table3": "^0.6.5",
|
||||
"gradient-string": "^3.0.0",
|
||||
"listr2": "^9.0.5",
|
||||
"ora": "^9.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -164,6 +168,8 @@
|
||||
|
||||
"@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="],
|
||||
|
||||
"@types/tinycolor2": ["@types/tinycolor2@1.4.6", "", {}, "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.48.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/type-utils": "8.48.0", "@typescript-eslint/utils": "8.48.0", "@typescript-eslint/visitor-keys": "8.48.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.48.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.48.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", "@typescript-eslint/typescript-estree": "8.48.0", "@typescript-eslint/visitor-keys": "8.48.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ=="],
|
||||
@@ -196,6 +202,8 @@
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
@@ -216,6 +224,8 @@
|
||||
|
||||
"bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
|
||||
|
||||
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
@@ -224,9 +234,9 @@
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
||||
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="],
|
||||
|
||||
@@ -234,6 +244,8 @@
|
||||
|
||||
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||
|
||||
"cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="],
|
||||
@@ -242,12 +254,16 @@
|
||||
|
||||
"cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="],
|
||||
|
||||
"cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
||||
|
||||
"compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
@@ -296,7 +312,7 @@
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"emojilib": ["emojilib@2.4.0", "", {}, "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw=="],
|
||||
|
||||
@@ -330,6 +346,8 @@
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
|
||||
|
||||
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
|
||||
@@ -390,6 +408,8 @@
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"gradient-string": ["gradient-string@3.0.0", "", { "dependencies": { "chalk": "^5.3.0", "tinygradient": "^1.1.5" } }, "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg=="],
|
||||
|
||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||
|
||||
"handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="],
|
||||
@@ -492,6 +512,8 @@
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="],
|
||||
|
||||
"load-json-file": ["load-json-file@4.0.0", "", { "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^4.0.0", "pify": "^3.0.0", "strip-bom": "^3.0.0" } }, "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
@@ -528,6 +550,8 @@
|
||||
|
||||
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
|
||||
|
||||
"log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
|
||||
|
||||
"make-asynchronous": ["make-asynchronous@1.0.1", "", { "dependencies": { "p-event": "^6.0.0", "type-fest": "^4.6.0", "web-worker": "1.2.0" } }, "sha512-T9BPOmEOhp6SmV25SwLVcHK4E6JyG/coH3C6F1NjNXSziv/fd4GmsqMk8YR6qpPOswfaOCApSNkZv6fxoaYFcQ=="],
|
||||
@@ -666,6 +690,8 @@
|
||||
|
||||
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"semantic-release": ["semantic-release@25.0.2", "", { "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", "@semantic-release/github": "^12.0.0", "@semantic-release/npm": "^13.1.1", "@semantic-release/release-notes-generator": "^14.1.0", "aggregate-error": "^5.0.0", "cosmiconfig": "^9.0.0", "debug": "^4.0.0", "env-ci": "^11.0.0", "execa": "^9.0.0", "figures": "^6.0.0", "find-versions": "^6.0.0", "get-stream": "^6.0.0", "git-log-parser": "^1.2.0", "hook-std": "^4.0.0", "hosted-git-info": "^9.0.0", "import-from-esm": "^2.0.0", "lodash-es": "^4.17.21", "marked": "^15.0.0", "marked-terminal": "^7.3.0", "micromatch": "^4.0.2", "p-each-series": "^3.0.0", "p-reduce": "^3.0.0", "read-package-up": "^12.0.0", "resolve-from": "^5.0.0", "semver": "^7.3.2", "semver-diff": "^5.0.0", "signale": "^1.2.1", "yargs": "^18.0.0" }, "bin": { "semantic-release": "bin/semantic-release.js" } }, "sha512-6qGjWccl5yoyugHt3jTgztJ9Y0JVzyH8/Voc/D8PlLat9pwxQYXz7W1Dpnq5h0/G5GCYGUaDSlYcyk3AMh5A6g=="],
|
||||
@@ -688,6 +714,8 @@
|
||||
|
||||
"skin-tone": ["skin-tone@2.0.0", "", { "dependencies": { "unicode-emoji-modifier-base": "^1.0.0" } }, "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA=="],
|
||||
|
||||
"slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"spawn-error-forwarder": ["spawn-error-forwarder@1.0.0", "", {}, "sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g=="],
|
||||
@@ -706,7 +734,7 @@
|
||||
|
||||
"stream-combiner2": ["stream-combiner2@1.1.1", "", { "dependencies": { "duplexer2": "~0.1.0", "readable-stream": "^2.0.2" } }, "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
@@ -746,10 +774,14 @@
|
||||
|
||||
"time-span": ["time-span@5.1.0", "", { "dependencies": { "convert-hrtime": "^5.0.0" } }, "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA=="],
|
||||
|
||||
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"traverse": ["traverse@0.6.8", "", {}, "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA=="],
|
||||
@@ -760,7 +792,7 @@
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@5.2.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-xxCJm+Bckc6kQBknN7i9fnP/xobQRsRQxR01CztFkp/h++yfVxUUcmMgfR2HttJx/dpWjS9ubVuyspJv24Q9DA=="],
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typescript": ["typescript@5.3.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw=="],
|
||||
|
||||
@@ -792,13 +824,15 @@
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
|
||||
|
||||
"workerpool": ["workerpool@9.3.4", "", {}, "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
@@ -820,18 +854,12 @@
|
||||
|
||||
"@commitlint/config-validator/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@commitlint/format/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"@commitlint/load/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"@commitlint/parse/conventional-changelog-angular": ["conventional-changelog-angular@7.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ=="],
|
||||
|
||||
"@commitlint/parse/conventional-commits-parser": ["conventional-commits-parser@5.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^2.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "conventional-commits-parser": "cli.mjs" } }, "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA=="],
|
||||
|
||||
"@commitlint/top-level/find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="],
|
||||
|
||||
"@commitlint/types/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@@ -858,12 +886,22 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"cli-highlight/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
||||
|
||||
"cli-table3/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
|
||||
|
||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"conventional-changelog-writer/meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="],
|
||||
|
||||
"conventional-commits-parser/meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="],
|
||||
@@ -872,6 +910,8 @@
|
||||
|
||||
"env-ci/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
|
||||
|
||||
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"fdir/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
@@ -888,12 +928,10 @@
|
||||
|
||||
"load-json-file/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="],
|
||||
|
||||
"log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"log-symbols/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
|
||||
|
||||
"make-asynchronous/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"marked-terminal/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"mocha/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"npm/@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
||||
@@ -1210,8 +1248,6 @@
|
||||
|
||||
"npm/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"ora/log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
|
||||
|
||||
"ora/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
|
||||
@@ -1224,8 +1260,12 @@
|
||||
|
||||
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||
|
||||
"read-package-up/type-fest": ["type-fest@5.2.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-xxCJm+Bckc6kQBknN7i9fnP/xobQRsRQxR01CztFkp/h++yfVxUUcmMgfR2HttJx/dpWjS9ubVuyspJv24Q9DA=="],
|
||||
|
||||
"read-pkg/parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="],
|
||||
|
||||
"read-pkg/type-fest": ["type-fest@5.2.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-xxCJm+Bckc6kQBknN7i9fnP/xobQRsRQxR01CztFkp/h++yfVxUUcmMgfR2HttJx/dpWjS9ubVuyspJv24Q9DA=="],
|
||||
|
||||
"readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
@@ -1246,7 +1286,11 @@
|
||||
|
||||
"signale/figures": ["figures@2.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA=="],
|
||||
|
||||
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
"slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
@@ -1262,10 +1306,16 @@
|
||||
|
||||
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"yargs-unparser/camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
||||
|
||||
"@commitlint/config-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@commitlint/parse/conventional-commits-parser/split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
@@ -1304,14 +1354,26 @@
|
||||
|
||||
"@semantic-release/release-notes-generator/read-package-up/read-pkg": ["read-pkg@9.0.1", "", { "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", "parse-json": "^8.0.0", "type-fest": "^4.6.0", "unicorn-magic": "^0.1.0" } }, "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA=="],
|
||||
|
||||
"@semantic-release/release-notes-generator/read-package-up/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="],
|
||||
|
||||
"cli-highlight/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
||||
|
||||
"cli-table3/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cli-table3/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"env-ci/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
|
||||
@@ -1328,8 +1390,12 @@
|
||||
|
||||
"env-ci/execa/strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
||||
|
||||
"eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"mocha/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"npm/@sigstore/sign/proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="],
|
||||
@@ -1370,8 +1436,6 @@
|
||||
|
||||
"semantic-release/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
|
||||
|
||||
"semantic-release/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"semantic-release/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||
|
||||
"signale/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
@@ -1384,11 +1448,13 @@
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@commitlint/top-level/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="],
|
||||
|
||||
@@ -1404,8 +1470,18 @@
|
||||
|
||||
"@semantic-release/release-notes-generator/read-package-up/read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"cli-highlight/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"cli-highlight/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cli-highlight/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"cli-table3/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"env-ci/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"env-ci/execa/onetime/mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
|
||||
@@ -1424,23 +1500,21 @@
|
||||
|
||||
"semantic-release/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"semantic-release/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"semantic-release/yargs/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"signale/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"signale/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||
|
||||
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"@commitlint/top-level/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="],
|
||||
|
||||
"@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="],
|
||||
|
||||
"cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"pkg-conf/find-up/locate-path/p-locate/p-limit": ["p-limit@1.3.0", "", { "dependencies": { "p-try": "^1.0.0" } }, "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q=="],
|
||||
"cli-highlight/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"semantic-release/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
"pkg-conf/find-up/locate-path/p-locate/p-limit": ["p-limit@1.3.0", "", { "dependencies": { "p-try": "^1.0.0" } }, "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q=="],
|
||||
|
||||
"signale/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
|
||||
|
||||
+217
-19
@@ -435,7 +435,142 @@ for (const [flag, handler] of Object.entries(commandHandlers)) {
|
||||
}
|
||||
```
|
||||
|
||||
## Delegation System Patterns (v4.0+)
|
||||
## UI System Patterns (Phase 5, v4.5.0+)
|
||||
|
||||
### Central UI Module (src/utils/ui.ts)
|
||||
|
||||
#### Initialization Pattern
|
||||
```typescript
|
||||
import { ui } from '../utils/ui';
|
||||
|
||||
// Initialize UI (call once at startup)
|
||||
async function main() {
|
||||
await ui.init();
|
||||
// UI functions now available
|
||||
}
|
||||
```
|
||||
|
||||
#### Semantic Color Usage
|
||||
```typescript
|
||||
// Apply semantic colors
|
||||
ui.color('Success!', 'success') // Green bold
|
||||
ui.color('Error!', 'error') // Red bold
|
||||
ui.color('Warning!', 'warning') // Yellow
|
||||
ui.color('Info', 'info') // Cyan
|
||||
ui.color('Command: ccs', 'command') // Yellow italic
|
||||
ui.color('/path/to/file', 'path') // Cyan underline
|
||||
```
|
||||
|
||||
#### ASCII Status Indicators (NO EMOJIS)
|
||||
```typescript
|
||||
// Preferred: ASCII indicators only
|
||||
ui.ok('Installation complete') // [OK] Installation complete
|
||||
ui.fail('Build failed') // [X] Build failed
|
||||
ui.warn('Deprecated feature') // [!] Deprecated feature
|
||||
ui.info('Checking system') // [i] Checking system
|
||||
|
||||
// NEVER use emoji (violates CLAUDE.md)
|
||||
// DON'T: console.log('✓ Success') or '❌ Error'
|
||||
```
|
||||
|
||||
#### Box & Table Rendering
|
||||
```typescript
|
||||
// Styled boxes
|
||||
ui.box('Content here', {
|
||||
title: 'Header',
|
||||
borderColor: '#00ECFA',
|
||||
padding: 1
|
||||
});
|
||||
|
||||
ui.errorBox('Error message', 'ERROR');
|
||||
ui.infoBox('Info content', 'INFO');
|
||||
|
||||
// Styled tables
|
||||
ui.table([
|
||||
['Column 1', 'Column 2'],
|
||||
['Data 1', 'Data 2']
|
||||
], {
|
||||
head: ['Header 1', 'Header 2'],
|
||||
style: 'unicode'
|
||||
});
|
||||
```
|
||||
|
||||
#### Spinner & Progress
|
||||
```typescript
|
||||
// Simple spinner (falls back to plain text in non-TTY)
|
||||
const spin = await ui.spinner('Processing...');
|
||||
await doWork();
|
||||
spin.succeed('Done!');
|
||||
|
||||
// Spinner with message update
|
||||
const spin = await ui.spinner('Loading');
|
||||
spin.update('Still loading...');
|
||||
spin.succeed('Completed');
|
||||
```
|
||||
|
||||
#### Task Lists (Listr2 Integration - Phase 5)
|
||||
```typescript
|
||||
// Create task list with intelligent renderer selection
|
||||
const ctx = await ui.taskList([
|
||||
{
|
||||
title: 'Install dependencies',
|
||||
task: async () => {
|
||||
await installDeps();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Build project',
|
||||
task: async () => {
|
||||
await build();
|
||||
},
|
||||
skip: () => !needsBuild() ? 'Already built' : undefined
|
||||
}
|
||||
], {
|
||||
concurrent: false // Set true for parallel execution
|
||||
});
|
||||
|
||||
// Renderer selection (automatic):
|
||||
// - TTY: Default renderer with subtask hierarchy
|
||||
// - Non-TTY: Simple renderer (plain text)
|
||||
// - CI environment: Simple renderer
|
||||
// - Claude Code context: Fallback spinner-based
|
||||
```
|
||||
|
||||
#### Context Detection
|
||||
```typescript
|
||||
// Check if running in interactive TTY
|
||||
if (ui.isInteractive()) {
|
||||
// Can use full spinner/box features
|
||||
}
|
||||
|
||||
// Check if running inside Claude Code tool
|
||||
if (ui.isClaudeCodeContext()) {
|
||||
// May need fallback rendering
|
||||
}
|
||||
```
|
||||
|
||||
### UI Compliance Rules (STRICT)
|
||||
|
||||
**Mandatory**:
|
||||
- ALWAYS call `await ui.init()` before using any UI functions
|
||||
- ASCII status indicators ONLY: [OK], [X], [!], [i] - NO EMOJIS
|
||||
- Respect NO_COLOR and FORCE_COLOR environment variables
|
||||
- Use semantic colors (success, error, warning, info, dim, primary, secondary, command, path)
|
||||
|
||||
**Patterns**:
|
||||
- Status messages: `ui.ok()`, `ui.fail()`, `ui.warn()`, `ui.info()`
|
||||
- Styled output: `ui.box()`, `ui.table()`, `ui.spinner()`
|
||||
- Progress: Use `ui.taskList()` for multi-step operations (Phase 5)
|
||||
- Fallback-first: Always test in non-TTY (pipe) mode
|
||||
|
||||
**Testing**:
|
||||
```bash
|
||||
# Test in non-TTY (pipes/CI)
|
||||
ccs doctor | cat # Forces plain text output
|
||||
NO_COLOR=1 ccs doctor # Disables colors
|
||||
```
|
||||
|
||||
## Delegation System Patterns (v4.0+, Phase 5 Enhanced)
|
||||
|
||||
### Stream-JSON Parsing
|
||||
|
||||
@@ -532,12 +667,76 @@ function executeHeadless(claudeCli, profile, prompt, sessionId = null) {
|
||||
}
|
||||
```
|
||||
|
||||
### Result Formatting Pattern
|
||||
### Result Formatting Pattern (Phase 5 Enhanced - Async)
|
||||
|
||||
#### Cost and Duration Extraction
|
||||
```javascript
|
||||
// Extract cost and duration from Claude CLI output
|
||||
function parseExecutionStats(output) {
|
||||
#### Async Result Formatter with UI Integration
|
||||
```typescript
|
||||
// result-formatter.ts - Phase 5 now fully async
|
||||
import { ui } from '../utils/ui';
|
||||
|
||||
class ResultFormatter {
|
||||
// Initialize UI and format result
|
||||
static async format(result: ExecutionResult): Promise<string> {
|
||||
await ui.init(); // Phase 5: Initialize UI layer
|
||||
|
||||
// Build styled output
|
||||
let output = '';
|
||||
|
||||
// Header box with status indicator
|
||||
const modelName = this.getModelDisplayName(result.profile);
|
||||
const headerIcon = result.success ? '[i]' : '[X]';
|
||||
output += ui.box(`${headerIcon} Delegated to ${modelName}`, {
|
||||
borderStyle: 'round',
|
||||
padding: 0,
|
||||
});
|
||||
output += '\n\n';
|
||||
|
||||
// Info table with styled columns
|
||||
output += this.formatInfoTable(result);
|
||||
|
||||
// Error details if needed
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
output += '\n' + ui.errorBox('Execution Errors', 'ERRORS');
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Phase 5: Table formatting
|
||||
private static formatInfoTable(result: ExecutionResult): string {
|
||||
const rows = [
|
||||
['Working Directory', result.cwd],
|
||||
['Model', this.getModelDisplayName(result.profile)],
|
||||
['Duration', `${(result.duration / 1000).toFixed(2)}s`],
|
||||
['Exit Code', result.exitCode === 0 ? '[OK]' : '[X]'],
|
||||
];
|
||||
|
||||
if (result.totalCost) {
|
||||
rows.push(['Total Cost', `$${result.totalCost.toFixed(4)}`]);
|
||||
}
|
||||
|
||||
return ui.table(rows, {
|
||||
style: 'unicode',
|
||||
wordWrap: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in delegation-handler.ts
|
||||
const formatted = await ResultFormatter.format(result); // Phase 5: Now async!
|
||||
console.log(formatted);
|
||||
```
|
||||
|
||||
#### Cost and Duration Extraction (Unchanged from v4.0)
|
||||
```typescript
|
||||
// Extract cost and duration from stream-JSON output
|
||||
interface ExecutionStats {
|
||||
cost: number | null;
|
||||
duration: number | null;
|
||||
exitCode: number;
|
||||
}
|
||||
|
||||
function parseExecutionStats(output: string): ExecutionStats {
|
||||
const costRegex = /Cost:\s+\$(\d+\.\d+)/;
|
||||
const durationRegex = /Duration:\s+(\d+)ms/;
|
||||
|
||||
@@ -546,20 +745,10 @@ function parseExecutionStats(output) {
|
||||
|
||||
return {
|
||||
cost: costMatch ? parseFloat(costMatch[1]) : null,
|
||||
duration: durationMatch ? parseInt(durationMatch[1]) : null
|
||||
duration: durationMatch ? parseInt(durationMatch[1]) : null,
|
||||
exitCode: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Format results
|
||||
function formatResults(exitCode, stats) {
|
||||
const status = exitCode === 0 ? '[OK]' : '[X]';
|
||||
const costStr = stats.cost ? `\$${stats.cost.toFixed(4)}` : 'N/A';
|
||||
const durationStr = stats.duration ? `${(stats.duration / 1000).toFixed(2)}s` : 'N/A';
|
||||
|
||||
console.log(`\n${status} Execution complete`);
|
||||
console.log(` Cost: ${costStr}`);
|
||||
console.log(` Duration: ${durationStr}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Symlinking Patterns (v4.1+)
|
||||
@@ -960,10 +1149,19 @@ These code standards ensure the CCS codebase remains:
|
||||
- **Compatible**: Cross-platform symlinking, Windows fallbacks, unified behavior
|
||||
- **Extensible**: Clean subsystem boundaries, reusable patterns
|
||||
- **Type-Safe**: Enhanced ESLint strictness enforces zero-tolerance for code quality issues
|
||||
- **User-Friendly**: Semantic UI system with intelligent fallback rendering (Phase 5)
|
||||
|
||||
**Phase 5 (2025-12-01) - UI & Listr2 Integration**:
|
||||
- **Central UI Module**: src/utils/ui.ts with semantic colors, status indicators, boxes, tables
|
||||
- **Listr2 Integration**: Task list progress with intelligent renderer selection
|
||||
- **Claude Code Detection**: Automatic fallback for tool context (isClaudeCodeContext)
|
||||
- **Async Formatting**: Result formatter now async with ui.init() call
|
||||
- **Compliance**: ASCII-only indicators ([OK], [X], [!], [i]), NO_COLOR respect
|
||||
|
||||
**v4.x Specific Standards**:
|
||||
- **Delegation patterns**: Stream-JSON parsing, session management, headless execution
|
||||
- **Symlinking patterns**: Cross-platform symlink creation, validation, repair
|
||||
- **UI patterns**: Semantic colors, status indicators, boxes, tables, spinners, task lists
|
||||
- **Subsystem organization**: Clear separation (auth, delegation, glmt, management, utils)
|
||||
- **Naming conventions**: handler, executor, manager, validator, formatter, parser suffixes
|
||||
|
||||
@@ -973,4 +1171,4 @@ These code standards ensure the CCS codebase remains:
|
||||
- **No shell dependencies**: error-codes.sh, progress-indicator.sh, prompt.sh removed
|
||||
- **First-run bootstrap**: Auto-installs @kaitranntt/ccs npm package globally
|
||||
|
||||
Following these standards helps maintain the quality, modularity, and extensibility of the v4.x architecture while enabling future development with AI delegation, shared data management, and comprehensive diagnostics.
|
||||
Following these standards helps maintain the quality, modularity, and extensibility of the v4.x architecture while enabling future development with AI delegation, shared data management, and comprehensive diagnostics. Phase 5 adds a professional UI layer that gracefully degrades in non-TTY environments while maintaining strict compliance with project constraints.
|
||||
+62
-19
@@ -6,7 +6,7 @@ CCS (Claude Code Switch) v4.5.0 is a lightweight CLI wrapper enabling instant pr
|
||||
|
||||
## Version Evolution
|
||||
|
||||
### v4.5.0 Architecture (Current, Phase 02 Complete - 2025-11-28)
|
||||
### v4.5.0 Architecture (Current, Phase 02 Complete - 2025-11-28, UI Phase 1 - 2025-12-01)
|
||||
- **Total LOC**: ~8,477 lines (JavaScript/TypeScript)
|
||||
- **Main File**: src/ccs.ts - 593 lines (**44.6% reduction** from 1,071 lines)
|
||||
- **Key Features**: AI delegation, stream-JSON output, shell completion, doctor diagnostics, sync command
|
||||
@@ -23,7 +23,7 @@ CCS (Claude Code Switch) v4.5.0 is a lightweight CLI wrapper enabling instant pr
|
||||
- **Phase 02 (2025-11-28)**: Modular command architecture, native installer deprecation, main file 44.6% reduction
|
||||
- **v4.5.0**: npm-first distribution, TypeScript package with quality gates, modular commands, text-only output (no emojis)
|
||||
|
||||
## Core Components (Phase 02 Complete - 2025-11-28)
|
||||
## Core Components (Phase 05 Complete - 2025-12-01 | UI Phase 1 + Listr2)
|
||||
|
||||
### 1. Main Entry Point (`src/ccs.ts` - 593 lines, 44.6% reduction)
|
||||
|
||||
@@ -103,16 +103,16 @@ interface CommandHandler {
|
||||
- **shell-executor.ts** (1.5KB): Cross-platform shell command execution with process management
|
||||
- **package-manager-detector.ts** (3.8KB): Package manager detection (npm, yarn, pnpm, bun)
|
||||
|
||||
### 3. Delegation System (`src/delegation/` - ~1,200 lines)
|
||||
### 3. Delegation System (`src/delegation/` - ~1,200 lines, Phase 5 Enhanced)
|
||||
|
||||
**New in v4.0**: Complete delegation subsystem
|
||||
**New in v4.0**: Complete delegation subsystem; **Phase 5 Enhanced** with UI layer & Listr2
|
||||
|
||||
**Components**:
|
||||
- **delegation-handler.js** (~300 lines): Routes `-p` commands, validates profiles
|
||||
- **headless-executor.js** (~400 lines): Executes Claude CLI in headless mode with stream-JSON
|
||||
- **session-manager.js** (~200 lines): Manages delegation session persistence (continue support)
|
||||
- **result-formatter.js** (~150 lines): Formats execution results with cost/duration
|
||||
- **settings-parser.js** (~150 lines): Parses profile settings for validation
|
||||
- **delegation-handler.ts** (~300 lines): Routes `-p` commands, validates profiles; async formatting
|
||||
- **headless-executor.ts** (~400 lines): Executes Claude CLI in headless mode with stream-JSON; UI progress
|
||||
- **session-manager.ts** (~200 lines): Manages delegation session persistence (continue support)
|
||||
- **result-formatter.ts** (~150 lines): **Async formatting** with styled boxes and tables (Phase 5)
|
||||
- **settings-parser.ts** (~150 lines): Parses profile settings for validation
|
||||
|
||||
**Key Features**:
|
||||
- **Stream-JSON output**: Real-time tool visibility (`--output-format stream-json --verbose`)
|
||||
@@ -121,6 +121,11 @@ interface CommandHandler {
|
||||
- **Signal handling**: Ctrl+C kills child processes properly
|
||||
- **Cost tracking**: USD cost display per delegation
|
||||
- **13 Claude Code tools** supported: Bash, Read, Write, Edit, Glob, Grep, NotebookEdit, SlashCommand, TodoWrite, etc.
|
||||
- **Phase 5 Enhancements**:
|
||||
- **Styled output**: UI layer integration for semantic boxes and tables
|
||||
- **Async formatting**: Result formatter now fully async with ui.init() call
|
||||
- **Listr2 integration**: Optional task list progress in TTY mode
|
||||
- **Fallback chain**: Graceful degradation for non-TTY/CI environments
|
||||
|
||||
**Delegation Flow**:
|
||||
```
|
||||
@@ -271,19 +276,20 @@ src/ # TypeScript source files (Phase 02 Modular Archite
|
||||
│ ├── instance-manager.ts # Instance lifecycle (~220 lines)
|
||||
│ ├── recovery-manager.ts # Auto-recovery (~80 lines)
|
||||
│ └── shared-manager.ts # Shared symlinking (~50 lines)
|
||||
├── utils/ # Utilities (expanded in v4.x + Phase 02)
|
||||
├── utils/ # Utilities (expanded in v4.x + Phase 02 + Phase 05, UI + Listr2)
|
||||
│ ├── claude-detector.ts # CLI detection (~70 lines)
|
||||
│ ├── claude-dir-installer.ts # .claude/ installer (v4.1.1, ~150 lines)
|
||||
│ ├── claude-symlink-manager.ts # Selective symlinks (v4.1, ~200 lines)
|
||||
│ ├── config-manager.ts # Config management (~80 lines)
|
||||
│ ├── shell-executor.ts # 1.5KB - Cross-platform execution (Phase 02 NEW)
|
||||
│ ├── package-manager-detector.ts # 3.8KB - Package manager detection (Phase 02 NEW)
|
||||
│ ├── delegation-validator.js # Delegation validation (v4.0, ~100 lines)
|
||||
│ ├── error-codes.js # Error codes (~50 lines)
|
||||
│ ├── error-manager.js # Error handling (~200 lines)
|
||||
│ ├── helpers.js # Utilities (~100 lines)
|
||||
│ ├── progress-indicator.js # Progress display (~150 lines)
|
||||
│ ├── prompt.js # User prompting (~100 lines)
|
||||
│ ├── shell-executor.ts # 1.5KB - Cross-platform execution (Phase 02)
|
||||
│ ├── package-manager-detector.ts # 3.8KB - Package manager detection (Phase 02)
|
||||
│ ├── ui.ts # 5.2KB - Central UI abstraction (Phase 5, Listr2 integration)
|
||||
│ ├── delegation-validator.ts # Delegation validation (v4.0, ~100 lines)
|
||||
│ ├── error-codes.ts # Error codes (~50 lines)
|
||||
│ ├── error-manager.ts # Error handling (~200 lines)
|
||||
│ ├── helpers.ts # Utilities (~100 lines)
|
||||
│ ├── progress-indicator.ts # Progress display (~150 lines)
|
||||
│ ├── prompt.ts # User prompting (~100 lines)
|
||||
│ ├── shell-completion.ts # Shell completion (v4.1.4, ~250 lines)
|
||||
│ └── update-checker.ts # Update checker (v4.1, ~100 lines)
|
||||
├── types/ # TypeScript type definitions
|
||||
@@ -424,7 +430,28 @@ ccs.js: execClaude(["command"], {CLAUDE_CONFIG_DIR: instancePath})
|
||||
Claude CLI: Read credentials from instance, execute
|
||||
```
|
||||
|
||||
## Key Features (v4.5.0 - Phase 02 Complete)
|
||||
## Key Features (v4.5.0 - Phase 02 Complete, UI Phase 1)
|
||||
|
||||
### 0. Central UI Abstraction Layer (UI Phase 1 - 2025-12-01, Phase 5 Complete)
|
||||
- **New Module**: src/utils/ui.ts (5.2KB) - Semantic, TTY-aware CLI styling
|
||||
- **Dependencies**: chalk@5.6.2, boxen@8.0.1, gradient-string@3.0.0, cli-table3@0.6.5, ora@5.4.1, listr2@8.0.0
|
||||
- **Features**:
|
||||
- Semantic color system (success, error, warning, info, dim, primary, secondary, command, path)
|
||||
- ASCII-only status indicators ([OK], [X], [!], [i]) - NO EMOJIS per CLAUDE.md
|
||||
- TTY-aware output (respects NO_COLOR, FORCE_COLOR env vars)
|
||||
- Box rendering (with fallback ASCII renderer)
|
||||
- Table rendering via cli-table3
|
||||
- Spinner/progress (ora wrapper with fallback)
|
||||
- Section headers with optional gradient
|
||||
- **Listr2 task lists** (Phase 5 NEW) - intelligent renderer selection:
|
||||
- TTY mode: Default renderer with subtask hierarchy
|
||||
- Non-TTY/CI: Simple renderer for clean output
|
||||
- Claude Code detection: Automatic fallback for tool context
|
||||
- Lazy loading of ESM modules for CommonJS compatibility
|
||||
- **Type Support**: SemanticColor, BoxOptions, TableOptions, SpinnerOptions, SpinnerController, TaskItem, TaskListOptions
|
||||
- **Compliance**: Strict CLAUDE.md adherence (no emojis, TTY-aware, NO_COLOR respect)
|
||||
- **Fallback Architecture**: Works in non-TTY environments with graceful degradation
|
||||
- **Claude Code Integration** (Phase 5): `isClaudeCodeContext()` detection for adaptive UI rendering
|
||||
|
||||
### 1. npm-First Installation (Phase 02 - 2025-11-28)
|
||||
- **Recommended method**: All users directed to npm installation
|
||||
@@ -552,6 +579,22 @@ Claude CLI: Read credentials from instance, execute
|
||||
|
||||
## Summary
|
||||
|
||||
**CCS Phase 5 Achievements (2025-12-01, UI + Listr2 Integration)**:
|
||||
- **Listr2 Task Lists**: Integrated task list progress display with intelligent renderer selection
|
||||
- **Claude Code Detection**: Automatic fallback for tool context via `isClaudeCodeContext()`
|
||||
- **Async Result Formatting**: Delegation system now fully async with ui initialization
|
||||
- **UI Layer Integration**: Styled boxes and tables for enhanced delegation output
|
||||
- **Adaptive Rendering**: TTY (default), non-TTY (simple), CI (simple), Claude Code (fallback)
|
||||
- **Type System**: TaskItem, TaskListOptions for task list operations
|
||||
|
||||
**CCS UI Phase 1 Achievements (2025-12-01)**:
|
||||
- **Central UI Module**: Introduced src/utils/ui.ts for semantic, TTY-aware CLI styling
|
||||
- **CLAUDE.md Compliance**: ASCII-only indicators, NO_COLOR respect, TTY detection
|
||||
- **ESM Compatibility**: Lazy loading strategy for chalk, boxen, gradient-string, ora in CommonJS project
|
||||
- **Fallback Architecture**: Works in non-TTY (pipes/CI) with graceful degradation to plain text
|
||||
- **Type System**: Complete TypeScript definitions for all UI functions
|
||||
- **Color Palette**: Professional cyan-to-blue gradient (#00ECFA to #0099FF)
|
||||
|
||||
**CCS Phase 02 Achievements (2025-11-28)**:
|
||||
- **npm-First Distribution**: Deprecated native shell installers, all users directed to npm
|
||||
- **Modular Command Architecture**: 6 specialized command handlers with single responsibility principle
|
||||
|
||||
@@ -70,7 +70,11 @@
|
||||
"postinstall": "node scripts/postinstall.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "^5.6.2",
|
||||
"cli-table3": "^0.6.5",
|
||||
"gradient-string": "^3.0.0",
|
||||
"listr2": "^9.0.5",
|
||||
"ora": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+174
-116
@@ -13,7 +13,19 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import ProfileRegistry from './profile-registry';
|
||||
import { InstanceManager } from '../management/instance-manager';
|
||||
import { colored } from '../utils/helpers';
|
||||
import {
|
||||
initUI,
|
||||
header,
|
||||
subheader,
|
||||
color,
|
||||
dim,
|
||||
ok,
|
||||
fail,
|
||||
warn,
|
||||
info,
|
||||
table,
|
||||
infoBox,
|
||||
} from '../utils/ui';
|
||||
import { detectClaudeCli } from '../utils/claude-detector';
|
||||
import { InteractivePrompt } from '../utils/prompt';
|
||||
import packageJson from '../../package.json';
|
||||
@@ -57,56 +69,54 @@ class AuthCommands {
|
||||
/**
|
||||
* Show help for auth commands
|
||||
*/
|
||||
showHelp(): void {
|
||||
console.log(colored('CCS Concurrent Account Management', 'bold'));
|
||||
async showHelp(): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
console.log(header('CCS Concurrent Account Management'));
|
||||
console.log('');
|
||||
console.log(colored('Usage:', 'cyan'));
|
||||
console.log(` ${colored('ccs auth', 'yellow')} <command> [options]`);
|
||||
console.log(subheader('Usage'));
|
||||
console.log(` ${color('ccs auth', 'command')} <command> [options]`);
|
||||
console.log('');
|
||||
console.log(colored('Commands:', 'cyan'));
|
||||
console.log(` ${colored('create <profile>', 'yellow')} Create new profile and login`);
|
||||
console.log(` ${colored('list', 'yellow')} List all saved profiles`);
|
||||
console.log(` ${colored('show <profile>', 'yellow')} Show profile details`);
|
||||
console.log(` ${colored('remove <profile>', 'yellow')} Remove saved profile`);
|
||||
console.log(` ${colored('default <profile>', 'yellow')} Set default profile`);
|
||||
console.log(subheader('Commands'));
|
||||
console.log(` ${color('create <profile>', 'command')} Create new profile and login`);
|
||||
console.log(` ${color('list', 'command')} List all saved profiles`);
|
||||
console.log(` ${color('show <profile>', 'command')} Show profile details`);
|
||||
console.log(` ${color('remove <profile>', 'command')} Remove saved profile`);
|
||||
console.log(` ${color('default <profile>', 'command')} Set default profile`);
|
||||
console.log('');
|
||||
console.log(colored('Examples:', 'cyan'));
|
||||
console.log(subheader('Examples'));
|
||||
console.log(` ${dim('# Create & login to work profile')}`);
|
||||
console.log(` ${color('ccs auth create work', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Set work as default')}`);
|
||||
console.log(` ${color('ccs auth default work', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# List all profiles')}`);
|
||||
console.log(` ${color('ccs auth list', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Use work profile')}`);
|
||||
console.log(` ${color('ccs work "review code"', 'command')}`);
|
||||
console.log('');
|
||||
console.log(subheader('Options'));
|
||||
console.log(
|
||||
` ${colored('ccs auth create work', 'yellow')} # Create & login to work profile`
|
||||
` ${color('--force', 'command')} Allow overwriting existing profile (create)`
|
||||
);
|
||||
console.log(
|
||||
` ${colored('ccs auth default work', 'yellow')} # Set work as default`
|
||||
` ${color('--yes, -y', 'command')} Skip confirmation prompts (remove)`
|
||||
);
|
||||
console.log(
|
||||
` ${colored('ccs auth list', 'yellow')} # List all profiles`
|
||||
` ${color('--json', 'command')} Output in JSON format (list, show)`
|
||||
);
|
||||
console.log(
|
||||
` ${colored('ccs work "review code"', 'yellow')} # Use work profile`
|
||||
);
|
||||
console.log(
|
||||
` ${colored('ccs "review code"', 'yellow')} # Use default profile`
|
||||
` ${color('--verbose', 'command')} Show additional details (list)`
|
||||
);
|
||||
console.log('');
|
||||
console.log(colored('Options:', 'cyan'));
|
||||
console.log(subheader('Note'));
|
||||
console.log(
|
||||
` ${colored('--force', 'yellow')} Allow overwriting existing profile (create)`
|
||||
` By default, ${color('ccs', 'command')} uses Claude CLI defaults from ~/.claude/`
|
||||
);
|
||||
console.log(
|
||||
` ${colored('--yes, -y', 'yellow')} Skip confirmation prompts (remove)`
|
||||
);
|
||||
console.log(
|
||||
` ${colored('--json', 'yellow')} Output in JSON format (list, show)`
|
||||
);
|
||||
console.log(
|
||||
` ${colored('--verbose', 'yellow')} Show additional details (list)`
|
||||
);
|
||||
console.log('');
|
||||
console.log(colored('Note:', 'cyan'));
|
||||
console.log(
|
||||
` By default, ${colored('ccs', 'yellow')} uses Claude CLI defaults from ~/.claude/`
|
||||
);
|
||||
console.log(
|
||||
` Use ${colored('ccs auth default <profile>', 'yellow')} to change the default profile.`
|
||||
` Use ${color('ccs auth default <profile>', 'command')} to change the default profile.`
|
||||
);
|
||||
console.log('');
|
||||
}
|
||||
@@ -129,28 +139,29 @@ class AuthCommands {
|
||||
* Create new profile and prompt for login
|
||||
*/
|
||||
async handleCreate(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
const { profileName, force } = this.parseArgs(args);
|
||||
|
||||
if (!profileName) {
|
||||
console.error('[X] Profile name is required');
|
||||
console.log(fail('Profile name is required'));
|
||||
console.log('');
|
||||
console.log(`Usage: ${colored('ccs auth create <profile> [--force]', 'yellow')}`);
|
||||
console.log(`Usage: ${color('ccs auth create <profile> [--force]', 'command')}`);
|
||||
console.log('');
|
||||
console.log('Example:');
|
||||
console.log(` ${colored('ccs auth create work', 'yellow')}`);
|
||||
console.log(` ${color('ccs auth create work', 'command')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if profile already exists
|
||||
if (!force && this.registry.hasProfile(profileName)) {
|
||||
console.error(`[X] Profile already exists: ${profileName}`);
|
||||
console.log(` Use ${colored('--force', 'yellow')} to overwrite`);
|
||||
console.log(fail(`Profile already exists: ${profileName}`));
|
||||
console.log(` Use ${color('--force', 'command')} to overwrite`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Create instance directory
|
||||
console.log(`[i] Creating profile: ${profileName}`);
|
||||
console.log(info(`Creating profile: ${profileName}`));
|
||||
const instancePath = this.instanceMgr.ensureInstance(profileName);
|
||||
|
||||
// Create/update profile entry
|
||||
@@ -164,19 +175,19 @@ class AuthCommands {
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[i] Instance directory: ${instancePath}`);
|
||||
console.log(info(`Instance directory: ${instancePath}`));
|
||||
console.log('');
|
||||
console.log(colored('[i] Starting Claude in isolated instance...', 'yellow'));
|
||||
console.log(colored('[i] You will be prompted to login with your account.', 'yellow'));
|
||||
console.log(warn('Starting Claude in isolated instance...'));
|
||||
console.log(warn('You will be prompted to login with your account.'));
|
||||
console.log('');
|
||||
|
||||
// Detect Claude CLI
|
||||
const claudeCli = detectClaudeCli();
|
||||
if (!claudeCli) {
|
||||
console.error('[X] Claude CLI not found');
|
||||
console.log(fail('Claude CLI not found'));
|
||||
console.log('');
|
||||
console.log('Please install Claude CLI first:');
|
||||
console.log(' https://claude.ai/download');
|
||||
console.log(` ${color('https://claude.ai/download', 'path')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -189,45 +200,63 @@ class AuthCommands {
|
||||
child.on('exit', (code: number | null) => {
|
||||
if (code === 0) {
|
||||
console.log('');
|
||||
console.log(colored('[OK] Profile created successfully', 'green'));
|
||||
console.log('');
|
||||
console.log(` Profile: ${profileName}`);
|
||||
console.log(` Instance: ${instancePath}`);
|
||||
console.log('');
|
||||
console.log('Usage:');
|
||||
console.log(
|
||||
` ${colored(`ccs ${profileName} "your prompt here"`, 'yellow')} # Use this specific profile`
|
||||
infoBox(
|
||||
`Profile: ${profileName}\n` + `Instance: ${instancePath}\n` + `Type: account`,
|
||||
'Profile Created'
|
||||
)
|
||||
);
|
||||
console.log('');
|
||||
console.log(header('Usage'));
|
||||
console.log(` ${color(`ccs ${profileName} "your prompt here"`, 'command')}`);
|
||||
console.log('');
|
||||
console.log('To set as default (so you can use just "ccs"):');
|
||||
console.log(` ${colored(`ccs auth default ${profileName}`, 'yellow')}`);
|
||||
console.log(` ${color(`ccs auth default ${profileName}`, 'command')}`);
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('');
|
||||
console.error('[X] Login failed or cancelled');
|
||||
console.log(fail('Login failed or cancelled'));
|
||||
console.log('');
|
||||
console.log('To retry:');
|
||||
console.log(` ${colored(`ccs auth create ${profileName} --force`, 'yellow')}`);
|
||||
console.log(` ${color(`ccs auth create ${profileName} --force`, 'command')}`);
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err: Error) => {
|
||||
console.error(`[X] Failed to execute Claude CLI: ${err.message}`);
|
||||
console.log(fail(`Failed to execute Claude CLI: ${err.message}`));
|
||||
process.exit(1);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[X] Failed to create profile: ${(error as Error).message}`);
|
||||
console.log(fail(`Failed to create profile: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format relative time (e.g., "2h ago", "1d ago")
|
||||
*/
|
||||
private formatRelativeTime(date: Date): string {
|
||||
const now = Date.now();
|
||||
const diff = now - date.getTime();
|
||||
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (days > 0) return `${days}d ago`;
|
||||
if (hours > 0) return `${hours}h ago`;
|
||||
if (minutes > 0) return `${minutes}m ago`;
|
||||
return 'just now';
|
||||
}
|
||||
|
||||
/**
|
||||
* List all saved profiles
|
||||
*/
|
||||
async handleList(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
const { verbose, json } = this.parseArgs(args);
|
||||
|
||||
try {
|
||||
@@ -260,20 +289,18 @@ class AuthCommands {
|
||||
|
||||
// Human-readable output
|
||||
if (profileNames.length === 0) {
|
||||
console.log(colored('No account profiles found', 'yellow'));
|
||||
console.log(warn('No account profiles found'));
|
||||
console.log('');
|
||||
console.log('To create your first profile:');
|
||||
console.log(
|
||||
` ${colored('ccs auth create <profile>', 'yellow')} # Create and login to profile`
|
||||
);
|
||||
console.log(` ${color('ccs auth create <profile>', 'command')}`);
|
||||
console.log('');
|
||||
console.log('Example:');
|
||||
console.log(` ${colored('ccs auth create work', 'yellow')}`);
|
||||
console.log(` ${color('ccs auth create work', 'command')}`);
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(colored('Saved Account Profiles:', 'bold'));
|
||||
console.log(header('Saved Account Profiles'));
|
||||
console.log('');
|
||||
|
||||
// Sort by last_used (descending), then alphabetically
|
||||
@@ -296,31 +323,50 @@ class AuthCommands {
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
sorted.forEach((name) => {
|
||||
// Build table rows
|
||||
const rows: string[][] = sorted.map((name) => {
|
||||
const profile = profiles[name];
|
||||
const isDefault = name === defaultProfile;
|
||||
const indicator = isDefault ? colored('[*]', 'green') : '[ ]';
|
||||
|
||||
console.log(
|
||||
`${indicator} ${colored(name, 'cyan')}${isDefault ? colored(' (default)', 'green') : ''}`
|
||||
);
|
||||
// Status column
|
||||
const status = isDefault ? color('[OK] default', 'success') : color('[OK]', 'success');
|
||||
|
||||
console.log(` Type: ${profile.type || 'account'}`);
|
||||
|
||||
if (verbose) {
|
||||
console.log(` Created: ${new Date(profile.created).toLocaleString()}`);
|
||||
if (profile.last_used) {
|
||||
console.log(` Last used: ${new Date(profile.last_used).toLocaleString()}`);
|
||||
}
|
||||
// Last used column
|
||||
let lastUsed = '-';
|
||||
if (profile.last_used) {
|
||||
lastUsed = this.formatRelativeTime(new Date(profile.last_used));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
const row = [
|
||||
color(name, isDefault ? 'primary' : 'info'),
|
||||
profile.type || 'account',
|
||||
status,
|
||||
];
|
||||
|
||||
if (verbose) {
|
||||
row.push(lastUsed);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
console.log(`Total profiles: ${profileNames.length}`);
|
||||
// Headers
|
||||
const headers = verbose
|
||||
? ['Profile', 'Type', 'Status', 'Last Used']
|
||||
: ['Profile', 'Type', 'Status'];
|
||||
|
||||
// Print table
|
||||
console.log(
|
||||
table(rows, {
|
||||
head: headers,
|
||||
colWidths: verbose ? [15, 12, 15, 12] : [15, 12, 15],
|
||||
})
|
||||
);
|
||||
console.log('');
|
||||
console.log(dim(`Total: ${profileNames.length} profile(s)`));
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error(`[X] Failed to list profiles: ${(error as Error).message}`);
|
||||
console.log(fail(`Failed to list profiles: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -329,12 +375,13 @@ class AuthCommands {
|
||||
* Show details for a specific profile
|
||||
*/
|
||||
async handleShow(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
const { profileName, json } = this.parseArgs(args);
|
||||
|
||||
if (!profileName) {
|
||||
console.error('[X] Profile name is required');
|
||||
console.log(fail('Profile name is required'));
|
||||
console.log('');
|
||||
console.log(`Usage: ${colored('ccs auth show <profile> [--json]', 'yellow')}`);
|
||||
console.log(`Usage: ${color('ccs auth show <profile> [--json]', 'command')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -372,22 +419,27 @@ class AuthCommands {
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
console.log(colored(`Profile: ${profileName}`, 'bold'));
|
||||
const defaultBadge = isDefault ? color(' (default)', 'success') : '';
|
||||
console.log(header(`Profile: ${profileName}${defaultBadge}`));
|
||||
console.log('');
|
||||
console.log(` Type: ${profile.type || 'account'}`);
|
||||
console.log(` Default: ${isDefault ? 'Yes' : 'No'}`);
|
||||
console.log(` Instance: ${instancePath}`);
|
||||
console.log(` Created: ${new Date(profile.created).toLocaleString()}`);
|
||||
|
||||
if (profile.last_used) {
|
||||
console.log(` Last used: ${new Date(profile.last_used).toLocaleString()}`);
|
||||
} else {
|
||||
console.log(` Last used: Never`);
|
||||
}
|
||||
// Details table
|
||||
const details = [
|
||||
['Type', profile.type || 'account'],
|
||||
['Instance', instancePath],
|
||||
['Created', new Date(profile.created).toLocaleString()],
|
||||
['Last Used', profile.last_used ? new Date(profile.last_used).toLocaleString() : 'Never'],
|
||||
['Sessions', `${sessionCount}`],
|
||||
];
|
||||
|
||||
console.log(
|
||||
table(details, {
|
||||
colWidths: [15, 45],
|
||||
})
|
||||
);
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error(`[X] ${(error as Error).message}`);
|
||||
console.log(fail((error as Error).message));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -396,17 +448,18 @@ class AuthCommands {
|
||||
* Remove a saved profile
|
||||
*/
|
||||
async handleRemove(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
const { profileName, yes } = this.parseArgs(args);
|
||||
|
||||
if (!profileName) {
|
||||
console.error('[X] Profile name is required');
|
||||
console.log(fail('Profile name is required'));
|
||||
console.log('');
|
||||
console.log(`Usage: ${colored('ccs auth remove <profile> [--yes]', 'yellow')}`);
|
||||
console.log(`Usage: ${color('ccs auth remove <profile> [--yes]', 'command')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!this.registry.hasProfile(profileName)) {
|
||||
console.error(`[X] Profile not found: ${profileName}`);
|
||||
console.log(fail(`Profile not found: ${profileName}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -427,7 +480,7 @@ class AuthCommands {
|
||||
|
||||
// Display impact
|
||||
console.log('');
|
||||
console.log(`Profile '${colored(profileName, 'cyan')}' will be permanently deleted.`);
|
||||
console.log(`Profile '${color(profileName, 'command')}' will be permanently deleted.`);
|
||||
console.log(` Instance path: ${instancePath}`);
|
||||
console.log(` Sessions: ${sessionCount} conversation${sessionCount !== 1 ? 's' : ''}`);
|
||||
console.log('');
|
||||
@@ -441,7 +494,7 @@ class AuthCommands {
|
||||
));
|
||||
|
||||
if (!confirmed) {
|
||||
console.log('[i] Cancelled');
|
||||
console.log(info('Cancelled'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -451,11 +504,10 @@ class AuthCommands {
|
||||
// Delete profile
|
||||
this.registry.deleteProfile(profileName);
|
||||
|
||||
console.log(colored('[OK] Profile removed successfully', 'green'));
|
||||
console.log(` Profile: ${profileName}`);
|
||||
console.log(ok(`Profile removed: ${profileName}`));
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error(`[X] Failed to remove profile: ${(error as Error).message}`);
|
||||
console.log(fail(`Failed to remove profile: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -464,26 +516,28 @@ class AuthCommands {
|
||||
* Set default profile
|
||||
*/
|
||||
async handleDefault(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
const { profileName } = this.parseArgs(args);
|
||||
|
||||
if (!profileName) {
|
||||
console.error('[X] Profile name is required');
|
||||
console.log(fail('Profile name is required'));
|
||||
console.log('');
|
||||
console.log(`Usage: ${colored('ccs auth default <profile>', 'yellow')}`);
|
||||
console.log(`Usage: ${color('ccs auth default <profile>', 'command')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
this.registry.setDefaultProfile(profileName);
|
||||
|
||||
console.log(colored('[OK] Default profile set', 'green'));
|
||||
console.log(` Profile: ${profileName}`);
|
||||
console.log(ok(`Default profile set: ${profileName}`));
|
||||
console.log('');
|
||||
console.log('Now you can use:');
|
||||
console.log(` ${colored('ccs "your prompt"', 'yellow')} # Uses ${profileName} profile`);
|
||||
console.log(
|
||||
` ${color('ccs "your prompt"', 'command')} ${dim(`# Uses ${profileName} profile`)}`
|
||||
);
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error(`[X] ${(error as Error).message}`);
|
||||
console.log(fail((error as Error).message));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -493,7 +547,7 @@ class AuthCommands {
|
||||
*/
|
||||
async route(args: string[]): Promise<void> {
|
||||
if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
||||
this.showHelp();
|
||||
await this.showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -507,8 +561,9 @@ class AuthCommands {
|
||||
|
||||
case 'save':
|
||||
// Deprecated - redirect to create
|
||||
console.log(colored('[!] Command "save" is deprecated', 'yellow'));
|
||||
console.log(` Use: ${colored('ccs auth create <profile>', 'yellow')} instead`);
|
||||
await initUI();
|
||||
console.log(warn('Command "save" is deprecated'));
|
||||
console.log(` Use: ${color('ccs auth create <profile>', 'command')} instead`);
|
||||
console.log('');
|
||||
await this.handleCreate(commandArgs);
|
||||
break;
|
||||
@@ -530,26 +585,29 @@ class AuthCommands {
|
||||
break;
|
||||
|
||||
case 'current':
|
||||
console.log(colored('[!] Command "current" has been removed', 'yellow'));
|
||||
await initUI();
|
||||
console.log(warn('Command "current" has been removed'));
|
||||
console.log('');
|
||||
console.log('Each profile has its own login in an isolated instance.');
|
||||
console.log('Use "ccs auth list" to see all profiles.');
|
||||
console.log(`Use ${color('ccs auth list', 'command')} to see all profiles.`);
|
||||
console.log('');
|
||||
break;
|
||||
|
||||
case 'cleanup':
|
||||
console.log(colored('[!] Command "cleanup" has been removed', 'yellow'));
|
||||
await initUI();
|
||||
console.log(warn('Command "cleanup" has been removed'));
|
||||
console.log('');
|
||||
console.log('No cleanup needed - no separate vault files.');
|
||||
console.log('Use "ccs auth list" to see all profiles.');
|
||||
console.log(`Use ${color('ccs auth list', 'command')} to see all profiles.`);
|
||||
console.log('');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`[X] Unknown command: ${command}`);
|
||||
await initUI();
|
||||
console.log(fail(`Unknown command: ${command}`));
|
||||
console.log('');
|
||||
console.log('Run for help:');
|
||||
console.log(` ${colored('ccs auth --help', 'yellow')}`);
|
||||
console.log(` ${color('ccs auth --help', 'command')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -204,7 +204,7 @@ async function main(): Promise<void> {
|
||||
|
||||
// Special case: help command
|
||||
if (firstArg === '--help' || firstArg === '-h' || firstArg === 'help') {
|
||||
handleHelpCommand();
|
||||
await handleHelpCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -253,10 +253,10 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: profile command
|
||||
if (firstArg === 'profile') {
|
||||
const { handleProfileCommand } = await import('./commands/profile-command');
|
||||
await handleProfileCommand(args.slice(1));
|
||||
// Special case: api command (manages API profiles)
|
||||
if (firstArg === 'api') {
|
||||
const { handleApiCommand } = await import('./commands/api-command');
|
||||
await handleApiCommand(args.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ async function main(): Promise<void> {
|
||||
// Detect Claude CLI first (needed for all paths)
|
||||
const claudeCli = detectClaudeCli();
|
||||
if (!claudeCli) {
|
||||
ErrorManager.showClaudeNotFound();
|
||||
await ErrorManager.showClaudeNotFound();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ async function main(): Promise<void> {
|
||||
// Check if this is a profile not found error with suggestions
|
||||
if (err.profileName && err.availableProfiles !== undefined) {
|
||||
const allProfiles = err.availableProfiles.split('\n');
|
||||
ErrorManager.showProfileNotFound(err.profileName, allProfiles, err.suggestions);
|
||||
await ErrorManager.showProfileNotFound(err.profileName, allProfiles, err.suggestions);
|
||||
} else {
|
||||
console.error(`[X] ${err.message}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
/**
|
||||
* API Command Handler
|
||||
*
|
||||
* Manages CCS API profiles for custom API providers.
|
||||
* Commands: create, list, remove
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
initUI,
|
||||
header,
|
||||
subheader,
|
||||
color,
|
||||
dim,
|
||||
ok,
|
||||
fail,
|
||||
warn,
|
||||
info,
|
||||
table,
|
||||
infoBox,
|
||||
} from '../utils/ui';
|
||||
import { InteractivePrompt } from '../utils/prompt';
|
||||
import { getCcsDir, getConfigPath, loadConfig } from '../utils/config-manager';
|
||||
|
||||
interface ApiCommandArgs {
|
||||
name?: string;
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
force?: boolean;
|
||||
yes?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments for api commands
|
||||
*/
|
||||
function parseArgs(args: string[]): ApiCommandArgs {
|
||||
const result: ApiCommandArgs = {};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === '--base-url' && args[i + 1]) {
|
||||
result.baseUrl = args[++i];
|
||||
} else if (arg === '--api-key' && args[i + 1]) {
|
||||
result.apiKey = args[++i];
|
||||
} else if (arg === '--model' && args[i + 1]) {
|
||||
result.model = args[++i];
|
||||
} else if (arg === '--force') {
|
||||
result.force = true;
|
||||
} else if (arg === '--yes' || arg === '-y') {
|
||||
result.yes = true;
|
||||
} else if (!arg.startsWith('-') && !result.name) {
|
||||
result.name = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate API profile name
|
||||
*/
|
||||
function validateApiName(name: string): string | null {
|
||||
if (!name) {
|
||||
return 'API name is required';
|
||||
}
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9._-]*$/.test(name)) {
|
||||
return 'API name must start with letter, contain only letters, numbers, dot, dash, underscore';
|
||||
}
|
||||
if (name.length > 32) {
|
||||
return 'API name must be 32 characters or less';
|
||||
}
|
||||
// Reserved names
|
||||
const reserved = ['default', 'auth', 'api', 'doctor', 'sync', 'update', 'help', 'version'];
|
||||
if (reserved.includes(name.toLowerCase())) {
|
||||
return `'${name}' is a reserved name`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate URL format
|
||||
*/
|
||||
function validateUrl(url: string): string | null {
|
||||
if (!url) {
|
||||
return 'Base URL is required';
|
||||
}
|
||||
try {
|
||||
new URL(url);
|
||||
return null;
|
||||
} catch {
|
||||
return 'Invalid URL format (must include protocol, e.g., https://)';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if API profile already exists in config.json
|
||||
*/
|
||||
function apiExists(name: string): boolean {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
return name in config.profiles;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create settings.json file for API profile
|
||||
*/
|
||||
function createSettingsFile(name: string, baseUrl: string, apiKey: string, model: string): string {
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${name}.settings.json`);
|
||||
|
||||
const settings = {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: baseUrl,
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
ANTHROPIC_MODEL: model,
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
||||
return settingsPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update config.json with new API profile
|
||||
*/
|
||||
function updateConfig(name: string, _settingsPath: string): void {
|
||||
const configPath = getConfigPath();
|
||||
const ccsDir = getCcsDir();
|
||||
|
||||
// Read existing config or create new
|
||||
let config: { profiles: Record<string, string>; cliproxy?: Record<string, unknown> };
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} catch {
|
||||
config = { profiles: {} };
|
||||
}
|
||||
|
||||
// Use relative path with ~ for portability
|
||||
const relativePath = `~/.ccs/${name}.settings.json`;
|
||||
config.profiles[name] = relativePath;
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(ccsDir)) {
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write config atomically (write to temp, then rename)
|
||||
const tempPath = configPath + '.tmp';
|
||||
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tempPath, configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 'ccs api create' command
|
||||
*/
|
||||
async function handleCreate(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
const parsedArgs = parseArgs(args);
|
||||
|
||||
console.log(header('Create API Profile'));
|
||||
console.log('');
|
||||
|
||||
// Step 1: API name
|
||||
let name = parsedArgs.name;
|
||||
if (!name) {
|
||||
name = await InteractivePrompt.input('API name', {
|
||||
validate: validateApiName,
|
||||
});
|
||||
} else {
|
||||
const error = validateApiName(name);
|
||||
if (error) {
|
||||
console.log(fail(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if exists
|
||||
if (apiExists(name) && !parsedArgs.force) {
|
||||
console.log(fail(`API '${name}' already exists`));
|
||||
console.log(` Use ${color('--force', 'command')} to overwrite`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 2: Base URL
|
||||
let baseUrl = parsedArgs.baseUrl;
|
||||
if (!baseUrl) {
|
||||
baseUrl = await InteractivePrompt.input('API Base URL (e.g., https://api.example.com)', {
|
||||
validate: validateUrl,
|
||||
});
|
||||
} else {
|
||||
const error = validateUrl(baseUrl);
|
||||
if (error) {
|
||||
console.log(fail(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: API Key
|
||||
let apiKey = parsedArgs.apiKey;
|
||||
if (!apiKey) {
|
||||
apiKey = await InteractivePrompt.password('API Key');
|
||||
if (!apiKey) {
|
||||
console.log(fail('API key is required'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Model (optional)
|
||||
const defaultModel = 'claude-sonnet-4-5-20250929';
|
||||
let model = parsedArgs.model;
|
||||
if (!model && !parsedArgs.yes) {
|
||||
model = await InteractivePrompt.input('Default model', {
|
||||
default: defaultModel,
|
||||
});
|
||||
}
|
||||
model = model || defaultModel;
|
||||
|
||||
// Create files
|
||||
console.log('');
|
||||
console.log(info('Creating API profile...'));
|
||||
|
||||
try {
|
||||
const settingsPath = createSettingsFile(name, baseUrl, apiKey, model);
|
||||
updateConfig(name, settingsPath);
|
||||
|
||||
console.log('');
|
||||
console.log(
|
||||
infoBox(
|
||||
`API: ${name}\n` +
|
||||
`Settings: ~/.ccs/${name}.settings.json\n` +
|
||||
`Base URL: ${baseUrl}\n` +
|
||||
`Model: ${model}`,
|
||||
'API Profile Created'
|
||||
)
|
||||
);
|
||||
console.log('');
|
||||
console.log(header('Usage'));
|
||||
console.log(` ${color(`ccs ${name} "your prompt"`, 'command')}`);
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.log(fail(`Failed to create API profile: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if API profile has real API key (not placeholder)
|
||||
*/
|
||||
function isApiConfigured(apiName: string): boolean {
|
||||
try {
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${apiName}.settings.json`);
|
||||
if (!fs.existsSync(settingsPath)) return false;
|
||||
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
const token = settings?.env?.ANTHROPIC_AUTH_TOKEN || '';
|
||||
// Check if it's a placeholder or empty
|
||||
return token.length > 0 && !token.includes('YOUR_') && !token.includes('your-');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 'ccs api list' command
|
||||
*/
|
||||
async function handleList(): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
console.log(header('CCS API Profiles'));
|
||||
console.log('');
|
||||
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const apis = Object.keys(config.profiles);
|
||||
|
||||
if (apis.length === 0) {
|
||||
console.log(warn('No API profiles configured'));
|
||||
console.log('');
|
||||
console.log('To create an API profile:');
|
||||
console.log(` ${color('ccs api create', 'command')}`);
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build table data with status indicators
|
||||
const rows: string[][] = apis.map((name) => {
|
||||
const settingsPath = config.profiles[name];
|
||||
const status = isApiConfigured(name) ? color('[OK]', 'success') : color('[!]', 'warning');
|
||||
|
||||
return [name, settingsPath, status];
|
||||
});
|
||||
|
||||
// Print table
|
||||
console.log(
|
||||
table(rows, {
|
||||
head: ['API', 'Settings File', 'Status'],
|
||||
colWidths: [15, 35, 10],
|
||||
})
|
||||
);
|
||||
console.log('');
|
||||
|
||||
// Show CLIProxy variants if any
|
||||
if (config.cliproxy && Object.keys(config.cliproxy).length > 0) {
|
||||
console.log(subheader('CLIProxy Variants'));
|
||||
const cliproxyRows = Object.entries(config.cliproxy).map(([name, v]) => {
|
||||
const variant = v as { provider: string; settings: string };
|
||||
return [name, variant.provider, variant.settings];
|
||||
});
|
||||
|
||||
console.log(
|
||||
table(cliproxyRows, {
|
||||
head: ['Variant', 'Provider', 'Settings'],
|
||||
colWidths: [15, 15, 30],
|
||||
})
|
||||
);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(dim(`Total: ${apis.length} API profile(s)`));
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.log(fail(`Failed to list API profiles: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 'ccs api remove' command
|
||||
*/
|
||||
async function handleRemove(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
const parsedArgs = parseArgs(args);
|
||||
|
||||
// Load config first to get available APIs
|
||||
let config: { profiles: Record<string, string>; cliproxy?: Record<string, unknown> };
|
||||
try {
|
||||
config = loadConfig();
|
||||
} catch {
|
||||
console.log(fail('Failed to load config'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const apis = Object.keys(config.profiles);
|
||||
if (apis.length === 0) {
|
||||
console.log(warn('No API profiles to remove'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Interactive API selection if not provided
|
||||
let name = parsedArgs.name;
|
||||
if (!name) {
|
||||
console.log(header('Remove API Profile'));
|
||||
console.log('');
|
||||
console.log('Available APIs:');
|
||||
apis.forEach((p, i) => console.log(` ${i + 1}. ${p}`));
|
||||
console.log('');
|
||||
|
||||
name = await InteractivePrompt.input('API name to remove', {
|
||||
validate: (val) => {
|
||||
if (!val) return 'API name is required';
|
||||
if (!apis.includes(val)) return `API '${val}' not found`;
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!(name in config.profiles)) {
|
||||
console.log(fail(`API '${name}' not found`));
|
||||
console.log('');
|
||||
console.log('Available APIs:');
|
||||
apis.forEach((p) => console.log(` - ${p}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const settingsPath = config.profiles[name];
|
||||
const expandedPath = path.join(getCcsDir(), `${name}.settings.json`);
|
||||
|
||||
// Confirm deletion
|
||||
console.log('');
|
||||
console.log(`API '${color(name, 'command')}' will be removed.`);
|
||||
console.log(` Settings: ${settingsPath}`);
|
||||
console.log('');
|
||||
|
||||
const confirmed =
|
||||
parsedArgs.yes ||
|
||||
(await InteractivePrompt.confirm('Delete this API profile?', { default: false }));
|
||||
|
||||
if (!confirmed) {
|
||||
console.log(info('Cancelled'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Remove from config.json
|
||||
delete config.profiles[name];
|
||||
const configPath = getConfigPath();
|
||||
const tempPath = configPath + '.tmp';
|
||||
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tempPath, configPath);
|
||||
|
||||
// Remove settings file if it exists
|
||||
if (fs.existsSync(expandedPath)) {
|
||||
fs.unlinkSync(expandedPath);
|
||||
}
|
||||
|
||||
console.log(ok(`API profile removed: ${name}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show help for api commands
|
||||
*/
|
||||
async function showHelp(): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
console.log(header('CCS API Management'));
|
||||
console.log('');
|
||||
console.log(subheader('Usage'));
|
||||
console.log(` ${color('ccs api', 'command')} <command> [options]`);
|
||||
console.log('');
|
||||
console.log(subheader('Commands'));
|
||||
console.log(` ${color('create [name]', 'command')} Create new API profile (interactive)`);
|
||||
console.log(` ${color('list', 'command')} List all API profiles`);
|
||||
console.log(` ${color('remove <name>', 'command')} Remove an API profile`);
|
||||
console.log('');
|
||||
console.log(subheader('Options'));
|
||||
console.log(` ${color('--base-url <url>', 'command')} API base URL (create)`);
|
||||
console.log(` ${color('--api-key <key>', 'command')} API key (create)`);
|
||||
console.log(` ${color('--model <model>', 'command')} Default model (create)`);
|
||||
console.log(` ${color('--force', 'command')} Overwrite existing (create)`);
|
||||
console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts`);
|
||||
console.log('');
|
||||
console.log(subheader('Examples'));
|
||||
console.log(` ${dim('# Interactive wizard')}`);
|
||||
console.log(` ${color('ccs api create', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Create with name')}`);
|
||||
console.log(` ${color('ccs api create myapi', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Remove API profile')}`);
|
||||
console.log(` ${color('ccs api remove myapi', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Show all API profiles')}`);
|
||||
console.log(` ${color('ccs api list', 'command')}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Main api command router
|
||||
*/
|
||||
export async function handleApiCommand(args: string[]): Promise<void> {
|
||||
const command = args[0];
|
||||
|
||||
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
||||
await showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'create':
|
||||
await handleCreate(args.slice(1));
|
||||
break;
|
||||
case 'list':
|
||||
await handleList();
|
||||
break;
|
||||
case 'remove':
|
||||
case 'delete':
|
||||
case 'rm':
|
||||
await handleRemove(args.slice(1));
|
||||
break;
|
||||
default:
|
||||
await initUI();
|
||||
console.log(fail(`Unknown command: ${command}`));
|
||||
console.log('');
|
||||
console.log('Run for help:');
|
||||
console.log(` ${color('ccs api --help', 'command')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
+204
-107
@@ -1,142 +1,239 @@
|
||||
import { colored } from '../utils/helpers';
|
||||
import { initUI, box, color, dim, sectionHeader, subheader } from '../utils/ui';
|
||||
|
||||
// Version is read from VERSION file during build
|
||||
const VERSION = '5.3.0';
|
||||
|
||||
/**
|
||||
* Print a major section with ═══ borders (only for 3 main sections)
|
||||
* Format:
|
||||
* ═══ TITLE ═══
|
||||
* Subtitle line 1
|
||||
* Subtitle line 2
|
||||
*
|
||||
* command Description
|
||||
*/
|
||||
function printMajorSection(title: string, subtitles: string[], items: [string, string][]): void {
|
||||
// Section header with ═══ borders
|
||||
console.log(sectionHeader(title));
|
||||
|
||||
// Subtitles on separate lines (dim)
|
||||
for (const subtitle of subtitles) {
|
||||
console.log(` ${dim(subtitle)}`);
|
||||
}
|
||||
|
||||
// Empty line before items
|
||||
console.log('');
|
||||
|
||||
// Calculate max command length for alignment
|
||||
const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length));
|
||||
|
||||
for (const [cmd, desc] of items) {
|
||||
const paddedCmd = cmd.padEnd(maxCmdLen + 2);
|
||||
console.log(` ${color(paddedCmd, 'command')} ${desc}`);
|
||||
}
|
||||
|
||||
// Extra spacing after section
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a sub-section with colored title
|
||||
* Format:
|
||||
* Title (context):
|
||||
* command Description
|
||||
*/
|
||||
function printSubSection(title: string, items: [string, string][]): void {
|
||||
// Sub-section header (colored, no borders)
|
||||
console.log(subheader(`${title}:`));
|
||||
|
||||
// Calculate max command length for alignment
|
||||
const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length));
|
||||
|
||||
for (const [cmd, desc] of items) {
|
||||
const paddedCmd = cmd.padEnd(maxCmdLen + 2);
|
||||
console.log(` ${color(paddedCmd, 'command')} ${desc}`);
|
||||
}
|
||||
|
||||
// Spacing after section
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a config/paths section
|
||||
* Format:
|
||||
* Title:
|
||||
* Label: path
|
||||
*/
|
||||
function printConfigSection(title: string, items: [string, string][]): void {
|
||||
console.log(subheader(`${title}:`));
|
||||
|
||||
// Calculate max label length for alignment
|
||||
const maxLabelLen = Math.max(...items.map(([label]) => label.length));
|
||||
|
||||
for (const [label, path] of items) {
|
||||
const paddedLabel = label.padEnd(maxLabelLen);
|
||||
console.log(` ${paddedLabel} ${color(path, 'path')}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display comprehensive help information for CCS (Claude Code Switch)
|
||||
*/
|
||||
export function handleHelpCommand(): void {
|
||||
console.log(colored('CCS (Claude Code Switch) - Profile switching for Claude CLI', 'bold'));
|
||||
export async function handleHelpCommand(): Promise<void> {
|
||||
// Initialize UI (if not already)
|
||||
await initUI();
|
||||
|
||||
// Hero box with ASCII art logo
|
||||
// Each letter: C=╔═╗/║ /╚═╝, C=╔═╗/║ /╚═╝, S=╔═╗/╚═╗/╚═╝
|
||||
const logo = `
|
||||
╔═╗ ╔═╗ ╔═╗
|
||||
║ ║ ╚═╗ v${VERSION}
|
||||
╚═╝ ╚═╝ ╚═╝
|
||||
|
||||
Claude Code Profile & Model Switcher`.trim();
|
||||
|
||||
console.log(
|
||||
box(logo, {
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
titleAlignment: 'center',
|
||||
})
|
||||
);
|
||||
console.log('');
|
||||
|
||||
console.log(colored('Usage:', 'cyan'));
|
||||
console.log(` ${colored('ccs', 'yellow')} [profile] [claude-args...]`);
|
||||
console.log(` ${colored('ccs', 'yellow')} [flags]`);
|
||||
// Usage section
|
||||
console.log(subheader('Usage:'));
|
||||
console.log(` ${color('ccs', 'command')} [profile] [claude-args...]`);
|
||||
console.log(` ${color('ccs', 'command')} [flags]`);
|
||||
console.log('');
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SECTION 1: API KEY MODELS + PROFILE MANAGEMENT
|
||||
// MAJOR SECTION 1: API Key Profiles
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log(colored('═══ API Key Profiles ═══', 'cyanBold'));
|
||||
console.log(' Configure in ~/.ccs/*.settings.json');
|
||||
console.log('');
|
||||
console.log(` ${colored('ccs', 'yellow')} Use default Claude account`);
|
||||
console.log(` ${colored('ccs glm', 'yellow')} GLM 4.6 (API key required)`);
|
||||
console.log(` ${colored('ccs glmt', 'yellow')} GLM with thinking mode`);
|
||||
console.log(` ${colored('ccs kimi', 'yellow')} Kimi for Coding (API key)`);
|
||||
console.log(` ${colored('ccs profile create', 'yellow')} Create custom API profile`);
|
||||
console.log(` ${colored('ccs profile remove', 'yellow')} Remove a profile`);
|
||||
console.log(` ${colored('ccs profile list', 'yellow')} List all profiles`);
|
||||
console.log('');
|
||||
printMajorSection(
|
||||
'API Key Profiles',
|
||||
['Configure in ~/.ccs/*.settings.json'],
|
||||
[
|
||||
['ccs', 'Use default Claude account'],
|
||||
['ccs glm', 'GLM 4.6 (API key required)'],
|
||||
['ccs glmt', 'GLM with thinking mode'],
|
||||
['ccs kimi', 'Kimi for Coding (API key)'],
|
||||
['', ''], // Spacer
|
||||
['ccs api create', 'Create custom API profile'],
|
||||
['ccs api remove', 'Remove an API profile'],
|
||||
['ccs api list', 'List all API profiles'],
|
||||
]
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SECTION 2: ACCOUNT MANAGEMENT
|
||||
// MAJOR SECTION 2: Account Management
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log(colored('═══ Account Management ═══', 'cyanBold'));
|
||||
console.log(' Run multiple Claude accounts concurrently');
|
||||
console.log('');
|
||||
console.log(
|
||||
` ${colored('ccs auth --help', 'yellow')} Show account management commands`
|
||||
printMajorSection(
|
||||
'Account Management',
|
||||
['Run multiple Claude accounts concurrently'],
|
||||
[
|
||||
['ccs auth --help', 'Show account management commands'],
|
||||
['ccs auth create <name>', 'Create new account profile'],
|
||||
['ccs auth list', 'List all account profiles'],
|
||||
]
|
||||
);
|
||||
console.log(` ${colored('ccs auth create <name>', 'yellow')} Create new account profile`);
|
||||
console.log(` ${colored('ccs auth list', 'yellow')} List all account profiles`);
|
||||
console.log('');
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SECTION 3: CLI PROXY (OAUTH PROVIDERS)
|
||||
// MAJOR SECTION 3: CLI Proxy (OAuth Providers)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log(colored('═══ CLI Proxy (OAuth Providers) ═══', 'cyanBold'));
|
||||
console.log(' Zero-config OAuth authentication via CLIProxyAPI');
|
||||
console.log(' First run: Browser opens for authentication');
|
||||
console.log(' Settings: ~/.ccs/{provider}.settings.json (created after auth)');
|
||||
console.log('');
|
||||
console.log(
|
||||
` ${colored('ccs gemini', 'yellow')} Google Gemini (gemini-2.5-pro)`
|
||||
printMajorSection(
|
||||
'CLI Proxy (OAuth Providers)',
|
||||
[
|
||||
'Zero-config OAuth authentication via CLIProxyAPI',
|
||||
'First run: Browser opens for authentication',
|
||||
'Settings: ~/.ccs/{provider}.settings.json (created after auth)',
|
||||
],
|
||||
[
|
||||
['ccs gemini', 'Google Gemini (gemini-2.5-pro)'],
|
||||
['ccs codex', 'OpenAI Codex (gpt-5.1-codex-max)'],
|
||||
['ccs agy', 'Antigravity (gemini-3-pro-preview)'],
|
||||
['ccs qwen', 'Qwen Code (qwen3-coder)'],
|
||||
['', ''], // Spacer
|
||||
['ccs <provider> --auth', 'Authenticate only'],
|
||||
['ccs <provider> --logout', 'Clear authentication'],
|
||||
['ccs <provider> --headless', 'Headless auth (for SSH)'],
|
||||
['ccs codex "explain code"', 'Use with prompt'],
|
||||
]
|
||||
);
|
||||
console.log(
|
||||
` ${colored('ccs codex', 'yellow')} OpenAI Codex (gpt-5.1-codex-max)`
|
||||
);
|
||||
console.log(
|
||||
` ${colored('ccs agy', 'yellow')} Antigravity (gemini-3-pro-preview)`
|
||||
);
|
||||
console.log(` ${colored('ccs qwen', 'yellow')} Qwen Code (qwen3-coder)`);
|
||||
console.log('');
|
||||
console.log(` ${colored('ccs <provider> --auth', 'yellow')} Authenticate only`);
|
||||
console.log(` ${colored('ccs <provider> --logout', 'yellow')} Clear authentication`);
|
||||
console.log(` ${colored('ccs <provider> --headless', 'yellow')} Headless auth (for SSH)`);
|
||||
console.log(` ${colored('ccs codex "explain code"', 'yellow')} Use with prompt`);
|
||||
console.log('');
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// DELEGATION
|
||||
// SUB-SECTIONS (simpler styling)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log(colored('Delegation (inside Claude Code CLI):', 'cyan'));
|
||||
console.log(
|
||||
` ${colored('/ccs "task"', 'yellow')} Delegate task (auto-selects profile)`
|
||||
);
|
||||
console.log(
|
||||
` ${colored('/ccs --glm "task"', 'yellow')} Force GLM-4.6 for simple tasks`
|
||||
);
|
||||
console.log(` ${colored('/ccs --kimi "task"', 'yellow')} Force Kimi for long context`);
|
||||
console.log(
|
||||
` ${colored('/ccs:continue "follow-up"', 'yellow')} Continue last delegation session`
|
||||
);
|
||||
|
||||
// Delegation
|
||||
printSubSection('Delegation (inside Claude Code CLI)', [
|
||||
['/ccs "task"', 'Delegate task (auto-selects profile)'],
|
||||
['/ccs --glm "task"', 'Force GLM-4.6 for simple tasks'],
|
||||
['/ccs --kimi "task"', 'Force Kimi for long context'],
|
||||
['/ccs:continue "follow-up"', 'Continue last delegation session'],
|
||||
]);
|
||||
|
||||
// Diagnostics
|
||||
printSubSection('Diagnostics', [
|
||||
['ccs doctor', 'Run health check and diagnostics'],
|
||||
['ccs sync', 'Sync delegation commands and skills'],
|
||||
['ccs update', 'Update CCS to latest version'],
|
||||
]);
|
||||
|
||||
// Flags
|
||||
printSubSection('Flags', [
|
||||
['-h, --help', 'Show this help message'],
|
||||
['-v, --version', 'Show version and installation info'],
|
||||
['-sc, --shell-completion', 'Install shell auto-completion'],
|
||||
]);
|
||||
|
||||
// Configuration
|
||||
printConfigSection('Configuration', [
|
||||
['Config File:', '~/.ccs/config.json'],
|
||||
['Profiles:', '~/.ccs/profiles.json'],
|
||||
['Instances:', '~/.ccs/instances/'],
|
||||
['Settings:', '~/.ccs/*.settings.json'],
|
||||
]);
|
||||
|
||||
// CLI Proxy paths
|
||||
console.log(subheader('CLI Proxy:'));
|
||||
console.log(` Binary: ${color('~/.ccs/cliproxy/bin/cli-proxy-api', 'path')}`);
|
||||
console.log(` Config: ${color('~/.ccs/cliproxy/config.yaml', 'path')}`);
|
||||
console.log(` Auth: ${color('~/.ccs/cliproxy/auth/', 'path')}`);
|
||||
console.log(` ${dim('Port: 8317 (default)')}`);
|
||||
console.log('');
|
||||
|
||||
console.log(colored('Diagnostics:', 'cyan'));
|
||||
// Shared Data
|
||||
console.log(subheader('Shared Data:'));
|
||||
console.log(` Commands: ${color('~/.ccs/shared/commands/', 'path')}`);
|
||||
console.log(` Skills: ${color('~/.ccs/shared/skills/', 'path')}`);
|
||||
console.log(` Agents: ${color('~/.ccs/shared/agents/', 'path')}`);
|
||||
console.log(` ${dim('Note: Symlinked across all profiles')}`);
|
||||
console.log('');
|
||||
|
||||
// Examples (aligned with consistent spacing)
|
||||
console.log(subheader('Examples:'));
|
||||
console.log(` $ ${color('ccs', 'command')} ${dim('# Use default account')}`);
|
||||
console.log(
|
||||
` ${colored('ccs doctor', 'yellow')} Run health check and diagnostics`
|
||||
` $ ${color('ccs gemini', 'command')} ${dim('# OAuth (browser opens first time)')}`
|
||||
);
|
||||
console.log(
|
||||
` ${colored('ccs sync', 'yellow')} Sync delegation commands and skills`
|
||||
);
|
||||
console.log(` ${colored('ccs update', 'yellow')} Update CCS to latest version`);
|
||||
console.log(` $ ${color('ccs glm "implement API"', 'command')} ${dim('# API key model')}`);
|
||||
console.log('');
|
||||
|
||||
console.log(colored('Flags:', 'cyan'));
|
||||
console.log(` ${colored('-h, --help', 'yellow')} Show this help message`);
|
||||
console.log(
|
||||
` ${colored('-v, --version', 'yellow')} Show version and installation info`
|
||||
);
|
||||
console.log(
|
||||
` ${colored('-sc, --shell-completion', 'yellow')} Install shell auto-completion`
|
||||
);
|
||||
// Docs link
|
||||
console.log(` ${dim('Docs: https://github.com/kaitranntt/ccs')}`);
|
||||
console.log('');
|
||||
|
||||
console.log(colored('Configuration:', 'cyan'));
|
||||
console.log(' Config File: ~/.ccs/config.json');
|
||||
console.log(' Profiles: ~/.ccs/profiles.json');
|
||||
console.log(' Instances: ~/.ccs/instances/');
|
||||
console.log(' Settings: ~/.ccs/*.settings.json');
|
||||
// Uninstall
|
||||
console.log(subheader('Uninstall:'));
|
||||
console.log(` ${color('npm uninstall -g @kaitranntt/ccs', 'command')}`);
|
||||
console.log('');
|
||||
|
||||
console.log(colored('CLI Proxy:', 'cyan'));
|
||||
console.log(' Binary: ~/.ccs/cliproxy/bin/cli-proxy-api');
|
||||
console.log(' Config: ~/.ccs/cliproxy/config.yaml');
|
||||
console.log(' Auth: ~/.ccs/cliproxy/auth/');
|
||||
console.log(' Port: 8317 (default)');
|
||||
// License
|
||||
console.log(dim('License: MIT'));
|
||||
console.log('');
|
||||
|
||||
console.log(colored('Shared Data:', 'cyan'));
|
||||
console.log(' Commands: ~/.ccs/shared/commands/');
|
||||
console.log(' Skills: ~/.ccs/shared/skills/');
|
||||
console.log(' Agents: ~/.ccs/shared/agents/');
|
||||
console.log(' Note: Symlinked across all profiles');
|
||||
console.log('');
|
||||
|
||||
console.log(colored('Examples:', 'cyan'));
|
||||
console.log(` ${colored('$ ccs', 'yellow')} # Use default account`);
|
||||
console.log(
|
||||
` ${colored('$ ccs gemini', 'yellow')} # OAuth (browser opens first time)`
|
||||
);
|
||||
console.log(` ${colored('$ ccs glm "implement API"', 'yellow')} # API key model`);
|
||||
console.log('');
|
||||
console.log(` Docs: ${colored('https://github.com/kaitranntt/ccs', 'cyan')}`);
|
||||
console.log('');
|
||||
|
||||
console.log(colored('Uninstall:', 'yellow'));
|
||||
console.log(' npm uninstall -g @kaitranntt/ccs');
|
||||
console.log('');
|
||||
|
||||
console.log(`${colored('License:', 'cyan')} MIT`);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -1,423 +0,0 @@
|
||||
/**
|
||||
* Profile Command Handler
|
||||
*
|
||||
* Manages CCS profiles for custom API providers.
|
||||
* Commands: create, list
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { colored } from '../utils/helpers';
|
||||
import { InteractivePrompt } from '../utils/prompt';
|
||||
import { getCcsDir, getConfigPath, loadConfig } from '../utils/config-manager';
|
||||
|
||||
interface ProfileCommandArgs {
|
||||
name?: string;
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
force?: boolean;
|
||||
yes?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments for profile commands
|
||||
*/
|
||||
function parseArgs(args: string[]): ProfileCommandArgs {
|
||||
const result: ProfileCommandArgs = {};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === '--base-url' && args[i + 1]) {
|
||||
result.baseUrl = args[++i];
|
||||
} else if (arg === '--api-key' && args[i + 1]) {
|
||||
result.apiKey = args[++i];
|
||||
} else if (arg === '--model' && args[i + 1]) {
|
||||
result.model = args[++i];
|
||||
} else if (arg === '--force') {
|
||||
result.force = true;
|
||||
} else if (arg === '--yes' || arg === '-y') {
|
||||
result.yes = true;
|
||||
} else if (!arg.startsWith('-') && !result.name) {
|
||||
result.name = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate profile name
|
||||
*/
|
||||
function validateProfileName(name: string): string | null {
|
||||
if (!name) {
|
||||
return 'Profile name is required';
|
||||
}
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9._-]*$/.test(name)) {
|
||||
return 'Profile name must start with letter, contain only letters, numbers, dot, dash, underscore';
|
||||
}
|
||||
if (name.length > 32) {
|
||||
return 'Profile name must be 32 characters or less';
|
||||
}
|
||||
// Reserved names
|
||||
const reserved = ['default', 'auth', 'profile', 'doctor', 'sync', 'update', 'help', 'version'];
|
||||
if (reserved.includes(name.toLowerCase())) {
|
||||
return `'${name}' is a reserved name`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate URL format
|
||||
*/
|
||||
function validateUrl(url: string): string | null {
|
||||
if (!url) {
|
||||
return 'Base URL is required';
|
||||
}
|
||||
try {
|
||||
new URL(url);
|
||||
return null;
|
||||
} catch {
|
||||
return 'Invalid URL format (must include protocol, e.g., https://)';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if profile already exists in config.json
|
||||
*/
|
||||
function profileExists(name: string): boolean {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
return name in config.profiles;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create settings.json file for profile
|
||||
*/
|
||||
function createSettingsFile(name: string, baseUrl: string, apiKey: string, model: string): string {
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${name}.settings.json`);
|
||||
|
||||
const settings = {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: baseUrl,
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
ANTHROPIC_MODEL: model,
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
||||
return settingsPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update config.json with new profile
|
||||
*/
|
||||
function updateConfig(name: string, _settingsPath: string): void {
|
||||
const configPath = getConfigPath();
|
||||
const ccsDir = getCcsDir();
|
||||
|
||||
// Read existing config or create new
|
||||
let config: { profiles: Record<string, string>; cliproxy?: Record<string, unknown> };
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} catch {
|
||||
config = { profiles: {} };
|
||||
}
|
||||
|
||||
// Use relative path with ~ for portability
|
||||
const relativePath = `~/.ccs/${name}.settings.json`;
|
||||
config.profiles[name] = relativePath;
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(ccsDir)) {
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write config atomically (write to temp, then rename)
|
||||
const tempPath = configPath + '.tmp';
|
||||
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tempPath, configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 'ccs profile create' command
|
||||
*/
|
||||
async function handleCreate(args: string[]): Promise<void> {
|
||||
const parsedArgs = parseArgs(args);
|
||||
|
||||
console.log(colored('Create API Profile', 'bold'));
|
||||
console.log('');
|
||||
|
||||
// Step 1: Profile name
|
||||
let name = parsedArgs.name;
|
||||
if (!name) {
|
||||
name = await InteractivePrompt.input('Profile name', {
|
||||
validate: validateProfileName,
|
||||
});
|
||||
} else {
|
||||
const error = validateProfileName(name);
|
||||
if (error) {
|
||||
console.error(`[X] ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if exists
|
||||
if (profileExists(name) && !parsedArgs.force) {
|
||||
console.error(`[X] Profile '${name}' already exists`);
|
||||
console.log(` Use ${colored('--force', 'yellow')} to overwrite`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 2: Base URL
|
||||
let baseUrl = parsedArgs.baseUrl;
|
||||
if (!baseUrl) {
|
||||
baseUrl = await InteractivePrompt.input('API Base URL (e.g., https://api.example.com)', {
|
||||
validate: validateUrl,
|
||||
});
|
||||
} else {
|
||||
const error = validateUrl(baseUrl);
|
||||
if (error) {
|
||||
console.error(`[X] ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: API Key
|
||||
let apiKey = parsedArgs.apiKey;
|
||||
if (!apiKey) {
|
||||
apiKey = await InteractivePrompt.password('API Key');
|
||||
if (!apiKey) {
|
||||
console.error('[X] API key is required');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Model (optional)
|
||||
const defaultModel = 'claude-sonnet-4-5-20250929';
|
||||
let model = parsedArgs.model;
|
||||
if (!model && !parsedArgs.yes) {
|
||||
model = await InteractivePrompt.input('Default model', {
|
||||
default: defaultModel,
|
||||
});
|
||||
}
|
||||
model = model || defaultModel;
|
||||
|
||||
// Create files
|
||||
console.log('');
|
||||
console.log('[i] Creating profile...');
|
||||
|
||||
try {
|
||||
const settingsPath = createSettingsFile(name, baseUrl, apiKey, model);
|
||||
updateConfig(name, settingsPath);
|
||||
|
||||
console.log(colored('[OK] Profile created successfully', 'green'));
|
||||
console.log('');
|
||||
console.log(` Profile: ${name}`);
|
||||
console.log(` Settings: ~/.ccs/${name}.settings.json`);
|
||||
console.log(` Base URL: ${baseUrl}`);
|
||||
console.log(` Model: ${model}`);
|
||||
console.log('');
|
||||
console.log(colored('Usage:', 'cyan'));
|
||||
console.log(` ${colored(`ccs ${name} "your prompt"`, 'yellow')}`);
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error(`[X] Failed to create profile: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 'ccs profile list' command
|
||||
*/
|
||||
async function handleList(): Promise<void> {
|
||||
console.log(colored('CCS Profiles', 'bold'));
|
||||
console.log('');
|
||||
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const profiles = Object.keys(config.profiles);
|
||||
|
||||
if (profiles.length === 0) {
|
||||
console.log(colored('No profiles configured', 'yellow'));
|
||||
console.log('');
|
||||
console.log('To create a profile:');
|
||||
console.log(` ${colored('ccs profile create', 'yellow')}`);
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(colored('Settings-based Profiles:', 'cyan'));
|
||||
// Calculate max name length for alignment
|
||||
const maxNameLen = Math.max(...profiles.map((n) => n.length));
|
||||
profiles.forEach((name) => {
|
||||
const settingsPath = config.profiles[name];
|
||||
const paddedName = name.padEnd(maxNameLen);
|
||||
console.log(` ${colored(paddedName, 'yellow')} ${settingsPath}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// Also show CLIProxy profiles if any
|
||||
if (config.cliproxy && Object.keys(config.cliproxy).length > 0) {
|
||||
console.log(colored('CLIProxy Variants:', 'cyan'));
|
||||
Object.entries(config.cliproxy).forEach(([name, variant]) => {
|
||||
const v = variant as { provider: string; settings: string };
|
||||
console.log(` ${colored(name, 'yellow')} → ${v.provider} (${v.settings})`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`Total: ${profiles.length} profile(s)`);
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error(`[X] Failed to list profiles: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 'ccs profile remove' command
|
||||
*/
|
||||
async function handleRemove(args: string[]): Promise<void> {
|
||||
const parsedArgs = parseArgs(args);
|
||||
|
||||
// Load config first to get available profiles
|
||||
let config: { profiles: Record<string, string>; cliproxy?: Record<string, unknown> };
|
||||
try {
|
||||
config = loadConfig();
|
||||
} catch {
|
||||
console.error('[X] Failed to load config');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const profiles = Object.keys(config.profiles);
|
||||
if (profiles.length === 0) {
|
||||
console.log(colored('No profiles to remove', 'yellow'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Interactive profile selection if not provided
|
||||
let name = parsedArgs.name;
|
||||
if (!name) {
|
||||
console.log(colored('Remove Profile', 'bold'));
|
||||
console.log('');
|
||||
console.log('Available profiles:');
|
||||
profiles.forEach((p, i) => console.log(` ${i + 1}. ${p}`));
|
||||
console.log('');
|
||||
|
||||
name = await InteractivePrompt.input('Profile name to remove', {
|
||||
validate: (val) => {
|
||||
if (!val) return 'Profile name is required';
|
||||
if (!profiles.includes(val)) return `Profile '${val}' not found`;
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!(name in config.profiles)) {
|
||||
console.error(`[X] Profile '${name}' not found`);
|
||||
console.log('');
|
||||
console.log('Available profiles:');
|
||||
profiles.forEach((p) => console.log(` - ${p}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const settingsPath = config.profiles[name];
|
||||
const expandedPath = path.join(getCcsDir(), `${name}.settings.json`);
|
||||
|
||||
// Confirm deletion
|
||||
console.log('');
|
||||
console.log(`Profile '${colored(name, 'yellow')}' will be removed.`);
|
||||
console.log(` Settings: ${settingsPath}`);
|
||||
console.log('');
|
||||
|
||||
const confirmed =
|
||||
parsedArgs.yes || (await InteractivePrompt.confirm('Delete this profile?', { default: false }));
|
||||
|
||||
if (!confirmed) {
|
||||
console.log('[i] Cancelled');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Remove from config.json
|
||||
delete config.profiles[name];
|
||||
const configPath = getConfigPath();
|
||||
const tempPath = configPath + '.tmp';
|
||||
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tempPath, configPath);
|
||||
|
||||
// Remove settings file if it exists
|
||||
if (fs.existsSync(expandedPath)) {
|
||||
fs.unlinkSync(expandedPath);
|
||||
}
|
||||
|
||||
console.log(colored('[OK] Profile removed', 'green'));
|
||||
console.log(` Profile: ${name}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show help for profile commands
|
||||
*/
|
||||
function showHelp(): void {
|
||||
console.log(colored('CCS Profile Management', 'bold'));
|
||||
console.log('');
|
||||
console.log(colored('Usage:', 'cyan'));
|
||||
console.log(` ${colored('ccs profile', 'yellow')} <command> [options]`);
|
||||
console.log('');
|
||||
console.log(colored('Commands:', 'cyan'));
|
||||
console.log(` ${colored('create [name]', 'yellow')} Create new API profile (interactive)`);
|
||||
console.log(` ${colored('list', 'yellow')} List all profiles`);
|
||||
console.log(` ${colored('remove <name>', 'yellow')} Remove a profile`);
|
||||
console.log('');
|
||||
console.log(colored('Options:', 'cyan'));
|
||||
console.log(` ${colored('--base-url <url>', 'yellow')} API base URL (create)`);
|
||||
console.log(` ${colored('--api-key <key>', 'yellow')} API key (create)`);
|
||||
console.log(` ${colored('--model <model>', 'yellow')} Default model (create)`);
|
||||
console.log(` ${colored('--force', 'yellow')} Overwrite existing (create)`);
|
||||
console.log(` ${colored('--yes, -y', 'yellow')} Skip confirmation prompts`);
|
||||
console.log('');
|
||||
console.log(colored('Examples:', 'cyan'));
|
||||
console.log(` ${colored('ccs profile create', 'yellow')} # Interactive wizard`);
|
||||
console.log(` ${colored('ccs profile create myapi', 'yellow')} # Create with name`);
|
||||
console.log(` ${colored('ccs profile remove myapi', 'yellow')} # Remove profile`);
|
||||
console.log(` ${colored('ccs profile list', 'yellow')} # Show all profiles`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Main profile command router
|
||||
*/
|
||||
export async function handleProfileCommand(args: string[]): Promise<void> {
|
||||
const command = args[0];
|
||||
|
||||
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
||||
showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'create':
|
||||
await handleCreate(args.slice(1));
|
||||
break;
|
||||
case 'list':
|
||||
await handleList();
|
||||
break;
|
||||
case 'remove':
|
||||
case 'delete':
|
||||
case 'rm':
|
||||
await handleRemove(args.slice(1));
|
||||
break;
|
||||
default:
|
||||
console.error(`[X] Unknown command: ${command}`);
|
||||
console.log('');
|
||||
console.log('Run for help:');
|
||||
console.log(` ${colored('ccs profile --help', 'yellow')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export class DelegationHandler {
|
||||
const result = await HeadlessExecutor.execute(parsed.profile, parsed.prompt, parsed.options);
|
||||
|
||||
// 5. Format and display results
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
console.log(formatted);
|
||||
|
||||
// 6. Exit with proper code
|
||||
@@ -83,7 +83,7 @@ export class DelegationHandler {
|
||||
sessionId: lastSession.sessionId,
|
||||
});
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
console.log(formatted);
|
||||
|
||||
process.exit(result.exitCode || 0);
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import { SessionManager } from './session-manager';
|
||||
import { SettingsParser } from './settings-parser';
|
||||
import { ui } from '../utils/ui';
|
||||
|
||||
// Type definitions for delegation responses
|
||||
interface ClaudeMessage {
|
||||
@@ -214,6 +215,9 @@ export class HeadlessExecutor {
|
||||
console.error(`[i] Claude CLI args: ${args.join(' ')}`);
|
||||
}
|
||||
|
||||
// Initialize UI before spawning
|
||||
await ui.init();
|
||||
|
||||
// Execute with spawn
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
@@ -225,7 +229,7 @@ export class HeadlessExecutor {
|
||||
if (showProgress) {
|
||||
const modelName =
|
||||
profile === 'glm' ? 'GLM-4.6' : profile === 'kimi' ? 'Kimi' : profile.toUpperCase();
|
||||
console.error(`[i] Delegating to ${modelName}...`);
|
||||
console.error(ui.info(`Delegating to ${modelName}...`));
|
||||
}
|
||||
|
||||
const proc = spawn(claudeCli, args, {
|
||||
@@ -274,7 +278,7 @@ export class HeadlessExecutor {
|
||||
if (showProgress) {
|
||||
progressInterval = setInterval(() => {
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
process.stderr.write(`[i] Still running... ${elapsed}s elapsed\r`);
|
||||
process.stderr.write(`${ui.info(`Still running... ${elapsed}s elapsed`)}\r`);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
@@ -445,9 +449,9 @@ export class HeadlessExecutor {
|
||||
if (showProgress) {
|
||||
const durationSec = (duration / 1000).toFixed(1);
|
||||
if (timedOut) {
|
||||
console.error(`[i] Execution timed out after ${durationSec}s`);
|
||||
console.error(ui.warn(`Execution timed out after ${durationSec}s`));
|
||||
} else {
|
||||
console.error(`[i] Execution completed in ${durationSec}s`);
|
||||
console.error(ui.info(`Execution completed in ${durationSec}s`));
|
||||
}
|
||||
console.error(''); // Blank line before formatted output
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
/**
|
||||
* Formats delegation execution results for display
|
||||
* Creates ASCII box output with file change tracking
|
||||
* Creates styled box output with file change tracking
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import { ui } from '../utils/ui';
|
||||
|
||||
interface ExecutionResult {
|
||||
profile: string;
|
||||
@@ -55,19 +56,15 @@ class ResultFormatter {
|
||||
/**
|
||||
* Format execution result with complete source-of-truth
|
||||
*/
|
||||
static format(result: ExecutionResult): string {
|
||||
static async format(result: ExecutionResult): Promise<string> {
|
||||
await ui.init();
|
||||
|
||||
const {
|
||||
profile,
|
||||
cwd,
|
||||
exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
duration,
|
||||
success,
|
||||
content,
|
||||
sessionId,
|
||||
totalCost,
|
||||
numTurns,
|
||||
subtype,
|
||||
permissionDenials,
|
||||
errors,
|
||||
@@ -90,15 +87,25 @@ class ResultFormatter {
|
||||
// Build formatted output
|
||||
let output = '';
|
||||
|
||||
// Header
|
||||
output += this.formatHeader(profile, success);
|
||||
// Header box
|
||||
const modelName = this.getModelDisplayName(profile);
|
||||
const headerIcon = success ? '[i]' : '[X]';
|
||||
output += ui.box(`${headerIcon} Delegated to ${modelName} (ccs:${profile})`, {
|
||||
borderStyle: 'round',
|
||||
padding: 0,
|
||||
});
|
||||
output += '\n\n';
|
||||
|
||||
// Info box (file detection handled by delegated session itself)
|
||||
output += this.formatInfoBox(cwd, profile, duration, exitCode, sessionId, totalCost, numTurns);
|
||||
// Info table
|
||||
output += this.formatInfoTable(result);
|
||||
output += '\n';
|
||||
|
||||
// Task output
|
||||
output += '\n';
|
||||
output += this.formatOutput(displayOutput);
|
||||
if (displayOutput?.trim()) {
|
||||
output += displayOutput.trim() + '\n';
|
||||
} else {
|
||||
output += ui.info('No output from delegated task') + '\n';
|
||||
}
|
||||
|
||||
// Permission denials if present
|
||||
if (permissionDenials && permissionDenials.length > 0) {
|
||||
@@ -115,12 +122,14 @@ class ResultFormatter {
|
||||
// Stderr if present
|
||||
if (stderr && stderr.trim()) {
|
||||
output += '\n';
|
||||
output += this.formatStderr(stderr);
|
||||
output += ui.warn('Stderr:') + '\n';
|
||||
output += stderr.trim() + '\n';
|
||||
}
|
||||
|
||||
// Footer
|
||||
output += '\n';
|
||||
output += this.formatFooter(success, duration);
|
||||
output += success ? ui.ok('Delegation completed') : ui.fail('Delegation failed');
|
||||
output += '\n';
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -242,107 +251,36 @@ class ResultFormatter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Format header with delegation indicator
|
||||
* Format info as table
|
||||
*/
|
||||
private static formatHeader(profile: string, success: boolean): string {
|
||||
const modelName = this.getModelDisplayName(profile);
|
||||
const icon = success ? '[i]' : '[X]';
|
||||
return `${icon} Delegated to ${modelName} (ccs:${profile})\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format info box with delegation details
|
||||
*/
|
||||
private static formatInfoBox(
|
||||
cwd: string,
|
||||
profile: string,
|
||||
duration: number,
|
||||
exitCode: number,
|
||||
sessionId?: string,
|
||||
totalCost?: number,
|
||||
numTurns?: number
|
||||
): string {
|
||||
private static formatInfoTable(result: ExecutionResult): string {
|
||||
const { cwd, profile, duration, exitCode, sessionId, totalCost, numTurns } = result;
|
||||
const modelName = this.getModelDisplayName(profile);
|
||||
const durationSec = (duration / 1000).toFixed(1);
|
||||
|
||||
// Calculate box width (fit longest line + padding)
|
||||
const maxWidth = 70;
|
||||
const cwdLine = `Working Directory: ${cwd}`;
|
||||
const boxWidth = Math.min(Math.max(cwdLine.length + 4, 50), maxWidth);
|
||||
|
||||
const lines: string[] = [
|
||||
`Working Directory: ${this.truncate(cwd, boxWidth - 22)}`,
|
||||
`Model: ${modelName}`,
|
||||
`Duration: ${durationSec}s`,
|
||||
`Exit Code: ${exitCode}`,
|
||||
const rows: string[][] = [
|
||||
['Working Dir', this.truncate(cwd, 40)],
|
||||
['Model', modelName],
|
||||
['Duration', `${durationSec}s`],
|
||||
['Exit Code', `${exitCode}`],
|
||||
];
|
||||
|
||||
// Add JSON-specific fields if available
|
||||
if (sessionId) {
|
||||
// Abbreviate session ID (Git-style first 8 chars) to prevent wrapping
|
||||
const shortId = sessionId.length > 8 ? sessionId.substring(0, 8) : sessionId;
|
||||
lines.push(`Session ID: ${shortId}`);
|
||||
rows.push(['Session', shortId]);
|
||||
}
|
||||
|
||||
if (totalCost !== undefined && totalCost !== null) {
|
||||
lines.push(`Cost: $${totalCost.toFixed(4)}`);
|
||||
rows.push(['Cost', `$${totalCost.toFixed(4)}`]);
|
||||
}
|
||||
|
||||
if (numTurns) {
|
||||
lines.push(`Turns: ${numTurns}`);
|
||||
rows.push(['Turns', `${numTurns}`]);
|
||||
}
|
||||
|
||||
let box = '';
|
||||
box += '╔' + '═'.repeat(boxWidth - 2) + '╗\n';
|
||||
|
||||
for (const line of lines) {
|
||||
const padding = boxWidth - line.length - 4;
|
||||
box += '║ ' + line + ' '.repeat(Math.max(0, padding)) + ' ║\n';
|
||||
}
|
||||
|
||||
box += '╚' + '═'.repeat(boxWidth - 2) + '╝';
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format task output
|
||||
*/
|
||||
private static formatOutput(output: string): string {
|
||||
if (!output || !output.trim()) {
|
||||
return '[i] No output from delegated task\n';
|
||||
}
|
||||
|
||||
return output.trim() + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format stderr output
|
||||
*/
|
||||
private static formatStderr(stderr: string): string {
|
||||
return `[!] Stderr:\n${stderr.trim()}\n\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file list (created or modified) - Currently unused
|
||||
*/
|
||||
/*
|
||||
private static formatFileList(label: string, files: string[]): string {
|
||||
let output = `[i] ${label} Files:\n`;
|
||||
|
||||
for (const file of files) {
|
||||
output += ` - ${file}\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format footer with completion status
|
||||
*/
|
||||
private static formatFooter(success: boolean, _duration: number): string {
|
||||
const icon = success ? '[OK]' : '[X]';
|
||||
const status = success ? 'Delegation completed' : 'Delegation failed';
|
||||
return `${icon} ${status}\n`;
|
||||
return ui.table(rows, {
|
||||
colWidths: [15, 45],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,10 +310,11 @@ class ResultFormatter {
|
||||
/**
|
||||
* Format minimal result (for quick tasks)
|
||||
*/
|
||||
static formatMinimal(result: ExecutionResult): string {
|
||||
static async formatMinimal(result: ExecutionResult): Promise<string> {
|
||||
await ui.init();
|
||||
const { profile, success, duration } = result;
|
||||
const modelName = this.getModelDisplayName(profile);
|
||||
const icon = success ? '[OK]' : '[X]';
|
||||
const icon = success ? ui.ok('') : ui.fail('');
|
||||
const durationSec = (duration / 1000).toFixed(1);
|
||||
|
||||
return `${icon} ${modelName} delegation ${success ? 'completed' : 'failed'} (${durationSec}s)\n`;
|
||||
@@ -384,8 +323,8 @@ class ResultFormatter {
|
||||
/**
|
||||
* Format verbose result (with full details)
|
||||
*/
|
||||
static formatVerbose(result: ExecutionResult): string {
|
||||
const basic = this.format(result);
|
||||
static async formatVerbose(result: ExecutionResult): Promise<string> {
|
||||
const basic = await this.format(result);
|
||||
|
||||
// Add additional debug info
|
||||
let verbose = basic;
|
||||
@@ -413,47 +352,50 @@ class ResultFormatter {
|
||||
/**
|
||||
* Format timeout error (session exceeded time limit)
|
||||
*/
|
||||
private static formatTimeoutError(result: ExecutionResult): string {
|
||||
const { profile, cwd, duration, sessionId, totalCost, numTurns, permissionDenials } = result;
|
||||
private static async formatTimeoutError(result: ExecutionResult): Promise<string> {
|
||||
await ui.init();
|
||||
|
||||
const { profile, duration, sessionId, totalCost, permissionDenials } = result;
|
||||
const modelName = this.getModelDisplayName(profile);
|
||||
const timeoutMin = (duration / 60000).toFixed(1);
|
||||
|
||||
let output = '';
|
||||
|
||||
// Header
|
||||
output += this.formatHeader(profile, false);
|
||||
|
||||
// Info box
|
||||
output += this.formatInfoBox(cwd, profile, duration, 0, sessionId, totalCost, numTurns);
|
||||
|
||||
// Timeout message
|
||||
// Error header
|
||||
output += ui.errorBox(
|
||||
`Execution Timeout\n\n` +
|
||||
`Delegation to ${modelName} exceeded time limit.\n` +
|
||||
`Session was gracefully terminated after ${timeoutMin} minutes.`,
|
||||
'TIMEOUT'
|
||||
);
|
||||
output += '\n';
|
||||
const timeoutMin = (duration / 60000).toFixed(1);
|
||||
output += `[!] Execution timed out after ${timeoutMin} minutes\n\n`;
|
||||
output += 'The delegated session exceeded its time limit before completing the task.\n';
|
||||
output += 'Session was gracefully terminated and saved for continuation.\n';
|
||||
|
||||
// Permission denials if present
|
||||
// Info table
|
||||
output += this.formatInfoTable(result);
|
||||
output += '\n';
|
||||
|
||||
// Permission denials
|
||||
if (permissionDenials && permissionDenials.length > 0) {
|
||||
output += '\n';
|
||||
output += ui.warn('Permission denials may have caused delays:') + '\n';
|
||||
output += this.formatPermissionDenials(permissionDenials);
|
||||
output += '\n';
|
||||
output += 'The task may require permissions that were denied.\n';
|
||||
output += 'Consider running with --permission-mode bypassPermissions or execute manually.\n';
|
||||
}
|
||||
|
||||
// Suggestions
|
||||
output += '\n';
|
||||
output += 'Suggestions:\n';
|
||||
output += ` - Continue session: ccs ${profile}:continue -p "finish the task"\n`;
|
||||
output += ` - Increase timeout: ccs ${profile} -p "task" --timeout ${duration * 2}\n`;
|
||||
output += ' - Break task into smaller steps\n';
|
||||
output += ' - Run task manually in main Claude session\n';
|
||||
output += ui.header('SUGGESTIONS') + '\n';
|
||||
output += ` Continue session:\n`;
|
||||
output += ` ${ui.color(`ccs ${profile}:continue "finish the task"`, 'command')}\n\n`;
|
||||
output += ` Increase timeout:\n`;
|
||||
output += ` ${ui.color(`ccs ${profile} --timeout ${Math.round((duration * 2) / 1000)}`, 'command')}\n\n`;
|
||||
output += ` Break into smaller tasks\n\n`;
|
||||
|
||||
output += '\n';
|
||||
// Abbreviate session ID (Git-style first 8 chars)
|
||||
const shortId = sessionId && sessionId.length > 8 ? sessionId.substring(0, 8) : sessionId;
|
||||
output += `[i] Session persisted with ID: ${shortId}\n`;
|
||||
// Session info
|
||||
if (sessionId) {
|
||||
const shortId = sessionId.length > 8 ? sessionId.substring(0, 8) : sessionId;
|
||||
output += ui.dim(`Session persisted: ${shortId}`) + '\n';
|
||||
}
|
||||
if (totalCost !== undefined && totalCost !== null) {
|
||||
output += `[i] Cost: $${totalCost.toFixed(4)}\n`;
|
||||
output += ui.dim(`Cost: $${totalCost.toFixed(4)}`) + '\n';
|
||||
}
|
||||
|
||||
return output;
|
||||
@@ -463,14 +405,13 @@ class ResultFormatter {
|
||||
* Format permission denials
|
||||
*/
|
||||
private static formatPermissionDenials(denials: PermissionDenial[]): string {
|
||||
let output = '[!] Permission Denials:\n';
|
||||
let output = ui.warn('Permission Denials:') + '\n';
|
||||
|
||||
for (const denial of denials) {
|
||||
const tool = denial.tool_name || 'Unknown';
|
||||
const input = denial.tool_input || {};
|
||||
const command = input.command || input.description || JSON.stringify(input);
|
||||
|
||||
output += ` - ${tool}: ${command}\n`;
|
||||
const cmd = input.command || input.description || JSON.stringify(input);
|
||||
output += ` - ${tool}: ${this.truncate(cmd, 50)}\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
@@ -480,11 +421,11 @@ class ResultFormatter {
|
||||
* Format errors array
|
||||
*/
|
||||
private static formatErrors(errors: ErrorInfo[]): string {
|
||||
let output = '[X] Errors:\n';
|
||||
let output = ui.fail('Errors:') + '\n';
|
||||
|
||||
for (const error of errors) {
|
||||
const message = error.message || error.error || JSON.stringify(error);
|
||||
output += ` - ${message}\n`;
|
||||
const msg = error.message || error.error || JSON.stringify(error);
|
||||
output += ` - ${msg}\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
|
||||
+141
-162
@@ -6,8 +6,8 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { spawn } from 'child_process';
|
||||
import { colored } from '../utils/helpers';
|
||||
import { detectClaudeCli } from '../utils/claude-detector';
|
||||
import { initUI, header, box, table, color, ok, fail, warn, info } from '../utils/ui';
|
||||
import packageJson from '../../package.json';
|
||||
import {
|
||||
isCLIProxyInstalled,
|
||||
@@ -50,9 +50,6 @@ try {
|
||||
};
|
||||
}
|
||||
|
||||
// Import cli-table3
|
||||
const Table = require('cli-table3');
|
||||
|
||||
interface HealthCheckDetails {
|
||||
status: 'OK' | 'ERROR' | 'WARN';
|
||||
info: string;
|
||||
@@ -133,40 +130,43 @@ class Doctor {
|
||||
* Run all health checks
|
||||
*/
|
||||
async runAllChecks(): Promise<HealthCheck> {
|
||||
console.log(colored('Running CCS Health Check...', 'cyan'));
|
||||
await initUI();
|
||||
|
||||
// Hero header box
|
||||
console.log(box(`CCS Health Check v${this.ccsVersion}`, { borderStyle: 'round', padding: 0 }));
|
||||
console.log('');
|
||||
|
||||
// Store CCS version in details
|
||||
this.results.details['CCS Version'] = { status: 'OK', info: `v${this.ccsVersion}` };
|
||||
|
||||
// Group 1: System
|
||||
console.log(colored('System:', 'bold'));
|
||||
console.log(header('SYSTEM'));
|
||||
await this.checkClaudeCli();
|
||||
this.checkCcsDirectory();
|
||||
console.log('');
|
||||
|
||||
// Group 2: Configuration
|
||||
console.log(colored('Configuration:', 'bold'));
|
||||
console.log(header('CONFIGURATION'));
|
||||
this.checkConfigFiles();
|
||||
this.checkClaudeSettings();
|
||||
console.log('');
|
||||
|
||||
// Group 3: Profiles & Delegation
|
||||
console.log(colored('Profiles & Delegation:', 'bold'));
|
||||
console.log(header('PROFILES & DELEGATION'));
|
||||
this.checkProfiles();
|
||||
this.checkInstances();
|
||||
this.checkDelegation();
|
||||
console.log('');
|
||||
|
||||
// Group 4: System Health
|
||||
console.log(colored('System Health:', 'bold'));
|
||||
console.log(header('SYSTEM HEALTH'));
|
||||
this.checkPermissions();
|
||||
this.checkCcsSymlinks();
|
||||
this.checkSettingsSymlinks();
|
||||
console.log('');
|
||||
|
||||
// Group 5: CLIProxy (OAuth profiles)
|
||||
console.log(colored('CLIProxy (OAuth Profiles):', 'bold'));
|
||||
console.log(header('CLIPROXY (OAUTH PROFILES)'));
|
||||
await this.checkCLIProxy();
|
||||
console.log('');
|
||||
|
||||
@@ -183,7 +183,8 @@ class Doctor {
|
||||
const claudeCli = detectClaudeCli();
|
||||
|
||||
if (!claudeCli) {
|
||||
spinner.fail(` ${'Claude CLI'.padEnd(26)}${colored('[X]', 'red')} Not found in PATH`);
|
||||
spinner.fail();
|
||||
console.log(` ${fail('Claude CLI'.padEnd(22))} Not found in PATH`);
|
||||
this.results.addCheck(
|
||||
'Claude CLI',
|
||||
'error',
|
||||
@@ -218,17 +219,15 @@ class Doctor {
|
||||
const versionMatch = result.match(/(\d+\.\d+\.\d+)/);
|
||||
const version = versionMatch ? versionMatch[1] : 'unknown';
|
||||
|
||||
spinner.succeed(
|
||||
` ${'Claude CLI'.padEnd(26)}${colored('[OK]', 'green')} ${claudeCli} (v${version})`
|
||||
);
|
||||
spinner.succeed();
|
||||
console.log(` ${ok('Claude CLI'.padEnd(22))} v${version} (${claudeCli})`);
|
||||
this.results.addCheck('Claude CLI', 'success', `Found: ${claudeCli}`, undefined, {
|
||||
status: 'OK',
|
||||
info: `v${version} (${claudeCli})`,
|
||||
});
|
||||
} catch (_err) {
|
||||
spinner.fail(
|
||||
` ${'Claude CLI'.padEnd(26)}${colored('[X]', 'red')} Not found or not working`
|
||||
);
|
||||
spinner.fail();
|
||||
console.log(` ${fail('Claude CLI'.padEnd(22))} Not found or not working`);
|
||||
this.results.addCheck(
|
||||
'Claude CLI',
|
||||
'error',
|
||||
@@ -246,13 +245,15 @@ class Doctor {
|
||||
const spinner = ora('Checking ~/.ccs/ directory').start();
|
||||
|
||||
if (fs.existsSync(this.ccsDir)) {
|
||||
spinner.succeed(` ${'CCS Directory'.padEnd(26)}${colored('[OK]', 'green')} ~/.ccs/`);
|
||||
spinner.succeed();
|
||||
console.log(` ${ok('CCS Directory'.padEnd(22))} ~/.ccs/`);
|
||||
this.results.addCheck('CCS Directory', 'success', undefined, undefined, {
|
||||
status: 'OK',
|
||||
info: '~/.ccs/',
|
||||
});
|
||||
} else {
|
||||
spinner.fail(` ${'CCS Directory'.padEnd(26)}${colored('[X]', 'red')} Not found`);
|
||||
spinner.fail();
|
||||
console.log(` ${fail('CCS Directory'.padEnd(22))} Not found`);
|
||||
this.results.addCheck(
|
||||
'CCS Directory',
|
||||
'error',
|
||||
@@ -289,7 +290,8 @@ class Doctor {
|
||||
const spinner = ora(`Checking ${file.name}`).start();
|
||||
|
||||
if (!fs.existsSync(file.path)) {
|
||||
spinner.fail(` ${file.name.padEnd(26)}${colored('[X]', 'red')} Not found`);
|
||||
spinner.fail();
|
||||
console.log(` ${fail(file.name.padEnd(22))} Not found`);
|
||||
this.results.addCheck(
|
||||
file.name,
|
||||
'error',
|
||||
@@ -306,7 +308,7 @@ class Doctor {
|
||||
JSON.parse(content);
|
||||
|
||||
// Extract useful info based on file type
|
||||
let info = 'Valid';
|
||||
let fileInfo = 'Valid';
|
||||
let status: 'OK' | 'WARN' = 'OK';
|
||||
|
||||
if (file.profile) {
|
||||
@@ -314,23 +316,23 @@ class Doctor {
|
||||
const validation = DelegationValidator.validate(file.profile);
|
||||
|
||||
if (validation.valid) {
|
||||
info = 'Key configured';
|
||||
fileInfo = 'Key configured';
|
||||
status = 'OK';
|
||||
} else if (validation.error && validation.error.includes('placeholder')) {
|
||||
info = 'Placeholder key (not configured)';
|
||||
fileInfo = 'Placeholder key';
|
||||
status = 'WARN';
|
||||
} else {
|
||||
info = 'Valid JSON';
|
||||
fileInfo = 'Valid JSON';
|
||||
status = 'OK';
|
||||
}
|
||||
}
|
||||
|
||||
const statusIcon = status === 'OK' ? colored('[OK]', 'green') : colored('[!]', 'yellow');
|
||||
|
||||
if (status === 'WARN') {
|
||||
spinner.warn(` ${file.name.padEnd(26)}${statusIcon} ${info}`);
|
||||
spinner.warn();
|
||||
console.log(` ${warn(file.name.padEnd(22))} ${fileInfo}`);
|
||||
} else {
|
||||
spinner.succeed(` ${file.name.padEnd(26)}${statusIcon} ${info}`);
|
||||
spinner.succeed();
|
||||
console.log(` ${ok(file.name.padEnd(22))} ${fileInfo}`);
|
||||
}
|
||||
|
||||
this.results.addCheck(
|
||||
@@ -340,11 +342,12 @@ class Doctor {
|
||||
undefined,
|
||||
{
|
||||
status: status,
|
||||
info: info,
|
||||
info: fileInfo,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
spinner.fail(` ${file.name.padEnd(26)}${colored('[X]', 'red')} Invalid JSON`);
|
||||
spinner.fail();
|
||||
console.log(` ${fail(file.name.padEnd(22))} Invalid JSON`);
|
||||
this.results.addCheck(
|
||||
file.name,
|
||||
'error',
|
||||
@@ -362,11 +365,11 @@ class Doctor {
|
||||
private checkClaudeSettings(): void {
|
||||
const spinner = ora('Checking ~/.claude/settings.json').start();
|
||||
const settingsPath = path.join(this.claudeDir, 'settings.json');
|
||||
const settingsName = '~/.claude/settings.json';
|
||||
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
spinner.warn(
|
||||
` ${'~/.claude/settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Not found`
|
||||
);
|
||||
spinner.warn();
|
||||
console.log(` ${warn(settingsName.padEnd(22))} Not found`);
|
||||
this.results.addCheck(
|
||||
'Claude Settings',
|
||||
'warning',
|
||||
@@ -380,12 +383,12 @@ class Doctor {
|
||||
try {
|
||||
const content = fs.readFileSync(settingsPath, 'utf8');
|
||||
JSON.parse(content);
|
||||
spinner.succeed(` ${'~/.claude/settings.json'.padEnd(26)}${colored('[OK]', 'green')}`);
|
||||
spinner.succeed();
|
||||
console.log(` ${ok(settingsName.padEnd(22))} Valid`);
|
||||
this.results.addCheck('Claude Settings', 'success');
|
||||
} catch (e) {
|
||||
spinner.warn(
|
||||
` ${'~/.claude/settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Invalid JSON`
|
||||
);
|
||||
spinner.warn();
|
||||
console.log(` ${warn(settingsName.padEnd(22))} Invalid JSON`);
|
||||
this.results.addCheck(
|
||||
'Claude Settings',
|
||||
'warning',
|
||||
@@ -403,7 +406,8 @@ class Doctor {
|
||||
const configPath = path.join(this.ccsDir, 'config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
spinner.info(` ${'Profiles'.padEnd(26)}${colored('[SKIP]', 'cyan')} config.json not found`);
|
||||
spinner.info();
|
||||
console.log(` ${info('Profiles'.padEnd(22))} config.json not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -411,7 +415,8 @@ class Doctor {
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
|
||||
if (!config.profiles || typeof config.profiles !== 'object') {
|
||||
spinner.fail(` ${'Profiles'.padEnd(26)}${colored('[X]', 'red')} Missing profiles object`);
|
||||
spinner.fail();
|
||||
console.log(` ${fail('Profiles'.padEnd(22))} Missing profiles object`);
|
||||
this.results.addCheck(
|
||||
'Profiles',
|
||||
'error',
|
||||
@@ -425,9 +430,8 @@ class Doctor {
|
||||
const profileCount = Object.keys(config.profiles).length;
|
||||
const profileNames = Object.keys(config.profiles).join(', ');
|
||||
|
||||
spinner.succeed(
|
||||
` ${'Profiles'.padEnd(26)}${colored('[OK]', 'green')} ${profileCount} configured (${profileNames})`
|
||||
);
|
||||
spinner.succeed();
|
||||
console.log(` ${ok('Profiles'.padEnd(22))} ${profileCount} configured (${profileNames})`);
|
||||
this.results.addCheck(
|
||||
'Profiles',
|
||||
'success',
|
||||
@@ -439,7 +443,8 @@ class Doctor {
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
spinner.fail(` ${'Profiles'.padEnd(26)}${colored('[X]', 'red')} ${(e as Error).message}`);
|
||||
spinner.fail();
|
||||
console.log(` ${fail('Profiles'.padEnd(22))} ${(e as Error).message}`);
|
||||
this.results.addCheck('Profiles', 'error', (e as Error).message, undefined, {
|
||||
status: 'ERROR',
|
||||
info: (e as Error).message,
|
||||
@@ -455,7 +460,8 @@ class Doctor {
|
||||
const instancesDir = path.join(this.ccsDir, 'instances');
|
||||
|
||||
if (!fs.existsSync(instancesDir)) {
|
||||
spinner.info(` ${'Instances'.padEnd(26)}${colored('[i]', 'cyan')} No account profiles`);
|
||||
spinner.info();
|
||||
console.log(` ${info('Instances'.padEnd(22))} No account profiles`);
|
||||
this.results.addCheck('Instances', 'success', 'No account profiles configured');
|
||||
return;
|
||||
}
|
||||
@@ -465,14 +471,14 @@ class Doctor {
|
||||
});
|
||||
|
||||
if (instances.length === 0) {
|
||||
spinner.info(` ${'Instances'.padEnd(26)}${colored('[i]', 'cyan')} No account profiles`);
|
||||
spinner.info();
|
||||
console.log(` ${info('Instances'.padEnd(22))} No account profiles`);
|
||||
this.results.addCheck('Instances', 'success', 'No account profiles');
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.succeed(
|
||||
` ${'Instances'.padEnd(26)}${colored('[OK]', 'green')} ${instances.length} account profiles`
|
||||
);
|
||||
spinner.succeed();
|
||||
console.log(` ${ok('Instances'.padEnd(22))} ${instances.length} account profiles`);
|
||||
this.results.addCheck('Instances', 'success', `${instances.length} account profiles`);
|
||||
}
|
||||
|
||||
@@ -488,7 +494,8 @@ class Doctor {
|
||||
const hasContinueCommand = fs.existsSync(path.join(ccsClaudeCommandsDir, 'ccs', 'continue.md'));
|
||||
|
||||
if (!hasCcsCommand || !hasContinueCommand) {
|
||||
spinner.warn(` ${'Delegation'.padEnd(26)}${colored('[!]', 'yellow')} Not installed`);
|
||||
spinner.warn();
|
||||
console.log(` ${warn('Delegation'.padEnd(22))} Not installed`);
|
||||
this.results.addCheck(
|
||||
'Delegation',
|
||||
'warning',
|
||||
@@ -511,7 +518,8 @@ class Doctor {
|
||||
}
|
||||
|
||||
if (readyProfiles.length === 0) {
|
||||
spinner.warn(` ${'Delegation'.padEnd(26)}${colored('[!]', 'yellow')} No profiles ready`);
|
||||
spinner.warn();
|
||||
console.log(` ${warn('Delegation'.padEnd(22))} No profiles ready`);
|
||||
this.results.addCheck(
|
||||
'Delegation',
|
||||
'warning',
|
||||
@@ -522,8 +530,9 @@ class Doctor {
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.succeed(
|
||||
` ${'Delegation'.padEnd(26)}${colored('[OK]', 'green')} ${readyProfiles.length} profiles ready (${readyProfiles.join(', ')})`
|
||||
spinner.succeed();
|
||||
console.log(
|
||||
` ${ok('Delegation'.padEnd(22))} ${readyProfiles.length} profiles ready (${readyProfiles.join(', ')})`
|
||||
);
|
||||
this.results.addCheck(
|
||||
'Delegation',
|
||||
@@ -544,17 +553,15 @@ class Doctor {
|
||||
try {
|
||||
fs.writeFileSync(testFile, 'test', 'utf8');
|
||||
fs.unlinkSync(testFile);
|
||||
spinner.succeed(
|
||||
` ${'Permissions'.padEnd(26)}${colored('[OK]', 'green')} Write access verified`
|
||||
);
|
||||
spinner.succeed();
|
||||
console.log(` ${ok('Permissions'.padEnd(22))} Write access verified`);
|
||||
this.results.addCheck('Permissions', 'success', undefined, undefined, {
|
||||
status: 'OK',
|
||||
info: 'Write access verified',
|
||||
});
|
||||
} catch (_e) {
|
||||
spinner.fail(
|
||||
` ${'Permissions'.padEnd(26)}${colored('[X]', 'red')} Cannot write to ~/.ccs/`
|
||||
);
|
||||
spinner.fail();
|
||||
console.log(` ${fail('Permissions'.padEnd(22))} Cannot write to ~/.ccs/`);
|
||||
this.results.addCheck(
|
||||
'Permissions',
|
||||
'error',
|
||||
@@ -578,9 +585,8 @@ class Doctor {
|
||||
|
||||
if (health.healthy) {
|
||||
const itemCount = manager.ccsItems.length;
|
||||
spinner.succeed(
|
||||
` ${'CCS Symlinks'.padEnd(26)}${colored('[OK]', 'green')} ${itemCount}/${itemCount} items linked`
|
||||
);
|
||||
spinner.succeed();
|
||||
console.log(` ${ok('CCS Symlinks'.padEnd(22))} ${itemCount}/${itemCount} items linked`);
|
||||
this.results.addCheck(
|
||||
'CCS Symlinks',
|
||||
'success',
|
||||
@@ -592,9 +598,8 @@ class Doctor {
|
||||
}
|
||||
);
|
||||
} else {
|
||||
spinner.warn(
|
||||
` ${'CCS Symlinks'.padEnd(26)}${colored('[!]', 'yellow')} ${health.issues.length} issues found`
|
||||
);
|
||||
spinner.warn();
|
||||
console.log(` ${warn('CCS Symlinks'.padEnd(22))} ${health.issues.length} issues found`);
|
||||
this.results.addCheck(
|
||||
'CCS Symlinks',
|
||||
'warning',
|
||||
@@ -604,7 +609,8 @@ class Doctor {
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
spinner.warn(` ${'CCS Symlinks'.padEnd(26)}${colored('[!]', 'yellow')} Could not check`);
|
||||
spinner.warn();
|
||||
console.log(` ${warn('CCS Symlinks'.padEnd(22))} Could not check`);
|
||||
this.results.addCheck(
|
||||
'CCS Symlinks',
|
||||
'warning',
|
||||
@@ -620,6 +626,7 @@ class Doctor {
|
||||
*/
|
||||
private checkSettingsSymlinks(): void {
|
||||
const spinner = ora('Checking settings.json symlinks').start();
|
||||
const settingsLabel = 'settings.json';
|
||||
|
||||
try {
|
||||
const sharedDir = path.join(this.homedir, '.ccs', 'shared');
|
||||
@@ -628,9 +635,8 @@ class Doctor {
|
||||
|
||||
// Check shared settings exists and points to ~/.claude/
|
||||
if (!fs.existsSync(sharedSettings)) {
|
||||
spinner.warn(
|
||||
` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Not found`
|
||||
);
|
||||
spinner.warn();
|
||||
console.log(` ${warn(settingsLabel.padEnd(22))} Not found (shared)`);
|
||||
this.results.addCheck(
|
||||
'Settings Symlinks',
|
||||
'warning',
|
||||
@@ -642,9 +648,8 @@ class Doctor {
|
||||
|
||||
const sharedStats = fs.lstatSync(sharedSettings);
|
||||
if (!sharedStats.isSymbolicLink()) {
|
||||
spinner.warn(
|
||||
` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Not a symlink`
|
||||
);
|
||||
spinner.warn();
|
||||
console.log(` ${warn(settingsLabel.padEnd(22))} Not a symlink (shared)`);
|
||||
this.results.addCheck(
|
||||
'Settings Symlinks',
|
||||
'warning',
|
||||
@@ -658,9 +663,8 @@ class Doctor {
|
||||
const resolvedShared = path.resolve(path.dirname(sharedSettings), sharedTarget);
|
||||
|
||||
if (resolvedShared !== claudeSettings) {
|
||||
spinner.warn(
|
||||
` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Wrong target`
|
||||
);
|
||||
spinner.warn();
|
||||
console.log(` ${warn(settingsLabel.padEnd(22))} Wrong target (shared)`);
|
||||
this.results.addCheck(
|
||||
'Settings Symlinks',
|
||||
'warning',
|
||||
@@ -673,9 +677,8 @@ class Doctor {
|
||||
// Check each instance
|
||||
const instancesDir = path.join(this.ccsDir, 'instances');
|
||||
if (!fs.existsSync(instancesDir)) {
|
||||
spinner.succeed(
|
||||
` ${'settings.json'.padEnd(26)}${colored('[OK]', 'green')} Shared symlink valid`
|
||||
);
|
||||
spinner.succeed();
|
||||
console.log(` ${ok(settingsLabel.padEnd(22))} Shared symlink valid`);
|
||||
this.results.addCheck('Settings Symlinks', 'success', 'Shared symlink valid', undefined, {
|
||||
status: 'OK',
|
||||
info: 'Shared symlink valid',
|
||||
@@ -716,9 +719,8 @@ class Doctor {
|
||||
}
|
||||
|
||||
if (broken > 0) {
|
||||
spinner.warn(
|
||||
` ${'settings.json'.padEnd(26)}${colored('[!]', 'yellow')} ${broken} broken instance(s)`
|
||||
);
|
||||
spinner.warn();
|
||||
console.log(` ${warn(settingsLabel.padEnd(22))} ${broken} broken instance(s)`);
|
||||
this.results.addCheck(
|
||||
'Settings Symlinks',
|
||||
'warning',
|
||||
@@ -727,9 +729,8 @@ class Doctor {
|
||||
{ status: 'WARN', info: `${broken} broken instance(s)` }
|
||||
);
|
||||
} else {
|
||||
spinner.succeed(
|
||||
` ${'settings.json'.padEnd(26)}${colored('[OK]', 'green')} ${instances.length} instance(s) valid`
|
||||
);
|
||||
spinner.succeed();
|
||||
console.log(` ${ok(settingsLabel.padEnd(22))} ${instances.length} instance(s) valid`);
|
||||
this.results.addCheck(
|
||||
'Settings Symlinks',
|
||||
'success',
|
||||
@@ -742,7 +743,8 @@ class Doctor {
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
spinner.warn(` ${'settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Check failed`);
|
||||
spinner.warn();
|
||||
console.log(` ${warn(settingsLabel.padEnd(22))} Check failed`);
|
||||
this.results.addCheck(
|
||||
'Settings Symlinks',
|
||||
'warning',
|
||||
@@ -762,16 +764,16 @@ class Doctor {
|
||||
|
||||
if (isCLIProxyInstalled()) {
|
||||
const binaryPath = getCLIProxyPath();
|
||||
binarySpinner.succeed(
|
||||
` ${'CLIProxy Binary'.padEnd(26)}${colored('[OK]', 'green')} v${CLIPROXY_VERSION}`
|
||||
);
|
||||
binarySpinner.succeed();
|
||||
console.log(` ${ok('CLIProxy Binary'.padEnd(22))} v${CLIPROXY_VERSION}`);
|
||||
this.results.addCheck('CLIProxy Binary', 'success', undefined, undefined, {
|
||||
status: 'OK',
|
||||
info: `v${CLIPROXY_VERSION} (${binaryPath})`,
|
||||
});
|
||||
} else {
|
||||
binarySpinner.info(
|
||||
` ${'CLIProxy Binary'.padEnd(26)}${colored('[i]', 'cyan')} Not installed (downloads on first use)`
|
||||
binarySpinner.info();
|
||||
console.log(
|
||||
` ${info('CLIProxy Binary'.padEnd(22))} Not installed (downloads on first use)`
|
||||
);
|
||||
this.results.addCheck(
|
||||
'CLIProxy Binary',
|
||||
@@ -787,17 +789,15 @@ class Doctor {
|
||||
const configPath = getConfigPath();
|
||||
|
||||
if (fs.existsSync(configPath)) {
|
||||
configSpinner.succeed(
|
||||
` ${'CLIProxy Config'.padEnd(26)}${colored('[OK]', 'green')} cliproxy/config.yaml`
|
||||
);
|
||||
configSpinner.succeed();
|
||||
console.log(` ${ok('CLIProxy Config'.padEnd(22))} cliproxy/config.yaml`);
|
||||
this.results.addCheck('CLIProxy Config', 'success', undefined, undefined, {
|
||||
status: 'OK',
|
||||
info: 'cliproxy/config.yaml',
|
||||
});
|
||||
} else {
|
||||
configSpinner.info(
|
||||
` ${'CLIProxy Config'.padEnd(26)}${colored('[i]', 'cyan')} Not created (generated on first use)`
|
||||
);
|
||||
configSpinner.info();
|
||||
console.log(` ${info('CLIProxy Config'.padEnd(22))} Not created (on first use)`);
|
||||
this.results.addCheck('CLIProxy Config', 'success', 'Not created yet', undefined, {
|
||||
status: 'OK',
|
||||
info: 'Generated on first use',
|
||||
@@ -812,17 +812,15 @@ class Doctor {
|
||||
|
||||
if (status.authenticated) {
|
||||
const lastAuth = status.lastAuth ? ` (${status.lastAuth.toLocaleDateString()})` : '';
|
||||
authSpinner.succeed(
|
||||
` ${`${providerName} Auth`.padEnd(26)}${colored('[OK]', 'green')} Authenticated${lastAuth}`
|
||||
);
|
||||
authSpinner.succeed();
|
||||
console.log(` ${ok(`${providerName} Auth`.padEnd(22))} Authenticated${lastAuth}`);
|
||||
this.results.addCheck(`${providerName} Auth`, 'success', undefined, undefined, {
|
||||
status: 'OK',
|
||||
info: `Authenticated${lastAuth}`,
|
||||
});
|
||||
} else {
|
||||
authSpinner.info(
|
||||
` ${`${providerName} Auth`.padEnd(26)}${colored('[i]', 'cyan')} Not authenticated`
|
||||
);
|
||||
authSpinner.info();
|
||||
console.log(` ${info(`${providerName} Auth`.padEnd(22))} Not authenticated`);
|
||||
this.results.addCheck(
|
||||
`${providerName} Auth`,
|
||||
'success',
|
||||
@@ -838,17 +836,15 @@ class Doctor {
|
||||
const portAvailable = await isPortAvailable(CLIPROXY_DEFAULT_PORT);
|
||||
|
||||
if (portAvailable) {
|
||||
portSpinner.succeed(
|
||||
` ${'CLIProxy Port'.padEnd(26)}${colored('[OK]', 'green')} ${CLIPROXY_DEFAULT_PORT} available`
|
||||
);
|
||||
portSpinner.succeed();
|
||||
console.log(` ${ok('CLIProxy Port'.padEnd(22))} ${CLIPROXY_DEFAULT_PORT} available`);
|
||||
this.results.addCheck('CLIProxy Port', 'success', undefined, undefined, {
|
||||
status: 'OK',
|
||||
info: `Port ${CLIPROXY_DEFAULT_PORT} available`,
|
||||
});
|
||||
} else {
|
||||
portSpinner.warn(
|
||||
` ${'CLIProxy Port'.padEnd(26)}${colored('[!]', 'yellow')} ${CLIPROXY_DEFAULT_PORT} in use`
|
||||
);
|
||||
portSpinner.warn();
|
||||
console.log(` ${warn('CLIProxy Port'.padEnd(22))} ${CLIPROXY_DEFAULT_PORT} in use`);
|
||||
this.results.addCheck(
|
||||
'CLIProxy Port',
|
||||
'warning',
|
||||
@@ -864,68 +860,48 @@ class Doctor {
|
||||
*/
|
||||
private showReport(): void {
|
||||
console.log('');
|
||||
|
||||
// Calculate exact table width to match header bars
|
||||
// colWidths: [20, 10, 35] = 65 + borders (4) = 69 total
|
||||
const tableWidth = 69;
|
||||
const headerBar = '═'.repeat(tableWidth);
|
||||
|
||||
console.log(colored(headerBar, 'cyan'));
|
||||
console.log(colored(' Health Check Summary', 'bold'));
|
||||
console.log(colored(headerBar, 'cyan'));
|
||||
|
||||
// Create summary table with detailed information
|
||||
const table = new Table({
|
||||
head: [colored('Component', 'cyan'), colored('Status', 'cyan'), colored('Details', 'cyan')],
|
||||
colWidths: [20, 10, 35],
|
||||
wordWrap: true,
|
||||
chars: {
|
||||
top: '═',
|
||||
'top-mid': '╤',
|
||||
'top-left': '╔',
|
||||
'top-right': '╗',
|
||||
bottom: '═',
|
||||
'bottom-mid': '╧',
|
||||
'bottom-left': '╚',
|
||||
'bottom-right': '╝',
|
||||
left: '║',
|
||||
'left-mid': '╟',
|
||||
mid: '─',
|
||||
'mid-mid': '┼',
|
||||
right: '║',
|
||||
'right-mid': '╢',
|
||||
middle: '│',
|
||||
},
|
||||
});
|
||||
|
||||
// Populate table with collected details
|
||||
for (const [component, detail] of Object.entries(this.results.details)) {
|
||||
const statusColor =
|
||||
detail.status === 'OK' ? 'green' : detail.status === 'ERROR' ? 'red' : 'yellow';
|
||||
table.push([component, colored(detail.status, statusColor), detail.info || '']);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
console.log(header('HEALTH CHECK SUMMARY'));
|
||||
console.log('');
|
||||
|
||||
// Show errors and warnings if present
|
||||
// Build summary table rows
|
||||
const rows: string[][] = Object.entries(this.results.details).map(([component, detail]) => {
|
||||
const statusIndicator =
|
||||
detail.status === 'OK'
|
||||
? color('[OK]', 'success')
|
||||
: detail.status === 'ERROR'
|
||||
? color('[X]', 'error')
|
||||
: color('[!]', 'warning');
|
||||
|
||||
return [component, statusIndicator, detail.info || ''];
|
||||
});
|
||||
|
||||
console.log(
|
||||
table(rows, {
|
||||
head: ['Component', 'Status', 'Details'],
|
||||
colWidths: [20, 12, 35],
|
||||
})
|
||||
);
|
||||
console.log('');
|
||||
|
||||
// Show errors if present
|
||||
if (this.results.hasErrors()) {
|
||||
console.log(colored('Errors:', 'red'));
|
||||
console.log(header('ERRORS'));
|
||||
this.results.errors.forEach((err) => {
|
||||
console.log(` [X] ${err.name}: ${err.message}`);
|
||||
console.log(` ${fail(err.name)}: ${err.message}`);
|
||||
if (err.fix) {
|
||||
console.log(` Fix: ${err.fix}`);
|
||||
console.log(` Fix: ${color(err.fix, 'command')}`);
|
||||
}
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Show warnings if present
|
||||
if (this.results.hasWarnings()) {
|
||||
console.log(colored('Warnings:', 'yellow'));
|
||||
this.results.warnings.forEach((warn) => {
|
||||
console.log(` [!] ${warn.name}: ${warn.message}`);
|
||||
if (warn.fix) {
|
||||
console.log(` Fix: ${warn.fix}`);
|
||||
console.log(header('WARNINGS'));
|
||||
this.results.warnings.forEach((w) => {
|
||||
console.log(` ${warn(w.name)}: ${w.message}`);
|
||||
if (w.fix) {
|
||||
console.log(` Fix: ${color(w.fix, 'command')}`);
|
||||
}
|
||||
});
|
||||
console.log('');
|
||||
@@ -933,12 +909,15 @@ class Doctor {
|
||||
|
||||
// Final status
|
||||
if (this.results.isHealthy() && !this.results.hasWarnings()) {
|
||||
console.log(colored('[OK] All checks passed! Your CCS installation is healthy.', 'green'));
|
||||
console.log(ok('All checks passed! Installation is healthy.'));
|
||||
} else if (this.results.hasErrors()) {
|
||||
console.log(colored('[X] Status: Installation has errors', 'red'));
|
||||
console.log('Run suggested fixes above to resolve issues.');
|
||||
console.log(fail('Installation has errors. Run suggested fixes above.'));
|
||||
} else {
|
||||
console.log(colored('[OK] Status: Installation healthy (warnings only)', 'green'));
|
||||
console.log(
|
||||
ok(
|
||||
`Installation healthy (${this.results.warnings.length} warning${this.results.warnings.length !== 1 ? 's' : ''})`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
@@ -41,3 +41,64 @@ export interface TerminalInfo {
|
||||
* Helper result types
|
||||
*/
|
||||
export type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
|
||||
|
||||
// =============================================================================
|
||||
// UI TYPES (Phase 1: CLI UI/UX Enhancement)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Semantic color names for UI elements
|
||||
*/
|
||||
export type SemanticColor =
|
||||
| 'success' // green - [OK] messages
|
||||
| 'error' // red - [X] messages
|
||||
| 'warning' // yellow - [!] messages
|
||||
| 'info' // cyan - [i] messages
|
||||
| 'dim' // gray - secondary text
|
||||
| 'primary' // #00ECFA - headers, emphasis
|
||||
| 'secondary' // #0099FF - links, accents
|
||||
| 'command' // yellow italic - command examples
|
||||
| 'path'; // cyan underline - file paths
|
||||
|
||||
/**
|
||||
* Box style options for boxen wrapper
|
||||
*/
|
||||
export interface BoxOptions {
|
||||
title?: string;
|
||||
titleAlignment?: 'left' | 'center' | 'right';
|
||||
padding?: number;
|
||||
margin?: number;
|
||||
borderColor?: string;
|
||||
borderStyle?: 'single' | 'double' | 'round' | 'bold' | 'classic';
|
||||
}
|
||||
|
||||
/**
|
||||
* Table style options for cli-table3 wrapper
|
||||
*/
|
||||
export interface TableOptions {
|
||||
head?: string[];
|
||||
colWidths?: number[];
|
||||
wordWrap?: boolean;
|
||||
style?: 'unicode' | 'ascii';
|
||||
}
|
||||
|
||||
/**
|
||||
* Spinner options for ora wrapper
|
||||
*/
|
||||
export interface SpinnerOptions {
|
||||
text: string;
|
||||
color?: string;
|
||||
prefixText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spinner control interface
|
||||
*/
|
||||
export interface SpinnerController {
|
||||
succeed: (msg?: string) => void;
|
||||
fail: (msg?: string) => void;
|
||||
warn: (msg?: string) => void;
|
||||
info: (msg?: string) => void;
|
||||
update: (text: string) => void;
|
||||
stop: () => void;
|
||||
}
|
||||
+127
-93
@@ -1,4 +1,4 @@
|
||||
import { colored } from './helpers';
|
||||
import { initUI, header, color, dim, errorBox } from './ui';
|
||||
import { ERROR_CODES, getErrorDocUrl, ErrorCode } from './error-codes';
|
||||
|
||||
/**
|
||||
@@ -23,60 +23,70 @@ export class ErrorManager {
|
||||
* Show error code and documentation URL
|
||||
*/
|
||||
static showErrorCode(errorCode: ErrorCode): void {
|
||||
console.error(colored(`Error: ${errorCode}`, 'yellow'));
|
||||
console.error(colored(getErrorDocUrl(errorCode), 'yellow'));
|
||||
console.error(dim(`Error: ${errorCode}`));
|
||||
console.error(dim(getErrorDocUrl(errorCode)));
|
||||
console.error('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Claude CLI not found error
|
||||
*/
|
||||
static showClaudeNotFound(): void {
|
||||
static async showClaudeNotFound(): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
console.error('');
|
||||
console.error(colored('[X] Claude CLI not found', 'red'));
|
||||
console.error(
|
||||
errorBox(
|
||||
'Claude CLI not found\n\n' +
|
||||
'CCS requires Claude CLI to be installed\n' +
|
||||
'and available in PATH.',
|
||||
'ERROR'
|
||||
)
|
||||
);
|
||||
console.error('');
|
||||
console.error('CCS requires Claude CLI to be installed and available in PATH.');
|
||||
|
||||
console.error(header('SOLUTIONS'));
|
||||
console.error('');
|
||||
console.error(colored('Solutions:', 'yellow'));
|
||||
console.error(' 1. Install Claude CLI:');
|
||||
console.error(' https://docs.claude.com/en/docs/claude-code/installation');
|
||||
console.error(' 1. Install Claude CLI');
|
||||
console.error(` ${color('https://docs.claude.com/install', 'path')}`);
|
||||
console.error('');
|
||||
console.error(' 2. Verify installation:');
|
||||
console.error(' command -v claude (Unix)');
|
||||
console.error(' Get-Command claude (Windows)');
|
||||
console.error(' 2. Verify installation');
|
||||
console.error(` ${color('command -v claude', 'command')} (Unix)`);
|
||||
console.error(` ${color('Get-Command claude', 'command')} (Windows)`);
|
||||
console.error('');
|
||||
console.error(' 3. Custom path (if installed elsewhere):');
|
||||
console.error(' export CCS_CLAUDE_PATH="/path/to/claude"');
|
||||
console.error(' 3. Custom path (if installed elsewhere)');
|
||||
console.error(` ${color('export CCS_CLAUDE_PATH="/path/to/claude"', 'command')}`);
|
||||
console.error('');
|
||||
|
||||
this.showErrorCode(ERROR_CODES.CLAUDE_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show settings file not found error
|
||||
*/
|
||||
static showSettingsNotFound(settingsPath: string): void {
|
||||
static async showSettingsNotFound(settingsPath: string): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
const isClaudeSettings =
|
||||
settingsPath.includes('.claude') && settingsPath.endsWith('settings.json');
|
||||
|
||||
console.error('');
|
||||
console.error(colored('[X] Settings file not found', 'red'));
|
||||
console.error('');
|
||||
console.error(`File: ${settingsPath}`);
|
||||
console.error(errorBox('Settings file not found\n\n' + `File: ${settingsPath}`, 'ERROR'));
|
||||
console.error('');
|
||||
|
||||
if (isClaudeSettings) {
|
||||
console.error('This file is auto-created when you login to Claude CLI.');
|
||||
console.error('');
|
||||
console.error(colored('Solutions:', 'yellow'));
|
||||
console.error(` echo '{}' > ${settingsPath}`);
|
||||
console.error(' claude /login');
|
||||
console.error(header('SOLUTIONS'));
|
||||
console.error(` ${color(`echo '{}' > ${settingsPath}`, 'command')}`);
|
||||
console.error(` ${color('claude /login', 'command')}`);
|
||||
console.error('');
|
||||
console.error('Why: Newer Claude CLI versions require explicit login.');
|
||||
console.error(dim('Why: Newer Claude CLI versions require explicit login.'));
|
||||
} else {
|
||||
console.error(colored('Solutions:', 'yellow'));
|
||||
console.error(' npm install -g @kaitranntt/ccs --force');
|
||||
console.error(header('SOLUTIONS'));
|
||||
console.error(` ${color('npm install -g @kaitranntt/ccs --force', 'command')}`);
|
||||
console.error('');
|
||||
console.error('This will recreate missing profile settings.');
|
||||
console.error(dim('This will recreate missing profile settings.'));
|
||||
}
|
||||
|
||||
console.error('');
|
||||
@@ -86,120 +96,139 @@ export class ErrorManager {
|
||||
/**
|
||||
* Show invalid configuration error
|
||||
*/
|
||||
static showInvalidConfig(configPath: string, errorDetail: string): void {
|
||||
static async showInvalidConfig(configPath: string, errorDetail: string): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
console.error('');
|
||||
console.error(colored('[X] Configuration invalid', 'red'));
|
||||
console.error(
|
||||
errorBox(
|
||||
'Configuration invalid\n\n' + `File: ${configPath}\n` + `Issue: ${errorDetail}`,
|
||||
'ERROR'
|
||||
)
|
||||
);
|
||||
console.error('');
|
||||
console.error(`File: ${configPath}`);
|
||||
console.error(`Issue: ${errorDetail}`);
|
||||
|
||||
console.error(header('SOLUTIONS'));
|
||||
console.error('');
|
||||
console.error(colored('Solutions:', 'yellow'));
|
||||
console.error(' # Backup corrupted file');
|
||||
console.error(` mv ${configPath} ${configPath}.backup`);
|
||||
console.error(` ${dim('# Backup corrupted file')}`);
|
||||
console.error(` ${color(`mv ${configPath} ${configPath}.backup`, 'command')}`);
|
||||
console.error('');
|
||||
console.error(' # Reinstall CCS');
|
||||
console.error(' npm install -g @kaitranntt/ccs --force');
|
||||
console.error(` ${dim('# Reinstall CCS')}`);
|
||||
console.error(` ${color('npm install -g @kaitranntt/ccs --force', 'command')}`);
|
||||
console.error('');
|
||||
console.error('Your profile settings will be preserved.');
|
||||
console.error(dim('Your profile settings will be preserved.'));
|
||||
console.error('');
|
||||
|
||||
this.showErrorCode(ERROR_CODES.CONFIG_INVALID_JSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show profile not found error
|
||||
*/
|
||||
static showProfileNotFound(
|
||||
static async showProfileNotFound(
|
||||
profileName: string,
|
||||
availableProfiles: string[],
|
||||
suggestions: string[] = []
|
||||
): void {
|
||||
): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
console.error('');
|
||||
console.error(colored(`[X] Profile '${profileName}' not found`, 'red'));
|
||||
console.error(errorBox(`Profile '${profileName}' not found`, 'ERROR'));
|
||||
console.error('');
|
||||
|
||||
if (suggestions && suggestions.length > 0) {
|
||||
console.error(colored('Did you mean:', 'yellow'));
|
||||
suggestions.forEach((s) => console.error(` ${s}`));
|
||||
console.error(header('DID YOU MEAN'));
|
||||
suggestions.forEach((s) => console.error(` ${color(s, 'command')}`));
|
||||
console.error('');
|
||||
}
|
||||
|
||||
console.error(colored('Available profiles:', 'cyan'));
|
||||
availableProfiles.forEach((line) => console.error(` ${line}`));
|
||||
console.error(header('AVAILABLE PROFILES'));
|
||||
availableProfiles.forEach((line) => console.error(` ${color(line, 'info')}`));
|
||||
console.error('');
|
||||
console.error(colored('Solutions:', 'yellow'));
|
||||
console.error(' # Use existing profile');
|
||||
console.error(' ccs <profile> "your prompt"');
|
||||
|
||||
console.error(header('SOLUTIONS'));
|
||||
console.error('');
|
||||
console.error(' # Create new account profile');
|
||||
console.error(' ccs auth create <name>');
|
||||
console.error(' Use an existing profile:');
|
||||
console.error(` ${color('ccs <profile> "your prompt"', 'command')}`);
|
||||
console.error('');
|
||||
console.error(' Create a new account profile:');
|
||||
console.error(` ${color('ccs auth create <name>', 'command')}`);
|
||||
console.error('');
|
||||
|
||||
this.showErrorCode(ERROR_CODES.PROFILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show permission denied error
|
||||
*/
|
||||
static showPermissionDenied(path: string): void {
|
||||
static async showPermissionDenied(filePath: string): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
console.error('');
|
||||
console.error(colored('[X] Permission denied', 'red'));
|
||||
console.error(errorBox('Permission denied\n\n' + `Cannot write to: ${filePath}`, 'ERROR'));
|
||||
console.error('');
|
||||
console.error(`Cannot write to: ${path}`);
|
||||
|
||||
console.error(header('SOLUTIONS'));
|
||||
console.error('');
|
||||
console.error(colored('Solutions:', 'yellow'));
|
||||
console.error(' # Fix ownership');
|
||||
console.error(' sudo chown -R $USER ~/.ccs ~/.claude');
|
||||
console.error(` ${dim('# Fix ownership')}`);
|
||||
console.error(` ${color('sudo chown -R $USER ~/.ccs ~/.claude', 'command')}`);
|
||||
console.error('');
|
||||
console.error(' # Fix permissions');
|
||||
console.error(' chmod 755 ~/.ccs ~/.claude');
|
||||
console.error(` ${dim('# Fix permissions')}`);
|
||||
console.error(` ${color('chmod 755 ~/.ccs ~/.claude', 'command')}`);
|
||||
console.error('');
|
||||
console.error(' # Retry installation');
|
||||
console.error(' npm install -g @kaitranntt/ccs --force');
|
||||
console.error(` ${dim('# Retry installation')}`);
|
||||
console.error(` ${color('npm install -g @kaitranntt/ccs --force', 'command')}`);
|
||||
console.error('');
|
||||
|
||||
this.showErrorCode(ERROR_CODES.FS_CANNOT_WRITE_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show CLIProxy OAuth timeout error
|
||||
*/
|
||||
static showOAuthTimeout(provider: string): void {
|
||||
static async showOAuthTimeout(provider: string): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
console.error('');
|
||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
||||
console.error(colored('| OAuth Timeout |', 'red'));
|
||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
||||
console.error(
|
||||
errorBox('OAuth Timeout\n\n' + 'Authentication did not complete within 2 minutes.', 'ERROR')
|
||||
);
|
||||
console.error('');
|
||||
console.error('Authentication did not complete within 2 minutes.');
|
||||
console.error('');
|
||||
console.error(colored('Troubleshooting:', 'yellow'));
|
||||
|
||||
console.error(header('TROUBLESHOOTING'));
|
||||
console.error(' 1. Check if browser opened (popup blocker?)');
|
||||
console.error(' 2. Complete login in browser, then return here');
|
||||
console.error(' 3. Try different browser');
|
||||
console.error(' 4. Disable browser extensions temporarily');
|
||||
console.error('');
|
||||
console.error(colored('For headless/SSH environments:', 'cyan'));
|
||||
console.error(` ccs ${provider} --auth --headless`);
|
||||
|
||||
console.error(header('FOR HEADLESS/SSH ENVIRONMENTS'));
|
||||
console.error(` ${color(`ccs ${provider} --auth --headless`, 'command')}`);
|
||||
console.error('');
|
||||
console.error('This displays manual authentication steps.');
|
||||
console.error(dim('This displays manual authentication steps.'));
|
||||
console.error('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show CLIProxy port conflict error
|
||||
*/
|
||||
static showPortConflict(port: number): void {
|
||||
static async showPortConflict(port: number): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
console.error('');
|
||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
||||
console.error(colored('| Port Conflict |', 'red'));
|
||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
||||
console.error(
|
||||
errorBox('Port Conflict\n\n' + `CLIProxy port ${port} is already in use.`, 'ERROR')
|
||||
);
|
||||
console.error('');
|
||||
console.error(`CLIProxy port ${port} is already in use.`);
|
||||
|
||||
console.error(header('SOLUTIONS'));
|
||||
console.error('');
|
||||
console.error(colored('Solutions:', 'yellow'));
|
||||
console.error(' 1. Find process using port:');
|
||||
console.error(` lsof -i :${port} (macOS/Linux)`);
|
||||
console.error(` netstat -ano | findstr ${port} (Windows)`);
|
||||
console.error(` ${color(`lsof -i :${port}`, 'command')} (macOS/Linux)`);
|
||||
console.error(` ${color(`netstat -ano | findstr ${port}`, 'command')} (Windows)`);
|
||||
console.error('');
|
||||
console.error(' 2. Kill the process:');
|
||||
console.error(` lsof -ti:${port} | xargs kill -9`);
|
||||
console.error(` ${color(`lsof -ti:${port} | xargs kill -9`, 'command')}`);
|
||||
console.error('');
|
||||
console.error(' 3. Wait and retry (process may exit on its own)');
|
||||
console.error('');
|
||||
@@ -208,38 +237,43 @@ export class ErrorManager {
|
||||
/**
|
||||
* Show CLIProxy binary download failure error
|
||||
*/
|
||||
static showBinaryDownloadFailed(url: string, error: string): void {
|
||||
static async showBinaryDownloadFailed(url: string, error: string): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
console.error('');
|
||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
||||
console.error(colored('| Binary Download Failed |', 'red'));
|
||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
||||
console.error(errorBox('Binary Download Failed\n\n' + `Error: ${error}`, 'ERROR'));
|
||||
console.error('');
|
||||
console.error(`Error: ${error}`);
|
||||
console.error('');
|
||||
console.error(colored('Troubleshooting:', 'yellow'));
|
||||
|
||||
console.error(header('TROUBLESHOOTING'));
|
||||
console.error(' 1. Check internet connection');
|
||||
console.error(' 2. Check firewall/proxy settings');
|
||||
console.error(' 3. Try again in a few minutes');
|
||||
console.error('');
|
||||
console.error(colored('Manual download:', 'cyan'));
|
||||
console.error(` URL: ${url}`);
|
||||
console.error(' Save to: ~/.ccs/bin/cliproxyapi');
|
||||
console.error(' chmod +x ~/.ccs/bin/cliproxyapi');
|
||||
|
||||
console.error(header('MANUAL DOWNLOAD'));
|
||||
console.error(` URL: ${color(url, 'path')}`);
|
||||
console.error(` Save to: ${color('~/.ccs/bin/cliproxyapi', 'path')}`);
|
||||
console.error(` ${color('chmod +x ~/.ccs/bin/cliproxyapi', 'command')}`);
|
||||
console.error('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show CLIProxy authentication required error
|
||||
*/
|
||||
static showAuthRequired(provider: string): void {
|
||||
static async showAuthRequired(provider: string): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
const displayName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
||||
|
||||
console.error('');
|
||||
console.error(colored(`[X] ${provider} authentication required`, 'red'));
|
||||
console.error(errorBox(`${displayName} authentication required`, 'ERROR'));
|
||||
console.error('');
|
||||
console.error(colored('To authenticate:', 'yellow'));
|
||||
console.error(` ccs ${provider} --auth`);
|
||||
|
||||
console.error(header('TO AUTHENTICATE'));
|
||||
console.error(` ${color(`ccs ${provider} --auth`, 'command')}`);
|
||||
console.error('');
|
||||
console.error('This will open a browser for OAuth login.');
|
||||
console.error('After login, you can use the profile normally.');
|
||||
console.error(dim('This will open a browser for OAuth login.'));
|
||||
console.error(dim('After login, you can use the profile normally.'));
|
||||
console.error('');
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,20 @@ import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { ColorName, TerminalInfo } from '../types';
|
||||
|
||||
// =============================================================================
|
||||
// DEPRECATION NOTICE
|
||||
// =============================================================================
|
||||
// The color functions in this file are deprecated in favor of the new UI layer.
|
||||
// Please use `import { ui } from './ui'` instead for:
|
||||
// - ui.color(text, semantic) - semantic colors
|
||||
// - ui.ok(), ui.fail(), ui.warn(), ui.info() - status indicators
|
||||
// - ui.box(), ui.table() - containers
|
||||
// - ui.header(), ui.subheader() - section headers
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* TTY-aware color detection (matches lib/ccs bash logic)
|
||||
* @deprecated Use `import { ui } from './ui'` instead
|
||||
*/
|
||||
function getColors(): Record<ColorName, string> {
|
||||
const forcedColors = process.env.FORCE_COLOR;
|
||||
@@ -33,6 +45,7 @@ export const colors = getColors();
|
||||
|
||||
/**
|
||||
* Helper: Apply color to text (returns plain text if colors disabled)
|
||||
* @deprecated Use `import { ui } from './ui'` and `ui.color()` instead
|
||||
*/
|
||||
export function colored(text: string, colorName: ColorName = 'reset'): string {
|
||||
const currentColors = getColors();
|
||||
|
||||
@@ -52,8 +52,8 @@ export function execClaude(
|
||||
else process.exit(code || 0);
|
||||
});
|
||||
|
||||
child.on('error', () => {
|
||||
ErrorManager.showClaudeNotFound();
|
||||
child.on('error', async () => {
|
||||
await ErrorManager.showClaudeNotFound();
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
+629
@@ -0,0 +1,629 @@
|
||||
/**
|
||||
* Central UI Abstraction Layer
|
||||
*
|
||||
* Provides semantic, TTY-aware styling for CLI output.
|
||||
* Wraps chalk, boxen, cli-table3, ora with consistent API.
|
||||
*
|
||||
* Constraints:
|
||||
* - NO EMOJIS (ASCII only: [OK], [X], [!], [i])
|
||||
* - TTY-aware (plain text in pipes/CI)
|
||||
* - Respects NO_COLOR environment variable
|
||||
*
|
||||
* @module utils/ui
|
||||
*/
|
||||
|
||||
import type {
|
||||
BoxOptions,
|
||||
TableOptions,
|
||||
SpinnerOptions,
|
||||
SemanticColor,
|
||||
SpinnerController,
|
||||
} from '../types/utils';
|
||||
|
||||
// =============================================================================
|
||||
// ESM MODULE TYPES & LAZY LOADING
|
||||
// =============================================================================
|
||||
|
||||
// Type definitions for dynamically imported ESM modules
|
||||
type ChalkInstance = typeof import('chalk').default;
|
||||
type BoxenFunction = typeof import('boxen').default;
|
||||
type GradientStringInstance = typeof import('gradient-string').default;
|
||||
// ora v9 is ESM-only, imported dynamically at runtime
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type OraModule = any;
|
||||
// listr2 v9 is ESM-only, imported dynamically at runtime
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ListrClass = any;
|
||||
|
||||
// Module cache for lazy loading
|
||||
let chalkModule: ChalkInstance | null = null;
|
||||
let boxenModule: BoxenFunction | null = null;
|
||||
let gradientModule: GradientStringInstance | null = null;
|
||||
let oraModule: OraModule | null = null;
|
||||
let listrModule: ListrClass | null = null;
|
||||
|
||||
// Initialization state
|
||||
let initialized = false;
|
||||
|
||||
// =============================================================================
|
||||
// COLOR PALETTE (Professional cyan-to-blue)
|
||||
// =============================================================================
|
||||
|
||||
const COLORS = {
|
||||
primary: '#00ECFA', // Bright cyan
|
||||
secondary: '#0099FF', // Sky blue
|
||||
neutral: '#808080', // Gray
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
// INITIALIZATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Initialize UI dependencies (call once at startup)
|
||||
* Uses dynamic imports for ESM packages in CommonJS project
|
||||
*/
|
||||
export async function initUI(): Promise<void> {
|
||||
if (initialized) return;
|
||||
|
||||
try {
|
||||
// Dynamic import for ESM-only packages
|
||||
const [chalkImport, boxenImport, gradientImport, oraImport, listrImport] = await Promise.all([
|
||||
import('chalk'),
|
||||
import('boxen'),
|
||||
import('gradient-string'),
|
||||
import('ora'),
|
||||
import('listr2'),
|
||||
]);
|
||||
|
||||
chalkModule = chalkImport.default;
|
||||
boxenModule = boxenImport.default;
|
||||
gradientModule = gradientImport.default;
|
||||
oraModule = oraImport.default;
|
||||
listrModule = listrImport.Listr;
|
||||
initialized = true;
|
||||
} catch (_e) {
|
||||
// Fallback: UI works without colors if imports fail
|
||||
console.error('[!] UI initialization failed, using plain text mode');
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TTY & COLOR DETECTION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check if colors should be used
|
||||
* Respects NO_COLOR and FORCE_COLOR environment variables
|
||||
*/
|
||||
function useColors(): boolean {
|
||||
// FORCE_COLOR overrides all checks
|
||||
if (process.env.FORCE_COLOR) return true;
|
||||
// NO_COLOR disables colors
|
||||
if (process.env.NO_COLOR) return false;
|
||||
// Otherwise, check if TTY
|
||||
return !!process.stdout.isTTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if interactive mode (TTY + not CI)
|
||||
*/
|
||||
export function isInteractive(): boolean {
|
||||
return !!process.stdout.isTTY && !process.env.CI && !process.env.NO_COLOR;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COLOR SYSTEM
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Apply semantic color to text
|
||||
*/
|
||||
export function color(text: string, semantic: SemanticColor): string {
|
||||
if (!chalkModule || !useColors()) return text;
|
||||
|
||||
switch (semantic) {
|
||||
case 'success':
|
||||
return chalkModule.green.bold(text);
|
||||
case 'error':
|
||||
return chalkModule.red.bold(text);
|
||||
case 'warning':
|
||||
return chalkModule.yellow(text);
|
||||
case 'info':
|
||||
return chalkModule.cyan(text);
|
||||
case 'dim':
|
||||
return chalkModule.gray(text);
|
||||
case 'primary':
|
||||
return chalkModule.hex(COLORS.primary).bold(text);
|
||||
case 'secondary':
|
||||
return chalkModule.hex(COLORS.secondary)(text);
|
||||
case 'command':
|
||||
return chalkModule.yellow.bold(text);
|
||||
case 'path':
|
||||
return chalkModule.cyan.underline(text);
|
||||
default:
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply gradient to text (for headers)
|
||||
* Uses cyan-to-blue gradient for professional look
|
||||
*/
|
||||
export function gradientText(text: string): string {
|
||||
if (!gradientModule || !useColors()) return text;
|
||||
return gradientModule([COLORS.primary, COLORS.secondary])(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bold text
|
||||
*/
|
||||
export function bold(text: string): string {
|
||||
if (!chalkModule || !useColors()) return text;
|
||||
return chalkModule.bold(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dim text
|
||||
*/
|
||||
export function dim(text: string): string {
|
||||
if (!chalkModule || !useColors()) return text;
|
||||
return chalkModule.dim(text);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATUS INDICATORS (ASCII only - NO EMOJIS)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Success indicator: [OK]
|
||||
*/
|
||||
export function ok(message: string): string {
|
||||
return `${color('[OK]', 'success')} ${message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicator: [X]
|
||||
*/
|
||||
export function fail(message: string): string {
|
||||
return `${color('[X]', 'error')} ${message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning indicator: [!]
|
||||
*/
|
||||
export function warn(message: string): string {
|
||||
return `${color('[!]', 'warning')} ${message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Info indicator: [i]
|
||||
*/
|
||||
export function info(message: string): string {
|
||||
return `${color('[i]', 'info')} ${message}`;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BOX RENDERING
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fallback ASCII box renderer (when boxen not available)
|
||||
*/
|
||||
function renderAsciiBox(content: string, options: BoxOptions): string {
|
||||
const lines = content.split('\n');
|
||||
const maxLen = Math.max(...lines.map((l) => l.length), (options.title?.length || 0) + 4);
|
||||
const width = maxLen + 4;
|
||||
|
||||
let result = '';
|
||||
const padding = options.padding ?? 1;
|
||||
|
||||
// Top border with optional title
|
||||
if (options.title) {
|
||||
const titlePad = Math.floor((width - options.title.length - 4) / 2);
|
||||
result +=
|
||||
'+' +
|
||||
'-'.repeat(titlePad) +
|
||||
' ' +
|
||||
options.title +
|
||||
' ' +
|
||||
'-'.repeat(width - titlePad - options.title.length - 4) +
|
||||
'+\n';
|
||||
} else {
|
||||
result += '+' + '-'.repeat(width - 2) + '+\n';
|
||||
}
|
||||
|
||||
// Padding top
|
||||
for (let i = 0; i < padding; i++) {
|
||||
result += '|' + ' '.repeat(width - 2) + '|\n';
|
||||
}
|
||||
|
||||
// Content
|
||||
for (const line of lines) {
|
||||
const pad = width - line.length - 4;
|
||||
result += '| ' + line + ' '.repeat(Math.max(0, pad)) + ' |\n';
|
||||
}
|
||||
|
||||
// Padding bottom
|
||||
for (let i = 0; i < padding; i++) {
|
||||
result += '|' + ' '.repeat(width - 2) + '|\n';
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
result += '+' + '-'.repeat(width - 2) + '+';
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render content in a styled box
|
||||
*/
|
||||
export function box(content: string, options: BoxOptions = {}): string {
|
||||
if (!boxenModule) {
|
||||
return renderAsciiBox(content, options);
|
||||
}
|
||||
|
||||
const borderColor = useColors() ? options.borderColor || COLORS.primary : undefined;
|
||||
|
||||
return boxenModule(content, {
|
||||
padding: options.padding ?? 1,
|
||||
margin: options.margin ?? 0,
|
||||
borderStyle: options.borderStyle || 'round',
|
||||
borderColor,
|
||||
title: options.title,
|
||||
titleAlignment: options.titleAlignment || 'center',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render error box (red border)
|
||||
*/
|
||||
export function errorBox(content: string, title = 'ERROR'): string {
|
||||
return box(content, {
|
||||
title,
|
||||
borderColor: 'red',
|
||||
borderStyle: 'round',
|
||||
padding: 1,
|
||||
margin: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render info box (primary color border)
|
||||
*/
|
||||
export function infoBox(content: string, title?: string): string {
|
||||
return box(content, {
|
||||
title,
|
||||
borderColor: COLORS.primary,
|
||||
borderStyle: 'round',
|
||||
padding: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TABLE RENDERING
|
||||
// =============================================================================
|
||||
|
||||
// cli-table3 is CommonJS, use dynamic import pattern
|
||||
|
||||
const Table = require('cli-table3');
|
||||
|
||||
/**
|
||||
* Create styled table
|
||||
*/
|
||||
export function table(rows: string[][], options: TableOptions = {}): string {
|
||||
// Build table configuration
|
||||
const tableConfig: Record<string, unknown> = {
|
||||
wordWrap: options.wordWrap ?? true,
|
||||
chars:
|
||||
options.style === 'ascii'
|
||||
? {
|
||||
top: '-',
|
||||
'top-mid': '+',
|
||||
'top-left': '+',
|
||||
'top-right': '+',
|
||||
bottom: '-',
|
||||
'bottom-mid': '+',
|
||||
'bottom-left': '+',
|
||||
'bottom-right': '+',
|
||||
left: '|',
|
||||
'left-mid': '+',
|
||||
mid: '-',
|
||||
'mid-mid': '+',
|
||||
right: '|',
|
||||
'right-mid': '+',
|
||||
middle: '|',
|
||||
}
|
||||
: {
|
||||
top: '─',
|
||||
'top-mid': '┬',
|
||||
'top-left': '┌',
|
||||
'top-right': '┐',
|
||||
bottom: '─',
|
||||
'bottom-mid': '┴',
|
||||
'bottom-left': '└',
|
||||
'bottom-right': '┘',
|
||||
left: '│',
|
||||
'left-mid': '├',
|
||||
mid: '─',
|
||||
'mid-mid': '┼',
|
||||
right: '│',
|
||||
'right-mid': '┤',
|
||||
middle: '│',
|
||||
},
|
||||
};
|
||||
|
||||
// Only add head if provided (cli-table3 requires head length to match rows)
|
||||
if (options.head && options.head.length > 0) {
|
||||
tableConfig.head = options.head.map((h: string) => color(h, 'primary'));
|
||||
}
|
||||
|
||||
// Only add colWidths if provided
|
||||
if (options.colWidths) {
|
||||
tableConfig.colWidths = options.colWidths;
|
||||
}
|
||||
|
||||
const tableInstance = new Table(tableConfig);
|
||||
|
||||
rows.forEach((row) => tableInstance.push(row));
|
||||
return tableInstance.toString();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SPINNER / PROGRESS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Create and start a spinner
|
||||
* Falls back to plain text output in non-TTY environments
|
||||
*/
|
||||
export async function spinner(options: SpinnerOptions | string): Promise<SpinnerController> {
|
||||
const opts = typeof options === 'string' ? { text: options } : options;
|
||||
const isEnabled = isInteractive();
|
||||
|
||||
// Lazy load ora if not already loaded
|
||||
if (!oraModule && isEnabled) {
|
||||
try {
|
||||
oraModule = (await import('ora')).default;
|
||||
} catch (_e) {
|
||||
// Fallback to plain text
|
||||
}
|
||||
}
|
||||
|
||||
if (oraModule && isEnabled) {
|
||||
const s = oraModule({
|
||||
text: opts.text,
|
||||
color: 'cyan',
|
||||
prefixText: opts.prefixText,
|
||||
isEnabled,
|
||||
}).start();
|
||||
|
||||
return {
|
||||
succeed: (msg?: string) => s.succeed(msg || `${color('[OK]', 'success')} ${opts.text}`),
|
||||
fail: (msg?: string) => s.fail(msg || `${color('[X]', 'error')} ${opts.text}`),
|
||||
warn: (msg?: string) => s.warn(msg || `${color('[!]', 'warning')} ${opts.text}`),
|
||||
info: (msg?: string) => s.info(msg || `${color('[i]', 'info')} ${opts.text}`),
|
||||
update: (text: string) => {
|
||||
s.text = text;
|
||||
},
|
||||
stop: () => s.stop(),
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback: plain text (non-TTY)
|
||||
console.log(`[i] ${opts.text}...`);
|
||||
return {
|
||||
succeed: (msg?: string) => console.log(ok(msg || opts.text)),
|
||||
fail: (msg?: string) => console.log(fail(msg || opts.text)),
|
||||
warn: (msg?: string) => console.log(warn(msg || opts.text)),
|
||||
info: (msg?: string) => console.log(info(msg || opts.text)),
|
||||
update: (_text: string) => {
|
||||
/* no-op in non-TTY */
|
||||
},
|
||||
stop: () => {
|
||||
/* no-op */
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECTION HEADERS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Print section header with optional gradient
|
||||
*/
|
||||
export function header(text: string, useGradient = true): string {
|
||||
if (useGradient && useColors()) {
|
||||
return gradientText(text);
|
||||
}
|
||||
return bold(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print subsection header
|
||||
*/
|
||||
export function subheader(text: string): string {
|
||||
return color(text, 'primary');
|
||||
}
|
||||
|
||||
/**
|
||||
* Print horizontal rule
|
||||
*/
|
||||
export function hr(char = '─', width = 60): string {
|
||||
if (!useColors()) {
|
||||
return '-'.repeat(width);
|
||||
}
|
||||
return dim(char.repeat(width));
|
||||
}
|
||||
|
||||
/**
|
||||
* Print section header with ═══ borders
|
||||
* Format: ═══ Title ═══
|
||||
*/
|
||||
export function sectionHeader(title: string): string {
|
||||
const border = '═══';
|
||||
const headerText = `${border} ${title} ${border}`;
|
||||
// Use gradient + bold for visual appeal
|
||||
if (gradientModule && chalkModule && useColors()) {
|
||||
return chalkModule.bold(gradientModule([COLORS.primary, COLORS.secondary])(headerText));
|
||||
}
|
||||
// Fallback to bold primary color
|
||||
if (useColors() && chalkModule) {
|
||||
return chalkModule.hex(COLORS.primary).bold(headerText);
|
||||
}
|
||||
return headerText;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TASK LISTS (Listr2 Integration)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Detect if running inside Claude Code tool context
|
||||
*
|
||||
* Heuristics:
|
||||
* - No TTY (stdout captured)
|
||||
* - CI-like environment
|
||||
* - CLAUDE_CODE env var set
|
||||
*/
|
||||
export function isClaudeCodeContext(): boolean {
|
||||
return (
|
||||
!process.stdout.isTTY ||
|
||||
!!process.env.CI ||
|
||||
!!process.env.CLAUDE_CODE ||
|
||||
process.env.TERM === 'dumb'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Task list item interface
|
||||
*/
|
||||
export interface TaskItem<T> {
|
||||
title: string;
|
||||
task: (ctx: T) => Promise<void> | void;
|
||||
skip?: () => boolean | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task list options
|
||||
*/
|
||||
export interface TaskListOptions {
|
||||
concurrent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a task list for progress display
|
||||
* Uses Listr2 in TTY mode, falls back to spinners in non-TTY
|
||||
*/
|
||||
export async function taskList<T>(tasks: TaskItem<T>[], options: TaskListOptions = {}): Promise<T> {
|
||||
// Lazy load Listr2 if not already loaded
|
||||
if (!listrModule && isInteractive()) {
|
||||
try {
|
||||
const listr2 = await import('listr2');
|
||||
listrModule = listr2.Listr;
|
||||
} catch (_e) {
|
||||
// Fallback to sequential execution with spinners
|
||||
return runTasksFallback(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
if (listrModule && isInteractive()) {
|
||||
// Determine renderer based on context
|
||||
// Use 'simple' in non-TTY, CI, or Claude Code context
|
||||
const useSimple = isClaudeCodeContext();
|
||||
|
||||
const list = new listrModule(
|
||||
tasks.map((t) => ({
|
||||
title: t.title,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
task: async (ctx: any) => t.task(ctx),
|
||||
skip: t.skip,
|
||||
})),
|
||||
{
|
||||
concurrent: options.concurrent ?? false,
|
||||
renderer: useSimple ? 'simple' : 'default',
|
||||
rendererOptions: {
|
||||
showSubtasks: true,
|
||||
collapseSubtasks: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return list.run({} as T);
|
||||
}
|
||||
|
||||
// Fallback: non-interactive or Listr2 not available
|
||||
return runTasksFallback(tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback task runner (no Listr2)
|
||||
* Uses spinners for sequential task execution
|
||||
*/
|
||||
async function runTasksFallback<T>(tasks: TaskItem<T>[]): Promise<T> {
|
||||
const ctx = {} as T;
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.skip) {
|
||||
const skipResult = task.skip();
|
||||
if (skipResult) {
|
||||
console.log(info(`${task.title} [skipped]`));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const spin = await spinner(task.title);
|
||||
try {
|
||||
await task.task(ctx);
|
||||
spin.succeed();
|
||||
} catch (e) {
|
||||
spin.fail(`${task.title}: ${(e as Error).message}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UNIFIED EXPORT OBJECT
|
||||
// =============================================================================
|
||||
|
||||
export const ui = {
|
||||
// Initialization
|
||||
init: initUI,
|
||||
isInteractive,
|
||||
isClaudeCodeContext,
|
||||
|
||||
// Colors
|
||||
color,
|
||||
gradientText,
|
||||
bold,
|
||||
dim,
|
||||
|
||||
// Status indicators (ASCII only)
|
||||
ok,
|
||||
fail,
|
||||
warn,
|
||||
info,
|
||||
|
||||
// Containers
|
||||
box,
|
||||
errorBox,
|
||||
infoBox,
|
||||
table,
|
||||
|
||||
// Progress
|
||||
spinner,
|
||||
taskList,
|
||||
|
||||
// Headers
|
||||
header,
|
||||
subheader,
|
||||
sectionHeader,
|
||||
hr,
|
||||
} as const;
|
||||
|
||||
export default ui;
|
||||
@@ -3,7 +3,7 @@ const { ResultFormatter } = require('../../../dist/delegation/result-formatter')
|
||||
|
||||
describe('ResultFormatter', () => {
|
||||
describe('Basic formatting', () => {
|
||||
it('formats successful result', () => {
|
||||
it('formats successful result', async () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/home/user/project',
|
||||
@@ -14,17 +14,17 @@ describe('ResultFormatter', () => {
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
|
||||
assert.ok(formatted.includes('Delegated to GLM-4.6'));
|
||||
assert.ok(formatted.includes('ccs:glm'));
|
||||
assert.ok(formatted.includes('/home/user/project'));
|
||||
assert.ok(formatted.includes('2.3s'));
|
||||
assert.ok(formatted.includes('Exit Code: 0'));
|
||||
assert.ok(formatted.includes('0')); // Exit code in table
|
||||
assert.ok(formatted.includes('[OK]'));
|
||||
});
|
||||
|
||||
it('formats failed result', () => {
|
||||
it('formats failed result', async () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/home/user/project',
|
||||
@@ -35,16 +35,16 @@ describe('ResultFormatter', () => {
|
||||
success: false
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
|
||||
assert.ok(formatted.includes('[X]'));
|
||||
assert.ok(formatted.includes('Exit Code: 1'));
|
||||
assert.ok(formatted.includes('1')); // Exit code in table
|
||||
assert.ok(formatted.includes('Delegation failed'));
|
||||
assert.ok(formatted.includes('Stderr:'));
|
||||
assert.ok(formatted.includes('Command failed'));
|
||||
});
|
||||
|
||||
it('handles empty output', () => {
|
||||
it('handles empty output', async () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
@@ -55,7 +55,7 @@ describe('ResultFormatter', () => {
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
|
||||
assert.ok(formatted.includes('No output'));
|
||||
});
|
||||
@@ -111,7 +111,7 @@ describe('ResultFormatter', () => {
|
||||
});
|
||||
|
||||
describe('Output passthrough', () => {
|
||||
it('passes through stdout content', () => {
|
||||
it('passes through stdout content', async () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/home/user/project',
|
||||
@@ -122,13 +122,13 @@ describe('ResultFormatter', () => {
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
|
||||
assert.ok(formatted.includes('src/new.js'));
|
||||
assert.ok(formatted.includes('src/old.js'));
|
||||
});
|
||||
|
||||
it('preserves multi-line output', () => {
|
||||
it('preserves multi-line output', async () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
@@ -139,7 +139,7 @@ describe('ResultFormatter', () => {
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
|
||||
assert.ok(formatted.includes('Line 1'));
|
||||
assert.ok(formatted.includes('Line 2'));
|
||||
@@ -148,7 +148,7 @@ describe('ResultFormatter', () => {
|
||||
});
|
||||
|
||||
describe('ASCII box formatting', () => {
|
||||
it('uses ASCII box characters', () => {
|
||||
it('uses ASCII box characters', async () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
@@ -159,19 +159,15 @@ describe('ResultFormatter', () => {
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
|
||||
assert.ok(formatted.includes('╔'));
|
||||
assert.ok(formatted.includes('╗'));
|
||||
assert.ok(formatted.includes('╚'));
|
||||
assert.ok(formatted.includes('╝'));
|
||||
assert.ok(formatted.includes('║'));
|
||||
assert.ok(formatted.includes('═'));
|
||||
// Check for box characters (round border style)
|
||||
assert.ok(formatted.includes('─') || formatted.includes('╔'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model display names', () => {
|
||||
it('shows correct model display names', () => {
|
||||
it('shows correct model display names', async () => {
|
||||
const glmResult = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
@@ -182,17 +178,17 @@ describe('ResultFormatter', () => {
|
||||
success: true
|
||||
};
|
||||
|
||||
const glmFormatted = ResultFormatter.format(glmResult);
|
||||
const glmFormatted = await ResultFormatter.format(glmResult);
|
||||
assert.ok(glmFormatted.includes('GLM-4.6'));
|
||||
|
||||
const kimiResult = { ...glmResult, profile: 'kimi' };
|
||||
const kimiFormatted = ResultFormatter.format(kimiResult);
|
||||
const kimiFormatted = await ResultFormatter.format(kimiResult);
|
||||
assert.ok(kimiFormatted.includes('Kimi'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Duration formatting', () => {
|
||||
it('formats duration correctly', () => {
|
||||
it('formats duration correctly', async () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
@@ -203,14 +199,14 @@ describe('ResultFormatter', () => {
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
|
||||
assert.ok(formatted.includes('12.3s'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Minimal format', () => {
|
||||
it('supports minimal format', () => {
|
||||
it('supports minimal format', async () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
@@ -221,7 +217,7 @@ describe('ResultFormatter', () => {
|
||||
success: true
|
||||
};
|
||||
|
||||
const minimal = ResultFormatter.formatMinimal(result);
|
||||
const minimal = await ResultFormatter.formatMinimal(result);
|
||||
|
||||
assert.ok(minimal.includes('[OK]'));
|
||||
assert.ok(minimal.includes('GLM-4.6'));
|
||||
@@ -231,7 +227,7 @@ describe('ResultFormatter', () => {
|
||||
});
|
||||
|
||||
describe('Cost handling', () => {
|
||||
it('handles undefined totalCost in timeout error', () => {
|
||||
it('handles undefined totalCost in timeout error', async () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
@@ -242,14 +238,14 @@ describe('ResultFormatter', () => {
|
||||
timedOut: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
|
||||
assert.ok(formatted.includes('Execution timed out'));
|
||||
assert.ok(formatted.includes('timed out') || formatted.includes('Timeout'));
|
||||
assert.ok(formatted.includes('test-ses'));
|
||||
assert.ok(!formatted.includes('Cost: $'));
|
||||
});
|
||||
|
||||
it('handles null totalCost in timeout error', () => {
|
||||
it('handles null totalCost in timeout error', async () => {
|
||||
const result = {
|
||||
profile: 'kimi',
|
||||
cwd: '/test',
|
||||
@@ -260,13 +256,13 @@ describe('ResultFormatter', () => {
|
||||
timedOut: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
|
||||
assert.ok(formatted.includes('Execution timed out'));
|
||||
assert.ok(formatted.includes('timed out') || formatted.includes('Timeout'));
|
||||
assert.ok(!formatted.includes('Cost: $'));
|
||||
});
|
||||
|
||||
it('shows totalCost when defined in timeout error', () => {
|
||||
it('shows totalCost when defined in timeout error', async () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
@@ -277,12 +273,12 @@ describe('ResultFormatter', () => {
|
||||
timedOut: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
|
||||
assert.ok(formatted.includes('Cost: $0.1234'));
|
||||
});
|
||||
|
||||
it('handles undefined totalCost in normal result', () => {
|
||||
it('handles undefined totalCost in normal result', async () => {
|
||||
const result = {
|
||||
profile: 'kimi',
|
||||
cwd: '/test',
|
||||
@@ -296,7 +292,7 @@ describe('ResultFormatter', () => {
|
||||
numTurns: 2
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
const formatted = await ResultFormatter.format(result);
|
||||
|
||||
assert.ok(formatted.includes('[OK]'));
|
||||
assert.ok(!formatted.includes('Cost: $'));
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* UI Module Unit Tests
|
||||
*
|
||||
* Tests the central UI abstraction layer for CLI styling
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
describe('UI Module', function () {
|
||||
let ui;
|
||||
|
||||
before(async function () {
|
||||
// Dynamic import for ESM module - import the built dist file
|
||||
const uiModule = await import('../../../dist/utils/ui.js');
|
||||
ui = uiModule;
|
||||
|
||||
// Initialize UI dependencies
|
||||
await ui.initUI();
|
||||
});
|
||||
|
||||
describe('Color System', function () {
|
||||
it('should apply semantic colors to text', function () {
|
||||
const result = ui.color('test message', 'success');
|
||||
assert.ok(result.includes('test message'), 'should contain original text');
|
||||
});
|
||||
|
||||
it('should return plain text when NO_COLOR is set', function () {
|
||||
const originalNoColor = process.env.NO_COLOR;
|
||||
process.env.NO_COLOR = '1';
|
||||
|
||||
const result = ui.color('test', 'success');
|
||||
assert.strictEqual(result, 'test', 'should return unmodified text');
|
||||
|
||||
// Restore
|
||||
if (originalNoColor === undefined) {
|
||||
delete process.env.NO_COLOR;
|
||||
} else {
|
||||
process.env.NO_COLOR = originalNoColor;
|
||||
}
|
||||
});
|
||||
|
||||
it('should apply bold formatting', function () {
|
||||
const result = ui.bold('bold text');
|
||||
assert.ok(result.includes('bold text'), 'should contain original text');
|
||||
});
|
||||
|
||||
it('should apply dim formatting', function () {
|
||||
const result = ui.dim('dim text');
|
||||
assert.ok(result.includes('dim text'), 'should contain original text');
|
||||
});
|
||||
|
||||
it('should apply gradient to text', function () {
|
||||
const result = ui.gradientText('gradient header');
|
||||
assert.ok(result.includes('gradient header'), 'should contain original text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status Indicators', function () {
|
||||
it('should format success message with [OK]', function () {
|
||||
const result = ui.ok('Operation successful');
|
||||
assert.ok(result.includes('[OK]'), 'should include [OK] indicator');
|
||||
assert.ok(result.includes('Operation successful'), 'should include message');
|
||||
});
|
||||
|
||||
it('should format error message with [X]', function () {
|
||||
const result = ui.fail('Operation failed');
|
||||
assert.ok(result.includes('[X]'), 'should include [X] indicator');
|
||||
assert.ok(result.includes('Operation failed'), 'should include message');
|
||||
});
|
||||
|
||||
it('should format warning message with [!]', function () {
|
||||
const result = ui.warn('Warning detected');
|
||||
assert.ok(result.includes('[!]'), 'should include [!] indicator');
|
||||
assert.ok(result.includes('Warning detected'), 'should include message');
|
||||
});
|
||||
|
||||
it('should format info message with [i]', function () {
|
||||
const result = ui.info('Information');
|
||||
assert.ok(result.includes('[i]'), 'should include [i] indicator');
|
||||
assert.ok(result.includes('Information'), 'should include message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Box Rendering', function () {
|
||||
it('should render box with content', function () {
|
||||
const result = ui.box('Box content');
|
||||
assert.ok(result.includes('Box content'), 'should include content');
|
||||
});
|
||||
|
||||
it('should render error box', function () {
|
||||
const result = ui.errorBox('Error message');
|
||||
assert.ok(result.includes('Error message'), 'should include error message');
|
||||
assert.ok(result.includes('ERROR'), 'should include ERROR title');
|
||||
});
|
||||
|
||||
it('should render info box', function () {
|
||||
const result = ui.infoBox('Info message', 'Title');
|
||||
assert.ok(result.includes('Info message'), 'should include info message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table Rendering', function () {
|
||||
it('should render table with rows and headers', function () {
|
||||
const rows = [
|
||||
['Row1', 'Data1'],
|
||||
['Row2', 'Data2'],
|
||||
];
|
||||
const result = ui.table(rows, { head: ['Column1', 'Column2'] });
|
||||
|
||||
assert.ok(result.includes('Row1'), 'should include first row');
|
||||
assert.ok(result.includes('Data1'), 'should include first data');
|
||||
assert.ok(result.includes('Row2'), 'should include second row');
|
||||
assert.ok(result.includes('Column1'), 'should include header');
|
||||
});
|
||||
|
||||
it('should render table without headers', function () {
|
||||
const rows = [
|
||||
['Test', 'Value'],
|
||||
['Test2', 'Value2'],
|
||||
];
|
||||
const result = ui.table(rows);
|
||||
|
||||
// Should contain data and unicode box characters
|
||||
assert.ok(result.includes('Test'), 'should include data');
|
||||
assert.ok(result.includes('│') || result.includes('|'), 'should include vertical separators');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Headers', function () {
|
||||
it('should format section header', function () {
|
||||
const result = ui.header('Section Title');
|
||||
assert.ok(result.includes('Section Title'), 'should include title text');
|
||||
});
|
||||
|
||||
it('should format subsection header', function () {
|
||||
const result = ui.subheader('Subsection');
|
||||
assert.ok(result.includes('Subsection'), 'should include subsection text');
|
||||
});
|
||||
|
||||
it('should render horizontal rule', function () {
|
||||
const result = ui.hr('─', 40);
|
||||
assert.ok(result.length >= 40, 'should be at least specified width');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interactive Detection', function () {
|
||||
it('should detect interactive mode', function () {
|
||||
// This depends on test environment
|
||||
const result = ui.isInteractive();
|
||||
assert.strictEqual(typeof result, 'boolean', 'should return boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NO_COLOR Compliance', function () {
|
||||
it('should disable colors when NO_COLOR is set', function () {
|
||||
const originalNoColor = process.env.NO_COLOR;
|
||||
process.env.NO_COLOR = '1';
|
||||
|
||||
// All color functions should return plain text
|
||||
assert.strictEqual(ui.color('text', 'success'), 'text');
|
||||
assert.strictEqual(ui.bold('text'), 'text');
|
||||
assert.strictEqual(ui.dim('text'), 'text');
|
||||
assert.strictEqual(ui.gradientText('text'), 'text');
|
||||
|
||||
// Restore
|
||||
if (originalNoColor === undefined) {
|
||||
delete process.env.NO_COLOR;
|
||||
} else {
|
||||
process.env.NO_COLOR = originalNoColor;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user