@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,412 @@
|
|
|
1
|
+
import { sourceIdForUrl } from "./git-source.js";
|
|
2
|
+
import { PACKAGE_HOSTS, bundleRoot, describeCoordinates, selectionKey } from "./package-model.js";
|
|
3
|
+
import { projectSourceOf } from "./sources.js";
|
|
4
|
+
import { CliUsageError } from "./usage-error.js";
|
|
5
|
+
export const PACKAGE_USAGE = "Usage: agent-depot package list; package discover <source-id>; package inspect <selection> --host <host>; package approve <selection> --host <host> [--action <install|update|uninstall>]; package install|update|uninstall <selection> --host <host> [--yes]; package forget <selection> --host <host> --yes";
|
|
6
|
+
export async function runPackageCommand(values, packages, sources, output) {
|
|
7
|
+
const [command, ...args] = values;
|
|
8
|
+
if (command === "list") {
|
|
9
|
+
requireNoArgs(args, "package list");
|
|
10
|
+
for (const record of await packages.list()) {
|
|
11
|
+
output(formatPackageRecord(record));
|
|
12
|
+
}
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
if (command === "discover") {
|
|
16
|
+
if (args.length !== 1 || args[0].startsWith("--")) {
|
|
17
|
+
throw new CliUsageError(PACKAGE_USAGE);
|
|
18
|
+
}
|
|
19
|
+
const sourceId = args[0];
|
|
20
|
+
for (const descriptor of await packages.discover([sourceId])) {
|
|
21
|
+
output(`Package ${formatDescriptorSelection(sourceId, descriptor)}: ${descriptor.name}`);
|
|
22
|
+
describeDescriptor(descriptor, output, " ");
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
if (command === "inspect") {
|
|
27
|
+
const selection = await resolveSelection(parseSelectionArgs(args, { allowConfirmation: false }), packages, sources);
|
|
28
|
+
describeInspection(selection, await packages.inspect(selection), output);
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
if (command === "approve") {
|
|
32
|
+
const parsed = parseSelectionArgs(args, { allowConfirmation: false, allowApprovalAction: true });
|
|
33
|
+
const action = parsed.approvalAction ?? "install";
|
|
34
|
+
if (action === "select" || action === "forget") {
|
|
35
|
+
throw new CliUsageError("Package approval only applies to install, update, and uninstall actions");
|
|
36
|
+
}
|
|
37
|
+
const selection = await resolveSelection(parsed, packages, sources);
|
|
38
|
+
const plan = await packages.planLifecycle(selection, action);
|
|
39
|
+
describeLifecyclePlan(plan, await packages.approvalStatus(selection, action), output);
|
|
40
|
+
await packages.approve(selection, action);
|
|
41
|
+
output(`Approved Package ${action}: ${formatPackageSelection(selection)}`);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
if (command === "install" || command === "update" || command === "uninstall") {
|
|
45
|
+
const parsed = parseSelectionArgs(args, { allowConfirmation: true });
|
|
46
|
+
const selection = await resolveSelection(parsed, packages, sources);
|
|
47
|
+
const plan = await packages.planLifecycle(selection, command);
|
|
48
|
+
const approval = await packages.approvalStatus(selection, command);
|
|
49
|
+
describeLifecyclePlan(plan, approval, output);
|
|
50
|
+
if (approval !== "approved") {
|
|
51
|
+
throw new Error(`Package ${command} needs approval; run package approve ${formatPackageSelection(selection)} --host ${selection.host} --action ${command} after reviewing the preview`);
|
|
52
|
+
}
|
|
53
|
+
if (!parsed.confirmed) {
|
|
54
|
+
throw new Error(`Package ${command} not confirmed; rerun with --yes after reviewing the preview`);
|
|
55
|
+
}
|
|
56
|
+
const result = await packages.executeLifecycle(plan, true);
|
|
57
|
+
output(`Package ${formatPackageSelection(selection)}: ${describePackageResult(result)}`);
|
|
58
|
+
return packageResultSucceeded(result) ? 0 : 1;
|
|
59
|
+
}
|
|
60
|
+
if (command === "forget") {
|
|
61
|
+
const parsed = parseSelectionArgs(args, { allowConfirmation: true });
|
|
62
|
+
const selection = await resolveSelection(parsed, packages, sources);
|
|
63
|
+
output(`Package forget preview: ${formatPackageSelection(selection)}`);
|
|
64
|
+
output(" Host commands: none; only Agent Depot's Package selection record is removed");
|
|
65
|
+
if (!parsed.confirmed) {
|
|
66
|
+
throw new Error("Package forget not confirmed; rerun with --yes after reviewing the preview");
|
|
67
|
+
}
|
|
68
|
+
await packages.forget(selection, true);
|
|
69
|
+
output(`Forgot Package: ${formatPackageSelection(selection)}`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
throw new CliUsageError(PACKAGE_USAGE);
|
|
73
|
+
}
|
|
74
|
+
export function formatPackageSelection(selection) {
|
|
75
|
+
return `${sourceToken(selection.source)}::${describeSelectionCoordinate(selection)}`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The two spellings the CLI prints name the same selection: `discover` prints
|
|
79
|
+
* the registered Source id, and `list` prints the Source's own token. Both are
|
|
80
|
+
* accepted wherever a selection is taken as input.
|
|
81
|
+
*/
|
|
82
|
+
export function packageSelectionMatches(selection, value) {
|
|
83
|
+
return formatPackageSelection(selection) === value
|
|
84
|
+
|| selectionKey(selection) === value
|
|
85
|
+
|| `${sourceIdToken(selection.source)}::${describeSelectionCoordinate(selection)}` === value;
|
|
86
|
+
}
|
|
87
|
+
export function describePackageUpdateCheck(check) {
|
|
88
|
+
const available = check.available === undefined ? "unknown" : describeVersion(check.available);
|
|
89
|
+
const reason = check.reason === undefined ? "" : `; ${check.reason}`;
|
|
90
|
+
return `Package ${formatPackageSelection(check.installed.selection)}: ${check.status}; available ${available}${reason}`;
|
|
91
|
+
}
|
|
92
|
+
export function describePackageResult(result) {
|
|
93
|
+
switch (result.status) {
|
|
94
|
+
case "installed":
|
|
95
|
+
return `installed (${describeEvidence(result.verified)})`;
|
|
96
|
+
case "updated":
|
|
97
|
+
return `updated (${describeEvidence(result.previous)} -> ${describeEvidence(result.verified)})`;
|
|
98
|
+
case "removed":
|
|
99
|
+
case "selected":
|
|
100
|
+
case "forgotten":
|
|
101
|
+
case "not confirmed":
|
|
102
|
+
return result.status;
|
|
103
|
+
case "stale plan":
|
|
104
|
+
case "manual required":
|
|
105
|
+
case "failed":
|
|
106
|
+
return `${result.status}: ${result.reason}${"output" in result && result.output ? `; ${result.output}` : ""}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function requireNoArgs(args, usage) {
|
|
110
|
+
if (args.length !== 0) {
|
|
111
|
+
throw new CliUsageError(`Usage: agent-depot ${usage}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function parseSelectionArgs(args, options) {
|
|
115
|
+
let selection;
|
|
116
|
+
let host;
|
|
117
|
+
let confirmed = false;
|
|
118
|
+
let approvalAction;
|
|
119
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
120
|
+
const argument = args[index];
|
|
121
|
+
if (argument === "--yes") {
|
|
122
|
+
if (!options.allowConfirmation) {
|
|
123
|
+
throw new CliUsageError(PACKAGE_USAGE);
|
|
124
|
+
}
|
|
125
|
+
confirmed = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (argument === "--host") {
|
|
129
|
+
host = parseHost(requireOptionValue(args, ++index, "--host"));
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (argument === "--action" && options.allowApprovalAction === true) {
|
|
133
|
+
approvalAction = parseApprovalAction(requireOptionValue(args, ++index, "--action"));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (argument.startsWith("--")) {
|
|
137
|
+
throw new CliUsageError(PACKAGE_USAGE);
|
|
138
|
+
}
|
|
139
|
+
if (selection !== undefined) {
|
|
140
|
+
throw new CliUsageError(PACKAGE_USAGE);
|
|
141
|
+
}
|
|
142
|
+
selection = argument;
|
|
143
|
+
}
|
|
144
|
+
if (selection === undefined) {
|
|
145
|
+
throw new CliUsageError(PACKAGE_USAGE);
|
|
146
|
+
}
|
|
147
|
+
return { selection, ...(host === undefined ? {} : { host }), confirmed, ...(approvalAction === undefined ? {} : { approvalAction }) };
|
|
148
|
+
}
|
|
149
|
+
function parseHost(value) {
|
|
150
|
+
if (PACKAGE_HOSTS.includes(value)) {
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
throw new CliUsageError(`Unsupported Package Host ${JSON.stringify(value)}; expected ${PACKAGE_HOSTS.join(" or ")}`);
|
|
154
|
+
}
|
|
155
|
+
function parseApprovalAction(value) {
|
|
156
|
+
if (value === "install" || value === "update" || value === "uninstall") {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
throw new CliUsageError("Package approval action must be install, update, or uninstall");
|
|
160
|
+
}
|
|
161
|
+
async function resolveSelection(args, packages, sources) {
|
|
162
|
+
const token = parseSelectionToken(args.selection);
|
|
163
|
+
const host = resolveHost(token, args.host);
|
|
164
|
+
// The record is the user's selection, version pin included; discovery only supplies one for a bundle not recorded yet.
|
|
165
|
+
const recorded = await matchRecordedSelection(packages, args.selection, token, host);
|
|
166
|
+
if (recorded !== undefined) {
|
|
167
|
+
return recorded;
|
|
168
|
+
}
|
|
169
|
+
const source = await resolveSource(sources, token.sourceToken);
|
|
170
|
+
const descriptors = await packages.discover([source.source.id]);
|
|
171
|
+
const descriptor = resolveDescriptor(descriptors, token, host);
|
|
172
|
+
return Object.freeze({ ...descriptor.coordinates, source: source.projectSource, version: { policy: "latest" } });
|
|
173
|
+
}
|
|
174
|
+
async function matchRecordedSelection(packages, raw, token, host) {
|
|
175
|
+
const records = await packages.list();
|
|
176
|
+
const direct = records.find((record) => packageSelectionMatches(record.selection, raw));
|
|
177
|
+
if (direct !== undefined) {
|
|
178
|
+
return direct.selection;
|
|
179
|
+
}
|
|
180
|
+
const candidates = records.filter((record) => {
|
|
181
|
+
if (record.selection.host !== host) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (sourceToken(record.selection.source) !== token.sourceToken) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (record.selection.host === "pi") {
|
|
188
|
+
return record.selection.root === token.root;
|
|
189
|
+
}
|
|
190
|
+
return record.selection.marketplaceRoot === token.root &&
|
|
191
|
+
(token.pluginName === undefined || record.selection.pluginName === token.pluginName);
|
|
192
|
+
});
|
|
193
|
+
// A token that omits the plugin name is ambiguous across plugins; discovery reports it instead of guessing.
|
|
194
|
+
return candidates.length === 1 ? candidates[0].selection : undefined;
|
|
195
|
+
}
|
|
196
|
+
function resolveDescriptor(descriptors, token, host) {
|
|
197
|
+
const matches = descriptors.filter((descriptor) => {
|
|
198
|
+
if (descriptor.host !== host || bundleRoot(descriptor.coordinates) !== token.root) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return descriptor.coordinates.host !== "claude" || token.pluginName === undefined || descriptor.coordinates.pluginName === token.pluginName;
|
|
202
|
+
});
|
|
203
|
+
if (matches.length === 1) {
|
|
204
|
+
return matches[0];
|
|
205
|
+
}
|
|
206
|
+
if (matches.length > 1) {
|
|
207
|
+
throw new Error(`Package selection ${JSON.stringify(token.root)} is ambiguous for Host ${host}; include #<plugin-name>`);
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`No ${host} Package at ${JSON.stringify(displayRoot(token.root))} in Source ${JSON.stringify(token.sourceToken)}`);
|
|
210
|
+
}
|
|
211
|
+
function parseSelectionToken(selection) {
|
|
212
|
+
const separator = selection.indexOf("::");
|
|
213
|
+
if (separator <= 0 || separator === selection.length - 2) {
|
|
214
|
+
throw new CliUsageError("Package selection must be <source-id>::<bundle-path> or <source-id>::<host>:<bundle-path>");
|
|
215
|
+
}
|
|
216
|
+
const sourceTokenValue = selection.slice(0, separator);
|
|
217
|
+
let coordinate = selection.slice(separator + 2);
|
|
218
|
+
let host;
|
|
219
|
+
for (const candidate of PACKAGE_HOSTS) {
|
|
220
|
+
const prefix = `${candidate}:`;
|
|
221
|
+
if (coordinate.startsWith(prefix)) {
|
|
222
|
+
host = candidate;
|
|
223
|
+
coordinate = coordinate.slice(prefix.length);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const hash = coordinate.indexOf("#");
|
|
228
|
+
const rootPart = hash === -1 ? coordinate : coordinate.slice(0, hash);
|
|
229
|
+
const pluginName = hash === -1 ? undefined : normalizePluginName(coordinate.slice(hash + 1));
|
|
230
|
+
return Object.freeze({
|
|
231
|
+
sourceToken: sourceTokenValue,
|
|
232
|
+
...(host === undefined ? {} : { host }),
|
|
233
|
+
root: normalizeRoot(rootPart),
|
|
234
|
+
...(pluginName === undefined ? {} : { pluginName }),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function resolveHost(token, host) {
|
|
238
|
+
if (token.host !== undefined && host !== undefined && token.host !== host) {
|
|
239
|
+
throw new CliUsageError(`Package selection Host ${token.host} conflicts with --host ${host}`);
|
|
240
|
+
}
|
|
241
|
+
const resolved = host ?? token.host;
|
|
242
|
+
if (resolved === undefined) {
|
|
243
|
+
throw new CliUsageError("Package selection needs --host pi or --host claude unless the selection is host-qualified");
|
|
244
|
+
}
|
|
245
|
+
if (resolved === "pi" && token.pluginName !== undefined) {
|
|
246
|
+
throw new CliUsageError("Pi Package selections do not include a plugin name");
|
|
247
|
+
}
|
|
248
|
+
return resolved;
|
|
249
|
+
}
|
|
250
|
+
function normalizeRoot(value) {
|
|
251
|
+
if (value === "" || value === ".") {
|
|
252
|
+
return "";
|
|
253
|
+
}
|
|
254
|
+
if (value.trim() !== value || value.startsWith("/") || value.includes("\\") || value.includes("#")) {
|
|
255
|
+
throw new CliUsageError(`Package bundle path ${JSON.stringify(value)} is not a Source-relative directory`);
|
|
256
|
+
}
|
|
257
|
+
const parts = value.split("/");
|
|
258
|
+
if (parts.some((part) => part === "" || part === "." || part === ".." || hasControlCharacter(part))) {
|
|
259
|
+
throw new CliUsageError(`Package bundle path ${JSON.stringify(value)} is not a canonical Source-relative directory`);
|
|
260
|
+
}
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
function normalizePluginName(value) {
|
|
264
|
+
if (value === "" || value.trim() !== value || value.includes("/") || value.includes("\\") || value.includes("#") || /\s/u.test(value) || hasControlCharacter(value)) {
|
|
265
|
+
throw new CliUsageError(`Package plugin name ${JSON.stringify(value)} is not valid`);
|
|
266
|
+
}
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
function hasControlCharacter(value) {
|
|
270
|
+
return Array.from(value).some((character) => {
|
|
271
|
+
const code = character.codePointAt(0) ?? 0;
|
|
272
|
+
return code <= 0x1f || code === 0x7f;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
async function resolveSource(sources, token) {
|
|
276
|
+
const matches = (await sources.listSources()).filter((source) => sourceMatchesToken(source, token));
|
|
277
|
+
if (matches.length !== 1) {
|
|
278
|
+
throw new Error(matches.length === 0
|
|
279
|
+
? `Source is not registered: ${token}`
|
|
280
|
+
: `Source token ${JSON.stringify(token)} matches more than one Source`);
|
|
281
|
+
}
|
|
282
|
+
const source = matches[0];
|
|
283
|
+
return Object.freeze({ source, projectSource: projectSourceOf(source) });
|
|
284
|
+
}
|
|
285
|
+
function sourceMatchesToken(source, token) {
|
|
286
|
+
if (source.id === token) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
return source.kind === "git" && (source.url === token || sourceToken(projectSourceOf(source)) === token);
|
|
290
|
+
}
|
|
291
|
+
function sourceToken(source) {
|
|
292
|
+
if (source.kind === "builtin") {
|
|
293
|
+
return source.id;
|
|
294
|
+
}
|
|
295
|
+
return "url" in source ? source.url : source.path;
|
|
296
|
+
}
|
|
297
|
+
/** The registered Source id: derived for a Git Source, and its own token otherwise. */
|
|
298
|
+
function sourceIdToken(source) {
|
|
299
|
+
return "url" in source ? sourceIdForUrl(source.url) : sourceToken(source);
|
|
300
|
+
}
|
|
301
|
+
function formatDescriptorSelection(sourceId, descriptor) {
|
|
302
|
+
return `${sourceId}::${describeSelectionCoordinate(descriptor.coordinates)}`;
|
|
303
|
+
}
|
|
304
|
+
function describeSelectionCoordinate(coordinates) {
|
|
305
|
+
return coordinates.host === "pi"
|
|
306
|
+
? `pi:${displayRoot(coordinates.root)}`
|
|
307
|
+
: `claude:${displayRoot(coordinates.marketplaceRoot)}#${coordinates.pluginName}`;
|
|
308
|
+
}
|
|
309
|
+
function formatPackageRecord(record) {
|
|
310
|
+
const verified = record.verified === undefined ? "unknown" : describeVersion(record.verified);
|
|
311
|
+
return `Package ${formatPackageSelection(record.selection)}\tinstallId=${record.installId}\tverified=${verified}\tverifiedAt=${record.verifiedAt}`;
|
|
312
|
+
}
|
|
313
|
+
function describeInspection(selection, inspection, output) {
|
|
314
|
+
output(`Package inspect: ${formatPackageSelection(selection)}`);
|
|
315
|
+
output(` installed: ${describeEvidence(inspection.installed)}`);
|
|
316
|
+
output(` available: ${inspection.available === undefined ? "unknown" : describeVersion(inspection.available)}`);
|
|
317
|
+
output(` approval: ${inspection.approval}`);
|
|
318
|
+
if (inspection.drift !== undefined) {
|
|
319
|
+
output(` drift: ${inspection.drift}`);
|
|
320
|
+
}
|
|
321
|
+
if (inspection.descriptor !== undefined) {
|
|
322
|
+
describeDescriptor(inspection.descriptor, output, " ");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
export function describeLifecyclePlan(plan, approval, output) {
|
|
326
|
+
output(`Package ${plan.action} preview: ${formatPackageSelection(plan.selection)}`);
|
|
327
|
+
output(` approval: ${approval}`);
|
|
328
|
+
output(` origin: installId=${plan.origin.installId}; manifest=${plan.origin.manifestDigest}; commit=${plan.origin.resolvedCommit ?? "unknown"}`);
|
|
329
|
+
describeDescriptor(plan.descriptor, output, " ");
|
|
330
|
+
output(` host commands (${plan.commands.length}):`);
|
|
331
|
+
if (plan.commands.length === 0) {
|
|
332
|
+
output(" none");
|
|
333
|
+
}
|
|
334
|
+
for (const [index, command] of plan.commands.entries()) {
|
|
335
|
+
output(` [${index}] purpose=${command.purpose}; argv=${JSON.stringify([command.executable, ...command.args])}; resolved=${command.resolvedPath ?? "unresolved"}; summary=${command.summary}`);
|
|
336
|
+
}
|
|
337
|
+
output(` affects (${plan.affects.length}):`);
|
|
338
|
+
for (const coordinate of plan.affects) {
|
|
339
|
+
output(` ${describeCoordinates(coordinate)}`);
|
|
340
|
+
}
|
|
341
|
+
output(` working directory: ${plan.cwd}`);
|
|
342
|
+
output(` environment: ${plan.environment}`);
|
|
343
|
+
if (plan.skipExecution === true) {
|
|
344
|
+
output(" host state already satisfies this action; execution will run no host command");
|
|
345
|
+
}
|
|
346
|
+
for (const warning of plan.warnings) {
|
|
347
|
+
output(` warning: ${warning}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function describeDescriptor(descriptor, output, indent) {
|
|
351
|
+
output(`${indent}descriptor: ${descriptor.name}; host=${descriptor.host}; coordinates=${describeCoordinates(descriptor.coordinates)}`);
|
|
352
|
+
output(`${indent}version: ${descriptor.version === undefined ? "unknown" : describeVersion(descriptor.version)}`);
|
|
353
|
+
output(`${indent}manifest digest: ${descriptor.manifestDigest}`);
|
|
354
|
+
output(`${indent}components (${descriptor.components.length}):`);
|
|
355
|
+
if (descriptor.components.length === 0) {
|
|
356
|
+
output(`${indent} none`);
|
|
357
|
+
}
|
|
358
|
+
for (const component of descriptor.components) {
|
|
359
|
+
output(`${indent} ${describeComponent(component)}`);
|
|
360
|
+
}
|
|
361
|
+
for (const warning of descriptor.warnings) {
|
|
362
|
+
output(`${indent}warning: ${warning}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const COMPONENT_RISK = Object.freeze({
|
|
366
|
+
executable: "high: the host runs it",
|
|
367
|
+
instruction: "medium: the host loads it into agent context",
|
|
368
|
+
data: "low: data only",
|
|
369
|
+
});
|
|
370
|
+
function describeComponent(component) {
|
|
371
|
+
const paths = component.paths.length === 0 ? "none" : component.paths.join(", ");
|
|
372
|
+
const skills = component.skillNames === undefined || component.skillNames.length === 0
|
|
373
|
+
? ""
|
|
374
|
+
: `; skills=${component.skillNames.join(", ")}`;
|
|
375
|
+
return `${component.kind}: ownership=${component.ownership}; effect=${component.effect}; risk=${COMPONENT_RISK[component.effect]}; paths=${paths}${skills}`;
|
|
376
|
+
}
|
|
377
|
+
function describeVersion(version) {
|
|
378
|
+
return version.kind === "manifest-version"
|
|
379
|
+
? `${version.version} (${version.declaredBy})`
|
|
380
|
+
: `commit ${version.commit}`;
|
|
381
|
+
}
|
|
382
|
+
function describeEvidence(evidence) {
|
|
383
|
+
switch (evidence.kind) {
|
|
384
|
+
case "installed": {
|
|
385
|
+
if (evidence.version !== undefined) {
|
|
386
|
+
return `installed ${evidence.version}`;
|
|
387
|
+
}
|
|
388
|
+
if (evidence.commit !== undefined) {
|
|
389
|
+
return `installed commit ${evidence.commit}`;
|
|
390
|
+
}
|
|
391
|
+
return "installed with unknown version";
|
|
392
|
+
}
|
|
393
|
+
case "absent":
|
|
394
|
+
return "absent";
|
|
395
|
+
case "unknown":
|
|
396
|
+
return `unknown (${evidence.reason})`;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function packageResultSucceeded(result) {
|
|
400
|
+
return result.status === "installed" || result.status === "updated" || result.status === "removed" ||
|
|
401
|
+
result.status === "selected" || result.status === "forgotten";
|
|
402
|
+
}
|
|
403
|
+
function displayRoot(root) {
|
|
404
|
+
return root === "" ? "." : root;
|
|
405
|
+
}
|
|
406
|
+
function requireOptionValue(args, index, option) {
|
|
407
|
+
const value = args[index];
|
|
408
|
+
if (!value || value.startsWith("--")) {
|
|
409
|
+
throw new CliUsageError(`Missing value for ${option}`);
|
|
410
|
+
}
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { defaultGitSourceCachePath, GitSourceSnapshotAccess, githubTreeLocation, validateSkillDirectoryPath, } from "./git-source.js";
|
|
4
|
+
import { assertNoSymlinkPath } from "./path-safety.js";
|
|
5
|
+
import { defaultBuiltInSkillsRoot } from "./skill-discovery.js";
|
|
6
|
+
/**
|
|
7
|
+
* The directory scope a Source's snapshot paths are rooted at. A GitHub tree
|
|
8
|
+
* URL scopes its snapshot below that directory, so the Package discovery pass
|
|
9
|
+
* and the owned-Skill guard must read the same scope or they disagree about the
|
|
10
|
+
* same Source.
|
|
11
|
+
*/
|
|
12
|
+
export function sourceBundleScope(source) {
|
|
13
|
+
if (source.kind === "builtin") {
|
|
14
|
+
return Object.freeze({});
|
|
15
|
+
}
|
|
16
|
+
const directory = githubTreeLocation(source.url)?.directory;
|
|
17
|
+
return Object.freeze(directory === undefined ? {} : { directory });
|
|
18
|
+
}
|
|
19
|
+
export function createNodePackageContentAccess(options = {}) {
|
|
20
|
+
return new NodePackageContentAccess(options);
|
|
21
|
+
}
|
|
22
|
+
class NodePackageContentAccess {
|
|
23
|
+
builtInRoot;
|
|
24
|
+
gitSnapshots;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.builtInRoot = options.builtInRoot ?? defaultBuiltInSkillsRoot();
|
|
27
|
+
this.gitSnapshots = new GitSourceSnapshotAccess({
|
|
28
|
+
cachePath: options.gitCachePath ?? defaultGitSourceCachePath(),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async readView(source) {
|
|
32
|
+
return source.kind === "builtin" ? this.builtInView(source.id) : this.gitView(source);
|
|
33
|
+
}
|
|
34
|
+
builtInView(sourceId) {
|
|
35
|
+
return {
|
|
36
|
+
sourceId,
|
|
37
|
+
origin: Object.freeze({ kind: "builtin" }),
|
|
38
|
+
find: async (root, names) => filterByBasename(await listBuiltInFiles(this.builtInRoot, root), names),
|
|
39
|
+
list: async (root) => listBuiltInFiles(this.builtInRoot, root),
|
|
40
|
+
read: async (filePath) => readBuiltInFile(this.builtInRoot, filePath),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async gitView(source) {
|
|
44
|
+
const location = githubTreeLocation(source.url);
|
|
45
|
+
const directory = location?.directory;
|
|
46
|
+
const repositoryUrl = stripUrlScheme(location?.repositoryUrl ?? source.url);
|
|
47
|
+
const ref = source.ref ?? location?.ref;
|
|
48
|
+
const resolvedCommit = await this.gitSnapshots.readResolvedCommit(source);
|
|
49
|
+
const toRepoPath = (sourcePath) => {
|
|
50
|
+
if (sourcePath === "") {
|
|
51
|
+
return directory ?? "";
|
|
52
|
+
}
|
|
53
|
+
return directory === undefined ? sourcePath : `${directory}/${sourcePath}`;
|
|
54
|
+
};
|
|
55
|
+
const toSourcePath = (repoPath) => directory === undefined ? repoPath : repoPath.slice(directory.length + 1);
|
|
56
|
+
return {
|
|
57
|
+
sourceId: source.id,
|
|
58
|
+
origin: Object.freeze({
|
|
59
|
+
kind: "git",
|
|
60
|
+
repositoryUrl,
|
|
61
|
+
...(ref === undefined ? {} : { ref }),
|
|
62
|
+
...(directory === undefined ? {} : { directory }),
|
|
63
|
+
}),
|
|
64
|
+
resolvedCommit,
|
|
65
|
+
find: async (root, names) => filterByBasename(await this.listSourcePaths(source, root, toRepoPath, toSourcePath), names),
|
|
66
|
+
list: async (root) => Object.freeze(await this.listSourcePaths(source, root, toRepoPath, toSourcePath)),
|
|
67
|
+
read: async (filePath) => this.gitSnapshots.readFile(source, toRepoPath(filePath)),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async listSourcePaths(source, root, toRepoPath, toSourcePath) {
|
|
71
|
+
const files = (await this.gitSnapshots.listFiles(source, toRepoPath(root))).map(toSourcePath);
|
|
72
|
+
files.sort((left, right) => left.localeCompare(right));
|
|
73
|
+
return files;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Strips the scheme prefix, e.g. `https://github.com/owner/repo` becomes `github.com/owner/repo`. */
|
|
77
|
+
function stripUrlScheme(repositoryUrl) {
|
|
78
|
+
const separator = repositoryUrl.indexOf("://");
|
|
79
|
+
return separator >= 0 ? repositoryUrl.slice(separator + 3) : repositoryUrl;
|
|
80
|
+
}
|
|
81
|
+
function filterByBasename(files, names) {
|
|
82
|
+
const wanted = new Set(names);
|
|
83
|
+
return Object.freeze(files.filter((filePath) => wanted.has(path.posix.basename(filePath))));
|
|
84
|
+
}
|
|
85
|
+
async function pathKind(candidate) {
|
|
86
|
+
try {
|
|
87
|
+
const information = await lstat(candidate);
|
|
88
|
+
if (information.isSymbolicLink()) {
|
|
89
|
+
return "symlink";
|
|
90
|
+
}
|
|
91
|
+
return information.isDirectory() ? "directory" : "file";
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
95
|
+
return "missing";
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function listBuiltInFiles(root, selectedRoot) {
|
|
101
|
+
const relativeRoot = selectedRoot === "" ? "" : validateSkillDirectoryPath(selectedRoot);
|
|
102
|
+
if ((await pathKind(root)) !== "directory") {
|
|
103
|
+
throw new Error(`Built-in Source root is not a directory: ${root}`);
|
|
104
|
+
}
|
|
105
|
+
await assertNoSymlinkPath(root, "Built-in Source root");
|
|
106
|
+
const selectedDirectory = relativeRoot === "" ? root : path.join(root, ...relativeRoot.split("/"));
|
|
107
|
+
if (relativeRoot !== "") {
|
|
108
|
+
if ((await pathKind(selectedDirectory)) !== "directory") {
|
|
109
|
+
return Object.freeze([]);
|
|
110
|
+
}
|
|
111
|
+
await assertNoSymlinkPath(selectedDirectory, "Built-in Source path");
|
|
112
|
+
}
|
|
113
|
+
const files = [];
|
|
114
|
+
await walkBuiltInFiles(root, relativeRoot, files);
|
|
115
|
+
files.sort((left, right) => left.localeCompare(right));
|
|
116
|
+
return Object.freeze(files);
|
|
117
|
+
}
|
|
118
|
+
async function readBuiltInFile(root, selectedPath) {
|
|
119
|
+
const relativePath = validateSkillDirectoryPath(selectedPath);
|
|
120
|
+
if ((await pathKind(root)) !== "directory") {
|
|
121
|
+
throw new Error(`Built-in Source root is not a directory: ${root}`);
|
|
122
|
+
}
|
|
123
|
+
await assertNoSymlinkPath(root, "Built-in Source root");
|
|
124
|
+
const absolutePath = path.join(root, ...relativePath.split("/"));
|
|
125
|
+
if ((await pathKind(absolutePath)) !== "file") {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
await assertNoSymlinkPath(absolutePath, "Built-in Source path");
|
|
129
|
+
return readFile(absolutePath, "utf8");
|
|
130
|
+
}
|
|
131
|
+
async function walkBuiltInFiles(root, relativeDirectory, files) {
|
|
132
|
+
const absoluteDirectory = relativeDirectory === "" ? root : path.join(root, ...relativeDirectory.split("/"));
|
|
133
|
+
const entries = await readdir(absoluteDirectory, { withFileTypes: true });
|
|
134
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
const relativePath = relativeDirectory === "" ? entry.name : `${relativeDirectory}/${entry.name}`;
|
|
137
|
+
try {
|
|
138
|
+
validateSkillDirectoryPath(relativePath);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
throw new Error(`Built-in Source contains an unsafe path: ${relativePath}`);
|
|
142
|
+
}
|
|
143
|
+
if (entry.isSymbolicLink()) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const absolutePath = path.join(root, ...relativePath.split("/"));
|
|
147
|
+
if (entry.isDirectory()) {
|
|
148
|
+
await assertNoSymlinkPath(absolutePath, "Built-in Source path");
|
|
149
|
+
await walkBuiltInFiles(root, relativePath, files);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (!entry.isFile()) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
await assertNoSymlinkPath(absolutePath, "Built-in Source path");
|
|
156
|
+
files.push(relativePath);
|
|
157
|
+
}
|
|
158
|
+
}
|