impel-cli 0.18.15-beta.1 → 0.18.15-beta.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.15-beta.1",
3
+ "version": "0.18.15-beta.2",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,8 +20,9 @@ import { assertProviderScopes, ensureTenantSelection, tenantCredential } from ".
20
20
  import { maybePrintUpdateNotice } from "../updates.js";
21
21
  import {
22
22
  nativeSpawnInvocation,
23
- resolveNativeBinary,
24
23
  } from "../nativeProcess.js";
24
+ import { resolveReviewedVendorCliBinary } from "../vendorCliBinaries.js";
25
+ import { PINNED_VENDOR_CLI_VERSIONS } from "../vendorCliVersions.js";
25
26
  import { RUNTIME_BRAND } from "../runtimeBrand.js";
26
27
  import {
27
28
  cacheProviderReadiness,
@@ -115,7 +116,15 @@ async function assertLiveProviderReadiness({ config, gatewayUrl, credential, ten
115
116
  }
116
117
 
117
118
  function runNativeCli(tool, argv, environment) {
118
- const binary = resolveNativeBinary(tool, environment);
119
+ const binary = resolveReviewedVendorCliBinary(tool, environment);
120
+ if (!binary) {
121
+ const version = PINNED_VENDOR_CLI_VERSIONS[tool];
122
+ console.error(
123
+ `impel ${tool}: reviewed ${tool === "claude" ? "Claude Code" : "Codex"} CLI `
124
+ + `v${version} is not installed. Run \`impel update\` to repair it.`,
125
+ );
126
+ return Promise.resolve(127);
127
+ }
119
128
  let invocation;
120
129
  try {
121
130
  invocation = nativeSpawnInvocation(binary, argv, environment);
@@ -4,8 +4,9 @@ import path from "node:path";
4
4
 
5
5
  import { appPaths, CLAUDE_CONFIG_ID, readTenantManifest } from "../apps.js";
6
6
  import { crossAppModelsEnabled, loadConfig, maskSecret } from "../config.js";
7
- import { environmentValue, findNativeBinary } from "../nativeProcess.js";
7
+ import { environmentValue } from "../nativeProcess.js";
8
8
  import { tenantCliClientReadiness } from "../provisioning.js";
9
+ import { findReviewedVendorCliBinary } from "../vendorCliBinaries.js";
9
10
  import { windowsTenantShortcutName } from "../shellEntries.js";
10
11
  import {
11
12
  ensureTenantSelection,
@@ -86,7 +87,7 @@ export async function cmdStatus(overrides = {}) {
86
87
  cliReadiness: tenantCliClientReadiness,
87
88
  desktopReadiness,
88
89
  installedVersion,
89
- findNativeBinary,
90
+ findNativeBinary: findReviewedVendorCliBinary,
90
91
  maybePrintUpdateNotice,
91
92
  platform: process.platform,
92
93
  environment: process.env,
package/src/macSetup.js CHANGED
@@ -7,12 +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";
10
11
  import { PINNED_VENDOR_CLI_VERSIONS } from "./vendorCliVersions.js";
11
12
 
12
13
  const MAX_INSTALLER_BYTES = 512 * 1024;
13
14
  const INSTALLER_DOWNLOAD_TIMEOUT_MS = 30_000;
14
15
  const INSTALLER_RUN_TIMEOUT_MS = 10 * 60 * 1_000;
15
- const MAC_SIGNING_TEAMS = Object.freeze({ claude: "Q6L2SF6YDW", codex: "2DC432GLL2" });
16
16
 
17
17
  /**
18
18
  * The script bytes are pinned per impel-cli release. Both vendor scripts then
@@ -48,36 +48,6 @@ export const MAC_CLI_INSTALLERS = Object.freeze({
48
48
  }),
49
49
  });
50
50
 
51
- function verifyMacCli(tool, binary, environment, run = spawnSync) {
52
- try {
53
- const result = run(binary, ["--version"], {
54
- encoding: "utf8",
55
- env: environment,
56
- stdio: ["ignore", "pipe", "pipe"],
57
- timeout: 15_000,
58
- });
59
- if (result?.status !== 0 || result?.error) return false;
60
- const explicitOverride = tool === "claude" ? environment.IMPEL_CLAUDE_BIN : environment.IMPEL_CODEX_BIN;
61
- if (explicitOverride && path.resolve(explicitOverride) === path.resolve(binary)) return true;
62
- const resolved = fs.realpathSync(binary);
63
- const verified = run("/usr/bin/codesign", ["--verify", "--strict", resolved], {
64
- encoding: "utf8",
65
- stdio: ["ignore", "pipe", "pipe"],
66
- timeout: 15_000,
67
- });
68
- if (verified?.status !== 0 || verified?.error) return false;
69
- const details = run("/usr/bin/codesign", ["-dv", "--verbose=4", resolved], {
70
- encoding: "utf8",
71
- stdio: ["ignore", "pipe", "pipe"],
72
- timeout: 15_000,
73
- });
74
- return details?.status === 0
75
- && String(details.stderr || details.stdout || "").includes(`TeamIdentifier=${MAC_SIGNING_TEAMS[tool]}`);
76
- } catch {
77
- return false;
78
- }
79
- }
80
-
81
51
  function detectMacClis(find, environment, verify) {
82
52
  const detect = (tool) => find(
83
53
  tool,
@@ -206,7 +176,7 @@ export async function prepareMacClis({
206
176
  const io = {
207
177
  environment: process.env,
208
178
  find: findNativeBinary,
209
- verify: verifyMacCli,
179
+ verify: verifyReviewedMacVendorCli,
210
180
  run: spawnSync,
211
181
  fetchImpl: fetch,
212
182
  fsImpl: fs,
@@ -258,7 +228,10 @@ export async function prepareMacClis({
258
228
  await io.syncSkills({
259
229
  client: "claude",
260
230
  gatewayUrl,
261
- env: { CLAUDE_CONFIG_DIR: claudeProfile.configDir },
231
+ env: {
232
+ CLAUDE_CONFIG_DIR: claudeProfile.configDir,
233
+ IMPEL_CLAUDE_BIN: binaries.claude,
234
+ },
262
235
  label: "Impel isolated Claude (impel claude)",
263
236
  // Lets Windows spawns use a git-safe cwd (see skillSyncSpawnDirectory).
264
237
  homeDir: os.homedir(),
@@ -268,7 +241,10 @@ export async function prepareMacClis({
268
241
  await io.syncSkills({
269
242
  client: "codex",
270
243
  gatewayUrl,
271
- env: { CODEX_HOME: codexProfile.codexHome },
244
+ env: {
245
+ CODEX_HOME: codexProfile.codexHome,
246
+ IMPEL_CODEX_BIN: binaries.codex,
247
+ },
272
248
  label: "Impel isolated Codex (impel codex)",
273
249
  homeDir: os.homedir(),
274
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,
@@ -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
+ }