@iseoane/agent-depot 0.3.0 → 0.4.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 +10 -0
- package/README.md +77 -1
- package/dist/src/app-flow.js +12 -5
- package/dist/src/cli.js +5 -0
- package/dist/src/profile-cli.js +88 -0
- package/dist/src/profile-export.js +135 -0
- package/dist/src/profile-import.js +218 -0
- package/dist/src/profile.js +130 -0
- package/dist/src/project-manifest.js +6 -0
- package/dist/src/tui/app-actions.js +3 -3
- package/dist/src/tui/app.js +10 -4
- package/dist/src/tui/profile-view.js +249 -0
- package/dist/src/tui/sources-recipes.js +216 -0
- package/dist/src/tui/sources-view.js +16 -7
- package/package.json +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createAppOperations } from "../app-flow.js";
|
|
5
|
+
import { BUILT_IN_SOURCE } from "../sources.js";
|
|
6
|
+
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
7
|
+
import { ActionPanel } from "./catalog-panel.js";
|
|
8
|
+
import { hasUnmanagedCopy, isInstalled, loadInstalledSkills } from "./catalog-installs.js";
|
|
9
|
+
import { appName, appStepLines } from "./app-actions.js";
|
|
10
|
+
import { errorText } from "./batch.js";
|
|
11
|
+
import { useMounted } from "./view-state.js";
|
|
12
|
+
import { PreviewConfirm } from "./panel-parts.js";
|
|
13
|
+
import { answerYesNo } from "./mode-keys.js";
|
|
14
|
+
import { rowStyle, theme } from "./theme.js";
|
|
15
|
+
import { computeWindow, pageStep } from "./window.js";
|
|
16
|
+
/**
|
|
17
|
+
* Returns section focus, key dispatch (true when consumed), the recipe panel,
|
|
18
|
+
* and the Git list's share of the common height budget. Sources owns key dispatch.
|
|
19
|
+
*/
|
|
20
|
+
export function useSourcesRecipes({ operations, environment, onCapturingChange, height, gitCount, }) {
|
|
21
|
+
const homeDirectory = environment?.homeDirectory;
|
|
22
|
+
const appEnvironment = environment?.appEnvironment;
|
|
23
|
+
const apps = useMemo(() => createAppOperations({
|
|
24
|
+
homeDirectory,
|
|
25
|
+
...appEnvironment,
|
|
26
|
+
}), [homeDirectory, appEnvironment]);
|
|
27
|
+
const mounted = useMounted();
|
|
28
|
+
const [rows, setRows] = useState([]);
|
|
29
|
+
const [error, setError] = useState();
|
|
30
|
+
const [loadError, setLoadError] = useState();
|
|
31
|
+
const [focused, setFocused] = useState(false);
|
|
32
|
+
// Ref mirrors keep Tab and subsequent keys in a single burst on the same section.
|
|
33
|
+
const focusRef = useRef(false);
|
|
34
|
+
const [index, setIndex] = useState(0);
|
|
35
|
+
const indexRef = useRef(0);
|
|
36
|
+
const [mode, setModeState] = useState({ kind: "browse" });
|
|
37
|
+
const modeRef = useRef(mode);
|
|
38
|
+
const setMode = (next) => {
|
|
39
|
+
modeRef.current = next;
|
|
40
|
+
onCapturingChange?.(next.kind !== "browse");
|
|
41
|
+
setModeState(next);
|
|
42
|
+
};
|
|
43
|
+
const finish = async () => {
|
|
44
|
+
await reload();
|
|
45
|
+
if (mounted.current)
|
|
46
|
+
setMode({ kind: "browse" });
|
|
47
|
+
};
|
|
48
|
+
const preview = async (entry, status) => {
|
|
49
|
+
setError(undefined);
|
|
50
|
+
setMode({ kind: "busy" });
|
|
51
|
+
try {
|
|
52
|
+
const steps = await apps.preview(entry);
|
|
53
|
+
if (mounted.current) {
|
|
54
|
+
setMode({
|
|
55
|
+
kind: "approve",
|
|
56
|
+
entry,
|
|
57
|
+
lines: [
|
|
58
|
+
`File: ${entry.file}`,
|
|
59
|
+
`Platform: ${entry.recipe?.platform ?? "all"}`,
|
|
60
|
+
`Status: ${status}`,
|
|
61
|
+
...steps.flatMap(step => [step.step, ...appStepLines(step)]),
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (mounted.current) {
|
|
68
|
+
setError(errorText(error));
|
|
69
|
+
setMode({ kind: "browse" });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const flow = {
|
|
74
|
+
operations,
|
|
75
|
+
env: environment ?? {},
|
|
76
|
+
isMounted: () => mounted.current,
|
|
77
|
+
setAction: action => setMode(action.kind === "browse"
|
|
78
|
+
? { kind: "browse" }
|
|
79
|
+
: { kind: "install", action }),
|
|
80
|
+
setMessage: message => {
|
|
81
|
+
if (mounted.current)
|
|
82
|
+
setError(message?.text);
|
|
83
|
+
},
|
|
84
|
+
keepMarked: () => undefined,
|
|
85
|
+
finish: async (result) => {
|
|
86
|
+
if (mounted.current) {
|
|
87
|
+
setError(result.kind === "error" ? result.text : undefined);
|
|
88
|
+
setMode({ kind: result.kind === "ok" ? "prompt" : "browse" });
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const startAI = async () => {
|
|
93
|
+
setMode({ kind: "busy" });
|
|
94
|
+
try {
|
|
95
|
+
if (!operations.discoverSkills)
|
|
96
|
+
throw new Error("Discovery is not supported by the configured operations");
|
|
97
|
+
const [skills, installed] = await Promise.all([
|
|
98
|
+
operations.discoverSkills([BUILT_IN_SOURCE.id]),
|
|
99
|
+
loadInstalledSkills(operations, environment ?? {}),
|
|
100
|
+
]);
|
|
101
|
+
const skill = skills.find(skill => skill.name === "agent-depot-apprecipe");
|
|
102
|
+
if (!skill)
|
|
103
|
+
throw new Error("Built-in agent-depot-apprecipe Skill is unavailable");
|
|
104
|
+
if (!mounted.current)
|
|
105
|
+
return;
|
|
106
|
+
if (isInstalled(skill, installed) || hasUnmanagedCopy(skill, installed)) {
|
|
107
|
+
setMode({ kind: "prompt" });
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
flow.setAction({ kind: "host", skills: [skill], cursor: 0, selected: [] });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (mounted.current) {
|
|
115
|
+
setError(errorText(error));
|
|
116
|
+
setMode({ kind: "browse" });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
/** Handles captured modes and browse keys only in the focused section. */
|
|
121
|
+
const handleKey = (input, key) => {
|
|
122
|
+
const current = modeRef.current;
|
|
123
|
+
if (current.kind === "busy")
|
|
124
|
+
return true;
|
|
125
|
+
if (current.kind === "install") {
|
|
126
|
+
// Host/scope/version are selection prompts; version chooses latest or fixed
|
|
127
|
+
// by number. Only the separate fixed mode accepts free-text version input.
|
|
128
|
+
// n declines these selections; fixed input and in-flight installs are untouched.
|
|
129
|
+
const decline = input === "n" && ["host", "scope", "version"].includes(current.action.kind);
|
|
130
|
+
handleCatalogActionKey(flow, current.action, input, decline ? { ...key, escape: true } : key);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (current.kind === "guide" || current.kind === "manual" || current.kind === "prompt") {
|
|
134
|
+
if (key.escape || input === "n")
|
|
135
|
+
setMode({ kind: "browse" });
|
|
136
|
+
else if (current.kind === "guide" && input === "1")
|
|
137
|
+
void startAI();
|
|
138
|
+
else if (current.kind === "guide" && input === "2")
|
|
139
|
+
setMode({ kind: "manual" });
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (current.kind === "approve") {
|
|
143
|
+
answerYesNo(input, key, () => {
|
|
144
|
+
setMode({ kind: "busy" });
|
|
145
|
+
void apps.approve(current.entry).then(finish, error => {
|
|
146
|
+
if (mounted.current) {
|
|
147
|
+
setError(errorText(error));
|
|
148
|
+
setMode({ kind: "browse" });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}, () => setMode({ kind: "browse" }));
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (key.tab) {
|
|
155
|
+
setError(undefined);
|
|
156
|
+
focusRef.current = !focusRef.current;
|
|
157
|
+
setFocused(focusRef.current);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
if (!focusRef.current)
|
|
161
|
+
return false;
|
|
162
|
+
if (key.downArrow || input === "j" || key.upArrow || input === "k" || key.pageDown || key.pageUp) {
|
|
163
|
+
const step = key.pageDown || key.pageUp ? pageStep(recipeHeight) : 1;
|
|
164
|
+
const down = key.downArrow || input === "j" || key.pageDown;
|
|
165
|
+
indexRef.current = Math.min(Math.max(indexRef.current + (down ? step : -step), 0), Math.max(rows.length - 1, 0));
|
|
166
|
+
setIndex(indexRef.current);
|
|
167
|
+
}
|
|
168
|
+
else if (input === "n") {
|
|
169
|
+
setError(undefined);
|
|
170
|
+
setMode({ kind: "guide" });
|
|
171
|
+
}
|
|
172
|
+
else if (input === "r") {
|
|
173
|
+
void reload();
|
|
174
|
+
}
|
|
175
|
+
else if (key.return) {
|
|
176
|
+
const row = rows[Math.min(indexRef.current, rows.length - 1)];
|
|
177
|
+
if (row)
|
|
178
|
+
void preview(row.entry, row.status);
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
};
|
|
182
|
+
/** Reloads parsing and approval receipts, never version/latest commands. */
|
|
183
|
+
const reload = useCallback(async () => {
|
|
184
|
+
try {
|
|
185
|
+
const entries = await apps.load();
|
|
186
|
+
const rows = await Promise.all(entries.map(async (entry) => {
|
|
187
|
+
const inspection = await apps.approvalStatus(entry);
|
|
188
|
+
const status = inspection.status === "invalid"
|
|
189
|
+
? `invalid recipe: ${inspection.reason}`
|
|
190
|
+
: inspection.status;
|
|
191
|
+
return { entry, status };
|
|
192
|
+
}));
|
|
193
|
+
if (mounted.current) {
|
|
194
|
+
setRows(rows);
|
|
195
|
+
setLoadError(undefined);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (mounted.current)
|
|
200
|
+
setLoadError(errorText(error));
|
|
201
|
+
}
|
|
202
|
+
}, [apps, mounted]);
|
|
203
|
+
useEffect(() => { void reload(); }, [reload]);
|
|
204
|
+
// Give each nonempty section half the rows, then reclaim unused Git capacity.
|
|
205
|
+
const budget = Math.max(height, gitCount > 0 && rows.length > 0 ? 2 : 1);
|
|
206
|
+
const gitShare = Math.min(gitCount, Math.max(1, Math.floor(budget / 2)));
|
|
207
|
+
const recipeHeight = Math.min(rows.length, budget - gitShare);
|
|
208
|
+
const gitHeight = Math.max(1, budget - recipeHeight);
|
|
209
|
+
const at = Math.min(index, rows.length - 1);
|
|
210
|
+
const window = computeWindow(rows.length, at, recipeHeight);
|
|
211
|
+
const panel = (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { ...rowStyle(focused && rows.length === 0), children: ["App recipes \u00B7 ", apps.directory, " \u00B7 Tab switches section"] }), focused ? _jsx(Text, { color: theme.muted, children: "j/k move \u00B7 Enter preview \u00B7 n add recipe \u00B7 r reload" }) : null, loadError ? _jsxs(Text, { color: theme.error, children: ["Error loading App recipes: ", loadError] }) : null, error ? _jsx(Text, { color: theme.error, children: error }) : null, rows.slice(window.start, window.end).map(({ entry, status }, offset) => {
|
|
212
|
+
const selected = focused && window.start + offset === at;
|
|
213
|
+
return (_jsxs(Text, { ...rowStyle(selected), children: [selected ? "> " : " ", entry.file.split(/[\\/]/u).pop(), " \u00B7 ", status] }, entry.file));
|
|
214
|
+
}), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, mode.kind === "guide" ? (_jsxs(Text, { children: ["1 With the AI skill · install if missing, then copy the prompt\n", "2 Manual · write a recipe using the schema\nn or Esc closes"] })) : null, mode.kind === "manual" ? (_jsxs(Text, { children: ["Write a JSON recipe in ", apps.directory, "\n", "Schema: agent-depot app schema \u00B7 n or Esc closes"] })) : null, mode.kind === "prompt" ? (_jsxs(Text, { children: ["Use the agent-depot-apprecipe skill to create the recipe for <app>", "\n", "Recipes: ", apps.directory, " \u00B7 n or Esc closes"] })) : null, mode.kind === "install" ? _jsx(ActionPanel, { mode: mode.action }) : null, mode.kind === "busy" ? _jsx(Text, { children: "Working..." }) : null, mode.kind === "approve" ? (_jsx(PreviewConfirm, { lines: mode.lines, question: `Approve ${appName(mode.entry)}? y/n` })) : null] }));
|
|
215
|
+
return { focused, handleKey, gitHeight, panel };
|
|
216
|
+
}
|
|
@@ -5,6 +5,7 @@ import { initialMode, modeReducer } from "./sources-mode.js";
|
|
|
5
5
|
import { rowStyle, theme } from "./theme.js";
|
|
6
6
|
import { computeWindow, pageStep, useListHeight } from "./window.js";
|
|
7
7
|
import { useKeys } from "./keys.js";
|
|
8
|
+
import { useSourcesRecipes } from "./sources-recipes.js";
|
|
8
9
|
/** The highlighted index: by stable id while that source exists, otherwise the bounded fallback index. */
|
|
9
10
|
function resolveIndex(sources, selection) {
|
|
10
11
|
const found = sources.findIndex((source) => source.id === selection.id);
|
|
@@ -28,8 +29,8 @@ async function refreshEach(operations, targets) {
|
|
|
28
29
|
function errorText(error) {
|
|
29
30
|
return error instanceof Error ? error.message : String(error);
|
|
30
31
|
}
|
|
31
|
-
export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight }) {
|
|
32
|
-
const height = useListHeight(listHeight,
|
|
32
|
+
export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight, environment }) {
|
|
33
|
+
const height = useListHeight(listHeight, 9);
|
|
33
34
|
const [state, setState] = useState({ status: "loading" });
|
|
34
35
|
// The highlight follows the source id; the index is only the fallback once that source is gone.
|
|
35
36
|
// Selection and marks are mirrored in refs updated first, so keys delivered in one burst act on the latest state.
|
|
@@ -95,6 +96,9 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
95
96
|
const sourcesRef = useRef(sources);
|
|
96
97
|
sourcesRef.current = sources;
|
|
97
98
|
const selected = resolveIndex(sources, selection);
|
|
99
|
+
const recipes = useSourcesRecipes({
|
|
100
|
+
operations, environment, onCapturingChange, height, gitCount: sources.length,
|
|
101
|
+
});
|
|
98
102
|
const select = (index) => {
|
|
99
103
|
const listed = sourcesRef.current;
|
|
100
104
|
const bounded = Math.min(Math.max(index, 0), Math.max(listed.length - 1, 0));
|
|
@@ -201,6 +205,11 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
201
205
|
dispatch({ type: "done" });
|
|
202
206
|
return;
|
|
203
207
|
}
|
|
208
|
+
if (recipes.handleKey(input, key)) {
|
|
209
|
+
if (key.tab)
|
|
210
|
+
setMessage(undefined);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
204
213
|
const listed = sourcesRef.current;
|
|
205
214
|
const at = resolveIndex(listed, selectionRef.current);
|
|
206
215
|
const highlighted = listed[at];
|
|
@@ -211,10 +220,10 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
211
220
|
select(at - 1);
|
|
212
221
|
}
|
|
213
222
|
else if (key.pageDown) {
|
|
214
|
-
select(at + pageStep(
|
|
223
|
+
select(at + pageStep(recipes.gitHeight));
|
|
215
224
|
}
|
|
216
225
|
else if (key.pageUp) {
|
|
217
|
-
select(at - pageStep(
|
|
226
|
+
select(at - pageStep(recipes.gitHeight));
|
|
218
227
|
}
|
|
219
228
|
else if (key.return && highlighted) {
|
|
220
229
|
onOpenCatalog?.(highlighted);
|
|
@@ -255,11 +264,11 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
255
264
|
return _jsx(Text, { children: "Loading sources..." });
|
|
256
265
|
if (state.status === "error")
|
|
257
266
|
return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
|
|
258
|
-
const window = computeWindow(sources.length, selected,
|
|
267
|
+
const window = computeWindow(sources.length, selected, recipes.gitHeight);
|
|
259
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) => {
|
|
260
269
|
const index = window.start + offset;
|
|
261
|
-
return (_jsxs(Text, { ...rowStyle(index === selected), children: [index === selected ? "> " : " ", marked.has(source.id) ? "[x] " : "[ ] ", source.id, " ", source.kind, " ", source.url] }, source.id));
|
|
262
|
-
}), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : null, _jsx(Prompt, { mode: mode }), message ? _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) : null] }));
|
|
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));
|
|
271
|
+
}), 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] }));
|
|
263
272
|
}
|
|
264
273
|
function Prompt({ mode }) {
|
|
265
274
|
if (mode.kind === "input")
|