impel-cli 0.18.15 → 0.18.16
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 +213 -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
|
@@ -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
|
}
|