Merge pull request #4162 from jeremymcs/claude/refactor-code-cleanup-078AQ
Refactor CLI arg parsing and consolidate shared helpers
This commit is contained in:
commit
5958184e2a
8 changed files with 420 additions and 328 deletions
122
package-lock.json
generated
122
package-lock.json
generated
|
|
@ -41,7 +41,6 @@
|
|||
"mime-types": "^3.0.1",
|
||||
"minimatch": "^10.2.3",
|
||||
"openai": "^6.26.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.3",
|
||||
"playwright": "^1.58.2",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
|
|
@ -899,7 +898,6 @@
|
|||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
|
|
@ -2616,7 +2614,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
|
||||
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.3",
|
||||
|
|
@ -2854,7 +2851,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -2868,7 +2866,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -2882,7 +2881,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -2896,7 +2896,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -2910,7 +2911,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -2924,7 +2926,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -2938,7 +2941,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -2952,7 +2956,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -2966,7 +2971,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -2980,7 +2986,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -2994,7 +3001,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3008,7 +3016,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3022,7 +3031,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3036,7 +3046,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3050,7 +3061,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3064,7 +3076,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3078,7 +3091,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3092,7 +3106,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3106,7 +3121,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3120,7 +3136,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3134,7 +3151,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3148,7 +3166,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3162,7 +3181,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3176,7 +3196,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.59.0",
|
||||
|
|
@ -3190,7 +3211,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@sapphire/async-queue": {
|
||||
"version": "1.5.5",
|
||||
|
|
@ -4292,7 +4314,8 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/hosted-git-info": {
|
||||
"version": "3.0.5",
|
||||
|
|
@ -4363,7 +4386,6 @@
|
|||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
|
|
@ -4687,7 +4709,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
|
|
@ -5560,7 +5581,6 @@
|
|||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
|
|
@ -5731,6 +5751,7 @@
|
|||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
|
|
@ -6245,7 +6266,6 @@
|
|||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
|
||||
"integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
|
|
@ -7104,6 +7124,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
|
|
@ -7427,6 +7448,7 @@
|
|||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
|
|
@ -7434,7 +7456,6 @@
|
|||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -7515,6 +7536,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
|
|
@ -7692,7 +7714,6 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -7702,7 +7723,6 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
|
|
@ -7816,6 +7836,7 @@
|
|||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
|
|
@ -8376,6 +8397,7 @@
|
|||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
|
|
@ -8670,6 +8692,7 @@
|
|||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8687,6 +8710,7 @@
|
|||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8704,6 +8728,7 @@
|
|||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8721,6 +8746,7 @@
|
|||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8738,6 +8764,7 @@
|
|||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8755,6 +8782,7 @@
|
|||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8772,6 +8800,7 @@
|
|||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8789,6 +8818,7 @@
|
|||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8806,6 +8836,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8823,6 +8854,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8840,6 +8872,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8857,6 +8890,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8874,6 +8908,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8891,6 +8926,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8908,6 +8944,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8925,6 +8962,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8942,6 +8980,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8959,6 +8998,7 @@
|
|||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8976,6 +9016,7 @@
|
|||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8993,6 +9034,7 @@
|
|||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -9010,6 +9052,7 @@
|
|||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -9027,6 +9070,7 @@
|
|||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -9044,6 +9088,7 @@
|
|||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -9061,6 +9106,7 @@
|
|||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -9078,6 +9124,7 @@
|
|||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -9095,6 +9142,7 @@
|
|||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -9106,6 +9154,7 @@
|
|||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
|
|
@ -9334,7 +9383,6 @@
|
|||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,6 @@
|
|||
"mime-types": "^3.0.1",
|
||||
"minimatch": "^10.2.3",
|
||||
"openai": "^6.26.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.3",
|
||||
"playwright": "^1.58.2",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { homedir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
export const appRoot = join(homedir(), '.gsd')
|
||||
export const agentDir = join(appRoot, 'agent')
|
||||
export const sessionsDir = join(appRoot, 'sessions')
|
||||
export const authFilePath = join(agentDir, 'auth.json')
|
||||
export const webPidFilePath = join(appRoot, 'web-server.pid')
|
||||
|
|
@ -5,10 +5,11 @@ import { getProjectSessionsDir } from './project-sessions.js'
|
|||
import { launchWebMode, stopWebMode, type WebModeLaunchStatus, type WebModeStopOptions, type WebModeStopResult } from './web-mode.js'
|
||||
|
||||
export interface CliFlags {
|
||||
mode?: 'text' | 'json' | 'rpc'
|
||||
mode?: 'text' | 'json' | 'rpc' | 'mcp'
|
||||
print?: boolean
|
||||
continue?: boolean
|
||||
noSession?: boolean
|
||||
worktree?: boolean | string
|
||||
model?: string
|
||||
listModels?: string | true
|
||||
extensions: string[]
|
||||
|
|
@ -24,8 +25,9 @@ export interface CliFlags {
|
|||
webPort?: number
|
||||
/** Additional allowed origins for CORS: `--allowed-origins http://192.168.1.10:8080` */
|
||||
webAllowedOrigins?: string[]
|
||||
help?: boolean
|
||||
version?: boolean
|
||||
|
||||
/** Set by `gsd sessions` when the user picks a specific session to resume */
|
||||
_selectedSessionPath?: string
|
||||
}
|
||||
|
||||
type WritableLike = Pick<typeof process.stderr, 'write'>
|
||||
|
|
@ -47,13 +49,20 @@ export function parseCliArgs(argv: string[]): CliFlags {
|
|||
const arg = args[i]
|
||||
if (arg === '--mode' && i + 1 < args.length) {
|
||||
const mode = args[++i]
|
||||
if (mode === 'text' || mode === 'json' || mode === 'rpc') flags.mode = mode
|
||||
if (mode === 'text' || mode === 'json' || mode === 'rpc' || mode === 'mcp') flags.mode = mode
|
||||
} else if (arg === '--print' || arg === '-p') {
|
||||
flags.print = true
|
||||
} else if (arg === '--continue' || arg === '-c') {
|
||||
flags.continue = true
|
||||
} else if (arg === '--no-session') {
|
||||
flags.noSession = true
|
||||
} else if (arg === '--worktree' || arg === '-w') {
|
||||
// -w with no value → auto-generate name; -w <name> → use that name
|
||||
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
||||
flags.worktree = args[++i]
|
||||
} else {
|
||||
flags.worktree = true
|
||||
}
|
||||
} else if (arg === '--web') {
|
||||
flags.web = true
|
||||
// Peek at next arg — if it looks like a path (not another flag), capture it
|
||||
|
|
@ -81,10 +90,6 @@ export function parseCliArgs(argv: string[]): CliFlags {
|
|||
flags.tools = args[++i].split(',')
|
||||
} else if (arg === '--list-models') {
|
||||
flags.listModels = (i + 1 < args.length && !args[i + 1].startsWith('-')) ? args[++i] : true
|
||||
} else if (arg === '--version' || arg === '-v') {
|
||||
flags.version = true
|
||||
} else if (arg === '--help' || arg === '-h') {
|
||||
flags.help = true
|
||||
} else if (!arg.startsWith('--') && !arg.startsWith('-')) {
|
||||
flags.messages.push(arg)
|
||||
}
|
||||
|
|
|
|||
328
src/cli.ts
328
src/cli.ts
|
|
@ -16,14 +16,15 @@ import { agentDir, sessionsDir, authFilePath } from './app-paths.js'
|
|||
import { initResources, buildResourceLoader, getNewerManagedResourceVersion } from './resource-loader.js'
|
||||
import { ensureManagedTools } from './tool-bootstrap.js'
|
||||
import { loadStoredEnvKeys } from './wizard.js'
|
||||
import { migratePiCredentials, getPiDefaultModelAndProvider } from './pi-migration.js'
|
||||
import { migratePiCredentials } from './pi-migration.js'
|
||||
import { shouldRunOnboarding, runOnboarding } from './onboarding.js'
|
||||
import chalk from 'chalk'
|
||||
import { checkForUpdates } from './update-check.js'
|
||||
import { printHelp, printSubcommandHelp } from './help-text.js'
|
||||
import { applySecurityOverrides } from './security-overrides.js'
|
||||
import { validateConfiguredModel } from './startup-model-validation.js'
|
||||
import {
|
||||
parseCliArgs as parseWebCliArgs,
|
||||
parseCliArgs,
|
||||
runWebCliBranch,
|
||||
migrateLegacyFlatSessions,
|
||||
} from './cli-web-branch.js'
|
||||
|
|
@ -42,28 +43,6 @@ if (parseInt(process.versions.node) >= 22) {
|
|||
process.env.NODE_COMPILE_CACHE ??= join(agentDir, '.compile-cache')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Minimal CLI arg parser — detects print/subagent mode flags
|
||||
// ---------------------------------------------------------------------------
|
||||
interface CliFlags {
|
||||
mode?: 'text' | 'json' | 'rpc' | 'mcp'
|
||||
print?: boolean
|
||||
continue?: boolean
|
||||
noSession?: boolean
|
||||
worktree?: boolean | string
|
||||
model?: string
|
||||
listModels?: string | true
|
||||
extensions: string[]
|
||||
appendSystemPrompt?: string
|
||||
tools?: string[]
|
||||
messages: string[]
|
||||
web?: boolean
|
||||
webPath?: string
|
||||
|
||||
/** Set by `gsd sessions` when the user picks a specific session to resume */
|
||||
_selectedSessionPath?: string
|
||||
}
|
||||
|
||||
function exitIfManagedResourcesAreNewer(currentAgentDir: string): void {
|
||||
const currentVersion = process.env.GSD_VERSION || '0.0.0'
|
||||
const managedVersion = getNewerManagedResourceVersion(currentAgentDir, currentVersion)
|
||||
|
|
@ -79,124 +58,112 @@ function exitIfManagedResourcesAreNewer(currentAgentDir: string): void {
|
|||
process.exit(1)
|
||||
}
|
||||
|
||||
function parseCliArgs(argv: string[]): CliFlags {
|
||||
const flags: CliFlags = { extensions: [], messages: [] }
|
||||
const args = argv.slice(2) // skip node + script
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i]
|
||||
if (arg === '--mode' && i + 1 < args.length) {
|
||||
const m = args[++i]
|
||||
if (m === 'text' || m === 'json' || m === 'rpc' || m === 'mcp') flags.mode = m
|
||||
} else if (arg === '--print' || arg === '-p') {
|
||||
flags.print = true
|
||||
} else if (arg === '--continue' || arg === '-c') {
|
||||
flags.continue = true
|
||||
} else if (arg === '--no-session') {
|
||||
flags.noSession = true
|
||||
} else if (arg === '--model' && i + 1 < args.length) {
|
||||
flags.model = args[++i]
|
||||
} else if (arg === '--extension' && i + 1 < args.length) {
|
||||
flags.extensions.push(args[++i])
|
||||
} else if (arg === '--append-system-prompt' && i + 1 < args.length) {
|
||||
flags.appendSystemPrompt = args[++i]
|
||||
} else if (arg === '--tools' && i + 1 < args.length) {
|
||||
flags.tools = args[++i].split(',')
|
||||
} else if (arg === '--list-models') {
|
||||
flags.listModels = (i + 1 < args.length && !args[i + 1].startsWith('-')) ? args[++i] : true
|
||||
} else if (arg === '--version' || arg === '-v') {
|
||||
process.stdout.write((process.env.GSD_VERSION || '0.0.0') + '\n')
|
||||
process.exit(0)
|
||||
} else if (arg === '--worktree' || arg === '-w') {
|
||||
// -w with no value → auto-generate name; -w <name> → use that name
|
||||
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
||||
flags.worktree = args[++i]
|
||||
} else {
|
||||
flags.worktree = true
|
||||
}
|
||||
} else if (arg === '--help' || arg === '-h') {
|
||||
printHelp(process.env.GSD_VERSION || '0.0.0')
|
||||
process.exit(0)
|
||||
} else if (arg === '--web') {
|
||||
flags.web = true
|
||||
// Capture optional project path after --web (not a flag)
|
||||
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
||||
flags.webPath = args[++i]
|
||||
}
|
||||
} else if (!arg.startsWith('--') && !arg.startsWith('-')) {
|
||||
flags.messages.push(arg)
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers used by both the print and interactive code paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Print the non-interactive-mode error and exit. Called both from the early
|
||||
* TTY gate (before heavy init) and from the interactive-mode TTY gate right
|
||||
* before `InteractiveMode.run()`. The `includeWebHint` variant also lists
|
||||
* `--web` and `headless` as alternatives.
|
||||
*/
|
||||
function printNonTtyErrorAndExit(missing: string | undefined, includeWebHint: boolean): never {
|
||||
const suffix = missing ? ` but ${missing} not a TTY` : ''
|
||||
process.stderr.write(`[gsd] Error: Interactive mode requires a terminal (TTY)${suffix}.\n`)
|
||||
process.stderr.write('[gsd] Non-interactive alternatives:\n')
|
||||
process.stderr.write('[gsd] gsd auto Auto-mode (pipeable, no TUI)\n')
|
||||
process.stderr.write('[gsd] gsd --print "your message" Single-shot prompt\n')
|
||||
if (includeWebHint) {
|
||||
process.stderr.write('[gsd] gsd --web [path] Browser-only web mode\n')
|
||||
}
|
||||
return flags
|
||||
process.stderr.write('[gsd] gsd --mode rpc JSON-RPC over stdin/stdout\n')
|
||||
process.stderr.write('[gsd] gsd --mode mcp MCP server over stdin/stdout\n')
|
||||
process.stderr.write('[gsd] gsd --mode text "message" Text output mode\n')
|
||||
if (includeWebHint) {
|
||||
process.stderr.write('[gsd] gsd headless Auto-mode without TUI\n')
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the configured default model against the registry and reset it if
|
||||
* it no longer exists. Must run AFTER extensions have registered their
|
||||
* providers so that extension models (e.g. pi-claude-cli) are visible.
|
||||
* Print extension load/conflict errors from an extensions result. Downgrades
|
||||
* conflicts with built-in tools to warnings (#1347).
|
||||
*/
|
||||
function validateConfiguredModel(
|
||||
function printExtensionErrors(errors: ReadonlyArray<{ error: string }>): void {
|
||||
for (const err of errors) {
|
||||
const isConflict = err.error.includes('supersedes') || err.error.includes('conflicts with')
|
||||
const prefix = isConflict ? 'Extension conflict' : 'Extension load error'
|
||||
process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-apply the validated model to the session when `createAgentSession()`
|
||||
* reports that it had to use a fallback. Prevents silently overriding the
|
||||
* persisted model of resumed conversations (#3534).
|
||||
*/
|
||||
async function reapplyValidatedModelOnFallback(
|
||||
session: { setModel(model: { provider: string; id: string }): unknown | Promise<unknown> },
|
||||
modelRegistry: ModelRegistry,
|
||||
settingsManager: SettingsManager,
|
||||
): void {
|
||||
const configuredProvider = settingsManager.getDefaultProvider()
|
||||
const configuredModel = settingsManager.getDefaultModel()
|
||||
const allModels = modelRegistry.getAll()
|
||||
const availableModels = modelRegistry.getAvailable()
|
||||
const configuredExists = configuredProvider && configuredModel &&
|
||||
allModels.some((m) => m.provider === configuredProvider && m.id === configuredModel)
|
||||
const configuredAvailable = configuredProvider && configuredModel &&
|
||||
availableModels.some((m) => m.provider === configuredProvider && m.id === configuredModel)
|
||||
|
||||
if (!configuredModel || !configuredExists) {
|
||||
// Model not configured at all, or removed from registry — pick a fallback.
|
||||
// Only fires when the model is genuinely unknown (not just temporarily unavailable).
|
||||
const piDefault = getPiDefaultModelAndProvider()
|
||||
const preferred =
|
||||
(piDefault
|
||||
? availableModels.find((m) => m.provider === piDefault.provider && m.id === piDefault.model)
|
||||
: undefined) ||
|
||||
availableModels.find((m) => m.provider === 'openai' && m.id === 'gpt-5.4') ||
|
||||
availableModels.find((m) => m.provider === 'openai') ||
|
||||
availableModels.find((m) => m.provider === 'anthropic' && m.id === 'claude-opus-4-6') ||
|
||||
availableModels.find((m) => m.provider === 'anthropic' && m.id.includes('opus')) ||
|
||||
availableModels.find((m) => m.provider === 'anthropic') ||
|
||||
availableModels[0]
|
||||
if (preferred) {
|
||||
settingsManager.setDefaultModelAndProvider(preferred.provider, preferred.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsManager.getDefaultThinkingLevel() !== 'off' && !configuredExists) {
|
||||
settingsManager.setDefaultThinkingLevel('off')
|
||||
fallbackMessage: string | undefined,
|
||||
): Promise<void> {
|
||||
if (!fallbackMessage) return
|
||||
const validatedProvider = settingsManager.getDefaultProvider()
|
||||
const validatedModelId = settingsManager.getDefaultModel()
|
||||
if (!validatedProvider || !validatedModelId) return
|
||||
const correctModel = modelRegistry.getAvailable()
|
||||
.find((m) => m.provider === validatedProvider && m.id === validatedModelId)
|
||||
if (!correctModel) return
|
||||
try {
|
||||
await session.setModel(correctModel)
|
||||
} catch {
|
||||
// Provider not ready — leave session on its current model
|
||||
}
|
||||
}
|
||||
|
||||
const cliFlags = parseCliArgs(process.argv)
|
||||
const isPrintMode = cliFlags.print || cliFlags.mode !== undefined
|
||||
|
||||
// Early resource-skew check — must run before TTY gate so version mismatch
|
||||
// errors surface even in non-TTY environments.
|
||||
async function ensureRtkBootstrap(): Promise<void> {
|
||||
if ((ensureRtkBootstrap as { _done?: boolean })._done) return
|
||||
// `gsd [subcommand] --help` / `-h` — print help before any subcommand runs.
|
||||
// loader.ts only catches --help/-h as the *first* arg; here we handle the
|
||||
// case where it appears later (e.g. `gsd update --help`, `gsd --foo --help`).
|
||||
// Prefer subcommand-specific help when the first positional is a known
|
||||
// subcommand, otherwise fall back to general help.
|
||||
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
||||
const helpSubcommand = cliFlags.messages[0]
|
||||
const version = process.env.GSD_VERSION || '0.0.0'
|
||||
if (!helpSubcommand || !printSubcommandHelp(helpSubcommand, version)) {
|
||||
printHelp(version)
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// RTK bootstrap — runs once per process, memoized via a module-level promise
|
||||
// so concurrent callers await the same initialization.
|
||||
let rtkBootstrapPromise: Promise<void> | undefined
|
||||
async function doRtkBootstrap(): Promise<void> {
|
||||
// RTK is opt-in via experimental.rtk preference. Default: disabled.
|
||||
// Honor GSD_RTK_DISABLED if already explicitly set in the environment
|
||||
// (env var takes precedence over preferences for manual override).
|
||||
if (!process.env[GSD_RTK_DISABLED_ENV]) {
|
||||
const prefs = loadEffectiveGSDPreferences();
|
||||
const rtkEnabled = prefs?.preferences.experimental?.rtk === true;
|
||||
const prefs = loadEffectiveGSDPreferences()
|
||||
const rtkEnabled = prefs?.preferences.experimental?.rtk === true
|
||||
if (!rtkEnabled) {
|
||||
process.env[GSD_RTK_DISABLED_ENV] = "1";
|
||||
process.env[GSD_RTK_DISABLED_ENV] = '1'
|
||||
}
|
||||
}
|
||||
|
||||
const rtkStatus = await bootstrapRtk()
|
||||
;(ensureRtkBootstrap as { _done?: boolean })._done = true
|
||||
markStartup('bootstrapRtk')
|
||||
if (!rtkStatus.available && rtkStatus.supported && rtkStatus.enabled && rtkStatus.reason) {
|
||||
process.stderr.write(`[gsd] Warning: RTK unavailable — continuing without shell-command compression (${rtkStatus.reason}).\n`)
|
||||
}
|
||||
}
|
||||
function ensureRtkBootstrap(): Promise<void> {
|
||||
return (rtkBootstrapPromise ??= doRtkBootstrap())
|
||||
}
|
||||
|
||||
// `gsd update` — update to the latest version via npm
|
||||
if (cliFlags.messages[0] === 'update') {
|
||||
|
|
@ -211,22 +178,7 @@ exitIfManagedResourcesAreNewer(agentDir)
|
|||
// handles that prevent process.exit() from completing promptly.
|
||||
const hasSubcommand = cliFlags.messages.length > 0
|
||||
if (!process.stdin.isTTY && !isPrintMode && !hasSubcommand && !cliFlags.listModels && !cliFlags.web) {
|
||||
process.stderr.write('[gsd] Error: Interactive mode requires a terminal (TTY).\n')
|
||||
process.stderr.write('[gsd] Non-interactive alternatives:\n')
|
||||
process.stderr.write('[gsd] gsd auto Auto-mode (pipeable, no TUI)\n')
|
||||
process.stderr.write('[gsd] gsd --print "your message" Single-shot prompt\n')
|
||||
process.stderr.write('[gsd] gsd --mode rpc JSON-RPC over stdin/stdout\n')
|
||||
process.stderr.write('[gsd] gsd --mode mcp MCP server over stdin/stdout\n')
|
||||
process.stderr.write('[gsd] gsd --mode text "message" Text output mode\n')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// `gsd <subcommand> --help` — show subcommand-specific help
|
||||
const subcommand = cliFlags.messages[0]
|
||||
if (subcommand && process.argv.includes('--help')) {
|
||||
if (printSubcommandHelp(subcommand, process.env.GSD_VERSION || '0.0.0')) {
|
||||
process.exit(0)
|
||||
}
|
||||
printNonTtyErrorAndExit(undefined, false)
|
||||
}
|
||||
|
||||
const packageCommand = await runPackageCommand({
|
||||
|
|
@ -252,8 +204,7 @@ if (cliFlags.messages[0] === 'config') {
|
|||
|
||||
// `gsd web stop [path|all]` — stop web server before anything else
|
||||
if (cliFlags.messages[0] === 'web' && cliFlags.messages[1] === 'stop') {
|
||||
const webFlags = parseWebCliArgs(process.argv)
|
||||
const webBranch = await runWebCliBranch(webFlags, {
|
||||
const webBranch = await runWebCliBranch(cliFlags, {
|
||||
stopWebMode,
|
||||
stderr: process.stderr,
|
||||
baseSessionsDir: sessionsDir,
|
||||
|
|
@ -267,8 +218,7 @@ if (cliFlags.messages[0] === 'web' && cliFlags.messages[1] === 'stop') {
|
|||
// `gsd --web [path]` or `gsd web [start] [path]` — launch browser-only web mode
|
||||
if (cliFlags.web || (cliFlags.messages[0] === 'web' && cliFlags.messages[1] !== 'stop')) {
|
||||
await ensureRtkBootstrap()
|
||||
const webFlags = parseWebCliArgs(process.argv)
|
||||
const webBranch = await runWebCliBranch(webFlags, {
|
||||
const webBranch = await runWebCliBranch(cliFlags, {
|
||||
stderr: process.stderr,
|
||||
baseSessionsDir: sessionsDir,
|
||||
agentDir,
|
||||
|
|
@ -356,21 +306,24 @@ if (cliFlags.messages[0] === 'headless') {
|
|||
process.exit(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a headless command by invoking the headless entrypoint with a synthetic
|
||||
* argv. Shared by the `auto` shorthand (#2732) and the auto-piped-stdout
|
||||
* redirect so they use the same bootstrap + dynamic-import dance.
|
||||
*/
|
||||
async function runHeadlessFromAuto(headlessArgs: string[]): Promise<never> {
|
||||
await ensureRtkBootstrap()
|
||||
const { runHeadless, parseHeadlessArgs } = await import('./headless.js')
|
||||
const argv = [process.argv[0], process.argv[1], 'headless', ...headlessArgs]
|
||||
await runHeadless(parseHeadlessArgs(argv))
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// `gsd auto [args...]` — shorthand for `gsd headless auto [args...]` (#2732)
|
||||
// Without this, `gsd auto` falls through to the interactive TUI which hangs
|
||||
// when stdin/stdout are piped (non-TTY environments).
|
||||
if (cliFlags.messages[0] === 'auto') {
|
||||
await ensureRtkBootstrap()
|
||||
const { runHeadless, parseHeadlessArgs } = await import('./headless.js')
|
||||
// Rewrite argv so parseHeadlessArgs sees: [node, gsd, headless, auto, ...rest]
|
||||
const rewrittenArgv = [
|
||||
process.argv[0],
|
||||
process.argv[1],
|
||||
'headless',
|
||||
...cliFlags.messages, // ['auto', ...extra args]
|
||||
]
|
||||
await runHeadless(parseHeadlessArgs(rewrittenArgv))
|
||||
process.exit(0)
|
||||
await runHeadlessFromAuto(cliFlags.messages)
|
||||
}
|
||||
|
||||
// Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
|
||||
|
|
@ -535,39 +488,8 @@ if (isPrintMode) {
|
|||
// Before this, extension-provided models (e.g. claude-code/*) were not yet in the
|
||||
// registry, causing the user's valid choice to be silently overwritten.
|
||||
validateConfiguredModel(modelRegistry, settingsManager)
|
||||
|
||||
// Re-apply the validated model to the session only when findInitialModel() used a
|
||||
// fallback (not when restoring an existing session's model). This prevents silently
|
||||
// overriding the persisted model of resumed conversations (#3534).
|
||||
if (modelFallbackMessage) {
|
||||
const validatedProvider = settingsManager.getDefaultProvider()
|
||||
const validatedModelId = settingsManager.getDefaultModel()
|
||||
if (validatedProvider && validatedModelId) {
|
||||
const correctModel = modelRegistry.getAvailable()
|
||||
.find((m) => m.provider === validatedProvider && m.id === validatedModelId)
|
||||
if (correctModel) {
|
||||
try {
|
||||
await session.setModel(correctModel)
|
||||
} catch {
|
||||
// Provider not ready — leave session on its current model
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionsResult.errors.length > 0) {
|
||||
for (const err of extensionsResult.errors) {
|
||||
// Downgrade conflicts with built-in tools to warnings (#1347)
|
||||
const isConflict = err.error.includes("supersedes") || err.error.includes("conflicts with");
|
||||
const prefix = isConflict ? "Extension conflict" : "Extension load error";
|
||||
process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate configured model now that extension providers are registered.
|
||||
// Must run after createAgentSession() which flushes pendingProviderRegistrations
|
||||
// so extension models (e.g. pi-claude-cli) are visible in the registry.
|
||||
validateConfiguredModel(modelRegistry, settingsManager)
|
||||
await reapplyValidatedModelOnFallback(session, modelRegistry, settingsManager, modelFallbackMessage)
|
||||
printExtensionErrors(extensionsResult.errors)
|
||||
|
||||
// Apply --model override if specified
|
||||
if (cliFlags.model) {
|
||||
|
|
@ -666,11 +588,8 @@ if (!cliFlags.worktree && !isPrintMode) {
|
|||
// which handles non-interactive output gracefully.
|
||||
// ---------------------------------------------------------------------------
|
||||
if (cliFlags.messages[0] === 'auto' && !process.stdout.isTTY) {
|
||||
await ensureRtkBootstrap()
|
||||
const { runHeadless, parseHeadlessArgs } = await import('./headless.js')
|
||||
process.stderr.write('[gsd] stdout is not a terminal — running auto-mode in headless mode.\n')
|
||||
await runHeadless(parseHeadlessArgs(['node', 'gsd', 'headless', ...cliFlags.messages.slice(1)]))
|
||||
process.exit(0)
|
||||
await runHeadlessFromAuto(cliFlags.messages.slice(1))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -724,38 +643,8 @@ markStartup('createAgentSession')
|
|||
// Before this, extension-provided models (e.g. claude-code/*) were not yet in the
|
||||
// registry, causing the user's valid choice to be silently overwritten.
|
||||
validateConfiguredModel(modelRegistry, settingsManager)
|
||||
|
||||
// Re-apply the validated model to the session only when findInitialModel() used a
|
||||
// fallback (not when restoring an existing session's model). This prevents silently
|
||||
// overriding the persisted model of resumed conversations (#3534).
|
||||
if (interactiveFallbackMsg) {
|
||||
const validatedProvider = settingsManager.getDefaultProvider()
|
||||
const validatedModelId = settingsManager.getDefaultModel()
|
||||
if (validatedProvider && validatedModelId) {
|
||||
const correctModel = modelRegistry.getAvailable()
|
||||
.find((m) => m.provider === validatedProvider && m.id === validatedModelId)
|
||||
if (correctModel) {
|
||||
try {
|
||||
await session.setModel(correctModel)
|
||||
} catch {
|
||||
// Provider not ready — leave session on its current model
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionsResult.errors.length > 0) {
|
||||
for (const err of extensionsResult.errors) {
|
||||
const isConflict = err.error.includes("supersedes") || err.error.includes("conflicts with");
|
||||
const prefix = isConflict ? "Extension conflict" : "Extension load error";
|
||||
process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate configured model now that extension providers are registered.
|
||||
// Must run after createAgentSession() which flushes pendingProviderRegistrations
|
||||
// so extension models (e.g. pi-claude-cli) are visible in the registry.
|
||||
validateConfiguredModel(modelRegistry, settingsManager)
|
||||
await reapplyValidatedModelOnFallback(session, modelRegistry, settingsManager, interactiveFallbackMsg)
|
||||
printExtensionErrors(extensionsResult.errors)
|
||||
|
||||
// Restore scoped models from settings on startup.
|
||||
// The upstream InteractiveMode reads enabledModels from settings when /scoped-models is opened,
|
||||
|
|
@ -806,16 +695,7 @@ if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|||
: !process.stdin.isTTY
|
||||
? 'stdin is'
|
||||
: 'stdout is'
|
||||
process.stderr.write(`[gsd] Error: Interactive mode requires a terminal (TTY) but ${missing} not a TTY.\n`)
|
||||
process.stderr.write('[gsd] Non-interactive alternatives:\n')
|
||||
process.stderr.write('[gsd] gsd auto Auto-mode (pipeable, no TUI)\n')
|
||||
process.stderr.write('[gsd] gsd --print "your message" Single-shot prompt\n')
|
||||
process.stderr.write('[gsd] gsd --web [path] Browser-only web mode\n')
|
||||
process.stderr.write('[gsd] gsd --mode rpc JSON-RPC over stdin/stdout\n')
|
||||
process.stderr.write('[gsd] gsd --mode mcp MCP server over stdin/stdout\n')
|
||||
process.stderr.write('[gsd] gsd --mode text "message" Text output mode\n')
|
||||
process.stderr.write('[gsd] gsd headless Auto-mode without TUI\n')
|
||||
process.exit(1)
|
||||
printNonTtyErrorAndExit(missing, true)
|
||||
}
|
||||
|
||||
// Welcome screen — shown on every fresh interactive session before TUI takes over.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const GSD_LOGO: readonly string[] = [
|
|||
/**
|
||||
* Render the logo block with a color function applied to each line.
|
||||
*
|
||||
* @param color — e.g. `(s) => `\x1b[36m${s}\x1b[0m`` or picocolors.cyan
|
||||
* @param color — e.g. `(s) => `\x1b[36m${s}\x1b[0m`` or chalk.cyan
|
||||
* @returns Ready-to-write string with leading/trailing newlines.
|
||||
*/
|
||||
export function renderLogo(color: (s: string) => string): string {
|
||||
|
|
|
|||
|
|
@ -100,8 +100,8 @@ const OTHER_PROVIDERS = [
|
|||
// ─── Dynamic imports ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Dynamically import @clack/prompts and picocolors.
|
||||
* Dynamic import with fallback so the module doesn't crash if they're missing.
|
||||
* Dynamically import @clack/prompts.
|
||||
* Dynamic import with fallback so the module doesn't crash if it's missing.
|
||||
*/
|
||||
async function loadClack(): Promise<ClackModule> {
|
||||
try {
|
||||
|
|
@ -111,10 +111,23 @@ async function loadClack(): Promise<ClackModule> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the PicoModule color surface from chalk. Chalk is already a
|
||||
* dependency of the CLI; this adapter keeps the onboarding call sites stable
|
||||
* while removing the redundant picocolors dep.
|
||||
*/
|
||||
async function loadPico(): Promise<PicoModule> {
|
||||
try {
|
||||
const mod = await import('picocolors')
|
||||
return mod.default ?? mod
|
||||
const { default: chalk } = await import('chalk')
|
||||
return {
|
||||
cyan: (s: string) => chalk.cyan(s),
|
||||
green: (s: string) => chalk.green(s),
|
||||
yellow: (s: string) => chalk.yellow(s),
|
||||
dim: (s: string) => chalk.dim(s),
|
||||
bold: (s: string) => chalk.bold(s),
|
||||
red: (s: string) => chalk.red(s),
|
||||
reset: (s: string) => chalk.reset(s),
|
||||
}
|
||||
} catch {
|
||||
// Fallback: return identity functions
|
||||
const identity = (s: string) => s
|
||||
|
|
@ -135,9 +148,34 @@ function openBrowser(url: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
/** Check if an error is a clack cancel signal */
|
||||
function isCancelError(p: ClackModule, err: unknown): boolean {
|
||||
return p.isCancel(err)
|
||||
/** Sentinel returned by runStep when the user cancels — tells the caller
|
||||
* to abort the entire wizard. */
|
||||
const STEP_CANCELLED = Symbol('step-cancelled')
|
||||
type StepCancelled = typeof STEP_CANCELLED
|
||||
|
||||
/**
|
||||
* Run a single onboarding step with shared error handling:
|
||||
* - user cancel (Ctrl+C) → p.cancel(cancelMessage), returns STEP_CANCELLED
|
||||
* - other error → p.log.warn + optional info follow-up, returns null
|
||||
* - success → the step's return value
|
||||
*/
|
||||
async function runStep<T>(
|
||||
p: ClackModule,
|
||||
warnLabel: string,
|
||||
fn: () => Promise<T>,
|
||||
opts: { cancelMessage?: string; errorInfo?: string } = {},
|
||||
): Promise<T | null | StepCancelled> {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (err) {
|
||||
if (p.isCancel(err)) {
|
||||
p.cancel(opts.cancelMessage ?? 'Setup cancelled.')
|
||||
return STEP_CANCELLED
|
||||
}
|
||||
p.log.warn(`${warnLabel}: ${err instanceof Error ? err.message : String(err)}`)
|
||||
if (opts.errorInfo) p.log.info(opts.errorInfo)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Public API ───────────────────────────────────────────────────────────────
|
||||
|
|
@ -191,54 +229,30 @@ export async function runOnboarding(authStorage: AuthStorage): Promise<void> {
|
|||
p.intro(pc.bold('Welcome to GSD — let\'s get you set up'))
|
||||
|
||||
// ── LLM Provider Selection ────────────────────────────────────────────────
|
||||
let llmConfigured = false
|
||||
try {
|
||||
llmConfigured = await runLlmStep(p, pc, authStorage)
|
||||
} catch (err) {
|
||||
// User cancelled (Ctrl+C in clack throws) or unexpected error
|
||||
if (isCancelError(p, err)) {
|
||||
p.cancel('Setup cancelled — you can run /login inside GSD later.')
|
||||
return
|
||||
}
|
||||
p.log.warn(`LLM setup failed: ${err instanceof Error ? err.message : String(err)}`)
|
||||
p.log.info('You can configure your LLM provider later with /login inside GSD.')
|
||||
}
|
||||
const llmResult = await runStep(p, 'LLM setup failed', () => runLlmStep(p, pc, authStorage), {
|
||||
cancelMessage: 'Setup cancelled — you can run /login inside GSD later.',
|
||||
errorInfo: 'You can configure your LLM provider later with /login inside GSD.',
|
||||
})
|
||||
if (llmResult === STEP_CANCELLED) return
|
||||
const llmConfigured = llmResult ?? false
|
||||
|
||||
// ── Web Search Provider ──────────────────────────────────────────────────
|
||||
let searchConfigured: string | null = null
|
||||
try {
|
||||
searchConfigured = await runWebSearchStep(p, pc, authStorage, llmConfigured)
|
||||
} catch (err) {
|
||||
if (isCancelError(p, err)) {
|
||||
p.cancel('Setup cancelled.')
|
||||
return
|
||||
}
|
||||
p.log.warn(`Web search setup failed: ${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
const searchResult = await runStep(p, 'Web search setup failed',
|
||||
() => runWebSearchStep(p, pc, authStorage, llmConfigured))
|
||||
if (searchResult === STEP_CANCELLED) return
|
||||
const searchConfigured = searchResult
|
||||
|
||||
// ── Remote Questions ─────────────────────────────────────────────────────
|
||||
let remoteConfigured: string | null = null
|
||||
try {
|
||||
remoteConfigured = await runRemoteQuestionsStep(p, pc, authStorage)
|
||||
} catch (err) {
|
||||
if (isCancelError(p, err)) {
|
||||
p.cancel('Setup cancelled.')
|
||||
return
|
||||
}
|
||||
p.log.warn(`Remote questions setup failed: ${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
const remoteResult = await runStep(p, 'Remote questions setup failed',
|
||||
() => runRemoteQuestionsStep(p, pc, authStorage))
|
||||
if (remoteResult === STEP_CANCELLED) return
|
||||
const remoteConfigured = remoteResult
|
||||
|
||||
// ── Tool API Keys ─────────────────────────────────────────────────────────
|
||||
let toolKeyCount = 0
|
||||
try {
|
||||
toolKeyCount = await runToolKeysStep(p, pc, authStorage)
|
||||
} catch (err) {
|
||||
if (isCancelError(p, err)) {
|
||||
p.cancel('Setup cancelled.')
|
||||
return
|
||||
}
|
||||
p.log.warn(`Tool key setup failed: ${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
const toolResult = await runStep(p, 'Tool key setup failed',
|
||||
() => runToolKeysStep(p, pc, authStorage))
|
||||
if (toolResult === STEP_CANCELLED) return
|
||||
const toolKeyCount = toolResult ?? 0
|
||||
|
||||
// ── Summary ───────────────────────────────────────────────────────────────
|
||||
const summaryLines: string[] = []
|
||||
|
|
|
|||
154
src/tests/parse-cli-args.test.ts
Normal file
154
src/tests/parse-cli-args.test.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
// GSD-2 — Unit tests for parseCliArgs (canonical CLI flag parser)
|
||||
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
||||
|
||||
import test, { describe } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { parseCliArgs } from '../cli-web-branch.ts'
|
||||
|
||||
function parse(...args: string[]) {
|
||||
return parseCliArgs(['node', 'gsd', ...args])
|
||||
}
|
||||
|
||||
describe('parseCliArgs — modes', () => {
|
||||
test('accepts mcp mode (added during refactor)', () => {
|
||||
assert.equal(parse('--mode', 'mcp').mode, 'mcp')
|
||||
})
|
||||
|
||||
test('still accepts text/json/rpc modes', () => {
|
||||
assert.equal(parse('--mode', 'text').mode, 'text')
|
||||
assert.equal(parse('--mode', 'json').mode, 'json')
|
||||
assert.equal(parse('--mode', 'rpc').mode, 'rpc')
|
||||
})
|
||||
|
||||
test('ignores unknown mode values', () => {
|
||||
assert.equal(parse('--mode', 'bogus').mode, undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseCliArgs — worktree flag', () => {
|
||||
test('-w with no value sets worktree=true', () => {
|
||||
assert.equal(parse('-w').worktree, true)
|
||||
})
|
||||
|
||||
test('--worktree with no value sets worktree=true', () => {
|
||||
assert.equal(parse('--worktree').worktree, true)
|
||||
})
|
||||
|
||||
test('-w followed by a name captures the name', () => {
|
||||
assert.equal(parse('-w', 'feature-x').worktree, 'feature-x')
|
||||
})
|
||||
|
||||
test('--worktree followed by a name captures the name', () => {
|
||||
assert.equal(parse('--worktree', 'feature-x').worktree, 'feature-x')
|
||||
})
|
||||
|
||||
test('-w followed by another flag does not consume the flag', () => {
|
||||
const flags = parse('-w', '--print')
|
||||
assert.equal(flags.worktree, true)
|
||||
assert.equal(flags.print, true)
|
||||
})
|
||||
|
||||
test('worktree is undefined when flag not passed', () => {
|
||||
assert.equal(parse('hello').worktree, undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseCliArgs — short flags and basic options', () => {
|
||||
test('-p sets print', () => {
|
||||
assert.equal(parse('-p').print, true)
|
||||
})
|
||||
|
||||
test('--print sets print', () => {
|
||||
assert.equal(parse('--print').print, true)
|
||||
})
|
||||
|
||||
test('-c sets continue', () => {
|
||||
assert.equal(parse('-c').continue, true)
|
||||
})
|
||||
|
||||
test('--no-session sets noSession', () => {
|
||||
assert.equal(parse('--no-session').noSession, true)
|
||||
})
|
||||
|
||||
test('--model captures model id', () => {
|
||||
assert.equal(parse('--model', 'claude-opus-4-6').model, 'claude-opus-4-6')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseCliArgs — list flags and accumulators', () => {
|
||||
test('--extension accumulates multiple values', () => {
|
||||
const flags = parse('--extension', 'a', '--extension', 'b')
|
||||
assert.deepEqual(flags.extensions, ['a', 'b'])
|
||||
})
|
||||
|
||||
test('--tools splits comma-separated list', () => {
|
||||
assert.deepEqual(parse('--tools', 'read,write,edit').tools, ['read', 'write', 'edit'])
|
||||
})
|
||||
|
||||
test('--list-models with no value sets to true', () => {
|
||||
assert.equal(parse('--list-models').listModels, true)
|
||||
})
|
||||
|
||||
test('--list-models with provider filter captures provider', () => {
|
||||
assert.equal(parse('--list-models', 'anthropic').listModels, 'anthropic')
|
||||
})
|
||||
|
||||
test('--list-models followed by another flag does not consume it', () => {
|
||||
const flags = parse('--list-models', '--print')
|
||||
assert.equal(flags.listModels, true)
|
||||
assert.equal(flags.print, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseCliArgs — web mode flags', () => {
|
||||
test('--web with no path sets web=true', () => {
|
||||
const flags = parse('--web')
|
||||
assert.equal(flags.web, true)
|
||||
assert.equal(flags.webPath, undefined)
|
||||
})
|
||||
|
||||
test('--web with a path captures it', () => {
|
||||
const flags = parse('--web', '/tmp/project')
|
||||
assert.equal(flags.web, true)
|
||||
assert.equal(flags.webPath, '/tmp/project')
|
||||
})
|
||||
|
||||
test('--port parses valid integer', () => {
|
||||
assert.equal(parse('--port', '8080').webPort, 8080)
|
||||
})
|
||||
|
||||
test('--port rejects non-numeric', () => {
|
||||
assert.equal(parse('--port', 'abc').webPort, undefined)
|
||||
})
|
||||
|
||||
test('--port rejects out-of-range values', () => {
|
||||
assert.equal(parse('--port', '0').webPort, undefined)
|
||||
assert.equal(parse('--port', '70000').webPort, undefined)
|
||||
})
|
||||
|
||||
test('--allowed-origins splits and trims comma list', () => {
|
||||
assert.deepEqual(
|
||||
parse('--allowed-origins', 'http://a.com, http://b.com ,http://c.com').webAllowedOrigins,
|
||||
['http://a.com', 'http://b.com', 'http://c.com'],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseCliArgs — positional messages', () => {
|
||||
test('non-flag positional args become messages', () => {
|
||||
const flags = parse('hello', 'world')
|
||||
assert.deepEqual(flags.messages, ['hello', 'world'])
|
||||
})
|
||||
|
||||
test('messages and flags can be interleaved', () => {
|
||||
const flags = parse('hello', '--print', 'world')
|
||||
assert.deepEqual(flags.messages, ['hello', 'world'])
|
||||
assert.equal(flags.print, true)
|
||||
})
|
||||
|
||||
test('default messages and extensions are empty arrays', () => {
|
||||
const flags = parse()
|
||||
assert.deepEqual(flags.messages, [])
|
||||
assert.deepEqual(flags.extensions, [])
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue