@iseoane/agent-depot 0.2.0 → 0.3.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +117 -3
  3. package/dist/src/app-cli.js +172 -0
  4. package/dist/src/app-flow.js +457 -0
  5. package/dist/src/app-latest.js +63 -0
  6. package/dist/src/app-owned-skills.js +42 -0
  7. package/dist/src/app-recipes.js +116 -0
  8. package/dist/src/app-schema.js +45 -0
  9. package/dist/src/app-version.js +46 -0
  10. package/dist/src/atomic-file.js +26 -0
  11. package/dist/src/cli.js +1256 -0
  12. package/dist/src/git-source.js +409 -0
  13. package/dist/src/path-safety.js +31 -0
  14. package/dist/src/process-runner.js +41 -0
  15. package/dist/src/project-installation.js +1411 -0
  16. package/dist/src/project-manifest.js +511 -0
  17. package/dist/src/skill-adoption.js +77 -0
  18. package/dist/src/skill-discovery.js +467 -0
  19. package/dist/src/skill-hosts.js +103 -0
  20. package/dist/src/skill-install.js +374 -0
  21. package/dist/src/skill-removal.js +119 -0
  22. package/dist/src/skill-update.js +244 -0
  23. package/dist/src/source-state.js +240 -0
  24. package/dist/src/sources.js +391 -0
  25. package/dist/src/tui/adoption-actions.js +144 -0
  26. package/dist/src/tui/app-actions.js +157 -0
  27. package/dist/src/tui/app.js +63 -0
  28. package/dist/src/tui/batch.js +17 -0
  29. package/dist/src/tui/bulk-actions.js +243 -0
  30. package/dist/src/tui/catalog-actions.js +9 -0
  31. package/dist/src/tui/catalog-browse.js +59 -0
  32. package/dist/src/tui/catalog-filter.js +20 -0
  33. package/dist/src/tui/catalog-flow.js +107 -0
  34. package/dist/src/tui/catalog-installs.js +193 -0
  35. package/dist/src/tui/catalog-panel.js +47 -0
  36. package/dist/src/tui/catalog-state.js +92 -0
  37. package/dist/src/tui/catalog-tree.js +35 -0
  38. package/dist/src/tui/catalog-view.js +89 -0
  39. package/dist/src/tui/environment.js +1 -0
  40. package/dist/src/tui/installations-browse.js +136 -0
  41. package/dist/src/tui/installations-mode.js +1 -0
  42. package/dist/src/tui/installations-panel.js +27 -0
  43. package/dist/src/tui/installations-rows.js +11 -0
  44. package/dist/src/tui/installations-tree.js +80 -0
  45. package/dist/src/tui/installations-view.js +154 -0
  46. package/dist/src/tui/installations.js +172 -0
  47. package/dist/src/tui/keys.js +31 -0
  48. package/dist/src/tui/manage-actions.js +174 -0
  49. package/dist/src/tui/manage-panel.js +65 -0
  50. package/dist/src/tui/mode-keys.js +61 -0
  51. package/dist/src/tui/panel-parts.js +23 -0
  52. package/dist/src/tui/render.js +8 -0
  53. package/dist/src/tui/sources-mode.js +17 -0
  54. package/dist/src/tui/sources-view.js +276 -0
  55. package/dist/src/tui/theme.js +29 -0
  56. package/dist/src/tui/tree-navigation.js +68 -0
  57. package/dist/src/tui/tree.js +53 -0
  58. package/dist/src/tui/unmanaged-actions.js +178 -0
  59. package/dist/src/tui/updates-flow.js +89 -0
  60. package/dist/src/tui/updates-keys.js +104 -0
  61. package/dist/src/tui/updates-mode.js +1 -0
  62. package/dist/src/tui/updates-model.js +48 -0
  63. package/dist/src/tui/updates-panel.js +44 -0
  64. package/dist/src/tui/updates-view.js +101 -0
  65. package/dist/src/tui/updates.js +230 -0
  66. package/dist/src/tui/view-state.js +42 -0
  67. package/dist/src/tui/window.js +31 -0
  68. package/dist/src/unmanaged-removal.js +60 -0
  69. package/dist/src/update-batch.js +376 -0
  70. package/dist/src/update-flow.js +135 -0
  71. package/dist/src/usage-error.js +7 -0
  72. package/dist/src/user-global-skill-inventory.js +565 -0
  73. package/dist/src/user-method.js +14 -0
  74. package/package.json +7 -4
  75. package/skills/agent-depot-apprecipe/SKILL.md +44 -0
  76. package/skills/agent-depot-apprecipe/assets/example.md +25 -0
  77. package/skills/doctor-md-agents/scripts/test_collect_sessions.py +0 -780
  78. package/skills/doctor-md-agents/scripts/test_extract_rules.py +0 -530
  79. package/skills/doctor-md-agents/scripts/test_render_report.py +0 -383
  80. package/skills/doctor-md-agents/scripts/test_scan_corrections.py +0 -343
  81. package/skills/doctor-md-agents/scripts/test_verify_citations.py +0 -236
  82. package/skills/doctor-md-agents/scripts/testdata/old_extract_rules_pre_t1.py.txt +0 -507
  83. package/skills/doctor-md-skill/scripts/test_collect_sessions.py +0 -593
  84. package/skills/doctor-md-skill/scripts/test_detect_machine_sessions.py +0 -181
  85. package/skills/doctor-md-skill/scripts/test_render_report.py +0 -241
  86. package/skills/doctor-md-skill/scripts/test_verify_citations.py +0 -196
@@ -0,0 +1,1256 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { fileURLToPath } from "node:url";
5
+ import path from "node:path";
6
+ import { checkAppUpdates, createAppOperations } from "./app-flow.js";
7
+ import { describeAppResult, describeManualCompletion, runAppCommand } from "./app-cli.js";
8
+ import { pathsOverlap } from "./path-safety.js";
9
+ import { skillTreeBaseline } from "./skill-discovery.js";
10
+ import { BuiltInSourceError, createSourceOperations, resolveProjectSource, SourceMetadataError, SourceNotFoundError, } from "./sources.js";
11
+ import { AGENT_DEPOT_PACKAGE_NAME, AGENT_DEPOT_PACKAGE_VERSION, builtinPinMismatch, defaultProjectManifestPath, parseProjectManifest, ProjectManifestStore, PROJECT_HOSTS, } from "./project-manifest.js";
12
+ import { inspectProjectSkillInstallation, assertNoOverlappingProjectSkillRemovalTargets, } from "./project-installation.js";
13
+ import { selectUpdateBatch, UpdateBatchSelectionError, } from "./update-batch.js";
14
+ import { describeUnmanagedRemoval, removeInspectedUnmanagedSkills } from "./unmanaged-removal.js";
15
+ import { executeHostAddition, executeHostRemoval, planHostAddition, planHostRemoval } from "./skill-hosts.js";
16
+ import { executeSkillRemoval, inspectSkillRemovals, planSkillRemoval, describeSkillRemoval, recheckSkillRemoval, removeSkillFiles, removeSkillsAndRecords, selectUserGlobalSkills, SkillSelectionError, userGlobalRemovalOptions, } from "./skill-removal.js";
17
+ import { defaultProjectSkillTreeAccess, executeOrRejectMethod, executeSingleInstall, externalMethodFailure, isValidFixedVersion, findSource, installOne, outputInstallationResult, outputInstallPreview, outputSingleInstallPreview, planSingleInstall, refreshResolvedSource, rejectInstallationCollision, resolveSelection, rollbackTransactions, } from "./skill-install.js";
18
+ import { CliUsageError } from "./usage-error.js";
19
+ import { selectionWithInstallation } from "./skill-update.js";
20
+ import { applyConfirmedUpdates, describeUpdatePreview, findPathConfirmationProblem, loadUpdates, previewUpdateCandidates } from "./update-flow.js";
21
+ import { assertNoOverlappingUserGlobalSkillRemovals, inspectUserGlobalSkillRemoval, scanUserGlobalSkillInventory, } from "./user-global-skill-inventory.js";
22
+ const USAGE = [
23
+ "Usage:",
24
+ " agent-depot source list",
25
+ " agent-depot source add <url>",
26
+ " agent-depot source refresh <id> [--yes]",
27
+ " agent-depot source remove <id> [--skill <id|path>...] [--all] [--yes]",
28
+ " agent-depot source migrate <old-id> <new-id> (--skill <path>... | --all) [--yes]",
29
+ " agent-depot discover <source-id> [source-id...]",
30
+ " agent-depot install --scope <project|user-global> --source <id> --skill <path> --host <host>... --version <latest|version> [--ref <git-ref>] [--method <json>] --portable-v1 [--yes] [--confirm-additional-host]",
31
+ " agent-depot install --scope project --manifest --portable-v1 [--yes] [--confirm-additional-host]",
32
+ " agent-depot update check --scope <project|user-global>",
33
+ " agent-depot update apply --scope <project|user-global> (--all | --skill <id|path>... | --app <name>...) [--yes] [--confirm-path <relative-path>...]",
34
+ " Apps run after Skills; --all --scope project skips Apps.",
35
+ " agent-depot skill remove <id|path>... [--yes]",
36
+ " agent-depot skill remove <id|path> --host <host>... [--yes]",
37
+ " agent-depot skill host add <id|path> --host <host>... [--yes] [--confirm-additional-host]",
38
+ " agent-depot uninstall [--skills] [--unmanaged-skill <exact-global-path>...] [--data] [--cli] [--yes]",
39
+ " agent-depot app schema|list",
40
+ " agent-depot app validate <file>...",
41
+ " agent-depot app approve <name> [--yes]",
42
+ " agent-depot app install|update|uninstall <name> [--yes] [--manual-done]",
43
+ " agent-depot app uninstall <name> --forget [--yes]",
44
+ " agent-depot tui",
45
+ " agent-depot --version",
46
+ ].join("\n");
47
+ const INSTALL_USAGE = "Usage: agent-depot install --scope <project|user-global> --source <id> --skill <path> --host <host>... --version <latest|version> [--ref <git-ref>] [--method <json>] --portable-v1 [--overwrite --yes] [--confirm-additional-host]";
48
+ const DISCOVER_USAGE = "Usage: agent-depot discover <source-id> [source-id...]\nSelect at least one Source ID explicitly; run `agent-depot source list` to see registered Sources";
49
+ const REMOVE_SOURCE_USAGE = "Usage: agent-depot source remove <id> [--skill <id|path>...] [--all] [--yes]";
50
+ const MIGRATE_SOURCE_USAGE = "Usage: agent-depot source migrate <old-id> <new-id> (--skill <path>... | --all) [--yes]";
51
+ const SKILL_REMOVE_USAGE = "Usage: agent-depot skill remove <id|path>... [--yes]\n agent-depot skill remove <id|path> --host <host>... [--yes]";
52
+ const SKILL_HOST_ADD_USAGE = "Usage: agent-depot skill host add <id|path> --host <host>... [--yes] [--confirm-additional-host]";
53
+ const UNINSTALL_USAGE = "Usage: agent-depot uninstall [--skills] [--unmanaged-skill <exact-global-path>...] [--data] [--cli] [--yes]";
54
+ const SOURCE_SUBCOMMANDS = new Map([
55
+ ["list", async (values, { operations, output }) => {
56
+ requireArgumentCount(values, 0, "source list");
57
+ for (const source of await operations.listSources()) {
58
+ output(formatSource(source));
59
+ }
60
+ }],
61
+ ["add", async (values, { operations, output }) => {
62
+ requireArgumentCount(values, 1, "source add <url>");
63
+ const source = await operations.addGitSource(values[0]);
64
+ output(`Added Git Source: ${source.id}\t${source.url}`);
65
+ }],
66
+ ["refresh", runSourceRefresh],
67
+ ["remove", (values, { operations, dependencies, output }) => runSourceRemoval(values, operations, dependencies, output)],
68
+ ["migrate", (values, { operations, dependencies, output }) => runSourceMigration(values, operations, dependencies, output)],
69
+ ]);
70
+ const COMMANDS = new Map([
71
+ ["app", (values, { dependencies, output }) => runAppCommand(values, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output)],
72
+ ["install", async (values, { operations, dependencies, output }) => {
73
+ await runInstall(values, operations, dependencies, output);
74
+ return 0;
75
+ }],
76
+ ["update", (values, { operations, dependencies, output }) => runUpdate(values, operations, dependencies, output)],
77
+ ["skill", async (values, { operations, dependencies, output }) => {
78
+ if (values[0] === "remove") {
79
+ await runSkillRemoval(values.slice(1), operations, dependencies, output);
80
+ }
81
+ else if (values[0] === "host" && values[1] === "add") {
82
+ await runSkillHostAdd(values.slice(2), operations, dependencies, output);
83
+ }
84
+ else {
85
+ throw new CliUsageError(USAGE);
86
+ }
87
+ return 0;
88
+ }],
89
+ ["uninstall", async (values, { operations, dependencies, output }) => {
90
+ await runUninstall(values, operations, dependencies, output);
91
+ return 0;
92
+ }],
93
+ ["discover", async (values, { operations, output }) => {
94
+ requireSourceIds(values);
95
+ const discoverSkills = operations.discoverSkills;
96
+ if (!discoverSkills) {
97
+ throw new CliUsageError("Discovery is unavailable in the configured Source operations");
98
+ }
99
+ let candidates;
100
+ try {
101
+ candidates = await discoverSkills.call(operations, values);
102
+ }
103
+ catch (error) {
104
+ throw new Error(formatDiscoveryError(error));
105
+ }
106
+ for (const candidate of candidates) {
107
+ output(formatCandidate(candidate));
108
+ }
109
+ return 0;
110
+ }],
111
+ ["tui", async (_values, { operations, dependencies }) => {
112
+ const isInteractive = dependencies.isInteractive ?? (() => process.stdin.isTTY === true);
113
+ if (!isInteractive()) {
114
+ throw new Error("agent-depot tui requires an interactive terminal");
115
+ }
116
+ // Loaded lazily so non-interactive commands never import Ink/React.
117
+ const renderTui = dependencies.renderTui ?? (await import("./tui/render.js")).renderTui;
118
+ await renderTui(operations, {
119
+ homeDirectory: dependencies.homeDirectory,
120
+ projectRoot: dependencies.projectRoot,
121
+ sourceAccess: dependencies.sourceAccess,
122
+ installationOptions: dependencies.installationOptions,
123
+ projectManifestStore: dependencies.projectManifestStore,
124
+ });
125
+ return 0;
126
+ }],
127
+ ["source", async (values, context) => {
128
+ const subcommand = SOURCE_SUBCOMMANDS.get(values[0] ?? "");
129
+ if (!subcommand) {
130
+ throw new CliUsageError(USAGE);
131
+ }
132
+ await subcommand(values.slice(1), context);
133
+ return 0;
134
+ }],
135
+ ]);
136
+ async function runSourceRefresh(values, { operations, output }) {
137
+ if (values.length < 1 || values.length > 2 || (values.length === 2 && values[1] !== "--yes")) {
138
+ throw new CliUsageError("Usage: agent-depot source refresh <id> [--yes]");
139
+ }
140
+ const source = await findSource(operations, values[0]);
141
+ if (source.kind === "builtin") {
142
+ throw new BuiltInSourceError();
143
+ }
144
+ output(`Preview: refresh Git Source ${source.id} from ${source.url}`);
145
+ if (values[1] !== "--yes") {
146
+ throw new CliUsageError("Refresh not confirmed; rerun with --yes to continue");
147
+ }
148
+ const refreshed = await operations.refreshSource(source.id);
149
+ output(`Refreshed Git Source: ${refreshed.id}\t${refreshed.url}`);
150
+ }
151
+ /** Runs the CLI application and returns a process exit code. */
152
+ export async function runCli(argv = process.argv.slice(2), dependencies = {}) {
153
+ const output = dependencies.stdout ?? ((line) => console.log(line));
154
+ const errorOutput = dependencies.stderr ?? ((line) => console.error(line));
155
+ const operations = dependencies.operations ?? createSourceOperations({ homeDirectory: dependencies.homeDirectory });
156
+ try {
157
+ if (argv[0] === "--version" || argv[0] === "-v") {
158
+ if (argv.length !== 1)
159
+ throw new CliUsageError(USAGE);
160
+ if (AGENT_DEPOT_PACKAGE_VERSION === undefined)
161
+ throw new Error("Unable to determine the Agent Depot package version");
162
+ output(AGENT_DEPOT_PACKAGE_VERSION);
163
+ return 0;
164
+ }
165
+ if (argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
166
+ if (argv.length !== 1)
167
+ throw new CliUsageError(USAGE);
168
+ output(USAGE);
169
+ return 0;
170
+ }
171
+ const command = COMMANDS.get(argv[0] ?? "");
172
+ if (!command) {
173
+ throw new CliUsageError(USAGE);
174
+ }
175
+ return await command(argv.slice(1), { operations, dependencies, output });
176
+ }
177
+ catch (error) {
178
+ errorOutput(`Error: ${error instanceof Error ? error.message : "unknown error"}`);
179
+ return 1;
180
+ }
181
+ }
182
+ /** Alias retained as a discoverable public CLI seam for embedding and tests. */
183
+ export const main = runCli;
184
+ async function runSourceRemoval(argv, operations, dependencies, output) {
185
+ const options = parseRemoveSourceOptions(argv);
186
+ const source = await findSource(operations, options.sourceId);
187
+ if (source.kind === "builtin") {
188
+ throw new BuiltInSourceError();
189
+ }
190
+ if (!operations.listUserGlobalInstallations || !operations.removeGitSource) {
191
+ throw new Error("Configured Source operations cannot remove user-global Sources");
192
+ }
193
+ const installations = await operations.listUserGlobalInstallations();
194
+ const dependent = installations.filter((selection) => selection.source.kind === "external" && "url" in selection.source && selection.source.url === source.url);
195
+ const selected = options.all
196
+ ? dependent
197
+ : resolveRemovalSelections(dependent, options.requested);
198
+ const removalOptions = userGlobalRemovalOptions(path.resolve(dependencies.homeDirectory ?? homedir()), dependencies.installationOptions);
199
+ output(`Preview: remove Git Source ${source.id} from ${source.url}`);
200
+ output(`Dependent user-global Skills (${dependent.length}):`);
201
+ for (const [index, selection] of dependent.entries()) {
202
+ const location = selection.installation?.path ?? "missing installation location";
203
+ output(` [${index}] ${selection.path} -> ${location}${selected.includes(selection) ? " (selected for removal)" : " (kept)"}`);
204
+ }
205
+ output(" Default: keep all dependent Skills tracked by default after Source removal");
206
+ const plan = await inspectSkillRemovals(selected, removalOptions, (selection, inspection) => {
207
+ for (const line of describeSkillRemoval(selection, inspection, "Skill "))
208
+ output(line);
209
+ });
210
+ assertNoOverlappingProjectSkillRemovalTargets(plan.inspections);
211
+ requireConfirmation(options.confirmed, "Removal not confirmed; rerun with --yes after reviewing every Source and Skill preview");
212
+ // Recheck every selected target before deleting the first one. A changed later
213
+ // target must never result in a partially removed batch.
214
+ const rechecked = await recheckSkillRemoval(plan, removalOptions);
215
+ await removeSkillFiles(plan, rechecked, removalOptions);
216
+ await operations.removeGitSource(source.id, selected);
217
+ output(`Removed Git Source: ${source.id}`);
218
+ if (selected.length > 0) {
219
+ output(`Removed ${selected.length} dependent user-global Skill${selected.length === 1 ? "" : "s"}; other dependent Skills remain tracked`);
220
+ }
221
+ }
222
+ function parseMigrateSourceOptions(argv) {
223
+ if (argv.length < 2 || argv[0]?.startsWith("--") || argv[1]?.startsWith("--")) {
224
+ throw new CliUsageError(MIGRATE_SOURCE_USAGE);
225
+ }
226
+ const oldSourceId = argv[0];
227
+ const newSourceId = argv[1];
228
+ const requested = [];
229
+ let all = false;
230
+ let confirmed = false;
231
+ for (let index = 2; index < argv.length; index += 1) {
232
+ switch (argv[index]) {
233
+ case "--all":
234
+ if (all || requested.length > 0)
235
+ throw new CliUsageError(MIGRATE_SOURCE_USAGE);
236
+ all = true;
237
+ break;
238
+ case "--skill":
239
+ if (all)
240
+ throw new CliUsageError(MIGRATE_SOURCE_USAGE);
241
+ requested.push(requireOptionValue(argv, ++index, "--skill"));
242
+ break;
243
+ case "--yes":
244
+ confirmed = true;
245
+ break;
246
+ default:
247
+ throw new CliUsageError(MIGRATE_SOURCE_USAGE);
248
+ }
249
+ }
250
+ if (all === (requested.length > 0)) {
251
+ throw new CliUsageError("Source migration requires exactly one selection mode: --all or one or more --skill paths");
252
+ }
253
+ return { oldSourceId, newSourceId, all, requested: Object.freeze(requested), confirmed };
254
+ }
255
+ async function runSourceMigration(argv, operations, _dependencies, output) {
256
+ const options = parseMigrateSourceOptions(argv);
257
+ if (!operations.listUserGlobalInstallations || !operations.migrateUserGlobalInstallations) {
258
+ throw new Error("Configured Source operations cannot migrate user-global Source selections");
259
+ }
260
+ const oldSource = await findSource(operations, options.oldSourceId);
261
+ const newSource = await findSource(operations, options.newSourceId);
262
+ if (oldSource.kind !== "git" || newSource.kind !== "git") {
263
+ throw new CliUsageError("Source migration requires two registered Git Sources; the built-in Source cannot be migrated");
264
+ }
265
+ if (oldSource.id === newSource.id) {
266
+ throw new CliUsageError("Source migration requires two different registered Git Sources");
267
+ }
268
+ const installations = await operations.listUserGlobalInstallations();
269
+ const dependent = installations.filter((selection) => selection.source.kind === "external" && "url" in selection.source && selection.source.url === oldSource.url);
270
+ const selected = options.all ? dependent : resolveMigrationSelections(dependent, options.requested);
271
+ const migrations = [];
272
+ output(`Preview: migrate user-global selections from ${oldSource.id} (${oldSource.url}) to ${newSource.id} (${newSource.url})`);
273
+ output(`Dependent user-global Skills (${dependent.length}):`);
274
+ for (const selection of dependent) {
275
+ output(` ${selection.path}${selected.includes(selection) ? " (selected for migration)" : " (kept)"}`);
276
+ }
277
+ for (const selection of selected) {
278
+ const migration = await planUserGlobalSkillMigration(selection, newSource, operations);
279
+ migrations.push(migration);
280
+ const installation = migration.to.installation;
281
+ output(` migrate ${JSON.stringify(selection.path)} -> ${JSON.stringify(migration.to.source)}`);
282
+ output(` retain installation at ${JSON.stringify(installation?.path ?? "missing installation location")}; no files or project manifests are changed`);
283
+ if (selection.installation?.baseline && installation?.resolvedVersion) {
284
+ output(" preserve content baseline and replace resolved version with evidence from the new Source");
285
+ }
286
+ else if (selection.installation?.baseline) {
287
+ output(" preserve content baseline; drop old resolved version evidence because it belongs to the old Source");
288
+ }
289
+ else {
290
+ output(" preserve installation location/adoption only; no trusted version evidence is carried over");
291
+ }
292
+ }
293
+ requireConfirmation(options.confirmed, "Migration not confirmed; rerun with --yes after reviewing every selected Skill");
294
+ await operations.migrateUserGlobalInstallations(oldSource.id, newSource.id, migrations);
295
+ output(`Migrated ${migrations.length} user-global Skill${migrations.length === 1 ? "" : "s"}; physical installations and project manifests were unchanged`);
296
+ }
297
+ async function planUserGlobalSkillMigration(selection, newSource, operations) {
298
+ const snapshot = operations.readSkillTreeSnapshot
299
+ ? await operations.readSkillTreeSnapshot(newSource, selection.path)
300
+ : undefined;
301
+ assertReplacementSnapshot(selection, snapshot);
302
+ const contentMatches = selection.installation?.baseline !== undefined && snapshot !== undefined &&
303
+ selection.installation.baseline.digest === skillTreeBaseline(snapshot.files).digest;
304
+ const source = selection.version.policy === "fixed"
305
+ ? { kind: "external", url: newSource.url, ref: selection.version.version }
306
+ : { kind: "external", url: newSource.url };
307
+ const installation = selection.installation === undefined
308
+ ? undefined
309
+ : {
310
+ path: selection.installation.path,
311
+ adopted: selection.installation.adopted,
312
+ ...(selection.installation.baseline === undefined ? {} : { baseline: selection.installation.baseline }),
313
+ ...(contentMatches && snapshot?.resolvedVersion !== undefined ? { resolvedVersion: snapshot.resolvedVersion } : {}),
314
+ };
315
+ return {
316
+ from: selection,
317
+ to: {
318
+ ...selection,
319
+ source,
320
+ ...(installation === undefined ? {} : { installation }),
321
+ },
322
+ };
323
+ }
324
+ function assertReplacementSnapshot(selection, snapshot) {
325
+ if (snapshot !== undefined && !snapshot.files.some((file) => file.path === `${selection.path}/SKILL.md`)) {
326
+ throw new Error(`Replacement Source does not contain selected Skill ${JSON.stringify(selection.path)}`);
327
+ }
328
+ if (selection.version.policy === "fixed" &&
329
+ (snapshot?.resolvedVersion?.kind !== "git-commit" || snapshot.resolvedVersion.commit !== selection.version.version)) {
330
+ throw new Error(`Cannot migrate fixed Skill ${JSON.stringify(selection.path)} without matching version evidence from the replacement Source`);
331
+ }
332
+ }
333
+ function resolveMigrationSelections(dependent, requested) {
334
+ const selected = [];
335
+ for (const value of requested) {
336
+ const candidate = dependent.find((selection) => selection.path === value);
337
+ if (!candidate) {
338
+ throw new CliUsageError(`Unknown dependent user-global Skill selection ${JSON.stringify(value)}`);
339
+ }
340
+ if (!selected.includes(candidate))
341
+ selected.push(candidate);
342
+ }
343
+ return Object.freeze(selected);
344
+ }
345
+ function parseRemoveSourceOptions(argv) {
346
+ if (argv.length === 0 || argv[0]?.startsWith("--")) {
347
+ throw new CliUsageError(REMOVE_SOURCE_USAGE);
348
+ }
349
+ const sourceId = argv[0];
350
+ const requested = [];
351
+ let all = false;
352
+ let confirmed = false;
353
+ for (let index = 1; index < argv.length; index += 1) {
354
+ switch (argv[index]) {
355
+ case "--all":
356
+ if (all)
357
+ throw new CliUsageError(REMOVE_SOURCE_USAGE);
358
+ if (requested.length > 0)
359
+ throw new CliUsageError("source remove cannot combine --all with --skill");
360
+ all = true;
361
+ break;
362
+ case "--skill":
363
+ if (all)
364
+ throw new CliUsageError("source remove cannot combine --all with --skill");
365
+ requested.push(requireOptionValue(argv, ++index, "--skill"));
366
+ break;
367
+ case "--yes":
368
+ confirmed = true;
369
+ break;
370
+ default:
371
+ throw new CliUsageError(REMOVE_SOURCE_USAGE);
372
+ }
373
+ }
374
+ return { sourceId, all, requested: Object.freeze(requested), confirmed };
375
+ }
376
+ function resolveRemovalSelections(dependent, requested) {
377
+ const selected = [];
378
+ for (const value of requested) {
379
+ const pathMatch = dependent.find((selection) => selection.path === value);
380
+ const numeric = /^\d+$/u.test(value) ? Number(value) : undefined;
381
+ const candidate = pathMatch ?? (numeric !== undefined && Number.isSafeInteger(numeric)
382
+ ? dependent[numeric]
383
+ : undefined);
384
+ if (!candidate) {
385
+ throw new CliUsageError(`Unknown dependent user-global Skill selection ${JSON.stringify(value)}`);
386
+ }
387
+ if (!selected.includes(candidate))
388
+ selected.push(candidate);
389
+ }
390
+ return Object.freeze(selected);
391
+ }
392
+ async function runSkillRemoval(argv, operations, dependencies, output) {
393
+ const requested = [];
394
+ const hosts = [];
395
+ let confirmed = false;
396
+ for (let index = 0; index < argv.length; index += 1) {
397
+ const argument = argv[index];
398
+ if (argument === "--yes") {
399
+ confirmed = true;
400
+ }
401
+ else if (argument === "--host") {
402
+ collectHosts(hosts, requireOptionValue(argv, ++index, argument));
403
+ }
404
+ else if (argument.startsWith("--")) {
405
+ throw new CliUsageError(SKILL_REMOVE_USAGE);
406
+ }
407
+ else {
408
+ requested.push(argument);
409
+ }
410
+ }
411
+ if (requested.length === 0 || (hosts.length > 0 && requested.length !== 1))
412
+ throw new CliUsageError(SKILL_REMOVE_USAGE);
413
+ if (!operations.listUserGlobalInstallations || !operations.removeUserGlobalInstallations) {
414
+ throw new Error("Configured Source operations cannot remove user-global Skills");
415
+ }
416
+ const installations = await operations.listUserGlobalInstallations();
417
+ const selected = selectManagedSkills(installations, requested);
418
+ const removalOptions = userGlobalRemovalOptions(path.resolve(dependencies.homeDirectory ?? homedir()), dependencies.installationOptions);
419
+ if (hosts.length > 0) {
420
+ await runSkillHostRemoval(selected[0], hosts, installations, confirmed, operations, removalOptions, output);
421
+ return;
422
+ }
423
+ output(`Preview: remove ${selected.length} user-global Skill${selected.length === 1 ? "" : "s"}`);
424
+ const plan = await planSkillRemoval(selected, removalOptions);
425
+ for (const line of plan.preview)
426
+ output(line);
427
+ output(" Installation records are reconciled after each successful Skill removal");
428
+ requireConfirmation(confirmed, "Removal not confirmed; rerun with --yes after reviewing every Skill preview");
429
+ await executeSkillRemoval(plan, operations, removalOptions, installations);
430
+ output(`Removed ${selected.length} user-global Skill${selected.length === 1 ? "" : "s"}`);
431
+ }
432
+ /** Resolves `<id|path>` arguments to managed installations, reporting selection problems as usage errors. */
433
+ function selectManagedSkills(installations, requested) {
434
+ try {
435
+ return selectUserGlobalSkills(installations, requested);
436
+ }
437
+ catch (error) {
438
+ if (error instanceof SkillSelectionError)
439
+ throw new CliUsageError(error.message);
440
+ throw error;
441
+ }
442
+ }
443
+ async function runSkillHostRemoval(selection, hosts, installations, confirmed, operations, options, output) {
444
+ const plan = await planHostRemoval(selection, hosts, options).catch(rethrowSelectionError);
445
+ output(plan.kind === "full"
446
+ ? "Preview: remove 1 user-global Skill (every recorded Host is removed)"
447
+ : `Preview: remove ${hosts.length} Host${hosts.length === 1 ? "" : "s"} from 1 user-global Skill`);
448
+ for (const line of plan.kind === "full" ? plan.plan.preview : plan.preview)
449
+ output(line);
450
+ output(" The installation record is reconciled after the locations are removed");
451
+ requireConfirmation(confirmed, "Removal not confirmed; rerun with --yes after reviewing the preview");
452
+ await executeHostRemoval(plan, operations, options, installations);
453
+ output(plan.kind === "full"
454
+ ? "Removed 1 user-global Skill"
455
+ : `Removed hosts ${plan.removed.join(", ")} from ${JSON.stringify(selection.path)}`);
456
+ }
457
+ async function runSkillHostAdd(argv, operations, dependencies, output) {
458
+ const requested = [];
459
+ const hosts = [];
460
+ let confirmed = false;
461
+ let confirmAdditionalHost = false;
462
+ for (let index = 0; index < argv.length; index += 1) {
463
+ const argument = argv[index];
464
+ if (argument === "--yes") {
465
+ confirmed = true;
466
+ }
467
+ else if (argument === "--confirm-additional-host") {
468
+ confirmAdditionalHost = true;
469
+ }
470
+ else if (argument === "--host") {
471
+ collectHosts(hosts, requireOptionValue(argv, ++index, argument));
472
+ }
473
+ else if (argument.startsWith("--")) {
474
+ throw new CliUsageError(SKILL_HOST_ADD_USAGE);
475
+ }
476
+ else {
477
+ requested.push(argument);
478
+ }
479
+ }
480
+ if (requested.length !== 1 || hosts.length === 0)
481
+ throw new CliUsageError(SKILL_HOST_ADD_USAGE);
482
+ if (!operations.listUserGlobalInstallations || !operations.updateUserGlobalInstallation) {
483
+ throw new Error("Configured Source operations cannot update user-global Skills");
484
+ }
485
+ const installations = await operations.listUserGlobalInstallations();
486
+ const [selection] = selectManagedSkills(installations, requested);
487
+ const options = userGlobalRemovalOptions(path.resolve(dependencies.homeDirectory ?? homedir()), dependencies.installationOptions);
488
+ const plan = await planHostAddition(selection, hosts, options).catch(rethrowSelectionError);
489
+ output("Preview: add Hosts to 1 user-global Skill");
490
+ for (const line of plan.preview)
491
+ output(line);
492
+ requireConfirmation(confirmed, "Host change not confirmed; rerun with --yes after reviewing the preview");
493
+ requireConfirmation(confirmAdditionalHost, "Adding a Host exposes the installed Skill to more hosts; rerun with --yes --confirm-additional-host");
494
+ await executeHostAddition(plan, operations, options, installations, true);
495
+ output(`Added hosts ${plan.hosts.join(", ")} to ${JSON.stringify(plan.selection.path)}`);
496
+ }
497
+ function rethrowSelectionError(error) {
498
+ if (error instanceof SkillSelectionError)
499
+ throw new CliUsageError(error.message);
500
+ throw error;
501
+ }
502
+ function parseUninstallOptions(argv) {
503
+ let skills = false;
504
+ const unmanagedSkills = [];
505
+ let data = false;
506
+ let cli = false;
507
+ let confirmed = false;
508
+ for (let index = 0; index < argv.length; index += 1) {
509
+ const argument = argv[index];
510
+ switch (argument) {
511
+ case "--skills":
512
+ if (skills)
513
+ throw new CliUsageError(UNINSTALL_USAGE);
514
+ skills = true;
515
+ break;
516
+ case "--unmanaged-skill": {
517
+ const selected = requireOptionValue(argv, ++index, "--unmanaged-skill");
518
+ if (unmanagedSkills.includes(selected))
519
+ throw new CliUsageError(`Duplicate --unmanaged-skill path ${JSON.stringify(selected)}`);
520
+ unmanagedSkills.push(selected);
521
+ break;
522
+ }
523
+ case "--data":
524
+ if (data)
525
+ throw new CliUsageError(UNINSTALL_USAGE);
526
+ data = true;
527
+ break;
528
+ case "--cli":
529
+ if (cli)
530
+ throw new CliUsageError(UNINSTALL_USAGE);
531
+ cli = true;
532
+ break;
533
+ case "--yes":
534
+ confirmed = true;
535
+ break;
536
+ default:
537
+ throw new CliUsageError(UNINSTALL_USAGE);
538
+ }
539
+ }
540
+ if (!skills && unmanagedSkills.length === 0 && !data && !cli) {
541
+ throw new CliUsageError("Select at least one uninstall choice: --skills, --unmanaged-skill, --data, or --cli");
542
+ }
543
+ return { skills, unmanagedSkills: Object.freeze(unmanagedSkills), data, cli, confirmed };
544
+ }
545
+ function assertUninstallCapabilities(options, operations) {
546
+ if (options.skills && !operations.listUserGlobalInstallations) {
547
+ throw new Error("Configured Source operations cannot inspect user-global Skill installations");
548
+ }
549
+ if (options.skills && !operations.removeUserGlobalInstallations) {
550
+ throw new Error("Configured Source operations cannot reconcile removed user-global Skills");
551
+ }
552
+ if (options.unmanagedSkills.length > 0 && !operations.listUserGlobalInstallations) {
553
+ throw new Error("Configured Source operations cannot inspect managed paths before removing unmanaged Skills");
554
+ }
555
+ if (options.data && !operations.removeCatalogData) {
556
+ throw new Error("Configured Source operations cannot remove user-global catalog data");
557
+ }
558
+ }
559
+ async function previewManagedUninstall(options, installations, removalOptions, output) {
560
+ if (!options.skills) {
561
+ output("Managed Skills: preserve (not selected)");
562
+ return { selections: [], inspections: [] };
563
+ }
564
+ output(`Managed Skills (${installations.length}):`);
565
+ const plan = await inspectSkillRemovals(installations, removalOptions, (selection, inspection) => {
566
+ for (const line of describeSkillRemoval(selection, inspection, ""))
567
+ output(line);
568
+ });
569
+ output(" Installation records are reconciled after each successful Skill removal");
570
+ return plan;
571
+ }
572
+ async function previewUnmanagedUninstall(options, homeDirectory, installations, output) {
573
+ const inspections = [];
574
+ if (options.unmanagedSkills.length === 0)
575
+ return inspections;
576
+ output(`Unmanaged Skills (${options.unmanagedSkills.length}):`);
577
+ for (const selectedPath of options.unmanagedSkills) {
578
+ const inspection = await inspectUserGlobalSkillRemoval(selectedPath, {
579
+ homeDirectory,
580
+ managedInstallations: installations,
581
+ });
582
+ inspections.push(inspection);
583
+ for (const line of describeUnmanagedRemoval(inspection))
584
+ output(line);
585
+ }
586
+ output(" Unmanaged Skills are never inferred from a Source and no project scope is used");
587
+ return inspections;
588
+ }
589
+ function outputDataAndCliUninstallPreview(options, output) {
590
+ if (options.data) {
591
+ output("Catalog/configuration data: remove the user-global Source state file");
592
+ output(" Git Source caches are retained");
593
+ if (!options.skills) {
594
+ output(" Managed Skills are preserved but become untracked when their state is removed");
595
+ }
596
+ }
597
+ else {
598
+ output("Catalog/configuration data: preserve (not selected)");
599
+ }
600
+ if (options.cli) {
601
+ output("Persistent CLI: package-manager handoff only; Agent Depot will not invoke or infer a package manager");
602
+ }
603
+ else {
604
+ output("Persistent CLI: preserve (not selected)");
605
+ }
606
+ }
607
+ function assertNoManagedUnmanagedOverlap(managed, unmanaged, message) {
608
+ for (const managedInspection of managed) {
609
+ for (const unmanagedInspection of unmanaged) {
610
+ if (managedInspection.paths.some((managedPath) => pathsOverlap(path.resolve(managedPath), path.resolve(unmanagedInspection.path)))) {
611
+ throw new Error(message(unmanagedInspection.path));
612
+ }
613
+ }
614
+ }
615
+ }
616
+ function outputUninstallSummary(options, removedManagedCount, output) {
617
+ if (options.skills) {
618
+ output(`Removed ${removedManagedCount} managed user-global Skill${removedManagedCount === 1 ? "" : "s"}`);
619
+ }
620
+ if (options.unmanagedSkills.length > 0) {
621
+ output(`Permanently deleted ${options.unmanagedSkills.length} unmanaged user-global Skill${options.unmanagedSkills.length === 1 ? "" : "s"}`);
622
+ }
623
+ if (options.data) {
624
+ output("Removed user-global catalog/configuration data; Git Source caches were retained");
625
+ }
626
+ if (options.cli) {
627
+ output("Handoff: remove the persistent Agent Depot CLI with the package manager that installed it; no package manager was invoked or inferred");
628
+ if (AGENT_DEPOT_PACKAGE_NAME !== undefined) {
629
+ output("Run the command for the package manager you used:");
630
+ output(` npm uninstall --global ${AGENT_DEPOT_PACKAGE_NAME}`);
631
+ output(` pnpm remove --global ${AGENT_DEPOT_PACKAGE_NAME}`);
632
+ }
633
+ output(" npx/pnpm dlx runs need no uninstall (only a cache entry)");
634
+ }
635
+ }
636
+ async function runUninstall(argv, operations, dependencies, output) {
637
+ const options = parseUninstallOptions(argv);
638
+ assertUninstallCapabilities(options, operations);
639
+ const homeDirectory = path.resolve(dependencies.homeDirectory ?? homedir());
640
+ // Keep the initial selection snapshot separate from the reconciled records.
641
+ // Removing one entry shrinks the persisted list, but must not skip the next
642
+ // selected entry or change its inspection/removal order.
643
+ const selectedInstallations = options.skills || options.unmanagedSkills.length > 0
644
+ ? await operations.listUserGlobalInstallations()
645
+ : [];
646
+ const removalOptions = userGlobalRemovalOptions(homeDirectory, dependencies.installationOptions);
647
+ const context = { operations, homeDirectory, removalOptions };
648
+ output("Uninstall preview (scope: user-global only)");
649
+ const managedPlan = await previewManagedUninstall(options, selectedInstallations, removalOptions, output);
650
+ const inspections = managedPlan.inspections;
651
+ const unmanagedInspections = await previewUnmanagedUninstall(options, homeDirectory, selectedInstallations, output);
652
+ outputDataAndCliUninstallPreview(options, output);
653
+ assertNoOverlappingProjectSkillRemovalTargets(inspections);
654
+ assertNoOverlappingUserGlobalSkillRemovals(unmanagedInspections);
655
+ assertNoManagedUnmanagedOverlap(inspections, unmanagedInspections, (unmanagedPath) => `Cannot remove unmanaged Skill ${JSON.stringify(unmanagedPath)} because it overlaps a selected managed Skill target; no path was changed`);
656
+ if (options.skills || options.unmanagedSkills.length > 0 || options.data) {
657
+ requireConfirmation(options.confirmed, "Uninstall not confirmed; rerun with --yes after reviewing every user-global deletion preview");
658
+ }
659
+ const managedRechecked = await recheckSkillRemoval(managedPlan, removalOptions);
660
+ const rechecked = managedRechecked.inspections;
661
+ const unmanagedRechecked = [];
662
+ for (const inspection of unmanagedInspections) {
663
+ unmanagedRechecked.push(await inspectUserGlobalSkillRemoval(inspection.path, {
664
+ homeDirectory,
665
+ managedInstallations: selectedInstallations,
666
+ }));
667
+ }
668
+ assertNoOverlappingUserGlobalSkillRemovals(unmanagedRechecked);
669
+ assertNoManagedUnmanagedOverlap(rechecked, unmanagedRechecked, () => "An unmanaged Skill target changed into an overlap with a selected managed Skill; no path was changed");
670
+ const expectedManagedInstallations = options.skills
671
+ ? await removeSkillsAndRecords(managedPlan, managedRechecked, operations, removalOptions, selectedInstallations)
672
+ : selectedInstallations;
673
+ await removeInspectedUnmanagedSkills(context, unmanagedInspections, expectedManagedInstallations);
674
+ if (options.data) {
675
+ await operations.removeCatalogData();
676
+ }
677
+ outputUninstallSummary(options, selectedInstallations.length, output);
678
+ }
679
+ async function runUpdate(argv, operations, dependencies, output) {
680
+ const options = parseUpdateOptions(argv);
681
+ const apps = dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory });
682
+ let appUpdates = [];
683
+ let appCheckFailed = false;
684
+ if (options.scope === "user-global") {
685
+ try {
686
+ appUpdates = await checkAppUpdates(apps);
687
+ }
688
+ catch (error) {
689
+ appCheckFailed = true;
690
+ output(`Error (Apps): ${error instanceof Error ? error.message : String(error)}`);
691
+ }
692
+ }
693
+ let loaded;
694
+ try {
695
+ loaded = await loadUpdates({
696
+ scope: options.scope,
697
+ operations,
698
+ ...(dependencies.homeDirectory === undefined ? {} : { homeDirectory: dependencies.homeDirectory }),
699
+ ...(dependencies.projectRoot === undefined ? {} : { projectRoot: dependencies.projectRoot }),
700
+ ...(dependencies.sourceAccess === undefined ? {} : { sourceAccess: dependencies.sourceAccess }),
701
+ ...(dependencies.projectManifestStore === undefined ? {} : { projectManifestStore: dependencies.projectManifestStore }),
702
+ ...(dependencies.installationOptions?.fileSystem === undefined ? {} : { installationFileSystem: dependencies.installationOptions.fileSystem }),
703
+ });
704
+ }
705
+ catch (error) {
706
+ output(`Error (Skills, ${options.scope}): ${error instanceof Error ? error.message : String(error)}`);
707
+ }
708
+ if (loaded) {
709
+ const { assessment, installed, context } = loaded;
710
+ const userGlobalInventory = options.action === "check" && options.scope === "user-global"
711
+ ? await scanUserGlobalSkillInventory({ homeDirectory: context.projectRoot, managedInstallations: installed, reportWarning: output })
712
+ : undefined;
713
+ outputUpdateAssessment(assessment, options.scope, output, userGlobalInventory);
714
+ }
715
+ for (const item of appUpdates) {
716
+ output(`App ${item.entry.recipe?.name ?? item.entry.file}: ${item.status}; installed ${item.installedVersion ?? "unknown"}; latest ${item.latestVersion ?? "unknown"}${item.reason ? `; ${item.reason}` : ""}`);
717
+ }
718
+ if (!loaded)
719
+ return 1;
720
+ if (options.action === "check") {
721
+ return appCheckFailed ? 1 : 0;
722
+ }
723
+ const { assessment, context } = loaded;
724
+ const selectedApps = options.all ? appUpdates.filter(item => item.status === "update available")
725
+ : options.requestedApps.map(name => {
726
+ const matches = appUpdates.filter(item => item.entry.recipe?.name === name);
727
+ if (matches.length !== 1 || matches[0].status !== "update available") {
728
+ throw new CliUsageError(`App ${JSON.stringify(name)} is unknown or not updateable`);
729
+ }
730
+ return matches[0];
731
+ });
732
+ const appPlans = [];
733
+ let appFailed = appCheckFailed ? 1 : 0;
734
+ for (const item of selectedApps) {
735
+ try {
736
+ const plan = await apps.planLifecycle(item.entry, "update", undefined, item);
737
+ output(`Preview: update App ${item.entry.recipe.name}: ${JSON.stringify(plan.argv ?? [])}; executable ${plan.executable ?? "unresolved"}; cwd ${plan.cwd}; environment ${plan.environment}`);
738
+ if (plan.warning)
739
+ output(plan.warning);
740
+ if (plan.manual)
741
+ output(`Manual (never executed): ${plan.manual}`);
742
+ appPlans.push(plan);
743
+ }
744
+ catch (error) {
745
+ appFailed++;
746
+ output(`App ${item.entry.recipe.name}: preview failed: ${error instanceof Error ? error.message : String(error)}`);
747
+ }
748
+ }
749
+ const selected = selectUpdateCandidates(assessment, options);
750
+ const { plans, failures: previewFailures, nonCanonicalPaths } = await previewUpdateCandidates(selected, context);
751
+ // Successes and failures are reported in candidate order, as the preview runs.
752
+ const failedIds = new Set(previewFailures.map(({ item }) => item.id));
753
+ const plansById = new Map(plans.map((plan) => [plan.item.id, plan]));
754
+ for (const item of selected) {
755
+ const plan = plansById.get(item.id);
756
+ if (plan !== undefined) {
757
+ for (const line of describeUpdatePreview(plan, options.scope, context.projectRoot))
758
+ output(line);
759
+ }
760
+ else if (failedIds.has(item.id)) {
761
+ const failure = previewFailures.find((candidate) => candidate.item.id === item.id);
762
+ output(`Preview failed for ${JSON.stringify(item.id)}: ${failure.message}`);
763
+ }
764
+ }
765
+ requireConfirmation(options.confirmed, "Update not confirmed; rerun with --yes after reviewing every candidate preview");
766
+ const pathProblem = findPathConfirmationProblem(nonCanonicalPaths, options.confirmedPaths);
767
+ if (pathProblem !== undefined)
768
+ throw new CliUsageError(pathProblem);
769
+ const result = await applyConfirmedUpdates(selected, context, options.confirmedPaths);
770
+ for (const { item } of previewFailures) {
771
+ if (!result.failed.some((failed) => failed.id === item.id)) {
772
+ output(`Preview failure was not selected for application: ${JSON.stringify(item.id)}`);
773
+ }
774
+ }
775
+ for (const item of result.updated) {
776
+ output(`Updated Skill ${JSON.stringify(item.selection.path)} (${item.id})`);
777
+ }
778
+ for (const item of result.failed) {
779
+ output(`Failed Skill ${JSON.stringify(item.selection.path)} (${item.id}): ${item.error ?? "unknown error"}`);
780
+ }
781
+ let appUpdated = 0;
782
+ for (const plan of appPlans) {
783
+ try {
784
+ const outcome = await apps.executeLifecycle(plan, true);
785
+ output(`App ${plan.entry.recipe.name}: ${describeAppResult(outcome)}${outcome.reason ? `: ${outcome.reason}` : ""}`);
786
+ if (outcome.status === "installed")
787
+ appUpdated++;
788
+ else {
789
+ appFailed++;
790
+ if (outcome.manual) {
791
+ output(`Manual (never executed): ${outcome.manual}`);
792
+ output(describeManualCompletion(plan));
793
+ }
794
+ }
795
+ }
796
+ catch (error) {
797
+ appFailed++;
798
+ output(`App ${plan.entry.recipe.name}: failed: ${error instanceof Error ? error.message : String(error)}`);
799
+ }
800
+ }
801
+ output(`Update summary: ${result.updated.length + appUpdated} updated, ${result.failed.length + appFailed} failed`);
802
+ return result.failed.length + appFailed === 0 ? 0 : 1;
803
+ }
804
+ function selectUpdateCandidates(assessment, options) {
805
+ if (!options.all && !options.requested.length)
806
+ return [];
807
+ const requested = options.all
808
+ ? "all"
809
+ : resolveUpdateSelectionIds(assessment, options.requested);
810
+ try {
811
+ return selectUpdateBatch(assessment, requested);
812
+ }
813
+ catch (error) {
814
+ if (error instanceof UpdateBatchSelectionError)
815
+ throw new CliUsageError(error.message);
816
+ throw error;
817
+ }
818
+ }
819
+ function parseUpdateOptions(argv) {
820
+ const action = argv[0];
821
+ if (action !== "check" && action !== "apply") {
822
+ throw new CliUsageError(USAGE);
823
+ }
824
+ let scope;
825
+ let all = false;
826
+ let confirmed = false;
827
+ const requested = [];
828
+ const requestedApps = [];
829
+ const confirmedPaths = [];
830
+ for (let index = 1; index < argv.length; index += 1) {
831
+ switch (argv[index]) {
832
+ case "--scope":
833
+ scope = requireOptionValue(argv, ++index, "--scope");
834
+ break;
835
+ case "--confirm-path":
836
+ confirmedPaths.push(normalizeConfirmedPath(requireOptionValue(argv, ++index, "--confirm-path")));
837
+ break;
838
+ case "--all":
839
+ all = true;
840
+ break;
841
+ case "--app":
842
+ requestedApps.push(requireOptionValue(argv, ++index, "--app"));
843
+ break;
844
+ case "--skill":
845
+ requested.push(requireOptionValue(argv, ++index, "--skill"));
846
+ break;
847
+ case "--yes":
848
+ confirmed = true;
849
+ break;
850
+ default:
851
+ throw new CliUsageError(USAGE);
852
+ }
853
+ }
854
+ if (scope !== "project" && scope !== "user-global") {
855
+ throw new CliUsageError("Update scope must be explicit: use --scope project or --scope user-global");
856
+ }
857
+ if (action === "check" && (all || requested.length > 0 || requestedApps.length > 0 || confirmed || confirmedPaths.length > 0)) {
858
+ throw new CliUsageError("update check only accepts --scope; use update apply to select and apply updates");
859
+ }
860
+ if (action === "apply" && (all === (requested.length + requestedApps.length > 0))) {
861
+ throw new CliUsageError("update apply requires exactly one selection mode: --all or one or more --skill/--app values");
862
+ }
863
+ if (requestedApps.length && scope !== "user-global")
864
+ throw new CliUsageError("Apps are user-global only");
865
+ if (new Set(requestedApps).size !== requestedApps.length)
866
+ throw new CliUsageError("Duplicate App selection");
867
+ if (scope === "user-global" && confirmedPaths.length > 0) {
868
+ throw new CliUsageError("--confirm-path only applies to --scope project");
869
+ }
870
+ return {
871
+ action,
872
+ scope: scope,
873
+ all,
874
+ requested: Object.freeze(requested),
875
+ requestedApps: Object.freeze(requestedApps),
876
+ confirmed,
877
+ confirmedPaths: Object.freeze(confirmedPaths),
878
+ };
879
+ }
880
+ /** Normalizes a --confirm-path value like the relative path shown in the preview. */
881
+ function normalizeConfirmedPath(value) {
882
+ const normalized = path.posix.normalize(value);
883
+ return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
884
+ }
885
+ function resolveUpdateSelectionIds(assessment, requested) {
886
+ return requested.map((value) => {
887
+ const numeric = /^\d+$/u.test(value) ? Number(value) : undefined;
888
+ if (numeric !== undefined && Number.isSafeInteger(numeric))
889
+ return numeric;
890
+ if (assessment.items.some((item) => item.id === value))
891
+ return value;
892
+ const matches = assessment.items.filter((item) => item.selection?.path === value);
893
+ if (matches.length === 1)
894
+ return matches[0].id;
895
+ if (matches.length > 1) {
896
+ throw new CliUsageError(`Skill path ${JSON.stringify(value)} is ambiguous; select its full update ID`);
897
+ }
898
+ throw new CliUsageError(`Unknown update Skill selection ${JSON.stringify(value)}`);
899
+ });
900
+ }
901
+ function outputUpdateAssessment(assessment, scope, output, userGlobalInventory) {
902
+ output(`Update check (scope: ${scope})`);
903
+ output(`Updateable (${assessment.updateable.length}):`);
904
+ for (const item of assessment.updateable)
905
+ outputUpdateItem(item, output);
906
+ output(`Current (${assessment.current.length}):`);
907
+ for (const item of assessment.current)
908
+ outputUpdateItem(item, output);
909
+ output(`Unknown (${assessment.unknown.length}):`);
910
+ for (const item of assessment.unknown)
911
+ outputUpdateItem(item, output);
912
+ if (userGlobalInventory === undefined)
913
+ return;
914
+ output(`Unmanaged (${userGlobalInventory.unmanaged.length}):`);
915
+ for (const entry of userGlobalInventory.unmanaged)
916
+ output(` ${entry.path}`);
917
+ if (userGlobalInventory.skipped.length === 0)
918
+ return;
919
+ output(`Skipped/unsafe entries (${userGlobalInventory.skipped.length}):`);
920
+ for (const entry of userGlobalInventory.skipped) {
921
+ output(` ${entry.path}: ${entry.detail ?? entry.reason}`);
922
+ }
923
+ output(" Skipped/unsafe entries are not Unmanaged entries or removal candidates; inspect them manually.");
924
+ }
925
+ function outputUpdateItem(item, output) {
926
+ const selection = item.selection;
927
+ output(` [${item.index}] ${item.id}${typeof selection?.path === "string" ? ` path=${selection.path}` : ""}: ${item.reason}`);
928
+ }
929
+ async function runInstall(argv, operations, dependencies, output) {
930
+ const options = parseInstallOptions(argv);
931
+ const sourceAccess = dependencies.sourceAccess ?? defaultProjectSkillTreeAccess();
932
+ if (options.scope === "user-global") {
933
+ await runUserGlobalInstall(options, operations, dependencies, sourceAccess, output);
934
+ return;
935
+ }
936
+ const projectRoot = path.resolve(dependencies.projectRoot ?? process.cwd());
937
+ const manifestStore = dependencies.projectManifestStore ?? new ProjectManifestStore(defaultProjectManifestPath(projectRoot));
938
+ const context = { options, operations, dependencies, sourceAccess, output, projectRoot, manifestStore };
939
+ if (options.fromManifest) {
940
+ await runManifestInstall(context);
941
+ return;
942
+ }
943
+ await runSingleProjectInstall(context);
944
+ }
945
+ async function runManifestInstall(context) {
946
+ const { options, operations, dependencies, sourceAccess, output, projectRoot, manifestStore } = context;
947
+ const manifest = await manifestStore.load();
948
+ if (manifest.skills.length === 0) {
949
+ throw new CliUsageError("The project manifest contains no selected Skills");
950
+ }
951
+ if (!options.portableV1) {
952
+ throw new CliUsageError("Compatibility is not known for a manifest install; rerun with --portable-v1 after reviewing the Skill");
953
+ }
954
+ // A built-in Skill pinned to another Agent Depot cannot be reproduced here:
955
+ // skip it with its reason and keep installing the others.
956
+ const installable = [];
957
+ for (const selection of manifest.skills) {
958
+ const mismatch = builtinPinMismatch(selection);
959
+ if (mismatch === undefined) {
960
+ installable.push(selection);
961
+ }
962
+ else {
963
+ output(`Skipped ${selection.path}: ${mismatch}`);
964
+ }
965
+ }
966
+ if (installable.length === 0) {
967
+ throw new CliUsageError("Nothing to install: every Skill in the manifest is pinned to another Agent Depot version");
968
+ }
969
+ const skipped = manifest.skills.length - installable.length;
970
+ if (skipped > 0) {
971
+ output(`Skipped ${skipped} of ${manifest.skills.length} Skills pinned to another Agent Depot version`);
972
+ }
973
+ const resolved = await resolveManifestSelections(installable, operations, sourceAccess, options.confirmed);
974
+ if (resolved.some((item) => item.method !== undefined) && resolved.length > 1) {
975
+ throw new SourceMetadataError("A batch install containing an external method is refused because command side effects cannot be rolled back safely; install that Skill separately");
976
+ }
977
+ // Inspect every physical destination before printing a confirmation preview.
978
+ // Two different manifest selections can have different Source/path
979
+ // identities while still resolving to the same basename directory.
980
+ const inspections = await inspectManifestInstallations(resolved, projectRoot, sourceAccess, dependencies);
981
+ for (let index = 0; index < resolved.length; index += 1) {
982
+ const item = resolved[index];
983
+ outputInstallPreview(item.selection, item.source, projectRoot, output, item.files, item.method, "project", inspections[index], false, options.confirmed);
984
+ }
985
+ requireConfirmation(options.confirmed, "Installation not confirmed; rerun with --yes to continue");
986
+ for (const inspection of inspections) {
987
+ rejectInstallationCollision(inspection, false);
988
+ }
989
+ const transactions = [];
990
+ const installed = [];
991
+ let methodFailed = false;
992
+ try {
993
+ for (let index = 0; index < resolved.length; index += 1) {
994
+ const item = resolved[index];
995
+ const transaction = await installOne(item.selection, item.source, sourceAccess, projectRoot, dependencies, item.method, options.portableV1, item.files, item.resolvedVersion, options.confirmAdditionalHostExposure, false, inspections[index]);
996
+ if (transaction) {
997
+ transactions.push(transaction);
998
+ installed.push({ item, result: transaction.result });
999
+ }
1000
+ }
1001
+ const nextManifest = parseProjectManifest({
1002
+ version: 1,
1003
+ skills: manifest.skills.map((selection) => {
1004
+ const installedSelection = installed.find(({ item }) => JSON.stringify([item.selection.source, item.selection.path]) === JSON.stringify([selection.source, selection.path]));
1005
+ return installedSelection === undefined
1006
+ ? selection
1007
+ : selectionWithInstallation(installedSelection.item.selection, installedSelection.result, projectRoot);
1008
+ }),
1009
+ });
1010
+ await manifestStore.save(nextManifest);
1011
+ for (const { item, result } of installed) {
1012
+ outputInstallationResult(result, projectRoot, output);
1013
+ try {
1014
+ await executeOrRejectMethod(item, operations, projectRoot);
1015
+ }
1016
+ catch (error) {
1017
+ methodFailed = true;
1018
+ throw externalMethodFailure(error);
1019
+ }
1020
+ }
1021
+ }
1022
+ catch (error) {
1023
+ if (methodFailed) {
1024
+ throw error;
1025
+ }
1026
+ await rollbackTransactions(transactions, error);
1027
+ }
1028
+ }
1029
+ async function runSingleProjectInstall(context) {
1030
+ const { options, operations, dependencies, sourceAccess, output, projectRoot, manifestStore } = context;
1031
+ if (!options.sourceId || !options.skillPath || !options.version || options.hosts.length === 0) {
1032
+ throw new CliUsageError(INSTALL_USAGE);
1033
+ }
1034
+ const existing = await manifestStore.load();
1035
+ const plan = await planSingleInstall({ ...options, scope: "project", sourceId: options.sourceId, skillPath: options.skillPath, version: options.version }, { operations, sourceAccess, environment: dependencies, root: projectRoot, existing: existing.skills, recordedIn: manifestStore.path });
1036
+ outputSingleInstallPreview(plan, output);
1037
+ requireConfirmation(options.confirmed, "Installation not confirmed; rerun with --yes to continue");
1038
+ await executeSingleInstall(plan, async (record) => {
1039
+ await manifestStore.save(parseProjectManifest({ version: 1, skills: [...existing.skills, record] }));
1040
+ }, output);
1041
+ output(`Saved project manifest: ${manifestStore.path}`);
1042
+ }
1043
+ async function runUserGlobalInstall(options, operations, dependencies, sourceAccess, output) {
1044
+ if (options.fromManifest || !options.sourceId || !options.skillPath || !options.version || options.hosts.length === 0) {
1045
+ throw new CliUsageError(INSTALL_USAGE);
1046
+ }
1047
+ const { listUserGlobalInstallations, addUserGlobalInstallation } = operations;
1048
+ if (!listUserGlobalInstallations || !addUserGlobalInstallation) {
1049
+ throw new Error("Configured Source operations cannot manage user-global installation state");
1050
+ }
1051
+ const homeDirectory = path.resolve(dependencies.homeDirectory ?? homedir());
1052
+ const plan = await planSingleInstall({ ...options, scope: "user-global", sourceId: options.sourceId, skillPath: options.skillPath, version: options.version }, { operations, sourceAccess, environment: dependencies, root: homeDirectory, existing: await listUserGlobalInstallations.call(operations), recordedIn: "user-global state" });
1053
+ outputSingleInstallPreview(plan, output);
1054
+ requireConfirmation(options.confirmed, "Installation not confirmed; rerun with --yes to continue");
1055
+ await executeSingleInstall(plan, (record) => addUserGlobalInstallation.call(operations, record), output);
1056
+ output("Saved user-global installation state: shared Source state");
1057
+ }
1058
+ /** Appends the comma-separated `--host` value, rejecting unknown or repeated Hosts. */
1059
+ function collectHosts(hosts, value) {
1060
+ for (const host of value.split(",")) {
1061
+ if (!PROJECT_HOSTS.includes(host) || hosts.includes(host)) {
1062
+ throw new CliUsageError(`Unsupported or duplicate Host ${JSON.stringify(host)}`);
1063
+ }
1064
+ hosts.push(host);
1065
+ }
1066
+ }
1067
+ const INSTALL_VALUE_OPTIONS = new Map([
1068
+ ["--scope", (draft, value) => { draft.scope = value; }],
1069
+ ["--source", (draft, value) => { draft.sourceId = value; }],
1070
+ ["--skill", (draft, value) => { draft.skillPath = value; }],
1071
+ ["--host", (draft, value) => { collectHosts(draft.hosts, value); }],
1072
+ ["--version", (draft, value) => { draft.versionValue = value; }],
1073
+ ["--ref", (draft, value) => { draft.ref = value; }],
1074
+ ["--method", (draft, value) => { draft.method = parseMethodOption(value); }],
1075
+ ]);
1076
+ const INSTALL_FLAG_OPTIONS = new Map([
1077
+ ["--manifest", (draft) => { draft.fromManifest = true; }],
1078
+ ["--portable-v1", (draft) => { draft.portableV1 = true; }],
1079
+ ["--yes", (draft) => { draft.confirmed = true; }],
1080
+ ["--overwrite", (draft) => { draft.overwrite = true; }],
1081
+ ["--confirm-additional-host", (draft) => { draft.confirmAdditionalHostExposure = true; }],
1082
+ ]);
1083
+ function parseInstallOptions(argv) {
1084
+ const draft = {
1085
+ fromManifest: false,
1086
+ portableV1: false,
1087
+ confirmed: false,
1088
+ overwrite: false,
1089
+ confirmAdditionalHostExposure: false,
1090
+ hosts: [],
1091
+ };
1092
+ for (let index = 0; index < argv.length; index += 1) {
1093
+ const argument = argv[index];
1094
+ const applyValue = INSTALL_VALUE_OPTIONS.get(argument);
1095
+ const applyFlag = INSTALL_FLAG_OPTIONS.get(argument);
1096
+ if (applyValue) {
1097
+ applyValue(draft, requireOptionValue(argv, ++index, argument));
1098
+ }
1099
+ else if (applyFlag) {
1100
+ applyFlag(draft);
1101
+ }
1102
+ else {
1103
+ throw new CliUsageError(INSTALL_USAGE);
1104
+ }
1105
+ }
1106
+ return finalizeInstallOptions(draft);
1107
+ }
1108
+ function finalizeInstallOptions(draft) {
1109
+ const { scope, sourceId, skillPath, versionValue, ref, method, fromManifest, hosts, overwrite } = draft;
1110
+ if (scope !== "project" && scope !== "user-global") {
1111
+ throw new CliUsageError("Installation scope must be explicit: use --scope project or --scope user-global");
1112
+ }
1113
+ const selectsSkill = sourceId !== undefined || skillPath !== undefined || ref !== undefined || hosts.length > 0;
1114
+ if (fromManifest) {
1115
+ if (scope !== "project") {
1116
+ throw new CliUsageError("--manifest is only supported with --scope project");
1117
+ }
1118
+ if (selectsSkill || versionValue !== undefined || method !== undefined || overwrite) {
1119
+ throw new CliUsageError("--manifest cannot be combined with --source, --skill, --host, --version, --ref, --method, or --overwrite");
1120
+ }
1121
+ }
1122
+ else if (versionValue === undefined) {
1123
+ throw new CliUsageError(selectsSkill ? "--version is required for a selected Skill" : INSTALL_USAGE);
1124
+ }
1125
+ return {
1126
+ scope,
1127
+ fromManifest,
1128
+ sourceId,
1129
+ skillPath,
1130
+ hosts: Object.freeze(hosts),
1131
+ version: versionValue === undefined ? undefined : parseVersionOption(versionValue),
1132
+ ref,
1133
+ method,
1134
+ portableV1: draft.portableV1,
1135
+ confirmed: draft.confirmed,
1136
+ overwrite,
1137
+ confirmAdditionalHostExposure: draft.confirmAdditionalHostExposure,
1138
+ };
1139
+ }
1140
+ function parseMethodOption(value) {
1141
+ try {
1142
+ return JSON.parse(value);
1143
+ }
1144
+ catch (error) {
1145
+ const detail = error instanceof Error ? `: ${error.message}` : "";
1146
+ throw new CliUsageError(`--method must be valid JSON${detail}`);
1147
+ }
1148
+ }
1149
+ function parseVersionOption(value) {
1150
+ if (value === "latest") {
1151
+ return { policy: "latest" };
1152
+ }
1153
+ const version = value.startsWith("fixed:") ? value.slice("fixed:".length) : value;
1154
+ if (!isValidFixedVersion(version)) {
1155
+ throw new CliUsageError("--version fixed:<value> must provide a non-empty version without whitespace or control characters");
1156
+ }
1157
+ return { policy: "fixed", version };
1158
+ }
1159
+ function requireOptionValue(argv, index, option) {
1160
+ const value = argv[index];
1161
+ if (!value || value.startsWith("--")) {
1162
+ throw new CliUsageError(`Missing value for ${option}`);
1163
+ }
1164
+ return value;
1165
+ }
1166
+ async function resolveManifestSelections(selections, operations, sourceAccess, confirmed) {
1167
+ const resolved = [];
1168
+ for (const selection of selections) {
1169
+ const initialSource = operations.resolveProjectSource
1170
+ ? await operations.resolveProjectSource(selection.source)
1171
+ : resolveProjectSource(selection.source);
1172
+ const source = await refreshResolvedSource(operations, selection.source, initialSource, confirmed);
1173
+ resolved.push(await resolveSelection(selection, source, operations, sourceAccess));
1174
+ }
1175
+ return Object.freeze(resolved);
1176
+ }
1177
+ async function inspectManifestInstallations(resolved, projectRoot, sourceAccess, dependencies) {
1178
+ const inspections = [];
1179
+ const owners = new Map();
1180
+ for (const item of resolved) {
1181
+ const inspection = await inspectProjectSkillInstallation({
1182
+ selection: item.selection,
1183
+ source: item.source,
1184
+ previewTree: item.files,
1185
+ portableV1: true,
1186
+ resolvedVersion: item.resolvedVersion,
1187
+ }, {
1188
+ projectRoot,
1189
+ sourceAccess,
1190
+ ...dependencies.installationOptions,
1191
+ });
1192
+ // Reserve both known Host paths, not only the currently selected target:
1193
+ // a later adoption decision may choose either physical location.
1194
+ for (const candidate of [inspection.canonicalPath, inspection.claudePath]) {
1195
+ const normalized = path.resolve(candidate);
1196
+ const previous = owners.get(normalized);
1197
+ if (previous !== undefined) {
1198
+ throw new CliUsageError(`Manifest selections ${JSON.stringify(previous)} and ${JSON.stringify(item.selection.path)} resolve to the same physical installation target ${JSON.stringify(normalized)}; remove one record before confirmation; no path was changed`);
1199
+ }
1200
+ owners.set(normalized, item.selection.path);
1201
+ }
1202
+ inspections.push(inspection);
1203
+ }
1204
+ return Object.freeze(inspections);
1205
+ }
1206
+ function requireConfirmation(confirmed, message) {
1207
+ if (!confirmed) {
1208
+ throw new CliUsageError(message);
1209
+ }
1210
+ }
1211
+ function formatSource(source) {
1212
+ if (source.kind === "builtin") {
1213
+ return `${source.id}\tbuiltin\t${source.name}\tread-only`;
1214
+ }
1215
+ return `${source.id}\tgit\t${source.url}`;
1216
+ }
1217
+ function formatCandidate(candidate) {
1218
+ const description = candidate.description.replace(/[\r\n]+/gu, "\\n");
1219
+ return `Candidate: ${candidate.sourceId}\t${candidate.path}\t${candidate.name}\t${description}`;
1220
+ }
1221
+ function formatDiscoveryError(error) {
1222
+ const message = error instanceof Error ? error.message : "unknown discovery error";
1223
+ if (error instanceof SourceNotFoundError) {
1224
+ return `${message}. Run \`agent-depot source list\` and pass a registered Source ID`;
1225
+ }
1226
+ if (message.startsWith("Git Source mirror is not available:")) {
1227
+ return `${message}. Refresh the selected Git Source with \`agent-depot source refresh <source-id> --yes\` before discovering`;
1228
+ }
1229
+ return message;
1230
+ }
1231
+ function requireArgumentCount(values, expected, usage) {
1232
+ if (values.length !== expected) {
1233
+ throw new CliUsageError(`Usage: agent-depot ${usage}`);
1234
+ }
1235
+ }
1236
+ function requireSourceIds(sourceIds) {
1237
+ if (sourceIds.length === 0) {
1238
+ throw new CliUsageError(DISCOVER_USAGE);
1239
+ }
1240
+ }
1241
+ function isEntrypoint() {
1242
+ if (!process.argv[1]) {
1243
+ return false;
1244
+ }
1245
+ try {
1246
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
1247
+ }
1248
+ catch {
1249
+ return false;
1250
+ }
1251
+ }
1252
+ if (isEntrypoint()) {
1253
+ runCli().then((exitCode) => {
1254
+ process.exitCode = exitCode;
1255
+ });
1256
+ }