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.
@@ -1,95 +1,145 @@
1
- import { loadConfig, resolveDefaultGateway, resolveDefaultAppUrl, maskSecret } from "../config.js";
2
- import { detectClaudeMode } from "../claudeSetup.js";
3
- import { detectCodexMode } from "../codexSetup.js";
4
- import { ensureTenantSelection, productAccessLabel, tenantCredential } from "../tenants.js";
5
- import { maybePrintUpdateNotice } from "../updates.js";
6
- import { findNativeBinary } from "../nativeProcess.js";
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
7
4
 
8
- function modeLabel(mode) {
9
- return mode === "gateway" ? "GATEWAY" : "ACCOUNT";
10
- }
5
+ import { appPaths, CLAUDE_CONFIG_ID, readTenantManifest } from "../apps.js";
6
+ import { loadConfig, maskSecret } from "../config.js";
7
+ import { environmentValue, findNativeBinary } from "../nativeProcess.js";
8
+ import { tenantCliClientReadiness } from "../provisioning.js";
9
+ import { windowsTenantShortcutName } from "../shellEntries.js";
10
+ import {
11
+ ensureTenantSelection,
12
+ PAT_SCOPE_CLAUDE,
13
+ PAT_SCOPE_CODEX,
14
+ productAccessLabel,
15
+ } from "../tenants.js";
16
+ import { installedVersion, maybePrintUpdateNotice } from "../updates.js";
17
+ import { windowsClaudeUserData } from "../windowsApps.js";
11
18
 
12
- export async function cmdStatus() {
13
- const config = loadConfig();
14
- const gatewayUrl = config?.gatewayUrl || resolveDefaultGateway();
15
- const appUrl = config?.appUrl || resolveDefaultAppUrl();
19
+ function readShellManifest(paths) {
20
+ try {
21
+ const value = JSON.parse(fs.readFileSync(path.join(paths.tenantRoot, "shell-entries.json"), "utf8"));
22
+ return Array.isArray(value?.entries) ? value.entries : [];
23
+ } catch {
24
+ return [];
25
+ }
26
+ }
16
27
 
17
- console.log(`Gateway URL: ${gatewayUrl}`);
18
- console.log(`App URL: ${appUrl}`);
19
- console.log(`PAT stored: ${config?.pat ? `yes (${maskSecret(config.pat)})` : "no — run `impel auth`"}`);
20
- let tenantId = config?.tenantId || null;
21
- let productAccess = null;
22
- let scopes = null;
23
- let selectionFresh = false;
24
- if (config?.pat) {
25
- try {
26
- const selected = await ensureTenantSelection(config, { refresh: true });
27
- tenantId = selected.tenantId;
28
- productAccess = selected.productAccess;
29
- scopes = selected.scopes;
30
- selectionFresh = true;
31
- } catch {
32
- // The reachability probe below still reports the underlying network/auth state.
28
+ function desktopReadiness(tenant, {
29
+ platform = process.platform,
30
+ homeDir = os.homedir(),
31
+ environment = process.env,
32
+ existsSync = fs.existsSync,
33
+ } = {}) {
34
+ if (!["darwin", "win32"].includes(platform)) {
35
+ return {
36
+ claude: { app: "unavailable", shell: "unavailable" },
37
+ codex: { app: "unavailable", shell: "unavailable" },
38
+ };
39
+ }
40
+ const claudeUserData = platform === "win32" ? windowsClaudeUserData(environment, tenant.id) : null;
41
+ const paths = appPaths(homeDir, tenant.id, { tenantName: tenant.name, claudeUserData });
42
+ const manifest = readTenantManifest(homeDir, tenant.id);
43
+ const available = new Set(manifest?.availableTargets || manifest?.targets || []);
44
+ const registered = new Set(readShellManifest(paths).map((entry) => entry.product));
45
+ const appData = environmentValue(environment, "APPDATA");
46
+ const states = {};
47
+ for (const [client, product] of [["claude", "claude"], ["codex", "chatgpt"]]) {
48
+ if (manifest && !available.has(product)) {
49
+ states[client] = { app: "unsupported", shell: "unsupported" };
50
+ continue;
51
+ }
52
+ const appReady = platform === "darwin"
53
+ ? existsSync(paths[product].launcher)
54
+ : existsSync(product === "claude"
55
+ ? path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`)
56
+ : path.join(paths.chatgpt.codexHome, "config.toml"));
57
+ let shellReady = appReady && registered.has(product);
58
+ if (platform === "win32" && appData) {
59
+ const shortcutPath = path.win32.join(
60
+ appData,
61
+ "Microsoft", "Windows", "Start Menu", "Programs", "Impel",
62
+ windowsTenantShortcutName(product, tenant.id, tenant.name),
63
+ );
64
+ shellReady = appReady && existsSync(shortcutPath);
33
65
  }
66
+ states[client] = {
67
+ app: appReady ? "ready" : "missing",
68
+ shell: shellReady ? "ready" : "missing",
69
+ };
34
70
  }
35
- const staleLabel = config?.pat && !selectionFresh ? " (cached selection; live check failed)" : "";
36
- const unavailableReason = config?.pat ? "live check failed" : "run `impel auth`";
37
- console.log(`Tenant: ${tenantId ? `${tenantId}${staleLabel}` : "not selected — run `impel tenant list`"}`);
38
- console.log(`Access: ${selectionFresh ? productAccessLabel(productAccess) : `Unknown — ${unavailableReason}`}`);
39
- console.log(`PAT scopes: ${selectionFresh && scopes?.length ? scopes.join(", ") : `unknown — ${unavailableReason}`}`);
40
- const claudeBinary = findNativeBinary("claude");
41
- const codexBinary = findNativeBinary("codex");
42
- console.log("Isolated CLI launchers:");
43
- console.log(
44
- ` Claude: ${config?.pat && claudeBinary ? "READY (`impel claude`)" : !config?.pat ? "NOT READY — run `impel setup`" : "NOT READY — Claude Code is not installed"}`
45
- );
46
- console.log(
47
- ` Codex: ${config?.pat && codexBinary ? "READY (`impel codex`)" : !config?.pat ? "NOT READY — run `impel setup`" : "NOT READY — Codex is not installed"}`
48
- );
49
-
50
- // Per-tool mode. Codex CLI and the Codex app/IDE share the same
51
- // ~/.codex/config.toml, so they always report the same mode.
52
- const claude = detectClaudeMode(gatewayUrl);
53
- const codex = detectCodexMode();
54
-
55
- console.log("");
56
- console.log("Native profile mode (isolated launchers do not change this):");
57
- console.log(` Claude Code: ${modeLabel(claude.mode)}`);
58
- console.log(` Codex CLI: ${modeLabel(codex.mode)}`);
59
- console.log(` Codex app: ${modeLabel(codex.mode)} (shares ~/.codex/config.toml with the CLI)`);
60
- console.log(" Flip: `impel use gateway|account [claude|codex|all]` (aliases: `impel on` / `impel off`)");
71
+ return states;
72
+ }
61
73
 
62
- maybePrintUpdateNotice();
74
+ function cliState({ supported, profileReady, binaryReady }) {
75
+ if (!supported) return "unsupported";
76
+ return profileReady && binaryReady ? "ready" : "missing";
77
+ }
63
78
 
64
- const url = `${gatewayUrl}/anthropic/v1/messages`;
65
- console.log("");
66
- process.stdout.write(`Reachability: probing ${url} ... `);
79
+ export async function cmdStatus(overrides = {}) {
80
+ const io = {
81
+ loadConfig,
82
+ ensureTenantSelection,
83
+ cliReadiness: tenantCliClientReadiness,
84
+ desktopReadiness,
85
+ installedVersion,
86
+ findNativeBinary,
87
+ maybePrintUpdateNotice,
88
+ platform: process.platform,
89
+ environment: process.env,
90
+ ...overrides,
91
+ };
92
+ const config = io.loadConfig();
93
+ console.log(`impel-cli: v${io.installedVersion() || "?"}`);
94
+ console.log(`Authentication: ${config?.pat ? `configured (${maskSecret(config.pat)})` : "not configured - run `impel setup`"}`);
95
+ if (!config?.pat) {
96
+ io.maybePrintUpdateNotice();
97
+ return;
98
+ }
67
99
 
68
- const controller = new AbortController();
69
- const timeout = setTimeout(() => controller.abort(), 5000);
100
+ let selected;
70
101
  try {
71
- const res = await fetch(url, {
72
- method: "POST",
73
- headers: {
74
- "content-type": "application/json",
75
- ...(config?.pat && tenantId
76
- ? { authorization: `Bearer ${tenantCredential(config.pat, tenantId)}` }
77
- : {}),
78
- },
79
- body: "{}",
80
- signal: controller.signal,
102
+ selected = await io.ensureTenantSelection(config, { refresh: true });
103
+ console.log(`Authentication check: accepted (${productAccessLabel(selected.productAccess)})`);
104
+ } catch (error) {
105
+ console.log(`Authentication check: unavailable (${error?.message || error})`);
106
+ console.log(`Current CLI tenant: ${config.tenantId || "not selected"}`);
107
+ console.log("Run `impel setup` to refresh authentication, then `impel update` to repair local tenants.");
108
+ io.maybePrintUpdateNotice();
109
+ return;
110
+ }
111
+
112
+ console.log(`Current CLI tenant: ${selected.tenantId}`);
113
+ console.log("Change it with: impel tenant use <tenant>");
114
+ const scopes = new Set(selected.scopes || []);
115
+ const scopeKnown = selected.scopes != null;
116
+ const binaries = {
117
+ claude: Boolean(io.findNativeBinary("claude", io.environment, io.platform)),
118
+ codex: Boolean(io.findNativeBinary("codex", io.environment, io.platform)),
119
+ };
120
+ console.log("Tenant readiness:");
121
+ let incomplete = false;
122
+ for (const tenant of [...selected.tenants].sort((left, right) => left.id.localeCompare(right.id))) {
123
+ const profiles = io.cliReadiness(tenant.id);
124
+ const desktop = io.desktopReadiness(tenant, overrides);
125
+ const claudeCli = cliState({
126
+ supported: !scopeKnown || scopes.has(PAT_SCOPE_CLAUDE),
127
+ profileReady: profiles.claude,
128
+ binaryReady: binaries.claude,
81
129
  });
82
- // Any HTTP response — even a 4xx from a malformed/empty test body — means
83
- // the gateway is up and routing. This is a best-effort reachability
84
- // check, not a validity check of the request itself.
85
- console.log(`HTTP ${res.status} ${res.statusText || ""}`.trim());
86
- if (res.status === 401 || res.status === 403) {
87
- console.log(" (gateway is reachable; that status usually means the PAT was rejected)");
88
- }
89
- } catch (err) {
90
- console.log("unreachable");
91
- console.log(` ${err.name === "AbortError" ? "timed out after 5s" : err.message}`);
92
- } finally {
93
- clearTimeout(timeout);
130
+ const codexCli = cliState({
131
+ supported: !scopeKnown || scopes.has(PAT_SCOPE_CODEX),
132
+ profileReady: profiles.codex,
133
+ binaryReady: binaries.codex,
134
+ });
135
+ const states = [claudeCli, codexCli, desktop.claude.app, desktop.codex.app, desktop.claude.shell, desktop.codex.shell];
136
+ if (states.includes("missing")) incomplete = true;
137
+ console.log(
138
+ ` ${tenant.id}${tenant.id === selected.tenantId ? " (current)" : ""}: `
139
+ + `Claude CLI ${claudeCli}, Codex CLI ${codexCli}, `
140
+ + `Claude app ${desktop.claude.app}, ChatGPT app ${desktop.codex.app}`,
141
+ );
94
142
  }
143
+ if (incomplete) console.log("Repair or finish missing tenant surfaces with: impel update");
144
+ io.maybePrintUpdateNotice();
95
145
  }
@@ -92,7 +92,7 @@ async function requestJson({ flags, path, query, method = "GET", body }) {
92
92
  fail("impel tasks: the control plane did not return live PAT scopes; retry after it is upgraded.");
93
93
  }
94
94
  if (!selected.scopes.includes(PAT_SCOPE_TASKS)) {
95
- fail("impel tasks: this PAT is missing the \"tasks\" scope; create a fresh PAT in Impel Gateway setup, then run `impel auth --pat <pat>`.");
95
+ fail("impel tasks: this PAT is missing the \"tasks\" scope; create a fresh PAT in Impel Gateway setup, then run `impel setup` and provide it.");
96
96
  }
97
97
  const selectedOrgId = flags.org || selected.tenantId;
98
98
  const resolvedQuery = { ...(query || {}) };
@@ -1,7 +1,6 @@
1
1
  import { parseFlags } from "../args.js";
2
2
  import { loadConfig } from "../config.js";
3
3
  import { ensureTenantSelection, productAccessLabel, selectTenant } from "../tenants.js";
4
- import { cmdApps } from "./apps.js";
5
4
  import { cmdLaunch } from "./launch.js";
6
5
 
7
6
  const HELP = `impel tenant - select the organization used by Impel sessions
@@ -9,7 +8,7 @@ const HELP = `impel tenant - select the organization used by Impel sessions
9
8
  Usage:
10
9
  impel tenant list
11
10
  impel tenant current
12
- impel tenant use <org-slug> [--launch claude|codex|apps]
11
+ impel tenant use <org-slug> [--launch claude|codex]
13
12
 
14
13
  Aliases: impel tenants ..., impel org ...
15
14
  `;
@@ -69,10 +68,9 @@ export async function cmdTenant(argv) {
69
68
 
70
69
  const launch = flags.launch;
71
70
  if (!launch) {
72
- console.log("Next: `impel claude`, `impel codex`, or `impel app open`.");
71
+ console.log("Next: `impel claude` or `impel codex`. Desktop apps are available from Finder or Windows Search.");
73
72
  return;
74
73
  }
75
74
  if (launch === "claude" || launch === "codex") return cmdLaunch(launch, []);
76
- if (launch === "apps") return cmdApps(["open", "all"]);
77
- throw new Error(`unknown launch target "${launch}"; use claude, codex, or apps`);
75
+ throw new Error(`unknown launch target "${launch}"; use claude or codex`);
78
76
  }
@@ -1,21 +1,13 @@
1
- // `impel update` — bring everything current in one command: the CLI itself
2
- // (npm reinstall from the public package), then the managed desktop apps/profiles, then
3
- // skills and native tenant agents. The app step re-executes the freshly installed CLI so the new code
4
- // performs it.
1
+ // `impel update` — update the CLI, then let the freshly installed build
2
+ // reconcile every tenant and managed surface from the live control-plane list.
5
3
 
6
- import fs from "node:fs";
7
- import os from "node:os";
8
- import path from "node:path";
9
4
  import { spawnSync } from "node:child_process";
10
5
  import { fileURLToPath } from "node:url";
11
6
 
12
7
  import { parseFlags } from "../args.js";
13
- import { CLAUDE_CONFIG_ID, appPaths } from "../apps.js";
14
8
  import { loadConfig, redactSecretText } from "../config.js";
15
9
  import { nativeCommandInvocation } from "../nativeProcess.js";
16
- import { windowsClaudeUserData } from "../windowsApps.js";
17
10
  import { withProgress } from "../progress.js";
18
- import { installedAppTenantIds } from "./apps.js";
19
11
  import { runInstallRecovery } from "../installRecovery/engine.js";
20
12
  import {
21
13
  fetchRemoteVersion,
@@ -30,16 +22,14 @@ const CLI_BIN = fileURLToPath(new URL("../../bin/impel.js", import.meta.url));
30
22
 
31
23
  const HELP = `impel update - update everything Impel in one command
32
24
 
33
- Reinstalls impel-cli from npm, then cascades with the NEW build:
34
- \`impel app update all\` (update every locally installed tenant's vendor apps
35
- and managed app profiles), then \`impel skills sync all\` and
36
- \`impel agents sync all\` across native and isolated CLI profiles.
25
+ Reinstalls impel-cli from npm, then uses the new build to discover every
26
+ accessible tenant. Missing tenants are installed; existing tenant profiles and
27
+ apps are repaired and upgraded in place while preserving local state.
37
28
 
38
29
  Usage:
39
- impel update Update the CLI, then cascade to apps, skills, and agents
30
+ impel update Update the CLI and reconcile every accessible tenant
40
31
  impel update --check Report whether an update is available; change nothing
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
32
+ impel update --skip-apps Reconcile only isolated CLI profiles
43
33
  impel update --no-recovery Disable local and hosted recovery for this run
44
34
  `;
45
35
 
@@ -93,17 +83,25 @@ function defaultSelfUpdate(spec) {
93
83
 
94
84
  // The cascading steps re-execute the (freshly installed) CLI binary so the
95
85
  // NEW code performs them, not the process that started the update.
96
- export function defaultRunAppsUpdate({
86
+ export function defaultRunConvergence({
87
+ skipApps = false,
88
+ noRecovery = false,
97
89
  spawn = spawnSync,
98
90
  execPath = process.execPath,
99
91
  cliBin = CLI_BIN,
100
92
  } = {}) {
101
- const result = spawn(execPath, [cliBin, "app", "update", "all"], {
93
+ const args = [cliBin, "_converge"];
94
+ if (skipApps) args.push("--skip-apps");
95
+ if (noRecovery) args.push("--no-recovery");
96
+ const result = spawn(execPath, args, {
102
97
  stdio: "inherit",
103
98
  });
104
99
  return result.status === 0;
105
100
  }
106
101
 
102
+ // Compatibility export for callers from the previous cascade implementation.
103
+ export const defaultRunAppsUpdate = defaultRunConvergence;
104
+
107
105
  export function defaultRunSkillsSync({
108
106
  spawn = spawnSync,
109
107
  execPath = process.execPath,
@@ -126,36 +124,6 @@ export function defaultRunAgentsSync({
126
124
  return result.status === 0;
127
125
  }
128
126
 
129
- function anyAppInstalled(homeDir = os.homedir()) {
130
- if (installedAppTenantIds(["claude", "chatgpt"], {
131
- homeDir,
132
- platform: process.platform,
133
- environment: process.env,
134
- }).length > 0) return true;
135
- const config = loadConfig();
136
- const claudeUserData = process.platform === "win32"
137
- ? windowsClaudeUserData(process.env, config?.tenantId || null)
138
- : null;
139
- const paths = appPaths(homeDir, config?.tenantId || null, { claudeUserData });
140
- if (process.platform === "win32") {
141
- return (
142
- fs.existsSync(path.join(
143
- paths.claude.userData,
144
- "configLibrary",
145
- `${CLAUDE_CONFIG_ID}.json`,
146
- ))
147
- || fs.existsSync(path.join(paths.chatgpt.codexHome, "config.toml"))
148
- );
149
- }
150
- const legacyPaths = appPaths(homeDir);
151
- return (
152
- fs.existsSync(paths.claude.launcher)
153
- || fs.existsSync(paths.chatgpt.launcher)
154
- || fs.existsSync(legacyPaths.claude.launcher)
155
- || fs.existsSync(legacyPaths.chatgpt.launcher)
156
- );
157
- }
158
-
159
127
  export async function cmdUpdate(argv, overrides = {}) {
160
128
  const io = {
161
129
  fetchRemoteVersion,
@@ -163,16 +131,29 @@ export async function cmdUpdate(argv, overrides = {}) {
163
131
  refreshUpdateCache,
164
132
  writeCache: writeUpdateCache,
165
133
  selfUpdate: defaultSelfUpdate,
134
+ runConvergence: defaultRunConvergence,
166
135
  runAppsUpdate: defaultRunAppsUpdate,
167
136
  runSkillsSync: defaultRunSkillsSync,
168
137
  runAgentsSync: defaultRunAgentsSync,
169
- appsInstalled: anyAppInstalled,
170
138
  platform: process.platform,
171
139
  progress: withProgress,
172
140
  recoverInstall: runInstallRecovery,
173
141
  loadConfig,
174
142
  ...overrides,
175
143
  };
144
+ // Older embedders injected the three public cascade callbacks. Treat an
145
+ // injected app step as the convergence boundary so their tests/automation
146
+ // never fall through to a real fresh-build child process.
147
+ if (!overrides.runConvergence && (
148
+ overrides.runAppsUpdate || overrides.runSkillsSync || overrides.runAgentsSync
149
+ )) {
150
+ io.runConvergence = (options) => {
151
+ if (!options?.skipApps && overrides.runAppsUpdate && !overrides.runAppsUpdate()) return false;
152
+ if (overrides.runSkillsSync && !overrides.runSkillsSync()) return false;
153
+ if (overrides.runAgentsSync && !overrides.runAgentsSync()) return false;
154
+ return true;
155
+ };
156
+ }
176
157
  const { flags } = parseFlags(argv, {
177
158
  check: { type: "boolean" },
178
159
  "skip-apps": { type: "boolean" },
@@ -228,6 +209,7 @@ export async function cmdUpdate(argv, overrides = {}) {
228
209
  const recovery = await io.recoverInstall(
229
210
  {
230
211
  failure: {
212
+ scope: "shared",
231
213
  platform: io.platform,
232
214
  architecture: process.arch,
233
215
  step: "install.impel_cli",
@@ -278,41 +260,13 @@ export async function cmdUpdate(argv, overrides = {}) {
278
260
  console.log(`CLI: updated${remote ? ` to v${remote}` : ""}.`);
279
261
  }
280
262
 
281
- // ── Cascade: every tenant app, then non-app managed profiles ───────────
282
- let cascadeFailed = false;
283
- if (flags["skip-apps"]) {
284
- console.log("Apps: skipped (--skip-apps).");
285
- } else if (io.platform !== "darwin" && io.platform !== "win32") {
286
- console.log("Apps: skipped — isolated desktop apps are unavailable on this platform.");
287
- } else if (!io.appsInstalled()) {
288
- console.log("Apps: none installed; skipping (run `impel app install` or `impel setup`).");
289
- } else {
290
- console.log(io.platform === "win32"
291
- ? "Apps: updating every installed tenant's signed Claude and ChatGPT profiles…"
292
- : "Apps: refreshing every installed tenant and rebuilding only stale app bundles…");
293
- // These child commands inherit the terminal and render their own progress
294
- // and log lines. Wrapping them in another spinner makes both processes
295
- // write the same terminal row, producing glued output such as
296
- // "Updating managed desktop apps (...)Updating all managed...".
297
- if (!await io.runAppsUpdate()) {
298
- console.error("impel update: the app update failed; re-run `impel app update` after fixing the issue.");
299
- cascadeFailed = true;
300
- }
301
- }
302
-
303
- console.log("Skills: syncing native and isolated CLI profiles…");
304
- if (!await io.runSkillsSync()) {
305
- console.error("impel update: skill sync failed; re-run `impel skills sync` after fixing the issue.");
306
- cascadeFailed = true;
307
- }
308
-
309
- console.log("Agents: syncing the selected tenant into native and isolated CLI profiles…");
310
- if (!await io.runAgentsSync()) {
311
- console.error("impel update: agent sync failed; re-run `impel agents sync` after fixing the issue.");
312
- cascadeFailed = true;
313
- }
314
-
315
- if (cascadeFailed) {
263
+ console.log("Tenants: discovering, installing, repairing, and upgrading every accessible tenant…");
264
+ const convergenceArgs = {
265
+ skipApps: Boolean(flags["skip-apps"]),
266
+ noRecovery: Boolean(flags["no-recovery"]),
267
+ };
268
+ if (!await io.runConvergence(convergenceArgs)) {
269
+ console.error("impel update: tenant convergence failed; rerun `impel update` after addressing the reported issue.");
316
270
  process.exitCode = 1;
317
271
  return;
318
272
  }