@iseoane/agent-depot 0.5.2 → 0.6.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 +19 -0
- package/README.es.md +51 -29
- package/README.md +44 -25
- package/dist/src/app-flow.js +56 -24
- package/dist/src/profile-export.js +72 -9
- package/dist/src/profile-import.js +3 -2
- package/dist/src/profile.js +71 -7
- package/dist/src/tui/app-actions.js +82 -33
- package/dist/src/tui/app.js +19 -14
- package/dist/src/tui/catalog-browse.js +6 -1
- package/dist/src/tui/catalog-panel.js +11 -11
- package/dist/src/tui/catalog-state.js +28 -0
- package/dist/src/tui/catalog-tree.js +22 -5
- package/dist/src/tui/catalog-view.js +59 -16
- package/dist/src/tui/header-layout.js +33 -0
- package/dist/src/tui/installations-panel.js +3 -3
- package/dist/src/tui/installations-rows.js +20 -5
- package/dist/src/tui/installations-tree.js +4 -7
- package/dist/src/tui/installations-view.js +15 -6
- package/dist/src/tui/installations.js +9 -7
- package/dist/src/tui/manage-panel.js +7 -6
- package/dist/src/tui/mode-keys.js +9 -0
- package/dist/src/tui/package-rows.js +17 -5
- package/dist/src/tui/panel-parts.js +38 -6
- package/dist/src/tui/preview-window.js +48 -0
- package/dist/src/tui/profile-view.js +261 -42
- package/dist/src/tui/sources-mode.js +3 -1
- package/dist/src/tui/sources-recipes.js +50 -19
- package/dist/src/tui/sources-view.js +50 -14
- package/dist/src/tui/theme.js +14 -7
- package/dist/src/tui/updates-flow.js +2 -2
- package/dist/src/tui/updates-keys.js +34 -5
- package/dist/src/tui/updates-model.js +2 -0
- package/dist/src/tui/updates-panel.js +40 -15
- package/dist/src/tui/updates-preview.js +65 -0
- package/dist/src/tui/updates-view.js +17 -2
- package/dist/src/tui/updates.js +14 -5
- package/dist/src/tui/window.js +6 -4
- package/package.json +4 -3
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
3
3
|
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
4
4
|
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
8
|
import { useSourcesRecipes } from "./sources-recipes.js";
|
|
9
|
+
import { BorderPanel, DecisionPrompt, FieldLine, PreviewLines } from "./panel-parts.js";
|
|
10
|
+
import { previewViewport } from "./preview-window.js";
|
|
11
|
+
import { movePreviewCursor } from "./mode-keys.js";
|
|
9
12
|
/** Footer keys for the selected row; App seeds the footer before the view reports a selection. */
|
|
10
13
|
export function sourceRowHints(row) {
|
|
11
14
|
if (row === undefined)
|
|
@@ -37,7 +40,18 @@ async function refreshEach(operations, targets) {
|
|
|
37
40
|
function errorText(error) {
|
|
38
41
|
return error instanceof Error ? error.message : String(error);
|
|
39
42
|
}
|
|
43
|
+
function refreshPreviewLines(sources) {
|
|
44
|
+
return sources.map(source => `Source: refresh Git Source ${source.id} from ${source.url}`);
|
|
45
|
+
}
|
|
46
|
+
function refreshViewport(mode, rows, columns) {
|
|
47
|
+
return previewViewport(refreshPreviewLines(mode.sources), mode.cursor, rows, columns, {
|
|
48
|
+
title: "Refresh Git Sources",
|
|
49
|
+
questions: ["Scroll to end; n cancels.", "Refresh these Sources? y/n"],
|
|
50
|
+
footerRows: 2,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
40
53
|
export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight, environment, onHintsChange }) {
|
|
54
|
+
const { columns, rows: terminalRows } = useWindowSize();
|
|
41
55
|
const height = useListHeight(listHeight, 9);
|
|
42
56
|
const [state, setState] = useState({ status: "loading" });
|
|
43
57
|
// The highlight follows the source id; the index is only the fallback once that source is gone.
|
|
@@ -111,6 +125,8 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
111
125
|
const row = rows[resolveIndex(rows, selectionRef.current)];
|
|
112
126
|
return row?.kind === "app" ? row.recipe.entry.file : undefined;
|
|
113
127
|
},
|
|
128
|
+
terminalRows: terminalRows || 24,
|
|
129
|
+
terminalColumns: columns || 80,
|
|
114
130
|
});
|
|
115
131
|
const rows = [
|
|
116
132
|
...sources.map((source) => ({ id: source.id, kind: "skills", source })),
|
|
@@ -119,7 +135,9 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
119
135
|
rowsRef.current = rows;
|
|
120
136
|
const selected = resolveIndex(rows, selection);
|
|
121
137
|
const selectedRow = rows[selected];
|
|
122
|
-
const hints =
|
|
138
|
+
const hints = mode.kind === "confirm" && mode.action === "refresh"
|
|
139
|
+
? "↑↓/Pg page · y refresh · n cancel"
|
|
140
|
+
: sourceRowHints(selectedRow);
|
|
123
141
|
useEffect(() => { onHintsChange?.(hints); }, [hints, onHintsChange]);
|
|
124
142
|
const select = (index) => {
|
|
125
143
|
setMessage(undefined);
|
|
@@ -221,6 +239,16 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
221
239
|
return;
|
|
222
240
|
}
|
|
223
241
|
if (mode.kind === "confirm") {
|
|
242
|
+
if (mode.action === "refresh") {
|
|
243
|
+
const viewport = refreshViewport(mode, terminalRows || 24, columns || 80);
|
|
244
|
+
const cursor = movePreviewCursor(mode.cursor, viewport.total, input, key, viewport.height);
|
|
245
|
+
if (cursor !== undefined) {
|
|
246
|
+
dispatch({ type: "preview-cursor", cursor });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (input === "y" && !viewport.reachedEnd)
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
224
252
|
if (input === "y") {
|
|
225
253
|
if (mode.action === "refresh") {
|
|
226
254
|
const { sources: targets } = mode;
|
|
@@ -296,28 +324,36 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
296
324
|
// Sources removed since they were marked no longer count.
|
|
297
325
|
const selectedCount = sources.filter((source) => marked.has(source.id)).length;
|
|
298
326
|
if (state.status === "loading")
|
|
299
|
-
return _jsx(
|
|
327
|
+
return _jsx(BorderPanel, { title: "Sources", children: _jsx(FieldLine, { label: "Status", value: "Loading sources..." }) });
|
|
300
328
|
if (state.status === "error")
|
|
301
|
-
return
|
|
329
|
+
return _jsx(BorderPanel, { title: "Source error", children: _jsx(Text, { color: theme.error, children: state.message }) });
|
|
302
330
|
const window = computeWindow(rows.length, selected, height);
|
|
303
|
-
|
|
331
|
+
if (mode.kind === "confirm")
|
|
332
|
+
return _jsx(Prompt, { mode: mode, terminalRows: terminalRows || 24, terminalColumns: columns || 80 });
|
|
333
|
+
if (recipes.capturing)
|
|
334
|
+
return _jsx(Box, { flexDirection: "column", children: recipes.panel });
|
|
335
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.muted, children: [" ", "ID KIND LOCATION / STATUS"] }), builtin ? _jsxs(Text, { children: [" ", _jsx(Text, { color: theme.value, children: builtin.id }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: "skills" }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.warning, children: "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) => {
|
|
304
336
|
const index = window.start + offset;
|
|
305
337
|
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,
|
|
307
|
-
|
|
338
|
+
return (_jsxs(Text, { ...rowStyle(index === selected), children: [index === selected ? "> " : " ", row.kind === "skills" ? (marked.has(row.id) ? "[x] " : "[ ] ") : " ", _jsx(Text, { color: theme.value, children: id }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.kind }), _jsx(Text, { color: theme.legend, children: " " }), row.kind === "skills"
|
|
339
|
+
? _jsx(Text, { color: theme.value, children: row.source.url })
|
|
340
|
+
: _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: row.recipe.entry.file }), _jsx(Text, { color: theme.legend, children: " \u00B7 " }), _jsx(Text, { color: row.recipe.status === "approved" ? theme.success : theme.warning, children: row.recipe.status })] })] }, row.id));
|
|
341
|
+
}), 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(BorderPanel, { title: message.kind === "error" ? "Source action · failed" : "Source action · complete", children: _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) })) : null] }));
|
|
308
342
|
}
|
|
309
|
-
function Prompt({ mode }) {
|
|
343
|
+
function Prompt({ mode, terminalRows = 24, terminalColumns = 80 }) {
|
|
310
344
|
if (mode.kind === "add-kind")
|
|
311
345
|
return _jsx(Text, { children: "Add: 1 Git Source (skills + Packages) \u00B7 2 App recipe (Esc cancel)" });
|
|
312
346
|
if (mode.kind === "input")
|
|
313
347
|
return _jsxs(Text, { children: ["URL: ", mode.value] });
|
|
314
348
|
if (mode.kind === "busy")
|
|
315
|
-
return _jsx(
|
|
349
|
+
return _jsx(BorderPanel, { title: "Sources", children: _jsx(FieldLine, { label: "Status", value: "Working..." }) });
|
|
316
350
|
if (mode.kind === "confirm") {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
:
|
|
320
|
-
|
|
351
|
+
return mode.action === "refresh" ? (() => {
|
|
352
|
+
const viewport = refreshViewport(mode, terminalRows, terminalColumns);
|
|
353
|
+
return (_jsxs(BorderPanel, { title: "Refresh Git Sources", children: [_jsx(PreviewLines, { lines: viewport.rows }), viewport.total > viewport.height ? _jsxs(Text, { color: theme.legend, children: [viewport.start + 1, "\u2013", viewport.end, " of ", viewport.total] }) : null, _jsxs(Text, { color: theme.legend, children: ["Review ", viewport.cursor + 1, " of ", viewport.total, " \u00B7 arrows/Pg page"] }), viewport.reachedEnd
|
|
354
|
+
? _jsx(DecisionPrompt, { children: "Refresh these Sources?" })
|
|
355
|
+
: _jsx(Text, { color: theme.legend, children: "Scroll to end; n cancels." })] }));
|
|
356
|
+
})() : (_jsxs(BorderPanel, { title: "Remove Git Source", children: [_jsx(FieldLine, { label: "Source", value: `remove Git Source ${mode.source.id} from ${mode.source.url}` }), _jsx(DecisionPrompt, { children: "Remove this Source?" })] }));
|
|
321
357
|
}
|
|
322
358
|
return null;
|
|
323
359
|
}
|
package/dist/src/tui/theme.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
/** Central xeo palette for the TUI; views take every colour from here. */
|
|
2
2
|
export const theme = {
|
|
3
|
-
/**
|
|
3
|
+
/** Panel titles and active labels use Xeo accent pink. */
|
|
4
|
+
title: "#ff2e9a",
|
|
4
5
|
accent: "#ff2e9a",
|
|
5
|
-
/**
|
|
6
|
+
/** Borders and group nodes. */
|
|
7
|
+
border: "#7aa2f7",
|
|
6
8
|
group: "#7aa2f7",
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
+
/** Field names and markers such as hosts. */
|
|
10
|
+
field: "#7aa2f7",
|
|
11
|
+
marker: "#7aa2f7",
|
|
12
|
+
/** Primary and emphasized values. */
|
|
13
|
+
text: "#a9b1d6",
|
|
14
|
+
value: "#c0caf5",
|
|
15
|
+
version: "#ff9e64",
|
|
9
16
|
success: "#9ece6a",
|
|
10
17
|
warning: "#e0af68",
|
|
11
18
|
error: "#f7768e",
|
|
12
|
-
/**
|
|
19
|
+
/** Legends, hints and footer. */
|
|
20
|
+
legend: "#565f89",
|
|
13
21
|
muted: "#414868",
|
|
14
|
-
/** Inactive tabs. */
|
|
22
|
+
/** Inactive tabs and selected-row background. */
|
|
15
23
|
inactive: "#565f89",
|
|
16
|
-
/** Background of the selected row. */
|
|
17
24
|
selection: "#292e42",
|
|
18
25
|
};
|
|
19
26
|
/** Columns assumed when the terminal width is unknown. */
|
|
@@ -49,7 +49,7 @@ export function useUpdatesFlow(inputs) {
|
|
|
49
49
|
try {
|
|
50
50
|
const prepared = await prepareUpdates(current, selected);
|
|
51
51
|
if (mounted.current)
|
|
52
|
-
setMode({ kind: "preview", prepared });
|
|
52
|
+
setMode({ kind: "preview", prepared, cursor: 0 });
|
|
53
53
|
}
|
|
54
54
|
catch (error) {
|
|
55
55
|
if (!mounted.current)
|
|
@@ -72,7 +72,7 @@ export function useUpdatesFlow(inputs) {
|
|
|
72
72
|
resolve(done);
|
|
73
73
|
};
|
|
74
74
|
pendingAnswer.current = answer;
|
|
75
|
-
setMode({ kind: "manual", plan, reason, answer });
|
|
75
|
+
setMode({ kind: "manual", plan, reason, answer, cursor: 0 });
|
|
76
76
|
}));
|
|
77
77
|
result = { kind: outcome.failed === 0 ? "ok" : "error", lines: outcome.lines };
|
|
78
78
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { answerYesNo } from "./mode-keys.js";
|
|
1
|
+
import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
|
|
2
2
|
import { pageStep } from "./window.js";
|
|
3
3
|
const SCOPE_KEYS = new Map([["t", "all"], ["p", "project"], ["g", "user-global"]]);
|
|
4
4
|
/** Moves the cursor with the arrows, j/k and paging; returns whether the key was one of them. */
|
|
@@ -61,18 +61,28 @@ function handleBrowseKey(context, input, key) {
|
|
|
61
61
|
context.changeScope(scope);
|
|
62
62
|
else if (input === "r")
|
|
63
63
|
context.recheck();
|
|
64
|
+
else if (input === "u")
|
|
65
|
+
context.startPreview();
|
|
64
66
|
else
|
|
65
67
|
foldKey(context, snapshot, key);
|
|
66
68
|
}
|
|
67
69
|
function handlePreviewKey(context, mode, input, key) {
|
|
70
|
+
const metrics = context.previewMetrics(mode);
|
|
71
|
+
const cursor = movePreviewCursor(mode.cursor, metrics.length, input, key, metrics.pageSize);
|
|
72
|
+
if (cursor !== undefined) {
|
|
73
|
+
context.setMode({ ...mode, cursor });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
68
76
|
if (mode.prepared.applicable.length === 0) {
|
|
69
77
|
if (key.escape || key.return || input === "n")
|
|
70
78
|
context.cancel();
|
|
71
79
|
return;
|
|
72
80
|
}
|
|
73
81
|
answerYesNo(input, key, () => {
|
|
82
|
+
if (!context.previewAtEnd(mode))
|
|
83
|
+
return;
|
|
74
84
|
if (mode.prepared.nonCanonicalPaths.length > 0)
|
|
75
|
-
context.setMode({ ...mode, kind: "confirm-paths" });
|
|
85
|
+
context.setMode({ ...mode, kind: "confirm-paths", cursor: 0 });
|
|
76
86
|
else
|
|
77
87
|
context.apply(mode.prepared, []);
|
|
78
88
|
}, context.cancel);
|
|
@@ -80,8 +90,16 @@ function handlePreviewKey(context, mode, input, key) {
|
|
|
80
90
|
/** Handles a key in the current Updates mode; `busy` ignores keys. */
|
|
81
91
|
export function handleUpdatesKey(context, mode, input, key) {
|
|
82
92
|
switch (mode.kind) {
|
|
83
|
-
case "manual":
|
|
93
|
+
case "manual": {
|
|
94
|
+
const metrics = context.previewMetrics(mode);
|
|
95
|
+
const cursor = movePreviewCursor(mode.cursor, metrics.length, input, key, metrics.pageSize);
|
|
96
|
+
if (cursor !== undefined) {
|
|
97
|
+
context.setMode({ ...mode, cursor });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
84
100
|
answerYesNo(input, key, () => {
|
|
101
|
+
if (!context.previewAtEnd(mode))
|
|
102
|
+
return;
|
|
85
103
|
context.setMode({ kind: "busy", label: "Checking App version..." });
|
|
86
104
|
mode.answer(true);
|
|
87
105
|
}, () => {
|
|
@@ -89,15 +107,26 @@ export function handleUpdatesKey(context, mode, input, key) {
|
|
|
89
107
|
mode.answer(false);
|
|
90
108
|
});
|
|
91
109
|
return;
|
|
110
|
+
}
|
|
92
111
|
case "browse":
|
|
93
112
|
handleBrowseKey(context, input, key);
|
|
94
113
|
return;
|
|
95
114
|
case "preview":
|
|
96
115
|
handlePreviewKey(context, mode, input, key);
|
|
97
116
|
return;
|
|
98
|
-
case "confirm-paths":
|
|
99
|
-
|
|
117
|
+
case "confirm-paths": {
|
|
118
|
+
const metrics = context.previewMetrics(mode);
|
|
119
|
+
const cursor = movePreviewCursor(mode.cursor, metrics.length, input, key, metrics.pageSize);
|
|
120
|
+
if (cursor !== undefined) {
|
|
121
|
+
context.setMode({ ...mode, cursor });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
answerYesNo(input, key, () => {
|
|
125
|
+
if (context.previewAtEnd(mode))
|
|
126
|
+
context.apply(mode.prepared, mode.prepared.nonCanonicalPaths);
|
|
127
|
+
}, context.cancel);
|
|
100
128
|
return;
|
|
129
|
+
}
|
|
101
130
|
default:
|
|
102
131
|
return;
|
|
103
132
|
}
|
|
@@ -5,9 +5,11 @@ export const PHASE_LABEL = { refreshing: REFRESHING, checking: CHECKING };
|
|
|
5
5
|
export function describeUnknownReasons(rows) {
|
|
6
6
|
const newer = rows.filter((row) => /^installed by a newer agent depot/i.test(row.reason)).length;
|
|
7
7
|
const pinned = rows.filter((row) => /^pinned to agent depot/i.test(row.reason)).length;
|
|
8
|
+
const localChanges = rows.filter((row) => row.kind === "package" && /local modifications|local changes/i.test(row.reason)).length;
|
|
8
9
|
const parts = [
|
|
9
10
|
...(newer > 0 ? [`${newer} installed by a newer Agent Depot`] : []),
|
|
10
11
|
...(pinned > 0 ? [`${pinned} pinned to another Agent Depot`] : []),
|
|
12
|
+
...(localChanges > 0 ? [`${localChanges} Package update comparison unavailable because the checkout has local changes`] : []),
|
|
11
13
|
];
|
|
12
14
|
return parts.length === 0 ? "" : ` (${parts.join(", ")})`;
|
|
13
15
|
}
|
|
@@ -1,13 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Box, Text } from "ink";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
3
3
|
import { rowStyle, theme } from "./theme.js";
|
|
4
|
+
import { BorderPanel, FieldLine, PreviewConfirm } from "./panel-parts.js";
|
|
5
|
+
import { previewViewport } from "./preview-window.js";
|
|
4
6
|
import { CHECKING, describeRefresh, describeUnknownReasons, REFRESHING } from "./updates-model.js";
|
|
5
|
-
import { formatAgo } from "./updates.js";
|
|
6
|
-
import {
|
|
7
|
+
import { formatAgo, previewLineText } from "./updates.js";
|
|
8
|
+
import { updateModePreviewChrome, updateModePreviewLines, updateModeQuestion } from "./updates-preview.js";
|
|
9
|
+
function updateResultField(line, defaultOk) {
|
|
10
|
+
if (line.startsWith("Update summary: ")) {
|
|
11
|
+
const successful = /^Update summary: \d+ updated, 0 failed$/.test(line);
|
|
12
|
+
return { ok: successful, label: "Update summary:", value: line.slice("Update summary: ".length) };
|
|
13
|
+
}
|
|
14
|
+
if (line.startsWith("Updated "))
|
|
15
|
+
return { ok: true, label: "Updated", value: line.slice("Updated ".length) };
|
|
16
|
+
if (line.startsWith("Failed "))
|
|
17
|
+
return { ok: false, label: "Failed", value: line.slice("Failed ".length) };
|
|
18
|
+
if (line.startsWith("Cancelled "))
|
|
19
|
+
return { ok: false, label: "Cancelled", value: line.slice("Cancelled ".length) };
|
|
20
|
+
if (line === "Update cancelled")
|
|
21
|
+
return { ok: true, label: undefined, value: line };
|
|
22
|
+
return { ok: defaultOk, label: undefined, value: line };
|
|
23
|
+
}
|
|
7
24
|
/** What the last check found out: its age, how the sources were refreshed, and what failed. */
|
|
8
25
|
export function CheckStatus({ data, now }) {
|
|
9
26
|
const errors = data.results.filter((result) => result.error !== undefined);
|
|
10
|
-
return (_jsxs(
|
|
27
|
+
return (_jsxs(BorderPanel, { title: "Update check", children: [_jsxs(Text, { color: theme.field, children: ["checked ", _jsx(Text, { color: theme.value, children: formatAgo(now - data.checkedAt) }), _jsx(Text, { color: theme.legend, children: " \u00B7 " }), _jsx(Text, { color: theme.value, children: describeRefresh(data) })] }), data.refresh.failed.map((failure) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.warning, children: "[WARN] " }), _jsx(FieldLine, { label: "Refresh failed for", value: `${failure.source}: ${failure.reason}` })] }, failure.source))), data.appError ? _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: "Error (Apps):", value: data.appError })] }) : null, data.packageError ? _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: "Error (Packages):", value: data.packageError })] }) : null, errors.map((result) => _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: `Error (${result.scope}):`, value: result.error ?? "" })] }, result.scope))] }));
|
|
11
28
|
}
|
|
12
29
|
/** One line of the list: an update, the fold of unassessable items, or one of those when unfolded. */
|
|
13
30
|
export function EntryLine({ entry, selected, checked, view, showUnknown }) {
|
|
@@ -17,28 +34,36 @@ export function EntryLine({ entry, selected, checked, view, showUnknown }) {
|
|
|
17
34
|
}
|
|
18
35
|
const { row } = entry;
|
|
19
36
|
if (entry.kind === "unknown") {
|
|
20
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { ...rowStyle(selected), children: [cursor, " ", row.path, " ", row.scope] }), _jsxs(Text, { color: theme.muted, children: [" ", row.
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { ...rowStyle(selected), children: [cursor, " ", _jsx(Text, { color: theme.field, children: row.path }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.scope })] }), _jsxs(Text, { color: theme.muted, children: [" ", row.kind === "package" && /local modifications|local changes/i.test(row.reason)
|
|
38
|
+
? `Update comparison unavailable: ${row.reason}`
|
|
39
|
+
: row.reason] })] }));
|
|
21
40
|
}
|
|
22
|
-
return (_jsxs(Text, { ...rowStyle(selected), children: [cursor, checked ? "[x]" : "[ ]", " ", row.path, " ", row.scope, " ", row.policy, " ", row.installed,
|
|
41
|
+
return (_jsxs(Text, { ...rowStyle(selected), children: [cursor, checked ? "[x]" : "[ ]", " ", _jsx(Text, { color: theme.field, children: row.path }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.scope }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.policy }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.value, children: row.installed }), _jsx(Text, { color: theme.legend, children: " -> " }), _jsx(Text, { color: theme.value, children: row.available })] }));
|
|
23
42
|
}
|
|
24
43
|
/** Under the list: the window indicator, what is up to date or selected, and the last result. */
|
|
25
44
|
export function UpdatesFooter({ indicator, upToDate, updatableCount, checkedCount, message }) {
|
|
26
|
-
return (_jsxs(_Fragment, { children: [indicator ? _jsx(Text, { color: theme.muted, children: indicator }) : null, upToDate > 0 ? _jsxs(Text, { color: theme.muted, children: [upToDate, " up to date"] }) : null, updatableCount > 0 ? _jsxs(Text, { color: theme.muted, children: [checkedCount, " selected"] }) : null, message ? (_jsx(
|
|
45
|
+
return (_jsxs(_Fragment, { children: [indicator ? _jsx(Text, { color: theme.muted, children: indicator }) : null, upToDate > 0 ? _jsxs(Text, { color: theme.muted, children: [upToDate, " up to date"] }) : null, updatableCount > 0 ? _jsxs(Text, { color: theme.muted, children: [checkedCount, " selected"] }) : null, message ? (_jsx(BorderPanel, { title: "Update result", children: message.lines.map((line, position) => {
|
|
46
|
+
const result = updateResultField(line, message.kind === "ok");
|
|
47
|
+
return _jsxs(Text, { children: [_jsxs(Text, { color: result.ok ? theme.success : theme.error, children: ["[", result.ok ? "OK" : "FAIL", "] "] }), result.label === undefined ? _jsx(Text, { color: theme.value, children: result.value }) : _jsx(FieldLine, { label: result.label, value: result.value })] }, position);
|
|
48
|
+
}) })) : null] }));
|
|
49
|
+
}
|
|
50
|
+
function CapturedUpdatePreview({ mode }) {
|
|
51
|
+
const { rows, columns } = useWindowSize();
|
|
52
|
+
const lines = updateModePreviewLines(mode).map(previewLineText);
|
|
53
|
+
const chrome = updateModePreviewChrome(mode);
|
|
54
|
+
const viewport = previewViewport(lines, mode.cursor, rows || 24, columns || 80, chrome);
|
|
55
|
+
return (_jsx(PreviewConfirm, { title: chrome.title, lines: lines, cursor: mode.cursor, question: updateModeQuestion(mode, viewport.reachedEnd), chrome: chrome }));
|
|
27
56
|
}
|
|
28
|
-
/** The prompt of the preview and confirmation modes; browsing and the check itself show nothing. */
|
|
29
57
|
export function ModePanel({ mode }) {
|
|
30
58
|
switch (mode.kind) {
|
|
31
59
|
case "manual":
|
|
32
|
-
return (
|
|
60
|
+
return _jsx(CapturedUpdatePreview, { mode: mode });
|
|
33
61
|
case "browse":
|
|
34
62
|
return null;
|
|
35
63
|
case "busy":
|
|
36
|
-
return mode.label === REFRESHING || mode.label === CHECKING ? null : _jsx(
|
|
64
|
+
return mode.label === REFRESHING || mode.label === CHECKING ? null : (_jsx(BorderPanel, { title: "Updates", children: _jsx(FieldLine, { label: "Status", value: mode.label }) }));
|
|
37
65
|
case "preview":
|
|
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
|
-
? _jsx(Text, { children: "Nothing can be applied. Enter/Esc to go back" })
|
|
40
|
-
: _jsxs(Text, { children: ["Apply ", mode.prepared.applicable.length, " ", mode.prepared.applicable.length === 1 ? "update" : "updates", "? y/n"] })] }));
|
|
41
66
|
case "confirm-paths":
|
|
42
|
-
return
|
|
67
|
+
return _jsx(CapturedUpdatePreview, { mode: mode });
|
|
43
68
|
}
|
|
44
69
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { previewText } from "./updates.js";
|
|
2
|
+
export function updateModePreviewLines(mode) {
|
|
3
|
+
switch (mode.kind) {
|
|
4
|
+
case "preview":
|
|
5
|
+
return [
|
|
6
|
+
...mode.prepared.lines,
|
|
7
|
+
...mode.prepared.appFailures.map(previewText),
|
|
8
|
+
...mode.prepared.packageFailures.map(previewText),
|
|
9
|
+
...mode.prepared.failures.map(({ item, message }) => previewText(`Preview failed for ${JSON.stringify(item.id)}: ${message}`)),
|
|
10
|
+
];
|
|
11
|
+
case "manual":
|
|
12
|
+
return [
|
|
13
|
+
{ kind: "field", label: "App:", value: mode.plan.entry.recipe?.name ?? mode.plan.entry.file },
|
|
14
|
+
{ kind: "field", label: "Environment:", value: mode.plan.environment },
|
|
15
|
+
{ kind: "field", label: "Working directory:", value: mode.plan.cwd },
|
|
16
|
+
{ kind: "field", label: "Manual:", value: mode.plan.manual ?? "" },
|
|
17
|
+
...(mode.reason === undefined ? [] : [previewText(mode.reason)]),
|
|
18
|
+
];
|
|
19
|
+
case "confirm-paths":
|
|
20
|
+
return [
|
|
21
|
+
previewText("Outside .agents/skills and .claude/skills:"),
|
|
22
|
+
...mode.prepared.nonCanonicalPaths.map(({ scope, path }) => ({
|
|
23
|
+
kind: "field",
|
|
24
|
+
label: "Path:",
|
|
25
|
+
value: `[${scope}] ${JSON.stringify(path)}`,
|
|
26
|
+
})),
|
|
27
|
+
];
|
|
28
|
+
default:
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function updateModePreviewChrome(mode) {
|
|
33
|
+
switch (mode.kind) {
|
|
34
|
+
case "preview": {
|
|
35
|
+
const count = mode.prepared.applicable.length;
|
|
36
|
+
const question = `Apply ${count} ${count === 1 ? "update" : "updates"}? y/n`;
|
|
37
|
+
return {
|
|
38
|
+
title: "Update batch preview",
|
|
39
|
+
questions: ["Scroll to end before confirming; n cancels.", question],
|
|
40
|
+
footerRows: 2,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
case "manual":
|
|
44
|
+
return {
|
|
45
|
+
title: "Manual App update",
|
|
46
|
+
questions: ["Scroll to end before confirming; n cancels.", "Manual step done, check now? y/n"],
|
|
47
|
+
footerRows: 2,
|
|
48
|
+
};
|
|
49
|
+
case "confirm-paths":
|
|
50
|
+
return {
|
|
51
|
+
title: "Confirm replacing non-canonical locations",
|
|
52
|
+
questions: ["Scroll to end before confirming; n cancels.", "Replace exactly these paths? y/n"],
|
|
53
|
+
footerRows: 2,
|
|
54
|
+
};
|
|
55
|
+
default:
|
|
56
|
+
return { title: "Updates", questions: ["Continue? y/n"] };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function updateModeQuestion(mode, reachedEnd) {
|
|
60
|
+
if (mode.kind === "preview" && mode.prepared.applicable.length === 0) {
|
|
61
|
+
return "Nothing can be applied. Enter or Esc to go back.";
|
|
62
|
+
}
|
|
63
|
+
const chrome = updateModePreviewChrome(mode);
|
|
64
|
+
return reachedEnd ? chrome.questions.at(-1) ?? "Continue? y/n" : chrome.questions[0] ?? "Scroll to end before confirming; n cancels.";
|
|
65
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
2
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { theme } from "./theme.js";
|
|
5
|
+
import { previewViewport } from "./preview-window.js";
|
|
5
6
|
import { computeWindow, useListHeight } from "./window.js";
|
|
6
7
|
import { BROWSE } from "./updates-mode.js";
|
|
7
8
|
import { useMirrored } from "./view-state.js";
|
|
@@ -9,12 +10,16 @@ import { useUpdatesFlow } from "./updates-flow.js";
|
|
|
9
10
|
import { handleUpdatesKey } from "./updates-keys.js";
|
|
10
11
|
import { viewOf } from "./updates-model.js";
|
|
11
12
|
import { CheckStatus, EntryLine, ModePanel, UpdatesFooter } from "./updates-panel.js";
|
|
13
|
+
import { previewLineText } from "./updates.js";
|
|
14
|
+
import { updateModePreviewChrome, updateModePreviewLines } from "./updates-preview.js";
|
|
12
15
|
import { useKeys } from "./keys.js";
|
|
16
|
+
import { BorderPanel, FieldLine } from "./panel-parts.js";
|
|
13
17
|
const NO_ENVIRONMENT = {};
|
|
14
18
|
/** How often the age of the check is redrawn. */
|
|
15
19
|
const CLOCK_TICK_MS = 10_000;
|
|
16
20
|
export function UpdatesView({ operations, environment, onCapturingChange, listHeight, now }) {
|
|
17
21
|
const height = useListHeight(listHeight, 9);
|
|
22
|
+
const { rows: terminalRows, columns: terminalColumns } = useWindowSize();
|
|
18
23
|
const env = environment ?? NO_ENVIRONMENT;
|
|
19
24
|
const clock = now ?? Date.now;
|
|
20
25
|
// Scope, cursor, fold and selection are mirrored in refs updated first, so keys delivered in one burst act on the latest state.
|
|
@@ -74,8 +79,17 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
74
79
|
dataRef.current = data;
|
|
75
80
|
const { shown, updatable, unknown, upToDate, entries } = viewOf(data, scope, showUnknown);
|
|
76
81
|
const index = Math.min(cursor, Math.max(entries.length - 1, 0));
|
|
82
|
+
const previewGeometry = (current) => {
|
|
83
|
+
const cursor = "cursor" in current ? current.cursor : 0;
|
|
84
|
+
return previewViewport(updateModePreviewLines(current).map(previewLineText), cursor, terminalRows || 24, terminalColumns || 80, updateModePreviewChrome(current));
|
|
85
|
+
};
|
|
77
86
|
const keyContext = {
|
|
78
87
|
height,
|
|
88
|
+
previewMetrics: current => {
|
|
89
|
+
const geometry = previewGeometry(current);
|
|
90
|
+
return { length: geometry.total, pageSize: geometry.height };
|
|
91
|
+
},
|
|
92
|
+
previewAtEnd: current => previewGeometry(current).reachedEnd,
|
|
79
93
|
latest,
|
|
80
94
|
checked: () => checkedRef.current,
|
|
81
95
|
setChecked,
|
|
@@ -97,5 +111,6 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
97
111
|
const window = computeWindow(entries.length, index, height);
|
|
98
112
|
const view = { shown, updatable, unknown, upToDate, entries };
|
|
99
113
|
const hasErrors = data?.appError !== undefined || (data?.results ?? []).some((result) => result.error !== undefined);
|
|
100
|
-
|
|
114
|
+
const showingBrowse = mode.kind === "browse";
|
|
115
|
+
return (_jsxs(Box, { flexDirection: "column", children: [showingBrowse ? _jsxs(Text, { color: theme.title, bold: true, children: ["Updates (scope: ", scope, ") t all \u00B7 p project \u00B7 g user-global \u00B7 u update"] }) : null, state.status === "loading" ? _jsx(BorderPanel, { title: "Update check", children: _jsx(FieldLine, { label: "Status", value: state.label }) }) : null, showingBrowse && data ? _jsx(CheckStatus, { data: data, now: clock() }) : null, showingBrowse && data && !hasErrors && shown.length === 0 ? _jsx(Text, { children: "No installations" }) : null, showingBrowse ? entries.slice(window.start, window.end).map((entry, offset) => (_jsx(EntryLine, { entry: entry, selected: window.start + offset === index, checked: entry.kind === "update" && checked.includes(entry.row.key), view: view, showUnknown: showUnknown }, entry.kind === "summary" ? "summary" : entry.row.key))) : null, showingBrowse ? _jsx(UpdatesFooter, { indicator: window.indicator, upToDate: upToDate, updatableCount: updatable.length, checkedCount: checked.length, message: message }) : null, _jsx(ModePanel, { mode: mode })] }));
|
|
101
116
|
}
|
package/dist/src/tui/updates.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function loadUpdateRows(operations, environment, options) {
|
|
|
111
111
|
catch (error) {
|
|
112
112
|
appError = errorText(error);
|
|
113
113
|
}
|
|
114
|
-
for (const check of appChecks) {
|
|
114
|
+
for (const check of appChecks.filter(candidate => candidate.inspection.status === "installed")) {
|
|
115
115
|
rows.push({
|
|
116
116
|
kind: "app",
|
|
117
117
|
key: `app:${check.entry.file}`,
|
|
@@ -161,6 +161,12 @@ export function formatAgo(elapsedMs) {
|
|
|
161
161
|
return `${minutes} min ago`;
|
|
162
162
|
return `${Math.floor(minutes / 60)} h ago`;
|
|
163
163
|
}
|
|
164
|
+
export function previewText(text) {
|
|
165
|
+
return { kind: "text", text };
|
|
166
|
+
}
|
|
167
|
+
export function previewLineText(line) {
|
|
168
|
+
return line.kind === "field" ? `${line.label} ${line.value}` : line.text;
|
|
169
|
+
}
|
|
164
170
|
/** Plan step: previews the selection from the assessed snapshots, scope by scope. Changes nothing. */
|
|
165
171
|
export async function prepareUpdates(data, selected) {
|
|
166
172
|
const groups = [];
|
|
@@ -176,7 +182,7 @@ export async function prepareUpdates(data, selected) {
|
|
|
176
182
|
continue;
|
|
177
183
|
const { context } = loaded;
|
|
178
184
|
const outcome = await previewUpdateCandidates(items, context);
|
|
179
|
-
lines.push(...outcome.plans.flatMap((plan) => describeUpdatePreview(plan, scope, context.projectRoot, "prompt")));
|
|
185
|
+
lines.push(...outcome.plans.flatMap((plan) => describeUpdatePreview(plan, scope, context.projectRoot, "prompt").map(previewText)));
|
|
180
186
|
failures.push(...outcome.failures);
|
|
181
187
|
groups.push({
|
|
182
188
|
scope,
|
|
@@ -192,7 +198,10 @@ export async function prepareUpdates(data, selected) {
|
|
|
192
198
|
try {
|
|
193
199
|
const plan = await data.apps.planLifecycle(row.check.entry, "update", undefined, row.check);
|
|
194
200
|
appPlans.push(plan);
|
|
195
|
-
lines.push(
|
|
201
|
+
lines.push({ kind: "field", label: "Environment:", value: plan.environment }, previewText(row.path), { kind: "field", label: "Version:", value: `${plan.previousVersion} -> ${plan.latestVersion ?? "unknown"}` }, ...(plan.argv ? [
|
|
202
|
+
{ kind: "field", label: "argv:", value: JSON.stringify(plan.argv) },
|
|
203
|
+
{ kind: "field", label: "Executable:", value: plan.executable ?? `${JSON.stringify(plan.argv[0])} not found in PATH` },
|
|
204
|
+
] : []), ...(plan.manual ? [{ kind: "field", label: "Manual:", value: plan.manual }] : []), { kind: "field", label: "Working directory:", value: plan.cwd }, ...(plan.warning ? [previewText(plan.warning)] : []));
|
|
196
205
|
}
|
|
197
206
|
catch (error) {
|
|
198
207
|
appFailures.push(`Failed ${row.path}: preview failed: ${errorText(error)}`);
|
|
@@ -205,9 +214,9 @@ export async function prepareUpdates(data, selected) {
|
|
|
205
214
|
const plan = await data.packages.planLifecycle(row.check.installed.selection, "update");
|
|
206
215
|
const approval = await data.packages.approvalStatus(row.check.installed.selection, "update");
|
|
207
216
|
packagePlans.push({ plan, approval });
|
|
208
|
-
lines.push(...packagePlanLines(plan, approval));
|
|
217
|
+
lines.push(...packagePlanLines(plan, approval).map(previewText));
|
|
209
218
|
if (approval === "needs approval")
|
|
210
|
-
lines.push("This apply approves the declaration above.");
|
|
219
|
+
lines.push(previewText("This apply approves the declaration above."));
|
|
211
220
|
}
|
|
212
221
|
catch (error) {
|
|
213
222
|
packageFailures.push(`Failed ${row.path}: preview failed: ${errorText(error)}`);
|
package/dist/src/tui/window.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useWindowSize } from "ink";
|
|
2
|
+
import { headerTabRowCount } from "./header-layout.js";
|
|
2
3
|
/** Rows assumed when the terminal size is unknown (for example under test renderers). */
|
|
3
4
|
export const FALLBACK_TERMINAL_ROWS = 24;
|
|
4
|
-
/**
|
|
5
|
-
export const APP_CHROME_ROWS =
|
|
5
|
+
/** Header borders, title, one tab row and footer; extra tab rows are added per terminal width. */
|
|
6
|
+
export const APP_CHROME_ROWS = 5;
|
|
6
7
|
const MINIMUM_ROWS = 3;
|
|
7
8
|
/** The slice of a list that fits in `height` rows while keeping `selected` visible (roughly centred). */
|
|
8
9
|
export function computeWindow(total, selected, height) {
|
|
@@ -23,8 +24,9 @@ export function pageStep(height) {
|
|
|
23
24
|
* view's own `reserved` lines. `override` fixes the height for tests.
|
|
24
25
|
*/
|
|
25
26
|
export function useListHeight(override, reserved) {
|
|
26
|
-
const { rows } = useWindowSize();
|
|
27
|
+
const { rows, columns } = useWindowSize();
|
|
27
28
|
if (override !== undefined)
|
|
28
29
|
return Math.max(override, 1);
|
|
29
|
-
|
|
30
|
+
const chromeRows = APP_CHROME_ROWS + headerTabRowCount(Math.max(columns - 4, 1)) - 1;
|
|
31
|
+
return Math.max((rows || FALLBACK_TERMINAL_ROWS) - chromeRows - reserved, MINIMUM_ROWS);
|
|
30
32
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iseoane/agent-depot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A cross-platform catalog for reusable coding-agent skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "iseoane",
|
|
@@ -82,12 +82,13 @@
|
|
|
82
82
|
"audit:dupes": "npx --yes fallow@3.30.0 dupes",
|
|
83
83
|
"audit:health": "pnpm test:coverage && npx --yes fallow@3.30.0 health --coverage coverage/v8 --min-score 85",
|
|
84
84
|
"audit:all": "pnpm audit:dead-code && pnpm audit:dupes && pnpm audit:health",
|
|
85
|
-
"test:e2e": "pnpm build && node --test --test-timeout=120000 \"dist/tests/e2e/*.e2e.test.js\"",
|
|
85
|
+
"test:e2e": "pnpm build && node --import ./dist/tests/isolated-env.js --test --test-timeout=120000 \"dist/tests/e2e/*.e2e.test.js\"",
|
|
86
86
|
"prepublishOnly": "pnpm build && pnpm test",
|
|
87
87
|
"test:pack": "pnpm build && node scripts/pack-smoke.mjs"
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
90
|
"ink": "^7.1.1",
|
|
91
|
-
"react": "^19.3.0"
|
|
91
|
+
"react": "^19.3.0",
|
|
92
|
+
"string-width": "^8.3.0"
|
|
92
93
|
}
|
|
93
94
|
}
|