impel-cli 0.18.3 → 0.18.4

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/README.md CHANGED
@@ -329,6 +329,14 @@ run a real package manager, or modify native personal profiles.
329
329
  CI runs on Ubuntu, macOS, and Windows with supported Node versions. See
330
330
  [RELEASING.md](RELEASING.md) for the npm release process.
331
331
 
332
+ The separate Windows vendor contract runs only on a disposable GitHub-hosted
333
+ Windows VM. It installs the exact newer Claude fixture that preceded the
334
+ current compatibility pin, invokes the production downloader and installer to
335
+ downgrade to the exact pinned MSIX, verifies the registered AppX identity,
336
+ version, architecture, executable, and idempotent second run, then removes the
337
+ package. Docker/Wine remains useful for Windows path and process semantics, but
338
+ cannot validate AppX deployment, packaged services, trust, or Store behavior.
339
+
332
340
  The macOS config-contract job downloads the exact ChatGPT archive declared in
333
341
  `PINNED_VENDOR_APPS`, verifies its checksum, signature, app version, and
334
342
  embedded Codex version, then validates a deterministic generated profile with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.3",
3
+ "version": "0.18.4",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -91,13 +91,18 @@ export async function cmdConverge(argv = [], overrides = {}) {
91
91
  // answer (not unref'd) so the auto-decline reliably fires even when the
92
92
  // prompt is the only thing keeping the event loop alive.
93
93
  const allowed = confirmed(await new Promise((resolve) => {
94
- const timer = setTimeout(() => resolve(""), io.vendorPromptTimeoutMs ?? 60_000);
94
+ const controller = new AbortController();
95
+ const timer = setTimeout(() => {
96
+ controller.abort();
97
+ resolve("");
98
+ }, io.vendorPromptTimeoutMs ?? 60_000);
95
99
  const settle = (answer) => {
96
100
  clearTimeout(timer);
97
101
  resolve(answer);
98
102
  };
99
103
  Promise.resolve(io.promptText(
100
104
  `Allow the verified ${label} vendor ${context.mode === "update" ? "update" : "installation"}? [y/N] (auto-N in 60s) `,
105
+ { signal: controller.signal },
101
106
  )).then(settle, () => settle(""));
102
107
  }));
103
108
  vendorAppDecisions.set(target, allowed);
@@ -272,7 +277,14 @@ export async function cmdConverge(argv = [], overrides = {}) {
272
277
  || product === target
273
278
  || (target === "codex" && product === "chatgpt")
274
279
  ));
275
- return vendorAppTargetReady(retryReport, target);
280
+ const installed = vendorAppTargetReady(retryReport, target);
281
+ return {
282
+ installed,
283
+ error: installed
284
+ ? null
285
+ : retryReport?.tenants?.[0]?.errors?.join("; ")
286
+ || `The ${target} vendor app did not reach a verified state.`,
287
+ };
276
288
  },
277
289
  },
278
290
  }, overrides.recoveryOverrides || {});
@@ -453,7 +453,14 @@ export async function cmdSetup(argv, overrides = {}) {
453
453
  || product === target
454
454
  || (target === "codex" && product === "chatgpt")
455
455
  ));
456
- return vendorAppTargetReady(retryReport, target);
456
+ const installed = vendorAppTargetReady(retryReport, target);
457
+ return {
458
+ installed,
459
+ error: installed
460
+ ? null
461
+ : retryReport?.tenants?.[0]?.errors?.join("; ")
462
+ || `The ${target} vendor app did not reach a verified state.`,
463
+ };
457
464
  },
458
465
  },
459
466
  }, overrides.recoveryOverrides || {});
@@ -500,10 +500,22 @@ async function installVendorApp(input, context) {
500
500
  if (typeof context.installVendorApp !== "function") {
501
501
  return result("failed", "Vendor app installation is unavailable in this recovery context.");
502
502
  }
503
- const installed = await context.installVendorApp(input.target);
504
- return installed === false
505
- ? result("failed", `The ${input.target} vendor app did not install cleanly.`)
506
- : result("succeeded", `The ${input.target} vendor app installation completed.`);
503
+ const outcome = await context.installVendorApp(input.target);
504
+ const installed = typeof outcome === "object" && outcome !== null
505
+ ? outcome.installed !== false
506
+ : outcome !== false;
507
+ if (installed) {
508
+ return result("succeeded", `The ${input.target} vendor app installation completed.`);
509
+ }
510
+ const detail = typeof outcome === "object" && outcome?.error
511
+ ? redactInstallRecoveryText(outcome.error).slice(0, 1_000)
512
+ : null;
513
+ return result(
514
+ "failed",
515
+ detail
516
+ ? `The ${input.target} vendor app did not install cleanly: ${detail}`
517
+ : `The ${input.target} vendor app did not install cleanly.`,
518
+ );
507
519
  }
508
520
 
509
521
  function repairUserPath(input, context, io) {
package/src/prompt.js CHANGED
@@ -40,12 +40,32 @@ export function promptSecret(question) {
40
40
  });
41
41
  }
42
42
 
43
- export function promptText(question) {
43
+ export function promptText(question, {
44
+ signal = null,
45
+ input = process.stdin,
46
+ output = process.stdout,
47
+ } = {}) {
44
48
  return new Promise((resolve) => {
45
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
46
- rl.question(question, (answer) => {
49
+ const rl = readline.createInterface({ input, output });
50
+ let settled = false;
51
+ const finish = (answer, aborted = false) => {
52
+ if (settled) return;
53
+ settled = true;
54
+ signal?.removeEventListener("abort", abort);
47
55
  rl.close();
48
- resolve(answer.trim());
56
+ // A cancelled readline prompt otherwise leaves the next status line on
57
+ // the same terminal row as its unanswered question.
58
+ if (aborted && output?.isTTY) output.write("\n");
59
+ resolve(String(answer || "").trim());
60
+ };
61
+ const abort = () => finish("", true);
62
+ if (signal?.aborted) {
63
+ abort();
64
+ return;
65
+ }
66
+ signal?.addEventListener("abort", abort, { once: true });
67
+ rl.question(question, (answer) => {
68
+ finish(answer);
49
69
  });
50
70
  });
51
71
  }
@@ -14,6 +14,7 @@ export const WINDOWS_CHATGPT_PACKAGE = "9PLM9XGG6VKS";
14
14
  export const WINDOWS_CHATGPT_PACKAGE_NAME = "OpenAI.Codex";
15
15
  export const WINDOWS_CHATGPT_PUBLISHER_ID = "2p2nqsd0c76g0";
16
16
  const WINDOWS_CLAUDE_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
17
+ const WINDOWS_WINGET_UPDATE_NOT_APPLICABLE = 0x8A15002B;
17
18
 
18
19
  export function windowsClaudeUserData(environment = process.env, tenantId = null) {
19
20
  const localAppData = environmentValue(environment, "LOCALAPPDATA")
@@ -34,6 +35,16 @@ function isFile(filePath) {
34
35
  }
35
36
  }
36
37
 
38
+ // A CLI launched from PowerShell 7 inherits its PSModulePath. Passing that
39
+ // value into Windows PowerShell 5.1 makes legacy modules such as
40
+ // Microsoft.PowerShell.Security visible but unloadable. Omit the variable so
41
+ // powershell.exe reconstructs its own version-correct default module path.
42
+ function windowsPowerShellEnvironment(environment = process.env) {
43
+ return Object.fromEntries(
44
+ Object.entries(environment).filter(([key]) => key.toLowerCase() !== "psmodulepath"),
45
+ );
46
+ }
47
+
37
48
  function versionedSquirrelCandidates(root, readDirectory = fs.readdirSync) {
38
49
  try {
39
50
  return readDirectory(root, { withFileTypes: true })
@@ -61,7 +72,7 @@ function installedMsixChatGPT(environment, run = spawnSync) {
61
72
  try {
62
73
  const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
63
74
  encoding: "utf8",
64
- env: environment,
75
+ env: windowsPowerShellEnvironment(environment),
65
76
  stdio: ["ignore", "pipe", "ignore"],
66
77
  windowsHide: true,
67
78
  });
@@ -179,7 +190,7 @@ function pinnedWindowsClaudeApp(environment = process.env, dependencies = {}) {
179
190
  const result = io.run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
180
191
  encoding: "utf8",
181
192
  env: {
182
- ...environment,
193
+ ...windowsPowerShellEnvironment(environment),
183
194
  IMPEL_CLAUDE_PACKAGE_VERSION: io.pin.packageVersion,
184
195
  IMPEL_CLAUDE_PACKAGE_PUBLISHER: io.pin.publisher,
185
196
  IMPEL_CLAUDE_PACKAGE_ARCHITECTURE: expectedArchitecture,
@@ -269,7 +280,7 @@ function installPinnedWindowsClaudeMsix(packagePath, environment, pin, architect
269
280
  {
270
281
  encoding: "utf8",
271
282
  env: {
272
- ...environment,
283
+ ...windowsPowerShellEnvironment(environment),
273
284
  IMPEL_CLAUDE_MSIX_PATH: packagePath,
274
285
  IMPEL_CLAUDE_PACKAGE_NAME: pin.packageName,
275
286
  IMPEL_CLAUDE_PACKAGE_VERSION: pin.packageVersion,
@@ -363,6 +374,7 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
363
374
  const io = {
364
375
  environment: process.env,
365
376
  find: findWindowsChatGPTApp,
377
+ logger: console,
366
378
  run: spawnSync,
367
379
  ...dependencies,
368
380
  };
@@ -388,10 +400,25 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
388
400
  result = { status: null, error };
389
401
  }
390
402
  const binary = io.find(io.environment) || before;
391
- const succeeded = result?.status === 0 && !result?.error;
403
+ // winget uses an error HRESULT when `upgrade` finds an installed package
404
+ // with no newer Store release. That is a healthy idempotent update result,
405
+ // not an installation failure (APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE).
406
+ const alreadyUpToDate = Boolean(
407
+ shouldUpdate
408
+ && binary
409
+ && !result?.error
410
+ && Number.isInteger(result?.status)
411
+ && (result.status >>> 0) === WINDOWS_WINGET_UPDATE_NOT_APPLICABLE,
412
+ );
413
+ if (alreadyUpToDate) {
414
+ io.logger.log("ChatGPT/Codex: Microsoft Store package already up to date.");
415
+ }
416
+ const succeeded = (result?.status === 0 && !result?.error) || alreadyUpToDate;
392
417
  return {
393
418
  binary,
394
- action: succeeded ? (shouldUpdate ? "updated" : "installed") : (before ? "existing" : "install-failed"),
419
+ action: alreadyUpToDate
420
+ ? "existing"
421
+ : succeeded ? (shouldUpdate ? "updated" : "installed") : (before ? "existing" : "install-failed"),
395
422
  result,
396
423
  };
397
424
  }