impel-cli 0.18.15-beta.1 → 0.18.15-beta.3
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 +1 -1
- package/src/apps.js +15 -2
- package/src/commands/apps.js +43 -8
- package/src/commands/launch.js +11 -2
- package/src/commands/status.js +3 -2
- package/src/macSetup.js +10 -34
- package/src/provisioning.js +10 -4
- package/src/skills.js +24 -8
- package/src/vendorCliBinaries.js +121 -0
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -679,7 +679,7 @@ export function migrateLegacyClaudeAppSessions(userData, homeDir = os.homedir())
|
|
|
679
679
|
return copied;
|
|
680
680
|
}
|
|
681
681
|
|
|
682
|
-
export async function fetchGatewayModels(config, fetchImpl = fetch) {
|
|
682
|
+
export async function fetchGatewayModels(config, fetchImpl = fetch, { allowEmpty = false } = {}) {
|
|
683
683
|
const controller = new AbortController();
|
|
684
684
|
const timeout = setTimeout(() => controller.abort(), 20000);
|
|
685
685
|
try {
|
|
@@ -697,7 +697,9 @@ export async function fetchGatewayModels(config, fetchImpl = fetch) {
|
|
|
697
697
|
}
|
|
698
698
|
if (!Array.isArray(payload?.data)) throw new Error("response has no model data array");
|
|
699
699
|
const models = payload.data.filter(isGatewayModel);
|
|
700
|
-
if (models.length === 0
|
|
700
|
+
if (models.length === 0 && !(allowEmpty && isExplicitNoSeatCatalog(payload))) {
|
|
701
|
+
throw new Error("gateway returned no supported models");
|
|
702
|
+
}
|
|
701
703
|
return { models, source: "gateway", version: payload.version ?? null };
|
|
702
704
|
} finally {
|
|
703
705
|
clearTimeout(timeout);
|
|
@@ -2547,6 +2549,17 @@ function isGatewayModel(model) {
|
|
|
2547
2549
|
return model && typeof model.id === "string" && (model.provider === "claude" || model.provider === "codex");
|
|
2548
2550
|
}
|
|
2549
2551
|
|
|
2552
|
+
function isExplicitNoSeatCatalog(payload) {
|
|
2553
|
+
const statuses = payload?.provider_status;
|
|
2554
|
+
return Boolean(statuses
|
|
2555
|
+
&& typeof statuses === "object"
|
|
2556
|
+
&& !Array.isArray(statuses)
|
|
2557
|
+
&& ["claude", "codex"].every((provider) => (
|
|
2558
|
+
statuses[provider]?.state === "no_seat"
|
|
2559
|
+
&& statuses[provider]?.routable === false
|
|
2560
|
+
)));
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2550
2563
|
function writeAtomic(target, contents, mode) {
|
|
2551
2564
|
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
2552
2565
|
if (fs.existsSync(target) && fs.readFileSync(target, "utf8") === contents) {
|
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/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/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:
|
|
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: {
|
|
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: {
|
|
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
|
});
|
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,
|
|
@@ -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
|
+
}
|