iterate-skill-installer 3.2.0 → 3.2.2
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 +65 -27
- package/package.json +9 -3
- package/test/mode.test.js +0 -231
package/lib/installer.js
CHANGED
|
@@ -54,6 +54,16 @@ const JSON_MAX_BYTES = 10 * 1024 * 1024; // 10 MiB
|
|
|
54
54
|
const CHECKSUMS_MAX_BYTES = 1 * 1024 * 1024; // 1 MiB
|
|
55
55
|
const TARBALL_MAX_BYTES = 100 * 1024 * 1024; // 100 MiB
|
|
56
56
|
|
|
57
|
+
// A strict upper bound for an interactive yes/no prompt. On EOF (piped stdin
|
|
58
|
+
// that ends) or a TTY that stops responding, the prompt must fall back to the
|
|
59
|
+
// default rather than hang the installer forever.
|
|
60
|
+
const ASK_YES_NO_TIMEOUT_MS = 30_000;
|
|
61
|
+
|
|
62
|
+
// SHA256 digests are exactly 64 lowercase/uppercase hex characters. Any other
|
|
63
|
+
// value in a checksum file is corrupt or tampered and must fail closed rather
|
|
64
|
+
// than feed a garbage hash into the integrity comparison.
|
|
65
|
+
const SHA256_HEX_RE = /^[0-9a-fA-F]{64}$/;
|
|
66
|
+
|
|
57
67
|
// Curl added --fail-with-body in 7.76. On older curl the installer must fall
|
|
58
68
|
// back to plain --fail so the flag is never rejected by the system curl.
|
|
59
69
|
const CURL_FAIL_WITH_BODY_MIN = { major: 7, minor: 76 };
|
|
@@ -242,20 +252,32 @@ function parseChecksums(text) {
|
|
|
242
252
|
const map = new Map();
|
|
243
253
|
for (const line of text.split(/\r?\n/)) {
|
|
244
254
|
const trimmed = line.trim();
|
|
245
|
-
if (!trimmed) continue;
|
|
255
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
246
256
|
const parts = trimmed.split(/\s+/);
|
|
247
|
-
if (parts.length
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
257
|
+
if (parts.length < 2) continue;
|
|
258
|
+
// Reject any entry whose digest is not a 64-char hex string so a corrupt
|
|
259
|
+
// or tampered checksum file fails closed: a missing asset ("not found")
|
|
260
|
+
// aborts the install, whereas a garbage digest would masquerade as a
|
|
261
|
+
// valid expected hash.
|
|
262
|
+
if (!SHA256_HEX_RE.test(parts[0])) continue;
|
|
263
|
+
// Normalize the filename the same way scripts/install.py does
|
|
264
|
+
// (_parse_checksum): strip all leading GNU tar binary-mode markers a la
|
|
265
|
+
// Python's name.lstrip("*") (ONE '*', or several in a malformed entry —
|
|
266
|
+
// strip them all so the two parsers can never disagree), then strip the
|
|
267
|
+
// './' prefix, then match on the basename so entries with a subpath or
|
|
268
|
+
// versioned component (e.g. "dist/iterate-skill.tar.gz" or
|
|
269
|
+
// "./iterate-skill.tar.gz") still resolve to the expected asset.
|
|
270
|
+
const normalized = parts[1].replace(/^\*+/, '').replace(/^\.\//, '');
|
|
271
|
+
const basename = normalized.split('/').pop();
|
|
272
|
+
// A duplicate basename carrying a *different* digest is a red flag (the
|
|
273
|
+
// file is hand-corrupted or two sources disagree); refuse the whole file
|
|
274
|
+
// instead of silently letting one value win.
|
|
275
|
+
if (map.has(basename) && map.get(basename) !== parts[0]) {
|
|
276
|
+
throw new InstallerError(
|
|
277
|
+
`Duplicate checksum entry for ${basename} with a different value — refusing to proceed.`,
|
|
278
|
+
);
|
|
258
279
|
}
|
|
280
|
+
map.set(basename, parts[0]);
|
|
259
281
|
}
|
|
260
282
|
return map;
|
|
261
283
|
}
|
|
@@ -463,18 +485,32 @@ async function installCli(pythonBin, sourceDir) {
|
|
|
463
485
|
*
|
|
464
486
|
* Used to decide whether the current directory should be treated as the
|
|
465
487
|
* target project when the installer is launched from a non-home directory.
|
|
466
|
-
* Falls back to ``defaultNo`` if the input is unrecognized
|
|
488
|
+
* Falls back to ``defaultNo`` if the input is unrecognized, on EOF (piped
|
|
489
|
+
* stdin that ends without an answer) or after a timeout, so a non-interactive
|
|
490
|
+
* or hung invocation resolves instead of blocking forever.
|
|
467
491
|
*/
|
|
468
492
|
function askYesNo(question, defaultNo = false) {
|
|
469
493
|
return new Promise((resolve) => {
|
|
470
494
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
471
495
|
const hint = defaultNo ? '[y/N]' : '[Y/n]';
|
|
472
|
-
|
|
496
|
+
let settled = false;
|
|
497
|
+
const finish = (value) => {
|
|
498
|
+
if (settled) return;
|
|
499
|
+
settled = true;
|
|
500
|
+
clearTimeout(timer);
|
|
473
501
|
rl.close();
|
|
502
|
+
resolve(value);
|
|
503
|
+
};
|
|
504
|
+
// EOF (stdin is a closed pipe, or the terminal disappears) never invokes
|
|
505
|
+
// rl.question's callback; the interface 'close' event is the signal that
|
|
506
|
+
// no answer is coming, so fall back to the default instead of hanging.
|
|
507
|
+
rl.on('close', () => finish(defaultNo));
|
|
508
|
+
const timer = setTimeout(() => finish(defaultNo), ASK_YES_NO_TIMEOUT_MS);
|
|
509
|
+
rl.question(`\x1b[36m◆\x1b[0m ${question} ${hint} `, (answer) => {
|
|
474
510
|
const a = answer.trim().toLowerCase();
|
|
475
|
-
if (a === 'y' || a === 'yes')
|
|
476
|
-
else if (a === 'n' || a === 'no')
|
|
477
|
-
else
|
|
511
|
+
if (a === 'y' || a === 'yes') finish(true);
|
|
512
|
+
else if (a === 'n' || a === 'no') finish(false);
|
|
513
|
+
else finish(defaultNo);
|
|
478
514
|
});
|
|
479
515
|
});
|
|
480
516
|
}
|
|
@@ -569,6 +605,18 @@ function parseArgs(argv) {
|
|
|
569
605
|
const arg = argv[i];
|
|
570
606
|
const next = argv[i + 1];
|
|
571
607
|
|
|
608
|
+
// -h/--help and -v/--version short-circuit parsing: later arguments (even
|
|
609
|
+
// malformed or value-consuming flags) must not turn `--help`/`--version`
|
|
610
|
+
// into an error exit. bin/cli.js exits 0 on these modes before installing.
|
|
611
|
+
if (arg === '--help' || arg === '-h') {
|
|
612
|
+
options.mode = 'help';
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
if (arg === '--version' || arg === '-v') {
|
|
616
|
+
options.mode = 'version';
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
|
|
572
620
|
switch (arg) {
|
|
573
621
|
case '--ai':
|
|
574
622
|
if (!next || next.startsWith('-')) {
|
|
@@ -607,16 +655,6 @@ function parseArgs(argv) {
|
|
|
607
655
|
// user who only wants the skill is not surprised by a global install.
|
|
608
656
|
options.noCli = true;
|
|
609
657
|
break;
|
|
610
|
-
case '--help':
|
|
611
|
-
case '-h':
|
|
612
|
-
// Show usage and exit 0 (handled by bin/cli.js).
|
|
613
|
-
options.mode = 'help';
|
|
614
|
-
break;
|
|
615
|
-
case '--version':
|
|
616
|
-
case '-v':
|
|
617
|
-
// Print the package version and exit 0 (handled by bin/cli.js).
|
|
618
|
-
options.mode = 'version';
|
|
619
|
-
break;
|
|
620
658
|
case '--token':
|
|
621
659
|
if (!next || next.startsWith('-')) {
|
|
622
660
|
console.error('Error: --token requires a value (a flag was found instead)');
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-skill-installer",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.2",
|
|
4
4
|
"description": "One-command installer for iterate-skill across AI coding assistants",
|
|
5
5
|
"main": "lib/installer.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin/",
|
|
8
|
+
"lib/",
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"README.md",
|
|
11
|
+
"README.zh-CN.md"
|
|
12
|
+
],
|
|
6
13
|
"bin": {
|
|
7
|
-
"iterate-skill-installer": "bin/cli.js"
|
|
8
|
-
"iterate-skill": "bin/cli.js"
|
|
14
|
+
"iterate-skill-installer": "bin/cli.js"
|
|
9
15
|
},
|
|
10
16
|
"scripts": {
|
|
11
17
|
"test": "node --check bin/cli.js && node --check lib/installer.js && node test/mode.test.js"
|
package/test/mode.test.js
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for resolveInstallMode() — the global vs project install-mode
|
|
3
|
-
* decision — and parseChecksums() — the SHA256SUMS.txt parser — in the npx
|
|
4
|
-
* installer. Uses node:assert so no extra test runner is required; run via
|
|
5
|
-
* `npm test`.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const assert = require('node:assert');
|
|
9
|
-
const fs = require('node:fs');
|
|
10
|
-
const path = require('node:path');
|
|
11
|
-
const os = require('node:os');
|
|
12
|
-
const { resolveInstallMode, parseChecksums, parseArgs, buildPythonInstallArgs, isGithubApiUrl } = require('../lib/installer');
|
|
13
|
-
|
|
14
|
-
const originalCwd = process.cwd();
|
|
15
|
-
const originalHome = os.homedir;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Temporarily set the home directory and chdir into a fresh temp directory
|
|
19
|
-
* (the "current working directory"), then run fn(cwd). Always restores state.
|
|
20
|
-
*/
|
|
21
|
-
function withHomeAndCwd(home, fn) {
|
|
22
|
-
return (async () => {
|
|
23
|
-
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'iterate-mode-'));
|
|
24
|
-
os.homedir = () => home;
|
|
25
|
-
process.chdir(cwd);
|
|
26
|
-
try {
|
|
27
|
-
return await fn(cwd);
|
|
28
|
-
} finally {
|
|
29
|
-
os.homedir = originalHome;
|
|
30
|
-
process.chdir(originalCwd);
|
|
31
|
-
fs.rmSync(cwd, { recursive: true, force: true });
|
|
32
|
-
}
|
|
33
|
-
})();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function run() {
|
|
37
|
-
// 1) Explicit --target is honored without asking.
|
|
38
|
-
await withHomeAndCwd('/home/u', async () => {
|
|
39
|
-
const opts = { global: true, target: '/x', targetExplicit: true, globalExplicit: false };
|
|
40
|
-
const ask = () => { throw new Error('should not ask'); };
|
|
41
|
-
const out = await resolveInstallMode(opts, ask);
|
|
42
|
-
assert.strictEqual(out.target, '/x', 'target should be preserved');
|
|
43
|
-
assert.strictEqual(out.global, true, 'global should be left untouched for explicit target');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// 2) Explicit --global is honored without asking.
|
|
47
|
-
await withHomeAndCwd('/home/u', async () => {
|
|
48
|
-
const opts = { global: true, target: null, targetExplicit: false, globalExplicit: true };
|
|
49
|
-
const ask = () => { throw new Error('should not ask'); };
|
|
50
|
-
const out = await resolveInstallMode(opts, ask);
|
|
51
|
-
assert.strictEqual(out.global, true, 'global should be preserved');
|
|
52
|
-
assert.strictEqual(out.target, null, 'target should stay null');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// 3) cwd === home: no ask, stays global.
|
|
56
|
-
await withHomeAndCwd('/home/u', async () => {
|
|
57
|
-
// Use process.cwd() (the resolved path) so home matches cwd exactly,
|
|
58
|
-
// avoiding macOS /var -> /private/var symlink mismatches.
|
|
59
|
-
os.homedir = () => process.cwd();
|
|
60
|
-
const opts = { global: true, target: null, targetExplicit: false, globalExplicit: false };
|
|
61
|
-
const ask = () => { throw new Error('should not ask when cwd is home'); };
|
|
62
|
-
const out = await resolveInstallMode(opts, ask);
|
|
63
|
-
assert.strictEqual(out.global, true, 'should stay global when cwd is home');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// 4) cwd !== home and user says yes -> project-level install into cwd.
|
|
67
|
-
await withHomeAndCwd('/home/u', async () => {
|
|
68
|
-
const opts = { global: true, target: null, targetExplicit: false, globalExplicit: false };
|
|
69
|
-
const out = await resolveInstallMode(opts, async () => true);
|
|
70
|
-
assert.strictEqual(out.global, false, 'should switch to project mode');
|
|
71
|
-
assert.strictEqual(out.target, process.cwd(), 'target should be cwd');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// 5) cwd !== home and user says no -> stays global.
|
|
75
|
-
await withHomeAndCwd('/home/u', async () => {
|
|
76
|
-
const opts = { global: true, target: null, targetExplicit: false, globalExplicit: false };
|
|
77
|
-
const out = await resolveInstallMode(opts, async () => false);
|
|
78
|
-
assert.strictEqual(out.global, true, 'should stay global when user declines');
|
|
79
|
-
assert.strictEqual(out.target, null, 'target should stay null');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
console.log('mode.test.js: all resolveInstallMode tests passed');
|
|
83
|
-
|
|
84
|
-
// parseChecksums: must strip the GNU tar binary-mode marker ('*') so both
|
|
85
|
-
// "HASH name" and "HASH *name" formats resolve, matching the Python side.
|
|
86
|
-
const plain = parseChecksums('abc123 iterate-skill.tar.gz\n');
|
|
87
|
-
assert.strictEqual(plain.get('iterate-skill.tar.gz'), 'abc123', 'plain format should parse');
|
|
88
|
-
|
|
89
|
-
const starred = parseChecksums('def456 *iterate-skill.tar.gz\n');
|
|
90
|
-
assert.strictEqual(starred.get('iterate-skill.tar.gz'), 'def456', 'starred format should strip the * marker');
|
|
91
|
-
|
|
92
|
-
const crlf = parseChecksums('abc123 iterate-skill.tar.gz\r\n');
|
|
93
|
-
assert.strictEqual(crlf.get('iterate-skill.tar.gz'), 'abc123', 'CRLF line endings should parse');
|
|
94
|
-
|
|
95
|
-
const multi = parseChecksums('aa a.txt\nbb b.txt\n');
|
|
96
|
-
assert.strictEqual(multi.get('a.txt'), 'aa');
|
|
97
|
-
assert.strictEqual(multi.get('b.txt'), 'bb');
|
|
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
|
-
|
|
110
|
-
// Multiple leading '*' markers must be stripped like Python's lstrip("*")
|
|
111
|
-
// (a malformed entry must never leave a leading '*' in the basename).
|
|
112
|
-
const multiStar = parseChecksums('abc123 **iterate-skill.tar.gz\n');
|
|
113
|
-
assert.strictEqual(multiStar.get('iterate-skill.tar.gz'), 'abc123', 'all leading * markers should strip (lstrip matching)');
|
|
114
|
-
|
|
115
|
-
console.log('mode.test.js: all parseChecksums tests passed');
|
|
116
|
-
|
|
117
|
-
// parseArgs: --no-cli must toggle skill-only mode, and default to off.
|
|
118
|
-
assert.strictEqual(parseArgs([]).noCli, false, 'noCli should default to false');
|
|
119
|
-
assert.strictEqual(parseArgs(['--no-cli']).noCli, true, '--no-cli should set noCli');
|
|
120
|
-
assert.strictEqual(parseArgs(['--no-cli', '--force', '--global']).noCli, true, 'noCli persists with other flags');
|
|
121
|
-
assert.strictEqual(parseArgs(['--global']).noCli, false, 'unrelated flags do not set noCli');
|
|
122
|
-
|
|
123
|
-
// parseArgs: unrelated flag surface is preserved (target/global/ai/force).
|
|
124
|
-
const combo = parseArgs(['--ai', 'trae', '--target', '/proj', '--force']);
|
|
125
|
-
assert.strictEqual(combo.ai, 'trae', 'ai should parse');
|
|
126
|
-
assert.strictEqual(combo.target, '/proj', 'target should parse');
|
|
127
|
-
assert.strictEqual(combo.force, true, 'force should parse');
|
|
128
|
-
assert.strictEqual(combo.global, false, 'target forces project mode');
|
|
129
|
-
|
|
130
|
-
// parseArgs: --version/-v/--help/-h must set the corresponding mode instead
|
|
131
|
-
// of falling through to the unknown-option error.
|
|
132
|
-
assert.strictEqual(parseArgs([]).mode, null, 'mode should default to null');
|
|
133
|
-
assert.strictEqual(parseArgs(['--version']).mode, 'version', '--version should set version mode');
|
|
134
|
-
assert.strictEqual(parseArgs(['-v']).mode, 'version', '-v should set version mode');
|
|
135
|
-
assert.strictEqual(parseArgs(['--help']).mode, 'help', '--help should set help mode');
|
|
136
|
-
assert.strictEqual(parseArgs(['-h']).mode, 'help', '-h should set help mode');
|
|
137
|
-
|
|
138
|
-
// parseArgs: value-consuming flags must reject a following flag as their
|
|
139
|
-
// value instead of silently swallowing it (regression: `--ai --force`
|
|
140
|
-
// previously set ai='--force' and misled the Python side with an invalid
|
|
141
|
-
// choice). process.exit is stubbed so the error path is assertable.
|
|
142
|
-
const origExit = process.exit;
|
|
143
|
-
process.exit = (code) => {
|
|
144
|
-
throw new Error(`exit(${code})`);
|
|
145
|
-
};
|
|
146
|
-
try {
|
|
147
|
-
assert.throws(() => parseArgs(['--ai', '--force']), /exit\(1\)/, '--ai must reject a flag as its value');
|
|
148
|
-
assert.throws(() => parseArgs(['--target', '--global']), /exit\(1\)/, '--target must reject a flag as its value');
|
|
149
|
-
assert.throws(() => parseArgs(['--token', '--yes']), /exit\(1\)/, '--token must reject a flag as its value');
|
|
150
|
-
assert.throws(() => parseArgs(['--ai']), /exit\(1\)/, '--ai without a value must error');
|
|
151
|
-
assert.throws(() => parseArgs(['--token']), /exit\(1\)/, '--token without a value must error');
|
|
152
|
-
} finally {
|
|
153
|
-
process.exit = origExit;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// parseArgs: a bare positional argument must produce a warning instead of
|
|
157
|
-
// being silently ignored (fix: typos like "npx iterate-skill-installer trae").
|
|
158
|
-
const origLog = console.log;
|
|
159
|
-
const warnings = [];
|
|
160
|
-
console.log = (msg) => { warnings.push(msg); };
|
|
161
|
-
try {
|
|
162
|
-
const opts = parseArgs(['trae', '--force']);
|
|
163
|
-
assert.strictEqual(opts.force, true, 'known flags still parse alongside positionals');
|
|
164
|
-
assert.ok(
|
|
165
|
-
warnings.some((w) => w.includes('Ignoring unexpected positional argument: trae')),
|
|
166
|
-
`positional arg should warn, got: ${JSON.stringify(warnings)}`,
|
|
167
|
-
);
|
|
168
|
-
} finally {
|
|
169
|
-
console.log = origLog;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// parseArgs: --global + --target together must warn that the later flag wins.
|
|
173
|
-
const conflictLogs = [];
|
|
174
|
-
console.log = (msg) => { conflictLogs.push(msg); };
|
|
175
|
-
try {
|
|
176
|
-
const targetThenGlobal = parseArgs(['--target', '/proj', '--global']);
|
|
177
|
-
assert.strictEqual(targetThenGlobal.global, true, 'later --global wins');
|
|
178
|
-
assert.strictEqual(targetThenGlobal.target, null, 'later --global clears the target');
|
|
179
|
-
assert.ok(
|
|
180
|
-
conflictLogs.some((w) => w.includes('--global overrides --target')),
|
|
181
|
-
`conflict should warn, got: ${JSON.stringify(conflictLogs)}`,
|
|
182
|
-
);
|
|
183
|
-
const globalThenTarget = parseArgs(['--global', '--target', '/proj']);
|
|
184
|
-
assert.strictEqual(globalThenTarget.global, false, 'later --target wins');
|
|
185
|
-
assert.strictEqual(globalThenTarget.target, '/proj', 'later --target is kept');
|
|
186
|
-
assert.ok(
|
|
187
|
-
conflictLogs.some((w) => w.includes('--target overrides --global')),
|
|
188
|
-
`conflict should warn, got: ${JSON.stringify(conflictLogs)}`,
|
|
189
|
-
);
|
|
190
|
-
} finally {
|
|
191
|
-
console.log = origLog;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
console.log('mode.test.js: all parseArgs tests passed');
|
|
195
|
-
|
|
196
|
-
// buildPythonInstallArgs: a relative --target must be resolved against the
|
|
197
|
-
// current working directory (the Python installer runs with a different
|
|
198
|
-
// cwd, so the raw relative path would point at the wrong place).
|
|
199
|
-
const resolved = buildPythonInstallArgs({ ai: 'trae', target: 'rel/sub', force: true, globalInstall: false });
|
|
200
|
-
assert.deepStrictEqual(
|
|
201
|
-
resolved,
|
|
202
|
-
['--ai', 'trae', '--target', path.resolve('rel/sub'), '--force'],
|
|
203
|
-
'relative --target should be resolved against cwd',
|
|
204
|
-
);
|
|
205
|
-
const globalArgs = buildPythonInstallArgs({ ai: null, target: null, force: false, globalInstall: true });
|
|
206
|
-
assert.deepStrictEqual(globalArgs, ['--global'], 'global flag should be forwarded');
|
|
207
|
-
assert.deepStrictEqual(
|
|
208
|
-
buildPythonInstallArgs({ ai: null, target: '/abs/target', force: false, globalInstall: false }),
|
|
209
|
-
['--target', '/abs/target'],
|
|
210
|
-
'absolute --target is passed through unchanged',
|
|
211
|
-
);
|
|
212
|
-
assert.deepStrictEqual(buildPythonInstallArgs({ ai: null, target: null, force: false, globalInstall: false }), [], 'no flags -> empty argv');
|
|
213
|
-
|
|
214
|
-
console.log('mode.test.js: all buildPythonInstallArgs tests passed');
|
|
215
|
-
|
|
216
|
-
// isGithubApiUrl: the caller's PAT must only ever be attached to the
|
|
217
|
-
// api.github.com host — never to public release-asset / other URLs
|
|
218
|
-
// (credential surface minimalism, mirroring scripts/install.py).
|
|
219
|
-
assert.strictEqual(isGithubApiUrl('https://api.github.com/repos/jingzhao-l/iterate-skill/releases/latest'), true, 'api host should match');
|
|
220
|
-
assert.strictEqual(isGithubApiUrl('https://api.github.com.evil.com/x'), false, 'lookalike host must not match');
|
|
221
|
-
assert.strictEqual(isGithubApiUrl('https://release-assets.githubusercontent.com/foo.tar.gz'), false, 'asset host must not receive the token');
|
|
222
|
-
assert.strictEqual(isGithubApiUrl('https://github.com/x'), false, 'github.com is not the API host');
|
|
223
|
-
assert.strictEqual(isGithubApiUrl('not a url'), false, 'malformed url must not match');
|
|
224
|
-
|
|
225
|
-
console.log('mode.test.js: all isGithubApiUrl tests passed');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
run().catch((err) => {
|
|
229
|
-
console.error(err);
|
|
230
|
-
process.exit(1);
|
|
231
|
-
});
|