@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,376 @@
1
+ import { builtinPinMismatch, parseProjectManifest, } from "./project-manifest.js";
2
+ import { resolveProjectSource, } from "./sources.js";
3
+ import { skillTreeBaseline, } from "./skill-discovery.js";
4
+ const immutableAssessmentSnapshots = new WeakMap();
5
+ export class UpdateBatchSelectionError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "UpdateBatchSelectionError";
9
+ }
10
+ }
11
+ /**
12
+ * Assesses every installed selection against a separately captured current
13
+ * Source snapshot. A missing or untrusted version is never promoted to an
14
+ * updateable result.
15
+ */
16
+ export async function assessUpdateBatch(value, options) {
17
+ const rawSelections = Array.isArray(value)
18
+ ? value
19
+ : parseProjectManifest(value).skills;
20
+ const resolveSource = options.resolveSource ?? resolveProjectSource;
21
+ const items = await Promise.all(rawSelections.map(async (rawSelection, index) => {
22
+ const parsed = parseRuntimeSelection(rawSelection);
23
+ if (!parsed.selection) {
24
+ return malformedSelectionAssessment(rawSelection, index, parsed.reason);
25
+ }
26
+ return assessSelection(parsed.selection, index, resolveSource, options.sourceAccess);
27
+ }));
28
+ const frozenItems = Object.freeze(items);
29
+ return Object.freeze({
30
+ items: frozenItems,
31
+ updateable: Object.freeze(items.filter((item) => item.status === "updateable")),
32
+ current: Object.freeze(items.filter((item) => item.status === "current")),
33
+ unknown: Object.freeze(items.filter((item) => item.status === "unknown")),
34
+ });
35
+ }
36
+ /** Selects all updateable entries or validates an explicit updateable subset. */
37
+ export function selectUpdateBatch(assessment, request) {
38
+ let ids;
39
+ if (request === "all") {
40
+ return assessment.updateable;
41
+ }
42
+ if (Array.isArray(request)) {
43
+ ids = request;
44
+ }
45
+ else if (!isModeSelection(request)) {
46
+ throw new UpdateBatchSelectionError("Update batch selection must specify mode 'all' or a subset of ids");
47
+ }
48
+ else {
49
+ const candidate = request;
50
+ if (candidate.mode === "all") {
51
+ return assessment.updateable;
52
+ }
53
+ if (candidate.mode === "subset" && Array.isArray(candidate.ids)) {
54
+ ids = candidate.ids;
55
+ }
56
+ else {
57
+ throw new UpdateBatchSelectionError("Update batch selection must specify mode 'all' or a subset of ids");
58
+ }
59
+ }
60
+ if (ids.length === 0) {
61
+ throw new UpdateBatchSelectionError("An update subset must contain at least one Skill");
62
+ }
63
+ const updateable = new Map(assessment.updateable.map((item) => [item.id, item]));
64
+ const byIndex = new Map(assessment.updateable.map((item) => [item.index, item]));
65
+ const selected = [];
66
+ const seen = new Set();
67
+ for (const requested of ids) {
68
+ const item = typeof requested === "number"
69
+ ? byIndex.get(requested)
70
+ : updateable.get(requested);
71
+ if (!item) {
72
+ const known = assessment.items.find((candidate) => candidate.id === requested || candidate.index === requested);
73
+ if (known?.status === "unknown") {
74
+ throw new UpdateBatchSelectionError(`Skill ${JSON.stringify(requested)} has unknown version status and is not updateable`);
75
+ }
76
+ if (known?.status === "current") {
77
+ throw new UpdateBatchSelectionError(`Skill ${JSON.stringify(requested)} has no available update`);
78
+ }
79
+ throw new UpdateBatchSelectionError(`Unknown update batch selection ${JSON.stringify(requested)}`);
80
+ }
81
+ if (seen.has(item.id)) {
82
+ throw new UpdateBatchSelectionError(`Update batch selection contains duplicate Skill ${JSON.stringify(requested)}`);
83
+ }
84
+ seen.add(item.id);
85
+ selected.push(item);
86
+ }
87
+ return Object.freeze(selected);
88
+ }
89
+ /** Alias that makes the assessment operation discoverable to embedders. */
90
+ export const assessAvailableUpdates = assessUpdateBatch;
91
+ async function assessSelection(selection, index, resolveSource, sourceAccess) {
92
+ const id = updateBatchItemId(selection);
93
+ const base = { id, index, selection };
94
+ if (!selection.installation) {
95
+ return Object.freeze({
96
+ ...base,
97
+ status: "unknown",
98
+ reason: "The Skill has no installed baseline or resolved version evidence",
99
+ });
100
+ }
101
+ let source;
102
+ try {
103
+ source = await resolveSource(selection.source);
104
+ }
105
+ catch (error) {
106
+ return Object.freeze({
107
+ ...base,
108
+ status: "unknown",
109
+ reason: `The current Source could not be resolved: ${errorMessage(error)}`,
110
+ });
111
+ }
112
+ let snapshot;
113
+ try {
114
+ snapshot = await readCurrentSnapshot(sourceAccess, source, selection.path);
115
+ assertValidSnapshot(snapshot);
116
+ const currentBaseline = skillTreeBaseline(snapshot.files);
117
+ const currentVersion = trustworthyVersion(source, snapshot.resolvedVersion);
118
+ const installedVersion = trustworthyVersionForSelection(source, selection.installation.resolvedVersion);
119
+ const evidence = assessEvidence(selection, installedVersion, currentVersion, currentBaseline);
120
+ const item = Object.freeze({
121
+ ...base,
122
+ source,
123
+ status: evidence.status,
124
+ reason: evidence.reason,
125
+ currentSnapshot: freezeSnapshot(snapshot),
126
+ currentBaseline,
127
+ ...(currentVersion === undefined ? {} : { currentVersion }),
128
+ });
129
+ // Keep a second private copy because Uint8Array contents remain mutable
130
+ // even when their containing object is frozen.
131
+ immutableAssessmentSnapshots.set(item, freezeSnapshot(snapshot));
132
+ return item;
133
+ }
134
+ catch (error) {
135
+ return Object.freeze({
136
+ ...base,
137
+ source,
138
+ status: "unknown",
139
+ reason: `The current Source snapshot could not be assessed: ${errorMessage(error)}`,
140
+ });
141
+ }
142
+ }
143
+ function isModeSelection(value) {
144
+ return value !== null && typeof value === "object" && !Array.isArray(value);
145
+ }
146
+ async function readCurrentSnapshot(sourceAccess, source, skillPath) {
147
+ if (sourceAccess.readSkillTreeSnapshot) {
148
+ return sourceAccess.readSkillTreeSnapshot(source, skillPath);
149
+ }
150
+ if (sourceAccess.readSkillTree) {
151
+ return { files: await sourceAccess.readSkillTree(source, skillPath) };
152
+ }
153
+ throw new Error("the configured Source access does not provide a Skill tree snapshot reader");
154
+ }
155
+ function assessEvidence(selection, installedVersion, currentVersion, currentBaseline) {
156
+ if (currentVersion === undefined) {
157
+ return {
158
+ status: "unknown",
159
+ reason: "The current Source snapshot has no trustworthy resolved version evidence",
160
+ };
161
+ }
162
+ if (installedVersion === undefined) {
163
+ return {
164
+ status: "unknown",
165
+ reason: "The installed Skill has no trustworthy resolved version evidence",
166
+ };
167
+ }
168
+ const pinMismatch = builtinPinMismatch(selection, currentVersion.kind === "builtin-package" ? currentVersion.version : undefined);
169
+ if (pinMismatch !== undefined) {
170
+ return { status: "unknown", reason: `${pinMismatch.charAt(0).toUpperCase()}${pinMismatch.slice(1)}` };
171
+ }
172
+ if (selection.version.policy === "fixed") {
173
+ const currentTarget = versionValue(currentVersion);
174
+ if (currentTarget !== selection.version.version) {
175
+ return {
176
+ status: "unknown",
177
+ reason: `The current Source resolved to ${JSON.stringify(currentTarget)}, not the fixed target ${JSON.stringify(selection.version.version)}`,
178
+ };
179
+ }
180
+ if (versionValue(installedVersion) !== selection.version.version) {
181
+ return {
182
+ status: "updateable",
183
+ reason: "The installed Skill is behind its fixed version target",
184
+ };
185
+ }
186
+ }
187
+ else if (installedVersion.kind === "builtin-package" && currentVersion.kind === "builtin-package" &&
188
+ compareSemver(installedVersion.version, currentVersion.version) > 0) {
189
+ return {
190
+ status: "unknown",
191
+ reason: `Installed by a newer Agent Depot (${installedVersion.version}); running ${currentVersion.version} — update Agent Depot`,
192
+ };
193
+ }
194
+ else if (!sameVersion(installedVersion, currentVersion)) {
195
+ return {
196
+ status: "updateable",
197
+ reason: "A newer resolved version is available for the latest policy",
198
+ };
199
+ }
200
+ const installedBaseline = selection.installation?.baseline;
201
+ if (!installedBaseline) {
202
+ return {
203
+ status: "unknown",
204
+ reason: "The installed Skill has no trustworthy content baseline",
205
+ };
206
+ }
207
+ if (installedBaseline.digest !== currentBaseline.digest) {
208
+ return {
209
+ status: "updateable",
210
+ reason: "The current Source snapshot differs from the installed content baseline",
211
+ };
212
+ }
213
+ return {
214
+ status: "current",
215
+ reason: selection.version.policy === "fixed"
216
+ ? "The installed Skill matches its fixed version and content baseline"
217
+ : "The installed Skill matches the current latest version and content baseline",
218
+ };
219
+ }
220
+ function parseRuntimeSelection(value) {
221
+ try {
222
+ if (!isPlainRecord(value)) {
223
+ throw new Error("the selection must be an object");
224
+ }
225
+ const candidate = value;
226
+ const source = candidate.source;
227
+ if (!isPlainRecord(source)) {
228
+ throw new Error("the selection Source must be an object");
229
+ }
230
+ const projectSource = runtimeProjectSource(source);
231
+ const version = candidate.version;
232
+ const validationSource = source.kind === "external" && typeof source.url === "string" &&
233
+ isPlainRecord(version) && version.policy === "fixed" && source.ref === undefined
234
+ ? { ...projectSource, ref: version.version }
235
+ : projectSource;
236
+ const parsed = parseProjectManifest({
237
+ version: 1,
238
+ skills: [{ ...candidate, source: validationSource }],
239
+ }).skills[0];
240
+ if (!parsed) {
241
+ return { reason: "the selection is missing" };
242
+ }
243
+ return { selection: parsed, reason: "" };
244
+ }
245
+ catch (error) {
246
+ return { reason: errorMessage(error) };
247
+ }
248
+ }
249
+ function isPlainRecord(value) {
250
+ return value !== null && typeof value === "object" && !Array.isArray(value);
251
+ }
252
+ function runtimeProjectSource(source) {
253
+ if (source.kind === "builtin") {
254
+ return { kind: "builtin", id: source.id };
255
+ }
256
+ if (source.kind !== "external") {
257
+ return source;
258
+ }
259
+ return {
260
+ kind: "external",
261
+ ...(typeof source.url === "string" ? { url: source.url } : {}),
262
+ ...(typeof source.path === "string" ? { path: source.path } : {}),
263
+ ...(source.ref === undefined ? {} : { ref: source.ref }),
264
+ };
265
+ }
266
+ function malformedSelectionAssessment(value, index, reason) {
267
+ return Object.freeze({
268
+ id: runtimeSelectionId(value, index),
269
+ index,
270
+ selection: value,
271
+ status: "unknown",
272
+ reason: `The Skill selection is malformed: ${reason}`,
273
+ });
274
+ }
275
+ function runtimeSelectionId(value, index) {
276
+ if (value !== null && typeof value === "object") {
277
+ try {
278
+ const candidate = value;
279
+ return JSON.stringify([candidate.source, candidate.path]);
280
+ }
281
+ catch {
282
+ // Fall through to an index-based identity for cyclic or otherwise unserializable input.
283
+ }
284
+ }
285
+ return `malformed:${index}`;
286
+ }
287
+ function assertValidSnapshot(value) {
288
+ if (value === null || typeof value !== "object" || !Array.isArray(value.files)) {
289
+ throw new Error("the Source snapshot must contain a files array");
290
+ }
291
+ for (const file of value.files) {
292
+ if (file === null || typeof file !== "object") {
293
+ throw new Error("the Source snapshot contains a malformed Skill tree file");
294
+ }
295
+ const candidate = file;
296
+ if (typeof candidate.path !== "string" || !(candidate.content instanceof Uint8Array) || typeof candidate.executable !== "boolean") {
297
+ throw new Error("the Source snapshot contains a malformed Skill tree file");
298
+ }
299
+ }
300
+ }
301
+ function trustworthyVersion(source, version) {
302
+ if (!version)
303
+ return undefined;
304
+ if (source.kind === "builtin" && version.kind === "builtin-package" && version.version.length > 0)
305
+ return version;
306
+ if (source.kind === "git" && version.kind === "git-commit" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(version.commit))
307
+ return version;
308
+ return undefined;
309
+ }
310
+ function trustworthyVersionForSelection(source, version) {
311
+ return trustworthyVersion(source, version);
312
+ }
313
+ function versionValue(version) {
314
+ return version.kind === "builtin-package" ? version.version : version.commit;
315
+ }
316
+ /** Compares two semver strings (prerelease aware); an unparsable input compares as 0 so it never blocks an update. */
317
+ function compareSemver(left, right) {
318
+ const a = parseSemver(left);
319
+ const b = parseSemver(right);
320
+ if (a === undefined || b === undefined)
321
+ return 0;
322
+ for (let index = 0; index < 3; index += 1) {
323
+ const difference = a.core[index] - b.core[index];
324
+ if (difference !== 0)
325
+ return Math.sign(difference);
326
+ }
327
+ if (a.pre.length === 0 || b.pre.length === 0)
328
+ return Math.sign(b.pre.length - a.pre.length);
329
+ for (let index = 0; index < Math.max(a.pre.length, b.pre.length); index += 1) {
330
+ const x = a.pre[index];
331
+ const y = b.pre[index];
332
+ if (x === undefined)
333
+ return -1;
334
+ if (y === undefined)
335
+ return 1;
336
+ if (x === y)
337
+ continue;
338
+ const xNumeric = /^\d+$/u.test(x);
339
+ const yNumeric = /^\d+$/u.test(y);
340
+ if (xNumeric && yNumeric)
341
+ return Math.sign(Number(x) - Number(y));
342
+ if (xNumeric !== yNumeric)
343
+ return xNumeric ? -1 : 1;
344
+ return x < y ? -1 : 1;
345
+ }
346
+ return 0;
347
+ }
348
+ function parseSemver(value) {
349
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
350
+ if (!match)
351
+ return undefined;
352
+ return { core: [Number(match[1]), Number(match[2]), Number(match[3])], pre: match[4] === undefined ? [] : match[4].split(".") };
353
+ }
354
+ function sameVersion(left, right) {
355
+ return left.kind === right.kind && versionValue(left) === versionValue(right);
356
+ }
357
+ export function updateBatchItemId(selection) {
358
+ return JSON.stringify([selection.source, selection.path]);
359
+ }
360
+ export function immutableUpdateSnapshot(item) {
361
+ const snapshot = immutableAssessmentSnapshots.get(item);
362
+ return snapshot === undefined ? undefined : freezeSnapshot(snapshot);
363
+ }
364
+ export function freezeSnapshot(snapshot) {
365
+ return Object.freeze({
366
+ files: Object.freeze(snapshot.files.map((file) => Object.freeze({
367
+ path: file.path,
368
+ content: Uint8Array.from(file.content),
369
+ executable: file.executable,
370
+ }))),
371
+ ...(snapshot.resolvedVersion === undefined ? {} : { resolvedVersion: Object.freeze({ ...snapshot.resolvedVersion }) }),
372
+ });
373
+ }
374
+ function errorMessage(error) {
375
+ return error instanceof Error ? error.message : String(error);
376
+ }
@@ -0,0 +1,135 @@
1
+ import { homedir } from "node:os";
2
+ import path from "node:path";
3
+ import { defaultProjectManifestPath, ProjectManifestStore, } from "./project-manifest.js";
4
+ import { defaultProjectSkillTreeAccess } from "./skill-install.js";
5
+ import { applyUpdateBatch, previewSkillUpdate, relativeProjectPath, } from "./skill-update.js";
6
+ import { resolveProjectSource } from "./sources.js";
7
+ import { assessUpdateBatch, } from "./update-batch.js";
8
+ /** Where the scope's installations live: its root and, for the project scope, its manifest store. */
9
+ function scopeLocation(input) {
10
+ if (input.scope === "user-global")
11
+ return { projectRoot: path.resolve(input.homeDirectory ?? homedir()) };
12
+ const projectRoot = path.resolve(input.projectRoot ?? process.cwd());
13
+ return {
14
+ projectRoot,
15
+ manifestStore: input.projectManifestStore ?? new ProjectManifestStore(defaultProjectManifestPath(projectRoot)),
16
+ };
17
+ }
18
+ /** The scope's installations, read without assessing them. */
19
+ export async function readScopeInstallations(input) {
20
+ const { manifestStore } = scopeLocation(input);
21
+ return manifestStore ? (await manifestStore.load()).skills : requireUserGlobalInstallations(input.operations);
22
+ }
23
+ /** Loads the scope's installations and assesses them like `update check`. Changes nothing. */
24
+ export async function loadUpdates(input) {
25
+ const { scope, operations } = input;
26
+ const sourceAccess = input.sourceAccess ?? defaultProjectSkillTreeAccess();
27
+ const { projectRoot, manifestStore } = scopeLocation(input);
28
+ const installed = await readScopeInstallations(input);
29
+ const resolveSource = operations.resolveProjectSource
30
+ ? (source) => operations.resolveProjectSource(source)
31
+ : resolveProjectSource;
32
+ const assessment = await assessUpdateBatch(installed, { sourceAccess, resolveSource });
33
+ const context = {
34
+ projectRoot,
35
+ scope,
36
+ sourceAccess,
37
+ sourceOperations: operations,
38
+ trackedInstallationPaths: installed.flatMap((selection) => selection.installation === undefined ? [] : [selection.installation.path]),
39
+ ...(manifestStore === undefined ? {} : { projectManifestStore: manifestStore }),
40
+ ...(input.installationFileSystem === undefined ? {} : { installationFileSystem: input.installationFileSystem }),
41
+ };
42
+ return { assessment, installed, context };
43
+ }
44
+ async function requireUserGlobalInstallations(operations) {
45
+ if (!operations.listUserGlobalInstallations) {
46
+ throw new Error("Configured Source operations cannot inspect user-global Skill updates");
47
+ }
48
+ return operations.listUserGlobalInstallations();
49
+ }
50
+ /** Previews every selected candidate independently; a failing preview never hides the others. */
51
+ export async function previewUpdateCandidates(selected, context) {
52
+ const plans = [];
53
+ const failures = [];
54
+ const nonCanonicalPaths = [];
55
+ for (const item of selected) {
56
+ try {
57
+ const plan = await previewSkillUpdate(item, context);
58
+ if (plan.nonCanonicalPath !== undefined)
59
+ nonCanonicalPaths.push(relativeProjectPath(context.projectRoot, plan.nonCanonicalPath));
60
+ plans.push(plan);
61
+ }
62
+ catch (error) {
63
+ failures.push({ item, message: error instanceof Error ? error.message : String(error) });
64
+ }
65
+ }
66
+ return { plans, failures, nonCanonicalPaths };
67
+ }
68
+ /** The preview `update apply` prints for one candidate. */
69
+ export function describeUpdatePreview(plan, scope, projectRoot, wording = "flags") {
70
+ const lines = [
71
+ `Preview: update Skill ${JSON.stringify(plan.selection.path)} from ${plan.source.id} (scope: ${scope})`,
72
+ ` target: ${plan.target}`,
73
+ ];
74
+ if (plan.nonCanonicalPath !== undefined) {
75
+ const relative = relativeProjectPath(projectRoot, plan.nonCanonicalPath);
76
+ lines.push(wording === "flags"
77
+ ? ` WARNING: non-canonical location ${JSON.stringify(relative)} (outside .agents/skills and .claude/skills); --yes together with --confirm-path ${relative} explicitly confirms replacing this exact path`
78
+ : ` WARNING: non-canonical location ${JSON.stringify(relative)} (outside .agents/skills and .claude/skills); a separate confirmation of this exact path is required to replace it`);
79
+ }
80
+ lines.push(` proposed changes: replace ${plan.snapshot.files.length} managed files and persist the new version/content baseline`);
81
+ if (plan.overwriteRequired) {
82
+ lines.push(wording === "flags"
83
+ ? " WARNING: the installed Skill has local modifications; --yes explicitly confirms replacing them"
84
+ : " WARNING: the installed Skill has local modifications; confirming replaces them");
85
+ }
86
+ if (plan.methodPreview) {
87
+ lines.push(` external command: argv=${JSON.stringify(plan.methodPreview.argv)}`, ` external cwd: ${JSON.stringify(plan.methodPreview.cwd)}`, ` external target: ${JSON.stringify(plan.methodPreview.target)}`, " declared changes:", ...plan.methodPreview.declaredChanges.map((change) => ` ${change}`), ` WARNING: ${plan.methodPreview.warning}`);
88
+ }
89
+ return lines;
90
+ }
91
+ /**
92
+ * `--yes` alone never confirms a repository-controlled non-canonical location:
93
+ * every pending path needs its own exact confirmation, and a value that matches
94
+ * no pending path is an error rather than silently ignored. Returns the problem, if any.
95
+ */
96
+ export function findPathConfirmationProblem(pending, confirmedPaths) {
97
+ const unmatched = confirmedPaths.filter((value) => !pending.includes(value));
98
+ if (unmatched.length > 0) {
99
+ const shown = pending.length === 0 ? "none" : pending.map((value) => JSON.stringify(value)).join(", ");
100
+ return `--confirm-path ${unmatched.map((value) => JSON.stringify(value)).join(", ")} does not match any pending non-canonical location (pending: ${shown}); no path was changed`;
101
+ }
102
+ const missing = pending.filter((value) => !confirmedPaths.includes(value));
103
+ if (missing.length > 0) {
104
+ const flags = missing.map((value) => `--confirm-path ${value}`).join(" ");
105
+ return `Non-canonical location ${missing.map((value) => JSON.stringify(value)).join(", ")} needs explicit path confirmation; --yes alone is not enough. Rerun with ${flags}; no path was changed`;
106
+ }
107
+ return undefined;
108
+ }
109
+ /**
110
+ * Applies the selected updates after the caller collected the confirmation.
111
+ * Overwriting modified installations and running external methods are confirmed
112
+ * by that same confirmation; a non-canonical path only when its exact relative
113
+ * path is listed.
114
+ */
115
+ export function applyConfirmedUpdates(selected, context, confirmedPaths,
116
+ /** Ids of the items whose preview showed the overwrite of local modifications; when given, any other item that turns out modified fails instead of being overwritten. */
117
+ previewedOverwrites) {
118
+ return applyUpdateBatch(selected, {
119
+ ...context,
120
+ // The callback is invoked independently for every candidate. In particular,
121
+ // a modified installation is never overwritten unless the caller confirmed.
122
+ confirm: async (plan) => {
123
+ if (previewedOverwrites !== undefined && plan.overwriteRequired && !previewedOverwrites.has(plan.item.id)) {
124
+ throw new Error(`Skill ${JSON.stringify(plan.item.id)} was modified after the preview, so the overwrite was never shown or confirmed; nothing was changed`);
125
+ }
126
+ return {
127
+ overwriteModifiedInstallation: true,
128
+ externalMethod: true,
129
+ ...(plan.nonCanonicalPath !== undefined && confirmedPaths.includes(relativeProjectPath(context.projectRoot, plan.nonCanonicalPath))
130
+ ? { nonCanonicalPath: plan.nonCanonicalPath }
131
+ : {}),
132
+ };
133
+ },
134
+ });
135
+ }
@@ -0,0 +1,7 @@
1
+ /** Raised for invalid CLI usage; the message is printed verbatim. */
2
+ export class CliUsageError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "CliUsageError";
6
+ }
7
+ }