iterate-skill-installer 2.4.1 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +19 -5
- package/README.zh-CN.md +17 -5
- package/bin/cli.js +15 -75
- package/lib/installer.js +151 -7
- package/package.json +1 -1
- package/test/mode.test.js +24 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jingzhao L
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -26,6 +26,13 @@ npx iterate-skill-installer --target ./my-project
|
|
|
26
26
|
|
|
27
27
|
# Force overwrite existing skill files
|
|
28
28
|
npx iterate-skill-installer --force
|
|
29
|
+
|
|
30
|
+
# Skill-only install — skip installing the iterate CLI
|
|
31
|
+
npx iterate-skill-installer --no-cli
|
|
32
|
+
|
|
33
|
+
# Show help / version
|
|
34
|
+
npx iterate-skill-installer --help
|
|
35
|
+
npx iterate-skill-installer --version
|
|
29
36
|
```
|
|
30
37
|
|
|
31
38
|
## What it does
|
|
@@ -42,12 +49,11 @@ npx iterate-skill-installer --force
|
|
|
42
49
|
7. Installs the `iterate` CLI onto your PATH (prefers `pipx`, otherwise
|
|
43
50
|
`pip install --user`) so you can run `iterate onboard` directly.
|
|
44
51
|
|
|
45
|
-
> **Note:** the installer
|
|
52
|
+
> **Note:** the installer normally puts the `iterate` CLI on your PATH so a single
|
|
46
53
|
> command gives you both the skill and the CLI. If you **don't** want the CLI
|
|
47
|
-
> auto-installed,
|
|
48
|
-
> `
|
|
49
|
-
>
|
|
50
|
-
> still install it later with `pipx install .` or `pip install .` from a checkout.
|
|
54
|
+
> auto-installed, pass `--no-cli` to install the skill only — you can install the
|
|
55
|
+
> CLI later with `pipx install .` or `pip install .` from a checkout. If the CLI
|
|
56
|
+
> install fails, you can still install it later the same way.
|
|
51
57
|
|
|
52
58
|
## Supported AI assistants
|
|
53
59
|
|
|
@@ -59,6 +65,14 @@ Trae, Claude / Claude Code, Cursor, Windsurf, GitHub Copilot, Codex, Gemini CLI,
|
|
|
59
65
|
- Python 3.10+
|
|
60
66
|
- `tar` command available on PATH (available by default on macOS, Linux, Windows 10+)
|
|
61
67
|
|
|
68
|
+
## Release tarball structure
|
|
69
|
+
|
|
70
|
+
The GitHub release asset `iterate-skill.tar.gz` must contain **exactly one
|
|
71
|
+
top-level directory** (e.g. `iterate-skill/`). The installer extracts it with
|
|
72
|
+
`tar --strip-components=1`, so a tarball with multiple top-level directories,
|
|
73
|
+
or with entries whose path disappears after stripping the top level, is
|
|
74
|
+
rejected instead of being silently mis-extracted.
|
|
75
|
+
|
|
62
76
|
## License
|
|
63
77
|
|
|
64
78
|
MIT
|
package/README.zh-CN.md
CHANGED
|
@@ -26,6 +26,13 @@ npx iterate-skill-installer --target ./my-project
|
|
|
26
26
|
|
|
27
27
|
# 强制覆盖已有技能文件
|
|
28
28
|
npx iterate-skill-installer --force
|
|
29
|
+
|
|
30
|
+
# 仅安装技能 —— 跳过安装 iterate CLI
|
|
31
|
+
npx iterate-skill-installer --no-cli
|
|
32
|
+
|
|
33
|
+
# 查看帮助 / 版本
|
|
34
|
+
npx iterate-skill-installer --help
|
|
35
|
+
npx iterate-skill-installer --version
|
|
29
36
|
```
|
|
30
37
|
|
|
31
38
|
## 它做了什么
|
|
@@ -42,11 +49,10 @@ npx iterate-skill-installer --force
|
|
|
42
49
|
7. 把 `iterate` CLI 安装到你的 PATH(优先 `pipx`,否则 `pip install --user`),
|
|
43
50
|
以便你直接运行 `iterate onboard`。
|
|
44
51
|
|
|
45
|
-
> **注意:**
|
|
46
|
-
> 得到技能和 CLI。如果**不**想自动安装 CLI
|
|
47
|
-
> `
|
|
48
|
-
>
|
|
49
|
-
> `pipx install .` 或 `pip install .` 补装。
|
|
52
|
+
> **注意:** 安装器通常会在你 PATH 上装好 `iterate` CLI,这样一条命令就能同时
|
|
53
|
+
> 得到技能和 CLI。如果**不**想自动安装 CLI,请加 `--no-cli` 只装技能——之后可从
|
|
54
|
+
> 检出目录用 `pipx install .` 或 `pip install .` 补装。如果 CLI 安装失败,你之后
|
|
55
|
+
> 也可用同样的方式补装。
|
|
50
56
|
|
|
51
57
|
## 支持的 AI 助手
|
|
52
58
|
|
|
@@ -58,6 +64,12 @@ Trae、Claude / Claude Code、Cursor、Windsurf、GitHub Copilot、Codex、Gemin
|
|
|
58
64
|
- Python 3.10+
|
|
59
65
|
- PATH 上存在 `tar` 命令(macOS、Linux、Windows 10+ 默认可用)
|
|
60
66
|
|
|
67
|
+
## 发布 tarball 结构
|
|
68
|
+
|
|
69
|
+
GitHub Release 资产 `iterate-skill.tar.gz` 必须**恰好包含一个顶层目录**(例如
|
|
70
|
+
`iterate-skill/`)。安装器使用 `tar --strip-components=1` 解压,因此包含多个顶层
|
|
71
|
+
目录、或条目在剥离顶层后路径为空的 tarball 会被拒绝,而不是被静默错误地解压。
|
|
72
|
+
|
|
61
73
|
## License / 许可证
|
|
62
74
|
|
|
63
75
|
MIT
|
package/bin/cli.js
CHANGED
|
@@ -10,87 +10,14 @@
|
|
|
10
10
|
* npx iterate-skill-installer --ai trae
|
|
11
11
|
* npx iterate-skill-installer --target /path/to/project
|
|
12
12
|
* npx iterate-skill-installer --global
|
|
13
|
+
* npx iterate-skill-installer --no-cli
|
|
13
14
|
*
|
|
14
15
|
* The installer downloads the latest GitHub release, verifies its SHA256
|
|
15
16
|
* checksum, then delegates file copying and assistant selection to the
|
|
16
17
|
* Python install script bundled in the release.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
|
-
const { main } = require('../lib/installer');
|
|
20
|
-
|
|
21
|
-
function parseArgs(argv) {
|
|
22
|
-
const options = {
|
|
23
|
-
global: true,
|
|
24
|
-
ai: null,
|
|
25
|
-
target: null,
|
|
26
|
-
force: false,
|
|
27
|
-
token: process.env.GITHUB_TOKEN || null,
|
|
28
|
-
// Track whether the user explicitly chose global/project mode so the
|
|
29
|
-
// installer can later ask about the current directory's project intent.
|
|
30
|
-
globalExplicit: false,
|
|
31
|
-
targetExplicit: false,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
for (let i = 0; i < argv.length; i++) {
|
|
35
|
-
const arg = argv[i];
|
|
36
|
-
const next = argv[i + 1];
|
|
37
|
-
|
|
38
|
-
switch (arg) {
|
|
39
|
-
case '--ai':
|
|
40
|
-
if (!next) {
|
|
41
|
-
console.error('Error: --ai requires a value');
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
options.ai = next;
|
|
45
|
-
i++;
|
|
46
|
-
break;
|
|
47
|
-
case '--target':
|
|
48
|
-
if (!next) {
|
|
49
|
-
console.error('Error: --target requires a value');
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
options.target = next;
|
|
53
|
-
options.global = false;
|
|
54
|
-
options.targetExplicit = true;
|
|
55
|
-
i++;
|
|
56
|
-
break;
|
|
57
|
-
case '--global':
|
|
58
|
-
options.global = true;
|
|
59
|
-
options.target = null;
|
|
60
|
-
options.globalExplicit = true;
|
|
61
|
-
break;
|
|
62
|
-
case '--force':
|
|
63
|
-
options.force = true;
|
|
64
|
-
break;
|
|
65
|
-
case '--token':
|
|
66
|
-
if (!next) {
|
|
67
|
-
console.error('Error: --token requires a value');
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
options.token = next;
|
|
71
|
-
i++;
|
|
72
|
-
break;
|
|
73
|
-
case '-h':
|
|
74
|
-
case '--help':
|
|
75
|
-
printHelp();
|
|
76
|
-
process.exit(0);
|
|
77
|
-
break;
|
|
78
|
-
case '-v':
|
|
79
|
-
case '--version':
|
|
80
|
-
console.log(require('../package.json').version);
|
|
81
|
-
process.exit(0);
|
|
82
|
-
break;
|
|
83
|
-
default:
|
|
84
|
-
if (arg.startsWith('-')) {
|
|
85
|
-
console.error(`Error: unknown option ${arg}`);
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return options;
|
|
93
|
-
}
|
|
20
|
+
const { main, parseArgs, VERSION } = require('../lib/installer');
|
|
94
21
|
|
|
95
22
|
function printHelp() {
|
|
96
23
|
console.log(`
|
|
@@ -101,6 +28,7 @@ Options:
|
|
|
101
28
|
--target <path> Install into a project directory instead of the global home dir.
|
|
102
29
|
--global Install into the user's home directory (default).
|
|
103
30
|
--force Overwrite existing skill files.
|
|
31
|
+
--no-cli Skip installing the iterate CLI (skill-only install).
|
|
104
32
|
--token <token> GitHub token for higher API rate limits.
|
|
105
33
|
-h, --help Show this help message.
|
|
106
34
|
-v, --version Show version.
|
|
@@ -109,11 +37,23 @@ Examples:
|
|
|
109
37
|
npx iterate-skill-installer
|
|
110
38
|
npx iterate-skill-installer --ai trae
|
|
111
39
|
npx iterate-skill-installer --target ./my-project
|
|
40
|
+
npx iterate-skill-installer --no-cli
|
|
112
41
|
`);
|
|
113
42
|
}
|
|
114
43
|
|
|
115
44
|
async function run() {
|
|
116
45
|
const options = parseArgs(process.argv.slice(2));
|
|
46
|
+
|
|
47
|
+
if (options.mode === 'help') {
|
|
48
|
+
printHelp();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (options.mode === 'version') {
|
|
53
|
+
console.log(`iterate-skill-installer ${VERSION}`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
117
57
|
try {
|
|
118
58
|
const code = await main(options);
|
|
119
59
|
process.exit(code);
|
package/lib/installer.js
CHANGED
|
@@ -38,6 +38,10 @@ const GITHUB_OWNER = 'jingzhao-l';
|
|
|
38
38
|
const GITHUB_REPO = 'iterate-skill';
|
|
39
39
|
const RELEASE_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`;
|
|
40
40
|
|
|
41
|
+
// Package version read from package.json at runtime so the --version/-v flag
|
|
42
|
+
// and the published npm version always stay in sync.
|
|
43
|
+
const VERSION = require('../package.json').version;
|
|
44
|
+
|
|
41
45
|
const ITERATE_BANNER = [
|
|
42
46
|
'██╗████████╗███████╗██████╗ █████╗ ████████╗███████╗',
|
|
43
47
|
'██║╚══██╔══╝██╔════╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝',
|
|
@@ -196,18 +200,48 @@ async function extractTarball(tarballPath, destDir) {
|
|
|
196
200
|
// archive contents and refuse to extract any entry whose resolved path
|
|
197
201
|
// escapes destDir (e.g. a "../" prefix). This keeps the installer
|
|
198
202
|
// dependency-free while closing the theoretical escape vector.
|
|
203
|
+
//
|
|
204
|
+
// Extraction uses --strip-components=1, which relies on the archive having
|
|
205
|
+
// exactly one top-level directory (see README "Release tarball structure").
|
|
206
|
+
// Archives that violate that contract — multiple top-level directories, or
|
|
207
|
+
// entries with no path left after stripping — are rejected outright.
|
|
199
208
|
const absoluteDest = path.resolve(destDir);
|
|
200
209
|
fs.mkdirSync(absoluteDest, { recursive: true });
|
|
201
210
|
|
|
202
211
|
// 1. List archive entries (no extraction yet).
|
|
203
212
|
const listing = await runCommand('tar', ['-tzf', tarballPath]);
|
|
213
|
+
const entries = listing.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
214
|
+
if (entries.length === 0) {
|
|
215
|
+
throw new InstallerError('Refusing to extract tarball: archive contains no entries.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 2. Enforce the single-top-level-directory contract required by
|
|
219
|
+
// --strip-components=1.
|
|
220
|
+
const topLevelComponents = new Set(entries.map((entry) => entry.split('/')[0]));
|
|
221
|
+
if (topLevelComponents.size !== 1) {
|
|
222
|
+
const tops = [...topLevelComponents].join(', ');
|
|
223
|
+
throw new InstallerError(
|
|
224
|
+
`Refusing to extract tarball: expected exactly one top-level directory, found multiple (${tops}).`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const topLevel = entries[0].split('/')[0];
|
|
204
228
|
|
|
205
|
-
//
|
|
229
|
+
// 3. Validate every entry stays inside destDir after stripping the single
|
|
206
230
|
// top-level directory component (--strip-components=1).
|
|
207
|
-
const entries = listing.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
208
231
|
for (const entry of entries) {
|
|
209
232
|
const stripped = entry.split('/').slice(1).join('/');
|
|
210
|
-
|
|
233
|
+
if (stripped === '') {
|
|
234
|
+
// The single top-level directory entry itself maps to destDir after
|
|
235
|
+
// stripping; any other entry that strips to an empty path is malformed.
|
|
236
|
+
const isTopLevelEntry = entry === topLevel || entry === `${topLevel}/`;
|
|
237
|
+
if (!isTopLevelEntry) {
|
|
238
|
+
throw new InstallerError(
|
|
239
|
+
`Refusing to extract tarball: entry "${entry}" has an empty path after stripping the top-level directory.`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const resolved = path.resolve(absoluteDest, stripped);
|
|
211
245
|
if (resolved !== absoluteDest && !resolved.startsWith(absoluteDest + path.sep)) {
|
|
212
246
|
throw new InstallerError(
|
|
213
247
|
`Refusing to extract tarball: entry "${entry}" escapes destination directory.`,
|
|
@@ -215,7 +249,7 @@ async function extractTarball(tarballPath, destDir) {
|
|
|
215
249
|
}
|
|
216
250
|
}
|
|
217
251
|
|
|
218
|
-
//
|
|
252
|
+
// 4. Now extract safely — the content has been validated above.
|
|
219
253
|
await runCommand('tar', ['-xzf', tarballPath, '-C', absoluteDest, '--strip-components=1']);
|
|
220
254
|
}
|
|
221
255
|
|
|
@@ -364,6 +398,97 @@ async function installRequirements(pythonBin, requirementsPath) {
|
|
|
364
398
|
await runCommand(pythonBin, ['-m', 'pip', 'install', '--quiet', '--requirement', requirementsPath]);
|
|
365
399
|
}
|
|
366
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Parse the installer's command-line arguments.
|
|
403
|
+
*
|
|
404
|
+
* Kept as a pure function (exported) so the flag surface — including the
|
|
405
|
+
* skill-only ``--no-cli`` switch — can be unit-tested without spawning the
|
|
406
|
+
* full download-and-install pipeline. ``process.exit`` is only reached for
|
|
407
|
+
* hard argument errors (e.g. a value-consuming flag missing its value).
|
|
408
|
+
*/
|
|
409
|
+
function parseArgs(argv) {
|
|
410
|
+
const options = {
|
|
411
|
+
global: true,
|
|
412
|
+
ai: null,
|
|
413
|
+
target: null,
|
|
414
|
+
force: false,
|
|
415
|
+
noCli: false,
|
|
416
|
+
token: process.env.GITHUB_TOKEN || null,
|
|
417
|
+
// Non-install action requested via -h/--help/-v/--version. bin/cli.js
|
|
418
|
+
// inspects this before running main() and exits 0 without installing.
|
|
419
|
+
mode: null,
|
|
420
|
+
// Track whether the user explicitly chose global/project mode so the
|
|
421
|
+
// installer can later ask about the current directory's project intent.
|
|
422
|
+
globalExplicit: false,
|
|
423
|
+
targetExplicit: false,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
for (let i = 0; i < argv.length; i++) {
|
|
427
|
+
const arg = argv[i];
|
|
428
|
+
const next = argv[i + 1];
|
|
429
|
+
|
|
430
|
+
switch (arg) {
|
|
431
|
+
case '--ai':
|
|
432
|
+
if (!next) {
|
|
433
|
+
console.error('Error: --ai requires a value');
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
options.ai = next;
|
|
437
|
+
i++;
|
|
438
|
+
break;
|
|
439
|
+
case '--target':
|
|
440
|
+
if (!next) {
|
|
441
|
+
console.error('Error: --target requires a value');
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
options.target = next;
|
|
445
|
+
options.global = false;
|
|
446
|
+
options.targetExplicit = true;
|
|
447
|
+
i++;
|
|
448
|
+
break;
|
|
449
|
+
case '--global':
|
|
450
|
+
options.global = true;
|
|
451
|
+
options.target = null;
|
|
452
|
+
options.globalExplicit = true;
|
|
453
|
+
break;
|
|
454
|
+
case '--force':
|
|
455
|
+
options.force = true;
|
|
456
|
+
break;
|
|
457
|
+
case '--no-cli':
|
|
458
|
+
// Skill-only install: skip the automated `iterate` CLI install so a
|
|
459
|
+
// user who only wants the skill is not surprised by a global install.
|
|
460
|
+
options.noCli = true;
|
|
461
|
+
break;
|
|
462
|
+
case '--help':
|
|
463
|
+
case '-h':
|
|
464
|
+
// Show usage and exit 0 (handled by bin/cli.js).
|
|
465
|
+
options.mode = 'help';
|
|
466
|
+
break;
|
|
467
|
+
case '--version':
|
|
468
|
+
case '-v':
|
|
469
|
+
// Print the package version and exit 0 (handled by bin/cli.js).
|
|
470
|
+
options.mode = 'version';
|
|
471
|
+
break;
|
|
472
|
+
case '--token':
|
|
473
|
+
if (!next) {
|
|
474
|
+
console.error('Error: --token requires a value');
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
options.token = next;
|
|
478
|
+
i++;
|
|
479
|
+
break;
|
|
480
|
+
default:
|
|
481
|
+
if (arg.startsWith('-')) {
|
|
482
|
+
console.error(`Error: unknown option ${arg}`);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return options;
|
|
490
|
+
}
|
|
491
|
+
|
|
367
492
|
async function main(options = {}) {
|
|
368
493
|
printBanner();
|
|
369
494
|
|
|
@@ -372,7 +497,14 @@ async function main(options = {}) {
|
|
|
372
497
|
// whether that directory is the target project.
|
|
373
498
|
options = await resolveInstallMode(options);
|
|
374
499
|
|
|
375
|
-
const {
|
|
500
|
+
const {
|
|
501
|
+
global: globalInstall = true,
|
|
502
|
+
ai = null,
|
|
503
|
+
token = null,
|
|
504
|
+
target = null,
|
|
505
|
+
force = false,
|
|
506
|
+
noCli = false,
|
|
507
|
+
} = options;
|
|
376
508
|
|
|
377
509
|
step('Checking environment');
|
|
378
510
|
const pythonBin = await findPython();
|
|
@@ -495,8 +627,18 @@ async function main(options = {}) {
|
|
|
495
627
|
}
|
|
496
628
|
success('iterate-skill installation finished.');
|
|
497
629
|
|
|
498
|
-
|
|
499
|
-
|
|
630
|
+
if (noCli) {
|
|
631
|
+
// Skill-only install: explain how to add the CLI later instead of
|
|
632
|
+
// silently installing it globally.
|
|
633
|
+
hint('Skipped installing the iterate CLI (--no-cli).');
|
|
634
|
+
hint('Install it later for `iterate onboard` with: pipx install <repo> (or) pip install .');
|
|
635
|
+
} else {
|
|
636
|
+
// Install the iterate CLI so `iterate onboard` works right after
|
|
637
|
+
// install. Made explicit up front so skill-only users are not surprised
|
|
638
|
+
// by a global CLI install (skip with --no-cli).
|
|
639
|
+
step('Installing the iterate CLI (skip with --no-cli)');
|
|
640
|
+
await installCli(pythonBin, sourceDir);
|
|
641
|
+
}
|
|
500
642
|
|
|
501
643
|
frameSection('Done', [
|
|
502
644
|
`\x1b[32m✓\x1b[0m iterate-skill ${tag} installed`,
|
|
@@ -510,6 +652,8 @@ async function main(options = {}) {
|
|
|
510
652
|
|
|
511
653
|
module.exports = {
|
|
512
654
|
main,
|
|
655
|
+
parseArgs,
|
|
656
|
+
VERSION,
|
|
513
657
|
ITERATE_BANNER,
|
|
514
658
|
InstallerError,
|
|
515
659
|
resolveInstallMode,
|
package/package.json
CHANGED
package/test/mode.test.js
CHANGED
|
@@ -9,7 +9,7 @@ const assert = require('node:assert');
|
|
|
9
9
|
const fs = require('node:fs');
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
const os = require('node:os');
|
|
12
|
-
const { resolveInstallMode, parseChecksums } = require('../lib/installer');
|
|
12
|
+
const { resolveInstallMode, parseChecksums, parseArgs } = require('../lib/installer');
|
|
13
13
|
|
|
14
14
|
const originalCwd = process.cwd();
|
|
15
15
|
const originalHome = os.homedir;
|
|
@@ -97,6 +97,29 @@ async function run() {
|
|
|
97
97
|
assert.strictEqual(multi.get('b.txt'), 'bb');
|
|
98
98
|
|
|
99
99
|
console.log('mode.test.js: all parseChecksums tests passed');
|
|
100
|
+
|
|
101
|
+
// parseArgs: --no-cli must toggle skill-only mode, and default to off.
|
|
102
|
+
assert.strictEqual(parseArgs([]).noCli, false, 'noCli should default to false');
|
|
103
|
+
assert.strictEqual(parseArgs(['--no-cli']).noCli, true, '--no-cli should set noCli');
|
|
104
|
+
assert.strictEqual(parseArgs(['--no-cli', '--force', '--global']).noCli, true, 'noCli persists with other flags');
|
|
105
|
+
assert.strictEqual(parseArgs(['--global']).noCli, false, 'unrelated flags do not set noCli');
|
|
106
|
+
|
|
107
|
+
// parseArgs: unrelated flag surface is preserved (target/global/ai/force).
|
|
108
|
+
const combo = parseArgs(['--ai', 'trae', '--target', '/proj', '--force']);
|
|
109
|
+
assert.strictEqual(combo.ai, 'trae', 'ai should parse');
|
|
110
|
+
assert.strictEqual(combo.target, '/proj', 'target should parse');
|
|
111
|
+
assert.strictEqual(combo.force, true, 'force should parse');
|
|
112
|
+
assert.strictEqual(combo.global, false, 'target forces project mode');
|
|
113
|
+
|
|
114
|
+
// parseArgs: --version/-v/--help/-h must set the corresponding mode instead
|
|
115
|
+
// of falling through to the unknown-option error.
|
|
116
|
+
assert.strictEqual(parseArgs([]).mode, null, 'mode should default to null');
|
|
117
|
+
assert.strictEqual(parseArgs(['--version']).mode, 'version', '--version should set version mode');
|
|
118
|
+
assert.strictEqual(parseArgs(['-v']).mode, 'version', '-v should set version mode');
|
|
119
|
+
assert.strictEqual(parseArgs(['--help']).mode, 'help', '--help should set help mode');
|
|
120
|
+
assert.strictEqual(parseArgs(['-h']).mode, 'help', '-h should set help mode');
|
|
121
|
+
|
|
122
|
+
console.log('mode.test.js: all parseArgs tests passed');
|
|
100
123
|
}
|
|
101
124
|
|
|
102
125
|
run().catch((err) => {
|