impel-cli 0.18.15 → 0.18.17

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.
@@ -76,7 +76,7 @@ export function selectCatalogAppTargets(
76
76
  requestedTargets,
77
77
  models,
78
78
  config,
79
- { log = console.log } = {},
79
+ { log = console.log, allowNone = false } = {},
80
80
  ) {
81
81
  const supported = appTargetsSupportedByModels(
82
82
  requestedTargets,
@@ -87,6 +87,14 @@ export function selectCatalogAppTargets(
87
87
  const unavailable = requestedTargets.filter((target) => !supportedSet.has(target));
88
88
  if (unavailable.length === 0) return supported;
89
89
 
90
+ if (allowNone && supported.length === 0) {
91
+ for (const target of unavailable) {
92
+ const capability = APP_CAPABILITIES[target];
93
+ log(`Skipping ${capability.label}: the selected tenant has no ${capability.provider} models.`);
94
+ }
95
+ return supported;
96
+ }
97
+
90
98
  if (requestedTargets.length === 1 || supported.length === 0) {
91
99
  const target = unavailable[0];
92
100
  const capability = APP_CAPABILITIES[target];
@@ -297,9 +305,9 @@ export function openManagedLauncher(launcher, {
297
305
  return false;
298
306
  }
299
307
 
300
- async function fetchWindowsCatalog(config, io) {
308
+ async function fetchWindowsCatalog(config, io, { allowEmpty = false } = {}) {
301
309
  try {
302
- return await io.fetchModels(config);
310
+ return await io.fetchModels(config, undefined, { allowEmpty });
303
311
  } catch (error) {
304
312
  throw new Error(`tenant model catalog is unavailable (${redactSecretText(error.message)}); the Impel app profiles were not changed`);
305
313
  }
@@ -374,12 +382,25 @@ export async function reconcileWindowsTenantApps({
374
382
  ...overrides,
375
383
  };
376
384
  const config = explicitTenantAppConfig(baseConfig, tenant, targets);
377
- const catalog = await fetchWindowsCatalog(config, io);
378
- const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, { log: io.log });
385
+ const catalog = await fetchWindowsCatalog(config, io, { allowEmpty: true });
386
+ const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, {
387
+ log: io.log,
388
+ allowNone: true,
389
+ });
379
390
  const supported = new Set(actionTargets);
380
391
  const unsupported = targets.filter((target) => !supported.has(target));
381
392
  const userData = io.claudeUserData(environment, config.tenantId);
382
393
  const paths = appPaths(homeDir, config.tenantId, { claudeUserData: userData, tenantName: config.tenantName });
394
+ if (actionTargets.length === 0) {
395
+ return {
396
+ tenantId: config.tenantId,
397
+ targets: [],
398
+ unsupported,
399
+ paths,
400
+ verification: {},
401
+ failed: [],
402
+ };
403
+ }
383
404
  const vendorPaths = {};
384
405
  const preparedTargets = new Set(skipVendor ? actionTargets : skipVendorTargets);
385
406
 
@@ -501,13 +522,28 @@ export async function reconcileMacTenantApps({
501
522
  const config = explicitTenantAppConfig(baseConfig, tenant, targets);
502
523
  let catalog;
503
524
  try {
504
- catalog = await io.fetchModels(config);
525
+ catalog = await io.fetchModels(config, undefined, { allowEmpty: true });
505
526
  } catch (error) {
506
527
  throw new Error(`tenant model catalog is unavailable (${redactSecretText(error?.message || error)}); no Impel app files were changed`);
507
528
  }
508
- const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, { log: io.log });
529
+ const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, {
530
+ log: io.log,
531
+ allowNone: true,
532
+ });
509
533
  const supported = new Set(actionTargets);
510
534
  const unsupported = targets.filter((target) => !supported.has(target));
535
+ const paths = appPaths(homeDir, config.tenantId, { tenantName: config.tenantName });
536
+ if (actionTargets.length === 0) {
537
+ return {
538
+ tenantId: config.tenantId,
539
+ targets: [],
540
+ unsupported,
541
+ paths,
542
+ verification: {},
543
+ installed: [],
544
+ failed: [],
545
+ };
546
+ }
511
547
  const vendorPaths = {};
512
548
  const statuses = io.status(actionTargets, homeDir, config.tenantId, config.tenantName);
513
549
  for (const status of statuses) vendorPaths[status.target] ||= status.vendorPath;
@@ -549,7 +585,6 @@ export async function reconcileMacTenantApps({
549
585
  vendorPaths,
550
586
  writeBundles: staleTargets,
551
587
  }) : [];
552
- const paths = appPaths(homeDir, config.tenantId, { tenantName: config.tenantName });
553
588
  const agentItems = [];
554
589
  for (const item of installed) {
555
590
  const { client, env, label } = appSkillTarget(item.target, paths);
@@ -13,6 +13,8 @@ import { promptText } from "../prompt.js";
13
13
  import { describeCliFailure, preparePlatformClis } from "../platformSetup.js";
14
14
  import { restoreNativeProfiles } from "./use.js";
15
15
 
16
+ const RETRYABLE_TENANT_DISCOVERY_ERROR = /could not reach|request timed out|fetch failed|ECONNRESET|ECONNABORTED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|network error|socket/iu;
17
+
16
18
  function confirmed(answer) {
17
19
  return /^(?:y|yes)$/iu.test(String(answer || "").trim());
18
20
  }
@@ -44,6 +46,7 @@ export async function cmdConverge(argv = [], overrides = {}) {
44
46
  platform: process.platform,
45
47
  environment: process.env,
46
48
  preparePlatformClis,
49
+ discoveryLogger: console,
47
50
  ...overrides,
48
51
  };
49
52
  if (overrides.prepareWindowsClis && !overrides.preparePlatformClis) {
@@ -58,10 +61,22 @@ export async function cmdConverge(argv = [], overrides = {}) {
58
61
  let listing;
59
62
  try {
60
63
  listing = await io.fetchTenants(config);
64
+ // Tenant discovery is the entry gate for the whole convergence. A single
65
+ // transient timeout should not force a human to rerun every update step.
61
66
  } catch (error) {
62
- console.error(`impel update: tenant discovery failed (${redactSecretText(error?.message || error)})`);
63
- process.exitCode = 1;
64
- return false;
67
+ if (!RETRYABLE_TENANT_DISCOVERY_ERROR.test(String(error?.message || error))) {
68
+ console.error(`impel update: tenant discovery failed (${redactSecretText(error?.message || error)})`);
69
+ process.exitCode = 1;
70
+ return false;
71
+ }
72
+ io.discoveryLogger.log("Tenants: discovery request failed transiently; retrying once…");
73
+ try {
74
+ listing = await io.fetchTenants(config);
75
+ } catch (retryError) {
76
+ console.error(`impel update: tenant discovery failed (${redactSecretText(retryError?.message || retryError)})`);
77
+ process.exitCode = 1;
78
+ return false;
79
+ }
65
80
  }
66
81
  const selected = selectDefaultTenant(listing, { currentTenantId: config.tenantId });
67
82
  config.tenantId = selected.id;
@@ -0,0 +1,192 @@
1
+ import os from "node:os";
2
+
3
+ import {
4
+ cursorLaunchSpec,
5
+ ensureCursorManagedAuthentication,
6
+ ensureCursorManagedModels,
7
+ launchManagedCursor,
8
+ managedCursorStatus,
9
+ prepareManagedCursor,
10
+ reusePreparedManagedCursor,
11
+ } from "../cursorLocal.js";
12
+ import { fetchHttp1 } from "../http1.js";
13
+ import { loadConfig, normalizeGatewayUrl, redactSecretText, resolveDefaultGateway } from "../config.js";
14
+ import { assertProviderScopes, ensureTenantSelection, tenantCredential } from "../tenants.js";
15
+
16
+ export const CURSOR_EXPERIMENT_HELP = `impel experimental cursor prepare|open|status
17
+
18
+ Prepare, open, and inspect a tenant-isolated Cursor desktop runtime experiment.
19
+ Agent inference uses the selected tenant's Impel gateway and an isolated,
20
+ non-secret Cursor smoke identity. The signed stable Cursor.app in /Applications
21
+ keeps updating normally; Impel mirrors and revalidates each compatible update
22
+ without modifying the vendor app or the user's personal Cursor profile.
23
+
24
+ Usage:
25
+ impel experimental cursor prepare
26
+ impel experimental cursor open [workspace-or-Cursor-args...]
27
+ impel experimental cursor status`;
28
+
29
+ async function cursorGatewayCatalog(options, dependencies = {}) {
30
+ const fetchImpl = dependencies.fetch || fetchHttp1;
31
+ let response;
32
+ for (let attempt = 0; attempt < 2; attempt += 1) {
33
+ const controller = new AbortController();
34
+ const timeout = setTimeout(() => controller.abort(), 7_000);
35
+ try {
36
+ response = await fetchImpl(
37
+ `${options.gatewayUrl}/experimental/openai/v1/models`,
38
+ {
39
+ headers: {
40
+ accept: "application/json",
41
+ authorization: `Bearer ${options.credential}`,
42
+ },
43
+ signal: controller.signal,
44
+ },
45
+ );
46
+ break;
47
+ } catch (error) {
48
+ if (error?.name === "AbortError" && attempt === 0) continue;
49
+ const detail = error?.name === "AbortError" ? "request timed out" : error?.message || error;
50
+ throw new Error(`could not read the Cursor model catalog: ${redactSecretText(detail)}`);
51
+ } finally {
52
+ clearTimeout(timeout);
53
+ }
54
+ }
55
+ let payload;
56
+ try {
57
+ payload = await response.json();
58
+ } catch {
59
+ throw new Error(
60
+ `Cursor model catalog endpoint returned non-JSON (HTTP ${response.status}); deploy the matching impel-gateway Cursor catalog route`,
61
+ );
62
+ }
63
+ if (!response.ok) {
64
+ const detail = payload?.error?.message || payload?.error || `Cursor model catalog returned HTTP ${response.status}`;
65
+ throw new Error(redactSecretText(typeof detail === "string" ? detail : JSON.stringify(detail)));
66
+ }
67
+ if (payload?.org_id !== options.tenantId) {
68
+ throw new Error("Cursor model catalog tenant did not match the selected tenant");
69
+ }
70
+ if (!Array.isArray(payload.data) || payload.data.length === 0) {
71
+ throw new Error("Cursor model catalog returned no routable models");
72
+ }
73
+ const invalid = payload.data.find((model) => (
74
+ typeof model?.id !== "string"
75
+ || !Array.isArray(model.api_types)
76
+ || !model.api_types.includes("responses")
77
+ || model?.capabilities?.supports_tool_use !== true
78
+ || model?.capabilities?.supports_streaming !== true
79
+ || (
80
+ model?.capabilities?.supports_reasoning === true
81
+ && (
82
+ !Array.isArray(model.capabilities.reasoning_effort)
83
+ || model.capabilities.reasoning_effort.length === 0
84
+ || model.capabilities.reasoning_effort.some((effort) => typeof effort !== "string")
85
+ || (
86
+ model.capabilities.default_reasoning_effort !== undefined
87
+ && !model.capabilities.reasoning_effort.includes(model.capabilities.default_reasoning_effort)
88
+ )
89
+ )
90
+ )
91
+ ));
92
+ if (invalid) throw new Error("Cursor model catalog did not advertise the required Responses/tool contract");
93
+ return payload;
94
+ }
95
+
96
+ function statusLines(status, tenantId) {
97
+ const vendorLabel = status.vendor
98
+ ? `${status.vendor.version} (${status.vendor.commit.slice(0, 12)})`
99
+ : "incompatible";
100
+ const managedLabel = status.manifest?.vendor?.version || "not prepared";
101
+ return [
102
+ `Managed Cursor tenant: ${tenantId}`,
103
+ `Installed stable Cursor: ${vendorLabel}`,
104
+ `Managed runtime: ${managedLabel}`,
105
+ `Managed bundle contract: ${status.ready ? "ready" : "needs prepare"}`,
106
+ `Local Agent UI contract: ${status.ready ? "ready" : "needs prepare"}`,
107
+ ...(status.compatibilityError ? [`Compatibility: blocked — ${status.compatibilityError}`] : []),
108
+ ...(status.runtimeError ? [`Managed signature: invalid — ${status.runtimeError}`] : []),
109
+ ];
110
+ }
111
+
112
+ export async function cmdCursorExperimental(argv, dependencies = {}) {
113
+ const [action, ...args] = argv;
114
+ const log = dependencies.log || console.log;
115
+ if (action == null || action === "help") {
116
+ if (args.length) throw new Error("usage: impel experimental cursor prepare|open|status");
117
+ log(CURSOR_EXPERIMENT_HELP);
118
+ return;
119
+ }
120
+ if (!["prepare", "open", "status"].includes(action) || (action !== "open" && args.length)) {
121
+ throw new Error("usage: impel experimental cursor prepare|open|status");
122
+ }
123
+
124
+ const config = (dependencies.loadConfig || loadConfig)();
125
+ if (!config?.pat) throw new Error("run `impel setup` before preparing managed Cursor");
126
+ const selection = await (dependencies.ensureTenantSelection || ensureTenantSelection)(config, {
127
+ refresh: !Array.isArray(config.scopes),
128
+ });
129
+ assertProviderScopes(selection.scopes, ["claude", "codex"], { requireLive: true });
130
+ const tenantId = selection.tenantId;
131
+ const homeDir = dependencies.homeDir || os.homedir();
132
+ const gatewayUrl = normalizeGatewayUrl(config.gatewayUrl || resolveDefaultGateway());
133
+ const common = {
134
+ tenantId,
135
+ homeDir,
136
+ appRoot: dependencies.appRoot,
137
+ ...(dependencies.vendorPath ? { vendorPath: dependencies.vendorPath } : {}),
138
+ };
139
+ const credential = tenantCredential(config.pat, tenantId);
140
+
141
+ if (action === "status") {
142
+ const status = (dependencies.managedCursorStatus || managedCursorStatus)(common, dependencies);
143
+ for (const line of statusLines(status, tenantId)) log(line);
144
+ try {
145
+ const catalog = await cursorGatewayCatalog({ gatewayUrl, credential, tenantId }, dependencies);
146
+ log(`Gateway Cursor catalog: ready (${catalog.data.length} model${catalog.data.length === 1 ? "" : "s"})`);
147
+ return { ...status, gatewayReady: true, catalog };
148
+ } catch (error) {
149
+ const gatewayError = redactSecretText(error?.message || error);
150
+ log(`Gateway Cursor catalog: unavailable — ${gatewayError}`);
151
+ return { ...status, gatewayReady: false, gatewayError };
152
+ }
153
+ }
154
+
155
+ let prepared;
156
+ try {
157
+ prepared = (dependencies.prepareManagedCursor || prepareManagedCursor)(common, dependencies);
158
+ } catch (error) {
159
+ if (action !== "open") throw error;
160
+ prepared = (dependencies.reusePreparedManagedCursor || reusePreparedManagedCursor)(common, dependencies);
161
+ log(`Installed Cursor update was not imported; reusing the last verified managed runtime — ${redactSecretText(error?.message || error)}`);
162
+ }
163
+ const updateLabel = prepared.updated ? "Prepared" : "Reused";
164
+ log(`${updateLabel} managed Cursor ${prepared.source.version} for tenant ${tenantId}.`);
165
+ if (action === "prepare") return prepared;
166
+ const catalog = await cursorGatewayCatalog({ gatewayUrl, credential, tenantId }, dependencies);
167
+ const managedModels = (dependencies.ensureCursorManagedModels || ensureCursorManagedModels)(
168
+ prepared.paths.userData,
169
+ catalog.data,
170
+ `${gatewayUrl}/experimental/openai/v1`,
171
+ dependencies,
172
+ );
173
+ const authentication = (dependencies.ensureCursorManagedAuthentication || ensureCursorManagedAuthentication)(
174
+ prepared.paths.userData,
175
+ dependencies,
176
+ );
177
+ const spec = (dependencies.cursorLaunchSpec || cursorLaunchSpec)({
178
+ paths: prepared.paths,
179
+ gatewayUrl,
180
+ credential,
181
+ tenantId,
182
+ cursorAuthToken: authentication.token,
183
+ argv: args,
184
+ environment: dependencies.environment || process.env,
185
+ });
186
+ await (dependencies.launchManagedCursor || launchManagedCursor)(spec, dependencies);
187
+ log(`Opened managed Cursor for tenant ${tenantId} with ${catalog.data.length} gateway model${catalog.data.length === 1 ? "" : "s"}.`);
188
+ return { ...prepared, catalog, managedModels, launched: true };
189
+
190
+ }
191
+
192
+ export { cursorGatewayCatalog };
@@ -1,5 +1,6 @@
1
1
  import { crossAppModelsEnabled, loadConfig, saveConfig } from "../config.js";
2
2
  import { assertProviderScopes, ensureTenantSelection } from "../tenants.js";
3
+ import { cmdCursorExperimental, CURSOR_EXPERIMENT_HELP } from "./cursorExperimental.js";
3
4
 
4
5
  const HELP = `impel experimental cross-app-models enable|disable|status
5
6
 
@@ -10,7 +11,9 @@ It does not modify native profiles or support the consumer ChatGPT app
10
11
  (com.openai.chat).
11
12
 
12
13
  Run \`impel app refresh all\` to refresh desktop configs. Reopen an Impel app
13
- or isolated CLI to apply the experiment.`;
14
+ or isolated CLI to apply the experiment.
15
+
16
+ ${CURSOR_EXPERIMENT_HELP}`;
14
17
 
15
18
  export async function cmdExperimental(argv, dependencies = {}) {
16
19
  const read = dependencies.loadConfig || loadConfig;
@@ -23,8 +26,11 @@ export async function cmdExperimental(argv, dependencies = {}) {
23
26
  log(HELP);
24
27
  return;
25
28
  }
29
+ if (experiment === "cursor") {
30
+ return cmdCursorExperimental([action, ...extra], dependencies);
31
+ }
26
32
  if (experiment !== "cross-app-models" || !["enable", "disable", "status"].includes(action) || extra.length > 0) {
27
- throw new Error("usage: impel experimental cross-app-models enable|disable|status");
33
+ throw new Error("usage: impel experimental cross-app-models enable|disable|status | impel experimental cursor prepare|open|status");
28
34
  }
29
35
 
30
36
  const config = read();
@@ -20,8 +20,9 @@ import { assertProviderScopes, ensureTenantSelection, tenantCredential } from ".
20
20
  import { maybePrintUpdateNotice } from "../updates.js";
21
21
  import {
22
22
  nativeSpawnInvocation,
23
- resolveNativeBinary,
24
23
  } from "../nativeProcess.js";
24
+ import { resolveReviewedVendorCliBinary } from "../vendorCliBinaries.js";
25
+ import { PINNED_VENDOR_CLI_VERSIONS } from "../vendorCliVersions.js";
25
26
  import { RUNTIME_BRAND } from "../runtimeBrand.js";
26
27
  import {
27
28
  cacheProviderReadiness,
@@ -115,7 +116,15 @@ async function assertLiveProviderReadiness({ config, gatewayUrl, credential, ten
115
116
  }
116
117
 
117
118
  function runNativeCli(tool, argv, environment) {
118
- const binary = resolveNativeBinary(tool, environment);
119
+ const binary = resolveReviewedVendorCliBinary(tool, environment);
120
+ if (!binary) {
121
+ const version = PINNED_VENDOR_CLI_VERSIONS[tool];
122
+ console.error(
123
+ `impel ${tool}: reviewed ${tool === "claude" ? "Claude Code" : "Codex"} CLI `
124
+ + `v${version} is not installed. Run \`impel update\` to repair it.`,
125
+ );
126
+ return Promise.resolve(127);
127
+ }
119
128
  let invocation;
120
129
  try {
121
130
  invocation = nativeSpawnInvocation(binary, argv, environment);
@@ -4,8 +4,9 @@ import path from "node:path";
4
4
 
5
5
  import { appPaths, CLAUDE_CONFIG_ID, readTenantManifest } from "../apps.js";
6
6
  import { crossAppModelsEnabled, loadConfig, maskSecret } from "../config.js";
7
- import { environmentValue, findNativeBinary } from "../nativeProcess.js";
7
+ import { environmentValue } from "../nativeProcess.js";
8
8
  import { tenantCliClientReadiness } from "../provisioning.js";
9
+ import { findReviewedVendorCliBinary } from "../vendorCliBinaries.js";
9
10
  import { windowsTenantShortcutName } from "../shellEntries.js";
10
11
  import {
11
12
  ensureTenantSelection,
@@ -86,7 +87,7 @@ export async function cmdStatus(overrides = {}) {
86
87
  cliReadiness: tenantCliClientReadiness,
87
88
  desktopReadiness,
88
89
  installedVersion,
89
- findNativeBinary,
90
+ findNativeBinary: findReviewedVendorCliBinary,
90
91
  maybePrintUpdateNotice,
91
92
  platform: process.platform,
92
93
  environment: process.env,
@@ -18,6 +18,7 @@ import {
18
18
  isNewerVersion,
19
19
  refreshUpdateCache,
20
20
  updateInstallSpec,
21
+ updateTagForVersion,
21
22
  writeUpdateCache,
22
23
  } from "../updates.js";
23
24
 
@@ -200,30 +201,41 @@ export async function cmdUpdate(argv, overrides = {}) {
200
201
  }
201
202
 
202
203
  const current = io.installedVersion();
203
- const remote = await io.progress("Checking npm for impel-cli updates", () => io.fetchRemoteVersion());
204
+ const updateTag = updateTagForVersion(current);
205
+ const installSpec = updateInstallSpec(updateTag);
206
+ const remote = await io.progress(
207
+ "Checking npm for impel-cli updates",
208
+ () => io.fetchRemoteVersion({ tag: updateTag }),
209
+ );
204
210
  if (remote) io.writeCache({ remoteVersion: remote, checkedAt: Date.now() });
205
211
 
206
212
  console.log(`impel-cli v${current ?? "?"}`);
207
- console.log(`npm latest: ${remote ? `v${remote}` : "unknown — registry check failed"}`);
213
+ console.log(`npm ${updateTag}: ${remote ? `v${remote}` : "unknown — registry check failed"}`);
208
214
 
209
215
  const updateAvailable = Boolean(current && remote && isNewerVersion(remote, current));
210
216
  const upToDate = Boolean(current && remote && !updateAvailable);
211
217
  if (flags.check) {
212
- console.log(upToDate ? "Up to date." : "Update available: run `impel update`.");
213
- if (!remote) process.exitCode = 1;
218
+ if (!remote) {
219
+ console.log("Update status unavailable; the installed CLI was not changed.");
220
+ process.exitCode = 1;
221
+ } else {
222
+ console.log(upToDate ? "Up to date." : "Update available: run `impel update`.");
223
+ }
214
224
  return;
215
225
  }
216
226
 
217
227
  // ── CLI ────────────────────────────────────────────────────────────────
218
- if (upToDate) {
228
+ if (!remote) {
229
+ console.warn("CLI: npm update check unavailable; keeping the installed build.");
230
+ } else if (upToDate) {
219
231
  console.log("CLI: already up to date.");
220
232
  } else {
221
- console.log("CLI: installing the latest build…");
222
- if (!await io.progress("Installing the latest impel-cli build", () => io.selfUpdate(updateInstallSpec()))) {
233
+ console.log(`CLI: installing the npm ${updateTag} build…`);
234
+ if (!await io.progress(`Installing the npm ${updateTag} impel-cli build`, () => io.selfUpdate(installSpec))) {
223
235
  console.error("impel update: `npm install -g` failed; the CLI was not updated.");
224
236
  if (io.platform === "win32") {
225
237
  console.error(" Verify `npm --version` in PowerShell, then retry `impel update`.");
226
- console.error(" Manual recovery: `npm install --global impel-cli@latest`.");
238
+ console.error(` Manual recovery: \`npm install --global ${installSpec}\`.`);
227
239
  }
228
240
  const config = io.loadConfig();
229
241
  let recovered = false;
@@ -239,7 +251,7 @@ export async function cmdUpdate(argv, overrides = {}) {
239
251
  platform: io.platform,
240
252
  architecture: process.arch,
241
253
  step: "install.impel_cli",
242
- command: `npm install --global ${RUNTIME_BRAND.cli.packageName}@latest`,
254
+ command: `npm install --global ${installSpec}`,
243
255
  message: `The npm-verified global ${RUNTIME_BRAND.cli.packageName} update failed.`,
244
256
  },
245
257
  config,
@@ -265,7 +277,7 @@ export async function cmdUpdate(argv, overrides = {}) {
265
277
  }
266
278
  },
267
279
  retryStep: async () => {
268
- updateState.ok = io.selfUpdate(updateInstallSpec()) === true;
280
+ updateState.ok = io.selfUpdate(installSpec) === true;
269
281
  return updateState.ok;
270
282
  },
271
283
  },
@@ -294,7 +306,7 @@ export async function cmdUpdate(argv, overrides = {}) {
294
306
  console.error(`impel update: npm reported success but the running CLI still resolves v${postInstall ?? "?"} (expected v${remote}).`);
295
307
  console.error(` Running CLI: ${CLI_BIN}`);
296
308
  console.error(" This usually means the `impel` on PATH belongs to a different npm prefix than `npm prefix -g`.");
297
- console.error(" Fix: run `npm prefix -g`, confirm it owns the `impel` shim on PATH, then `npm install --global impel-cli@latest` there.");
309
+ console.error(` Fix: run \`npm prefix -g\`, confirm it owns the \`impel\` shim on PATH, then \`npm install --global ${installSpec}\` there.`);
298
310
  process.exitCode = 1;
299
311
  return;
300
312
  }