@iseoane/agent-depot 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.es.md +969 -0
- package/README.md +244 -23
- package/dist/src/app-flow.js +52 -51
- package/dist/src/app-recipe-removal.js +63 -0
- package/dist/src/cli.js +122 -11
- package/dist/src/executable-resolution.js +54 -0
- package/dist/src/git-source.js +129 -6
- package/dist/src/package-bundles.js +614 -0
- package/dist/src/package-cli.js +412 -0
- package/dist/src/package-content.js +158 -0
- package/dist/src/package-flow.js +636 -0
- package/dist/src/package-host-plans.js +240 -0
- package/dist/src/package-host-state.js +349 -0
- package/dist/src/package-model.js +33 -0
- package/dist/src/package-owned-skills.js +122 -0
- package/dist/src/package-state.js +404 -0
- package/dist/src/profile-cli.js +15 -9
- package/dist/src/profile-export.js +44 -9
- package/dist/src/profile-import.js +80 -17
- package/dist/src/profile.js +78 -3
- package/dist/src/skill-discovery.js +2 -2
- package/dist/src/skill-install.js +16 -0
- package/dist/src/sources.js +11 -0
- package/dist/src/tui/app-actions.js +1 -1
- package/dist/src/tui/app.js +5 -6
- package/dist/src/tui/catalog-browse.js +10 -3
- package/dist/src/tui/catalog-panel.js +7 -3
- package/dist/src/tui/catalog-state.js +57 -6
- package/dist/src/tui/catalog-tree.js +52 -10
- package/dist/src/tui/catalog-view.js +27 -4
- package/dist/src/tui/environment.js +8 -1
- package/dist/src/tui/installations-browse.js +20 -2
- package/dist/src/tui/installations-rows.js +2 -1
- package/dist/src/tui/installations-tree.js +14 -1
- package/dist/src/tui/installations-view.js +8 -2
- package/dist/src/tui/installations.js +12 -2
- package/dist/src/tui/manage-actions.js +2 -0
- package/dist/src/tui/manage-panel.js +5 -0
- package/dist/src/tui/package-actions.js +165 -0
- package/dist/src/tui/package-rows.js +87 -0
- package/dist/src/tui/profile-view.js +36 -14
- package/dist/src/tui/sources-mode.js +2 -0
- package/dist/src/tui/sources-recipes.js +116 -53
- package/dist/src/tui/sources-view.js +57 -19
- package/dist/src/tui/tree.js +3 -4
- package/dist/src/tui/updates-flow.js +4 -1
- package/dist/src/tui/updates-panel.js +2 -2
- package/dist/src/tui/updates.js +72 -4
- package/dist/src/user-global-skill-inventory.js +7 -2
- package/package.json +2 -1
- package/skills/agent-depot-apprecipe/SKILL.md +29 -5
|
@@ -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
|
|
106
|
+
const rowsRef = useRef([]);
|
|
99
107
|
const recipes = useSourcesRecipes({
|
|
100
|
-
operations, environment, onCapturingChange,
|
|
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
|
-
|
|
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
|
|
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(
|
|
259
|
+
select(at + pageStep(height));
|
|
224
260
|
}
|
|
225
261
|
else if (key.pageUp) {
|
|
226
|
-
select(at - pageStep(
|
|
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
|
-
|
|
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(
|
|
268
|
-
return (_jsxs(Box, { flexDirection: "column", children: [builtin ? _jsxs(Text, { color: theme.muted, children: [" ", builtin.id, "
|
|
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
|
-
|
|
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")
|
package/dist/src/tui/tree.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export function defaultExpanded(
|
|
3
|
-
|
|
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":
|
package/dist/src/tui/updates.js
CHANGED
|
@@ -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
|
-
|
|
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 ??
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|