impel-cli 0.14.3 → 0.15.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.
@@ -16,7 +16,8 @@ import {
16
16
  import { promptSecret, promptText } from "../prompt.js";
17
17
  import { fetchTenants, normalizeTenantId, tenantCredential } from "../tenants.js";
18
18
  import { cmdApps } from "./apps.js";
19
- import { prepareWindowsClis, windowsCliInstallCommand } from "../windowsSetup.js";
19
+ import { prepareWindowsClis, windowsCliInstallCommands } from "../windowsSetup.js";
20
+ import { runInstallRecovery } from "../installRecovery/loop.js";
20
21
 
21
22
  const HELP = `impel setup - guided end-to-end gateway setup
22
23
 
@@ -34,6 +35,8 @@ Usage:
34
35
  impel setup --tenant <org> Preselect the organization
35
36
  impel setup --skip-apps Skip the desktop-app installation step
36
37
  impel setup --skip-clis On Windows, don't install missing vendor CLIs
38
+ impel setup --repair Explicitly opt into sanitized assisted recovery on failure
39
+ impel setup --no-recovery Disable local and hosted recovery for this run
37
40
  `;
38
41
 
39
42
  /**
@@ -104,6 +107,7 @@ export async function cmdSetup(argv, overrides = {}) {
104
107
  installApps: cmdApps,
105
108
  prepareWindowsClis,
106
109
  probe: probeGateway,
110
+ recoverInstall: runInstallRecovery,
107
111
  isTTY: process.stdin.isTTY,
108
112
  platform: process.platform,
109
113
  ...overrides,
@@ -115,6 +119,8 @@ export async function cmdSetup(argv, overrides = {}) {
115
119
  app: { type: "string" },
116
120
  "skip-apps": { type: "boolean" },
117
121
  "skip-clis": { type: "boolean" },
122
+ repair: { type: "boolean" },
123
+ "no-recovery": { type: "boolean" },
118
124
  help: { type: "boolean" },
119
125
  });
120
126
  if (flags.help) {
@@ -174,6 +180,15 @@ export async function cmdSetup(argv, overrides = {}) {
174
180
  console.log(` ✓ stored in ~/.config/impel/config.json (mode 0600)`);
175
181
  console.log("");
176
182
 
183
+ const setupFailures = [];
184
+ const recordSetupFailure = (failure) => {
185
+ setupFailures.push({
186
+ platform: io.platform,
187
+ architecture: process.arch,
188
+ ...failure,
189
+ });
190
+ };
191
+
177
192
  // ── Step 2: organization ─────────────────────────────────────────────────
178
193
  // A successful tenant fetch also proves the token: the app verifies it
179
194
  // against impel-identity before answering.
@@ -249,23 +264,42 @@ export async function cmdSetup(argv, overrides = {}) {
249
264
  }
250
265
  if (result.missingAfter.length > 0 && !flags["skip-clis"]) {
251
266
  platformSetupFailed = true;
252
- if (result.installSucceeded === false) {
253
- const failure = result.installFailure;
254
- const detail = failure?.message
255
- ? `: ${redactSecretText(failure.message)}`
256
- : Number.isInteger(failure?.status)
257
- ? ` (exit code ${failure.status})`
258
- : failure?.signal
259
- ? ` (signal ${failure.signal})`
260
- : "";
261
- console.error(` npm could not install the missing vendor CLIs${detail}.`);
262
- console.error(" Retry this command in the same PowerShell window:");
263
- console.error(` ${result.installCommand || windowsCliInstallCommand(result.missingAfter)}`);
264
- } else {
265
- console.error(" npm completed, but the installed commands are not discoverable.");
266
- console.error(" Close and reopen PowerShell, then re-run `impel setup`.");
267
- console.error(" If they are still missing, run `npm prefix --global` and add that directory to your user PATH.");
267
+ const fallbackCommands = windowsCliInstallCommands(result.missingAfter);
268
+ for (const tool of result.missingAfter) {
269
+ const label = tool === "claude" ? "Claude Code" : "Codex";
270
+ const installation = result.installations?.[tool];
271
+ const failure = installation?.failure;
272
+ if (installation?.succeeded === false || (!installation && result.installSucceeded === false)) {
273
+ const detail = failure?.message
274
+ ? `: ${redactSecretText(failure.message)}`
275
+ : Number.isInteger(failure?.status)
276
+ ? ` (exit code ${failure.status})`
277
+ : failure?.signal
278
+ ? ` (signal ${failure.signal})`
279
+ : "";
280
+ console.error(` ${label}'s native installer failed${detail}.`);
281
+ recordSetupFailure({
282
+ step: `install.vendor_cli.${tool}`,
283
+ command: installation?.command || result.installCommands?.[tool] || fallbackCommands[tool],
284
+ exitCode: failure?.status,
285
+ signal: failure?.signal,
286
+ errorCode: failure?.code,
287
+ message: failure?.message
288
+ ? `${label}'s native installer failed: ${failure.message}`
289
+ : `${label}'s native installer did not reach a verified state.`,
290
+ });
291
+ } else {
292
+ console.error(` ${label}'s native installer completed, but Impel could not verify the command.`);
293
+ recordSetupFailure({
294
+ step: `verify.vendor_cli.${tool}`,
295
+ command: installation?.command || result.installCommands?.[tool] || fallbackCommands[tool],
296
+ message: `${label}'s installer completed but the command was not discoverable or did not pass its version check.`,
297
+ });
298
+ }
299
+ console.error(" Retry its official installer in the same PowerShell window:");
300
+ console.error(` ${installation?.command || result.installCommands?.[tool] || fallbackCommands[tool]}`);
268
301
  }
302
+ console.error(" Then close and reopen PowerShell and re-run `impel setup`.");
269
303
  } else if (result.missingAfter.length > 0) {
270
304
  console.log(" Vendor CLI installation was skipped; install it before using the matching launcher.");
271
305
  } else {
@@ -275,6 +309,11 @@ export async function cmdSetup(argv, overrides = {}) {
275
309
  platformSetupFailed = true;
276
310
  console.error(` ✗ Windows CLI setup failed (${redactSecretText(error?.message || error)})`);
277
311
  console.error(" Fix the issue, then re-run `impel setup`.");
312
+ recordSetupFailure({
313
+ step: "setup.windows_clis",
314
+ errorCode: error?.code,
315
+ message: `Windows CLI setup failed: ${error?.message || error}`,
316
+ });
278
317
  }
279
318
  if (flags["skip-apps"]) {
280
319
  console.log(" Windows desktop apps: skipped (--skip-apps).");
@@ -285,13 +324,22 @@ export async function cmdSetup(argv, overrides = {}) {
285
324
  if (installed === false) {
286
325
  platformSetupFailed = true;
287
326
  console.error(" ✗ Windows desktop app setup failed; fix the issue above, then re-run `impel app install all`.");
327
+ recordSetupFailure({
328
+ step: "install.vendor_app.all",
329
+ message: "Windows desktop app setup returned an unsuccessful result.",
330
+ });
288
331
  } else {
289
- console.log(" ✓ Impel Claude and Impel ChatGPT profiles are ready");
332
+ console.log(" ✓ available Impel desktop app profiles are ready");
290
333
  }
291
334
  } catch (error) {
292
335
  platformSetupFailed = true;
293
336
  console.error(` ✗ Windows desktop app setup failed (${redactSecretText(error?.message || error)})`);
294
337
  console.error(" Fix the issue, then re-run `impel app install all`.");
338
+ recordSetupFailure({
339
+ step: "install.vendor_app.all",
340
+ errorCode: error?.code,
341
+ message: `Windows desktop app setup failed: ${error?.message || error}`,
342
+ });
295
343
  }
296
344
  }
297
345
  } else if (flags["skip-apps"]) {
@@ -306,13 +354,18 @@ export async function cmdSetup(argv, overrides = {}) {
306
354
  );
307
355
  try {
308
356
  await io.installApps(["install", "all"]);
309
- console.log(" ✓ Impel Claude and Impel ChatGPT installed");
357
+ console.log(" ✓ available Impel desktop apps installed");
310
358
  } catch (error) {
311
359
  platformSetupFailed = true;
312
360
  console.error(
313
361
  ` ✗ app installation failed (${redactSecretText(error?.message || error)})`
314
362
  );
315
363
  console.error(" Fix the issue, then re-run `impel app install`.");
364
+ recordSetupFailure({
365
+ step: "install.vendor_app.all",
366
+ errorCode: error?.code,
367
+ message: `Desktop app installation failed: ${error?.message || error}`,
368
+ });
316
369
  }
317
370
  }
318
371
  console.log("");
@@ -326,15 +379,113 @@ export async function cmdSetup(argv, overrides = {}) {
326
379
  verificationFailed = true;
327
380
  console.log(`4/4 Verify: ✗ gateway reachable but it rejected the token (HTTP ${probe.status}).`);
328
381
  console.log(" Mint a fresh token and re-run `impel setup`.");
329
- process.exitCode = 1;
382
+ recordSetupFailure({
383
+ step: "verify.gateway",
384
+ exitCode: probe.status,
385
+ message: `The gateway rejected the configured credential with HTTP ${probe.status}.`,
386
+ });
330
387
  } else {
331
388
  verificationFailed = true;
332
389
  console.log(`4/4 Verify: ✗ gateway unreachable (${probe.error}).`);
333
390
  console.log(" Check your network, then run `impel status` to retry.");
334
- process.exitCode = 1;
391
+ recordSetupFailure({
392
+ step: "verify.gateway",
393
+ message: `The gateway could not be reached: ${probe.error}.`,
394
+ });
395
+ }
396
+
397
+ if (platformSetupFailed || verificationFailed) {
398
+ const aggregateFailure = {
399
+ platform: io.platform,
400
+ architecture: process.arch,
401
+ step: setupFailures.length === 1 ? setupFailures[0].step : "setup.multiple",
402
+ message:
403
+ setupFailures.length === 1
404
+ ? setupFailures[0].message
405
+ : `${setupFailures.length} setup checks failed.`,
406
+ diagnostics: Object.fromEntries(
407
+ setupFailures.slice(0, 20).map((failure, index) => [
408
+ `failure${index + 1}`,
409
+ `${failure.step}: ${failure.message}`,
410
+ ])
411
+ ),
412
+ };
413
+ try {
414
+ const recovery = await io.recoverInstall(
415
+ {
416
+ failure: aggregateFailure,
417
+ config,
418
+ explicit: Boolean(flags.repair),
419
+ noRecovery: Boolean(flags["no-recovery"]),
420
+ actionContext: {
421
+ probeGateway: () => io.probe(gatewayUrl, pat, tenant.id),
422
+ installVendorClis: (tool) =>
423
+ io.prepareWindowsClis({
424
+ gatewayUrl,
425
+ tenantId: tenant.id,
426
+ skipInstall: false,
427
+ installTools: [tool],
428
+ }),
429
+ repairProfiles: async () => {
430
+ if (io.platform !== "win32") return false;
431
+ const result = await io.prepareWindowsClis({
432
+ gatewayUrl,
433
+ tenantId: tenant.id,
434
+ skipInstall: true,
435
+ });
436
+ return result.missingAfter.length === 0;
437
+ },
438
+ installVendorApp: (target) => io.installApps(["install", target]),
439
+ retryStep: async (step) => {
440
+ if (/vendor_cli|windows_clis/iu.test(step || "")) {
441
+ const result = await io.prepareWindowsClis({
442
+ gatewayUrl,
443
+ tenantId: tenant.id,
444
+ skipInstall: false,
445
+ });
446
+ return result.missingAfter.length === 0;
447
+ }
448
+ if (/vendor_app/iu.test(step || "")) {
449
+ return (await io.installApps(["install", "all"])) !== false;
450
+ }
451
+ if (/gateway/iu.test(step || "")) {
452
+ const result = await io.probe(gatewayUrl, pat, tenant.id);
453
+ return Boolean(result.reachable && !result.rejected);
454
+ }
455
+ return false;
456
+ },
457
+ },
458
+ },
459
+ {
460
+ isTTY: io.isTTY,
461
+ promptText: io.promptText,
462
+ ...(overrides.recoveryOverrides || {}),
463
+ }
464
+ );
465
+ if (recovery.fixed) {
466
+ if (io.platform === "win32" && setupFailures.some((failure) => /vendor_cli|windows_clis/iu.test(failure.step))) {
467
+ const checked = await io.prepareWindowsClis({
468
+ gatewayUrl,
469
+ tenantId: tenant.id,
470
+ skipInstall: true,
471
+ });
472
+ platformSetupFailed = checked.missingAfter.length > 0;
473
+ } else {
474
+ platformSetupFailed = false;
475
+ }
476
+ if (setupFailures.some((failure) => failure.step === "verify.gateway")) {
477
+ const checked = await io.probe(gatewayUrl, pat, tenant.id);
478
+ verificationFailed = !checked.reachable || checked.rejected;
479
+ } else {
480
+ verificationFailed = false;
481
+ }
482
+ }
483
+ } catch (error) {
484
+ console.warn(`Install recovery could not start (${redactSecretText(error?.message || error)}).`);
485
+ }
335
486
  }
336
487
 
337
- if (platformSetupFailed) process.exitCode = 1;
488
+ if (platformSetupFailed || verificationFailed) process.exitCode = 1;
338
489
 
339
490
  console.log("");
340
491
  if (platformSetupFailed || verificationFailed) {
@@ -16,6 +16,7 @@ import { nativeCommandInvocation } from "../nativeProcess.js";
16
16
  import { windowsClaudeUserData } from "../windowsApps.js";
17
17
  import { withProgress } from "../progress.js";
18
18
  import { installedAppTenantIds } from "./apps.js";
19
+ import { runInstallRecovery } from "../installRecovery/loop.js";
19
20
  import {
20
21
  fetchRemoteVersion,
21
22
  installedVersion,
@@ -38,6 +39,8 @@ Usage:
38
39
  impel update Update the CLI, then cascade to apps, skills, and agents
39
40
  impel update --check Report whether an update is available; change nothing
40
41
  impel update --skip-apps Skip the desktop-app step of the cascade
42
+ impel update --repair Explicitly opt into sanitized assisted recovery on failure
43
+ impel update --no-recovery Disable local and hosted recovery for this run
41
44
  `;
42
45
 
43
46
  function reportProcessFailure(stage, result) {
@@ -166,12 +169,16 @@ export async function cmdUpdate(argv, overrides = {}) {
166
169
  appsInstalled: anyAppInstalled,
167
170
  platform: process.platform,
168
171
  progress: withProgress,
172
+ recoverInstall: runInstallRecovery,
173
+ loadConfig,
169
174
  ...overrides,
170
175
  };
171
176
  const { flags } = parseFlags(argv, {
172
177
  check: { type: "boolean" },
173
178
  "skip-apps": { type: "boolean" },
174
179
  "refresh-cache": { type: "boolean" },
180
+ repair: { type: "boolean" },
181
+ "no-recovery": { type: "boolean" },
175
182
  help: { type: "boolean" },
176
183
  });
177
184
  if (flags.help) {
@@ -211,8 +218,39 @@ export async function cmdUpdate(argv, overrides = {}) {
211
218
  console.error(" Verify `npm --version` in PowerShell, then retry `impel update`.");
212
219
  console.error(" Manual recovery: `npm install --global impel-cli@latest`.");
213
220
  }
214
- process.exitCode = 1;
215
- return;
221
+ const config = io.loadConfig();
222
+ let recovered = false;
223
+ if (config?.pat && config?.tenantId && config?.appUrl) {
224
+ try {
225
+ const recovery = await io.recoverInstall(
226
+ {
227
+ failure: {
228
+ platform: io.platform,
229
+ architecture: process.arch,
230
+ step: "install.impel_cli",
231
+ command: "npm install --global impel-cli@latest",
232
+ message: "The npm-verified global impel-cli update failed.",
233
+ },
234
+ config,
235
+ explicit: Boolean(flags.repair),
236
+ noRecovery: Boolean(flags["no-recovery"]),
237
+ actionContext: {
238
+ retryStep: async () =>
239
+ io.selfUpdate(updateInstallSpec()) === true,
240
+ },
241
+ },
242
+ overrides.recoveryOverrides || {}
243
+ );
244
+ recovered = recovery.fixed;
245
+ } catch (error) {
246
+ console.warn(`impel update: assisted recovery could not start (${redactSecretText(error?.message || error)}).`);
247
+ }
248
+ }
249
+ if (!recovered) {
250
+ process.exitCode = 1;
251
+ return;
252
+ }
253
+ console.log("CLI: recovery verified the update path.");
216
254
  }
217
255
  console.log(`CLI: updated${remote ? ` to v${remote}` : ""}.`);
218
256
  }