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/macSetup.js
CHANGED
|
@@ -7,11 +7,12 @@ import { spawnSync } from "node:child_process";
|
|
|
7
7
|
import { ensureImpelClaudeProfile, ensureImpelCodexProfile } from "./cliProfiles.js";
|
|
8
8
|
import { findNativeBinary } from "./nativeProcess.js";
|
|
9
9
|
import { syncSkillsSafe } from "./skills.js";
|
|
10
|
+
import { verifyReviewedMacVendorCli } from "./vendorCliBinaries.js";
|
|
11
|
+
import { PINNED_VENDOR_CLI_VERSIONS } from "./vendorCliVersions.js";
|
|
10
12
|
|
|
11
13
|
const MAX_INSTALLER_BYTES = 512 * 1024;
|
|
12
14
|
const INSTALLER_DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
13
15
|
const INSTALLER_RUN_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
14
|
-
const MAC_SIGNING_TEAMS = Object.freeze({ claude: "Q6L2SF6YDW", codex: "2DC432GLL2" });
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* The script bytes are pinned per impel-cli release. Both vendor scripts then
|
|
@@ -19,59 +20,34 @@ const MAC_SIGNING_TEAMS = Object.freeze({ claude: "Q6L2SF6YDW", codex: "2DC432GL
|
|
|
19
20
|
*/
|
|
20
21
|
export const MAC_CLI_INSTALLERS = Object.freeze({
|
|
21
22
|
claude: Object.freeze({
|
|
23
|
+
version: PINNED_VENDOR_CLI_VERSIONS.claude,
|
|
22
24
|
url: "https://claude.ai/install.sh",
|
|
23
25
|
trustedOrigins: Object.freeze([
|
|
24
26
|
"https://claude.ai",
|
|
25
27
|
"https://downloads.claude.ai",
|
|
26
28
|
]),
|
|
27
29
|
sha256: "cde4f1702d3b1695f92b73d26888364e17bca476e17f0fd676484c951d36c125",
|
|
28
|
-
command:
|
|
29
|
-
args: Object.freeze([
|
|
30
|
+
command: `curl -fsSL https://claude.ai/install.sh | bash -s ${PINNED_VENDOR_CLI_VERSIONS.claude}`,
|
|
31
|
+
args: Object.freeze([PINNED_VENDOR_CLI_VERSIONS.claude]),
|
|
30
32
|
environment: Object.freeze({}),
|
|
31
33
|
}),
|
|
32
34
|
codex: Object.freeze({
|
|
35
|
+
version: PINNED_VENDOR_CLI_VERSIONS.codex,
|
|
33
36
|
url: "https://chatgpt.com/codex/install.sh",
|
|
34
37
|
trustedOrigins: Object.freeze([
|
|
35
38
|
"https://chatgpt.com",
|
|
36
39
|
"https://releases.openai.com",
|
|
37
40
|
]),
|
|
38
41
|
sha256: "ba92dd27e5c06f0d3bbc58bfa4b9cfb6599cd2742fbb1f92a2765e6c07dedb5a",
|
|
39
|
-
command:
|
|
42
|
+
command: `curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 CODEX_RELEASE=${PINNED_VENDOR_CLI_VERSIONS.codex} sh`,
|
|
40
43
|
args: Object.freeze([]),
|
|
41
|
-
environment: Object.freeze({
|
|
44
|
+
environment: Object.freeze({
|
|
45
|
+
CODEX_NON_INTERACTIVE: "1",
|
|
46
|
+
CODEX_RELEASE: PINNED_VENDOR_CLI_VERSIONS.codex,
|
|
47
|
+
}),
|
|
42
48
|
}),
|
|
43
49
|
});
|
|
44
50
|
|
|
45
|
-
function verifyMacCli(tool, binary, environment, run = spawnSync) {
|
|
46
|
-
try {
|
|
47
|
-
const result = run(binary, ["--version"], {
|
|
48
|
-
encoding: "utf8",
|
|
49
|
-
env: environment,
|
|
50
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
51
|
-
timeout: 15_000,
|
|
52
|
-
});
|
|
53
|
-
if (result?.status !== 0 || result?.error) return false;
|
|
54
|
-
const explicitOverride = tool === "claude" ? environment.IMPEL_CLAUDE_BIN : environment.IMPEL_CODEX_BIN;
|
|
55
|
-
if (explicitOverride && path.resolve(explicitOverride) === path.resolve(binary)) return true;
|
|
56
|
-
const resolved = fs.realpathSync(binary);
|
|
57
|
-
const verified = run("/usr/bin/codesign", ["--verify", "--strict", resolved], {
|
|
58
|
-
encoding: "utf8",
|
|
59
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
-
timeout: 15_000,
|
|
61
|
-
});
|
|
62
|
-
if (verified?.status !== 0 || verified?.error) return false;
|
|
63
|
-
const details = run("/usr/bin/codesign", ["-dv", "--verbose=4", resolved], {
|
|
64
|
-
encoding: "utf8",
|
|
65
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
66
|
-
timeout: 15_000,
|
|
67
|
-
});
|
|
68
|
-
return details?.status === 0
|
|
69
|
-
&& String(details.stderr || details.stdout || "").includes(`TeamIdentifier=${MAC_SIGNING_TEAMS[tool]}`);
|
|
70
|
-
} catch {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
51
|
function detectMacClis(find, environment, verify) {
|
|
76
52
|
const detect = (tool) => find(
|
|
77
53
|
tool,
|
|
@@ -200,7 +176,7 @@ export async function prepareMacClis({
|
|
|
200
176
|
const io = {
|
|
201
177
|
environment: process.env,
|
|
202
178
|
find: findNativeBinary,
|
|
203
|
-
verify:
|
|
179
|
+
verify: verifyReviewedMacVendorCli,
|
|
204
180
|
run: spawnSync,
|
|
205
181
|
fetchImpl: fetch,
|
|
206
182
|
fsImpl: fs,
|
|
@@ -252,7 +228,10 @@ export async function prepareMacClis({
|
|
|
252
228
|
await io.syncSkills({
|
|
253
229
|
client: "claude",
|
|
254
230
|
gatewayUrl,
|
|
255
|
-
env: {
|
|
231
|
+
env: {
|
|
232
|
+
CLAUDE_CONFIG_DIR: claudeProfile.configDir,
|
|
233
|
+
IMPEL_CLAUDE_BIN: binaries.claude,
|
|
234
|
+
},
|
|
256
235
|
label: "Impel isolated Claude (impel claude)",
|
|
257
236
|
// Lets Windows spawns use a git-safe cwd (see skillSyncSpawnDirectory).
|
|
258
237
|
homeDir: os.homedir(),
|
|
@@ -262,7 +241,10 @@ export async function prepareMacClis({
|
|
|
262
241
|
await io.syncSkills({
|
|
263
242
|
client: "codex",
|
|
264
243
|
gatewayUrl,
|
|
265
|
-
env: {
|
|
244
|
+
env: {
|
|
245
|
+
CODEX_HOME: codexProfile.codexHome,
|
|
246
|
+
IMPEL_CODEX_BIN: binaries.codex,
|
|
247
|
+
},
|
|
266
248
|
label: "Impel isolated Codex (impel codex)",
|
|
267
249
|
homeDir: os.homedir(),
|
|
268
250
|
});
|
package/src/provisioning.js
CHANGED
|
@@ -5,7 +5,6 @@ import path from "node:path";
|
|
|
5
5
|
import { syncAgentProfilesSafe } from "./agents.js";
|
|
6
6
|
import { crossAppModelsEnabled, redactSecretText } from "./config.js";
|
|
7
7
|
import { ensureImpelClaudeProfile, ensureImpelCodexProfile, tenantCliProfilePaths } from "./cliProfiles.js";
|
|
8
|
-
import { findNativeBinary } from "./nativeProcess.js";
|
|
9
8
|
import { mapWithConcurrency } from "./progress.js";
|
|
10
9
|
import { registerTenantShellEntries } from "./shellEntries.js";
|
|
11
10
|
import { resolveSkillsGateway, syncSkillsSafe } from "./skills.js";
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
} from "./tenants.js";
|
|
18
17
|
import { reconcileTenantApps } from "./commands/apps.js";
|
|
19
18
|
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
19
|
+
import { findReviewedVendorCliBinary } from "./vendorCliBinaries.js";
|
|
20
20
|
|
|
21
21
|
export function selectDefaultTenant(listing, { requested = null, currentTenantId = null } = {}) {
|
|
22
22
|
const normalized = requested ? normalizeTenantId(requested) : null;
|
|
@@ -93,13 +93,19 @@ async function prepareTenantCli(config, tenant, io, binaries) {
|
|
|
93
93
|
crossAppModels: crossAppModelsEnabled(config),
|
|
94
94
|
}),
|
|
95
95
|
root: (profile) => profile.configDir,
|
|
96
|
-
env: (profile) => ({
|
|
96
|
+
env: (profile) => ({
|
|
97
|
+
CLAUDE_CONFIG_DIR: profile.configDir,
|
|
98
|
+
IMPEL_CLAUDE_BIN: binaries.claude,
|
|
99
|
+
}),
|
|
97
100
|
},
|
|
98
101
|
codex: {
|
|
99
102
|
supported: providerSupported(config.scopes, PAT_SCOPE_CODEX),
|
|
100
103
|
ensure: () => io.ensureCodex(config.gatewayUrl, tenant.id),
|
|
101
104
|
root: (profile) => profile.codexHome,
|
|
102
|
-
env: (profile) => ({
|
|
105
|
+
env: (profile) => ({
|
|
106
|
+
CODEX_HOME: profile.codexHome,
|
|
107
|
+
IMPEL_CODEX_BIN: binaries.codex,
|
|
108
|
+
}),
|
|
103
109
|
},
|
|
104
110
|
};
|
|
105
111
|
const clients = {};
|
|
@@ -181,7 +187,7 @@ export async function reconcileAllTenants({
|
|
|
181
187
|
ensureCodex: ensureImpelCodexProfile,
|
|
182
188
|
syncSkills: syncSkillsSafe,
|
|
183
189
|
syncAgents: syncAgentProfilesSafe,
|
|
184
|
-
findBinary:
|
|
190
|
+
findBinary: findReviewedVendorCliBinary,
|
|
185
191
|
reconcileApps: reconcileTenantApps,
|
|
186
192
|
registerShellEntries: registerTenantShellEntries,
|
|
187
193
|
knownTenantIds: locallyKnownTenantIds,
|
package/src/skills.js
CHANGED
|
@@ -25,6 +25,8 @@ import path from "node:path";
|
|
|
25
25
|
|
|
26
26
|
import { resolveDefaultGateway, normalizeGatewayUrl } from "./config.js";
|
|
27
27
|
import { findNativeBinary, nativeCommandInvocation } from "./nativeProcess.js";
|
|
28
|
+
import { findReviewedVendorCliBinary } from "./vendorCliBinaries.js";
|
|
29
|
+
import { PINNED_VENDOR_CLI_VERSIONS } from "./vendorCliVersions.js";
|
|
28
30
|
|
|
29
31
|
/** The bundled plugin the Bifrost registry publishes; contains every served skill. */
|
|
30
32
|
export const SKILL_PLUGIN_NAME = "bifrost-all-skills";
|
|
@@ -499,6 +501,7 @@ export async function syncSkills({
|
|
|
499
501
|
platform = process.platform,
|
|
500
502
|
findGit = findNativeBinary,
|
|
501
503
|
homeDir = null,
|
|
504
|
+
findVendorBinary = findReviewedVendorCliBinary,
|
|
502
505
|
} = {}) {
|
|
503
506
|
const spec = CLIENT_SPECS[client];
|
|
504
507
|
if (!spec) {
|
|
@@ -512,6 +515,18 @@ export async function syncSkills({
|
|
|
512
515
|
return { client, label: displayLabel, skipped: true, reason: "disabled" };
|
|
513
516
|
}
|
|
514
517
|
|
|
518
|
+
let commandBinary = spec.bin;
|
|
519
|
+
if (platform === "darwin" && run === runSkillCommand) {
|
|
520
|
+
commandBinary = findVendorBinary(client, { ...process.env, ...env }, platform);
|
|
521
|
+
if (!commandBinary) {
|
|
522
|
+
logger.warn(
|
|
523
|
+
`impel: skipping skill sync for ${displayLabel} — reviewed ${spec.label} `
|
|
524
|
+
+ `v${PINNED_VENDOR_CLI_VERSIONS[client]} is not installed; run \`impel update\`.`,
|
|
525
|
+
);
|
|
526
|
+
return { client, label: displayLabel, skipped: true, reason: "binary-version-mismatch" };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
515
530
|
// Callers that manage real profiles pass their home directory so Windows
|
|
516
531
|
// spawns happen from a git-safe cwd (see skillSyncSpawnDirectory).
|
|
517
532
|
const spawnDirectory = skillSyncSpawnDirectory({ platform, homeDir });
|
|
@@ -521,9 +536,10 @@ export async function syncSkills({
|
|
|
521
536
|
|
|
522
537
|
try {
|
|
523
538
|
// Confirm the binary and its `plugin` subcommand exist before doing anything.
|
|
524
|
-
const help = await runCommand(
|
|
539
|
+
const help = await runCommand(commandBinary, ["plugin", "--help"], env);
|
|
525
540
|
if (help.missing) {
|
|
526
|
-
|
|
541
|
+
const location = commandBinary === spec.bin ? " on PATH" : "";
|
|
542
|
+
logger.warn(`impel: skipping skill sync for ${displayLabel} — \`${commandBinary}\` CLI not found${location}.`);
|
|
527
543
|
return { client, label: displayLabel, skipped: true, reason: "binary-missing" };
|
|
528
544
|
}
|
|
529
545
|
if (!help.ok) {
|
|
@@ -555,12 +571,12 @@ export async function syncSkills({
|
|
|
555
571
|
})[0];
|
|
556
572
|
const registerResult = await runSkillCommandWithRetry(
|
|
557
573
|
runCommand,
|
|
558
|
-
|
|
574
|
+
commandBinary,
|
|
559
575
|
registerCommand.args,
|
|
560
576
|
env,
|
|
561
577
|
);
|
|
562
578
|
if (registerResult.missing) {
|
|
563
|
-
failures.push({ phase: registerCommand.phase, reason: `\`${
|
|
579
|
+
failures.push({ phase: registerCommand.phase, reason: `\`${commandBinary}\` disappeared mid-sync` });
|
|
564
580
|
} else if (!isBenign(registerResult)) {
|
|
565
581
|
failures.push({
|
|
566
582
|
phase: registerCommand.phase,
|
|
@@ -573,7 +589,7 @@ export async function syncSkills({
|
|
|
573
589
|
// gives us the same dynamic name so refreshes can still use
|
|
574
590
|
// PLUGIN@MARKETPLACE.
|
|
575
591
|
if (!marketplaceName && !registerResult.missing) {
|
|
576
|
-
const listed = await runCommand(
|
|
592
|
+
const listed = await runCommand(commandBinary, ["plugin", "marketplace", "list", "--json"], env);
|
|
577
593
|
if (listed.ok) marketplaceName = resolveConfiguredMarketplaceName(listed.stdout, sourceUrl);
|
|
578
594
|
}
|
|
579
595
|
|
|
@@ -585,12 +601,12 @@ export async function syncSkills({
|
|
|
585
601
|
for (const command of commands) {
|
|
586
602
|
const result = await runSkillCommandWithRetry(
|
|
587
603
|
runCommand,
|
|
588
|
-
|
|
604
|
+
commandBinary,
|
|
589
605
|
command.args,
|
|
590
606
|
env,
|
|
591
607
|
);
|
|
592
608
|
if (result.missing) {
|
|
593
|
-
failures.push({ phase: command.phase, reason: `\`${
|
|
609
|
+
failures.push({ phase: command.phase, reason: `\`${commandBinary}\` disappeared mid-sync` });
|
|
594
610
|
break;
|
|
595
611
|
}
|
|
596
612
|
if (isBenign(result)) continue;
|
|
@@ -603,7 +619,7 @@ export async function syncSkills({
|
|
|
603
619
|
) {
|
|
604
620
|
const repair = await reregisterCodexMarketplace({
|
|
605
621
|
run: runCommand,
|
|
606
|
-
bin:
|
|
622
|
+
bin: commandBinary,
|
|
607
623
|
env,
|
|
608
624
|
marketplaceName,
|
|
609
625
|
sourceUrl,
|
package/src/updates.js
CHANGED
|
@@ -36,8 +36,19 @@ export function updateRegistry() {
|
|
|
36
36
|
).replace(/\/+$/u, "");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
function normalizeUpdateTag(tag) {
|
|
40
|
+
if (tag === "latest" || tag === "next") return tag;
|
|
41
|
+
throw new Error(`unsupported npm update tag "${tag}"`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Keep prerelease installs on npm's prerelease channel. */
|
|
45
|
+
export function updateTagForVersion(version) {
|
|
46
|
+
const parsed = validVersion(version) ? version.match(VERSION_RE) : null;
|
|
47
|
+
return parsed?.[4] ? "next" : "latest";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function updateInstallSpec(tag = "latest") {
|
|
51
|
+
return `${updatePackage()}@${normalizeUpdateTag(tag)}`;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
export function installedVersion() {
|
|
@@ -88,15 +99,17 @@ export function isNewerVersion(candidate, current) {
|
|
|
88
99
|
function fetchRemoteVersionWithNpm({
|
|
89
100
|
packageName,
|
|
90
101
|
registry,
|
|
102
|
+
tag,
|
|
91
103
|
timeoutMs,
|
|
92
104
|
spawnSyncImpl = spawnSync,
|
|
93
105
|
environment = process.env,
|
|
94
106
|
platform = process.platform,
|
|
95
107
|
}) {
|
|
96
108
|
try {
|
|
109
|
+
const target = tag === "latest" ? packageName : `${packageName}@${tag}`;
|
|
97
110
|
const invocation = nativeCommandInvocation(
|
|
98
111
|
"npm",
|
|
99
|
-
["view",
|
|
112
|
+
["view", target, "version", "--json", "--registry", registry],
|
|
100
113
|
environment,
|
|
101
114
|
platform,
|
|
102
115
|
);
|
|
@@ -122,39 +135,71 @@ function fetchRemoteVersionWithNpm({
|
|
|
122
135
|
export async function fetchRemoteVersion({
|
|
123
136
|
packageName = updatePackage(),
|
|
124
137
|
registry = updateRegistry(),
|
|
138
|
+
tag = "latest",
|
|
125
139
|
timeoutMs = 10_000,
|
|
126
140
|
fetchImpl = null,
|
|
141
|
+
fallbackToNpm = fetchImpl === null,
|
|
127
142
|
spawnSyncImpl = spawnSync,
|
|
128
143
|
environment = process.env,
|
|
129
144
|
platform = process.platform,
|
|
130
145
|
} = {}) {
|
|
146
|
+
const normalizedTag = normalizeUpdateTag(tag);
|
|
131
147
|
const normalizedRegistry = String(registry).replace(/\/+$/u, "");
|
|
132
148
|
if (!fetchImpl && normalizedRegistry !== DEFAULT_UPDATE_REGISTRY) {
|
|
133
149
|
return fetchRemoteVersionWithNpm({
|
|
134
150
|
packageName,
|
|
135
151
|
registry: normalizedRegistry,
|
|
152
|
+
tag: normalizedTag,
|
|
153
|
+
timeoutMs,
|
|
154
|
+
spawnSyncImpl,
|
|
155
|
+
environment,
|
|
156
|
+
platform,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// Windows npm commonly has proxy, certificate, or registry settings that
|
|
160
|
+
// Node's bare fetch does not inherit. Prefer the already-working npm client
|
|
161
|
+
// there, then retain the direct registry request as a fallback.
|
|
162
|
+
if (!fetchImpl && platform === "win32") {
|
|
163
|
+
const npmVersion = fetchRemoteVersionWithNpm({
|
|
164
|
+
packageName,
|
|
165
|
+
registry: normalizedRegistry,
|
|
166
|
+
tag: normalizedTag,
|
|
136
167
|
timeoutMs,
|
|
137
168
|
spawnSyncImpl,
|
|
138
169
|
environment,
|
|
139
170
|
platform,
|
|
140
171
|
});
|
|
172
|
+
if (npmVersion) return npmVersion;
|
|
141
173
|
}
|
|
142
174
|
const controller = new AbortController();
|
|
143
175
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
176
|
+
let remoteVersion = null;
|
|
144
177
|
try {
|
|
145
178
|
const encodedName = encodeURIComponent(packageName);
|
|
146
|
-
const response = await (fetchImpl || globalThis.fetch)(`${normalizedRegistry}/${encodedName}
|
|
179
|
+
const response = await (fetchImpl || globalThis.fetch)(`${normalizedRegistry}/${encodedName}/${normalizedTag}`, {
|
|
147
180
|
headers: { accept: "application/json" },
|
|
148
181
|
signal: controller.signal,
|
|
149
182
|
});
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
183
|
+
if (response.ok) {
|
|
184
|
+
const metadata = await response.json();
|
|
185
|
+
remoteVersion = validVersion(metadata?.version) ? metadata.version : null;
|
|
186
|
+
}
|
|
153
187
|
} catch {
|
|
154
|
-
|
|
188
|
+
// npm is the authoritative fallback below. It also honors npm's proxy and
|
|
189
|
+
// registry configuration, which is especially important on Windows.
|
|
155
190
|
} finally {
|
|
156
191
|
clearTimeout(timeout);
|
|
157
192
|
}
|
|
193
|
+
if (remoteVersion || !fallbackToNpm) return remoteVersion;
|
|
194
|
+
return fetchRemoteVersionWithNpm({
|
|
195
|
+
packageName,
|
|
196
|
+
registry: normalizedRegistry,
|
|
197
|
+
tag: normalizedTag,
|
|
198
|
+
timeoutMs,
|
|
199
|
+
spawnSyncImpl,
|
|
200
|
+
environment,
|
|
201
|
+
platform,
|
|
202
|
+
});
|
|
158
203
|
}
|
|
159
204
|
|
|
160
205
|
export function readUpdateCache() {
|
|
@@ -198,7 +243,9 @@ export function cacheIsFresh(cache, now = Date.now()) {
|
|
|
198
243
|
export async function refreshUpdateCache(dependencies = {}) {
|
|
199
244
|
const fetchLatest = dependencies.fetchRemoteVersion || fetchRemoteVersion;
|
|
200
245
|
const writeCache = dependencies.writeCache || writeUpdateCache;
|
|
201
|
-
const
|
|
246
|
+
const currentVersion = (dependencies.installedVersion || installedVersion)();
|
|
247
|
+
const tag = updateTagForVersion(currentVersion);
|
|
248
|
+
const remoteVersion = await fetchLatest({ tag });
|
|
202
249
|
if (!remoteVersion) {
|
|
203
250
|
try {
|
|
204
251
|
writeCache({ lastFailedAt: Date.now() });
|
|
@@ -207,7 +254,7 @@ export async function refreshUpdateCache(dependencies = {}) {
|
|
|
207
254
|
}
|
|
208
255
|
return null;
|
|
209
256
|
}
|
|
210
|
-
return writeCache({ remoteVersion, checkedAt: Date.now() });
|
|
257
|
+
return writeCache({ remoteVersion, updateTag: tag, checkedAt: Date.now() });
|
|
211
258
|
}
|
|
212
259
|
|
|
213
260
|
/** One-line human notice when npm has a newer stable package. */
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
environmentValue,
|
|
7
|
+
findNativeBinary,
|
|
8
|
+
resolveNativeBinary,
|
|
9
|
+
} from "./nativeProcess.js";
|
|
10
|
+
import { PINNED_VENDOR_CLI_VERSIONS } from "./vendorCliVersions.js";
|
|
11
|
+
|
|
12
|
+
export const MAC_VENDOR_SIGNING_TEAMS = Object.freeze({
|
|
13
|
+
claude: "Q6L2SF6YDW",
|
|
14
|
+
codex: "2DC432GLL2",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const VERSION_RE = /\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/u;
|
|
18
|
+
|
|
19
|
+
function vendorOverrideName(tool, environmentPrefix = "IMPEL") {
|
|
20
|
+
if (tool === "claude") return `${environmentPrefix}_CLAUDE_BIN`;
|
|
21
|
+
if (tool === "codex") return `${environmentPrefix}_CODEX_BIN`;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Extract the vendor release from `claude --version` or `codex --version`. */
|
|
26
|
+
export function parseVendorCliVersion(output) {
|
|
27
|
+
return String(output || "").match(VERSION_RE)?.[0] || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Accept only the exact reviewed macOS release and its vendor signature.
|
|
32
|
+
* Explicit binary overrides remain authoritative for development and recovery.
|
|
33
|
+
*/
|
|
34
|
+
export function verifyReviewedMacVendorCli(
|
|
35
|
+
tool,
|
|
36
|
+
binary,
|
|
37
|
+
environment = process.env,
|
|
38
|
+
{
|
|
39
|
+
run = spawnSync,
|
|
40
|
+
realpath = fs.realpathSync,
|
|
41
|
+
versions = PINNED_VENDOR_CLI_VERSIONS,
|
|
42
|
+
signingTeams = MAC_VENDOR_SIGNING_TEAMS,
|
|
43
|
+
environmentPrefix = "IMPEL",
|
|
44
|
+
} = {},
|
|
45
|
+
) {
|
|
46
|
+
if (!versions[tool] || !signingTeams[tool]) return false;
|
|
47
|
+
try {
|
|
48
|
+
const override = vendorOverrideName(tool, environmentPrefix);
|
|
49
|
+
const overriddenBinary = override ? environmentValue(environment, override) : null;
|
|
50
|
+
if (overriddenBinary && path.resolve(overriddenBinary) === path.resolve(binary)) return true;
|
|
51
|
+
|
|
52
|
+
const result = run(binary, ["--version"], {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
env: environment,
|
|
55
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
56
|
+
timeout: 15_000,
|
|
57
|
+
});
|
|
58
|
+
if (result?.status !== 0 || result?.error) return false;
|
|
59
|
+
|
|
60
|
+
const version = parseVendorCliVersion(`${result.stdout || ""}\n${result.stderr || ""}`);
|
|
61
|
+
if (version !== versions[tool]) return false;
|
|
62
|
+
|
|
63
|
+
const resolved = realpath(binary);
|
|
64
|
+
const verified = run("/usr/bin/codesign", ["--verify", "--strict", resolved], {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
67
|
+
timeout: 15_000,
|
|
68
|
+
});
|
|
69
|
+
if (verified?.status !== 0 || verified?.error) return false;
|
|
70
|
+
const details = run("/usr/bin/codesign", ["-dv", "--verbose=4", resolved], {
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
73
|
+
timeout: 15_000,
|
|
74
|
+
});
|
|
75
|
+
return details?.status === 0
|
|
76
|
+
&& String(details.stderr || details.stdout || "").includes(`TeamIdentifier=${signingTeams[tool]}`);
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Locate the reviewed macOS vendor CLI. On other platforms this intentionally
|
|
84
|
+
* preserves the existing native resolution behavior.
|
|
85
|
+
*/
|
|
86
|
+
export function findReviewedVendorCliBinary(
|
|
87
|
+
tool,
|
|
88
|
+
environment = process.env,
|
|
89
|
+
platform = process.platform,
|
|
90
|
+
environmentPrefix = "IMPEL",
|
|
91
|
+
{
|
|
92
|
+
find = findNativeBinary,
|
|
93
|
+
verify = verifyReviewedMacVendorCli,
|
|
94
|
+
} = {},
|
|
95
|
+
) {
|
|
96
|
+
if (platform !== "darwin") return find(tool, environment, platform, environmentPrefix);
|
|
97
|
+
return find(
|
|
98
|
+
tool,
|
|
99
|
+
environment,
|
|
100
|
+
platform,
|
|
101
|
+
environmentPrefix,
|
|
102
|
+
(binary) => verify(tool, binary, environment, { environmentPrefix }),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve a launch target. macOS deliberately returns null when only an
|
|
108
|
+
* unreviewed or version-drifted binary exists so callers can request repair
|
|
109
|
+
* instead of silently launching it.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveReviewedVendorCliBinary(
|
|
112
|
+
tool,
|
|
113
|
+
environment = process.env,
|
|
114
|
+
platform = process.platform,
|
|
115
|
+
environmentPrefix = "IMPEL",
|
|
116
|
+
) {
|
|
117
|
+
if (platform !== "darwin") {
|
|
118
|
+
return resolveNativeBinary(tool, environment, platform, environmentPrefix);
|
|
119
|
+
}
|
|
120
|
+
return findReviewedVendorCliBinary(tool, environment, platform, environmentPrefix);
|
|
121
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Exact vendor CLI releases reviewed for this impel-cli build. Keep macOS and
|
|
2
|
+
// Windows installers on the same versions; do not replace these with moving
|
|
3
|
+
// channels such as "stable" or "latest".
|
|
4
|
+
export const PINNED_VENDOR_CLI_VERSIONS = Object.freeze({
|
|
5
|
+
claude: "2.1.212",
|
|
6
|
+
codex: "0.146.0",
|
|
7
|
+
});
|