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/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: "curl -fsSL https://claude.ai/install.sh | bash -s stable",
29
- args: Object.freeze(["stable"]),
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: "curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 CODEX_RELEASE=0.144.6 sh",
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({ CODEX_NON_INTERACTIVE: "1", CODEX_RELEASE: "0.144.6" }),
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: verifyMacCli,
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: { CLAUDE_CONFIG_DIR: claudeProfile.configDir },
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: { CODEX_HOME: codexProfile.codexHome },
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
  });
@@ -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) => ({ CLAUDE_CONFIG_DIR: profile.configDir }),
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) => ({ CODEX_HOME: profile.codexHome }),
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: findNativeBinary,
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(spec.bin, ["plugin", "--help"], env);
539
+ const help = await runCommand(commandBinary, ["plugin", "--help"], env);
525
540
  if (help.missing) {
526
- logger.warn(`impel: skipping skill sync for ${displayLabel} — \`${spec.bin}\` CLI not found on PATH.`);
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
- spec.bin,
574
+ commandBinary,
559
575
  registerCommand.args,
560
576
  env,
561
577
  );
562
578
  if (registerResult.missing) {
563
- failures.push({ phase: registerCommand.phase, reason: `\`${spec.bin}\` disappeared mid-sync` });
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(spec.bin, ["plugin", "marketplace", "list", "--json"], env);
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
- spec.bin,
604
+ commandBinary,
589
605
  command.args,
590
606
  env,
591
607
  );
592
608
  if (result.missing) {
593
- failures.push({ phase: command.phase, reason: `\`${spec.bin}\` disappeared mid-sync` });
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: spec.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
- export function updateInstallSpec() {
40
- return `${updatePackage()}@latest`;
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", packageName, "version", "--json", "--registry", registry],
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}/latest`, {
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 (!response.ok) return null;
151
- const metadata = await response.json();
152
- return validVersion(metadata?.version) ? metadata.version : null;
183
+ if (response.ok) {
184
+ const metadata = await response.json();
185
+ remoteVersion = validVersion(metadata?.version) ? metadata.version : null;
186
+ }
153
187
  } catch {
154
- return null;
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 remoteVersion = await fetchLatest();
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
+ });