@iseoane/agent-depot 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.es.md +969 -0
  3. package/README.md +254 -25
  4. package/dist/src/app-flow.js +52 -51
  5. package/dist/src/app-recipe-removal.js +63 -0
  6. package/dist/src/cli.js +122 -11
  7. package/dist/src/executable-resolution.js +54 -0
  8. package/dist/src/git-source.js +129 -6
  9. package/dist/src/package-bundles.js +614 -0
  10. package/dist/src/package-cli.js +412 -0
  11. package/dist/src/package-content.js +158 -0
  12. package/dist/src/package-flow.js +636 -0
  13. package/dist/src/package-host-plans.js +240 -0
  14. package/dist/src/package-host-state.js +349 -0
  15. package/dist/src/package-model.js +33 -0
  16. package/dist/src/package-owned-skills.js +122 -0
  17. package/dist/src/package-state.js +404 -0
  18. package/dist/src/profile-cli.js +15 -9
  19. package/dist/src/profile-export.js +44 -9
  20. package/dist/src/profile-import.js +80 -17
  21. package/dist/src/profile.js +78 -3
  22. package/dist/src/skill-discovery.js +2 -2
  23. package/dist/src/skill-install.js +16 -0
  24. package/dist/src/sources.js +11 -0
  25. package/dist/src/tui/app-actions.js +1 -1
  26. package/dist/src/tui/app.js +5 -6
  27. package/dist/src/tui/catalog-browse.js +10 -3
  28. package/dist/src/tui/catalog-panel.js +7 -3
  29. package/dist/src/tui/catalog-state.js +57 -6
  30. package/dist/src/tui/catalog-tree.js +52 -10
  31. package/dist/src/tui/catalog-view.js +27 -4
  32. package/dist/src/tui/environment.js +8 -1
  33. package/dist/src/tui/installations-browse.js +20 -2
  34. package/dist/src/tui/installations-rows.js +2 -1
  35. package/dist/src/tui/installations-tree.js +14 -1
  36. package/dist/src/tui/installations-view.js +8 -2
  37. package/dist/src/tui/installations.js +12 -2
  38. package/dist/src/tui/manage-actions.js +2 -0
  39. package/dist/src/tui/manage-panel.js +5 -0
  40. package/dist/src/tui/package-actions.js +165 -0
  41. package/dist/src/tui/package-rows.js +87 -0
  42. package/dist/src/tui/profile-view.js +36 -14
  43. package/dist/src/tui/sources-mode.js +2 -0
  44. package/dist/src/tui/sources-recipes.js +116 -53
  45. package/dist/src/tui/sources-view.js +57 -19
  46. package/dist/src/tui/tree.js +3 -4
  47. package/dist/src/tui/updates-flow.js +4 -1
  48. package/dist/src/tui/updates-panel.js +2 -2
  49. package/dist/src/tui/updates.js +72 -4
  50. package/dist/src/user-global-skill-inventory.js +7 -2
  51. package/package.json +2 -1
  52. package/skills/agent-depot-apprecipe/SKILL.md +29 -5
@@ -6,6 +6,14 @@ import { rowStyle, theme } from "./theme.js";
6
6
  import { computeWindow, pageStep, useListHeight } from "./window.js";
7
7
  import { useKeys } from "./keys.js";
8
8
  import { useSourcesRecipes } from "./sources-recipes.js";
9
+ /** Footer keys for the selected row; App seeds the footer before the view reports a selection. */
10
+ export function sourceRowHints(row) {
11
+ if (row === undefined)
12
+ return "n add · q quit";
13
+ return row.kind === "app"
14
+ ? "j/k move · Enter preview/approve · n add · r refresh · d remove · q quit"
15
+ : "j/k move · space mark · a all · Enter catalog · n add · r refresh · d remove · q quit";
16
+ }
9
17
  /** The highlighted index: by stable id while that source exists, otherwise the bounded fallback index. */
10
18
  function resolveIndex(sources, selection) {
11
19
  const found = sources.findIndex((source) => source.id === selection.id);
@@ -29,7 +37,7 @@ async function refreshEach(operations, targets) {
29
37
  function errorText(error) {
30
38
  return error instanceof Error ? error.message : String(error);
31
39
  }
32
- export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight, environment }) {
40
+ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight, environment, onHintsChange }) {
33
41
  const height = useListHeight(listHeight, 9);
34
42
  const [state, setState] = useState({ status: "loading" });
35
43
  // The highlight follows the source id; the index is only the fallback once that source is gone.
@@ -95,12 +103,28 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
95
103
  const sources = allSources.filter((source) => source.kind === "git");
96
104
  const sourcesRef = useRef(sources);
97
105
  sourcesRef.current = sources;
98
- const selected = resolveIndex(sources, selection);
106
+ const rowsRef = useRef([]);
99
107
  const recipes = useSourcesRecipes({
100
- operations, environment, onCapturingChange, height, gitCount: sources.length,
108
+ operations, environment, onCapturingChange,
109
+ highlightedFile: () => {
110
+ const rows = rowsRef.current;
111
+ const row = rows[resolveIndex(rows, selectionRef.current)];
112
+ return row?.kind === "app" ? row.recipe.entry.file : undefined;
113
+ },
101
114
  });
115
+ const rows = [
116
+ ...sources.map((source) => ({ id: source.id, kind: "skills", source })),
117
+ ...recipes.rows.map((recipe) => ({ id: `recipe:${recipe.entry.file}`, kind: "app", recipe })),
118
+ ];
119
+ rowsRef.current = rows;
120
+ const selected = resolveIndex(rows, selection);
121
+ const selectedRow = rows[selected];
122
+ const hints = sourceRowHints(selectedRow);
123
+ useEffect(() => { onHintsChange?.(hints); }, [hints, onHintsChange]);
102
124
  const select = (index) => {
103
- const listed = sourcesRef.current;
125
+ setMessage(undefined);
126
+ recipes.clearMessage();
127
+ const listed = rowsRef.current;
104
128
  const bounded = Math.min(Math.max(index, 0), Math.max(listed.length - 1, 0));
105
129
  setSelection({ id: listed[bounded]?.id, index: bounded });
106
130
  };
@@ -164,6 +188,19 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
164
188
  const mode = modeRef.current;
165
189
  if (mode.kind === "busy")
166
190
  return;
191
+ if (mode.kind === "add-kind") {
192
+ if (key.escape || input === "n")
193
+ dispatch({ type: "done" });
194
+ else if (input === "1") {
195
+ typed.current = "";
196
+ dispatch({ type: "input" });
197
+ }
198
+ else if (input === "2") {
199
+ dispatch({ type: "done" });
200
+ recipes.startAdd();
201
+ }
202
+ return;
203
+ }
167
204
  if (mode.kind === "input") {
168
205
  if (key.escape)
169
206
  dispatch({ type: "done" });
@@ -205,14 +242,13 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
205
242
  dispatch({ type: "done" });
206
243
  return;
207
244
  }
208
- if (recipes.handleKey(input, key)) {
209
- if (key.tab)
210
- setMessage(undefined);
245
+ if (recipes.handleKey(input, key))
211
246
  return;
212
- }
247
+ const rows = rowsRef.current;
248
+ const at = resolveIndex(rows, selectionRef.current);
249
+ const row = rows[at];
213
250
  const listed = sourcesRef.current;
214
- const at = resolveIndex(listed, selectionRef.current);
215
- const highlighted = listed[at];
251
+ const highlighted = row?.kind === "skills" ? row.source : undefined;
216
252
  if (key.downArrow || input === "j") {
217
253
  select(at + 1);
218
254
  }
@@ -220,24 +256,23 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
220
256
  select(at - 1);
221
257
  }
222
258
  else if (key.pageDown) {
223
- select(at + pageStep(recipes.gitHeight));
259
+ select(at + pageStep(height));
224
260
  }
225
261
  else if (key.pageUp) {
226
- select(at - pageStep(recipes.gitHeight));
262
+ select(at - pageStep(height));
227
263
  }
228
264
  else if (key.return && highlighted) {
229
265
  onOpenCatalog?.(highlighted);
230
266
  }
231
267
  else if (input === "n") {
232
268
  setMessage(undefined);
233
- typed.current = "";
234
- dispatch({ type: "input" });
269
+ dispatch({ type: "add-kind" });
235
270
  }
236
271
  else if (input === " " && highlighted) {
237
272
  setMessage(undefined);
238
273
  toggle([highlighted.id]);
239
274
  }
240
- else if (input === "a") {
275
+ else if (input === "a" && row?.kind !== "app") {
241
276
  if (listed.length === 0)
242
277
  stop("There are no Git sources to select");
243
278
  else {
@@ -245,7 +280,7 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
245
280
  toggle(listed.map((source) => source.id));
246
281
  }
247
282
  }
248
- else if (input === "r") {
283
+ else if (input === "r" && row?.kind !== "app") {
249
284
  const targets = markedRef.current.size > 0
250
285
  ? listed.filter((source) => markedRef.current.has(source.id))
251
286
  : highlighted ? [highlighted] : [];
@@ -264,13 +299,16 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
264
299
  return _jsx(Text, { children: "Loading sources..." });
265
300
  if (state.status === "error")
266
301
  return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
267
- const window = computeWindow(sources.length, selected, recipes.gitHeight);
268
- return (_jsxs(Box, { flexDirection: "column", children: [builtin ? _jsxs(Text, { color: theme.muted, children: [" ", builtin.id, " \u00B7 included"] }) : null, sources.length === 0 ? _jsx(Text, { children: "No Git sources \u00B7 n to add" }) : null, sources.length > 0 ? _jsxs(Text, { color: theme.muted, children: [" ", "ID KIND URL"] }) : null, sources.slice(window.start, window.end).map((source, offset) => {
302
+ const window = computeWindow(rows.length, selected, height);
303
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.muted, children: [" ", "ID KIND LOCATION / STATUS"] }), builtin ? _jsxs(Text, { color: theme.muted, children: [" ", builtin.id, " skills included"] }) : null, rows.length === 0 ? _jsx(Text, { children: "No sources or App recipes \u00B7 n to add" }) : null, rows.slice(window.start, window.end).map((row, offset) => {
269
304
  const index = window.start + offset;
270
- return (_jsxs(Text, { ...rowStyle(!recipes.focused && index === selected), children: [!recipes.focused && index === selected ? "> " : " ", marked.has(source.id) ? "[x] " : "[ ] ", source.id, " ", source.kind, " ", source.url] }, source.id));
305
+ const id = row.kind === "skills" ? row.source.id : row.recipe.entry.recipe?.name ?? row.recipe.entry.file.split(/[\\/]/u).pop();
306
+ return (_jsxs(Text, { ...rowStyle(index === selected), children: [index === selected ? "> " : " ", row.kind === "skills" ? (marked.has(row.id) ? "[x] " : "[ ] ") : " ", id, " ", row.kind, " ", row.kind === "skills" ? row.source.url : `${row.recipe.entry.file} · ${row.recipe.status}`] }, row.id));
271
307
  }), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : null, recipes.panel, _jsx(Prompt, { mode: mode }), message ? _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) : null] }));
272
308
  }
273
309
  function Prompt({ mode }) {
310
+ if (mode.kind === "add-kind")
311
+ return _jsx(Text, { children: "Add: 1 Git Source (skills + Packages) \u00B7 2 App recipe (Esc cancel)" });
274
312
  if (mode.kind === "input")
275
313
  return _jsxs(Text, { children: ["URL: ", mode.value] });
276
314
  if (mode.kind === "busy")
@@ -1,7 +1,6 @@
1
- /** Initially only the first group with children is open; every other group starts collapsed. */
2
- export function defaultExpanded(roots) {
3
- const first = roots.find((root) => (root.children?.length ?? 0) > 0);
4
- return new Set(first ? [first.id] : []);
1
+ /** Every group starts collapsed until the user expands it. */
2
+ export function defaultExpanded() {
3
+ return new Set();
5
4
  }
6
5
  /** The visible nodes in display order: children appear only under expanded parents. */
7
6
  export function flattenVisible(roots, expanded) {
@@ -81,9 +81,12 @@ export function useUpdatesFlow(inputs) {
81
81
  }
82
82
  if (!mounted.current)
83
83
  return;
84
- setMessage(result);
85
84
  // The sources were fetched for the check that was just applied; only the installations changed.
85
+ // The summary lands with the refreshed list, so no check is still writing when the view reports done.
86
86
  await load(false);
87
+ if (!mounted.current)
88
+ return;
89
+ setMessage(result);
87
90
  };
88
91
  return { state, load, startPreview, apply };
89
92
  }
@@ -7,7 +7,7 @@ import { PreviewLines } from "./panel-parts.js";
7
7
  /** What the last check found out: its age, how the sources were refreshed, and what failed. */
8
8
  export function CheckStatus({ data, now }) {
9
9
  const errors = data.results.filter((result) => result.error !== undefined);
10
- return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["checked ", formatAgo(now - data.checkedAt), " \u00B7 ", describeRefresh(data)] }), data.refresh.failed.map((failure) => (_jsxs(Text, { color: theme.warning, children: ["refresh failed for ", failure.source, ": ", failure.reason] }, failure.source))), data.appError ? _jsxs(Text, { color: theme.error, children: ["Error (Apps): ", data.appError] }) : null, errors.map((result) => _jsxs(Text, { color: theme.error, children: ["Error (", result.scope, "): ", result.error] }, result.scope))] }));
10
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["checked ", formatAgo(now - data.checkedAt), " \u00B7 ", describeRefresh(data)] }), data.refresh.failed.map((failure) => (_jsxs(Text, { color: theme.warning, children: ["refresh failed for ", failure.source, ": ", failure.reason] }, failure.source))), data.appError ? _jsxs(Text, { color: theme.error, children: ["Error (Apps): ", data.appError] }) : null, data.packageError ? _jsxs(Text, { color: theme.error, children: ["Error (Packages): ", data.packageError] }) : null, errors.map((result) => _jsxs(Text, { color: theme.error, children: ["Error (", result.scope, "): ", result.error] }, result.scope))] }));
11
11
  }
12
12
  /** One line of the list: an update, the fold of unassessable items, or one of those when unfolded. */
13
13
  export function EntryLine({ entry, selected, checked, view, showUnknown }) {
@@ -35,7 +35,7 @@ export function ModePanel({ mode }) {
35
35
  case "busy":
36
36
  return mode.label === REFRESHING || mode.label === CHECKING ? null : _jsx(Text, { children: mode.label });
37
37
  case "preview":
38
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PreviewLines, { lines: mode.prepared.lines }), _jsx(PreviewLines, { lines: mode.prepared.appFailures }), mode.prepared.failures.map(({ item, message }) => (_jsxs(Text, { color: theme.error, children: ["Preview failed for ", JSON.stringify(item.id), ": ", message] }, item.id))), mode.prepared.applicable.length === 0
38
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PreviewLines, { lines: mode.prepared.lines }), _jsx(PreviewLines, { lines: mode.prepared.appFailures }), _jsx(PreviewLines, { lines: mode.prepared.packageFailures }), mode.prepared.failures.map(({ item, message }) => (_jsxs(Text, { color: theme.error, children: ["Preview failed for ", JSON.stringify(item.id), ": ", message] }, item.id))), mode.prepared.applicable.length === 0
39
39
  ? _jsx(Text, { children: "Nothing can be applied. Enter/Esc to go back" })
40
40
  : _jsxs(Text, { children: ["Apply ", mode.prepared.applicable.length, " ", mode.prepared.applicable.length === 1 ? "update" : "updates", "? y/n"] })] }));
41
41
  case "confirm-paths":
@@ -1,7 +1,10 @@
1
1
  import { checkAppUpdates, createAppOperations } from "../app-flow.js";
2
+ import { describeCoordinates, selectionKey } from "../package-model.js";
2
3
  import { formatVersionPolicy } from "../skill-install.js";
3
4
  import { applyConfirmedUpdates, describeUpdatePreview, loadUpdates, previewUpdateCandidates, readScopeInstallations, } from "../update-flow.js";
5
+ import { packageOperationsOf } from "./environment.js";
4
6
  import { formatVersionEvidence } from "./installations.js";
7
+ import { installedEvidenceLabel, packagePlanLines, packageStatusLabel, packageVersionLabel, recordedVersionLabel } from "./package-rows.js";
5
8
  /** Every scope the view can assess, in display order. */
6
9
  export const UPDATE_SCOPES = ["project", "user-global"];
7
10
  const APP_STATUS_LABEL = {
@@ -68,6 +71,7 @@ async function refreshSources(operations, selections) {
68
71
  */
69
72
  export async function loadUpdateRows(operations, environment, options) {
70
73
  const apps = createAppOperations({ homeDirectory: environment.homeDirectory, ...environment.appEnvironment });
74
+ const packages = packageOperationsOf(environment);
71
75
  let refresh = { attempted: false, refreshed: [], failed: [], unreadable: [] };
72
76
  if (options.refresh) {
73
77
  options.onPhase?.("refreshing");
@@ -121,7 +125,29 @@ export async function loadUpdateRows(operations, environment, options) {
121
125
  reason: check.reason ?? check.inspection.reason ?? check.inspection.status,
122
126
  });
123
127
  }
124
- return { results, rows, refresh, apps, appError, checkedAt: (options.now ?? Date.now)() };
128
+ let packageChecks = [];
129
+ let packageError;
130
+ try {
131
+ packageChecks = await packages.checkUpdates();
132
+ }
133
+ catch (error) {
134
+ packageError = errorText(error);
135
+ }
136
+ for (const check of packageChecks) {
137
+ rows.push({
138
+ kind: "package",
139
+ key: `package:${selectionKey(check.installed.selection)}`,
140
+ scope: "user-global",
141
+ check,
142
+ path: `Package: ${describeCoordinates(check.installed.selection)}`,
143
+ policy: formatVersionPolicy(check.installed.selection.version),
144
+ installed: recordedVersionLabel(check.installed),
145
+ available: check.available === undefined ? "?" : packageVersionLabel(check.available),
146
+ status: packageStatusLabel(check.status),
147
+ reason: check.reason ?? "the host reports no usable version evidence",
148
+ });
149
+ }
150
+ return { results, rows, refresh, apps, packages, appError, packageError, checkedAt: (options.now ?? Date.now)() };
125
151
  }
126
152
  /** `just now`, `42s ago`, `5 min ago`, `2 h ago`. */
127
153
  export function formatAgo(elapsedMs) {
@@ -140,10 +166,12 @@ export async function prepareUpdates(data, selected) {
140
166
  const groups = [];
141
167
  const appPlans = [];
142
168
  const appFailures = [];
169
+ const packagePlans = [];
170
+ const packageFailures = [];
143
171
  const lines = [];
144
172
  const failures = [];
145
173
  for (const { scope, loaded } of data.results) {
146
- const items = selected.filter((row) => row.kind !== "app" && row.scope === scope).map((row) => row.item);
174
+ const items = selected.filter((row) => row.kind !== "app" && row.kind !== "package" && row.scope === scope).map((row) => row.item);
147
175
  if (!loaded || items.length === 0)
148
176
  continue;
149
177
  const { context } = loaded;
@@ -164,16 +192,32 @@ export async function prepareUpdates(data, selected) {
164
192
  try {
165
193
  const plan = await data.apps.planLifecycle(row.check.entry, "update", undefined, row.check);
166
194
  appPlans.push(plan);
167
- lines.push(`Environment: ${plan.environment}`, row.path, `Version: ${plan.previousVersion} -> ${plan.latestVersion ?? "unknown"}`, ...(plan.argv ? [`argv: ${JSON.stringify(plan.argv)}`, `Executable: ${plan.executable ?? "unavailable"}`] : []), ...(plan.manual ? [`Manual: ${plan.manual}`] : []), `Working directory: ${plan.cwd}`, ...(plan.warning ? [plan.warning] : []));
195
+ lines.push(`Environment: ${plan.environment}`, row.path, `Version: ${plan.previousVersion} -> ${plan.latestVersion ?? "unknown"}`, ...(plan.argv ? [`argv: ${JSON.stringify(plan.argv)}`, `Executable: ${plan.executable ?? `${JSON.stringify(plan.argv[0])} not found in PATH`}`] : []), ...(plan.manual ? [`Manual: ${plan.manual}`] : []), `Working directory: ${plan.cwd}`, ...(plan.warning ? [plan.warning] : []));
168
196
  }
169
197
  catch (error) {
170
198
  appFailures.push(`Failed ${row.path}: preview failed: ${errorText(error)}`);
171
199
  }
172
200
  }
201
+ for (const row of selected) {
202
+ if (row.kind !== "package" || !data.packages)
203
+ continue;
204
+ try {
205
+ const plan = await data.packages.planLifecycle(row.check.installed.selection, "update");
206
+ const approval = await data.packages.approvalStatus(row.check.installed.selection, "update");
207
+ packagePlans.push({ plan, approval });
208
+ lines.push(...packagePlanLines(plan, approval));
209
+ if (approval === "needs approval")
210
+ lines.push("This apply approves the declaration above.");
211
+ }
212
+ catch (error) {
213
+ packageFailures.push(`Failed ${row.path}: preview failed: ${errorText(error)}`);
214
+ }
215
+ }
173
216
  return {
174
217
  groups, apps: data.apps, appPlans, appFailures,
218
+ packages: data.packages, packagePlans, packageFailures,
175
219
  lines,
176
- applicable: [...groups.flatMap((group) => group.applicable), ...appPlans],
220
+ applicable: [...groups.flatMap((group) => group.applicable), ...appPlans, ...packagePlans.map(({ plan }) => plan)],
177
221
  failures,
178
222
  nonCanonicalPaths: groups.flatMap((group) => group.nonCanonicalPaths.map((location) => ({ scope: group.scope, path: location }))),
179
223
  };
@@ -220,7 +264,31 @@ export async function runUpdates(prepared, confirmedPaths, confirmManual) {
220
264
  failedLines.push(`Failed ${name}: ${errorText(error)}`);
221
265
  }
222
266
  }
267
+ for (const { plan, approval } of prepared.packagePlans ?? []) {
268
+ const name = `Package: ${plan.descriptor.name} (${plan.selection.host})`;
269
+ try {
270
+ const packages = prepared.packages;
271
+ if (!packages)
272
+ throw new Error("Package operations unavailable; check again");
273
+ if (approval === "needs approval")
274
+ await packages.approve(plan.selection, plan.action);
275
+ const result = await packages.executeLifecycle(plan, true);
276
+ if (result.status === "updated") {
277
+ updated.push(`Updated ${name}: ${installedEvidenceLabel(result.previous)} -> ${installedEvidenceLabel(result.verified)}`);
278
+ }
279
+ else if (result.status === "failed" || result.status === "stale plan" || result.status === "manual required") {
280
+ failedLines.push(`Failed ${name}: ${result.reason}`);
281
+ }
282
+ else {
283
+ failedLines.push(`Failed ${name}: unexpected result ${JSON.stringify(result.status)}`);
284
+ }
285
+ }
286
+ catch (error) {
287
+ failedLines.push(`Failed ${name}: ${errorText(error)}`);
288
+ }
289
+ }
223
290
  failedLines.push(...prepared.appFailures);
291
+ failedLines.push(...prepared.packageFailures);
224
292
  const previewFailed = prepared.failures.map(({ item, message }) => `Failed ${item.selection.path}: preview failed: ${message}`);
225
293
  const failed = failedLines.length + previewFailed.length;
226
294
  return {
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { assertSkillNotAppOwned, loadAppOwnedSkillFilter } from "./app-owned-skills.js";
6
+ import { assertSkillNotPackageOwned, loadPackageOwnedSkillFilter, } from "./package-owned-skills.js";
6
7
  import { pathsOverlap } from "./path-safety.js";
7
8
  import { readExistingSkillTree } from "./skill-adoption.js";
8
9
  import { parseSkillFrontmatter, skillTreeBaseline } from "./skill-discovery.js";
@@ -51,7 +52,9 @@ export async function scanUserGlobalSkillInventory(options = {}) {
51
52
  const exposures = [];
52
53
  const symlinks = [];
53
54
  const isAppOwned = await loadAppOwnedSkillFilter({ ...options, homeDirectory });
54
- const state = { isAppOwned, managedPaths, managedCanonicalPaths, entries, skipped, exposures, symlinks };
55
+ const owned = await loadPackageOwnedSkillFilter({ ...options, homeDirectory });
56
+ const isPackageOwned = (name) => owned(name) !== undefined;
57
+ const state = { isAppOwned, isPackageOwned, managedPaths, managedCanonicalPaths, entries, skipped, exposures, symlinks };
55
58
  for (const root of GLOBAL_SKILL_ROOTS) {
56
59
  const rootPath = path.join(homeDirectory, ...root.relativePath);
57
60
  await scanRoot(root, rootPath, homeDirectory, state);
@@ -108,7 +111,7 @@ async function listScannableRoot(root, rootPath, homeDirectory, skipped) {
108
111
  }
109
112
  async function scanRootEntry(root, name, candidatePath, homeDirectory, state) {
110
113
  const { managedPaths, entries, skipped } = state;
111
- if (!managedPaths.has(pathKey(candidatePath)) && state.isAppOwned(name))
114
+ if (!managedPaths.has(pathKey(candidatePath)) && (state.isAppOwned(name) || state.isPackageOwned(name)))
112
115
  return;
113
116
  if (isTransientName(name)) {
114
117
  skipped.push({ name, path: candidatePath, root: root.kind, reason: "excluded-transient" });
@@ -293,6 +296,7 @@ export async function inspectUserGlobalSkillRemoval(candidatePath, options) {
293
296
  const homeDirectory = path.resolve(options.homeDirectory);
294
297
  const targetPath = validateExplicitGlobalPath(candidatePath, homeDirectory);
295
298
  await assertSkillNotAppOwned(path.basename(candidatePath), options);
299
+ assertSkillNotPackageOwned(path.basename(candidatePath), await loadPackageOwnedSkillFilter({ ...options, homeDirectory }));
296
300
  const fileSystem = options.fileSystem ?? nodeFileSystem;
297
301
  await assertRealGlobalAncestors(homeDirectory, path.dirname(targetPath), fileSystem);
298
302
  const managedPaths = managedProtectionPaths(homeDirectory, options.managedInstallations);
@@ -388,6 +392,7 @@ export async function inspectUserGlobalSymlinkRemoval(candidatePath, options) {
388
392
  const homeDirectory = path.resolve(options.homeDirectory);
389
393
  const linkPath = validateExplicitGlobalPath(candidatePath, homeDirectory);
390
394
  await assertSkillNotAppOwned(path.basename(candidatePath), options);
395
+ assertSkillNotPackageOwned(path.basename(candidatePath), await loadPackageOwnedSkillFilter({ ...options, homeDirectory }));
391
396
  const fileSystem = options.fileSystem ?? nodeFileSystem;
392
397
  await assertRealGlobalAncestors(homeDirectory, path.dirname(linkPath), fileSystem);
393
398
  const managedPath = managedProtectionPaths(homeDirectory, options.managedInstallations)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iseoane/agent-depot",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A cross-platform catalog for reusable coding-agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "iseoane",
@@ -30,6 +30,7 @@
30
30
  "!skills/**/test_*.py",
31
31
  "!skills/**/testdata",
32
32
  "README.md",
33
+ "README.es.md",
33
34
  "LICENSE",
34
35
  "CHANGELOG.md"
35
36
  ],
@@ -4,7 +4,7 @@ description: "Trigger: App recipe creation or repair. Research an App's actual i
4
4
  license: MIT
5
5
  metadata:
6
6
  author: "iseoane"
7
- version: "1.0"
7
+ version: "1.2"
8
8
  ---
9
9
 
10
10
  ## Activation Contract
@@ -17,7 +17,8 @@ Use for creating or repairing user-global App recipes, not portable Skills.
17
17
  - Write user-owned JSON in the environment-local `apps/` beside `sources.json`. Preserve existing files unless the user authorizes replacement. Keep Apps out of Sources and project manifests.
18
18
  - Never approve a recipe on the user's behalf. Approval belongs to the user via `agent-depot app approve <name> --yes`; changed bytes need renewed approval. Validation is not approval.
19
19
  - Use bare executable-plus-arguments arrays, without shells, shell syntax, credentials or environment assignments. Script-only routes (`curl | sh`, PowerShell scripts, `.cmd`) belong in `manual` text, never pipe-to-shell argv. Agent Depot displays manual text without executing it.
20
- - Steps run from the user's home with stdin EOF and no timeout. Use non-interactive flags; terminal-dependent installers belong in manual steps.
20
+ - Steps run from the user's home with stdin EOF and no timeout. Verify non-interactive behavior against the installed installer's help/version, not just an upstream quickstart. Use supported non-interactive flags; terminal-dependent installers belong in manual steps.
21
+ - Preflight the chosen channel before writing lifecycle steps. Missing prerequisites must be declared, never silently installed, configured or trusted. Recipe approval does not authorize separate prerequisite installation.
21
22
 
22
23
  ## Decision Gates
23
24
 
@@ -30,15 +31,38 @@ Use for creating or repairing user-global App recipes, not portable Skills.
30
31
  | brew / npm / pipx | Derive update and uninstall from that channel: upgrade/uninstall, global install/uninstall, or upgrade/uninstall respectively. |
31
32
  | Go install / release binary | Use the documented update route and a manual binary-removal step. |
32
33
  | App has its own uninstall | Prefer it after verifying scope and non-interactive flags; explain remaining channel cleanup. |
34
+ | Missing manager, runtime, PATH entry, repository/tap, permissions or hook dependency | Explain the blocker and preparation before approval. Use a complete manual route or ask whether the user wants an explicitly prerequisite-dependent recipe; never call it ready to install. |
35
+ | Installer needs several commands | Use schema-supported ordered steps only if present. Otherwise put the complete sequence in `manual` or obtain the user's explicit choice of the prerequisite-dependent route below; never invent dependency fields, concatenate commands in argv, or hide preparation in a fallback. |
36
+
37
+ ## Installer Preflight
38
+
39
+ Check installation **and** update, uninstall, version/latest and each Host setup/teardown. Record evidence as satisfied, missing or unverified:
40
+
41
+ - **Executable and environment:** inspect all manager/runtime PATH matches, resolved paths and versions in the environment Agent Depot actually inherits. A shell alias/function is not a spawnable executable. A terminal's PATH change does not reach an already-running TUI; tell the user to relaunch it from the prepared environment. Prefer read-only checks; do not create shims or edit shell configuration merely to make validation pass.
42
+ - **Channel registration and trust:** inspect required taps, repositories, registries and plugin marketplaces, their identities and current package metadata. Verify whether this installer version auto-registers them or requires explicit preparation; do not assume historical behavior. Report missing registration before writing argv. Registration can clone repositories and execute package definitions: do not run it as a supposedly read-only probe or without user authorization.
43
+ - **Runtime/build and hook requirements:** verify documented engine ranges, compiler/toolchain minimums, architecture/libc support and utilities such as `git`, `curl`, `jq` or extraction tools. Check write access to the actual global prefix and required elevation; never assume sudo or request credentials in chat. Inspect prerequisites for Host plugins separately from the App binary.
44
+ - **Unattended behavior:** use installed manager `--help`/documented help and upstream installer source to establish confirmation defaults and supported flags. Verify explicit formula/cask or package disambiguation where necessary. Do not invent `--yes` flags or rely on stdin EOF to count as consent. Run dry-run/read-only probes only after checking they do not install, register, authenticate or rewrite configuration; never test a lifecycle command just to see whether it prompts.
45
+ - **Version and update signals:** verify a release-meaningful version (not an unchanged `dev`) and a latest signal for the chosen channel. A GitHub release can be ahead of a package-manager formula. Use channel metadata when available and disclose freshness limits; do not run `brew update` or another mutating refresh as a read-only latest probe.
46
+ - **Preserved state:** identify existing binary copies, data directories and Host wiring without reading private memory content. Explain migrations, backups and teardown scope outside executable argv where appropriate; never delete user data or remove a shared tap/marketplace implicitly.
47
+
48
+ ### Homebrew checklist
49
+
50
+ Inspect `brew --version`, `brew help install`, `brew help upgrade`, `brew help uninstall`, `brew tap` and `brew list --versions`. Resolve `brew` from Agent Depot's PATH. Check the actual tap formula/cask and its release rather than trusting a possibly stale README. Homebrew auto-updates taps only for `install`, `outdated`, `upgrade`, `bundle`, `release` and `tap <name>`, never `info`, so `brew info --json=v2 <tap>/<formula>` reports the local clone's last refresh and lags a published release. When the tap mirrors a GitHub release, declare `latest` as `{"github": "owner/repo"}`; install and upgrade refresh the tap themselves. Use supported non-interactive flags for install/update and `--formula` or `--cask` when names conflict. Do not assume `brew install owner/tap/name` registers a missing tap automatically.
51
+
52
+ For example, Homebrew 7.0.8 encountered during recipe research required an explicit `brew tap gentleman-programming/tap`; its install help documented ask-by-default and `--yes`. This is version-specific evidence, not a universal flag or a claim about when the behavior changed. Recheck the user's installed version.
53
+
54
+ ### Representing preparation
55
+
56
+ Read the current schema; do not assume it has dependency or multi-command fields. When a required tap is missing and a lifecycle step accepts only one argv, `install.manual` can describe both registration and installation. Alternatively, with the user's explicit choice, retain automated install as prerequisite-dependent and provide the preparation commands and blocked status clearly in the handoff. An `argv` plus `manual` step falls back **only on spawn failure**: it cannot perform preparation before argv and does not handle a non-zero exit. Keep manual text concise; put research and caveats in the handoff. Never approve either route on the user's behalf. Execute preparation only with separate user authorization; execute recipe commands only after user approval and an explicit execution request.
33
57
 
34
58
  ## Execution Steps
35
59
 
36
60
  1. Record upstream evidence and platform support. Inspect all PATH matches (`which -a`/`whence -a`, or Windows `Get-Command -All`), resolved paths, and channel inventories such as `brew list`, `npm ls -g` or `pipx list`. List Windows shims seen from WSL separately: `/mnt/<drive>/` executables cannot serve a Linux recipe. Finish only when the actual channel is established or the user has chosen one.
37
- 2. Write required `name`, `install`, `update`, `uninstall`, and `version`. Each lifecycle step uses `argv`, `manual`, or both; manual fallback applies only to spawn failure, never non-zero exit. `version` needs read-only argv and a JavaScript regex with exactly one capture group. Choose optional `latest` as `github: owner/repo`, `npm: package`, or read-only `argv` plus `pattern`; omit it when unknown. Consult [the illustrative example](assets/example.md), not as upstream command evidence.
61
+ 2. Complete the Installer Preflight and resolve the prerequisite decision gate before writing required `name`, `install`, `update`, `uninstall`, and `version`. Each lifecycle step uses `argv`, `manual`, or both; manual fallback applies only to spawn failure, never non-zero exit. `version` needs read-only argv and a JavaScript regex with exactly one capture group. Choose optional `latest` as `github: owner/repo`, `npm: package`, or read-only `argv` plus `pattern`; omit it when unknown. Consult [the illustrative example](assets/example.md), not as upstream command evidence.
38
62
  3. Put user-global Host wiring in optional `setup`/`teardown` maps keyed by `pi`, `claude`, `codex`, or `opencode`. Keep install separate; uninstall never implicitly runs teardown. Delegate wiring to App commands or manual user instructions, not direct agent edits of Host configuration. Exclude project initialization.
39
63
  4. Declare optional `skills` only for documented App-owned directory/link names. Match whole names case-sensitively; only `*` is special, while `?` and brackets are literal. Reject empty/whitespace-only, path separators, NUL and star-only patterns. These declarations filter unmanaged adoption/removal even before approval/install; they verify no artifacts or provenance.
40
- 5. Run `agent-depot app validate <file>...` for current-platform recipes. Fix structural errors; report unapproved recipes as needs approval, not runtime-verified. Version commands run only for approved applicable content. Mark other platforms unverified. After user approval, the user can inspect `app list` and separately preview `app install|update|uninstall <name>` or `app setup|teardown <name> --host <host>`.
64
+ 5. Run `agent-depot app validate <file>...` for current-platform recipes. Fix structural errors; report unapproved recipes as needs approval, not runtime-verified. Distinguish structural validity, recipe approval, prerequisite readiness, installed version and Host wiring; none proves the others. A known missing prerequisite remains a blocker even when validation succeeds. Version commands run only for approved applicable content. Mark other platforms unverified. After user approval, the user can inspect `app list` and separately preview `app install|update|uninstall <name>` or `app setup|teardown <name> --host <host>`.
41
65
 
42
66
  ## Output Contract
43
67
 
44
- Return file paths, platform/channel evidence, validation results and runtime-verification limits; unsupported platforms; omitted optional fields and why; license, telemetry/update-check behavior (or unknown); and conflicting duplicate install routes, including plugin-marketplace versus setup routes. Explain that version alone determines installed state, updates require a changed version, and unknown latest is not an available update. End with the user's approval command, without running it.
68
+ Return file paths, platform/channel evidence, a concise prerequisite status (satisfied/missing/unverified) with preparation commands and whether the recipe is ready or blocked, validation results and runtime-verification limits; unsupported platforms; omitted optional fields and why; license, telemetry/update-check behavior (or unknown); and conflicting duplicate install routes, including plugin-marketplace versus setup routes. Explain that version alone determines installed state, updates require a changed version, and unknown latest is not an available update. End with the user's approval command, without running it.