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.
- package/README.md +48 -24
- package/package.json +1 -1
- package/src/apps.js +16 -1
- package/src/cli.js +3 -1
- package/src/commands/apps.js +126 -40
- package/src/commands/setup.js +173 -22
- package/src/commands/update.js +40 -2
- package/src/installRecovery/actions.js +440 -0
- package/src/installRecovery/checkpoint.js +59 -0
- package/src/installRecovery/client.js +84 -0
- package/src/installRecovery/loop.js +258 -0
- package/src/installRecovery/redact.js +83 -0
- package/src/nativeProcess.js +21 -3
- package/src/windowsSetup.js +115 -28
package/src/commands/setup.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
-
|
|
215
|
-
|
|
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
|
}
|