@iseoane/agent-depot 0.4.1 → 0.5.1
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 +45 -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 +150 -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 +645 -0
- package/dist/src/package-host-plans.js +243 -0
- package/dist/src/package-host-state.js +473 -0
- package/dist/src/package-model.js +33 -0
- package/dist/src/package-owned-skills.js +124 -0
- package/dist/src/package-state.js +404 -0
- package/dist/src/process-runner.js +9 -1
- 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 +7 -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 +10 -11
- package/dist/src/tui/catalog-browse.js +46 -13
- package/dist/src/tui/catalog-ownership.js +12 -0
- package/dist/src/tui/catalog-panel.js +16 -4
- package/dist/src/tui/catalog-state.js +62 -6
- package/dist/src/tui/catalog-tree.js +58 -12
- package/dist/src/tui/catalog-view.js +53 -7
- 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 +91 -0
- package/dist/src/tui/profile-view.js +36 -14
- package/dist/src/tui/render.js +1 -1
- 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/tui/window.js +3 -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
|
@@ -2,6 +2,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { createAppOperations } from "../app-flow.js";
|
|
5
|
+
import { planAppRecipeRemoval, removeAppRecipe } from "../app-recipe-removal.js";
|
|
5
6
|
import { BUILT_IN_SOURCE } from "../sources.js";
|
|
6
7
|
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
7
8
|
import { ActionPanel } from "./catalog-panel.js";
|
|
@@ -11,13 +12,11 @@ import { errorText } from "./batch.js";
|
|
|
11
12
|
import { useMounted } from "./view-state.js";
|
|
12
13
|
import { PreviewConfirm } from "./panel-parts.js";
|
|
13
14
|
import { answerYesNo } from "./mode-keys.js";
|
|
14
|
-
import {
|
|
15
|
-
import { computeWindow, pageStep } from "./window.js";
|
|
15
|
+
import { theme } from "./theme.js";
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* and the Git list's share of the common height budget. Sources owns key dispatch.
|
|
17
|
+
* Recipe loading and captured actions; Sources owns the single list and cursor.
|
|
19
18
|
*/
|
|
20
|
-
export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
19
|
+
export function useSourcesRecipes({ operations, environment, onCapturingChange, highlightedFile, }) {
|
|
21
20
|
const homeDirectory = environment?.homeDirectory;
|
|
22
21
|
const appEnvironment = environment?.appEnvironment;
|
|
23
22
|
const apps = useMemo(() => createAppOperations({
|
|
@@ -27,12 +26,8 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
27
26
|
const mounted = useMounted();
|
|
28
27
|
const [rows, setRows] = useState([]);
|
|
29
28
|
const [error, setError] = useState();
|
|
29
|
+
const [notice, setNotice] = useState();
|
|
30
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
31
|
const [mode, setModeState] = useState({ kind: "browse" });
|
|
37
32
|
const modeRef = useRef(mode);
|
|
38
33
|
const setMode = (next) => {
|
|
@@ -45,7 +40,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
45
40
|
if (mounted.current)
|
|
46
41
|
setMode({ kind: "browse" });
|
|
47
42
|
};
|
|
48
|
-
const preview = async (entry, status) => {
|
|
43
|
+
const preview = async (entry, status, removeAfterApproval = false) => {
|
|
49
44
|
setError(undefined);
|
|
50
45
|
setMode({ kind: "busy" });
|
|
51
46
|
try {
|
|
@@ -53,7 +48,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
53
48
|
if (mounted.current) {
|
|
54
49
|
setMode({
|
|
55
50
|
kind: "approve",
|
|
56
|
-
entry,
|
|
51
|
+
entry, removeAfterApproval,
|
|
57
52
|
lines: [
|
|
58
53
|
`File: ${entry.file}`,
|
|
59
54
|
`Platform: ${entry.recipe?.platform ?? "all"}`,
|
|
@@ -117,66 +112,142 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
117
112
|
}
|
|
118
113
|
}
|
|
119
114
|
};
|
|
120
|
-
|
|
115
|
+
const startRemoval = async (entry) => {
|
|
116
|
+
setError(undefined);
|
|
117
|
+
setMode({ kind: "busy" });
|
|
118
|
+
try {
|
|
119
|
+
const approval = await apps.approvalStatus(entry);
|
|
120
|
+
if (approval.status === "needs approval") {
|
|
121
|
+
await preview(entry, approval.status, true);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const removal = await planAppRecipeRemoval(apps, entry);
|
|
125
|
+
if (mounted.current)
|
|
126
|
+
setMode({ kind: removal.installed ? "uninstall-question" : "remove", removal });
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (mounted.current) {
|
|
130
|
+
setError(errorText(error));
|
|
131
|
+
setMode({ kind: "browse" });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const uninstall = async (current) => {
|
|
136
|
+
setMode({ kind: "busy" });
|
|
137
|
+
try {
|
|
138
|
+
const result = current.kind === "uninstall-manual"
|
|
139
|
+
? await apps.completeManual(current.plan, true)
|
|
140
|
+
: await apps.executeLifecycle(current.plan, true);
|
|
141
|
+
if (!mounted.current)
|
|
142
|
+
return;
|
|
143
|
+
if (result.status === "manual required")
|
|
144
|
+
setMode({ ...current, kind: "uninstall-manual" });
|
|
145
|
+
else if (result.status === "untracked")
|
|
146
|
+
setMode({ kind: "remove", removal: current.removal });
|
|
147
|
+
else {
|
|
148
|
+
setError(`Uninstall failed: ${result.reason ?? result.status}; recipe retained`);
|
|
149
|
+
setMode({ kind: "browse" });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (mounted.current) {
|
|
154
|
+
setError(`Uninstall failed: ${errorText(error)}; recipe retained`);
|
|
155
|
+
setMode({ kind: "browse" });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const startAdd = () => {
|
|
160
|
+
setError(undefined);
|
|
161
|
+
setMode({ kind: "guide" });
|
|
162
|
+
};
|
|
163
|
+
/** Captured actions take priority; browse actions use the unified cursor's recipe. */
|
|
121
164
|
const handleKey = (input, key) => {
|
|
122
165
|
const current = modeRef.current;
|
|
123
166
|
if (current.kind === "busy")
|
|
124
167
|
return true;
|
|
125
168
|
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
169
|
const decline = input === "n" && ["host", "scope", "version"].includes(current.action.kind);
|
|
130
170
|
handleCatalogActionKey(flow, current.action, input, decline ? { ...key, escape: true } : key);
|
|
131
171
|
return true;
|
|
132
172
|
}
|
|
133
173
|
if (current.kind === "guide" || current.kind === "manual" || current.kind === "prompt") {
|
|
134
|
-
if (key.escape || input === "n")
|
|
174
|
+
if (key.escape || input === "n") {
|
|
135
175
|
setMode({ kind: "browse" });
|
|
176
|
+
void reload();
|
|
177
|
+
}
|
|
136
178
|
else if (current.kind === "guide" && input === "1")
|
|
137
179
|
void startAI();
|
|
138
180
|
else if (current.kind === "guide" && input === "2")
|
|
139
181
|
setMode({ kind: "manual" });
|
|
140
182
|
return true;
|
|
141
183
|
}
|
|
184
|
+
const cancel = () => setMode({ kind: "browse" });
|
|
142
185
|
if (current.kind === "approve") {
|
|
143
186
|
answerYesNo(input, key, () => {
|
|
144
187
|
setMode({ kind: "busy" });
|
|
145
|
-
void apps.approve(current.entry).then(
|
|
188
|
+
void apps.approve(current.entry).then(async () => {
|
|
189
|
+
await reload();
|
|
190
|
+
if (!mounted.current)
|
|
191
|
+
return;
|
|
192
|
+
if (current.removeAfterApproval)
|
|
193
|
+
await startRemoval(current.entry);
|
|
194
|
+
else
|
|
195
|
+
setMode({ kind: "browse" });
|
|
196
|
+
}, error => {
|
|
146
197
|
if (mounted.current) {
|
|
147
198
|
setError(errorText(error));
|
|
148
|
-
|
|
199
|
+
cancel();
|
|
149
200
|
}
|
|
150
201
|
});
|
|
151
|
-
},
|
|
202
|
+
}, cancel);
|
|
152
203
|
return true;
|
|
153
204
|
}
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
205
|
+
if (current.kind === "uninstall-question") {
|
|
206
|
+
answerYesNo(input, key, () => {
|
|
207
|
+
setMode({ kind: "busy" });
|
|
208
|
+
void apps.planLifecycle(current.removal.entry, "uninstall").then(plan => {
|
|
209
|
+
if (mounted.current)
|
|
210
|
+
setMode({ kind: !plan.argv && plan.manual ? "uninstall-manual" : "uninstall-confirm", removal: current.removal, plan });
|
|
211
|
+
}, error => { if (mounted.current) {
|
|
212
|
+
setError(errorText(error));
|
|
213
|
+
cancel();
|
|
214
|
+
} });
|
|
215
|
+
}, cancel);
|
|
158
216
|
return true;
|
|
159
217
|
}
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
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);
|
|
218
|
+
if (current.kind === "uninstall-confirm" || current.kind === "uninstall-manual") {
|
|
219
|
+
answerYesNo(input, key, () => { void uninstall(current); }, cancel);
|
|
220
|
+
return true;
|
|
167
221
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
222
|
+
if (current.kind === "remove") {
|
|
223
|
+
answerYesNo(input, key, () => {
|
|
224
|
+
setMode({ kind: "busy" });
|
|
225
|
+
void removeAppRecipe(apps, current.removal, true).then(async () => {
|
|
226
|
+
if (mounted.current)
|
|
227
|
+
setNotice(`Removed App recipe: ${current.removal.file}`);
|
|
228
|
+
await finish();
|
|
229
|
+
}, error => {
|
|
230
|
+
if (mounted.current) {
|
|
231
|
+
setError(errorText(error));
|
|
232
|
+
cancel();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}, cancel);
|
|
236
|
+
return true;
|
|
171
237
|
}
|
|
172
|
-
|
|
238
|
+
const row = rows.find(row => row.entry.file === highlightedFile());
|
|
239
|
+
if (!row)
|
|
240
|
+
return false;
|
|
241
|
+
if (input === "n")
|
|
242
|
+
startAdd();
|
|
243
|
+
else if (input === "r")
|
|
173
244
|
void reload();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
245
|
+
else if (key.return)
|
|
246
|
+
void preview(row.entry, row.status);
|
|
247
|
+
else if (input === "d")
|
|
248
|
+
void startRemoval(row.entry);
|
|
249
|
+
else
|
|
250
|
+
return false;
|
|
180
251
|
return true;
|
|
181
252
|
};
|
|
182
253
|
/** Reloads parsing and approval receipts, never version/latest commands. */
|
|
@@ -201,16 +272,8 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
201
272
|
}
|
|
202
273
|
}, [apps, mounted]);
|
|
203
274
|
useEffect(() => { void reload(); }, [reload]);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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 };
|
|
275
|
+
const panel = (_jsxs(Box, { flexDirection: "column", children: [loadError ? _jsxs(Text, { color: theme.error, children: ["Error loading App recipes: ", loadError] }) : null, error ? _jsx(Text, { color: theme.error, children: error }) : null, notice ? _jsx(Text, { color: theme.success, children: notice }) : 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, mode.kind === "uninstall-question" ? (_jsxs(Text, { children: ["App ", appName(mode.removal.entry), " is installed or tracked", mode.removal.installedVersion ? ` (${mode.removal.installedVersion})` : "", ". Uninstall it before removing the recipe? y/n (n cancels removal)"] })) : null, mode.kind === "uninstall-confirm" || mode.kind === "uninstall-manual" ? (_jsx(PreviewConfirm, { lines: appStepLines(mode.plan), question: mode.kind === "uninstall-manual"
|
|
276
|
+
? "Complete the manual uninstall, then check now? y/n"
|
|
277
|
+
: `Uninstall ${appName(mode.removal.entry)}? y/n` })) : null, mode.kind === "remove" ? (_jsx(PreviewConfirm, { lines: [`Delete recipe: ${mode.removal.file}`, "Only this recipe file or symlink will be removed; App data and Host configuration are not deleted."], question: `Remove recipe for ${appName(mode.removal.entry)}? y/n` })) : null] }));
|
|
278
|
+
return { rows, handleKey, startAdd, reload, clearMessage: () => { setError(undefined); setNotice(undefined); }, panel, directory: apps.directory };
|
|
216
279
|
}
|
|
@@ -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 {
|
package/dist/src/tui/window.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useWindowSize } from "ink";
|
|
2
2
|
/** Rows assumed when the terminal size is unknown (for example under test renderers). */
|
|
3
3
|
export const FALLBACK_TERMINAL_ROWS = 24;
|
|
4
4
|
/** Lines the shell itself uses: title, tab bar, separator and footer hint. */
|
|
@@ -23,9 +23,8 @@ export function pageStep(height) {
|
|
|
23
23
|
* view's own `reserved` lines. `override` fixes the height for tests.
|
|
24
24
|
*/
|
|
25
25
|
export function useListHeight(override, reserved) {
|
|
26
|
-
const {
|
|
26
|
+
const { rows } = useWindowSize();
|
|
27
27
|
if (override !== undefined)
|
|
28
28
|
return Math.max(override, 1);
|
|
29
|
-
|
|
30
|
-
return Math.max(rows - APP_CHROME_ROWS - reserved, MINIMUM_ROWS);
|
|
29
|
+
return Math.max((rows || FALLBACK_TERMINAL_ROWS) - APP_CHROME_ROWS - reserved, MINIMUM_ROWS);
|
|
31
30
|
}
|