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 +21 -0
- package/README.md +12 -0
- package/README.zh-CN.md +10 -0
- package/bin/cli.js +12 -1
- package/lib/installer.js +52 -4
- package/package.json +1 -1
- package/test/mode.test.js +8 -0
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
|
-
//
|
|
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
|
|
|
@@ -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
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
|
|