impel-cli 0.16.4 → 0.17.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 +224 -945
- package/package.json +2 -2
- package/src/apps.js +8 -2
- package/src/cli.js +38 -41
- package/src/cliProfiles.js +5 -1
- package/src/commands/apps.js +215 -0
- package/src/commands/converge.js +174 -0
- package/src/commands/sessions.js +114 -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/config.js +8 -3
- 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/sessionCollector.js +996 -0
- package/src/sessionHooks.js +242 -0
- package/src/shellEntries.js +186 -0
- package/src/skills.js +1 -1
- package/src/tenants.js +1 -1
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { syncAgentProfilesSafe } from "./agents.js";
|
|
6
|
+
import { crossAppModelsEnabled, redactSecretText } from "./config.js";
|
|
7
|
+
import { ensureImpelClaudeProfile, ensureImpelCodexProfile, tenantCliProfilePaths } from "./cliProfiles.js";
|
|
8
|
+
import { findNativeBinary } from "./nativeProcess.js";
|
|
9
|
+
import { mapWithConcurrency } from "./progress.js";
|
|
10
|
+
import { registerTenantShellEntries } from "./shellEntries.js";
|
|
11
|
+
import { resolveSkillsGateway, syncSkillsSafe } from "./skills.js";
|
|
12
|
+
import {
|
|
13
|
+
normalizeTenantId,
|
|
14
|
+
PAT_SCOPE_CLAUDE,
|
|
15
|
+
PAT_SCOPE_CODEX,
|
|
16
|
+
tenantCredential,
|
|
17
|
+
} from "./tenants.js";
|
|
18
|
+
import { reconcileTenantApps } from "./commands/apps.js";
|
|
19
|
+
|
|
20
|
+
export function selectDefaultTenant(listing, { requested = null, currentTenantId = null } = {}) {
|
|
21
|
+
const normalized = requested ? normalizeTenantId(requested) : null;
|
|
22
|
+
if (normalized) {
|
|
23
|
+
const explicit = listing.tenants.find((tenant) => tenant.id === normalized || tenant.slug === normalized);
|
|
24
|
+
if (!explicit) throw new Error(`tenant "${normalized}" is not available to this user`);
|
|
25
|
+
return explicit;
|
|
26
|
+
}
|
|
27
|
+
return listing.tenants.find((tenant) => tenant.id === currentTenantId)
|
|
28
|
+
|| listing.tenants.find((tenant) => tenant.id === listing.defaultTenantId)
|
|
29
|
+
|| listing.tenants[0];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function mapSettledWithConcurrency(items, limit, fn) {
|
|
33
|
+
return mapWithConcurrency(items, limit, async (item, index) => {
|
|
34
|
+
try {
|
|
35
|
+
return { status: "fulfilled", value: await fn(item, index) };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { status: "rejected", reason: error };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildTenantInventory(listing, {
|
|
43
|
+
requestedTenantId = null,
|
|
44
|
+
currentTenantId = null,
|
|
45
|
+
localTenantIds = [],
|
|
46
|
+
} = {}) {
|
|
47
|
+
const live = [...listing.tenants].sort((left, right) => left.id.localeCompare(right.id));
|
|
48
|
+
const liveIds = new Set(live.map((tenant) => tenant.id));
|
|
49
|
+
const local = [...new Set(localTenantIds.map((tenantId) => normalizeTenantId(tenantId)))].sort();
|
|
50
|
+
const selected = selectDefaultTenant(listing, {
|
|
51
|
+
requested: requestedTenantId,
|
|
52
|
+
currentTenantId,
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
live,
|
|
56
|
+
selectedTenantId: selected.id,
|
|
57
|
+
missing: live.filter((tenant) => !local.includes(tenant.id)),
|
|
58
|
+
existing: live.filter((tenant) => local.includes(tenant.id)),
|
|
59
|
+
retainedInaccessible: local.filter((tenantId) => !liveIds.has(tenantId)),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function locallyKnownTenantIds({ homeDir = os.homedir(), readDirectory = fs.readdirSync } = {}) {
|
|
64
|
+
const roots = [
|
|
65
|
+
path.join(homeDir, ".config", "impel", "cli", "tenants"),
|
|
66
|
+
path.join(homeDir, ".config", "impel", "apps", "tenants"),
|
|
67
|
+
];
|
|
68
|
+
const ids = new Set();
|
|
69
|
+
for (const root of roots) {
|
|
70
|
+
try {
|
|
71
|
+
for (const entry of readDirectory(root, { withFileTypes: true })) {
|
|
72
|
+
if (!entry.isDirectory()) continue;
|
|
73
|
+
try { ids.add(normalizeTenantId(entry.name)); } catch { /* ignore invalid local debris */ }
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error?.code !== "ENOENT") throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return [...ids].sort((left, right) => left.localeCompare(right));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function providerSupported(scopes, scope) {
|
|
83
|
+
return scopes == null || scopes.includes(scope);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function prepareTenantCli(config, tenant, io, binaries) {
|
|
87
|
+
const gatewayUrl = resolveSkillsGateway(config.gatewayUrl);
|
|
88
|
+
const definitions = {
|
|
89
|
+
claude: {
|
|
90
|
+
supported: providerSupported(config.scopes, PAT_SCOPE_CLAUDE),
|
|
91
|
+
ensure: () => io.ensureClaude(config.gatewayUrl, tenant.id, {
|
|
92
|
+
crossAppModels: crossAppModelsEnabled(config),
|
|
93
|
+
}),
|
|
94
|
+
root: (profile) => profile.configDir,
|
|
95
|
+
env: (profile) => ({ CLAUDE_CONFIG_DIR: profile.configDir }),
|
|
96
|
+
},
|
|
97
|
+
codex: {
|
|
98
|
+
supported: providerSupported(config.scopes, PAT_SCOPE_CODEX),
|
|
99
|
+
ensure: () => io.ensureCodex(config.gatewayUrl, tenant.id),
|
|
100
|
+
root: (profile) => profile.codexHome,
|
|
101
|
+
env: (profile) => ({ CODEX_HOME: profile.codexHome }),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const clients = {};
|
|
105
|
+
for (const [client, definition] of Object.entries(definitions)) {
|
|
106
|
+
if (!definition.supported) {
|
|
107
|
+
clients[client] = { status: "unsupported", root: null, error: null };
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const profile = definition.ensure();
|
|
112
|
+
const root = definition.root(profile);
|
|
113
|
+
await io.syncSkills({
|
|
114
|
+
client,
|
|
115
|
+
gatewayUrl,
|
|
116
|
+
env: definition.env(profile),
|
|
117
|
+
label: `Impel ${client === "claude" ? "Claude" : "Codex"} CLI (${tenant.id})`,
|
|
118
|
+
});
|
|
119
|
+
clients[client] = binaries[client]
|
|
120
|
+
? { status: "ready", root, error: null }
|
|
121
|
+
: {
|
|
122
|
+
status: "failed",
|
|
123
|
+
root,
|
|
124
|
+
error: `${client === "claude" ? "Claude Code" : "Codex"} CLI is not installed or discoverable`,
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
clients[client] = {
|
|
128
|
+
status: "failed",
|
|
129
|
+
root: null,
|
|
130
|
+
error: redactSecretText(error?.message || error),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const agentProfiles = Object.entries(clients)
|
|
136
|
+
.filter(([, client]) => client.root)
|
|
137
|
+
.map(([client, value]) => ({
|
|
138
|
+
client,
|
|
139
|
+
root: value.root,
|
|
140
|
+
label: `Impel ${client === "claude" ? "Claude" : "Codex"} CLI (${tenant.id})`,
|
|
141
|
+
}));
|
|
142
|
+
if (agentProfiles.length) {
|
|
143
|
+
try {
|
|
144
|
+
await io.syncAgents({
|
|
145
|
+
profiles: agentProfiles,
|
|
146
|
+
gatewayUrl: config.gatewayUrl,
|
|
147
|
+
credential: tenantCredential(config.pat, tenant.id),
|
|
148
|
+
tenantId: tenant.id,
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const message = redactSecretText(error?.message || error);
|
|
152
|
+
for (const client of Object.values(clients)) {
|
|
153
|
+
if (client.root) {
|
|
154
|
+
client.status = "failed";
|
|
155
|
+
client.error = message;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return clients;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function reconcileAllTenants({
|
|
164
|
+
config,
|
|
165
|
+
listing,
|
|
166
|
+
mode = "update",
|
|
167
|
+
includeApps = true,
|
|
168
|
+
platform = process.platform,
|
|
169
|
+
homeDir = os.homedir(),
|
|
170
|
+
environment = process.env,
|
|
171
|
+
concurrency = 4,
|
|
172
|
+
confirmVendorInstall = async () => false,
|
|
173
|
+
} = {}, overrides = {}) {
|
|
174
|
+
const io = {
|
|
175
|
+
ensureClaude: ensureImpelClaudeProfile,
|
|
176
|
+
ensureCodex: ensureImpelCodexProfile,
|
|
177
|
+
syncSkills: syncSkillsSafe,
|
|
178
|
+
syncAgents: syncAgentProfilesSafe,
|
|
179
|
+
findBinary: findNativeBinary,
|
|
180
|
+
reconcileApps: reconcileTenantApps,
|
|
181
|
+
registerShellEntries: registerTenantShellEntries,
|
|
182
|
+
knownTenantIds: locallyKnownTenantIds,
|
|
183
|
+
...overrides,
|
|
184
|
+
};
|
|
185
|
+
// Dependency-injection compatibility for older embedders/tests. Production
|
|
186
|
+
// uses the explicit reconciler above and never calls the public app parser.
|
|
187
|
+
if (overrides.installApps && !overrides.reconcileApps) {
|
|
188
|
+
io.reconcileApps = async ({ tenant, mode: appMode, skipVendor }) => {
|
|
189
|
+
const args = [appMode === "setup" ? "install" : "update", "all", "--tenant", tenant.id];
|
|
190
|
+
if (skipVendor) args.push("--skip-vendor");
|
|
191
|
+
const completed = await overrides.installApps(args);
|
|
192
|
+
if (completed === false) throw new Error("desktop app provisioning did not complete");
|
|
193
|
+
return { targets: ["claude", "chatgpt"], unsupported: [] };
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const inventory = buildTenantInventory(listing, {
|
|
197
|
+
currentTenantId: config.tenantId,
|
|
198
|
+
localTenantIds: io.knownTenantIds({ homeDir }),
|
|
199
|
+
});
|
|
200
|
+
const liveTenants = inventory.live;
|
|
201
|
+
const inaccessible = inventory.retainedInaccessible;
|
|
202
|
+
const binaries = {
|
|
203
|
+
claude: io.findBinary("claude", environment, platform),
|
|
204
|
+
codex: io.findBinary("codex", environment, platform),
|
|
205
|
+
};
|
|
206
|
+
const results = new Map(liveTenants.map((tenant) => [tenant.id, {
|
|
207
|
+
tenantId: tenant.id,
|
|
208
|
+
tenantName: tenant.name,
|
|
209
|
+
cli: "pending",
|
|
210
|
+
apps: includeApps && ["darwin", "win32"].includes(platform) ? "pending" : "unavailable",
|
|
211
|
+
shell: includeApps && ["darwin", "win32"].includes(platform) ? "pending" : "unavailable",
|
|
212
|
+
clients: {
|
|
213
|
+
claude: { cli: "pending", app: includeApps && ["darwin", "win32"].includes(platform) ? "pending" : "unsupported", shell: includeApps && ["darwin", "win32"].includes(platform) ? "pending" : "unsupported" },
|
|
214
|
+
codex: { cli: "pending", app: includeApps && ["darwin", "win32"].includes(platform) ? "pending" : "unsupported", shell: includeApps && ["darwin", "win32"].includes(platform) ? "pending" : "unsupported" },
|
|
215
|
+
},
|
|
216
|
+
errors: [],
|
|
217
|
+
}]));
|
|
218
|
+
|
|
219
|
+
const cliOutcomes = await mapSettledWithConcurrency(liveTenants, concurrency, (tenant) => (
|
|
220
|
+
prepareTenantCli(config, tenant, io, binaries)
|
|
221
|
+
));
|
|
222
|
+
cliOutcomes.forEach((outcome, index) => {
|
|
223
|
+
const result = results.get(liveTenants[index].id);
|
|
224
|
+
if (outcome.status === "fulfilled") {
|
|
225
|
+
for (const client of ["claude", "codex"]) {
|
|
226
|
+
const clientResult = outcome.value[client];
|
|
227
|
+
result.clients[client].cli = clientResult.status;
|
|
228
|
+
if (clientResult.error) result.errors.push(clientResult.error);
|
|
229
|
+
}
|
|
230
|
+
const states = [result.clients.claude.cli, result.clients.codex.cli];
|
|
231
|
+
result.cli = states.includes("failed")
|
|
232
|
+
? "failed"
|
|
233
|
+
: states.includes("ready") ? "ready" : "unavailable";
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
result.cli = "failed";
|
|
237
|
+
result.clients.claude.cli = "failed";
|
|
238
|
+
result.clients.codex.cli = "failed";
|
|
239
|
+
result.errors.push(redactSecretText(outcome.reason?.message || outcome.reason));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (includeApps && ["darwin", "win32"].includes(platform)) {
|
|
244
|
+
const vendorPreparedTargets = new Set();
|
|
245
|
+
for (const tenant of liveTenants) {
|
|
246
|
+
const result = results.get(tenant.id);
|
|
247
|
+
let supported = new Set();
|
|
248
|
+
try {
|
|
249
|
+
const installed = await io.reconcileApps({
|
|
250
|
+
baseConfig: config,
|
|
251
|
+
tenant,
|
|
252
|
+
mode,
|
|
253
|
+
targets: ["claude", "chatgpt"],
|
|
254
|
+
skipVendorTargets: [...vendorPreparedTargets],
|
|
255
|
+
platform,
|
|
256
|
+
homeDir,
|
|
257
|
+
environment,
|
|
258
|
+
confirmVendorInstall,
|
|
259
|
+
}, overrides.appOverrides || {});
|
|
260
|
+
supported = new Set(installed.targets || []);
|
|
261
|
+
for (const target of supported) vendorPreparedTargets.add(target);
|
|
262
|
+
result.clients.claude.app = supported.has("claude") ? "ready" : "unsupported";
|
|
263
|
+
result.clients.codex.app = supported.has("chatgpt") ? "ready" : "unsupported";
|
|
264
|
+
result.apps = supported.size ? "ready" : "unavailable";
|
|
265
|
+
} catch (error) {
|
|
266
|
+
result.apps = "failed";
|
|
267
|
+
for (const client of Object.values(result.clients)) {
|
|
268
|
+
if (client.app === "pending") client.app = "failed";
|
|
269
|
+
if (client.shell === "pending") client.shell = "failed";
|
|
270
|
+
}
|
|
271
|
+
result.shell = "failed";
|
|
272
|
+
result.errors.push(redactSecretText(error?.message || error));
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (supported.size === 0) {
|
|
277
|
+
result.shell = "unavailable";
|
|
278
|
+
result.clients.claude.shell = "unsupported";
|
|
279
|
+
result.clients.codex.shell = "unsupported";
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const entries = io.registerShellEntries({
|
|
284
|
+
tenantId: tenant.id,
|
|
285
|
+
tenantName: tenant.name,
|
|
286
|
+
targets: [...supported],
|
|
287
|
+
platform,
|
|
288
|
+
homeDir,
|
|
289
|
+
environment,
|
|
290
|
+
});
|
|
291
|
+
const entryProducts = new Set(entries.map((entry) => entry.product));
|
|
292
|
+
result.clients.claude.shell = supported.has("claude")
|
|
293
|
+
? (entryProducts.has("claude") ? "ready" : "failed")
|
|
294
|
+
: "unsupported";
|
|
295
|
+
result.clients.codex.shell = supported.has("chatgpt")
|
|
296
|
+
? (entryProducts.has("chatgpt") ? "ready" : "failed")
|
|
297
|
+
: "unsupported";
|
|
298
|
+
result.shell = [...supported].every((target) => entryProducts.has(target)) ? "ready" : "failed";
|
|
299
|
+
if (result.shell === "failed") throw new Error("operating-system app registration did not complete");
|
|
300
|
+
} catch (error) {
|
|
301
|
+
result.shell = "failed";
|
|
302
|
+
for (const client of Object.values(result.clients)) {
|
|
303
|
+
if (client.shell === "pending") client.shell = "failed";
|
|
304
|
+
}
|
|
305
|
+
result.errors.push(redactSecretText(error?.message || error));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const values = liveTenants.map((tenant) => {
|
|
311
|
+
const result = results.get(tenant.id);
|
|
312
|
+
const states = Object.values(result.clients).flatMap((client) => [client.cli, client.app, client.shell]);
|
|
313
|
+
result.status = states.includes("failed") ? "failed" : states.includes("pending") ? "partial" : "ready";
|
|
314
|
+
return result;
|
|
315
|
+
});
|
|
316
|
+
return {
|
|
317
|
+
selectedTenantId: config.tenantId,
|
|
318
|
+
tenants: values,
|
|
319
|
+
inaccessible,
|
|
320
|
+
passed: values.every((result) => (
|
|
321
|
+
(result.cli === "ready" || result.cli === "unavailable")
|
|
322
|
+
&& (result.apps === "ready" || result.apps === "unavailable")
|
|
323
|
+
&& (result.shell === "ready" || result.shell === "unavailable")
|
|
324
|
+
)),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function printReconciliationSummary(report, log = console.log) {
|
|
329
|
+
log("Tenant readiness:");
|
|
330
|
+
for (const tenant of report.tenants) {
|
|
331
|
+
const detail = tenant.errors.length ? ` — ${tenant.errors.join("; ")}` : "";
|
|
332
|
+
log(
|
|
333
|
+
` ${tenant.tenantId}: Claude CLI ${tenant.clients.claude.cli}, Codex CLI ${tenant.clients.codex.cli}, `
|
|
334
|
+
+ `Claude app ${tenant.clients.claude.app}, ChatGPT app ${tenant.clients.codex.app}, system launch ${tenant.shell}${detail}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (report.inaccessible.length) {
|
|
338
|
+
log(` Retained local data for inaccessible tenants: ${report.inaccessible.join(", ")}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function tenantCliPathsReady(tenantId, existsSync = fs.existsSync) {
|
|
343
|
+
const paths = tenantCliProfilePaths(tenantId);
|
|
344
|
+
return existsSync(path.join(paths.claudeConfigDir, "settings.json"))
|
|
345
|
+
&& existsSync(path.join(paths.codexHome, "config.toml"));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function tenantCliClientReadiness(tenantId, existsSync = fs.existsSync) {
|
|
349
|
+
const paths = tenantCliProfilePaths(tenantId);
|
|
350
|
+
return {
|
|
351
|
+
claude: existsSync(path.join(paths.claudeConfigDir, "settings.json")),
|
|
352
|
+
codex: existsSync(path.join(paths.codexHome, "config.toml")),
|
|
353
|
+
};
|
|
354
|
+
}
|