iterate-skill-installer 2.9.0 → 2.10.0
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/lib/installer.js +75 -25
- package/package.json +1 -1
- package/test/mode.test.js +70 -1
package/lib/installer.js
CHANGED
|
@@ -38,6 +38,15 @@ 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
|
+
// Release asset filenames — named constants so the magic strings are not
|
|
42
|
+
// scattered through the download / verify pipeline.
|
|
43
|
+
const TARBALL_ASSET_NAME = 'iterate-skill.tar.gz';
|
|
44
|
+
const CHECKSUMS_ASSET_NAME = 'SHA256SUMS.txt';
|
|
45
|
+
|
|
46
|
+
// Hard cap on a single curl download: a misbehaving CDN or a corrupt release
|
|
47
|
+
// must never hang the installer forever.
|
|
48
|
+
const CURL_MAX_TIME_SECONDS = '120';
|
|
49
|
+
|
|
41
50
|
// Package version read from package.json at runtime so the --version/-v flag
|
|
42
51
|
// and the published npm version always stay in sync.
|
|
43
52
|
const VERSION = require('../package.json').version;
|
|
@@ -127,7 +136,7 @@ function commandExists(bin) {
|
|
|
127
136
|
async function fetchJson(url, token) {
|
|
128
137
|
// Prefer curl over Node.js fetch because curl uses the system CA store
|
|
129
138
|
// and avoids Node-specific certificate issues in some environments.
|
|
130
|
-
const args = ['-sSL', '--fail-with-body', '-H', 'Accept: application/vnd.github+json', '-H', 'X-GitHub-Api-Version: 2022-11-28', '-H', 'User-Agent: iterate-skill-installer'];
|
|
139
|
+
const args = ['-sSL', '--fail-with-body', '--max-time', CURL_MAX_TIME_SECONDS, '-H', 'Accept: application/vnd.github+json', '-H', 'X-GitHub-Api-Version: 2022-11-28', '-H', 'User-Agent: iterate-skill-installer'];
|
|
131
140
|
if (token) {
|
|
132
141
|
args.push('-H', `Authorization: Bearer ${token}`);
|
|
133
142
|
}
|
|
@@ -140,13 +149,26 @@ async function fetchJson(url, token) {
|
|
|
140
149
|
}
|
|
141
150
|
}
|
|
142
151
|
|
|
143
|
-
|
|
144
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Download a file with curl.
|
|
154
|
+
*
|
|
155
|
+
* When ``progress`` is enabled (used for the tarball), curl's ``--progress-bar``
|
|
156
|
+
* writes a live percentage bar straight to the terminal (stderr is inherited so
|
|
157
|
+
* the bar is visible; stdout stays captured for non-progress downloads).
|
|
158
|
+
*/
|
|
159
|
+
async function downloadFile(url, destPath, token, { progress = false } = {}) {
|
|
160
|
+
const args = ['-sSL', '--fail-with-body', '--max-time', CURL_MAX_TIME_SECONDS, '-o', destPath, '-H', 'User-Agent: iterate-skill-installer'];
|
|
161
|
+
if (progress) args.push('--progress-bar');
|
|
145
162
|
if (token) {
|
|
146
163
|
args.push('-H', `Authorization: Bearer ${token}`);
|
|
147
164
|
}
|
|
148
165
|
args.push(url);
|
|
149
|
-
|
|
166
|
+
if (progress) {
|
|
167
|
+
// Let curl write its progress bar directly to the terminal.
|
|
168
|
+
await runCommand('curl', args, { stdio: ['ignore', 'ignore', 'inherit'] });
|
|
169
|
+
} else {
|
|
170
|
+
await runCommand('curl', args);
|
|
171
|
+
}
|
|
150
172
|
}
|
|
151
173
|
|
|
152
174
|
function sha256File(filePath) {
|
|
@@ -166,11 +188,14 @@ function parseChecksums(text) {
|
|
|
166
188
|
if (!trimmed) continue;
|
|
167
189
|
const parts = trimmed.split(/\s+/);
|
|
168
190
|
if (parts.length >= 2) {
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
191
|
+
// Normalize the filename the same way scripts/install.py does
|
|
192
|
+
// (_parse_checksum): strip the GNU tar binary-mode marker ('*') and the
|
|
193
|
+
// './' prefix, then match on the basename so entries with a subpath or
|
|
194
|
+
// versioned component (e.g. "dist/iterate-skill.tar.gz" or
|
|
195
|
+
// "./iterate-skill.tar.gz") still resolve to the expected asset.
|
|
196
|
+
const normalized = parts[1].replace(/^\*/, '').replace(/^\.\//, '');
|
|
197
|
+
const basename = normalized.split('/').pop();
|
|
198
|
+
map.set(basename, parts[0]);
|
|
174
199
|
}
|
|
175
200
|
}
|
|
176
201
|
return map;
|
|
@@ -412,7 +437,7 @@ async function resolveInstallMode(options, ask = askYesNo) {
|
|
|
412
437
|
const home = os.homedir();
|
|
413
438
|
if (cwd !== home) {
|
|
414
439
|
const answer = await ask(
|
|
415
|
-
`当前目录 ${cwd} 看起来是项目目录,是否安装到此项目?(
|
|
440
|
+
`当前目录 ${cwd} 看起来是项目目录,是否安装到此项目?(输入 n 则全局安装到用户目录)`,
|
|
416
441
|
);
|
|
417
442
|
if (answer) {
|
|
418
443
|
options.target = cwd;
|
|
@@ -437,6 +462,25 @@ async function installRequirements(pythonBin, requirementsPath) {
|
|
|
437
462
|
await runCommand(pythonBin, ['-m', 'pip', 'install', '--quiet', '--requirement', requirementsPath]);
|
|
438
463
|
}
|
|
439
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Build the argv passed to the Python installer (scripts/install.py install).
|
|
467
|
+
*
|
|
468
|
+
* Exported as a pure function so the flag surface — including the relative
|
|
469
|
+
* ``--target`` resolution — can be unit-tested without running the full
|
|
470
|
+
* download pipeline. A relative ``--target`` is resolved against the current
|
|
471
|
+
* working directory here because the Python installer runs with ``cwd`` set
|
|
472
|
+
* to the extracted release directory; passing the raw relative path would
|
|
473
|
+
* make it point at the wrong place.
|
|
474
|
+
*/
|
|
475
|
+
function buildPythonInstallArgs(options) {
|
|
476
|
+
const args = [];
|
|
477
|
+
if (options.ai) args.push('--ai', options.ai);
|
|
478
|
+
if (options.target) args.push('--target', path.resolve(options.target));
|
|
479
|
+
if (options.force) args.push('--force');
|
|
480
|
+
if (options.globalInstall) args.push('--global');
|
|
481
|
+
return args;
|
|
482
|
+
}
|
|
483
|
+
|
|
440
484
|
/**
|
|
441
485
|
* Parse the installer's command-line arguments.
|
|
442
486
|
*
|
|
@@ -480,12 +524,18 @@ function parseArgs(argv) {
|
|
|
480
524
|
console.error('Error: --target requires a value (a flag was found instead)');
|
|
481
525
|
process.exit(1);
|
|
482
526
|
}
|
|
527
|
+
if (options.globalExplicit) {
|
|
528
|
+
warning('--target overrides --global: installing into the project directory instead.');
|
|
529
|
+
}
|
|
483
530
|
options.target = next;
|
|
484
531
|
options.global = false;
|
|
485
532
|
options.targetExplicit = true;
|
|
486
533
|
i++;
|
|
487
534
|
break;
|
|
488
535
|
case '--global':
|
|
536
|
+
if (options.targetExplicit) {
|
|
537
|
+
warning('--global overrides --target: installing into the user home directory instead.');
|
|
538
|
+
}
|
|
489
539
|
options.global = true;
|
|
490
540
|
options.target = null;
|
|
491
541
|
options.globalExplicit = true;
|
|
@@ -521,6 +571,9 @@ function parseArgs(argv) {
|
|
|
521
571
|
console.error(`Error: unknown option ${arg}`);
|
|
522
572
|
process.exit(1);
|
|
523
573
|
}
|
|
574
|
+
// A bare positional argument is not a valid installer flag; warn so a
|
|
575
|
+
// typo like "npx iterate-skill-installer trae" is not silently ignored.
|
|
576
|
+
warning(`Ignoring unexpected positional argument: ${arg}`);
|
|
524
577
|
break;
|
|
525
578
|
}
|
|
526
579
|
}
|
|
@@ -575,34 +628,34 @@ async function main(options = {}) {
|
|
|
575
628
|
}
|
|
576
629
|
success(`Latest release: ${tag}`);
|
|
577
630
|
|
|
578
|
-
const tarballAsset = release.assets?.find((a) => a.name ===
|
|
579
|
-
const checksumAsset = release.assets?.find((a) => a.name ===
|
|
631
|
+
const tarballAsset = release.assets?.find((a) => a.name === TARBALL_ASSET_NAME);
|
|
632
|
+
const checksumAsset = release.assets?.find((a) => a.name === CHECKSUMS_ASSET_NAME);
|
|
580
633
|
|
|
581
634
|
if (!tarballAsset) {
|
|
582
|
-
error(
|
|
635
|
+
error(`Release is missing the ${TARBALL_ASSET_NAME} asset.`);
|
|
583
636
|
return 1;
|
|
584
637
|
}
|
|
585
638
|
if (!checksumAsset) {
|
|
586
|
-
error(
|
|
639
|
+
error(`Release is missing ${CHECKSUMS_ASSET_NAME} asset.`);
|
|
587
640
|
return 1;
|
|
588
641
|
}
|
|
589
642
|
|
|
590
643
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'iterate-skill-install-'));
|
|
591
|
-
const tarballPath = path.join(tmpDir,
|
|
592
|
-
const checksumPath = path.join(tmpDir,
|
|
644
|
+
const tarballPath = path.join(tmpDir, TARBALL_ASSET_NAME);
|
|
645
|
+
const checksumPath = path.join(tmpDir, CHECKSUMS_ASSET_NAME);
|
|
593
646
|
|
|
594
647
|
try {
|
|
595
|
-
info(
|
|
596
|
-
await downloadFile(tarballAsset.browser_download_url, tarballPath, token);
|
|
648
|
+
info(`Downloading release tarball (${TARBALL_ASSET_NAME})...`);
|
|
649
|
+
await downloadFile(tarballAsset.browser_download_url, tarballPath, token, { progress: true });
|
|
597
650
|
|
|
598
651
|
info('Downloading checksums...');
|
|
599
652
|
await downloadFile(checksumAsset.browser_download_url, checksumPath, token);
|
|
600
653
|
|
|
601
654
|
info('Verifying checksum...');
|
|
602
655
|
const checksums = parseChecksums(fs.readFileSync(checksumPath, 'utf8'));
|
|
603
|
-
const expectedHash = checksums.get(
|
|
656
|
+
const expectedHash = checksums.get(TARBALL_ASSET_NAME);
|
|
604
657
|
if (!expectedHash) {
|
|
605
|
-
error(
|
|
658
|
+
error(`Checksum file does not contain ${TARBALL_ASSET_NAME}`);
|
|
606
659
|
return 1;
|
|
607
660
|
}
|
|
608
661
|
const actualHash = await sha256File(tarballPath);
|
|
@@ -649,11 +702,7 @@ async function main(options = {}) {
|
|
|
649
702
|
return 1;
|
|
650
703
|
}
|
|
651
704
|
|
|
652
|
-
const installArgs =
|
|
653
|
-
if (ai) installArgs.push('--ai', ai);
|
|
654
|
-
if (target) installArgs.push('--target', target);
|
|
655
|
-
if (force) installArgs.push('--force');
|
|
656
|
-
if (globalInstall) installArgs.push('--global');
|
|
705
|
+
const installArgs = buildPythonInstallArgs({ ai, target, force, globalInstall });
|
|
657
706
|
|
|
658
707
|
info('Starting skill installation (Python installer)...');
|
|
659
708
|
const installExit = await runPythonInstall(venvPython, installScript, installArgs, { cwd: sourceDir });
|
|
@@ -692,6 +741,7 @@ async function main(options = {}) {
|
|
|
692
741
|
module.exports = {
|
|
693
742
|
main,
|
|
694
743
|
parseArgs,
|
|
744
|
+
buildPythonInstallArgs,
|
|
695
745
|
VERSION,
|
|
696
746
|
ITERATE_BANNER,
|
|
697
747
|
InstallerError,
|
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, parseArgs } = require('../lib/installer');
|
|
12
|
+
const { resolveInstallMode, parseChecksums, parseArgs, buildPythonInstallArgs } = require('../lib/installer');
|
|
13
13
|
|
|
14
14
|
const originalCwd = process.cwd();
|
|
15
15
|
const originalHome = os.homedir;
|
|
@@ -96,6 +96,17 @@ async function run() {
|
|
|
96
96
|
assert.strictEqual(multi.get('a.txt'), 'aa');
|
|
97
97
|
assert.strictEqual(multi.get('b.txt'), 'bb');
|
|
98
98
|
|
|
99
|
+
// './' prefix and subpath/basename matching must resolve like the Python
|
|
100
|
+
// side (scripts/install.py _parse_checksum), which matches on basename.
|
|
101
|
+
const dotSlash = parseChecksums('abc123 ./iterate-skill.tar.gz\n');
|
|
102
|
+
assert.strictEqual(dotSlash.get('iterate-skill.tar.gz'), 'abc123', './ prefix should parse');
|
|
103
|
+
|
|
104
|
+
const subpath = parseChecksums('abc123 dist/iterate-skill.tar.gz\n');
|
|
105
|
+
assert.strictEqual(subpath.get('iterate-skill.tar.gz'), 'abc123', 'subpath entries should match by basename');
|
|
106
|
+
|
|
107
|
+
const starredSubpath = parseChecksums('abc123 *./release/iterate-skill-1.2.3.tar.gz\n');
|
|
108
|
+
assert.strictEqual(starredSubpath.get('iterate-skill-1.2.3.tar.gz'), 'abc123', '*./ and versioned subpath should match by basename');
|
|
109
|
+
|
|
99
110
|
console.log('mode.test.js: all parseChecksums tests passed');
|
|
100
111
|
|
|
101
112
|
// parseArgs: --no-cli must toggle skill-only mode, and default to off.
|
|
@@ -137,7 +148,65 @@ async function run() {
|
|
|
137
148
|
process.exit = origExit;
|
|
138
149
|
}
|
|
139
150
|
|
|
151
|
+
// parseArgs: a bare positional argument must produce a warning instead of
|
|
152
|
+
// being silently ignored (fix: typos like "npx iterate-skill-installer trae").
|
|
153
|
+
const origLog = console.log;
|
|
154
|
+
const warnings = [];
|
|
155
|
+
console.log = (msg) => { warnings.push(msg); };
|
|
156
|
+
try {
|
|
157
|
+
const opts = parseArgs(['trae', '--force']);
|
|
158
|
+
assert.strictEqual(opts.force, true, 'known flags still parse alongside positionals');
|
|
159
|
+
assert.ok(
|
|
160
|
+
warnings.some((w) => w.includes('Ignoring unexpected positional argument: trae')),
|
|
161
|
+
`positional arg should warn, got: ${JSON.stringify(warnings)}`,
|
|
162
|
+
);
|
|
163
|
+
} finally {
|
|
164
|
+
console.log = origLog;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// parseArgs: --global + --target together must warn that the later flag wins.
|
|
168
|
+
const conflictLogs = [];
|
|
169
|
+
console.log = (msg) => { conflictLogs.push(msg); };
|
|
170
|
+
try {
|
|
171
|
+
const targetThenGlobal = parseArgs(['--target', '/proj', '--global']);
|
|
172
|
+
assert.strictEqual(targetThenGlobal.global, true, 'later --global wins');
|
|
173
|
+
assert.strictEqual(targetThenGlobal.target, null, 'later --global clears the target');
|
|
174
|
+
assert.ok(
|
|
175
|
+
conflictLogs.some((w) => w.includes('--global overrides --target')),
|
|
176
|
+
`conflict should warn, got: ${JSON.stringify(conflictLogs)}`,
|
|
177
|
+
);
|
|
178
|
+
const globalThenTarget = parseArgs(['--global', '--target', '/proj']);
|
|
179
|
+
assert.strictEqual(globalThenTarget.global, false, 'later --target wins');
|
|
180
|
+
assert.strictEqual(globalThenTarget.target, '/proj', 'later --target is kept');
|
|
181
|
+
assert.ok(
|
|
182
|
+
conflictLogs.some((w) => w.includes('--target overrides --global')),
|
|
183
|
+
`conflict should warn, got: ${JSON.stringify(conflictLogs)}`,
|
|
184
|
+
);
|
|
185
|
+
} finally {
|
|
186
|
+
console.log = origLog;
|
|
187
|
+
}
|
|
188
|
+
|
|
140
189
|
console.log('mode.test.js: all parseArgs tests passed');
|
|
190
|
+
|
|
191
|
+
// buildPythonInstallArgs: a relative --target must be resolved against the
|
|
192
|
+
// current working directory (the Python installer runs with a different
|
|
193
|
+
// cwd, so the raw relative path would point at the wrong place).
|
|
194
|
+
const resolved = buildPythonInstallArgs({ ai: 'trae', target: 'rel/sub', force: true, globalInstall: false });
|
|
195
|
+
assert.deepStrictEqual(
|
|
196
|
+
resolved,
|
|
197
|
+
['--ai', 'trae', '--target', path.resolve('rel/sub'), '--force'],
|
|
198
|
+
'relative --target should be resolved against cwd',
|
|
199
|
+
);
|
|
200
|
+
const globalArgs = buildPythonInstallArgs({ ai: null, target: null, force: false, globalInstall: true });
|
|
201
|
+
assert.deepStrictEqual(globalArgs, ['--global'], 'global flag should be forwarded');
|
|
202
|
+
assert.deepStrictEqual(
|
|
203
|
+
buildPythonInstallArgs({ ai: null, target: '/abs/target', force: false, globalInstall: false }),
|
|
204
|
+
['--target', '/abs/target'],
|
|
205
|
+
'absolute --target is passed through unchanged',
|
|
206
|
+
);
|
|
207
|
+
assert.deepStrictEqual(buildPythonInstallArgs({ ai: null, target: null, force: false, globalInstall: false }), [], 'no flags -> empty argv');
|
|
208
|
+
|
|
209
|
+
console.log('mode.test.js: all buildPythonInstallArgs tests passed');
|
|
141
210
|
}
|
|
142
211
|
|
|
143
212
|
run().catch((err) => {
|