iterate-harness 1.8.0 → 1.9.1
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/bin/ih.js +8 -5
- package/bin/iterate-harness.js +8 -5
- package/lib/bootstrap.js +202 -14
- package/package.json +1 -1
package/bin/ih.js
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
"use strict";
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const { reportBootstrapFailure, runHarness } = require("../lib/bootstrap");
|
|
7
|
+
|
|
8
|
+
const fail = (error) => {
|
|
9
|
+
reportBootstrapFailure(error);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
};
|
|
7
12
|
|
|
8
13
|
try {
|
|
9
|
-
runHarness(process.argv.slice(2));
|
|
14
|
+
Promise.resolve(runHarness(process.argv.slice(2))).catch(fail);
|
|
10
15
|
} catch (error) {
|
|
11
|
-
|
|
12
|
-
process.stderr.write(`[iterate-harness] ${message}\n`);
|
|
13
|
-
process.exit(1);
|
|
16
|
+
fail(error);
|
|
14
17
|
}
|
package/bin/iterate-harness.js
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
"use strict";
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const { reportBootstrapFailure, runHarness } = require("../lib/bootstrap");
|
|
7
|
+
|
|
8
|
+
const fail = (error) => {
|
|
9
|
+
reportBootstrapFailure(error);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
};
|
|
7
12
|
|
|
8
13
|
try {
|
|
9
|
-
runHarness(process.argv.slice(2));
|
|
14
|
+
Promise.resolve(runHarness(process.argv.slice(2))).catch(fail);
|
|
10
15
|
} catch (error) {
|
|
11
|
-
|
|
12
|
-
process.stderr.write(`[iterate-harness] ${message}\n`);
|
|
13
|
-
process.exit(1);
|
|
16
|
+
fail(error);
|
|
14
17
|
}
|
package/lib/bootstrap.js
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* 1. Resolve a Python interpreter >= 3.10 (env override first).
|
|
9
9
|
* 2. Create/reuse the venv at ~/.iterate-harness-npm/venv.
|
|
10
10
|
* 3. pip-install the harness release tarball pinned to THIS npm package's
|
|
11
|
-
* version (lockstep: npm 1.6.0 installs harness v1.6.0).
|
|
11
|
+
* version (lockstep: npm 1.6.0 installs harness v1.6.0). When pip cannot
|
|
12
|
+
* fetch the tarball (typically a broken Python TLS trust store on macOS
|
|
13
|
+
* python.org builds), the wrapper retries by downloading the tarball with
|
|
14
|
+
* Node's own https stack — and, failing that, with curl (system trust
|
|
15
|
+
* store) — then pip-installs the local file.
|
|
12
16
|
* 4. Stamp the installed version; a mismatch (npm upgrade) triggers a
|
|
13
17
|
* re-install on the next run.
|
|
14
18
|
* 5. Spawn the venv's `ih` with argv/stdio/signals forwarded and the exit
|
|
@@ -21,6 +25,8 @@
|
|
|
21
25
|
|
|
22
26
|
const { spawn, spawnSync } = require("child_process");
|
|
23
27
|
const fs = require("fs");
|
|
28
|
+
const http = require("http");
|
|
29
|
+
const https = require("https");
|
|
24
30
|
const os = require("os");
|
|
25
31
|
const path = require("path");
|
|
26
32
|
|
|
@@ -31,6 +37,11 @@ const path = require("path");
|
|
|
31
37
|
const RUNTIME_DIR_NAME = ".iterate-harness-npm";
|
|
32
38
|
const VENV_DIR_NAME = "venv";
|
|
33
39
|
const STAMP_FILE_NAME = "version.stamp";
|
|
40
|
+
const TARBALL_CACHE_DIR_NAME = "cache";
|
|
41
|
+
|
|
42
|
+
const MAX_DOWNLOAD_REDIRECTS = 5;
|
|
43
|
+
const DOWNLOAD_TIMEOUT_MS = 120000;
|
|
44
|
+
const CURL_MAX_TIME_SECONDS = 240;
|
|
34
45
|
|
|
35
46
|
const PYTHON_ENV_VAR = "ITERATE_HARNESS_PYTHON";
|
|
36
47
|
const HOME_ENV_VAR = "ITERATE_HARNESS_NPM_HOME";
|
|
@@ -110,6 +121,18 @@ function needsBootstrap(stampContent, expectedVersion) {
|
|
|
110
121
|
return String(stampContent || "").trim() !== String(expectedVersion || "").trim();
|
|
111
122
|
}
|
|
112
123
|
|
|
124
|
+
function isRemoteHttpUrl(target) {
|
|
125
|
+
return /^https?:\/\//i.test(String(target || ""));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function downloadCachePath(homeDir, version) {
|
|
129
|
+
return path.join(homeDir, TARBALL_CACHE_DIR_NAME, `iterate-harness-${version}.tar.gz`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pipInstallArgs(target) {
|
|
133
|
+
return ["-m", "pip", "install", "--upgrade", "--force-reinstall", target];
|
|
134
|
+
}
|
|
135
|
+
|
|
113
136
|
// ---------------------------------------------------------------------------
|
|
114
137
|
// Runtime resolution
|
|
115
138
|
// ---------------------------------------------------------------------------
|
|
@@ -210,7 +233,158 @@ function installUrl(version, env) {
|
|
|
210
233
|
return releaseTarballUrl(version);
|
|
211
234
|
}
|
|
212
235
|
|
|
213
|
-
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Tarball download fallback (Node-side TLS — survives broken pip trust stores)
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
function downloadFile(url, dest, redirectBudget) {
|
|
241
|
+
const budget = redirectBudget === undefined ? MAX_DOWNLOAD_REDIRECTS : redirectBudget;
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
if (budget < 0) {
|
|
244
|
+
reject(new BootstrapError(`too many redirects while downloading ${url}`));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
let parsed;
|
|
248
|
+
try {
|
|
249
|
+
parsed = new URL(url);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
reject(new BootstrapError(`invalid download URL ${url}: ${error.message}`));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const client = parsed.protocol === "http:" ? http : https;
|
|
255
|
+
const request = client.get(url, { timeout: DOWNLOAD_TIMEOUT_MS }, (response) => {
|
|
256
|
+
const status = response.statusCode || 0;
|
|
257
|
+
if (status >= 300 && status < 400 && response.headers.location) {
|
|
258
|
+
response.resume();
|
|
259
|
+
const nextUrl = new URL(response.headers.location, url).toString();
|
|
260
|
+
resolve(downloadFile(nextUrl, dest, budget - 1));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (status < 200 || status >= 300) {
|
|
264
|
+
response.resume();
|
|
265
|
+
reject(new BootstrapError(`downloading ${url} failed with HTTP ${status}`));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const file = fs.createWriteStream(dest);
|
|
269
|
+
response.pipe(file);
|
|
270
|
+
file.on("finish", () => {
|
|
271
|
+
file.close((closeError) => {
|
|
272
|
+
if (closeError) {
|
|
273
|
+
reject(closeError);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
resolve(dest);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
file.on("error", (fileError) => {
|
|
280
|
+
file.destroy();
|
|
281
|
+
reject(fileError);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
request.on("timeout", () => {
|
|
285
|
+
request.destroy(new BootstrapError(`downloading ${url} timed out`));
|
|
286
|
+
});
|
|
287
|
+
request.on("error", (requestError) => reject(requestError));
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function curlDownload(url, dest, spawnFn) {
|
|
292
|
+
const runner = spawnFn || spawnSync;
|
|
293
|
+
const result = runner("curl", [
|
|
294
|
+
"-fsSL",
|
|
295
|
+
"--max-time",
|
|
296
|
+
String(CURL_MAX_TIME_SECONDS),
|
|
297
|
+
"-o",
|
|
298
|
+
dest,
|
|
299
|
+
url,
|
|
300
|
+
], {
|
|
301
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
302
|
+
windowsHide: true,
|
|
303
|
+
});
|
|
304
|
+
if (result.error) {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
error: new BootstrapError(`curl download of ${url} failed: ${result.error.message}`),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (result.status !== 0) {
|
|
311
|
+
const detail = `${result.stderr || result.stdout || ""}`.trim() || `exit code ${result.status}`;
|
|
312
|
+
return { ok: false, error: new BootstrapError(`curl download of ${url} failed: ${detail}`) };
|
|
313
|
+
}
|
|
314
|
+
return { ok: true, error: undefined };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function downloadTarballTo(url, dest, deps) {
|
|
318
|
+
const nodeDownload = (deps && deps.nodeDownload) || downloadFile;
|
|
319
|
+
const curlDownloadFn = (deps && deps.curlDownload) || curlDownload;
|
|
320
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
321
|
+
|
|
322
|
+
const attempts = [
|
|
323
|
+
{
|
|
324
|
+
name: "node-https",
|
|
325
|
+
run: () => nodeDownload(url, dest),
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: "curl",
|
|
329
|
+
run: async () => {
|
|
330
|
+
const outcome = curlDownloadFn(url, dest);
|
|
331
|
+
if (!outcome.ok) {
|
|
332
|
+
throw outcome.error;
|
|
333
|
+
}
|
|
334
|
+
return dest;
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
const failures = [];
|
|
340
|
+
for (const attempt of attempts) {
|
|
341
|
+
try {
|
|
342
|
+
await attempt.run();
|
|
343
|
+
return dest;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
failures.push(`${attempt.name}: ${error.message}`);
|
|
346
|
+
process.stderr.write(
|
|
347
|
+
`[iterate-harness] ${attempt.name} download failed (${error.message}); trying next strategy ...\n`
|
|
348
|
+
);
|
|
349
|
+
fs.rmSync(dest, { force: true });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
throw new BootstrapError(`direct download of ${url} failed (${failures.join("; ")})`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// Install chain: pip from URL first, Node-download + local pip as fallback
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
async function installHarness(options) {
|
|
360
|
+
const python = options.python;
|
|
361
|
+
const url = options.url;
|
|
362
|
+
const runStepFn = options.runStepFn || runStep;
|
|
363
|
+
const downloader = options.downloader || downloadTarballTo;
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
runStepFn(python, pipInstallArgs(url));
|
|
367
|
+
return;
|
|
368
|
+
} catch (primaryError) {
|
|
369
|
+
if (!isRemoteHttpUrl(url)) {
|
|
370
|
+
throw primaryError;
|
|
371
|
+
}
|
|
372
|
+
process.stderr.write(
|
|
373
|
+
`[iterate-harness] pip install from ${url} failed; retrying via direct download ...\n`
|
|
374
|
+
);
|
|
375
|
+
const cachePath = downloadCachePath(options.homeDir, options.version);
|
|
376
|
+
try {
|
|
377
|
+
await downloader(url, cachePath);
|
|
378
|
+
} catch (downloadError) {
|
|
379
|
+
throw new BootstrapError(
|
|
380
|
+
`${primaryError.message}\n\n(direct-download fallback also failed: ${downloadError.message})`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
runStepFn(python, pipInstallArgs(cachePath));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function bootstrap(homeDir, version, env) {
|
|
214
388
|
fs.mkdirSync(homeDir, { recursive: true });
|
|
215
389
|
|
|
216
390
|
const venvDir = path.join(homeDir, VENV_DIR_NAME);
|
|
@@ -231,20 +405,13 @@ function bootstrap(homeDir, version, env) {
|
|
|
231
405
|
|
|
232
406
|
const url = installUrl(version, env);
|
|
233
407
|
process.stderr.write(`[iterate-harness] pip installing iterate-harness ${version} ...\n`);
|
|
234
|
-
|
|
235
|
-
"-m",
|
|
236
|
-
"pip",
|
|
237
|
-
"install",
|
|
238
|
-
"--upgrade",
|
|
239
|
-
"--force-reinstall",
|
|
240
|
-
url,
|
|
241
|
-
]);
|
|
408
|
+
await installHarness({ python: executable.python, url, homeDir, version });
|
|
242
409
|
|
|
243
410
|
const stampPath = path.join(homeDir, STAMP_FILE_NAME);
|
|
244
411
|
fs.writeFileSync(stampPath, `${version}\n`, "utf8");
|
|
245
412
|
}
|
|
246
413
|
|
|
247
|
-
function ensureRuntime(env) {
|
|
414
|
+
async function ensureRuntime(env) {
|
|
248
415
|
const environment = env || process.env;
|
|
249
416
|
const version = packageVersion();
|
|
250
417
|
const homeDir = runtimeHomeDir(environment);
|
|
@@ -258,7 +425,7 @@ function ensureRuntime(env) {
|
|
|
258
425
|
: "";
|
|
259
426
|
|
|
260
427
|
if (!skipInstall && needsBootstrap(stampContent, version)) {
|
|
261
|
-
bootstrap(homeDir, version, environment);
|
|
428
|
+
await bootstrap(homeDir, version, environment);
|
|
262
429
|
}
|
|
263
430
|
|
|
264
431
|
if (!fs.existsSync(executable.ih)) {
|
|
@@ -279,8 +446,20 @@ function ensureRuntime(env) {
|
|
|
279
446
|
// Delegation to the real ih executable
|
|
280
447
|
// ---------------------------------------------------------------------------
|
|
281
448
|
|
|
282
|
-
function
|
|
283
|
-
const
|
|
449
|
+
function reportBootstrapFailure(error) {
|
|
450
|
+
const message = error instanceof BootstrapError ? error.message : (error && error.stack) || error;
|
|
451
|
+
process.stderr.write(`[iterate-harness] ${message}\n`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function runHarness(args, env) {
|
|
455
|
+
let target;
|
|
456
|
+
try {
|
|
457
|
+
target = await ensureRuntime(env);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
reportBootstrapFailure(error);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
284
463
|
const child = spawn(target, args, {
|
|
285
464
|
stdio: "inherit",
|
|
286
465
|
windowsHide: false,
|
|
@@ -310,6 +489,7 @@ function runHarness(args, env) {
|
|
|
310
489
|
|
|
311
490
|
module.exports = {
|
|
312
491
|
BootstrapError,
|
|
492
|
+
MAX_DOWNLOAD_REDIRECTS,
|
|
313
493
|
MIN_PYTHON_MAJOR,
|
|
314
494
|
MIN_PYTHON_MINOR,
|
|
315
495
|
PYTHON_ENV_VAR,
|
|
@@ -317,14 +497,22 @@ module.exports = {
|
|
|
317
497
|
INSTALL_URL_ENV_VAR,
|
|
318
498
|
SKIP_INSTALL_ENV_VAR,
|
|
319
499
|
detectPython,
|
|
500
|
+
curlDownload,
|
|
501
|
+
downloadCachePath,
|
|
502
|
+
downloadFile,
|
|
503
|
+
downloadTarballTo,
|
|
320
504
|
ensureRuntime,
|
|
505
|
+
installHarness,
|
|
321
506
|
installUrl,
|
|
507
|
+
isRemoteHttpUrl,
|
|
322
508
|
isSupportedPython,
|
|
323
509
|
needsBootstrap,
|
|
324
510
|
packageVersion,
|
|
325
511
|
parsePythonVersion,
|
|
512
|
+
pipInstallArgs,
|
|
326
513
|
pythonCandidates,
|
|
327
514
|
releaseTarballUrl,
|
|
515
|
+
reportBootstrapFailure,
|
|
328
516
|
runHarness,
|
|
329
517
|
venvExecutablePaths,
|
|
330
518
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-harness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "npm distribution wrapper for iterate-harness — installs the Python harness (ih CLI) into a managed venv on first run and exposes it as a global command",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|