@iseoane/agent-depot 0.2.1 → 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 +32 -0
- package/README.md +186 -1
- package/dist/src/app-cli.js +172 -0
- package/dist/src/app-flow.js +464 -0
- package/dist/src/app-latest.js +63 -0
- package/dist/src/app-owned-skills.js +42 -0
- package/dist/src/app-recipes.js +116 -0
- package/dist/src/app-schema.js +45 -0
- package/dist/src/app-version.js +46 -0
- package/dist/src/atomic-file.js +26 -0
- package/dist/src/cli.js +117 -20
- package/dist/src/process-runner.js +10 -1
- 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/skill-install.js +12 -0
- package/dist/src/source-state.js +3 -28
- package/dist/src/tui/app-actions.js +157 -0
- package/dist/src/tui/app.js +10 -4
- package/dist/src/tui/catalog-installs.js +5 -1
- package/dist/src/tui/installations-browse.js +24 -2
- package/dist/src/tui/installations-tree.js +13 -1
- package/dist/src/tui/installations-view.js +46 -6
- package/dist/src/tui/installations.js +26 -3
- package/dist/src/tui/manage-actions.js +2 -0
- package/dist/src/tui/manage-panel.js +6 -0
- 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/dist/src/tui/unmanaged-actions.js +2 -2
- package/dist/src/tui/updates-flow.js +18 -3
- package/dist/src/tui/updates-keys.js +9 -0
- package/dist/src/tui/updates-model.js +3 -3
- package/dist/src/tui/updates-panel.js +4 -2
- package/dist/src/tui/updates-view.js +1 -1
- package/dist/src/tui/updates.js +76 -5
- package/dist/src/unmanaged-removal.js +3 -0
- package/dist/src/user-global-skill-inventory.js +7 -1
- package/package.json +4 -4
- package/skills/agent-depot-apprecipe/SKILL.md +44 -0
- package/skills/agent-depot-apprecipe/assets/example.md +25 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { createAppOperations } from "../app-flow.js";
|
|
6
|
+
import { buildProfileExport, profileExportPreview, writeProfileExport, } from "../profile-export.js";
|
|
7
|
+
import { applyProfileImport, buildProfileImport, } from "../profile-import.js";
|
|
8
|
+
import { PROFILE_FORMAT } from "../profile.js";
|
|
9
|
+
import { useKeys } from "./keys.js";
|
|
10
|
+
import { answerYesNo, moveCursor, toggleChoice } from "./mode-keys.js";
|
|
11
|
+
import { ChecklistLines, PreviewConfirm, PreviewLines } from "./panel-parts.js";
|
|
12
|
+
import { computeWindow, useListHeight } from "./window.js";
|
|
13
|
+
function rowsFromExport(plan) {
|
|
14
|
+
return [
|
|
15
|
+
...plan.profile.sources.map((value, index) => ({
|
|
16
|
+
block: "sources", index, label: `Sources: ${value.url}`, selectable: true,
|
|
17
|
+
})),
|
|
18
|
+
...plan.profile.skills.map((value, index) => ({
|
|
19
|
+
block: "skills", index, label: `Skills: ${value.path}`, selectable: true,
|
|
20
|
+
})),
|
|
21
|
+
...plan.profile.apps.map((value, index) => ({
|
|
22
|
+
block: "apps", index, label: `Apps: ${value.name} (${value.platform ?? "all platforms"})`, selectable: true,
|
|
23
|
+
})),
|
|
24
|
+
...plan.exclusions.map((item, index) => ({
|
|
25
|
+
block: "excluded", index, label: `Excluded ${item.label}: ${item.reason}`, selectable: false,
|
|
26
|
+
})),
|
|
27
|
+
...plan.warnings.filter(warning => warning.kind === "load").map((warning, index) => ({
|
|
28
|
+
block: "excluded", index: plan.exclusions.length + index, label: warning.message, selectable: false,
|
|
29
|
+
})),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
function rowsFromPlan(plan) {
|
|
33
|
+
return plan.items.map((item, index) => ({
|
|
34
|
+
block: item.block, index,
|
|
35
|
+
label: `${item.status}: ${item.label}${item.difference ? `; ${item.difference}` : ""}`,
|
|
36
|
+
selectable: item.status === "add",
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function selectedPlanItems(plan, selected) {
|
|
40
|
+
return plan.items.filter((item, index) => item.status !== "add" || selected.some(row => row.index === index));
|
|
41
|
+
}
|
|
42
|
+
/** Collect choices, display shared-core previews and confirm portable Profile transfers. */
|
|
43
|
+
export function ProfileView({ operations, environment = {}, onCapturingChange, listHeight }) {
|
|
44
|
+
const apps = useMemo(() => createAppOperations({
|
|
45
|
+
homeDirectory: environment.homeDirectory, ...environment.appEnvironment,
|
|
46
|
+
}), [environment]);
|
|
47
|
+
const [mode, setModeState] = useState("browse");
|
|
48
|
+
const modeRef = useRef("browse");
|
|
49
|
+
const mounted = useRef(true);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
mounted.current = true;
|
|
52
|
+
return () => { mounted.current = false; };
|
|
53
|
+
}, []);
|
|
54
|
+
const setMode = (next) => {
|
|
55
|
+
modeRef.current = next;
|
|
56
|
+
onCapturingChange?.(next !== "browse" && next !== "results");
|
|
57
|
+
setModeState(next);
|
|
58
|
+
};
|
|
59
|
+
const [rows, setRows] = useState([]);
|
|
60
|
+
const [selected, setSelectedState] = useState([]);
|
|
61
|
+
const selectedRef = useRef([]);
|
|
62
|
+
const select = (next) => {
|
|
63
|
+
selectedRef.current = next;
|
|
64
|
+
setSelectedState(next);
|
|
65
|
+
};
|
|
66
|
+
const [cursor, setCursorState] = useState(0);
|
|
67
|
+
const cursorRef = useRef(0);
|
|
68
|
+
const cursorTo = (next) => {
|
|
69
|
+
cursorRef.current = next;
|
|
70
|
+
setCursorState(next);
|
|
71
|
+
};
|
|
72
|
+
const [text, setText] = useState("");
|
|
73
|
+
const typed = useRef("");
|
|
74
|
+
const [lines, setLines] = useState([]);
|
|
75
|
+
const [profile, setProfile] = useState();
|
|
76
|
+
const [plan, setPlan] = useState();
|
|
77
|
+
const diagnostics = useRef([]);
|
|
78
|
+
const action = useRef("export");
|
|
79
|
+
const profileVersion = useRef("");
|
|
80
|
+
const height = useListHeight(listHeight, 9);
|
|
81
|
+
const showLines = (next, nextMode) => {
|
|
82
|
+
setLines(next);
|
|
83
|
+
cursorTo(0);
|
|
84
|
+
setMode(nextMode);
|
|
85
|
+
};
|
|
86
|
+
const run = async (work) => {
|
|
87
|
+
setMode("busy");
|
|
88
|
+
try {
|
|
89
|
+
await work();
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (mounted.current) {
|
|
93
|
+
showLines([`Error: ${error instanceof Error ? error.message : String(error)}`], "results");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const prompt = (next) => {
|
|
98
|
+
typed.current = "";
|
|
99
|
+
setText("");
|
|
100
|
+
setMode(next);
|
|
101
|
+
};
|
|
102
|
+
const loadRows = (next, nextMode) => {
|
|
103
|
+
setRows(next);
|
|
104
|
+
select(next.filter(row => row.selectable));
|
|
105
|
+
cursorTo(0);
|
|
106
|
+
setMode(nextMode);
|
|
107
|
+
};
|
|
108
|
+
const chosenProfile = () => {
|
|
109
|
+
const chosen = selectedRef.current;
|
|
110
|
+
const isChosen = (block, index) => chosen.some(row => row.block === block && row.index === index);
|
|
111
|
+
return {
|
|
112
|
+
...profile,
|
|
113
|
+
sources: profile.sources.filter((_, index) => isChosen("sources", index)),
|
|
114
|
+
skills: profile.skills.filter((_, index) => isChosen("skills", index)),
|
|
115
|
+
apps: profile.apps.filter((_, index) => isChosen("apps", index)),
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
useKeys((input, key) => {
|
|
119
|
+
const current = modeRef.current;
|
|
120
|
+
if (current === "busy")
|
|
121
|
+
return;
|
|
122
|
+
if (current === "browse" || current === "results") {
|
|
123
|
+
if (input === "e") {
|
|
124
|
+
action.current = "export";
|
|
125
|
+
void run(async () => {
|
|
126
|
+
const exported = await buildProfileExport(operations, apps, { homeDirectory: environment.homeDirectory });
|
|
127
|
+
if (!mounted.current)
|
|
128
|
+
return;
|
|
129
|
+
setProfile(exported.profile);
|
|
130
|
+
diagnostics.current = [...exported.exclusions, ...exported.warnings];
|
|
131
|
+
loadRows(rowsFromExport(exported), "export-list");
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else if (input === "i") {
|
|
135
|
+
action.current = "import";
|
|
136
|
+
prompt("import-path");
|
|
137
|
+
}
|
|
138
|
+
else if (current === "results") {
|
|
139
|
+
const next = moveCursor(cursorRef.current, lines.length, input, key);
|
|
140
|
+
if (next !== undefined)
|
|
141
|
+
cursorTo(Math.max(0, next));
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (current === "export-path" || current === "import-path") {
|
|
146
|
+
if (key.escape) {
|
|
147
|
+
setMode("browse");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (key.return) {
|
|
151
|
+
if (!typed.current.trim())
|
|
152
|
+
return;
|
|
153
|
+
if (current === "export-path") {
|
|
154
|
+
showLines(profileExportPreview(chosenProfile(), diagnostics.current), "confirm");
|
|
155
|
+
}
|
|
156
|
+
else
|
|
157
|
+
void run(async () => {
|
|
158
|
+
const raw = JSON.parse(await readFile(typed.current, "utf8"));
|
|
159
|
+
const imported = await buildProfileImport(raw, operations, apps);
|
|
160
|
+
profileVersion.current = raw.agentDepotVersion;
|
|
161
|
+
if (!mounted.current)
|
|
162
|
+
return;
|
|
163
|
+
setPlan(imported);
|
|
164
|
+
loadRows(rowsFromPlan(imported), "import-list");
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else if (key.backspace || key.delete) {
|
|
168
|
+
typed.current = typed.current.slice(0, -1);
|
|
169
|
+
setText(typed.current);
|
|
170
|
+
}
|
|
171
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
172
|
+
typed.current += input;
|
|
173
|
+
setText(typed.current);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (current === "confirm") {
|
|
178
|
+
answerYesNo(input, key, () => {
|
|
179
|
+
void run(async () => {
|
|
180
|
+
let result;
|
|
181
|
+
if (action.current === "export") {
|
|
182
|
+
await writeProfileExport(typed.current, chosenProfile());
|
|
183
|
+
result = [`Exported profile to ${typed.current}`];
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const confirmed = { ...plan, items: selectedPlanItems(plan, selectedRef.current) };
|
|
187
|
+
const outcomes = await applyProfileImport(confirmed, operations, apps, true, () => undefined, { homeDirectory: environment.homeDirectory, ...environment.installationOptions });
|
|
188
|
+
result = outcomes.map(outcome => `${outcome.status}: ${outcome.item.label}${outcome.detail ? `; ${outcome.detail}` : ""}`);
|
|
189
|
+
}
|
|
190
|
+
if (mounted.current) {
|
|
191
|
+
showLines(result.length ? result : ["Nothing selected; nothing written."], "results");
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}, () => setMode("browse"));
|
|
195
|
+
if (input === "y" || input === "n" || key.escape)
|
|
196
|
+
return;
|
|
197
|
+
const next = moveCursor(cursorRef.current, lines.length, input, key);
|
|
198
|
+
if (next !== undefined)
|
|
199
|
+
cursorTo(Math.max(0, next));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (key.escape) {
|
|
203
|
+
setMode("browse");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Export diagnostics follow the checklist, but never own its cursor.
|
|
207
|
+
// Import same/conflict rows still draw a cursor so their differences can be reviewed.
|
|
208
|
+
const checklistLength = rows.filter(row => row.block !== "excluded").length;
|
|
209
|
+
const next = moveCursor(cursorRef.current, checklistLength, input, key);
|
|
210
|
+
if (next !== undefined)
|
|
211
|
+
cursorTo(Math.max(0, next));
|
|
212
|
+
else if (input === " " && rows[cursorRef.current]?.selectable) {
|
|
213
|
+
select(toggleChoice(rows, selectedRef.current, rows[cursorRef.current]));
|
|
214
|
+
}
|
|
215
|
+
else if (input === "a")
|
|
216
|
+
select(selectedRef.current.length ? [] : rows.filter(row => row.selectable));
|
|
217
|
+
else if (key.return) {
|
|
218
|
+
if (current === "export-list") {
|
|
219
|
+
if (!selectedRef.current.length) {
|
|
220
|
+
showLines(["Nothing selected; nothing written."], "results");
|
|
221
|
+
}
|
|
222
|
+
else
|
|
223
|
+
prompt("export-path");
|
|
224
|
+
}
|
|
225
|
+
else
|
|
226
|
+
void run(async () => {
|
|
227
|
+
// Reuse the CLI's core preview for the exact selected subset, including argv and Source notes.
|
|
228
|
+
const items = selectedPlanItems(plan, selectedRef.current);
|
|
229
|
+
const preview = await buildProfileImport({
|
|
230
|
+
format: PROFILE_FORMAT, agentDepotVersion: profileVersion.current,
|
|
231
|
+
sources: items.filter(item => item.block === "sources").map(item => item.value),
|
|
232
|
+
skills: items.filter(item => item.block === "skills").map(item => item.value),
|
|
233
|
+
apps: items.filter(item => item.block === "apps").map(item => item.value),
|
|
234
|
+
}, operations, apps);
|
|
235
|
+
if (mounted.current)
|
|
236
|
+
showLines(preview.preview, "confirm");
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
const listing = mode === "export-list" || mode === "import-list";
|
|
241
|
+
const window = computeWindow(listing ? rows.length : lines.length, cursor, height);
|
|
242
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Portable user-global Profile \u00B7 e export \u00B7 i import" }), mode === "busy" ? _jsx(Text, { children: "Loading Profile\u2026" }) : null, mode.endsWith("-path") ? _jsxs(Text, { children: ["Profile path: ", text, "_"] }) : null, listing ? ["sources", "skills", "apps", "excluded"].map(block => {
|
|
243
|
+
const choices = rows.slice(window.start, window.end).filter(row => row.block === block);
|
|
244
|
+
if (!choices.length)
|
|
245
|
+
return null;
|
|
246
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: block === "excluded" ? "Excluded" : block[0].toUpperCase() + block.slice(1) }), block === "excluded" ? _jsx(PreviewLines, { lines: choices.map(row => `${row.label} (not selectable)`) }) :
|
|
247
|
+
_jsx(ChecklistLines, { choices: choices, cursor: choices.indexOf(rows[cursor]), isSelected: row => selected.includes(row), keyOf: row => `${row.block}:${row.index}`, label: row => `${row.label}${row.selectable ? "" : " (not selectable)"}` })] }, block);
|
|
248
|
+
}) : null, mode === "confirm" ? _jsx(PreviewConfirm, { lines: lines.slice(window.start, window.end), question: "Apply Profile? (y/n) \u00B7 j/k scroll" }) : null, mode === "results" ? _jsxs(_Fragment, { children: [_jsx(PreviewLines, { lines: lines.slice(window.start, window.end) }), _jsx(Text, { children: "j/k scroll" })] }) : null, window.indicator && (listing || mode === "confirm" || mode === "results") ? _jsx(Text, { children: window.indicator }) : null, listing ? _jsx(Text, { children: "j/k move \u00B7 space toggle \u00B7 a all \u00B7 Enter preview \u00B7 Esc cancel" }) : null] });
|
|
249
|
+
}
|
|
@@ -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")
|
|
@@ -36,7 +36,7 @@ export async function prepareUnmanagedRemoval(operations, environment, group, lo
|
|
|
36
36
|
if (!operations.listUserGlobalInstallations)
|
|
37
37
|
throw new Error("Uninstall is not supported by the configured operations");
|
|
38
38
|
const installations = await operations.listUserGlobalInstallations();
|
|
39
|
-
const options = { homeDirectory: homeOf(environment), managedInstallations: installations };
|
|
39
|
+
const options = { homeDirectory: homeOf(environment), appEnvironment: environment.appEnvironment, managedInstallations: installations };
|
|
40
40
|
const items = [];
|
|
41
41
|
const preview = [`WARNING: agent-depot did not create these files (unmanaged skill ${group.name})`];
|
|
42
42
|
// A chosen directory takes the links that point at it along, so none is left dangling.
|
|
@@ -70,7 +70,7 @@ export async function prepareUnmanagedRemoval(operations, environment, group, lo
|
|
|
70
70
|
}
|
|
71
71
|
/** Execute step: each location is rechecked and removed on its own, so one failure does not stop the others. */
|
|
72
72
|
export async function runUnmanagedRemoval(prepared, operations, environment) {
|
|
73
|
-
const context = { operations, homeDirectory: homeOf(environment) };
|
|
73
|
+
const context = { operations, homeDirectory: homeOf(environment), appEnvironment: environment.appEnvironment };
|
|
74
74
|
const lines = [];
|
|
75
75
|
let failed = false;
|
|
76
76
|
const notRemoved = new Set();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { errorText } from "./batch.js";
|
|
3
3
|
import { BROWSE } from "./updates-mode.js";
|
|
4
4
|
import { CHECKING, emptyData, PHASE_LABEL, REFRESHING } from "./updates-model.js";
|
|
@@ -7,6 +7,10 @@ export function useUpdatesFlow(inputs) {
|
|
|
7
7
|
const { operations, env, clock, mounted, checkedRef, latest, setChecked, setCursor, setMode, setMessage } = inputs;
|
|
8
8
|
const [state, setState] = useState({ status: "loading", label: REFRESHING });
|
|
9
9
|
const loadToken = useRef(0);
|
|
10
|
+
const pendingAnswer = useRef(undefined);
|
|
11
|
+
useEffect(() => () => {
|
|
12
|
+
pendingAnswer.current?.(false);
|
|
13
|
+
}, []);
|
|
10
14
|
const load = useCallback(async (refresh) => {
|
|
11
15
|
const token = ++loadToken.current;
|
|
12
16
|
const phase = (next) => {
|
|
@@ -45,7 +49,7 @@ export function useUpdatesFlow(inputs) {
|
|
|
45
49
|
try {
|
|
46
50
|
const prepared = await prepareUpdates(current, selected);
|
|
47
51
|
if (mounted.current)
|
|
48
|
-
setMode({ kind: "preview",
|
|
52
|
+
setMode({ kind: "preview", prepared });
|
|
49
53
|
}
|
|
50
54
|
catch (error) {
|
|
51
55
|
if (!mounted.current)
|
|
@@ -58,7 +62,18 @@ export function useUpdatesFlow(inputs) {
|
|
|
58
62
|
setMode({ kind: "busy", label: "Applying updates..." });
|
|
59
63
|
let result;
|
|
60
64
|
try {
|
|
61
|
-
const outcome = await runUpdates(prepared, confirmedPaths)
|
|
65
|
+
const outcome = await runUpdates(prepared, confirmedPaths, (plan, reason) => new Promise(resolve => {
|
|
66
|
+
if (!mounted.current) {
|
|
67
|
+
resolve(false);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const answer = (done) => {
|
|
71
|
+
pendingAnswer.current = undefined;
|
|
72
|
+
resolve(done);
|
|
73
|
+
};
|
|
74
|
+
pendingAnswer.current = answer;
|
|
75
|
+
setMode({ kind: "manual", plan, reason, answer });
|
|
76
|
+
}));
|
|
62
77
|
result = { kind: outcome.failed === 0 ? "ok" : "error", lines: outcome.lines };
|
|
63
78
|
}
|
|
64
79
|
catch (error) {
|
|
@@ -80,6 +80,15 @@ function handlePreviewKey(context, mode, input, key) {
|
|
|
80
80
|
/** Handles a key in the current Updates mode; `busy` ignores keys. */
|
|
81
81
|
export function handleUpdatesKey(context, mode, input, key) {
|
|
82
82
|
switch (mode.kind) {
|
|
83
|
+
case "manual":
|
|
84
|
+
answerYesNo(input, key, () => {
|
|
85
|
+
context.setMode({ kind: "busy", label: "Checking App version..." });
|
|
86
|
+
mode.answer(true);
|
|
87
|
+
}, () => {
|
|
88
|
+
context.setMode({ kind: "busy", label: "Continuing updates..." });
|
|
89
|
+
mode.answer(false);
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
83
92
|
case "browse":
|
|
84
93
|
handleBrowseKey(context, input, key);
|
|
85
94
|
return;
|
|
@@ -15,9 +15,9 @@ const matchesScope = (row, filter) => filter === "all" || row.scope === filter;
|
|
|
15
15
|
/** The rows the list shows for a scope filter and fold state, and the cursor positions they make up. */
|
|
16
16
|
export function viewOf(data, scope, showUnknown) {
|
|
17
17
|
const shown = (data?.rows ?? []).filter((row) => matchesScope(row, scope));
|
|
18
|
-
const updatable = shown.filter((row) => row.
|
|
19
|
-
const unknown = shown.filter((row) => row.
|
|
20
|
-
const upToDate = shown.filter((row) => row.
|
|
18
|
+
const updatable = shown.filter((row) => row.status === "update available");
|
|
19
|
+
const unknown = shown.filter((row) => row.status === "cannot assess");
|
|
20
|
+
const upToDate = shown.filter((row) => row.status === "up to date").length;
|
|
21
21
|
const entries = [
|
|
22
22
|
...updatable.map((row) => ({ kind: "update", row })),
|
|
23
23
|
...(unknown.length > 0 ? [{ kind: "summary" }] : []),
|
|
@@ -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))), 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, 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 }) {
|
|
@@ -28,12 +28,14 @@ export function UpdatesFooter({ indicator, upToDate, updatableCount, checkedCoun
|
|
|
28
28
|
/** The prompt of the preview and confirmation modes; browsing and the check itself show nothing. */
|
|
29
29
|
export function ModePanel({ mode }) {
|
|
30
30
|
switch (mode.kind) {
|
|
31
|
+
case "manual":
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["App: ", mode.plan.entry.recipe.name] }), _jsxs(Text, { children: ["Environment: ", mode.plan.environment] }), _jsxs(Text, { children: ["Working directory: ", mode.plan.cwd] }), _jsxs(Text, { children: ["Manual: ", mode.plan.manual] }), mode.reason ? _jsx(Text, { color: theme.warning, children: mode.reason }) : null, _jsx(Text, { children: "Manual step done, check now? y/n" })] }));
|
|
31
33
|
case "browse":
|
|
32
34
|
return null;
|
|
33
35
|
case "busy":
|
|
34
36
|
return mode.label === REFRESHING || mode.label === CHECKING ? null : _jsx(Text, { children: mode.label });
|
|
35
37
|
case "preview":
|
|
36
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PreviewLines, { lines: mode.prepared.lines }), 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 }), 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
|
|
37
39
|
? _jsx(Text, { children: "Nothing can be applied. Enter/Esc to go back" })
|
|
38
40
|
: _jsxs(Text, { children: ["Apply ", mode.prepared.applicable.length, " ", mode.prepared.applicable.length === 1 ? "update" : "updates", "? y/n"] })] }));
|
|
39
41
|
case "confirm-paths":
|