iterate-skill-installer 2.4.3 → 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
@@ -29,6 +29,10 @@ npx iterate-skill-installer --force
29
29
 
30
30
  # Skill-only install — skip installing the iterate CLI
31
31
  npx iterate-skill-installer --no-cli
32
+
33
+ # Show help / version
34
+ npx iterate-skill-installer --help
35
+ npx iterate-skill-installer --version
32
36
  ```
33
37
 
34
38
  ## What it does
@@ -61,6 +65,14 @@ Trae, Claude / Claude Code, Cursor, Windsurf, GitHub Copilot, Codex, Gemini CLI,
61
65
  - Python 3.10+
62
66
  - `tar` command available on PATH (available by default on macOS, Linux, Windows 10+)
63
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
+
64
76
  ## License
65
77
 
66
78
  MIT
package/README.zh-CN.md CHANGED
@@ -29,6 +29,10 @@ npx iterate-skill-installer --force
29
29
 
30
30
  # 仅安装技能 —— 跳过安装 iterate CLI
31
31
  npx iterate-skill-installer --no-cli
32
+
33
+ # 查看帮助 / 版本
34
+ npx iterate-skill-installer --help
35
+ npx iterate-skill-installer --version
32
36
  ```
33
37
 
34
38
  ## 它做了什么
@@ -60,6 +64,12 @@ Trae、Claude / Claude Code、Cursor、Windsurf、GitHub Copilot、Codex、Gemin
60
64
  - Python 3.10+
61
65
  - PATH 上存在 `tar` 命令(macOS、Linux、Windows 10+ 默认可用)
62
66
 
67
+ ## 发布 tarball 结构
68
+
69
+ GitHub Release 资产 `iterate-skill.tar.gz` 必须**恰好包含一个顶层目录**(例如
70
+ `iterate-skill/`)。安装器使用 `tar --strip-components=1` 解压,因此包含多个顶层
71
+ 目录、或条目在剥离顶层后路径为空的 tarball 会被拒绝,而不是被静默错误地解压。
72
+
63
73
  ## License / 许可证
64
74
 
65
75
  MIT
package/bin/cli.js CHANGED
@@ -17,7 +17,7 @@
17
17
  * Python install script bundled in the release.
18
18
  */
19
19
 
20
- const { main, parseArgs } = require('../lib/installer');
20
+ const { main, parseArgs, VERSION } = require('../lib/installer');
21
21
 
22
22
  function printHelp() {
23
23
  console.log(`
@@ -43,6 +43,17 @@ Examples:
43
43
 
44
44
  async function run() {
45
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
+
46
57
  try {
47
58
  const code = await main(options);
48
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
 
@@ -380,6 +414,9 @@ function parseArgs(argv) {
380
414
  force: false,
381
415
  noCli: false,
382
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,
383
420
  // Track whether the user explicitly chose global/project mode so the
384
421
  // installer can later ask about the current directory's project intent.
385
422
  globalExplicit: false,
@@ -422,6 +459,16 @@ function parseArgs(argv) {
422
459
  // user who only wants the skill is not surprised by a global install.
423
460
  options.noCli = true;
424
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;
425
472
  case '--token':
426
473
  if (!next) {
427
474
  console.error('Error: --token requires a value');
@@ -606,6 +653,7 @@ async function main(options = {}) {
606
653
  module.exports = {
607
654
  main,
608
655
  parseArgs,
656
+ VERSION,
609
657
  ITERATE_BANNER,
610
658
  InstallerError,
611
659
  resolveInstallMode,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-skill-installer",
3
- "version": "2.4.3",
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
@@ -111,6 +111,14 @@ async function run() {
111
111
  assert.strictEqual(combo.force, true, 'force should parse');
112
112
  assert.strictEqual(combo.global, false, 'target forces project mode');
113
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
+
114
122
  console.log('mode.test.js: all parseArgs tests passed');
115
123
  }
116
124