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/src/commands/setup.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
// `impel setup` —
|
|
2
|
-
// verify. macOS installs the isolated desktop apps; Windows ensures the two
|
|
3
|
-
// official vendor CLIs exist and prepares isolated CLI + desktop app profiles.
|
|
4
|
-
// Native Claude Code/Codex profiles are never touched.
|
|
1
|
+
// `impel setup` — authenticate once, then converge every accessible tenant.
|
|
5
2
|
|
|
6
3
|
import { parseFlags } from "../args.js";
|
|
7
4
|
import {
|
|
8
|
-
crossAppModelsEnabled,
|
|
9
5
|
loadConfig,
|
|
10
6
|
saveConfig,
|
|
11
7
|
normalizeGatewayUrl,
|
|
@@ -15,104 +11,137 @@ import {
|
|
|
15
11
|
redactSecretText,
|
|
16
12
|
} from "../config.js";
|
|
17
13
|
import { promptSecret, promptText } from "../prompt.js";
|
|
18
|
-
import { ensureImpelClaudeProfile, ensureImpelCodexProfile } from "../cliProfiles.js";
|
|
19
14
|
import { fetchTenants, normalizeTenantId, tenantCredential } from "../tenants.js";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
import {
|
|
16
|
+
printReconciliationSummary,
|
|
17
|
+
reconcileAllTenants,
|
|
18
|
+
selectDefaultTenant,
|
|
19
|
+
} from "../provisioning.js";
|
|
20
|
+
import { prepareWindowsClis } from "../windowsSetup.js";
|
|
23
21
|
import { runInstallRecovery } from "../installRecovery/engine.js";
|
|
22
|
+
import { restoreNativeProfiles } from "./use.js";
|
|
24
23
|
|
|
25
|
-
const HELP = `impel setup -
|
|
24
|
+
const HELP = `impel setup - prepare every accessible Impel tenant
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
tenant-isolated CLI profiles, and installs isolated desktop app profiles.
|
|
32
|
-
Native client profiles stay untouched and the vendor executables stay signed
|
|
33
|
-
and unmodified.
|
|
26
|
+
Stores your Personal Access Token, discovers every tenant you can access,
|
|
27
|
+
prepares each tenant's isolated Claude and Codex CLI profiles, and installs
|
|
28
|
+
each supported desktop app on macOS and Windows. Personal Claude/Codex profiles
|
|
29
|
+
are never used for Impel sessions.
|
|
34
30
|
|
|
35
31
|
Usage:
|
|
36
|
-
impel setup Interactive
|
|
37
|
-
impel setup --pat <pat> Non-interactive
|
|
38
|
-
impel setup --tenant <org>
|
|
39
|
-
impel setup --skip-apps Skip
|
|
40
|
-
impel setup --skip-clis
|
|
41
|
-
impel setup --
|
|
42
|
-
impel setup --no-recovery Disable local and hosted recovery for this run
|
|
32
|
+
impel setup Interactive setup
|
|
33
|
+
impel setup --pat <pat> Non-interactive authentication
|
|
34
|
+
impel setup --tenant <org> Choose the default tenant for CLI launches
|
|
35
|
+
impel setup --skip-apps Skip desktop apps
|
|
36
|
+
impel setup --skip-clis Windows: skip missing vendor CLI installation
|
|
37
|
+
impel setup --no-recovery Disable automatic install recovery
|
|
43
38
|
`;
|
|
44
39
|
|
|
45
|
-
/**
|
|
46
|
-
* Pick the tenant for the wizard. Pure so it's testable: returns the tenant
|
|
47
|
-
* for an explicit request (or throws if unavailable), auto-selects a sole
|
|
48
|
-
* tenant, and otherwise resolves an interactive answer (number, id, slug, or
|
|
49
|
-
* empty = default).
|
|
50
|
-
*/
|
|
40
|
+
/** Kept as a pure compatibility helper for callers/tests. */
|
|
51
41
|
export function resolveTenantChoice(listing, { requested = null, answer = null, currentTenantId = null } = {}) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (requested) {
|
|
58
|
-
const tenant = byToken(requested);
|
|
59
|
-
if (!tenant) throw new Error(`tenant "${requested}" is not available to this user`);
|
|
42
|
+
if (requested) return selectDefaultTenant(listing, { requested, currentTenantId });
|
|
43
|
+
if (answer && /^\d+$/u.test(answer)) {
|
|
44
|
+
const tenant = listing.tenants[Number(answer) - 1];
|
|
45
|
+
if (!tenant) throw new Error(`tenant "${answer}" is not available to this user`);
|
|
60
46
|
return tenant;
|
|
61
47
|
}
|
|
62
|
-
if (listing
|
|
63
|
-
|
|
64
|
-
const fallbackId = listing.tenants.some((tenant) => tenant.id === currentTenantId)
|
|
65
|
-
? currentTenantId
|
|
66
|
-
: listing.defaultTenantId;
|
|
67
|
-
if (answer === null || answer === "") {
|
|
68
|
-
return listing.tenants.find((tenant) => tenant.id === fallbackId) || listing.tenants[0];
|
|
69
|
-
}
|
|
70
|
-
const index = /^\d+$/.test(answer) ? Number(answer) : NaN;
|
|
71
|
-
if (Number.isInteger(index) && index >= 1 && index <= listing.tenants.length) {
|
|
72
|
-
return listing.tenants[index - 1];
|
|
73
|
-
}
|
|
74
|
-
const tenant = byToken(answer);
|
|
75
|
-
if (!tenant) throw new Error(`tenant "${answer}" is not available to this user`);
|
|
76
|
-
return tenant;
|
|
48
|
+
if (answer) return selectDefaultTenant(listing, { requested: answer, currentTenantId });
|
|
49
|
+
return selectDefaultTenant(listing, { currentTenantId });
|
|
77
50
|
}
|
|
78
51
|
|
|
79
|
-
/** Best-effort gateway probe; any HTTP response means the gateway is routing. */
|
|
80
52
|
async function probeGateway(gatewayUrl, pat, tenantId, fetchImpl = fetch) {
|
|
81
|
-
const url = `${gatewayUrl}/anthropic/v1/messages`;
|
|
82
53
|
const controller = new AbortController();
|
|
83
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
54
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
84
55
|
try {
|
|
85
|
-
const
|
|
56
|
+
const response = await fetchImpl(`${gatewayUrl}/anthropic/v1/messages`, {
|
|
86
57
|
method: "POST",
|
|
87
58
|
headers: {
|
|
88
59
|
"content-type": "application/json",
|
|
89
|
-
|
|
60
|
+
authorization: `Bearer ${tenantCredential(pat, tenantId)}`,
|
|
90
61
|
},
|
|
91
62
|
body: "{}",
|
|
92
63
|
signal: controller.signal,
|
|
93
64
|
});
|
|
94
|
-
return { reachable: true, status:
|
|
95
|
-
} catch (
|
|
65
|
+
return { reachable: true, status: response.status, rejected: [401, 403].includes(response.status) };
|
|
66
|
+
} catch (error) {
|
|
96
67
|
return {
|
|
97
68
|
reachable: false,
|
|
98
|
-
error:
|
|
69
|
+
error: error?.name === "AbortError" ? "timed out after 5s" : redactSecretText(error?.message || error),
|
|
99
70
|
};
|
|
100
71
|
} finally {
|
|
101
72
|
clearTimeout(timeout);
|
|
102
73
|
}
|
|
103
74
|
}
|
|
104
75
|
|
|
76
|
+
function markProbeFailures(report, probes) {
|
|
77
|
+
probes.forEach((probe, index) => {
|
|
78
|
+
if (probe.reachable && !probe.rejected) return;
|
|
79
|
+
const tenant = report.tenants[index];
|
|
80
|
+
tenant.cli = "failed";
|
|
81
|
+
tenant.status = "failed";
|
|
82
|
+
for (const client of Object.values(tenant.clients)) {
|
|
83
|
+
if (client.cli === "ready") client.cli = "failed";
|
|
84
|
+
}
|
|
85
|
+
tenant.errors.push(probe.rejected
|
|
86
|
+
? `credential rejected (HTTP ${probe.status})`
|
|
87
|
+
: `gateway unreachable (${probe.error || "unknown error"})`);
|
|
88
|
+
});
|
|
89
|
+
report.passed = report.tenants.every((tenant) => (
|
|
90
|
+
["ready", "unavailable"].includes(tenant.cli)
|
|
91
|
+
&& ["ready", "unavailable"].includes(tenant.apps)
|
|
92
|
+
&& ["ready", "unavailable"].includes(tenant.shell)
|
|
93
|
+
));
|
|
94
|
+
return report;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function recomputeReport(report) {
|
|
98
|
+
report.passed = report.tenants.every((tenant) => (
|
|
99
|
+
["ready", "unavailable"].includes(tenant.cli)
|
|
100
|
+
&& ["ready", "unavailable"].includes(tenant.apps)
|
|
101
|
+
&& ["ready", "unavailable"].includes(tenant.shell)
|
|
102
|
+
));
|
|
103
|
+
return report;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function mergeTenantReport(report, replacement) {
|
|
107
|
+
const byId = new Map(replacement.tenants.map((tenant) => [tenant.tenantId, tenant]));
|
|
108
|
+
report.tenants = report.tenants.map((tenant) => byId.get(tenant.tenantId) || tenant);
|
|
109
|
+
return recomputeReport(report);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function describeWindowsCliFailure(prepared) {
|
|
113
|
+
const labels = { claude: "Claude Code", codex: "Codex" };
|
|
114
|
+
const details = [];
|
|
115
|
+
for (const tool of prepared.missingAfter || []) {
|
|
116
|
+
const installation = prepared.installations?.[tool];
|
|
117
|
+
const failure = installation?.failure;
|
|
118
|
+
const reason = failure?.message || failure?.code
|
|
119
|
+
|| (Number.isInteger(failure?.status) ? `exit ${failure.status}` : "not discoverable after installation");
|
|
120
|
+
details.push(`${labels[tool] || tool}'s native installer failed: ${redactSecretText(reason)}`);
|
|
121
|
+
if (installation?.command) details.push(`manual ${tool} installer: ${redactSecretText(installation.command)}`);
|
|
122
|
+
}
|
|
123
|
+
return details.join("; ") || `missing vendor CLIs: ${(prepared.missingAfter || []).join(", ")}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function confirmed(answer) {
|
|
127
|
+
return /^(?:y|yes)$/iu.test(String(answer || "").trim());
|
|
128
|
+
}
|
|
129
|
+
|
|
105
130
|
export async function cmdSetup(argv, overrides = {}) {
|
|
106
131
|
const io = {
|
|
132
|
+
loadConfig,
|
|
133
|
+
saveConfig,
|
|
107
134
|
promptSecret,
|
|
108
135
|
promptText,
|
|
109
136
|
fetchTenants,
|
|
110
|
-
installApps: cmdApps,
|
|
111
137
|
prepareWindowsClis,
|
|
138
|
+
reconcile: reconcileAllTenants,
|
|
112
139
|
probe: probeGateway,
|
|
113
140
|
recoverInstall: runInstallRecovery,
|
|
141
|
+
restoreNativeProfiles,
|
|
114
142
|
isTTY: process.stdin.isTTY,
|
|
115
143
|
platform: process.platform,
|
|
144
|
+
environment: process.env,
|
|
116
145
|
...overrides,
|
|
117
146
|
};
|
|
118
147
|
const { flags, positionals } = parseFlags(argv, {
|
|
@@ -122,7 +151,7 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
122
151
|
app: { type: "string" },
|
|
123
152
|
"skip-apps": { type: "boolean" },
|
|
124
153
|
"skip-clis": { type: "boolean" },
|
|
125
|
-
repair: { type: "boolean" },
|
|
154
|
+
repair: { type: "boolean" }, // deprecated compatibility no-op
|
|
126
155
|
"no-recovery": { type: "boolean" },
|
|
127
156
|
help: { type: "boolean" },
|
|
128
157
|
});
|
|
@@ -130,422 +159,277 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
130
159
|
console.log(HELP);
|
|
131
160
|
return;
|
|
132
161
|
}
|
|
133
|
-
if (positionals.length
|
|
134
|
-
console.error(
|
|
135
|
-
`impel setup: no longer takes a target ("${positionals[0]}"). Run bare \`impel setup\` — it installs the vendored Impel apps. To flip a native tool, use \`impel use gateway [claude|codex]\`.`
|
|
136
|
-
);
|
|
162
|
+
if (positionals.length) {
|
|
163
|
+
console.error(`impel setup: unexpected argument "${redactSecretText(positionals[0])}"; run bare \`impel setup\`.`);
|
|
137
164
|
process.exitCode = 1;
|
|
138
165
|
return;
|
|
139
166
|
}
|
|
140
167
|
|
|
141
|
-
const existing = loadConfig();
|
|
168
|
+
const existing = io.loadConfig();
|
|
142
169
|
const gatewayUrl = normalizeGatewayUrl(flags.gateway || existing?.gatewayUrl || resolveDefaultGateway());
|
|
143
170
|
const appUrl = normalizeGatewayUrl(flags.app || existing?.appUrl || resolveDefaultAppUrl());
|
|
144
|
-
|
|
145
|
-
console.log("Impel gateway setup");
|
|
146
|
-
console.log(` gateway: ${gatewayUrl}`);
|
|
147
|
-
console.log(` app: ${appUrl}`);
|
|
148
|
-
console.log("");
|
|
149
|
-
|
|
150
|
-
// ── Step 1: token ────────────────────────────────────────────────────────
|
|
151
171
|
let pat = flags.pat;
|
|
152
172
|
if (!pat && existing?.pat) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
} else {
|
|
159
|
-
pat = existing.pat;
|
|
160
|
-
}
|
|
161
|
-
} else if (!pat) {
|
|
162
|
-
if (!io.isTTY) {
|
|
163
|
-
console.error("impel setup: no token stored and stdin is not a TTY; pass --pat <impel_pat_...>.");
|
|
164
|
-
process.exitCode = 1;
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
console.log(`1/4 Token — create one at ${appUrl}/settings/gateway (step 3 on that page mints it).`);
|
|
168
|
-
pat = await io.promptSecret(" Paste your Personal Access Token (impel_pat_...): ");
|
|
173
|
+
pat = io.isTTY
|
|
174
|
+
? (await io.promptSecret(`Token (${maskSecret(existing.pat)} stored; Enter keeps it): `)) || existing.pat
|
|
175
|
+
: existing.pat;
|
|
176
|
+
} else if (!pat && io.isTTY) {
|
|
177
|
+
pat = await io.promptSecret("Impel Personal Access Token (impel_pat_...): ");
|
|
169
178
|
}
|
|
170
179
|
if (!pat) {
|
|
171
|
-
console.error("impel setup: no token provided
|
|
180
|
+
console.error("impel setup: no token provided; pass --pat or run interactively.");
|
|
172
181
|
process.exitCode = 1;
|
|
173
182
|
return;
|
|
174
183
|
}
|
|
175
|
-
if (!pat.startsWith("impel_pat_")) {
|
|
176
|
-
console.warn(
|
|
177
|
-
`impel: warning: that token doesn't start with "impel_pat_" — double check you copied the right value.`
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
184
|
|
|
181
185
|
const config = { ...(existing || {}), pat, gatewayUrl, appUrl, updatedAt: new Date().toISOString() };
|
|
182
|
-
saveConfig(config);
|
|
183
|
-
console.log(
|
|
184
|
-
console.log("");
|
|
185
|
-
|
|
186
|
-
const setupFailures = [];
|
|
187
|
-
const recordSetupFailure = (failure) => {
|
|
188
|
-
setupFailures.push({
|
|
189
|
-
platform: io.platform,
|
|
190
|
-
architecture: process.arch,
|
|
191
|
-
...failure,
|
|
192
|
-
});
|
|
193
|
-
};
|
|
186
|
+
io.saveConfig(config);
|
|
187
|
+
console.log("✓ Stored the Impel credential privately.");
|
|
194
188
|
|
|
195
|
-
// ── Step 2: organization ─────────────────────────────────────────────────
|
|
196
|
-
// A successful tenant fetch also proves the token: the app verifies it
|
|
197
|
-
// against impel-identity before answering.
|
|
198
189
|
let listing;
|
|
199
190
|
try {
|
|
200
191
|
listing = await io.fetchTenants(config);
|
|
201
192
|
} catch (error) {
|
|
202
193
|
console.error(`impel setup: could not verify the token (${redactSecretText(error?.message || error)})`);
|
|
203
|
-
console.error(" Check the token and your network, then re-run `impel setup`.");
|
|
204
194
|
process.exitCode = 1;
|
|
205
195
|
return;
|
|
206
196
|
}
|
|
197
|
+
const selected = selectDefaultTenant(listing, {
|
|
198
|
+
requested: flags.tenant || null,
|
|
199
|
+
currentTenantId: existing?.tenantId || null,
|
|
200
|
+
});
|
|
201
|
+
config.tenantId = selected.id;
|
|
202
|
+
config.tenantName = selected.name;
|
|
203
|
+
if (listing.productAccess) config.productAccess = listing.productAccess;
|
|
204
|
+
if (listing.scopes) config.scopes = listing.scopes;
|
|
205
|
+
config.tenantsUpdatedAt = new Date().toISOString();
|
|
206
|
+
io.saveConfig(config);
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
const orderedTenants = [...listing.tenants].sort((left, right) => left.id.localeCompare(right.id));
|
|
209
|
+
console.log(`Accessible tenants (${orderedTenants.length}):`);
|
|
210
|
+
for (const tenant of orderedTenants) {
|
|
211
|
+
console.log(` ${tenant.id}${tenant.id === selected.id ? " (CLI default)" : ""} — ${tenant.name}`);
|
|
212
|
+
}
|
|
209
213
|
try {
|
|
210
|
-
|
|
211
|
-
tenant = resolveTenantChoice(listing, {
|
|
212
|
-
requested: flags.tenant || null,
|
|
213
|
-
currentTenantId: existing?.tenantId || null,
|
|
214
|
-
});
|
|
215
|
-
} else {
|
|
216
|
-
console.log("2/4 Organization");
|
|
217
|
-
listing.tenants.forEach((candidate, index) => {
|
|
218
|
-
const markers = [
|
|
219
|
-
candidate.id === existing?.tenantId ? "current" : null,
|
|
220
|
-
candidate.id === listing.defaultTenantId ? "default" : null,
|
|
221
|
-
].filter(Boolean);
|
|
222
|
-
console.log(` [${index + 1}] ${candidate.id}${markers.length ? ` (${markers.join(", ")})` : ""}\t${candidate.name}`);
|
|
223
|
-
});
|
|
224
|
-
const fallback = resolveTenantChoice(listing, { currentTenantId: existing?.tenantId || null });
|
|
225
|
-
const answer = await io.promptText(` Select [1-${listing.tenants.length}] (Enter for ${fallback.id}): `);
|
|
226
|
-
tenant = resolveTenantChoice(listing, { answer, currentTenantId: existing?.tenantId || null });
|
|
227
|
-
}
|
|
214
|
+
io.restoreNativeProfiles({ quiet: true });
|
|
228
215
|
} catch (error) {
|
|
229
|
-
console.
|
|
230
|
-
process.exitCode = 1;
|
|
231
|
-
return;
|
|
216
|
+
console.warn(`Legacy native-profile cleanup was skipped safely (${redactSecretText(error?.message || error)}).`);
|
|
232
217
|
}
|
|
233
218
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// ── Step 3: apps ─────────────────────────────────────────────────────────
|
|
246
|
-
// Order inside `impel app install`: close running Claude/ChatGPT apps (a
|
|
247
|
-
// running app makes the install fail), bring the vendor apps to the verified
|
|
248
|
-
// version, then build the vendored Impel bundles from that fresh copy.
|
|
249
|
-
// Native Claude Code and Codex profiles are never touched.
|
|
250
|
-
let platformSetupFailed = false;
|
|
251
|
-
if (io.platform === "win32") {
|
|
252
|
-
console.log(
|
|
253
|
-
flags["skip-clis"]
|
|
254
|
-
? "3/4 Windows CLIs: preparing isolated profiles (vendor CLI installation skipped)…"
|
|
255
|
-
: "3/4 Windows CLIs: ensuring Claude Code and Codex are installed, then preparing isolated profiles…"
|
|
219
|
+
const vendorAppDecisions = new Map();
|
|
220
|
+
const confirmVendorInstall = async (target, context) => {
|
|
221
|
+
if (vendorAppDecisions.has(target)) return vendorAppDecisions.get(target);
|
|
222
|
+
if (!io.isTTY) {
|
|
223
|
+
vendorAppDecisions.set(target, false);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
const label = target === "claude" ? "Claude" : "ChatGPT/Codex";
|
|
227
|
+
const answer = await io.promptText(
|
|
228
|
+
`Allow the verified ${label} vendor ${context.mode === "update" ? "update" : "installation"}? [y/N] `,
|
|
256
229
|
);
|
|
230
|
+
const allowed = confirmed(answer);
|
|
231
|
+
vendorAppDecisions.set(target, allowed);
|
|
232
|
+
return allowed;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
let sharedFailure = null;
|
|
236
|
+
const prepareSharedClis = async ({ confirmedTools = null, inspectOnly = false } = {}) => {
|
|
237
|
+
if (io.platform !== "win32") return { binaries: {}, missingAfter: [] };
|
|
257
238
|
try {
|
|
258
|
-
const
|
|
239
|
+
const inspected = await io.prepareWindowsClis({
|
|
259
240
|
gatewayUrl,
|
|
260
|
-
tenantId:
|
|
261
|
-
skipInstall:
|
|
241
|
+
tenantId: selected.id,
|
|
242
|
+
skipInstall: true,
|
|
262
243
|
});
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (installation?.succeeded === false || (!installation && result.installSucceeded === false)) {
|
|
276
|
-
const detail = failure?.message
|
|
277
|
-
? `: ${redactSecretText(failure.message)}`
|
|
278
|
-
: Number.isInteger(failure?.status)
|
|
279
|
-
? ` (exit code ${failure.status})`
|
|
280
|
-
: failure?.signal
|
|
281
|
-
? ` (signal ${failure.signal})`
|
|
282
|
-
: "";
|
|
283
|
-
console.error(` ${label}'s native installer failed${detail}.`);
|
|
284
|
-
recordSetupFailure({
|
|
285
|
-
step: `install.vendor_cli.${tool}`,
|
|
286
|
-
command: installation?.command || result.installCommands?.[tool] || fallbackCommands[tool],
|
|
287
|
-
exitCode: failure?.status,
|
|
288
|
-
signal: failure?.signal,
|
|
289
|
-
errorCode: failure?.code,
|
|
290
|
-
message: failure?.message
|
|
291
|
-
? `${label}'s native installer failed: ${failure.message}`
|
|
292
|
-
: `${label}'s native installer did not reach a verified state.`,
|
|
293
|
-
});
|
|
294
|
-
} else {
|
|
295
|
-
console.error(` ${label}'s native installer completed, but Impel could not verify the command.`);
|
|
296
|
-
recordSetupFailure({
|
|
297
|
-
step: `verify.vendor_cli.${tool}`,
|
|
298
|
-
command: installation?.command || result.installCommands?.[tool] || fallbackCommands[tool],
|
|
299
|
-
message: `${label}'s installer completed but the command was not discoverable or did not pass its version check.`,
|
|
300
|
-
});
|
|
244
|
+
let prepared = inspected;
|
|
245
|
+
if (inspected.missingAfter.length && !inspectOnly && !flags["skip-clis"]) {
|
|
246
|
+
let installTools = confirmedTools;
|
|
247
|
+
if (installTools === null) {
|
|
248
|
+
installTools = [];
|
|
249
|
+
if (io.isTTY) {
|
|
250
|
+
for (const tool of inspected.missingAfter) {
|
|
251
|
+
const label = tool === "claude" ? "Claude Code" : "Codex";
|
|
252
|
+
if (confirmed(await io.promptText(`Install the official ${label} CLI for this user? [y/N] `))) {
|
|
253
|
+
installTools.push(tool);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
301
256
|
}
|
|
302
|
-
console.error(" Retry its official installer in the same PowerShell window:");
|
|
303
|
-
console.error(` ${installation?.command || result.installCommands?.[tool] || fallbackCommands[tool]}`);
|
|
304
257
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
} catch (error) {
|
|
312
|
-
platformSetupFailed = true;
|
|
313
|
-
console.error(` ✗ Windows CLI setup failed (${redactSecretText(error?.message || error)})`);
|
|
314
|
-
console.error(" Fix the issue, then re-run `impel setup`.");
|
|
315
|
-
recordSetupFailure({
|
|
316
|
-
step: "setup.windows_clis",
|
|
317
|
-
errorCode: error?.code,
|
|
318
|
-
message: `Windows CLI setup failed: ${error?.message || error}`,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
if (flags["skip-apps"]) {
|
|
322
|
-
console.log(" Windows desktop apps: skipped (--skip-apps).");
|
|
323
|
-
} else {
|
|
324
|
-
console.log(" Windows desktop apps: installing signed vendor apps and isolated Impel profiles…");
|
|
325
|
-
try {
|
|
326
|
-
const installed = await io.installApps(["install", "all"]);
|
|
327
|
-
if (installed === false) {
|
|
328
|
-
platformSetupFailed = true;
|
|
329
|
-
console.error(" ✗ Windows desktop app setup failed; fix the issue above, then re-run `impel app install all`.");
|
|
330
|
-
recordSetupFailure({
|
|
331
|
-
step: "install.vendor_app.all",
|
|
332
|
-
message: "Windows desktop app setup returned an unsuccessful result.",
|
|
258
|
+
if (installTools.length) {
|
|
259
|
+
prepared = await io.prepareWindowsClis({
|
|
260
|
+
gatewayUrl,
|
|
261
|
+
tenantId: selected.id,
|
|
262
|
+
skipInstall: false,
|
|
263
|
+
installTools,
|
|
333
264
|
});
|
|
334
|
-
} else {
|
|
335
|
-
console.log(" ✓ available Impel desktop app profiles are ready");
|
|
336
265
|
}
|
|
337
|
-
} catch (error) {
|
|
338
|
-
platformSetupFailed = true;
|
|
339
|
-
console.error(` ✗ Windows desktop app setup failed (${redactSecretText(error?.message || error)})`);
|
|
340
|
-
console.error(" Fix the issue, then re-run `impel app install all`.");
|
|
341
|
-
recordSetupFailure({
|
|
342
|
-
step: "install.vendor_app.all",
|
|
343
|
-
errorCode: error?.code,
|
|
344
|
-
message: `Windows desktop app setup failed: ${error?.message || error}`,
|
|
345
|
-
});
|
|
346
266
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
);
|
|
354
|
-
} else {
|
|
355
|
-
console.log(
|
|
356
|
-
"3/4 Installing the Impel desktop apps (running Claude/ChatGPT apps are closed, verified vendor builds installed first)…"
|
|
357
|
-
);
|
|
358
|
-
try {
|
|
359
|
-
await io.installApps(["install", "all"]);
|
|
360
|
-
console.log(" ✓ available Impel desktop apps installed");
|
|
267
|
+
if (prepared.missingAfter.length) {
|
|
268
|
+
sharedFailure = describeWindowsCliFailure(prepared);
|
|
269
|
+
return prepared;
|
|
270
|
+
}
|
|
271
|
+
sharedFailure = null;
|
|
272
|
+
return prepared;
|
|
361
273
|
} catch (error) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
` ✗ app installation failed (${redactSecretText(error?.message || error)})`
|
|
365
|
-
);
|
|
366
|
-
console.error(" Fix the issue, then re-run `impel app install`.");
|
|
367
|
-
recordSetupFailure({
|
|
368
|
-
step: "install.vendor_app.all",
|
|
369
|
-
errorCode: error?.code,
|
|
370
|
-
message: `Desktop app installation failed: ${error?.message || error}`,
|
|
371
|
-
});
|
|
274
|
+
sharedFailure = redactSecretText(error?.message || error);
|
|
275
|
+
return { binaries: {}, missingAfter: ["claude", "codex"] };
|
|
372
276
|
}
|
|
373
|
-
}
|
|
374
|
-
|
|
277
|
+
};
|
|
278
|
+
await prepareSharedClis();
|
|
375
279
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
280
|
+
const runConvergence = (
|
|
281
|
+
includeApps = !flags["skip-apps"],
|
|
282
|
+
tenants = orderedTenants,
|
|
283
|
+
confirmInstall = confirmVendorInstall,
|
|
284
|
+
) => io.reconcile({
|
|
285
|
+
config,
|
|
286
|
+
listing: { ...listing, tenants },
|
|
287
|
+
mode: "setup",
|
|
288
|
+
includeApps,
|
|
289
|
+
platform: io.platform,
|
|
290
|
+
environment: io.environment,
|
|
291
|
+
confirmVendorInstall: confirmInstall,
|
|
292
|
+
}, {
|
|
293
|
+
...(overrides.installApps ? { installApps: overrides.installApps } : {}),
|
|
294
|
+
...(overrides.registerShellEntries ? { registerShellEntries: overrides.registerShellEntries } : {}),
|
|
295
|
+
...(overrides.ensureClaude ? { ensureClaude: overrides.ensureClaude } : {}),
|
|
296
|
+
...(overrides.ensureCodex ? { ensureCodex: overrides.ensureCodex } : {}),
|
|
297
|
+
...(overrides.syncSkills ? { syncSkills: overrides.syncSkills } : {}),
|
|
298
|
+
...(overrides.syncAgents ? { syncAgents: overrides.syncAgents } : {}),
|
|
299
|
+
...(overrides.knownTenantIds ? { knownTenantIds: overrides.knownTenantIds } : {}),
|
|
300
|
+
...(overrides.findBinary ? { findBinary: overrides.findBinary } : {}),
|
|
301
|
+
});
|
|
399
302
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
`failure${index + 1}`,
|
|
412
|
-
`${failure.step}: ${failure.message}`,
|
|
413
|
-
])
|
|
414
|
-
),
|
|
415
|
-
};
|
|
303
|
+
const verifyConvergence = async (
|
|
304
|
+
includeApps = !flags["skip-apps"],
|
|
305
|
+
tenants = orderedTenants,
|
|
306
|
+
confirmInstall = confirmVendorInstall,
|
|
307
|
+
) => {
|
|
308
|
+
const nextReport = await runConvergence(includeApps, tenants, confirmInstall);
|
|
309
|
+
const probes = await Promise.all(tenants.map((tenant) => io.probe(gatewayUrl, pat, tenant.id)));
|
|
310
|
+
return markProbeFailures(nextReport, probes);
|
|
311
|
+
};
|
|
312
|
+
let report = await verifyConvergence();
|
|
313
|
+
if (sharedFailure) report.passed = false;
|
|
416
314
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
goals
|
|
429
|
-
id: "
|
|
430
|
-
description: "
|
|
431
|
-
run:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
315
|
+
if (sharedFailure && !flags["no-recovery"]) {
|
|
316
|
+
let sharedReady = false;
|
|
317
|
+
const recovery = await io.recoverInstall({
|
|
318
|
+
failure: {
|
|
319
|
+
scope: "shared",
|
|
320
|
+
platform: io.platform,
|
|
321
|
+
architecture: process.arch,
|
|
322
|
+
step: "setup.shared_vendor_clis",
|
|
323
|
+
message: sharedFailure,
|
|
324
|
+
},
|
|
325
|
+
config,
|
|
326
|
+
goals: [{
|
|
327
|
+
id: "shared-vendor-clis-ready",
|
|
328
|
+
description: "Shared vendor CLI prerequisites are installed and discoverable",
|
|
329
|
+
run: () => sharedReady,
|
|
330
|
+
}],
|
|
331
|
+
explicit: Boolean(flags.repair),
|
|
332
|
+
noRecovery: false,
|
|
333
|
+
actionContext: {
|
|
334
|
+
tenantId: selected.id,
|
|
335
|
+
installVendorClis: async (tool) => {
|
|
336
|
+
const prepared = await prepareSharedClis({ confirmedTools: [tool] });
|
|
337
|
+
sharedReady = !sharedFailure;
|
|
338
|
+
return prepared;
|
|
440
339
|
},
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
id: "desktop-apps",
|
|
446
|
-
description: "The Impel desktop app profiles installed cleanly",
|
|
447
|
-
run: () => appInstallState.ok === true,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
if (setupFailures.some((failure) => failure.step === "verify.gateway")) {
|
|
451
|
-
goals.push({
|
|
452
|
-
id: "gateway",
|
|
453
|
-
description: "The gateway accepts the stored credential",
|
|
454
|
-
run: async () => {
|
|
455
|
-
const checked = await io.probe(gatewayUrl, pat, tenant.id);
|
|
456
|
-
return checked.reachable && !checked.rejected
|
|
457
|
-
? true
|
|
458
|
-
: { ok: false, detail: checked.error || `HTTP ${checked.status}` };
|
|
340
|
+
retryStep: async () => {
|
|
341
|
+
await prepareSharedClis({ inspectOnly: true });
|
|
342
|
+
sharedReady = !sharedFailure;
|
|
343
|
+
return sharedReady;
|
|
459
344
|
},
|
|
460
|
-
}
|
|
461
|
-
}
|
|
345
|
+
},
|
|
346
|
+
}, overrides.recoveryOverrides || {});
|
|
347
|
+
if (recovery.fixed) await prepareSharedClis({ inspectOnly: true });
|
|
348
|
+
}
|
|
462
349
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
ensureImpelClaudeProfile(gatewayUrl, tenant.id, {
|
|
491
|
-
crossAppModels: crossAppModelsEnabled(config),
|
|
492
|
-
});
|
|
493
|
-
ensureImpelCodexProfile(gatewayUrl, tenant.id);
|
|
494
|
-
// Also refresh the app token helper: a stale baked node/CLI
|
|
495
|
-
// path there breaks every managed app's auth.
|
|
496
|
-
rewriteTenantTokenHelper(tenant.id);
|
|
497
|
-
return true;
|
|
498
|
-
},
|
|
499
|
-
installVendorApp,
|
|
500
|
-
retryStep: async (step) => {
|
|
501
|
-
if (/vendor_cli|windows_clis/iu.test(step || "")) {
|
|
502
|
-
const result = await io.prepareWindowsClis({
|
|
503
|
-
gatewayUrl,
|
|
504
|
-
tenantId: tenant.id,
|
|
505
|
-
skipInstall: false,
|
|
506
|
-
});
|
|
507
|
-
return result.missingAfter.length === 0;
|
|
508
|
-
}
|
|
509
|
-
if (/vendor_app/iu.test(step || "")) {
|
|
510
|
-
return (await installVendorApp("all")) !== false;
|
|
511
|
-
}
|
|
512
|
-
if (/gateway/iu.test(step || "")) {
|
|
513
|
-
const result = await io.probe(gatewayUrl, pat, tenant.id);
|
|
514
|
-
return Boolean(result.reachable && !result.rejected);
|
|
515
|
-
}
|
|
516
|
-
return false;
|
|
350
|
+
if (!flags["no-recovery"]) {
|
|
351
|
+
for (const failed of report.tenants.filter((tenant) => tenant.status === "failed")) {
|
|
352
|
+
const tenant = orderedTenants.find((candidate) => candidate.id === failed.tenantId);
|
|
353
|
+
if (!tenant) continue;
|
|
354
|
+
let retryReport = null;
|
|
355
|
+
let retryPassed = false;
|
|
356
|
+
const retryTenant = async (confirmInstall) => {
|
|
357
|
+
retryReport = await verifyConvergence(!flags["skip-apps"], [tenant], confirmInstall);
|
|
358
|
+
retryPassed = retryReport.passed;
|
|
359
|
+
return retryPassed;
|
|
360
|
+
};
|
|
361
|
+
const retrySafeState = async () => {
|
|
362
|
+
const profileReport = await verifyConvergence(false, [tenant], async () => false);
|
|
363
|
+
const profile = profileReport.tenants[0];
|
|
364
|
+
const current = report.tenants.find((candidate) => candidate.tenantId === tenant.id);
|
|
365
|
+
const cliReady = ["ready", "unavailable"].includes(profile?.cli);
|
|
366
|
+
const nonCliReady = ["ready", "unavailable"].includes(current?.apps)
|
|
367
|
+
&& ["ready", "unavailable"].includes(current?.shell);
|
|
368
|
+
retryPassed = cliReady && nonCliReady;
|
|
369
|
+
if (retryPassed) {
|
|
370
|
+
const combined = {
|
|
371
|
+
...current,
|
|
372
|
+
cli: profile.cli,
|
|
373
|
+
clients: {
|
|
374
|
+
claude: { ...current.clients.claude, cli: profile.clients.claude.cli },
|
|
375
|
+
codex: { ...current.clients.codex, cli: profile.clients.codex.cli },
|
|
517
376
|
},
|
|
377
|
+
errors: profile.errors,
|
|
378
|
+
status: "ready",
|
|
379
|
+
};
|
|
380
|
+
retryReport = { ...profileReport, tenants: [combined], passed: true };
|
|
381
|
+
}
|
|
382
|
+
return retryPassed;
|
|
383
|
+
};
|
|
384
|
+
await io.recoverInstall({
|
|
385
|
+
failure: {
|
|
386
|
+
scope: "tenant",
|
|
387
|
+
tenantId: tenant.id,
|
|
388
|
+
platform: io.platform,
|
|
389
|
+
architecture: process.arch,
|
|
390
|
+
step: "setup.tenant_convergence",
|
|
391
|
+
message: failed.errors.join("; ") || "Tenant readiness verification failed.",
|
|
392
|
+
diagnostics: { tenant: tenant.id },
|
|
393
|
+
},
|
|
394
|
+
config,
|
|
395
|
+
goals: [{
|
|
396
|
+
id: `tenant-${tenant.id}-ready`,
|
|
397
|
+
description: `Tenant ${tenant.id} is locally verified and launchable`,
|
|
398
|
+
run: () => retryPassed,
|
|
399
|
+
}],
|
|
400
|
+
explicit: Boolean(flags.repair),
|
|
401
|
+
noRecovery: false,
|
|
402
|
+
actionContext: {
|
|
403
|
+
tenantId: tenant.id,
|
|
404
|
+
probeGateway: () => io.probe(gatewayUrl, pat, tenant.id),
|
|
405
|
+
repairProfiles: async () => {
|
|
406
|
+
const profileReport = await verifyConvergence(false, [tenant]);
|
|
407
|
+
return ["ready", "unavailable"].includes(profileReport.tenants[0]?.cli);
|
|
518
408
|
},
|
|
409
|
+
retryStep: () => retrySafeState(),
|
|
410
|
+
installVendorApp: (target) => retryTenant(async (product) => (
|
|
411
|
+
target === "all"
|
|
412
|
+
|| product === target
|
|
413
|
+
|| (target === "codex" && product === "chatgpt")
|
|
414
|
+
)),
|
|
519
415
|
},
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
promptText: io.promptText,
|
|
523
|
-
...(overrides.recoveryOverrides || {}),
|
|
524
|
-
}
|
|
525
|
-
);
|
|
526
|
-
// "fixed" is only reported after every goal passed a fresh local check
|
|
527
|
-
// inside the engine, so no separate re-verification is needed here.
|
|
528
|
-
if (recovery.fixed) {
|
|
529
|
-
platformSetupFailed = false;
|
|
530
|
-
verificationFailed = false;
|
|
531
|
-
}
|
|
532
|
-
} catch (error) {
|
|
533
|
-
console.warn(`Install recovery could not start (${redactSecretText(error?.message || error)}).`);
|
|
416
|
+
}, overrides.recoveryOverrides || {});
|
|
417
|
+
if (retryReport) mergeTenantReport(report, retryReport);
|
|
534
418
|
}
|
|
535
419
|
}
|
|
420
|
+
recomputeReport(report);
|
|
421
|
+
report.passed = report.passed && !sharedFailure;
|
|
536
422
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
console.log("");
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
console.
|
|
543
|
-
|
|
423
|
+
printReconciliationSummary(report);
|
|
424
|
+
console.log(`CLI tenant: ${selected.id}`);
|
|
425
|
+
console.log("Change it: impel tenant use <tenant>");
|
|
426
|
+
console.log("Launch: impel claude | impel codex");
|
|
427
|
+
if (!report.passed || sharedFailure) {
|
|
428
|
+
if (sharedFailure) console.error(`Shared setup failure: ${sharedFailure}`);
|
|
429
|
+
console.error("Setup is incomplete. Fix the reported issue or rerun `impel setup`.");
|
|
430
|
+
process.exitCode = 1;
|
|
431
|
+
return report;
|
|
544
432
|
}
|
|
545
|
-
console.log("
|
|
546
|
-
|
|
547
|
-
if (io.platform === "win32") console.log(" impel app open all open Claude and ChatGPT with isolated Impel profiles");
|
|
548
|
-
console.log(" impel claude isolated Claude Code through the gateway");
|
|
549
|
-
console.log(" impel codex isolated Codex through the gateway");
|
|
550
|
-
console.log(" impel status full diagnostics any time");
|
|
433
|
+
console.log("Setup complete for every accessible tenant.");
|
|
434
|
+
return report;
|
|
551
435
|
}
|