impel-cli 0.15.0 → 0.15.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/README.md +4 -3
- package/package.json +1 -1
- package/src/apps.js +16 -1
- package/src/commands/apps.js +126 -40
- package/src/commands/setup.js +2 -2
- package/src/installRecovery/loop.js +1 -1
- package/src/skills.js +46 -2
package/README.md
CHANGED
|
@@ -118,9 +118,10 @@ isolated command-line workflow.
|
|
|
118
118
|
If setup or the CLI self-update fails, Impel first runs a deterministic local
|
|
119
119
|
check. In an interactive terminal it then offers assisted recovery; use
|
|
120
120
|
`impel setup --repair` (or `impel update --repair`) to opt in explicitly. Before
|
|
121
|
-
upload, the CLI prints the exact sanitized payload. The hosted agent
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
upload, the CLI prints the exact sanitized payload. The hosted agent returns a
|
|
122
|
+
reviewed action ID, and the CLI runs its allowlisted command locally; free-form
|
|
123
|
+
model commands are rejected and every mutation requires confirmation. Sessions
|
|
124
|
+
expire after 15 minutes, mutation capabilities are
|
|
124
125
|
single-use, and a private checkpoint lets `--repair` resume an interrupted run.
|
|
125
126
|
Use `--no-recovery` or `IMPEL_DISABLE_INSTALL_RECOVERY=1` to keep recovery off.
|
|
126
127
|
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -114,6 +114,16 @@ export function normalizeAppTarget(value) {
|
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** Return only app targets backed by at least one live tenant model. */
|
|
118
|
+
export function appTargetsSupportedByModels(targets, models, config = {}) {
|
|
119
|
+
if (!Array.isArray(models) || models.length === 0) return [];
|
|
120
|
+
if (crossAppModelsEnabled(config)) return [...targets];
|
|
121
|
+
const providers = new Set(models.map((model) => model?.provider));
|
|
122
|
+
return targets.filter((target) => providers.has(
|
|
123
|
+
target === "claude" ? "claude" : "codex",
|
|
124
|
+
));
|
|
125
|
+
}
|
|
126
|
+
|
|
117
127
|
function tenantAppName(value, fallback) {
|
|
118
128
|
const name = redactSecretText(String(value || fallback))
|
|
119
129
|
.normalize("NFC")
|
|
@@ -151,7 +161,7 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
151
161
|
|
|
152
162
|
// Bump when the written config/manifest schema changes; a mismatch forces the
|
|
153
163
|
// slow open path (and thus a full config rewrite) after a CLI update.
|
|
154
|
-
export const CURRENT_CONFIG_VERSION =
|
|
164
|
+
export const CURRENT_CONFIG_VERSION = 11;
|
|
155
165
|
|
|
156
166
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
157
167
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -604,6 +614,11 @@ export function installManagedAppFiles({
|
|
|
604
614
|
tenantName: config.tenantName || config.tenantId || null,
|
|
605
615
|
experiments: { crossAppModels: crossAppModelsEnabled(config) },
|
|
606
616
|
targets,
|
|
617
|
+
availableTargets: appTargetsSupportedByModels(
|
|
618
|
+
["claude", "chatgpt"],
|
|
619
|
+
models,
|
|
620
|
+
config,
|
|
621
|
+
),
|
|
607
622
|
models: models.map((model) => model.id),
|
|
608
623
|
updatedAt: new Date().toISOString(),
|
|
609
624
|
}, null, 2) + "\n", 0o600);
|
package/src/commands/apps.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import {
|
|
26
26
|
CURRENT_CONFIG_VERSION,
|
|
27
27
|
CLAUDE_CONFIG_ID,
|
|
28
|
+
appTargetsSupportedByModels,
|
|
28
29
|
appPaths,
|
|
29
30
|
appStatus,
|
|
30
31
|
bundleIsCurrent,
|
|
@@ -58,6 +59,45 @@ import {
|
|
|
58
59
|
|
|
59
60
|
const CLAUDE_KEYCHAIN_NOTICE = "Claude Keychain: enter your Mac login password and choose Always Allow on the first prompt for this tenant; Allow is temporary.";
|
|
60
61
|
|
|
62
|
+
const APP_CAPABILITIES = Object.freeze({
|
|
63
|
+
claude: Object.freeze({ label: "Impel Claude", provider: "Claude" }),
|
|
64
|
+
chatgpt: Object.freeze({ label: "Impel ChatGPT", provider: "Codex" }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Select targets the live tenant catalog can actually run. An explicit target
|
|
69
|
+
* remains fail-closed; `all` degrades to the available app with a clear notice.
|
|
70
|
+
*/
|
|
71
|
+
export function selectCatalogAppTargets(
|
|
72
|
+
requestedTargets,
|
|
73
|
+
models,
|
|
74
|
+
config,
|
|
75
|
+
{ log = console.log } = {},
|
|
76
|
+
) {
|
|
77
|
+
const supported = appTargetsSupportedByModels(
|
|
78
|
+
requestedTargets,
|
|
79
|
+
models,
|
|
80
|
+
config,
|
|
81
|
+
);
|
|
82
|
+
const supportedSet = new Set(supported);
|
|
83
|
+
const unavailable = requestedTargets.filter((target) => !supportedSet.has(target));
|
|
84
|
+
if (unavailable.length === 0) return supported;
|
|
85
|
+
|
|
86
|
+
if (requestedTargets.length === 1 || supported.length === 0) {
|
|
87
|
+
const target = unavailable[0];
|
|
88
|
+
const capability = APP_CAPABILITIES[target];
|
|
89
|
+
throw new Error(
|
|
90
|
+
`${capability.label} is unavailable because the selected tenant has no ${capability.provider} models; choose a tenant with ${capability.provider} access or install the other app`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const target of unavailable) {
|
|
95
|
+
const capability = APP_CAPABILITIES[target];
|
|
96
|
+
log(`Skipping ${capability.label}: the selected tenant has no ${capability.provider} models.`);
|
|
97
|
+
}
|
|
98
|
+
return supported;
|
|
99
|
+
}
|
|
100
|
+
|
|
61
101
|
// Each isolated desktop app maps to a client CLI + the env override that points
|
|
62
102
|
// that CLI at the app's private profile, so skill syncing lands in the app's
|
|
63
103
|
// installation rather than a global one.
|
|
@@ -328,6 +368,14 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
328
368
|
try {
|
|
329
369
|
if (action === "open") maybePrintUpdateNotice();
|
|
330
370
|
const config = await io.selectedConfig(targets, flags.tenant || null);
|
|
371
|
+
const catalog = await withProgress("Fetching the tenant model catalog", () => (
|
|
372
|
+
fetchWindowsCatalog(config, io)
|
|
373
|
+
));
|
|
374
|
+
const actionTargets = selectCatalogAppTargets(
|
|
375
|
+
targets,
|
|
376
|
+
catalog.models,
|
|
377
|
+
config,
|
|
378
|
+
);
|
|
331
379
|
const actionUserData = io.claudeUserData(io.environment, config.tenantId);
|
|
332
380
|
const actionPaths = appPaths(io.homeDir, config.tenantId, {
|
|
333
381
|
claudeUserData: actionUserData,
|
|
@@ -342,7 +390,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
342
390
|
chatgpt: fs.existsSync(path.join(actionPaths.chatgpt.codexHome, "config.toml")),
|
|
343
391
|
};
|
|
344
392
|
const vendorPaths = {};
|
|
345
|
-
for (const target of
|
|
393
|
+
for (const target of actionTargets) {
|
|
346
394
|
const isClaude = target === "claude";
|
|
347
395
|
const find = isClaude ? io.findClaudeApp : io.findChatGPTApp;
|
|
348
396
|
const ensure = isClaude ? io.ensureClaudeApp : io.ensureChatGPTApp;
|
|
@@ -367,16 +415,13 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
367
415
|
if (binary) vendorPaths[target] = binary;
|
|
368
416
|
}
|
|
369
417
|
|
|
370
|
-
if (
|
|
418
|
+
if (actionTargets.includes("claude") && action !== "refresh" && !vendorPaths.claude) {
|
|
371
419
|
throw new Error("Claude vendor app is unavailable; run `impel app install claude` or install it from https://claude.com/download");
|
|
372
420
|
}
|
|
373
|
-
|
|
374
|
-
fetchWindowsCatalog(config, io)
|
|
375
|
-
));
|
|
376
|
-
let managedChatGPT = targets.includes("chatgpt")
|
|
421
|
+
let managedChatGPT = actionTargets.includes("chatgpt")
|
|
377
422
|
? io.findManagedChatGPTApp(actionPaths.root)
|
|
378
423
|
: null;
|
|
379
|
-
const mustStageChatGPT =
|
|
424
|
+
const mustStageChatGPT = actionTargets.includes("chatgpt")
|
|
380
425
|
&& (action === "install"
|
|
381
426
|
|| action === "update"
|
|
382
427
|
|| (action === "open" && (!managedChatGPT
|
|
@@ -388,14 +433,14 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
388
433
|
managedChatGPT = io.stageChatGPTApp(vendorPaths.chatgpt, actionPaths.root);
|
|
389
434
|
}
|
|
390
435
|
|
|
391
|
-
const { userData } = configureWindowsApps(config,
|
|
436
|
+
const { userData } = configureWindowsApps(config, actionTargets, vendorPaths, catalog, io);
|
|
392
437
|
const tenantPaths = appPaths(io.homeDir, config.tenantId, {
|
|
393
438
|
claudeUserData: userData,
|
|
394
439
|
tenantName: config.tenantName,
|
|
395
440
|
});
|
|
396
441
|
const configuredTargets = action === "open"
|
|
397
|
-
?
|
|
398
|
-
:
|
|
442
|
+
? actionTargets.filter((target) => !profileWasInstalled[target])
|
|
443
|
+
: actionTargets;
|
|
399
444
|
if (action === "install" || action === "update" || action === "refresh" || configuredTargets.length > 0) {
|
|
400
445
|
const verb = action === "update" ? "Updated" : action === "refresh" ? "Refreshed" : "Installed";
|
|
401
446
|
for (const target of configuredTargets) {
|
|
@@ -417,7 +462,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
417
462
|
}
|
|
418
463
|
if (!flags["skip-agents"]) {
|
|
419
464
|
await withProgress("Syncing app agent profiles", () => io.syncAgents({
|
|
420
|
-
profiles:
|
|
465
|
+
profiles: actionTargets.map((target) => appAgentProfile(target, tenantPaths)),
|
|
421
466
|
gatewayUrl: config.gatewayUrl,
|
|
422
467
|
credential: config.pat,
|
|
423
468
|
tenantId: config.tenantId,
|
|
@@ -426,11 +471,11 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
426
471
|
console.log(`Models: ${catalog.models.length} from ${catalog.source}. The signed vendor apps and their normal profiles were not changed.`);
|
|
427
472
|
}
|
|
428
473
|
if (action === "open") {
|
|
429
|
-
if (
|
|
474
|
+
if (actionTargets.includes("claude")) {
|
|
430
475
|
await io.launchClaudeApp(vendorPaths.claude, tenantPaths.claude.userData, { environment: io.environment });
|
|
431
476
|
console.log("Opened Impel Claude with its tenant-isolated Windows profile.");
|
|
432
477
|
}
|
|
433
|
-
if (
|
|
478
|
+
if (actionTargets.includes("chatgpt")) {
|
|
434
479
|
managedChatGPT ||= io.findManagedChatGPTApp(tenantPaths.root);
|
|
435
480
|
if (!managedChatGPT) throw new Error("Impel ChatGPT is not staged; run `impel app install codex`");
|
|
436
481
|
await io.launchChatGPTApp(managedChatGPT, {
|
|
@@ -558,9 +603,24 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
558
603
|
const config = await io.selectedConfig(targets, flags.tenant || null);
|
|
559
604
|
console.log(`Tenant: ${config.tenantId} (desktop history is isolated per tenant).`);
|
|
560
605
|
|
|
606
|
+
let catalog;
|
|
607
|
+
try {
|
|
608
|
+
catalog = await withProgress("Fetching the tenant model catalog", () => io.fetchModels(config));
|
|
609
|
+
} catch (error) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
`tenant model catalog is unavailable (${redactSecretText(error.message)}); no Impel app files were changed`,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
const actionTargets = selectCatalogAppTargets(
|
|
615
|
+
targets,
|
|
616
|
+
catalog.models,
|
|
617
|
+
config,
|
|
618
|
+
{ log: io.log },
|
|
619
|
+
);
|
|
620
|
+
|
|
561
621
|
const vendorPaths = {};
|
|
562
622
|
if (!flags["skip-vendor"]) {
|
|
563
|
-
for (const target of
|
|
623
|
+
for (const target of actionTargets) {
|
|
564
624
|
// Reuse or install only the exact vendor build verified by this CLI.
|
|
565
625
|
// Moving "latest" releases are never cloned into a managed bundle.
|
|
566
626
|
const result = await withProgress(`Preparing the verified ${target} vendor app`, () => (
|
|
@@ -573,7 +633,7 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
573
633
|
}
|
|
574
634
|
|
|
575
635
|
const statuses = io.status(
|
|
576
|
-
|
|
636
|
+
actionTargets,
|
|
577
637
|
io.homeDir,
|
|
578
638
|
config.tenantId,
|
|
579
639
|
config.tenantName,
|
|
@@ -581,18 +641,9 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
581
641
|
for (const status of statuses) vendorPaths[status.target] ||= status.vendorPath;
|
|
582
642
|
const force = Boolean(flags.force);
|
|
583
643
|
const staleBundleTargets = force
|
|
584
|
-
? [...
|
|
644
|
+
? [...actionTargets]
|
|
585
645
|
: statuses.filter((status) => !io.bundleCurrent(status)).map((status) => status.target);
|
|
586
646
|
|
|
587
|
-
let catalog;
|
|
588
|
-
try {
|
|
589
|
-
catalog = await withProgress("Fetching the tenant model catalog", () => io.fetchModels(config));
|
|
590
|
-
} catch (error) {
|
|
591
|
-
throw new Error(
|
|
592
|
-
`tenant model catalog is unavailable (${redactSecretText(error.message)}); no Impel app files were changed`,
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
647
|
// A running vendor or Impel app only needs to close when its bundle will be
|
|
597
648
|
// swapped. Remember which managed launchers we closed so the update can
|
|
598
649
|
// reopen them afterward and feel in-place (the fast path never quits at all).
|
|
@@ -616,7 +667,7 @@ export async function cmdApps(argv, overrides = {}) {
|
|
|
616
667
|
staleBundleTargets.length > 0 ? "Rebuilding managed app bundles" : "Updating managed app profiles",
|
|
617
668
|
() => io.installFiles({
|
|
618
669
|
config,
|
|
619
|
-
targets,
|
|
670
|
+
targets: actionTargets,
|
|
620
671
|
models: catalog.models,
|
|
621
672
|
homeDir: io.homeDir,
|
|
622
673
|
vendorPaths,
|
|
@@ -705,8 +756,17 @@ export async function provisionAndOpenManagedApps({
|
|
|
705
756
|
);
|
|
706
757
|
}
|
|
707
758
|
|
|
759
|
+
const actionTargets = selectCatalogAppTargets(
|
|
760
|
+
targets,
|
|
761
|
+
catalog.models,
|
|
762
|
+
config,
|
|
763
|
+
{ log: io.log },
|
|
764
|
+
);
|
|
765
|
+
const actionTargetSet = new Set(actionTargets);
|
|
766
|
+
const actionStatuses = statuses.filter((status) => actionTargetSet.has(status.target));
|
|
767
|
+
|
|
708
768
|
const vendorPaths = {};
|
|
709
|
-
for (const status of
|
|
769
|
+
for (const status of actionStatuses) {
|
|
710
770
|
let vendorPath = status.vendorPath;
|
|
711
771
|
if (!vendorPath) {
|
|
712
772
|
const result = await withProgress(`Preparing the verified ${status.target} vendor app`, () => (
|
|
@@ -723,7 +783,7 @@ export async function provisionAndOpenManagedApps({
|
|
|
723
783
|
|
|
724
784
|
// Only stale or absent bundles pay the clone + codesign rebuild. The model
|
|
725
785
|
// catalog and vendor checks above finish before any running apps are closed.
|
|
726
|
-
const staleBundleTargets =
|
|
786
|
+
const staleBundleTargets = actionStatuses
|
|
727
787
|
.filter((status) => !io.bundleCurrent(status))
|
|
728
788
|
.map((status) => status.target);
|
|
729
789
|
if (staleBundleTargets.length > 0) {
|
|
@@ -737,7 +797,7 @@ export async function provisionAndOpenManagedApps({
|
|
|
737
797
|
staleBundleTargets.length > 0 ? "Rebuilding managed app bundles" : "Updating managed app profiles",
|
|
738
798
|
() => io.installFiles({
|
|
739
799
|
config,
|
|
740
|
-
targets,
|
|
800
|
+
targets: actionTargets,
|
|
741
801
|
models: catalog.models,
|
|
742
802
|
homeDir,
|
|
743
803
|
vendorPaths,
|
|
@@ -745,7 +805,7 @@ export async function provisionAndOpenManagedApps({
|
|
|
745
805
|
}),
|
|
746
806
|
);
|
|
747
807
|
const newlyInstalled = new Set(
|
|
748
|
-
|
|
808
|
+
actionStatuses.filter((status) => !status.launcherInstalled).map((status) => status.target),
|
|
749
809
|
);
|
|
750
810
|
for (const item of installed) {
|
|
751
811
|
if (newlyInstalled.has(item.target)) {
|
|
@@ -802,13 +862,30 @@ export function fastOpenLaunchers(targets, { homeDir = os.homedir(), config = lo
|
|
|
802
862
|
) {
|
|
803
863
|
return null;
|
|
804
864
|
}
|
|
865
|
+
const availableTargets = Array.isArray(manifest.availableTargets)
|
|
866
|
+
? manifest.availableTargets
|
|
867
|
+
: [];
|
|
868
|
+
const candidateTargets = targets.length > 1
|
|
869
|
+
? targets.filter((target) => availableTargets.includes(target))
|
|
870
|
+
: targets;
|
|
871
|
+
if (
|
|
872
|
+
candidateTargets.length === 0
|
|
873
|
+
|| candidateTargets.some((target) => !availableTargets.includes(target))
|
|
874
|
+
|| candidateTargets.some((target) => !manifest.targets?.includes(target))
|
|
875
|
+
) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
const candidateTargetSet = new Set(candidateTargets);
|
|
879
|
+
const candidateStatuses = statuses
|
|
880
|
+
? statuses.filter((status) => candidateTargetSet.has(status.target))
|
|
881
|
+
: appStatus(
|
|
882
|
+
candidateTargets,
|
|
883
|
+
homeDir,
|
|
884
|
+
config.tenantId,
|
|
885
|
+
config.tenantName || manifest.tenantName,
|
|
886
|
+
);
|
|
805
887
|
const launchers = [];
|
|
806
|
-
for (const status of
|
|
807
|
-
targets,
|
|
808
|
-
homeDir,
|
|
809
|
-
config.tenantId,
|
|
810
|
-
config.tenantName || manifest.tenantName,
|
|
811
|
-
)) {
|
|
888
|
+
for (const status of candidateStatuses) {
|
|
812
889
|
if (!bundleIsCurrent(status)) return null;
|
|
813
890
|
if (!fs.existsSync(status.configPath)) return null;
|
|
814
891
|
if (status.target === "claude" && !claudeConfigIsCurrent(status.configPath, config, gatewayUrl)) {
|
|
@@ -870,24 +947,33 @@ async function refreshApps(targets, { staleOnly = false, tenantId = null } = {})
|
|
|
870
947
|
} catch {
|
|
871
948
|
return;
|
|
872
949
|
}
|
|
950
|
+
const supportedTargetSet = new Set(appTargetsSupportedByModels(
|
|
951
|
+
installedTargets.map((status) => status.target),
|
|
952
|
+
catalog.models,
|
|
953
|
+
config,
|
|
954
|
+
));
|
|
955
|
+
const supportedStatuses = installedTargets.filter((status) => (
|
|
956
|
+
supportedTargetSet.has(status.target)
|
|
957
|
+
));
|
|
958
|
+
if (supportedStatuses.length === 0) return;
|
|
873
959
|
installManagedAppFiles({
|
|
874
960
|
config,
|
|
875
|
-
targets:
|
|
961
|
+
targets: supportedStatuses.map((status) => status.target),
|
|
876
962
|
models: catalog.models,
|
|
877
963
|
homeDir: os.homedir(),
|
|
878
|
-
vendorPaths: Object.fromEntries(
|
|
964
|
+
vendorPaths: Object.fromEntries(supportedStatuses.map((status) => [status.target, status.vendorPath])),
|
|
879
965
|
writeBundles: false,
|
|
880
966
|
});
|
|
881
967
|
|
|
882
968
|
const tenantPaths = appPaths(os.homedir(), config.tenantId, { tenantName: config.tenantName });
|
|
883
969
|
const gatewayUrl = resolveSkillsGateway(config.gatewayUrl);
|
|
884
|
-
for (const status of
|
|
970
|
+
for (const status of supportedStatuses) {
|
|
885
971
|
const { client, env, label } = appSkillTarget(status.target, tenantPaths);
|
|
886
972
|
await syncSkillsSafe({ client, gatewayUrl, env, label });
|
|
887
973
|
if (status.target === "chatgpt") secureManagedCodexHome(tenantPaths.chatgpt.codexHome);
|
|
888
974
|
}
|
|
889
975
|
await syncAgentProfilesSafe({
|
|
890
|
-
profiles:
|
|
976
|
+
profiles: supportedStatuses.map((status) => appAgentProfile(status.target, tenantPaths)),
|
|
891
977
|
gatewayUrl: config.gatewayUrl,
|
|
892
978
|
credential: config.pat,
|
|
893
979
|
tenantId: config.tenantId,
|
package/src/commands/setup.js
CHANGED
|
@@ -329,7 +329,7 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
329
329
|
message: "Windows desktop app setup returned an unsuccessful result.",
|
|
330
330
|
});
|
|
331
331
|
} else {
|
|
332
|
-
console.log(" ✓ Impel
|
|
332
|
+
console.log(" ✓ available Impel desktop app profiles are ready");
|
|
333
333
|
}
|
|
334
334
|
} catch (error) {
|
|
335
335
|
platformSetupFailed = true;
|
|
@@ -354,7 +354,7 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
354
354
|
);
|
|
355
355
|
try {
|
|
356
356
|
await io.installApps(["install", "all"]);
|
|
357
|
-
console.log(" ✓ Impel
|
|
357
|
+
console.log(" ✓ available Impel desktop apps installed");
|
|
358
358
|
} catch (error) {
|
|
359
359
|
platformSetupFailed = true;
|
|
360
360
|
console.error(
|
|
@@ -72,7 +72,7 @@ async function uploadConsent(io, explicit, failure) {
|
|
|
72
72
|
"Install recovery can send a sanitized failure envelope to Impel. Credentials, home paths, email addresses, ANSI controls, and raw tokens are removed."
|
|
73
73
|
);
|
|
74
74
|
io.log(
|
|
75
|
-
"The hosted agent
|
|
75
|
+
"The hosted agent selects typed action IDs; the Impel CLI runs the corresponding reviewed commands on this machine after local policy and confirmation checks."
|
|
76
76
|
);
|
|
77
77
|
io.log("Sanitized payload preview:");
|
|
78
78
|
io.log(JSON.stringify(failure, null, 2));
|
package/src/skills.js
CHANGED
|
@@ -74,6 +74,23 @@ export function resolveMarketplaceName(marketplace) {
|
|
|
74
74
|
return typeof name === "string" && name.trim() ? name.trim() : null;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** Resolve a marketplace name from Claude's local `marketplace list --json`. */
|
|
78
|
+
export function resolveConfiguredMarketplaceName(output, sourceUrl) {
|
|
79
|
+
let marketplaces;
|
|
80
|
+
try {
|
|
81
|
+
marketplaces = JSON.parse(String(output || ""));
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(marketplaces)) return null;
|
|
86
|
+
const normalizedSource = String(sourceUrl || "").replace(/\/+$/u, "");
|
|
87
|
+
const marketplace = marketplaces.find((candidate) => (
|
|
88
|
+
typeof candidate?.url === "string"
|
|
89
|
+
&& candidate.url.replace(/\/+$/u, "") === normalizedSource
|
|
90
|
+
));
|
|
91
|
+
return resolveMarketplaceName(marketplace);
|
|
92
|
+
}
|
|
93
|
+
|
|
77
94
|
/**
|
|
78
95
|
* Resolve the gateway URL for skill serving. Reuses the CLI's configured gateway
|
|
79
96
|
* (the caller passes `config.gatewayUrl`), then the env/default from config.js,
|
|
@@ -317,11 +334,38 @@ export async function syncSkills({
|
|
|
317
334
|
|
|
318
335
|
const manifestUrl = marketplaceUrl(gatewayUrl, client);
|
|
319
336
|
const sourceUrl = marketplaceSourceUrl(gatewayUrl, client);
|
|
320
|
-
|
|
321
|
-
const commands = buildSkillCommands({ client, marketplaceSourceUrl: sourceUrl, marketplaceName });
|
|
337
|
+
let marketplaceName = await fetchMarketplaceName(manifestUrl, fetchImpl);
|
|
322
338
|
|
|
323
339
|
logger.log(`Skills: syncing ${SKILL_PLUGIN_NAME} for ${displayLabel}…`);
|
|
324
340
|
const failures = [];
|
|
341
|
+
const registerCommand = buildSkillCommands({
|
|
342
|
+
client,
|
|
343
|
+
marketplaceSourceUrl: sourceUrl,
|
|
344
|
+
marketplaceName,
|
|
345
|
+
})[0];
|
|
346
|
+
const registerResult = await run(spec.bin, registerCommand.args, env);
|
|
347
|
+
if (registerResult.missing) {
|
|
348
|
+
failures.push({ phase: registerCommand.phase, reason: `\`${spec.bin}\` disappeared mid-sync` });
|
|
349
|
+
} else if (!isBenign(registerResult)) {
|
|
350
|
+
failures.push({
|
|
351
|
+
phase: registerCommand.phase,
|
|
352
|
+
reason: firstLine(registerResult.stderr) || `exit ${registerResult.status}`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// The public manifest fetch is deliberately best-effort and can time out.
|
|
357
|
+
// Once Claude has registered the marketplace, its local JSON index gives us
|
|
358
|
+
// the same dynamic name so refreshes can still use PLUGIN@MARKETPLACE.
|
|
359
|
+
if (!marketplaceName && client === "claude" && !registerResult.missing) {
|
|
360
|
+
const listed = await run(spec.bin, ["plugin", "marketplace", "list", "--json"], env);
|
|
361
|
+
if (listed.ok) marketplaceName = resolveConfiguredMarketplaceName(listed.stdout, sourceUrl);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const commands = registerResult.missing ? [] : buildSkillCommands({
|
|
365
|
+
client,
|
|
366
|
+
marketplaceSourceUrl: sourceUrl,
|
|
367
|
+
marketplaceName,
|
|
368
|
+
}).slice(1);
|
|
325
369
|
for (const command of commands) {
|
|
326
370
|
const result = await run(spec.bin, command.args, env);
|
|
327
371
|
if (result.missing) {
|