Merge pull request #35 from kaitranntt/feat/cli-ui-ux-enhancement

Merge feat/cli-ui-ux-enhancement into beta
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2025-12-02 15:22:13 -05:00
commit d726384553
21 files changed
+2535 -1167

No files matched your search

+3 -1
View File
@@ -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
+107 -33
View File
@@ -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
View File
@@ -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
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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}`);
}
+485
View File
@@ -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
View File
@@ -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);
}
-423
View File
@@ -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);
}
}
+2 -2
View File
@@ -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);
+8 -4
View File
@@ -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
}
+83 -142
View File
@@ -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
View File
@@ -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('');
+61
View File
@@ -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
View File
@@ -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('');
}
}
+13
View File
@@ -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();
+2 -2
View File
@@ -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
View File
@@ -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;
+33 -37
View File
@@ -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: $'));
+173
View File
@@ -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;
}
});
});
});