iterate-harness 1.9.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 CHANGED
@@ -3,12 +3,15 @@
3
3
 
4
4
  "use strict";
5
5
 
6
- const { BootstrapError, runHarness } = require("../lib/bootstrap");
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
- const message = error instanceof BootstrapError ? error.message : (error && error.stack) || error;
12
- process.stderr.write(`[iterate-harness] ${message}\n`);
13
- process.exit(1);
16
+ fail(error);
14
17
  }
@@ -3,12 +3,15 @@
3
3
 
4
4
  "use strict";
5
5
 
6
- const { BootstrapError, runHarness } = require("../lib/bootstrap");
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
- const message = error instanceof BootstrapError ? error.message : (error && error.stack) || error;
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
- function bootstrap(homeDir, version, env) {
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
- runStep(executable.python, [
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 runHarness(args, env) {
283
- const target = ensureRuntime(env);
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.9.0",
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",