@iseoane/agent-depot 0.4.1 → 0.5.0
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/CHANGELOG.md +29 -0
- package/README.es.md +969 -0
- package/README.md +244 -23
- package/dist/src/app-flow.js +52 -51
- package/dist/src/app-recipe-removal.js +63 -0
- package/dist/src/cli.js +122 -11
- package/dist/src/executable-resolution.js +54 -0
- package/dist/src/git-source.js +129 -6
- package/dist/src/package-bundles.js +614 -0
- package/dist/src/package-cli.js +412 -0
- package/dist/src/package-content.js +158 -0
- package/dist/src/package-flow.js +636 -0
- package/dist/src/package-host-plans.js +240 -0
- package/dist/src/package-host-state.js +349 -0
- package/dist/src/package-model.js +33 -0
- package/dist/src/package-owned-skills.js +122 -0
- package/dist/src/package-state.js +404 -0
- package/dist/src/profile-cli.js +15 -9
- package/dist/src/profile-export.js +44 -9
- package/dist/src/profile-import.js +80 -17
- package/dist/src/profile.js +78 -3
- package/dist/src/skill-discovery.js +2 -2
- package/dist/src/skill-install.js +16 -0
- package/dist/src/sources.js +11 -0
- package/dist/src/tui/app-actions.js +1 -1
- package/dist/src/tui/app.js +5 -6
- package/dist/src/tui/catalog-browse.js +10 -3
- package/dist/src/tui/catalog-panel.js +7 -3
- package/dist/src/tui/catalog-state.js +57 -6
- package/dist/src/tui/catalog-tree.js +52 -10
- package/dist/src/tui/catalog-view.js +27 -4
- package/dist/src/tui/environment.js +8 -1
- package/dist/src/tui/installations-browse.js +20 -2
- package/dist/src/tui/installations-rows.js +2 -1
- package/dist/src/tui/installations-tree.js +14 -1
- package/dist/src/tui/installations-view.js +8 -2
- package/dist/src/tui/installations.js +12 -2
- package/dist/src/tui/manage-actions.js +2 -0
- package/dist/src/tui/manage-panel.js +5 -0
- package/dist/src/tui/package-actions.js +165 -0
- package/dist/src/tui/package-rows.js +87 -0
- package/dist/src/tui/profile-view.js +36 -14
- package/dist/src/tui/sources-mode.js +2 -0
- package/dist/src/tui/sources-recipes.js +116 -53
- package/dist/src/tui/sources-view.js +57 -19
- package/dist/src/tui/tree.js +3 -4
- package/dist/src/tui/updates-flow.js +4 -1
- package/dist/src/tui/updates-panel.js +2 -2
- package/dist/src/tui/updates.js +72 -4
- package/dist/src/user-global-skill-inventory.js +7 -2
- package/package.json +2 -1
- package/skills/agent-depot-apprecipe/SKILL.md +29 -5
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { sameAppVersion } from "./app-version.js";
|
|
4
|
+
import { createPortableExecutableResolver } from "./executable-resolution.js";
|
|
5
|
+
import { claudeMarketplaceName, discoverBundlesInSnapshot } from "./package-bundles.js";
|
|
6
|
+
import { sourceBundleScope } from "./package-content.js";
|
|
7
|
+
import { affectedBy, classifyUpdate, hostInstallId, packageApprovalDigest, planHostSteps, PackagePlanError, } from "./package-host-plans.js";
|
|
8
|
+
import { findInstalledBundle, readHostInstallView } from "./package-host-state.js";
|
|
9
|
+
import { activeBundles as classifyActiveBundles, portableSkillConflicts, } from "./package-owned-skills.js";
|
|
10
|
+
import { bundleRoot, describeCoordinates, selectionKey, } from "./package-model.js";
|
|
11
|
+
import { defaultPackageStateDirectory, PackageStateStore, } from "./package-state.js";
|
|
12
|
+
import { runProcess } from "./process-runner.js";
|
|
13
|
+
import { SourceStateStore } from "./source-state.js";
|
|
14
|
+
import { createSourceContentAccess } from "./skill-discovery.js";
|
|
15
|
+
import { createSourceOperations, resolveProjectSource, } from "./sources.js";
|
|
16
|
+
const PACKAGE_OUTPUT_LIMIT = 1024 * 1024;
|
|
17
|
+
const DIAGNOSTIC_BYTES = 4096;
|
|
18
|
+
const WSL_EXECUTABLE_REASON = "will not run (Windows executable on WSL)";
|
|
19
|
+
export function createPackageOperations(environment = {}) {
|
|
20
|
+
const platform = environment.platform ?? process.platform;
|
|
21
|
+
const home = environment.homeDirectory ?? homedir();
|
|
22
|
+
const isWsl = environment.isWsl ?? (platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP));
|
|
23
|
+
const stateDirectory = environment.stateDirectory ?? defaultPackageStateDirectory(process.env, platform, home);
|
|
24
|
+
const state = new PackageStateStore(stateDirectory);
|
|
25
|
+
const sourceStatePath = environment.sourceStatePath ?? path.join(stateDirectory, "sources.json");
|
|
26
|
+
const runner = environment.runner ?? runProcess;
|
|
27
|
+
const contentAccess = environment.sourceContentAccess ?? createSourceContentAccess();
|
|
28
|
+
const sourceOperations = environment.sourceOperations ?? createSourceOperations({
|
|
29
|
+
...(environment.homeDirectory === undefined ? {} : { homeDirectory: environment.homeDirectory }),
|
|
30
|
+
sourceContentAccess: contentAccess,
|
|
31
|
+
});
|
|
32
|
+
const resolve = createPortableExecutableResolver({
|
|
33
|
+
platform,
|
|
34
|
+
isWsl,
|
|
35
|
+
...(environment.wslMountRoot === undefined ? {} : { wslMountRoot: environment.wslMountRoot }),
|
|
36
|
+
...(environment.resolveExecutable === undefined ? {} : { resolveExecutable: environment.resolveExecutable }),
|
|
37
|
+
});
|
|
38
|
+
const readHostView = () => readHostInstallView({
|
|
39
|
+
...(environment.homeDirectory === undefined ? {} : { homeDirectory: environment.homeDirectory }),
|
|
40
|
+
...(environment.readHostStateFile === undefined ? {} : { readHostStateFile: environment.readHostStateFile }),
|
|
41
|
+
});
|
|
42
|
+
async function resolveSourceFor(projectSource) {
|
|
43
|
+
return sourceOperations.resolveProjectSource
|
|
44
|
+
? await sourceOperations.resolveProjectSource(projectSource)
|
|
45
|
+
: resolveProjectSource(projectSource);
|
|
46
|
+
}
|
|
47
|
+
/** ADR 0004: a preview that leads to a write refreshes the Source first. */
|
|
48
|
+
async function refreshSourceFor(selection) {
|
|
49
|
+
const source = await resolveSourceFor(selection.source);
|
|
50
|
+
if (source.kind === "builtin") {
|
|
51
|
+
return source;
|
|
52
|
+
}
|
|
53
|
+
if (sourceOperations.refreshProjectSource === undefined) {
|
|
54
|
+
throw new PackagePlanError(`Planning a Package lifecycle for ${describeCoordinates(selection)} requires a Source refresh, but this environment cannot refresh project Sources`);
|
|
55
|
+
}
|
|
56
|
+
return await sourceOperations.refreshProjectSource(selection.source);
|
|
57
|
+
}
|
|
58
|
+
async function readResolvedCommit(source) {
|
|
59
|
+
if (source.kind === "builtin" || contentAccess.readResolvedVersion === undefined) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const evidence = await contentAccess.readResolvedVersion(source);
|
|
63
|
+
return evidence?.kind === "git-commit" ? evidence.commit : undefined;
|
|
64
|
+
}
|
|
65
|
+
async function deriveBundle(selection, source) {
|
|
66
|
+
const files = Object.freeze([...(await contentAccess.readSnapshot(source))]);
|
|
67
|
+
const coordinates = coordinatesOf(selection);
|
|
68
|
+
const descriptor = discoverBundlesInSnapshot(files, sourceBundleScope(source))
|
|
69
|
+
.find((candidate) => sameCoordinates(candidate.coordinates, coordinates));
|
|
70
|
+
if (descriptor === undefined) {
|
|
71
|
+
throw new PackagePlanError(`Source ${JSON.stringify(source.id)} no longer provides a ${selection.host} Package at ${JSON.stringify(bundleRoot(coordinates))}`);
|
|
72
|
+
}
|
|
73
|
+
const resolvedCommit = await readResolvedCommit(source);
|
|
74
|
+
const available = availableEvidence(descriptor, resolvedCommit);
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
source,
|
|
77
|
+
files,
|
|
78
|
+
descriptor,
|
|
79
|
+
...(resolvedCommit === undefined ? {} : { resolvedCommit }),
|
|
80
|
+
...(available === undefined ? {} : { available }),
|
|
81
|
+
marketplace: marketplaceResolution(descriptor, files),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function planFromBundle(selection, action, bundle, view) {
|
|
85
|
+
const commands = Object.freeze(planHostSteps(action, bundle.descriptor, selection, view, bundle.marketplace)
|
|
86
|
+
.map((argv) => toCommand(argv, action)));
|
|
87
|
+
const origin = Object.freeze({
|
|
88
|
+
manifestDigest: bundle.descriptor.manifestDigest,
|
|
89
|
+
installId: hostInstallId(bundle.descriptor, selection, view, bundle.marketplace),
|
|
90
|
+
...(bundle.resolvedCommit === undefined ? {} : { resolvedCommit: bundle.resolvedCommit }),
|
|
91
|
+
});
|
|
92
|
+
const installed = findInstalledBundle(view, bundle.descriptor, selection);
|
|
93
|
+
const plan = Object.freeze({
|
|
94
|
+
selection,
|
|
95
|
+
action,
|
|
96
|
+
descriptor: bundle.descriptor,
|
|
97
|
+
commands,
|
|
98
|
+
affects: affectedBy(action, selection, (await state.list()).map((record) => record.selection)),
|
|
99
|
+
warnings: bundle.descriptor.warnings,
|
|
100
|
+
cwd: home,
|
|
101
|
+
environment: isWsl ? "linux (WSL)" : platform,
|
|
102
|
+
origin,
|
|
103
|
+
...(actionSatisfied(action, selection, installed) ? { skipExecution: true } : {}),
|
|
104
|
+
});
|
|
105
|
+
const declaration = declarationFor(selection, bundle.descriptor, commands);
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
plan,
|
|
108
|
+
declaration,
|
|
109
|
+
digest: packageApprovalDigest(declaration),
|
|
110
|
+
bundle,
|
|
111
|
+
...(bundle.available === undefined ? {} : { available: bundle.available }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Refuses an activation whose bundle owns a Skill already managed as a
|
|
116
|
+
* portable user-global installation. The portable copy is never removed or
|
|
117
|
+
* replaced; the user forgets it first. No path is changed here.
|
|
118
|
+
*/
|
|
119
|
+
async function assertNoPortableSkillConflicts(descriptor, source, action) {
|
|
120
|
+
if (action !== "install" && action !== "select") {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let installations;
|
|
124
|
+
try {
|
|
125
|
+
installations = (await new SourceStateStore(sourceStatePath).load()).userGlobalInstallations;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
throw new PackagePlanError(`The portable Skill state at ${JSON.stringify(sourceStatePath)} could not be read, so the ${action} cannot be checked for duplicate Skills: ${reasonOf(error)}`);
|
|
129
|
+
}
|
|
130
|
+
const conflicts = portableSkillConflicts(descriptor, source, installations);
|
|
131
|
+
if (conflicts.length === 0) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
throw new PackagePlanError(`The ${descriptor.host} Package ${JSON.stringify(descriptor.name)} would duplicate ${conflicts.length === 1 ? "a Skill" : "Skills"} ` +
|
|
135
|
+
`already managed as portable installations (${conflicts.map(conflict => JSON.stringify(conflict.path)).join(", ")}); ` +
|
|
136
|
+
"forget those installations first. No path was changed.");
|
|
137
|
+
}
|
|
138
|
+
async function derivePlan(selection, action, refresh) {
|
|
139
|
+
const source = refresh ? await refreshSourceFor(selection) : await resolveSourceFor(selection.source);
|
|
140
|
+
const bundle = await deriveBundle(selection, source);
|
|
141
|
+
await assertNoPortableSkillConflicts(bundle.descriptor, selection.source, action);
|
|
142
|
+
return await planFromBundle(selection, action, bundle, await readHostView());
|
|
143
|
+
}
|
|
144
|
+
async function inspect(selection) {
|
|
145
|
+
let bundle;
|
|
146
|
+
let drift;
|
|
147
|
+
let approval = "needs approval";
|
|
148
|
+
let installId;
|
|
149
|
+
const view = await readHostView();
|
|
150
|
+
try {
|
|
151
|
+
bundle = await deriveBundle(selection, await resolveSourceFor(selection.source));
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
drift = reasonOf(error);
|
|
155
|
+
}
|
|
156
|
+
if (bundle !== undefined) {
|
|
157
|
+
try {
|
|
158
|
+
const derived = await planFromBundle(selection, "install", bundle, view);
|
|
159
|
+
approval = await state.approvalStatus(derived.digest);
|
|
160
|
+
installId = derived.plan.origin.installId;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
drift ??= reasonOf(error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const record = await state.find(selection);
|
|
167
|
+
const identityDrift = record === undefined || installId === undefined || record.installId === installId
|
|
168
|
+
? undefined
|
|
169
|
+
: `The recorded install identity ${JSON.stringify(record.installId)} no longer matches ${JSON.stringify(installId)}`;
|
|
170
|
+
const installed = bundle === undefined
|
|
171
|
+
? Object.freeze({ kind: "unknown", reason: drift ?? "the bundle could not be resolved" })
|
|
172
|
+
: findInstalledBundle(view, bundle.descriptor, selection);
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
...(bundle === undefined ? {} : { descriptor: bundle.descriptor }),
|
|
175
|
+
installed,
|
|
176
|
+
...(bundle?.available === undefined ? {} : { available: bundle.available }),
|
|
177
|
+
approval,
|
|
178
|
+
...((drift ?? identityDrift) === undefined ? {} : { drift: drift ?? identityDrift }),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async function checkOne(record) {
|
|
182
|
+
try {
|
|
183
|
+
const inspection = await inspect(record.selection);
|
|
184
|
+
const status = classifyUpdate(inspection.installed, inspection.available);
|
|
185
|
+
return Object.freeze({
|
|
186
|
+
installed: record,
|
|
187
|
+
status,
|
|
188
|
+
...(inspection.available === undefined ? {} : { available: inspection.available }),
|
|
189
|
+
...(status === "unknown" ? { reason: inspection.drift ?? describeEvidence(inspection.installed) } : {}),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
return Object.freeze({
|
|
194
|
+
installed: record,
|
|
195
|
+
status: "unknown",
|
|
196
|
+
reason: `The update check failed: ${reasonOf(error)}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function writeSelection(context, observed) {
|
|
201
|
+
const verified = observed === undefined ? undefined : verifiedEvidence(observed, context.available);
|
|
202
|
+
await state.upsert({
|
|
203
|
+
selection: context.selection,
|
|
204
|
+
installId: context.origin.installId,
|
|
205
|
+
...(context.origin.resolvedCommit === undefined ? {} : { lastDelegatedCommit: context.origin.resolvedCommit }),
|
|
206
|
+
...(verified === undefined ? {} : { verified }),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async function recordOutcome(context, observed) {
|
|
210
|
+
switch (context.action) {
|
|
211
|
+
case "install": {
|
|
212
|
+
if (observed.kind !== "installed") {
|
|
213
|
+
return { status: "failed", reason: `The host does not report ${describeCoordinates(context.selection)} installed (${describeEvidence(observed)})` };
|
|
214
|
+
}
|
|
215
|
+
const contradiction = pinContradiction(context.selection, observed);
|
|
216
|
+
if (contradiction !== undefined) {
|
|
217
|
+
return { status: "failed", reason: `${contradiction}; the selection record was left untouched` };
|
|
218
|
+
}
|
|
219
|
+
await writeSelection(context, observed);
|
|
220
|
+
return Object.freeze({ status: "installed", verified: observed });
|
|
221
|
+
}
|
|
222
|
+
case "update": {
|
|
223
|
+
if (observed.kind !== "installed") {
|
|
224
|
+
return { status: "failed", reason: `The update left no installed ${describeCoordinates(context.selection)} (${describeEvidence(observed)})` };
|
|
225
|
+
}
|
|
226
|
+
const contradiction = pinContradiction(context.selection, observed);
|
|
227
|
+
if (contradiction !== undefined) {
|
|
228
|
+
return { status: "failed", reason: `${contradiction}; the selection record was left untouched` };
|
|
229
|
+
}
|
|
230
|
+
const change = installedEvidenceChange(context.previous, observed);
|
|
231
|
+
if (change === "unknown") {
|
|
232
|
+
return {
|
|
233
|
+
status: "failed",
|
|
234
|
+
reason: `The observed version change cannot be verified (${describeEvidence(context.previous ?? Object.freeze({ kind: "absent" }))} -> ${describeEvidence(observed)}); the selection record was left untouched`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (change === "unchanged") {
|
|
238
|
+
return {
|
|
239
|
+
status: "failed",
|
|
240
|
+
reason: `The observed version did not change (${describeEvidence(observed)}); the selection record was left untouched`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
await writeSelection(context, observed);
|
|
244
|
+
return Object.freeze({
|
|
245
|
+
status: "updated",
|
|
246
|
+
previous: context.previous ?? Object.freeze({ kind: "absent" }),
|
|
247
|
+
verified: observed,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
case "uninstall": {
|
|
251
|
+
if (observed.kind !== "absent") {
|
|
252
|
+
return { status: "failed", reason: `The host still reports ${describeCoordinates(context.selection)} (${describeEvidence(observed)})` };
|
|
253
|
+
}
|
|
254
|
+
await state.drop(context.selection);
|
|
255
|
+
return Object.freeze({ status: "removed" });
|
|
256
|
+
}
|
|
257
|
+
case "select": {
|
|
258
|
+
await writeSelection(context);
|
|
259
|
+
return Object.freeze({ status: "selected" });
|
|
260
|
+
}
|
|
261
|
+
case "forget": {
|
|
262
|
+
await state.drop(context.selection);
|
|
263
|
+
return Object.freeze({ status: "forgotten" });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function contextFromDerived(derived, action, previous) {
|
|
268
|
+
return {
|
|
269
|
+
action,
|
|
270
|
+
selection: derived.plan.selection,
|
|
271
|
+
descriptor: derived.plan.descriptor,
|
|
272
|
+
origin: derived.plan.origin,
|
|
273
|
+
...(derived.available === undefined ? {} : { available: derived.available }),
|
|
274
|
+
...(previous === undefined ? {} : { previous }),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
async function completeSkipped(context) {
|
|
278
|
+
switch (context.action) {
|
|
279
|
+
case "install": {
|
|
280
|
+
return await recordOutcome(context, findInstalledBundle(await readHostView(), context.descriptor, context.selection));
|
|
281
|
+
}
|
|
282
|
+
case "select": {
|
|
283
|
+
await writeSelection(context);
|
|
284
|
+
return Object.freeze({ status: "selected" });
|
|
285
|
+
}
|
|
286
|
+
case "uninstall": {
|
|
287
|
+
await state.drop(context.selection);
|
|
288
|
+
return Object.freeze({ status: "removed" });
|
|
289
|
+
}
|
|
290
|
+
case "update":
|
|
291
|
+
case "forget": {
|
|
292
|
+
return await recordOutcome(context, findInstalledBundle(await readHostView(), context.descriptor, context.selection));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
async discover(sourceIds) {
|
|
298
|
+
const descriptors = [];
|
|
299
|
+
for (const source of await sourceOperations.selectSources(sourceIds)) {
|
|
300
|
+
descriptors.push(...discoverBundlesInSnapshot(await contentAccess.readSnapshot(source), sourceBundleScope(source)));
|
|
301
|
+
}
|
|
302
|
+
return Object.freeze(descriptors);
|
|
303
|
+
},
|
|
304
|
+
async list() {
|
|
305
|
+
return await state.list();
|
|
306
|
+
},
|
|
307
|
+
async activeBundles(bundles) {
|
|
308
|
+
const records = await state.list();
|
|
309
|
+
return classifyActiveBundles(bundles, records.map(record => record.selection), await readHostView());
|
|
310
|
+
},
|
|
311
|
+
inspect,
|
|
312
|
+
async approvalStatus(selection, action = "install") {
|
|
313
|
+
const derived = await derivePlan(selection, action, false);
|
|
314
|
+
return await state.approvalStatus(derived.digest);
|
|
315
|
+
},
|
|
316
|
+
async approve(selection, action = "install") {
|
|
317
|
+
const derived = await derivePlan(selection, action, false);
|
|
318
|
+
await state.approve(derived.digest);
|
|
319
|
+
},
|
|
320
|
+
async planLifecycle(selection, action) {
|
|
321
|
+
return (await derivePlan(selection, action, true)).plan;
|
|
322
|
+
},
|
|
323
|
+
async executeLifecycle(plan, confirmed) {
|
|
324
|
+
if (!confirmed) {
|
|
325
|
+
return Object.freeze({ status: "not confirmed" });
|
|
326
|
+
}
|
|
327
|
+
const fresh = await derivePlan(plan.selection, plan.action, false);
|
|
328
|
+
const difference = planDifference(plan, fresh);
|
|
329
|
+
if (difference !== undefined) {
|
|
330
|
+
return Object.freeze({ status: "stale plan", reason: difference });
|
|
331
|
+
}
|
|
332
|
+
if (await state.approvalStatus(fresh.digest) !== "approved") {
|
|
333
|
+
throw new PackagePlanError(`The ${plan.action} plan for ${describeCoordinates(plan.selection)} is not approved; review it and approve the current declaration first`);
|
|
334
|
+
}
|
|
335
|
+
const previous = findInstalledBundle(await readHostView(), fresh.plan.descriptor, plan.selection);
|
|
336
|
+
const context = contextFromDerived(fresh, plan.action, previous);
|
|
337
|
+
if (fresh.plan.skipExecution === true) {
|
|
338
|
+
return await completeSkipped(context);
|
|
339
|
+
}
|
|
340
|
+
for (const command of fresh.plan.commands) {
|
|
341
|
+
const resolution = await resolve(command.executable);
|
|
342
|
+
if (resolution.blocked) {
|
|
343
|
+
return { status: "manual required", reason: blockedReason(command.executable, plan.action) };
|
|
344
|
+
}
|
|
345
|
+
if (resolution.executable === undefined) {
|
|
346
|
+
return { status: "manual required", reason: missingExecutableReason(command.executable, plan.action) };
|
|
347
|
+
}
|
|
348
|
+
let result;
|
|
349
|
+
try {
|
|
350
|
+
result = await runner(resolution.executable, command.args, {
|
|
351
|
+
cwd: plan.cwd,
|
|
352
|
+
captureStdout: true,
|
|
353
|
+
maxOutputBytes: PACKAGE_OUTPUT_LIMIT,
|
|
354
|
+
maxStderrBytes: PACKAGE_OUTPUT_LIMIT,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
const code = error?.code;
|
|
359
|
+
if (code === undefined || !["ENOENT", "EACCES", "ENOEXEC", "EPERM"].includes(code)) {
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
363
|
+
return {
|
|
364
|
+
status: "manual required",
|
|
365
|
+
reason: `Cannot run ${JSON.stringify(command.executable)} for the Package ${plan.action} (${code}): ${detail}`,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (result.code !== 0 || result.signal || result.outputTooLarge) {
|
|
369
|
+
const failure = result.outputTooLarge
|
|
370
|
+
? `output exceeded ${PACKAGE_OUTPUT_LIMIT} bytes`
|
|
371
|
+
: `exit ${result.code}, signal ${result.signal}`;
|
|
372
|
+
const output = [
|
|
373
|
+
result.stdout.subarray(-DIAGNOSTIC_BYTES).toString("utf8"),
|
|
374
|
+
Buffer.from(result.stderr).subarray(-DIAGNOSTIC_BYTES).toString("utf8"),
|
|
375
|
+
].join(" ").trim();
|
|
376
|
+
return {
|
|
377
|
+
status: "failed",
|
|
378
|
+
reason: `The host command ${JSON.stringify(command.executable)} failed (${failure})`,
|
|
379
|
+
...(output === "" ? {} : { output }),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return await recordOutcome(context, findInstalledBundle(await readHostView(), fresh.plan.descriptor, plan.selection));
|
|
384
|
+
},
|
|
385
|
+
async forget(selection, confirmed) {
|
|
386
|
+
if (!confirmed) {
|
|
387
|
+
throw new PackagePlanError("Forgetting a Package requires explicit confirmation; no record was changed");
|
|
388
|
+
}
|
|
389
|
+
await state.drop(selection);
|
|
390
|
+
},
|
|
391
|
+
async checkUpdates(selections) {
|
|
392
|
+
const records = await state.list();
|
|
393
|
+
const selected = selections === undefined
|
|
394
|
+
? records
|
|
395
|
+
: records.filter((record) => selections.some((selection) => selectionKey(selection) === selectionKey(record.selection)));
|
|
396
|
+
return Object.freeze(await Promise.all(selected.map(async (record) => await checkOne(record))));
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function coordinatesOf(selection) {
|
|
401
|
+
return selection.host === "pi"
|
|
402
|
+
? Object.freeze({ host: selection.host, root: selection.root })
|
|
403
|
+
: Object.freeze({ host: selection.host, marketplaceRoot: selection.marketplaceRoot, pluginName: selection.pluginName });
|
|
404
|
+
}
|
|
405
|
+
function sameCoordinates(left, right) {
|
|
406
|
+
if (left.host === "pi" && right.host === "pi") {
|
|
407
|
+
return left.root === right.root;
|
|
408
|
+
}
|
|
409
|
+
if (left.host === "claude" && right.host === "claude") {
|
|
410
|
+
return left.marketplaceRoot === right.marketplaceRoot && left.pluginName === right.pluginName;
|
|
411
|
+
}
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
function availableEvidence(descriptor, resolvedCommit) {
|
|
415
|
+
if (descriptor.version !== undefined) {
|
|
416
|
+
return descriptor.version;
|
|
417
|
+
}
|
|
418
|
+
return resolvedCommit === undefined ? undefined : Object.freeze({ kind: "git-commit", commit: resolvedCommit });
|
|
419
|
+
}
|
|
420
|
+
/** The marketplace name lives in the Source's own manifest, never in host state before the first add. */
|
|
421
|
+
function marketplaceResolution(descriptor, files) {
|
|
422
|
+
const coordinates = descriptor.coordinates;
|
|
423
|
+
if (coordinates.host !== "claude") {
|
|
424
|
+
return Object.freeze({});
|
|
425
|
+
}
|
|
426
|
+
const root = coordinates.marketplaceRoot;
|
|
427
|
+
const filePath = root === "" ? ".claude-plugin/marketplace.json" : `${root}/.claude-plugin/marketplace.json`;
|
|
428
|
+
const raw = files.find((file) => file.path === filePath)?.content;
|
|
429
|
+
if (raw === undefined) {
|
|
430
|
+
return Object.freeze({});
|
|
431
|
+
}
|
|
432
|
+
const name = claudeMarketplaceName(raw);
|
|
433
|
+
return Object.freeze(name === undefined ? {} : { marketplaceName: name });
|
|
434
|
+
}
|
|
435
|
+
function declarationFor(selection, descriptor, commands) {
|
|
436
|
+
const source = declarationSource(selection.source);
|
|
437
|
+
return Object.freeze({
|
|
438
|
+
...source,
|
|
439
|
+
coordinates: coordinatesOf(selection),
|
|
440
|
+
manifestDigest: descriptor.manifestDigest,
|
|
441
|
+
argv: Object.freeze(commands.map((command) => Object.freeze([command.executable, ...command.args]))),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
function declarationSource(source) {
|
|
445
|
+
if (source.kind === "builtin") {
|
|
446
|
+
return Object.freeze({ sourceUrl: "" });
|
|
447
|
+
}
|
|
448
|
+
if ("url" in source) {
|
|
449
|
+
return Object.freeze({ sourceUrl: source.url, ...(source.ref === undefined ? {} : { ref: source.ref }) });
|
|
450
|
+
}
|
|
451
|
+
return Object.freeze({ sourceUrl: source.path, ...(source.ref === undefined ? {} : { ref: source.ref }) });
|
|
452
|
+
}
|
|
453
|
+
function toCommand(argv, action) {
|
|
454
|
+
const [executable, ...args] = argv;
|
|
455
|
+
if (executable === undefined) {
|
|
456
|
+
throw new PackagePlanError("A host step produced an empty argv");
|
|
457
|
+
}
|
|
458
|
+
return Object.freeze({
|
|
459
|
+
purpose: commandPurpose(action, executable, args),
|
|
460
|
+
executable,
|
|
461
|
+
args: Object.freeze(args),
|
|
462
|
+
summary: `${executable} ${args.join(" ")}`.trim(),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
function commandPurpose(action, executable, args) {
|
|
466
|
+
if (action === "uninstall") {
|
|
467
|
+
return "remove";
|
|
468
|
+
}
|
|
469
|
+
return executable === "claude" && args[0] === "plugin" && args[1] === "marketplace" ? "register" : "apply";
|
|
470
|
+
}
|
|
471
|
+
function actionSatisfied(action, selection, installed) {
|
|
472
|
+
switch (action) {
|
|
473
|
+
case "install": {
|
|
474
|
+
if (installed.kind !== "installed") {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const verdict = fixedPiPinVerdict(selection, installed);
|
|
478
|
+
return verdict.kind === "satisfied" || verdict.kind === "not-pinned";
|
|
479
|
+
}
|
|
480
|
+
case "uninstall": {
|
|
481
|
+
return installed.kind === "absent";
|
|
482
|
+
}
|
|
483
|
+
case "select": {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
case "update":
|
|
487
|
+
case "forget": {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/** The exact recheck issue 08 pins: manifest digest, install id, resolved commit, ordered steps. */
|
|
493
|
+
function planDifference(plan, fresh) {
|
|
494
|
+
if (fresh.plan.origin.manifestDigest !== plan.origin.manifestDigest) {
|
|
495
|
+
return "The bundle manifest changed since the plan was shown";
|
|
496
|
+
}
|
|
497
|
+
if (fresh.plan.origin.installId !== plan.origin.installId) {
|
|
498
|
+
return "The host install identity changed since the plan was shown";
|
|
499
|
+
}
|
|
500
|
+
if (fresh.plan.origin.resolvedCommit !== plan.origin.resolvedCommit) {
|
|
501
|
+
return "The resolved Source commit changed since the plan was shown";
|
|
502
|
+
}
|
|
503
|
+
if (fresh.plan.commands.length !== plan.commands.length) {
|
|
504
|
+
return "The ordered host steps changed since the plan was shown";
|
|
505
|
+
}
|
|
506
|
+
for (const [index, command] of fresh.plan.commands.entries()) {
|
|
507
|
+
const shown = plan.commands[index];
|
|
508
|
+
if (shown === undefined || shown.executable !== command.executable
|
|
509
|
+
|| shown.args.length !== command.args.length
|
|
510
|
+
|| shown.args.some((argument, argumentIndex) => argument !== command.args[argumentIndex])) {
|
|
511
|
+
return `Host step ${index + 1} changed since the plan was shown`;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Three outcomes, not two. `unknown` means the two observations carry no
|
|
518
|
+
* comparable evidence, and an update must not report that as a success. A host
|
|
519
|
+
* that had nothing installed and now reports an install has changed.
|
|
520
|
+
*/
|
|
521
|
+
function installedEvidenceChange(previous, observed) {
|
|
522
|
+
if (observed.kind !== "installed" || previous === undefined || previous.kind === "unknown") {
|
|
523
|
+
return "unknown";
|
|
524
|
+
}
|
|
525
|
+
if (previous.kind === "absent") {
|
|
526
|
+
return "changed";
|
|
527
|
+
}
|
|
528
|
+
if (previous.version !== undefined && observed.version !== undefined) {
|
|
529
|
+
return sameAppVersion(previous.version, observed.version) ? "unchanged" : "changed";
|
|
530
|
+
}
|
|
531
|
+
const previousCommit = previous.commit === undefined ? undefined : gitCommitId(previous.commit);
|
|
532
|
+
const observedCommit = observed.commit === undefined ? undefined : gitCommitId(observed.commit);
|
|
533
|
+
if (previousCommit !== undefined && observedCommit !== undefined) {
|
|
534
|
+
return previousCommit === observedCommit ? "unchanged" : "changed";
|
|
535
|
+
}
|
|
536
|
+
return "unknown";
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Whether host evidence settles a fixed Pi pin. Two hexadecimal refs must agree;
|
|
540
|
+
* a version pin compares through `sameAppVersion`. Evidence the pin cannot be
|
|
541
|
+
* read from is `unverifiable`, so it never satisfies a skip and never counts as
|
|
542
|
+
* a contradiction.
|
|
543
|
+
*/
|
|
544
|
+
function fixedPiPinVerdict(selection, observed) {
|
|
545
|
+
if (selection.host !== "pi" || selection.version.policy !== "fixed" || selection.version.version === "") {
|
|
546
|
+
return { kind: "not-pinned" };
|
|
547
|
+
}
|
|
548
|
+
const pin = selection.version.version;
|
|
549
|
+
if (observed.kind !== "installed") {
|
|
550
|
+
return { kind: "unverifiable", pin };
|
|
551
|
+
}
|
|
552
|
+
const pinCommit = gitCommitId(pin);
|
|
553
|
+
const observedCommit = observed.commit === undefined ? undefined : gitCommitId(observed.commit);
|
|
554
|
+
if (pinCommit !== undefined && observedCommit !== undefined) {
|
|
555
|
+
return pinCommit === observedCommit ? { kind: "satisfied" } : { kind: "contradicted", pin };
|
|
556
|
+
}
|
|
557
|
+
if (observed.version !== undefined && sameAppVersion(observed.version, pin)) {
|
|
558
|
+
return { kind: "satisfied" };
|
|
559
|
+
}
|
|
560
|
+
return { kind: "unverifiable", pin };
|
|
561
|
+
}
|
|
562
|
+
/** A fixed Pi pin the host positively contradicts. Unverifiable evidence is not a contradiction. */
|
|
563
|
+
function pinContradiction(selection, observed) {
|
|
564
|
+
const verdict = fixedPiPinVerdict(selection, observed);
|
|
565
|
+
if (verdict.kind !== "contradicted") {
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
return `The host reports ${describeCoordinates(selection)} ${describeEvidence(observed)}, which contradicts the fixed pin ${JSON.stringify(verdict.pin)}`;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* The record's `verified` field is a claim about the host, so it derives from the
|
|
572
|
+
* host observation alone. A version is recorded only when it agrees with the one
|
|
573
|
+
* the Source declares, because `declaredBy` names that declaration. Evidence that
|
|
574
|
+
* cannot be attested leaves the field absent.
|
|
575
|
+
*/
|
|
576
|
+
function verifiedEvidence(observed, available) {
|
|
577
|
+
if (observed.kind !== "installed") {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
if (observed.commit !== undefined) {
|
|
581
|
+
const commit = storableCommit(observed.commit);
|
|
582
|
+
return commit === undefined ? undefined : Object.freeze({ kind: "git-commit", commit });
|
|
583
|
+
}
|
|
584
|
+
if (observed.version === undefined
|
|
585
|
+
|| available?.kind !== "manifest-version"
|
|
586
|
+
|| !storableVersion(observed.version)
|
|
587
|
+
|| !sameAppVersion(observed.version, available.version)) {
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
return Object.freeze({ kind: "manifest-version", version: observed.version, declaredBy: available.declaredBy });
|
|
591
|
+
}
|
|
592
|
+
/** A hexadecimal git ref, normalized, or `undefined` for any other spelling. */
|
|
593
|
+
function gitCommitId(ref) {
|
|
594
|
+
const normalized = ref.trim().toLowerCase();
|
|
595
|
+
return /^[0-9a-f]{7,64}$/u.test(normalized) ? normalized : undefined;
|
|
596
|
+
}
|
|
597
|
+
/** The state store reads back only a 40- or 64-character lowercase commit ID. */
|
|
598
|
+
function storableCommit(commit) {
|
|
599
|
+
const normalized = gitCommitId(commit);
|
|
600
|
+
return normalized !== undefined && (normalized.length === 40 || normalized.length === 64) ? normalized : undefined;
|
|
601
|
+
}
|
|
602
|
+
/** The state store reads back only a value without surrounding or unsafe whitespace. */
|
|
603
|
+
function storableVersion(version) {
|
|
604
|
+
return version !== "" && version.trim() === version && !/\s/u.test(version) && !hasControlCharacter(version);
|
|
605
|
+
}
|
|
606
|
+
function hasControlCharacter(value) {
|
|
607
|
+
return Array.from(value).some((character) => (character.codePointAt(0) ?? 0) <= 0x1f || character.codePointAt(0) === 0x7f);
|
|
608
|
+
}
|
|
609
|
+
function describeEvidence(evidence) {
|
|
610
|
+
switch (evidence.kind) {
|
|
611
|
+
case "installed": {
|
|
612
|
+
if (evidence.version !== undefined) {
|
|
613
|
+
return `installed ${JSON.stringify(evidence.version)}`;
|
|
614
|
+
}
|
|
615
|
+
if (evidence.commit !== undefined) {
|
|
616
|
+
return `installed at commit ${evidence.commit}`;
|
|
617
|
+
}
|
|
618
|
+
return "installed with no version evidence";
|
|
619
|
+
}
|
|
620
|
+
case "absent": {
|
|
621
|
+
return "not installed";
|
|
622
|
+
}
|
|
623
|
+
case "unknown": {
|
|
624
|
+
return `unknown (${evidence.reason})`;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function blockedReason(executable, action) {
|
|
629
|
+
return `Cannot run ${JSON.stringify(executable)} for the Package ${action}: ${WSL_EXECUTABLE_REASON}; run the host command manually`;
|
|
630
|
+
}
|
|
631
|
+
function missingExecutableReason(executable, action) {
|
|
632
|
+
return `Required executable ${JSON.stringify(executable)} for the Package ${action} was not found or could not be started. Install it or make it available on the PATH used by Agent Depot, then retry.`;
|
|
633
|
+
}
|
|
634
|
+
function reasonOf(error) {
|
|
635
|
+
return error instanceof Error ? error.message : String(error);
|
|
636
|
+
}
|