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.
- package/README.md +55 -0
- package/docs/experimental-managed-cursor.md +205 -0
- package/package.json +2 -1
- package/src/apps.js +320 -43
- package/src/commands/apps.js +43 -8
- package/src/commands/converge.js +18 -3
- package/src/commands/cursorExperimental.js +192 -0
- package/src/commands/experimental.js +8 -2
- package/src/commands/launch.js +11 -2
- package/src/commands/status.js +3 -2
- package/src/commands/update.js +23 -11
- package/src/cursorLocal.js +1554 -0
- package/src/macSetup.js +20 -38
- package/src/provisioning.js +10 -4
- package/src/skills.js +24 -8
- package/src/updates.js +57 -10
- package/src/vendorCliBinaries.js +121 -0
- package/src/vendorCliVersions.js +7 -0
- package/src/windowsApps.js +64 -19
- package/src/windowsSetup.js +7 -4
package/src/commands/apps.js
CHANGED
|
@@ -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, {
|
|
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, {
|
|
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);
|
package/src/commands/converge.js
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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();
|
package/src/commands/launch.js
CHANGED
|
@@ -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 =
|
|
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);
|
package/src/commands/status.js
CHANGED
|
@@ -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
|
|
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,
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
213
|
-
|
|
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 (
|
|
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(
|
|
222
|
-
if (!await io.progress(
|
|
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(
|
|
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 ${
|
|
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(
|
|
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(
|
|
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
|
}
|