@iseoane/agent-depot 0.5.2 → 0.6.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 +47 -0
- package/README.es.md +137 -34
- package/README.md +120 -29
- 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/file-picker-model.js +87 -0
- package/dist/src/tui/file-picker.js +142 -0
- 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 +119 -6
- package/dist/src/tui/preview-window.js +51 -0
- package/dist/src/tui/profile-view.js +394 -136
- 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/text-wrap.js +53 -0
- 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 +8 -6
- package/package.json +6 -6
- package/skills/doctor-md-agents/README.md +3 -1
- package/skills/doctor-md-skill/README.md +2 -2
|
@@ -9,8 +9,10 @@ export function modeReducer(mode, event) {
|
|
|
9
9
|
return mode.kind === "input" ? { kind: "input", value: event.value } : mode;
|
|
10
10
|
case "confirm":
|
|
11
11
|
return event.action === "refresh"
|
|
12
|
-
? { kind: "confirm", action: "refresh", sources: event.sources }
|
|
12
|
+
? { kind: "confirm", action: "refresh", sources: event.sources, cursor: 0 }
|
|
13
13
|
: { kind: "confirm", action: "remove", source: event.source };
|
|
14
|
+
case "preview-cursor":
|
|
15
|
+
return mode.kind === "confirm" && mode.action === "refresh" ? { ...mode, cursor: event.cursor } : mode;
|
|
14
16
|
case "busy":
|
|
15
17
|
return { kind: "busy" };
|
|
16
18
|
case "done":
|
|
@@ -7,16 +7,31 @@ import { BUILT_IN_SOURCE } from "../sources.js";
|
|
|
7
7
|
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
8
8
|
import { ActionPanel } from "./catalog-panel.js";
|
|
9
9
|
import { hasUnmanagedCopy, isInstalled, loadInstalledSkills } from "./catalog-installs.js";
|
|
10
|
-
import { appName, appStepLines } from "./app-actions.js";
|
|
10
|
+
import { appName, appRecipePreviewLines, appStepLines } from "./app-actions.js";
|
|
11
11
|
import { errorText } from "./batch.js";
|
|
12
12
|
import { useMounted } from "./view-state.js";
|
|
13
|
-
import { PreviewConfirm } from "./panel-parts.js";
|
|
14
|
-
import { answerYesNo } from "./mode-keys.js";
|
|
13
|
+
import { BorderPanel, DecisionPrompt, PreviewConfirm } from "./panel-parts.js";
|
|
14
|
+
import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
|
|
15
|
+
import { previewViewport } from "./preview-window.js";
|
|
15
16
|
import { theme } from "./theme.js";
|
|
17
|
+
function recipeApprovalChrome(entry) {
|
|
18
|
+
return {
|
|
19
|
+
title: "Recipe approval · does not install",
|
|
20
|
+
questions: [`Approve ${appName(entry)}? y/n · recipe approval does not install the App.`, "Scroll to end before y; n cancels."],
|
|
21
|
+
footerRows: 2,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function uninstallChrome(mode) {
|
|
25
|
+
return {
|
|
26
|
+
title: mode.kind === "uninstall-manual" ? "App manual uninstall" : "App uninstall confirmation",
|
|
27
|
+
questions: [mode.kind === "uninstall-manual" ? "Complete the manual uninstall, then check now? y/n" : `Uninstall ${appName(mode.removal.entry)}? y/n`, "Scroll to end before y; n cancels."],
|
|
28
|
+
footerRows: 2,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
16
31
|
/**
|
|
17
32
|
* Recipe loading and captured actions; Sources owns the single list and cursor.
|
|
18
33
|
*/
|
|
19
|
-
export function useSourcesRecipes({ operations, environment, onCapturingChange, highlightedFile, }) {
|
|
34
|
+
export function useSourcesRecipes({ operations, environment, onCapturingChange, highlightedFile, terminalRows, terminalColumns, }) {
|
|
20
35
|
const homeDirectory = environment?.homeDirectory;
|
|
21
36
|
const appEnvironment = environment?.appEnvironment;
|
|
22
37
|
const apps = useMemo(() => createAppOperations({
|
|
@@ -48,13 +63,8 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
48
63
|
if (mounted.current) {
|
|
49
64
|
setMode({
|
|
50
65
|
kind: "approve",
|
|
51
|
-
entry, removeAfterApproval,
|
|
52
|
-
lines:
|
|
53
|
-
`File: ${entry.file}`,
|
|
54
|
-
`Platform: ${entry.recipe?.platform ?? "all"}`,
|
|
55
|
-
`Status: ${status}`,
|
|
56
|
-
...steps.flatMap(step => [step.step, ...appStepLines(step)]),
|
|
57
|
-
],
|
|
66
|
+
entry, removeAfterApproval, cursor: 0,
|
|
67
|
+
lines: appRecipePreviewLines(entry, status, steps),
|
|
58
68
|
});
|
|
59
69
|
}
|
|
60
70
|
}
|
|
@@ -183,7 +193,13 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
183
193
|
}
|
|
184
194
|
const cancel = () => setMode({ kind: "browse" });
|
|
185
195
|
if (current.kind === "approve") {
|
|
186
|
-
|
|
196
|
+
const viewport = previewViewport(current.lines, current.cursor, terminalRows, terminalColumns, recipeApprovalChrome(current.entry));
|
|
197
|
+
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
198
|
+
if (cursor !== undefined)
|
|
199
|
+
setMode({ ...current, cursor });
|
|
200
|
+
else if (input === "n" || key.escape)
|
|
201
|
+
cancel();
|
|
202
|
+
else if (input === "y" && viewport.reachedEnd) {
|
|
187
203
|
setMode({ kind: "busy" });
|
|
188
204
|
void apps.approve(current.entry).then(async () => {
|
|
189
205
|
await reload();
|
|
@@ -199,7 +215,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
199
215
|
cancel();
|
|
200
216
|
}
|
|
201
217
|
});
|
|
202
|
-
}
|
|
218
|
+
}
|
|
203
219
|
return true;
|
|
204
220
|
}
|
|
205
221
|
if (current.kind === "uninstall-question") {
|
|
@@ -207,7 +223,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
207
223
|
setMode({ kind: "busy" });
|
|
208
224
|
void apps.planLifecycle(current.removal.entry, "uninstall").then(plan => {
|
|
209
225
|
if (mounted.current)
|
|
210
|
-
setMode({ kind: !plan.argv && plan.manual ? "uninstall-manual" : "uninstall-confirm", removal: current.removal, plan });
|
|
226
|
+
setMode({ kind: !plan.argv && plan.manual ? "uninstall-manual" : "uninstall-confirm", removal: current.removal, plan, cursor: 0 });
|
|
211
227
|
}, error => { if (mounted.current) {
|
|
212
228
|
setError(errorText(error));
|
|
213
229
|
cancel();
|
|
@@ -216,7 +232,15 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
216
232
|
return true;
|
|
217
233
|
}
|
|
218
234
|
if (current.kind === "uninstall-confirm" || current.kind === "uninstall-manual") {
|
|
219
|
-
|
|
235
|
+
const lines = appStepLines(current.plan);
|
|
236
|
+
const viewport = previewViewport(lines, current.cursor, terminalRows, terminalColumns, uninstallChrome(current));
|
|
237
|
+
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
238
|
+
if (cursor !== undefined)
|
|
239
|
+
setMode({ ...current, cursor });
|
|
240
|
+
else if (input === "n" || key.escape)
|
|
241
|
+
cancel();
|
|
242
|
+
else if (input === "y" && viewport.reachedEnd)
|
|
243
|
+
void uninstall(current);
|
|
220
244
|
return true;
|
|
221
245
|
}
|
|
222
246
|
if (current.kind === "remove") {
|
|
@@ -272,8 +296,15 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
272
296
|
}
|
|
273
297
|
}, [apps, mounted]);
|
|
274
298
|
useEffect(() => { void reload(); }, [reload]);
|
|
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:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
299
|
+
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(BorderPanel, { title: "Add an App recipe", children: [_jsx(Text, { children: "1 With the AI skill \u00B7 install if missing, then copy the prompt" }), _jsx(Text, { children: "2 Manual \u00B7 write a recipe using the schema" }), _jsx(Text, { children: "n or Esc closes" })] })) : null, mode.kind === "manual" ? (_jsxs(BorderPanel, { title: "Write an App recipe", children: [_jsxs(Text, { children: ["Write a JSON recipe in ", apps.directory] }), _jsx(Text, { children: "Schema: agent-depot app schema \u00B7 n or Esc closes" })] })) : null, mode.kind === "prompt" ? (_jsxs(BorderPanel, { title: "App recipe authoring prompt", children: [_jsx(Text, { children: "Use the agent-depot-apprecipe skill to create the recipe for <app>" }), _jsxs(Text, { children: ["Recipes: ", apps.directory, " \u00B7 n or Esc closes"] })] })) : null, mode.kind === "install" ? _jsx(ActionPanel, { mode: mode.action }) : null, mode.kind === "busy" ? _jsx(BorderPanel, { title: "App recipe workflow", children: _jsx(Text, { children: "Working..." }) }) : null, mode.kind === "approve" ? (() => {
|
|
300
|
+
const chrome = recipeApprovalChrome(mode.entry);
|
|
301
|
+
const viewport = previewViewport(mode.lines, mode.cursor, terminalRows, terminalColumns, chrome);
|
|
302
|
+
return _jsx(PreviewConfirm, { title: chrome.title, lines: mode.lines, cursor: mode.cursor, question: viewport.reachedEnd ? chrome.questions[0] : chrome.questions[1], chrome: chrome });
|
|
303
|
+
})() : null, mode.kind === "uninstall-question" ? (_jsxs(BorderPanel, { title: "Uninstall before recipe removal", children: [_jsxs(Text, { children: ["App ", appName(mode.removal.entry), " is installed or tracked", mode.removal.installedVersion ? ` (${mode.removal.installedVersion})` : "", "."] }), _jsxs(Text, { children: [_jsx(DecisionPrompt, { children: "Uninstall it before removing the recipe?" }), _jsx(Text, { color: theme.legend, children: "(n cancels removal)" })] })] })) : null, mode.kind === "uninstall-confirm" || mode.kind === "uninstall-manual" ? (() => {
|
|
304
|
+
const chrome = uninstallChrome(mode);
|
|
305
|
+
const lines = appStepLines(mode.plan);
|
|
306
|
+
const viewport = previewViewport(lines, mode.cursor, terminalRows, terminalColumns, chrome);
|
|
307
|
+
return _jsx(PreviewConfirm, { title: chrome.title, lines: lines, cursor: mode.cursor, question: viewport.reachedEnd ? chrome.questions[0] : chrome.questions[1], chrome: chrome });
|
|
308
|
+
})() : null, mode.kind === "remove" ? (_jsx(PreviewConfirm, { title: "Recipe removal", 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] }));
|
|
309
|
+
return { rows, handleKey, startAdd, reload, clearMessage: () => { setError(undefined); setNotice(undefined); }, panel, directory: apps.directory, capturing: mode.kind !== "browse" };
|
|
279
310
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import stringWidth from "string-width";
|
|
2
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
3
|
+
/** Split text on grapheme boundaries so wide and combining characters wrap at `width` columns. */
|
|
4
|
+
export function wrapText(value, width) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
let line = "";
|
|
7
|
+
let lineWidth = 0;
|
|
8
|
+
for (const { segment } of graphemes.segment(value)) {
|
|
9
|
+
const segmentWidth = stringWidth(segment);
|
|
10
|
+
if (line && lineWidth + segmentWidth > width) {
|
|
11
|
+
lines.push(line);
|
|
12
|
+
line = "";
|
|
13
|
+
lineWidth = 0;
|
|
14
|
+
}
|
|
15
|
+
line += segment;
|
|
16
|
+
lineWidth += segmentWidth;
|
|
17
|
+
}
|
|
18
|
+
return [...lines, line];
|
|
19
|
+
}
|
|
20
|
+
/** `lines` capped to `maxRows`, with the last visible row shortened to `width` plus an ellipsis. */
|
|
21
|
+
export function fitLines(lines, maxRows, width) {
|
|
22
|
+
if (maxRows <= 0)
|
|
23
|
+
return [];
|
|
24
|
+
if (lines.length <= maxRows)
|
|
25
|
+
return lines;
|
|
26
|
+
const visible = lines.slice(0, maxRows);
|
|
27
|
+
let last = "";
|
|
28
|
+
let used = 0;
|
|
29
|
+
for (const { segment } of graphemes.segment(visible[maxRows - 1])) {
|
|
30
|
+
const segmentWidth = stringWidth(segment);
|
|
31
|
+
if (used + segmentWidth > width - 1)
|
|
32
|
+
break;
|
|
33
|
+
last += segment;
|
|
34
|
+
used += segmentWidth;
|
|
35
|
+
}
|
|
36
|
+
visible[maxRows - 1] = `${last}…`;
|
|
37
|
+
return visible;
|
|
38
|
+
}
|
|
39
|
+
/** One line shortened to `width` columns plus an ellipsis, for single-row labels. */
|
|
40
|
+
export function clipText(value, width) {
|
|
41
|
+
if (stringWidth(value) <= width)
|
|
42
|
+
return value;
|
|
43
|
+
let last = "";
|
|
44
|
+
let used = 0;
|
|
45
|
+
for (const { segment } of graphemes.segment(value)) {
|
|
46
|
+
const segmentWidth = stringWidth(segment);
|
|
47
|
+
if (used + segmentWidth > Math.max(width - 1, 0))
|
|
48
|
+
break;
|
|
49
|
+
last += segment;
|
|
50
|
+
used += segmentWidth;
|
|
51
|
+
}
|
|
52
|
+
return `${last}…`;
|
|
53
|
+
}
|
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
|
+
}
|