impel-cli 0.16.5 → 0.17.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 +224 -971
- package/package.json +2 -2
- package/src/cli.js +33 -41
- package/src/commands/apps.js +232 -0
- package/src/commands/converge.js +174 -0
- package/src/commands/setup.js +312 -428
- package/src/commands/status.js +133 -83
- package/src/commands/tasks.js +1 -1
- package/src/commands/tenant.js +3 -5
- package/src/commands/update.js +39 -85
- package/src/commands/use.js +67 -238
- package/src/installRecovery/engine.js +9 -27
- package/src/installRecovery/redact.js +4 -0
- package/src/installRecovery/tools.js +1 -1
- package/src/provisioning.js +354 -0
- package/src/shellEntries.js +186 -0
- package/src/skills.js +1 -1
- package/src/tenants.js +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "impel-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.17.1",
|
|
4
|
+
"description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"impel": "bin/impel.js"
|
package/src/cli.js
CHANGED
|
@@ -18,59 +18,40 @@ import { cmdSetup } from "./commands/setup.js";
|
|
|
18
18
|
import { cmdSessions } from "./commands/sessions.js";
|
|
19
19
|
import { cmdUpdate } from "./commands/update.js";
|
|
20
20
|
import { cmdExperimental } from "./commands/experimental.js";
|
|
21
|
+
import { cmdConverge } from "./commands/converge.js";
|
|
21
22
|
|
|
22
|
-
const HELP = `impel —
|
|
23
|
+
const HELP = `impel — isolated Impel workspaces for every tenant
|
|
23
24
|
|
|
24
25
|
Get started:
|
|
25
|
-
impel setup
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Run:
|
|
26
|
+
impel setup Authenticate and prepare every accessible tenant
|
|
27
|
+
(--pat <pat>, --tenant <org>, --no-recovery)
|
|
28
|
+
impel update Update the CLI and reconcile every tenant
|
|
29
|
+
(--check, --skip-apps, --no-recovery)
|
|
30
|
+
|
|
31
|
+
Work:
|
|
33
32
|
impel claude [args...] Launch Claude Code with an isolated Impel profile
|
|
34
33
|
impel codex [args...] Launch Codex with an isolated Impel profile
|
|
35
|
-
impel
|
|
36
|
-
impel
|
|
37
|
-
impel
|
|
34
|
+
impel tenant list List accessible organizations
|
|
35
|
+
impel tenant current Show the CLI's current organization
|
|
36
|
+
impel tenant use <org> Select the organization for CLI launches
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
impel
|
|
38
|
+
Workspace:
|
|
39
|
+
impel tasks list|get|create|update|delete Work with Impel tickets (aliases: task, ticket[s])
|
|
40
|
+
|
|
41
|
+
Account:
|
|
41
42
|
impel pat create --label <label> Mint a personal PAT (--org/--scopes/--ttl-days)
|
|
42
43
|
impel pat create --agent <agent-id> Mint an org-scoped PAT associated with an agent
|
|
43
44
|
impel pat revoke <token-id> --yes Irreversibly revoke one of your PATs
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
impel
|
|
47
|
-
impel
|
|
48
|
-
impel skills sync [claude|codex|all] Sync shared skills into managed clients
|
|
49
|
-
impel agents sync [claude|codex|all] Sync explicit tenant agents into native clients
|
|
50
|
-
|
|
51
|
-
Desktop apps (macOS and Windows):
|
|
52
|
-
target is claude, chatgpt/codex, or all (default: all)
|
|
53
|
-
impel app install [target] [--tenant <org>] Install that tenant's isolated app/profile
|
|
54
|
-
impel app update [target] [--tenant <org>] Update profiles; all without --tenant updates
|
|
55
|
-
every installed tenant (--force: clean rebuild)
|
|
56
|
-
impel app refresh [target] [--tenant <org>] Configs/catalog/skills/agents only; safe while apps run
|
|
57
|
-
impel app status [target] [--tenant <org>] Show isolated app and vendor versions
|
|
58
|
-
impel app uninstall [target] [--tenant <org>] [--keep-data]
|
|
59
|
-
Remove only that tenant's managed apps/data
|
|
60
|
-
|
|
61
|
-
Native profile switching (advanced; setup never touches native tools):
|
|
62
|
-
impel use gateway [claude|codex|all] Route native Claude Code/Codex through the gateway
|
|
63
|
-
impel use account [claude|codex|all] Revert to your own Anthropic/OpenAI login
|
|
64
|
-
impel on / impel off Aliases for the two commands above
|
|
45
|
+
|
|
46
|
+
Diagnostics:
|
|
47
|
+
impel status Authentication, current tenant, and local readiness
|
|
48
|
+
impel doctor [--tenant <org>|--all-tenants] Synthetic provider, routing, and latency checks
|
|
65
49
|
|
|
66
50
|
impel help Show this help
|
|
67
51
|
impel --version Show the installed version
|
|
68
52
|
|
|
69
53
|
Env:
|
|
70
|
-
IMPEL_GATEWAY_URL Default gateway URL when --gateway isn't passed.
|
|
71
|
-
IMPEL_APP_URL Default app/control-plane URL for setup, auth, PATs, and tasks.
|
|
72
54
|
IMPEL_SKIP_UPDATE_CHECK=1 Silence launch-time update notices.
|
|
73
|
-
IMPEL_SKIP_AGENT_SYNC=1 Keep the last native-agent catalog without refreshing it.
|
|
74
55
|
IMPEL_DISABLE_INSTALL_RECOVERY=1 Disable local and hosted install recovery.
|
|
75
56
|
|
|
76
57
|
Config file:
|
|
@@ -159,6 +140,8 @@ export async function main(argv) {
|
|
|
159
140
|
case "agent":
|
|
160
141
|
return cmdAgents(rest);
|
|
161
142
|
|
|
143
|
+
// Compatibility shims retained for one release. They can only refuse an
|
|
144
|
+
// old enable request or restore legacy Impel-owned native mutations.
|
|
162
145
|
case "on":
|
|
163
146
|
return runUse("gateway", rest[0]);
|
|
164
147
|
|
|
@@ -169,7 +152,7 @@ export async function main(argv) {
|
|
|
169
152
|
const [mode, targetToken] = rest;
|
|
170
153
|
if (mode !== "gateway" && mode !== "account") {
|
|
171
154
|
console.error(`impel use: expected \`gateway\` or \`account\`, got "${mode ?? ""}".`);
|
|
172
|
-
console.error("
|
|
155
|
+
console.error(" Native switching has been removed; use `impel claude` or `impel codex`.");
|
|
173
156
|
process.exitCode = 1;
|
|
174
157
|
return;
|
|
175
158
|
}
|
|
@@ -178,14 +161,23 @@ export async function main(argv) {
|
|
|
178
161
|
|
|
179
162
|
case "setup":
|
|
180
163
|
// The old per-tool aliases (setup claude|codex-cli|codex-app) flipped
|
|
181
|
-
// native configs; that swap is no longer part of setup.
|
|
182
|
-
// positional targets with a pointer to `impel use gateway`.
|
|
164
|
+
// native configs; that swap is no longer part of setup.
|
|
183
165
|
return cmdSetup(rest);
|
|
184
166
|
|
|
185
167
|
case "update":
|
|
186
168
|
case "upgrade":
|
|
187
169
|
return cmdUpdate(rest);
|
|
188
170
|
|
|
171
|
+
// Hidden continuation executed by the freshly installed CLI build.
|
|
172
|
+
case "_converge":
|
|
173
|
+
return cmdConverge(rest);
|
|
174
|
+
|
|
175
|
+
// Hidden OS-shell launcher. Setup/update guarantees the app is ready.
|
|
176
|
+
case "_app-launch": {
|
|
177
|
+
const [target = "all", ...args] = rest;
|
|
178
|
+
return cmdApps(["open", target, ...args]);
|
|
179
|
+
}
|
|
180
|
+
|
|
189
181
|
// Intentionally omitted from the public help while the contract is gated
|
|
190
182
|
// server-side and limited to Impel-managed desktop apps.
|
|
191
183
|
case "experimental":
|
package/src/commands/apps.js
CHANGED
|
@@ -180,6 +180,21 @@ export function installedAppTenantIds(targets, {
|
|
|
180
180
|
return installed.sort((left, right) => left.localeCompare(right));
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
async function maybeRunLegacyUpdateHandoff(argv, overrides) {
|
|
184
|
+
if (overrides.singleTenantUpdate) return null;
|
|
185
|
+
const [action, target, ...rest] = argv;
|
|
186
|
+
if (action !== "update" || target !== "all" || rest.length > 0) return null;
|
|
187
|
+
|
|
188
|
+
// impel-cli <=0.16.5 re-executes a newly installed build with this exact
|
|
189
|
+
// command. Route that cross-version handoff through the current full-tenant
|
|
190
|
+
// reconciler so first-time tenant targets are not limited to local installs.
|
|
191
|
+
const runConvergence = overrides.runAllTenantConvergence || (async () => {
|
|
192
|
+
const { cmdConverge } = await import("./converge.js");
|
|
193
|
+
return cmdConverge([]);
|
|
194
|
+
});
|
|
195
|
+
return runConvergence();
|
|
196
|
+
}
|
|
197
|
+
|
|
183
198
|
async function maybeUpdateAllInstalledTenants(argv, overrides, platform) {
|
|
184
199
|
if (overrides.singleTenantUpdate) return null;
|
|
185
200
|
const [action = "status", rawTarget, ...rest] = argv;
|
|
@@ -265,6 +280,221 @@ function configureWindowsApps(config, targets, vendorPaths, catalog, io) {
|
|
|
265
280
|
return { catalog, userData };
|
|
266
281
|
}
|
|
267
282
|
|
|
283
|
+
function explicitTenantAppConfig(baseConfig, tenant, targets) {
|
|
284
|
+
if (!baseConfig?.pat || !baseConfig?.gatewayUrl) throw new Error("an authenticated Impel config is required");
|
|
285
|
+
const tenantId = normalizeTenantId(tenant?.id || tenant?.slug);
|
|
286
|
+
const providers = crossAppModelsEnabled(baseConfig)
|
|
287
|
+
? ["claude", "codex"]
|
|
288
|
+
: targets.map((target) => (target === "claude" ? "claude" : "codex"));
|
|
289
|
+
assertProviderScopes(baseConfig.scopes, providers, { requireLive: true });
|
|
290
|
+
return {
|
|
291
|
+
...baseConfig,
|
|
292
|
+
tenantId,
|
|
293
|
+
tenantName: tenant?.name || tenantId,
|
|
294
|
+
pat: tenantCredential(baseConfig.pat, tenantId),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Reconcile one explicit tenant's Windows app profiles without parsing CLI
|
|
300
|
+
* arguments or changing the globally selected tenant. Shared vendor work is
|
|
301
|
+
* skipped after the first tenant by passing skipVendor.
|
|
302
|
+
*/
|
|
303
|
+
export async function reconcileWindowsTenantApps({
|
|
304
|
+
baseConfig,
|
|
305
|
+
tenant,
|
|
306
|
+
mode = "update",
|
|
307
|
+
targets = ["claude", "chatgpt"],
|
|
308
|
+
skipVendor = false,
|
|
309
|
+
skipVendorTargets = [],
|
|
310
|
+
confirmVendorInstall = async () => true,
|
|
311
|
+
homeDir = os.homedir(),
|
|
312
|
+
environment = process.env,
|
|
313
|
+
} = {}, overrides = {}) {
|
|
314
|
+
const io = {
|
|
315
|
+
findClaudeApp: findWindowsClaudeApp,
|
|
316
|
+
ensureClaudeApp: ensureWindowsClaudeApp,
|
|
317
|
+
claudeUserData: windowsClaudeUserData,
|
|
318
|
+
findChatGPTApp: findWindowsChatGPTApp,
|
|
319
|
+
ensureChatGPTApp: ensureWindowsChatGPTApp,
|
|
320
|
+
findManagedChatGPTApp: findManagedWindowsChatGPTApp,
|
|
321
|
+
stageChatGPTApp: stageWindowsChatGPTApp,
|
|
322
|
+
chatGPTStageIsCurrent: windowsChatGPTStageIsCurrent,
|
|
323
|
+
fetchModels: fetchGatewayModels,
|
|
324
|
+
installFiles: installManagedAppFiles,
|
|
325
|
+
syncSkills: syncSkillsSafe,
|
|
326
|
+
syncAgents: syncAgentProfilesSafe,
|
|
327
|
+
existsSync: fs.existsSync,
|
|
328
|
+
log: (message) => console.log(message),
|
|
329
|
+
...overrides,
|
|
330
|
+
};
|
|
331
|
+
const config = explicitTenantAppConfig(baseConfig, tenant, targets);
|
|
332
|
+
const catalog = await fetchWindowsCatalog(config, io);
|
|
333
|
+
const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, { log: io.log });
|
|
334
|
+
const supported = new Set(actionTargets);
|
|
335
|
+
const unsupported = targets.filter((target) => !supported.has(target));
|
|
336
|
+
const userData = io.claudeUserData(environment, config.tenantId);
|
|
337
|
+
const paths = appPaths(homeDir, config.tenantId, { claudeUserData: userData, tenantName: config.tenantName });
|
|
338
|
+
const vendorPaths = {};
|
|
339
|
+
const preparedTargets = new Set(skipVendor ? actionTargets : skipVendorTargets);
|
|
340
|
+
|
|
341
|
+
for (const target of actionTargets) {
|
|
342
|
+
const isClaude = target === "claude";
|
|
343
|
+
const find = isClaude ? io.findClaudeApp : io.findChatGPTApp;
|
|
344
|
+
const ensure = isClaude ? io.ensureClaudeApp : io.ensureChatGPTApp;
|
|
345
|
+
let binary = find(environment);
|
|
346
|
+
if (!preparedTargets.has(target)) {
|
|
347
|
+
const confirmed = await confirmVendorInstall(target, { platform: "win32", mode });
|
|
348
|
+
if (!confirmed) {
|
|
349
|
+
throw new Error(`${isClaude ? "Claude" : "ChatGPT/Codex"} vendor ${mode === "update" ? "update" : "installation"} requires confirmation`);
|
|
350
|
+
}
|
|
351
|
+
const vendor = ensure({ update: mode === "update" }, { environment });
|
|
352
|
+
binary = vendor.binary;
|
|
353
|
+
if (!binary) {
|
|
354
|
+
const label = isClaude ? "Claude" : "ChatGPT/Codex";
|
|
355
|
+
throw new Error(`${label} vendor installation failed (${windowsProcessFailure(vendor.result)})`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (binary) vendorPaths[target] = binary;
|
|
359
|
+
}
|
|
360
|
+
if (actionTargets.includes("claude") && !vendorPaths.claude) {
|
|
361
|
+
throw new Error("Claude vendor app is unavailable");
|
|
362
|
+
}
|
|
363
|
+
if (actionTargets.includes("chatgpt")) {
|
|
364
|
+
let managed = io.findManagedChatGPTApp(paths.root);
|
|
365
|
+
const stale = vendorPaths.chatgpt && !io.chatGPTStageIsCurrent(vendorPaths.chatgpt, paths.root);
|
|
366
|
+
if (!managed || stale) {
|
|
367
|
+
if (!vendorPaths.chatgpt) throw new Error("ChatGPT/Codex vendor app is unavailable");
|
|
368
|
+
managed = io.stageChatGPTApp(vendorPaths.chatgpt, paths.root);
|
|
369
|
+
}
|
|
370
|
+
if (!managed) throw new Error("the managed ChatGPT/Codex app could not be staged");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
configureWindowsApps(config, actionTargets, vendorPaths, catalog, {
|
|
374
|
+
...io,
|
|
375
|
+
homeDir,
|
|
376
|
+
environment,
|
|
377
|
+
});
|
|
378
|
+
for (const target of actionTargets) {
|
|
379
|
+
const { client, env, label } = appSkillTarget(target, paths);
|
|
380
|
+
await io.syncSkills({ client, gatewayUrl: resolveSkillsGateway(config.gatewayUrl), env, label });
|
|
381
|
+
if (target === "chatgpt") secureManagedCodexHome(paths.chatgpt.codexHome);
|
|
382
|
+
}
|
|
383
|
+
await io.syncAgents({
|
|
384
|
+
profiles: actionTargets.map((target) => appAgentProfile(target, paths)),
|
|
385
|
+
gatewayUrl: config.gatewayUrl,
|
|
386
|
+
credential: config.pat,
|
|
387
|
+
tenantId: config.tenantId,
|
|
388
|
+
});
|
|
389
|
+
const verification = {
|
|
390
|
+
claude: !actionTargets.includes("claude") || io.existsSync(path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`)),
|
|
391
|
+
chatgpt: !actionTargets.includes("chatgpt") || io.existsSync(path.join(paths.chatgpt.codexHome, "config.toml")),
|
|
392
|
+
};
|
|
393
|
+
if (!Object.values(verification).every(Boolean)) throw new Error("Windows app profile verification failed");
|
|
394
|
+
return { tenantId: config.tenantId, targets: actionTargets, unsupported, paths, verification };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Reconcile one explicit tenant's macOS app bundles and profiles. */
|
|
398
|
+
export async function reconcileMacTenantApps({
|
|
399
|
+
baseConfig,
|
|
400
|
+
tenant,
|
|
401
|
+
mode = "update",
|
|
402
|
+
targets = ["claude", "chatgpt"],
|
|
403
|
+
skipVendor = false,
|
|
404
|
+
skipVendorTargets = [],
|
|
405
|
+
confirmVendorInstall = async () => true,
|
|
406
|
+
homeDir = os.homedir(),
|
|
407
|
+
} = {}, overrides = {}) {
|
|
408
|
+
const io = {
|
|
409
|
+
status: appStatus,
|
|
410
|
+
bundleCurrent: bundleIsCurrent,
|
|
411
|
+
ensureVendor: ensureVendorApp,
|
|
412
|
+
fetchModels: fetchGatewayModels,
|
|
413
|
+
quitApps: quitBlockingApps,
|
|
414
|
+
installFiles: installManagedAppFiles,
|
|
415
|
+
syncSkills: syncSkillsSafe,
|
|
416
|
+
syncAgents: syncAgentProfilesSafe,
|
|
417
|
+
secureCodexHome: secureManagedCodexHome,
|
|
418
|
+
openLauncher: openManagedLauncher,
|
|
419
|
+
existsSync: fs.existsSync,
|
|
420
|
+
log: (message) => console.log(message),
|
|
421
|
+
...overrides,
|
|
422
|
+
};
|
|
423
|
+
const config = explicitTenantAppConfig(baseConfig, tenant, targets);
|
|
424
|
+
let catalog;
|
|
425
|
+
try {
|
|
426
|
+
catalog = await io.fetchModels(config);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
throw new Error(`tenant model catalog is unavailable (${redactSecretText(error?.message || error)}); no Impel app files were changed`);
|
|
429
|
+
}
|
|
430
|
+
const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, { log: io.log });
|
|
431
|
+
const supported = new Set(actionTargets);
|
|
432
|
+
const unsupported = targets.filter((target) => !supported.has(target));
|
|
433
|
+
const vendorPaths = {};
|
|
434
|
+
const statuses = io.status(actionTargets, homeDir, config.tenantId, config.tenantName);
|
|
435
|
+
for (const status of statuses) vendorPaths[status.target] ||= status.vendorPath;
|
|
436
|
+
const preparedTargets = new Set(skipVendor ? actionTargets : skipVendorTargets);
|
|
437
|
+
{
|
|
438
|
+
for (const target of actionTargets.filter((target) => !preparedTargets.has(target))) {
|
|
439
|
+
if (vendorPaths[target]) continue;
|
|
440
|
+
const confirmed = await confirmVendorInstall(target, { platform: "darwin", mode });
|
|
441
|
+
if (!confirmed) throw new Error(`${target} vendor installation requires confirmation`);
|
|
442
|
+
const result = await io.ensureVendor(target, { homeDir });
|
|
443
|
+
vendorPaths[target] = result.path;
|
|
444
|
+
if (!result.path) throw new Error(`${target} verified vendor app is unavailable; retry the pinned download`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const staleTargets = statuses.filter((status) => !io.bundleCurrent(status)).map((status) => status.target);
|
|
448
|
+
let closedApps = [];
|
|
449
|
+
if (staleTargets.length) {
|
|
450
|
+
const result = await io.quitApps(staleTargets, { tenantId: config.tenantId, tenantName: config.tenantName });
|
|
451
|
+
if (Array.isArray(result)) closedApps = result;
|
|
452
|
+
}
|
|
453
|
+
const reopenTargets = new Set(staleTargets.filter((target) => closedApps.includes(
|
|
454
|
+
managedLauncherName(target, { tenantId: config.tenantId, tenantName: config.tenantName }),
|
|
455
|
+
)));
|
|
456
|
+
const installed = await io.installFiles({
|
|
457
|
+
config,
|
|
458
|
+
targets: actionTargets,
|
|
459
|
+
models: catalog.models,
|
|
460
|
+
homeDir,
|
|
461
|
+
vendorPaths,
|
|
462
|
+
writeBundles: staleTargets,
|
|
463
|
+
});
|
|
464
|
+
const paths = appPaths(homeDir, config.tenantId, { tenantName: config.tenantName });
|
|
465
|
+
for (const item of installed) {
|
|
466
|
+
const { client, env, label } = appSkillTarget(item.target, paths);
|
|
467
|
+
await io.syncSkills({ client, gatewayUrl: resolveSkillsGateway(config.gatewayUrl), env, label });
|
|
468
|
+
if (item.target === "chatgpt") io.secureCodexHome(paths.chatgpt.codexHome);
|
|
469
|
+
}
|
|
470
|
+
await io.syncAgents({
|
|
471
|
+
profiles: installed.map((item) => appAgentProfile(item.target, paths)),
|
|
472
|
+
gatewayUrl: config.gatewayUrl,
|
|
473
|
+
credential: config.pat,
|
|
474
|
+
tenantId: config.tenantId,
|
|
475
|
+
});
|
|
476
|
+
for (const item of installed) {
|
|
477
|
+
if (reopenTargets.has(item.target)) io.openLauncher(item.launcher);
|
|
478
|
+
}
|
|
479
|
+
const verification = Object.fromEntries(actionTargets.map((target) => [
|
|
480
|
+
target,
|
|
481
|
+
io.existsSync(paths[target].launcher) && io.existsSync(
|
|
482
|
+
target === "claude"
|
|
483
|
+
? path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`)
|
|
484
|
+
: path.join(paths.chatgpt.codexHome, "config.toml"),
|
|
485
|
+
),
|
|
486
|
+
]));
|
|
487
|
+
if (!Object.values(verification).every(Boolean)) throw new Error("macOS app bundle/profile verification failed");
|
|
488
|
+
return { tenantId: config.tenantId, targets: actionTargets, unsupported, paths, verification, installed };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function reconcileTenantApps(options = {}, overrides = {}) {
|
|
492
|
+
const platform = options.platform || process.platform;
|
|
493
|
+
if (platform === "win32") return reconcileWindowsTenantApps(options, overrides);
|
|
494
|
+
if (platform === "darwin") return reconcileMacTenantApps(options, overrides);
|
|
495
|
+
return Promise.resolve({ tenantId: options.tenant?.id, targets: [], unsupported: ["claude", "chatgpt"], paths: null, verification: {} });
|
|
496
|
+
}
|
|
497
|
+
|
|
268
498
|
export async function cmdWindowsApps(argv, overrides = {}) {
|
|
269
499
|
const [action = "status", rawTarget, ...rest] = argv;
|
|
270
500
|
const targetToken = rawTarget?.startsWith("--") ? undefined : rawTarget;
|
|
@@ -497,6 +727,8 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
497
727
|
|
|
498
728
|
export async function cmdApps(argv, overrides = {}) {
|
|
499
729
|
const platform = overrides.platform || process.platform;
|
|
730
|
+
const legacyUpdateHandoff = await maybeRunLegacyUpdateHandoff(argv, overrides);
|
|
731
|
+
if (legacyUpdateHandoff !== null) return legacyUpdateHandoff;
|
|
500
732
|
const allTenantUpdate = await maybeUpdateAllInstalledTenants(argv, overrides, platform);
|
|
501
733
|
if (allTenantUpdate !== null) return allTenantUpdate;
|
|
502
734
|
if (platform === "win32") return cmdWindowsApps(argv, overrides);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Hidden fresh-build continuation used by `impel update`.
|
|
2
|
+
|
|
3
|
+
import { loadConfig, saveConfig, redactSecretText } from "../config.js";
|
|
4
|
+
import { runInstallRecovery } from "../installRecovery/engine.js";
|
|
5
|
+
import { printReconciliationSummary, reconcileAllTenants, selectDefaultTenant } from "../provisioning.js";
|
|
6
|
+
import { fetchTenants } from "../tenants.js";
|
|
7
|
+
import { promptText } from "../prompt.js";
|
|
8
|
+
import { restoreNativeProfiles } from "./use.js";
|
|
9
|
+
|
|
10
|
+
function confirmed(answer) {
|
|
11
|
+
return /^(?:y|yes)$/iu.test(String(answer || "").trim());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mergeTenantReport(report, replacement) {
|
|
15
|
+
const byId = new Map(replacement.tenants.map((tenant) => [tenant.tenantId, tenant]));
|
|
16
|
+
report.tenants = report.tenants.map((tenant) => byId.get(tenant.tenantId) || tenant);
|
|
17
|
+
report.passed = report.tenants.every((tenant) => (
|
|
18
|
+
["ready", "unavailable"].includes(tenant.cli)
|
|
19
|
+
&& ["ready", "unavailable"].includes(tenant.apps)
|
|
20
|
+
&& ["ready", "unavailable"].includes(tenant.shell)
|
|
21
|
+
));
|
|
22
|
+
return report;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function cmdConverge(argv = [], overrides = {}) {
|
|
26
|
+
const skipApps = argv.includes("--skip-apps");
|
|
27
|
+
const noRecovery = argv.includes("--no-recovery");
|
|
28
|
+
const io = {
|
|
29
|
+
loadConfig,
|
|
30
|
+
saveConfig,
|
|
31
|
+
fetchTenants,
|
|
32
|
+
reconcile: reconcileAllTenants,
|
|
33
|
+
recoverInstall: runInstallRecovery,
|
|
34
|
+
restoreNativeProfiles,
|
|
35
|
+
promptText,
|
|
36
|
+
isTTY: process.stdin.isTTY,
|
|
37
|
+
platform: process.platform,
|
|
38
|
+
environment: process.env,
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
const config = io.loadConfig();
|
|
42
|
+
if (!config?.pat) {
|
|
43
|
+
console.error("impel update: not authenticated; run `impel setup` first.");
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
let listing;
|
|
48
|
+
try {
|
|
49
|
+
listing = await io.fetchTenants(config);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`impel update: tenant discovery failed (${redactSecretText(error?.message || error)})`);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const selected = selectDefaultTenant(listing, { currentTenantId: config.tenantId });
|
|
56
|
+
config.tenantId = selected.id;
|
|
57
|
+
config.tenantName = selected.name;
|
|
58
|
+
if (listing.productAccess) config.productAccess = listing.productAccess;
|
|
59
|
+
if (listing.scopes) config.scopes = listing.scopes;
|
|
60
|
+
config.tenantsUpdatedAt = new Date().toISOString();
|
|
61
|
+
io.saveConfig(config);
|
|
62
|
+
try {
|
|
63
|
+
io.restoreNativeProfiles({ quiet: true });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn(`Legacy native-profile cleanup was skipped safely (${redactSecretText(error?.message || error)}).`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const vendorAppDecisions = new Map();
|
|
69
|
+
const confirmVendorInstall = async (target, context) => {
|
|
70
|
+
if (vendorAppDecisions.has(target)) return vendorAppDecisions.get(target);
|
|
71
|
+
if (!io.isTTY) {
|
|
72
|
+
vendorAppDecisions.set(target, false);
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const label = target === "claude" ? "Claude" : "ChatGPT/Codex";
|
|
76
|
+
const allowed = confirmed(await io.promptText(
|
|
77
|
+
`Allow the verified ${label} vendor ${context.mode === "update" ? "update" : "installation"}? [y/N] `,
|
|
78
|
+
));
|
|
79
|
+
vendorAppDecisions.set(target, allowed);
|
|
80
|
+
return allowed;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const run = (
|
|
84
|
+
tenants = listing.tenants,
|
|
85
|
+
includeApps = !skipApps,
|
|
86
|
+
confirmInstall = confirmVendorInstall,
|
|
87
|
+
) => io.reconcile({
|
|
88
|
+
config,
|
|
89
|
+
listing: { ...listing, tenants },
|
|
90
|
+
mode: "update",
|
|
91
|
+
includeApps,
|
|
92
|
+
platform: io.platform,
|
|
93
|
+
environment: io.environment,
|
|
94
|
+
confirmVendorInstall: confirmInstall,
|
|
95
|
+
}, overrides.reconcileOverrides || {});
|
|
96
|
+
let report = await run();
|
|
97
|
+
if (!report.passed && !noRecovery) {
|
|
98
|
+
for (const failed of report.tenants.filter((tenant) => tenant.status === "failed")) {
|
|
99
|
+
const tenant = listing.tenants.find((candidate) => candidate.id === failed.tenantId);
|
|
100
|
+
if (!tenant) continue;
|
|
101
|
+
let retryReport = null;
|
|
102
|
+
let retryPassed = false;
|
|
103
|
+
const retryTenant = async (confirmInstall) => {
|
|
104
|
+
retryReport = await run([tenant], !skipApps, confirmInstall);
|
|
105
|
+
retryPassed = retryReport.passed;
|
|
106
|
+
return retryPassed;
|
|
107
|
+
};
|
|
108
|
+
const retrySafeState = async () => {
|
|
109
|
+
const profileReport = await run([tenant], false, async () => false);
|
|
110
|
+
const profile = profileReport.tenants[0];
|
|
111
|
+
const current = report.tenants.find((candidate) => candidate.tenantId === tenant.id);
|
|
112
|
+
const cliReady = ["ready", "unavailable"].includes(profile?.cli);
|
|
113
|
+
const nonCliReady = ["ready", "unavailable"].includes(current?.apps)
|
|
114
|
+
&& ["ready", "unavailable"].includes(current?.shell);
|
|
115
|
+
retryPassed = cliReady && nonCliReady;
|
|
116
|
+
if (retryPassed) {
|
|
117
|
+
const combined = {
|
|
118
|
+
...current,
|
|
119
|
+
cli: profile.cli,
|
|
120
|
+
clients: {
|
|
121
|
+
claude: { ...current.clients.claude, cli: profile.clients.claude.cli },
|
|
122
|
+
codex: { ...current.clients.codex, cli: profile.clients.codex.cli },
|
|
123
|
+
},
|
|
124
|
+
errors: profile.errors,
|
|
125
|
+
status: "ready",
|
|
126
|
+
};
|
|
127
|
+
retryReport = { ...profileReport, tenants: [combined], passed: true };
|
|
128
|
+
}
|
|
129
|
+
return retryPassed;
|
|
130
|
+
};
|
|
131
|
+
await io.recoverInstall({
|
|
132
|
+
failure: {
|
|
133
|
+
scope: "tenant",
|
|
134
|
+
tenantId: tenant.id,
|
|
135
|
+
platform: io.platform,
|
|
136
|
+
architecture: process.arch,
|
|
137
|
+
step: "update.tenant_convergence",
|
|
138
|
+
message: failed.errors.join("; ") || "Tenant update verification failed.",
|
|
139
|
+
diagnostics: { tenant: tenant.id },
|
|
140
|
+
},
|
|
141
|
+
config,
|
|
142
|
+
goals: [{
|
|
143
|
+
id: `tenant-${tenant.id}-ready`,
|
|
144
|
+
description: `Tenant ${tenant.id} is upgraded and locally verified`,
|
|
145
|
+
run: () => retryPassed,
|
|
146
|
+
}],
|
|
147
|
+
explicit: true,
|
|
148
|
+
noRecovery: false,
|
|
149
|
+
actionContext: {
|
|
150
|
+
tenantId: tenant.id,
|
|
151
|
+
repairProfiles: async () => {
|
|
152
|
+
const profileReport = await run([tenant], false, async () => false);
|
|
153
|
+
return ["ready", "unavailable"].includes(profileReport.tenants[0]?.cli);
|
|
154
|
+
},
|
|
155
|
+
retryStep: () => retrySafeState(),
|
|
156
|
+
installVendorApp: (target) => retryTenant(async (product) => (
|
|
157
|
+
target === "all"
|
|
158
|
+
|| product === target
|
|
159
|
+
|| (target === "codex" && product === "chatgpt")
|
|
160
|
+
)),
|
|
161
|
+
},
|
|
162
|
+
}, overrides.recoveryOverrides || {});
|
|
163
|
+
if (retryReport) mergeTenantReport(report, retryReport);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
printReconciliationSummary(report);
|
|
167
|
+
console.log(`CLI tenant: ${selected.id} (change with \`impel tenant use <tenant>\`)`);
|
|
168
|
+
if (!report.passed) {
|
|
169
|
+
console.error("impel update: one or more tenant surfaces remain incomplete.");
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|