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 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 always puts the `iterate` CLI on your PATH so a single
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, use a manual install instead — copy `SKILL.md` + `config/` +
48
- > `scripts/` + `templates/` to your assistant dir, or run
49
- > `python scripts/install.py install` directly. If the CLI install fails, you can
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
- > **注意:** 安装器总是会把你 PATH 上的 `iterate` CLI 装好,所以一条命令就能同时
46
- > 得到技能和 CLI。如果**不**想自动安装 CLI,请改用手动安装——把
47
- > `SKILL.md` + `config/` + `scripts/` + `templates/` 复制到你的助手目录,或直接运行
48
- > `python scripts/install.py install`。如果 CLI 安装失败,你之后仍可从检出目录用
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
- // 2. Validate every entry stays inside destDir after stripping the single
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
- const resolved = path.resolve(absoluteDest, stripped || entry);
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
- // 3. Now extract safely — the content has been validated above.
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 { global: globalInstall = true, ai = null, token = null, target = null, force = false } = options;
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
- // Install the iterate CLI so `iterate onboard` works right after install.
499
- await installCli(pythonBin, sourceDir);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-skill-installer",
3
- "version": "2.4.1",
3
+ "version": "2.4.5",
4
4
  "description": "One-command installer for iterate-skill across AI coding assistants",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {
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) => {