impel-cli 0.14.2 → 0.15.0
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 +47 -24
- package/package.json +1 -1
- package/src/cli.js +3 -1
- package/src/commands/setup.js +171 -20
- package/src/commands/update.js +40 -2
- package/src/gateway/index.js +33 -5
- 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/README.md
CHANGED
|
@@ -110,8 +110,19 @@ impel update # install the latest Impel CLI later
|
|
|
110
110
|
|
|
111
111
|
`impel setup` keeps your normal Claude and Codex profiles separate. On Mac and
|
|
112
112
|
Windows it also prepares the supported isolated desktop experiences. On Windows,
|
|
113
|
-
it installs missing official vendor command-line tools
|
|
114
|
-
|
|
113
|
+
it installs missing official vendor command-line tools with the vendors'
|
|
114
|
+
checksum-verifying native installers, so Claude Code does not inherit its newer
|
|
115
|
+
Node.js requirement from the deprecated npm package. On Linux, it prepares the
|
|
116
|
+
isolated command-line workflow.
|
|
117
|
+
|
|
118
|
+
If setup or the CLI self-update fails, Impel first runs a deterministic local
|
|
119
|
+
check. In an interactive terminal it then offers assisted recovery; use
|
|
120
|
+
`impel setup --repair` (or `impel update --repair`) to opt in explicitly. Before
|
|
121
|
+
upload, the CLI prints the exact sanitized payload. The hosted agent can only
|
|
122
|
+
return a reviewed action ID; the CLI rejects free-form commands and asks before
|
|
123
|
+
every mutation. Sessions expire after 15 minutes, mutation capabilities are
|
|
124
|
+
single-use, and a private checkpoint lets `--repair` resume an interrupted run.
|
|
125
|
+
Use `--no-recovery` or `IMPEL_DISABLE_INSTALL_RECOVERY=1` to keep recovery off.
|
|
115
126
|
|
|
116
127
|
### If something does not work
|
|
117
128
|
|
|
@@ -133,11 +144,13 @@ access, scopes, and organization membership for every protected action.
|
|
|
133
144
|
|
|
134
145
|
```
|
|
135
146
|
impel setup [--pat <pat>] [--tenant <org>] [--skip-apps] [--skip-clis]
|
|
147
|
+
[--repair|--no-recovery]
|
|
136
148
|
Guided setup: token, tenant, platform clients, verify
|
|
137
149
|
(closes running Claude/ChatGPT apps, updates the vendor
|
|
138
150
|
apps, then installs the vendored Impel apps on macOS;
|
|
139
|
-
|
|
140
|
-
impel update [--check] [--skip-apps]
|
|
151
|
+
runs official native vendor CLI installers on Windows)
|
|
152
|
+
impel update [--check] [--skip-apps] [--repair|--no-recovery]
|
|
153
|
+
Update EVERYTHING: reinstall the CLI from npm, then
|
|
141
154
|
cascade to `app update all`, `skills sync all`, and
|
|
142
155
|
`agents sync all`
|
|
143
156
|
impel auth [--pat <pat>] [--gateway <url>] [--app <url>]
|
|
@@ -472,26 +485,36 @@ and pre-tenancy Impel CLI profiles, so switching organizations cannot strand an
|
|
|
472
485
|
older snapshot. These safeguards apply only to Impel-owned `CODEX_HOME`
|
|
473
486
|
directories; native `~/.codex` remains untouched.
|
|
474
487
|
|
|
475
|
-
On Windows, `impel setup`
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
488
|
+
On Windows, `impel setup` installs only missing command-line tools through the
|
|
489
|
+
vendors' current native PowerShell installers. Claude Code's npm installation is
|
|
490
|
+
deprecated and its current package requires Node.js 22, while Impel itself still
|
|
491
|
+
supports Node.js 18 or newer. The native path keeps that valid Impel setup
|
|
492
|
+
working. Codex's installer is run non-interactively. Both installers download
|
|
493
|
+
platform-specific native releases and verify their published SHA-256 checksums.
|
|
494
|
+
Use `--skip-clis` to prepare the profiles without installing missing vendor
|
|
495
|
+
CLIs. Each tool is installed independently, so a failure from one vendor does
|
|
496
|
+
not prevent the other tool from being installed and configured.
|
|
497
|
+
|
|
498
|
+
The launchers and skill sync still resolve executables through `PATH`/`PATHEXT`,
|
|
499
|
+
including existing global npm `.cmd` and `.bat` shims. Windows environment
|
|
500
|
+
references such as `%NVM_SYMLINK%` are expanded during lookup, and the standard
|
|
501
|
+
npm-global, Claude native-installer, Codex standalone-installer, and Windows
|
|
502
|
+
PowerShell directories are checked even when the parent PowerShell PATH is
|
|
503
|
+
stale. Candidates must also pass a bounded `--version` probe, so a stale or
|
|
504
|
+
half-removed npm shim cannot hide a healthy native install later in the search
|
|
505
|
+
path. Existing batch-shim arguments are forwarded through an explicitly
|
|
506
|
+
escaped `cmd.exe` invocation without Node shell mode; NULs and line breaks,
|
|
507
|
+
which cannot be represented safely through a batch shim, are rejected before
|
|
508
|
+
anything is launched. Managed Codex auth and MCP subprocesses bypass batch
|
|
509
|
+
shims entirely by calling Node directly.
|
|
510
|
+
|
|
511
|
+
If a native installer fails, the wizard identifies the affected tool, preserves
|
|
512
|
+
the other tool's successful install, and prints that vendor's exact official
|
|
513
|
+
PowerShell command to retry. If an installer exits successfully but its command
|
|
514
|
+
cannot be verified, close and reopen PowerShell, retry the printed command, and
|
|
515
|
+
run `impel setup` again. Setup exits non-zero and says it is incomplete until
|
|
516
|
+
both launchers are ready—it does not print a success banner after a failed
|
|
517
|
+
installation.
|
|
495
518
|
|
|
496
519
|
Windows receives the complete CLI workflow: setup/auth, tenant selection,
|
|
497
520
|
isolated Claude and Codex launches, native profile switching, tasks, skills,
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -23,7 +23,8 @@ const HELP = `impel — the Impel gateway CLI: isolated Claude Code, Codex, and
|
|
|
23
23
|
Get started:
|
|
24
24
|
impel setup Guided end-to-end onboarding: token, tenant,
|
|
25
25
|
platform clients, gateway verification
|
|
26
|
-
(--pat <pat> --tenant <org> --skip-apps/--skip-clis
|
|
26
|
+
(--pat <pat> --tenant <org> --skip-apps/--skip-clis
|
|
27
|
+
--repair/--no-recovery)
|
|
27
28
|
impel update Update everything: the CLI itself, then the
|
|
28
29
|
Impel apps, skills, and agents (--check, --skip-apps)
|
|
29
30
|
|
|
@@ -69,6 +70,7 @@ Env:
|
|
|
69
70
|
IMPEL_APP_URL Default app/control-plane URL for setup, auth, PATs, and tasks.
|
|
70
71
|
IMPEL_SKIP_UPDATE_CHECK=1 Silence launch-time update notices.
|
|
71
72
|
IMPEL_SKIP_AGENT_SYNC=1 Keep the last native-agent catalog without refreshing it.
|
|
73
|
+
IMPEL_DISABLE_INSTALL_RECOVERY=1 Disable local and hosted install recovery.
|
|
72
74
|
|
|
73
75
|
Config file:
|
|
74
76
|
~/.config/impel/config.json (mode 0600)
|
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,6 +324,10 @@ 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
332
|
console.log(" ✓ Impel Claude and Impel ChatGPT profiles are ready");
|
|
290
333
|
}
|
|
@@ -292,6 +335,11 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
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"]) {
|
|
@@ -313,6 +361,11 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
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
|
}
|
package/src/gateway/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const SAFE_NAMESPACE = /^[a-z][a-z0-9_-]{1,31}$/u;
|
|
|
11
11
|
const SAFE_PREFIX = /^[a-z][a-z0-9_]{2,31}_$/u;
|
|
12
12
|
const SAFE_ENVIRONMENT_PREFIX = /^[A-Z][A-Z0-9_]{1,31}$/u;
|
|
13
13
|
const SAFE_ROUTE = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*$/u;
|
|
14
|
+
const SAFE_TENANT_ID = /^[A-Za-z0-9_.-]{1,128}$/u;
|
|
14
15
|
const ANSI_ESCAPE_RE = /\u001B(?:\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/gu;
|
|
15
16
|
const CONTROL_TEST_RE = /[\u0000-\u001F\u007F-\u009F]/u;
|
|
16
17
|
const CONTROL_RE = /[\u0000-\u001F\u007F-\u009F]/gu;
|
|
@@ -29,6 +30,15 @@ function validateRoute(name, value) {
|
|
|
29
30
|
return value;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function validateTenantID(value) {
|
|
34
|
+
if (value == null) return null;
|
|
35
|
+
const tenantID = String(value || "").trim();
|
|
36
|
+
if (!SAFE_TENANT_ID.test(tenantID) || tenantID === "." || tenantID === ".." || CONTROL_TEST_RE.test(tenantID)) {
|
|
37
|
+
throw new Error("gateway CLI tenant.defaultId is invalid");
|
|
38
|
+
}
|
|
39
|
+
return tenantID;
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
function validateBrand(input) {
|
|
33
43
|
if (!input || Array.isArray(input) || typeof input !== "object" || input.schemaVersion !== 1) {
|
|
34
44
|
throw new Error("gateway CLI brand schemaVersion must be 1");
|
|
@@ -40,6 +50,8 @@ function validateBrand(input) {
|
|
|
40
50
|
const managedMarker = String(input.cli?.managedMarker || "");
|
|
41
51
|
const environmentPrefix = String(input.cli?.environmentPrefix || command.toUpperCase().replace(/-/gu, "_"));
|
|
42
52
|
const patPrefix = String(input.auth?.patPrefix || "");
|
|
53
|
+
const tenantPrefix = input.auth?.tenantPrefix == null ? null : String(input.auth.tenantPrefix);
|
|
54
|
+
const defaultTenantID = validateTenantID(input.tenant?.defaultId);
|
|
43
55
|
if (!SAFE_ID.test(productID)) throw new Error("gateway CLI product.id is invalid");
|
|
44
56
|
if (!SAFE_ID.test(command)) throw new Error("gateway CLI command is invalid");
|
|
45
57
|
if (!SAFE_NAMESPACE.test(configNamespace)) throw new Error("gateway CLI configNamespace is invalid");
|
|
@@ -47,13 +59,16 @@ function validateBrand(input) {
|
|
|
47
59
|
if (!SAFE_NAMESPACE.test(managedMarker)) throw new Error("gateway CLI managedMarker is invalid");
|
|
48
60
|
if (!SAFE_ENVIRONMENT_PREFIX.test(environmentPrefix)) throw new Error("gateway CLI environmentPrefix is invalid");
|
|
49
61
|
if (!SAFE_PREFIX.test(patPrefix)) throw new Error("gateway CLI PAT prefix is invalid");
|
|
62
|
+
if (tenantPrefix != null && !SAFE_PREFIX.test(tenantPrefix)) throw new Error("gateway CLI tenant prefix is invalid");
|
|
63
|
+
if (defaultTenantID && !tenantPrefix) throw new Error("gateway CLI tenant prefix is required when tenant.defaultId is set");
|
|
50
64
|
|
|
51
65
|
const defaultOrigin = input.gateway?.defaultOrigin == null ? null : normalizeGatewayUrl(input.gateway.defaultOrigin);
|
|
52
66
|
return Object.freeze({
|
|
53
67
|
schemaVersion: 1,
|
|
54
68
|
product: Object.freeze({ id: productID, displayName: validateText("product.displayName", input.product?.displayName) }),
|
|
55
69
|
cli: Object.freeze({ command, configNamespace, providerId: providerID, managedMarker, environmentPrefix }),
|
|
56
|
-
auth: Object.freeze({ patPrefix }),
|
|
70
|
+
auth: Object.freeze({ patPrefix, tenantPrefix }),
|
|
71
|
+
tenant: Object.freeze({ defaultId: defaultTenantID }),
|
|
57
72
|
gateway: Object.freeze({
|
|
58
73
|
defaultOrigin,
|
|
59
74
|
healthPath: validateRoute("healthPath", input.gateway?.healthPath),
|
|
@@ -183,9 +198,21 @@ export function createGatewayCli(options) {
|
|
|
183
198
|
return `${secret.slice(0, brand.auth.patPrefix.length + 4)}...${secret.slice(-4)}`;
|
|
184
199
|
}
|
|
185
200
|
|
|
201
|
+
function bearerCredential(pat) {
|
|
202
|
+
if (!brand.tenant.defaultId) return pat;
|
|
203
|
+
const encodedTenant = Buffer.from(brand.tenant.defaultId, "utf8").toString("base64url");
|
|
204
|
+
return `${brand.auth.tenantPrefix}${encodedTenant}.${pat}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
186
207
|
function redactSecretText(value) {
|
|
187
208
|
const escapedPrefix = brand.auth.patPrefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
188
|
-
const
|
|
209
|
+
const escapedTenantPrefix = brand.auth.tenantPrefix?.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
210
|
+
const tokenPattern = new RegExp(
|
|
211
|
+
escapedTenantPrefix
|
|
212
|
+
? `(?:${escapedTenantPrefix}[A-Za-z0-9_-]+\\.)?${escapedPrefix}[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)?`
|
|
213
|
+
: `${escapedPrefix}[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)?`,
|
|
214
|
+
"gu",
|
|
215
|
+
);
|
|
189
216
|
return String(value ?? "")
|
|
190
217
|
.replace(tokenPattern, "[REDACTED GATEWAY CREDENTIAL]")
|
|
191
218
|
.replace(ANSI_ESCAPE_RE, "")
|
|
@@ -244,7 +271,7 @@ export function createGatewayCli(options) {
|
|
|
244
271
|
if (!health.ok) throw new Error(`gateway health returned HTTP ${health.status}`);
|
|
245
272
|
const models = await fetch(gatewayRoutes.models(gatewayUrl), {
|
|
246
273
|
...requestOptions,
|
|
247
|
-
headers: { authorization: `Bearer ${pat}` },
|
|
274
|
+
headers: { authorization: `Bearer ${bearerCredential(pat)}` },
|
|
248
275
|
});
|
|
249
276
|
if (!models.ok) throw new Error(`gateway authentication returned HTTP ${models.status}`);
|
|
250
277
|
const body = await models.json();
|
|
@@ -353,6 +380,7 @@ export function createGatewayCli(options) {
|
|
|
353
380
|
const config = loadConfig();
|
|
354
381
|
if (!config?.pat || !config?.gatewayUrl) throw new Error(`not configured; run \`${brand.cli.command} setup\``);
|
|
355
382
|
print(`Gateway: ${config.gatewayUrl}`);
|
|
383
|
+
if (brand.tenant.defaultId) print(`Tenant: ${brand.tenant.defaultId}`);
|
|
356
384
|
print(`PAT: ${maskSecret(config.pat)}`);
|
|
357
385
|
print(`Profiles: ${profileRoot}`);
|
|
358
386
|
try {
|
|
@@ -367,7 +395,7 @@ export function createGatewayCli(options) {
|
|
|
367
395
|
async function cmdToken() {
|
|
368
396
|
const config = loadConfig();
|
|
369
397
|
if (!config?.pat) throw new Error(`not authenticated; run \`${brand.cli.command} setup\` or \`${brand.cli.command} auth\``);
|
|
370
|
-
output.write(`${config.pat}\n`);
|
|
398
|
+
output.write(`${bearerCredential(config.pat)}\n`);
|
|
371
399
|
return 0;
|
|
372
400
|
}
|
|
373
401
|
|
|
@@ -408,7 +436,7 @@ export function createGatewayCli(options) {
|
|
|
408
436
|
const profile = ensureClaudeProfile(config.gatewayUrl);
|
|
409
437
|
childEnvironment.CLAUDE_CONFIG_DIR = profile.configDir;
|
|
410
438
|
childEnvironment.ANTHROPIC_BASE_URL = gatewayRoutes.anthropic(config.gatewayUrl);
|
|
411
|
-
childEnvironment.ANTHROPIC_AUTH_TOKEN = config.pat;
|
|
439
|
+
childEnvironment.ANTHROPIC_AUTH_TOKEN = bearerCredential(config.pat);
|
|
412
440
|
} else if (tool === "codex") {
|
|
413
441
|
const profile = ensureCodexProfile(config.gatewayUrl);
|
|
414
442
|
childEnvironment.CODEX_HOME = profile.codexHome;
|