@iseoane/agent-depot 0.6.13 → 0.6.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +2 -1
- package/dist/src/flow-progress.js +6 -0
- package/dist/src/git-source.js +18 -6
- package/dist/src/preview-model.js +34 -0
- package/dist/src/process-runner.js +1 -0
- package/dist/src/profile-cli.js +12 -4
- package/dist/src/profile-export.js +23 -3
- package/dist/src/skill-update.js +1 -0
- package/dist/src/source-refresh-session.js +139 -0
- package/dist/src/sources.js +4 -4
- package/dist/src/tui/adoption-actions.js +21 -2
- package/dist/src/tui/app-actions.js +54 -45
- package/dist/src/tui/app.js +22 -4
- package/dist/src/tui/bulk-actions.js +42 -9
- package/dist/src/tui/catalog-actions.js +12 -0
- package/dist/src/tui/catalog-flow.js +31 -11
- package/dist/src/tui/catalog-panel.js +15 -13
- package/dist/src/tui/catalog-tree.js +7 -2
- package/dist/src/tui/catalog-view.js +12 -6
- package/dist/src/tui/file-picker.js +2 -1
- package/dist/src/tui/installations-panel.js +12 -4
- package/dist/src/tui/installations-rows.js +1 -1
- package/dist/src/tui/installations-view.js +16 -7
- package/dist/src/tui/keys.js +27 -4
- package/dist/src/tui/manage-actions.js +55 -27
- package/dist/src/tui/manage-panel.js +15 -29
- package/dist/src/tui/mode-keys.js +11 -1
- package/dist/src/tui/package-actions.js +25 -18
- package/dist/src/tui/package-rows.js +2 -1
- package/dist/src/tui/panel-parts.js +45 -36
- package/dist/src/tui/preview-render.js +93 -0
- package/dist/src/tui/preview-window.js +141 -10
- package/dist/src/tui/profile-view.js +49 -40
- package/dist/src/tui/render.js +11 -1
- package/dist/src/tui/safe-text.js +25 -0
- package/dist/src/tui/sources-mode.js +2 -2
- package/dist/src/tui/sources-recipes.js +53 -32
- package/dist/src/tui/sources-view.js +86 -39
- package/dist/src/tui/unmanaged-actions.js +17 -5
- package/dist/src/tui/updates-flow.js +11 -5
- package/dist/src/tui/updates-keys.js +2 -2
- package/dist/src/tui/updates-model.js +5 -2
- package/dist/src/tui/updates-panel.js +10 -9
- package/dist/src/tui/updates-preview.js +27 -44
- package/dist/src/tui/updates-view.js +8 -10
- package/dist/src/tui/updates.js +117 -33
- package/dist/src/update-flow.js +31 -15
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
3
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
5
|
import { createAppOperations } from "../app-flow.js";
|
|
5
6
|
import { planAppRecipeRemoval, removeAppRecipe } from "../app-recipe-removal.js";
|
|
@@ -7,12 +8,14 @@ import { BUILT_IN_SOURCE } from "../sources.js";
|
|
|
7
8
|
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
8
9
|
import { ActionPanel } from "./catalog-panel.js";
|
|
9
10
|
import { hasUnmanagedCopy, isInstalled, loadInstalledSkills } from "./catalog-installs.js";
|
|
10
|
-
import { appName, appRecipePreviewLines,
|
|
11
|
+
import { appName, appPlanLines, appRecipePreviewLines, recipeApprovalChrome } from "./app-actions.js";
|
|
11
12
|
import { errorText } from "./batch.js";
|
|
12
13
|
import { useMounted } from "./view-state.js";
|
|
13
|
-
import { BorderPanel, DecisionPrompt,
|
|
14
|
+
import { BorderPanel, DecisionPrompt, PagedPreview, Working } from "./panel-parts.js";
|
|
15
|
+
import { isYes } from "./keys.js";
|
|
14
16
|
import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
|
|
15
|
-
import {
|
|
17
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
18
|
+
import { lineText, textLine } from "./preview-render.js";
|
|
16
19
|
import { theme } from "./theme.js";
|
|
17
20
|
import { busy } from "./working-model.js";
|
|
18
21
|
/**
|
|
@@ -34,24 +37,24 @@ export function groupRecipeRows(rows) {
|
|
|
34
37
|
function recipeMemberLine(row) {
|
|
35
38
|
return `${row.entry.recipe?.platform ?? "all platforms"} · ${row.status} · ${row.entry.file}${row.entry.applicable ? " (used here)" : ""}`;
|
|
36
39
|
}
|
|
37
|
-
function recipeApprovalChrome(entry) {
|
|
38
|
-
return {
|
|
39
|
-
title: "Recipe approval · does not install",
|
|
40
|
-
questions: [`Approve ${appName(entry)}? y/n · recipe approval does not install the App.`, "Scroll to end before y; n cancels."],
|
|
41
|
-
footerRows: 2,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
40
|
function uninstallChrome(mode) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
const name = appName(mode.removal.entry);
|
|
42
|
+
return mode.kind === "uninstall-manual"
|
|
43
|
+
? { title: "App manual uninstall", action: "Check the uninstall now", consequence: "Complete the manual uninstall, then check now? y/n" }
|
|
44
|
+
: { title: "App uninstall confirmation", action: `Uninstall ${name}`, consequence: `Uninstall ${name}? y/n` };
|
|
45
|
+
}
|
|
46
|
+
function removeChrome(removal) {
|
|
47
|
+
const name = appName(removal.entry);
|
|
48
|
+
return { title: "Recipe removal", action: `Remove the ${name} recipe`, consequence: `Remove recipe for ${name}? y/n` };
|
|
49
|
+
}
|
|
50
|
+
function removeLines(removal) {
|
|
51
|
+
return [`Delete recipe: ${removal.file}`, "Only this recipe file or symlink will be removed; App data and Host configuration are not deleted."];
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
54
|
* Recipe loading and captured actions; Sources owns the single list and cursor.
|
|
53
55
|
*/
|
|
54
|
-
export function useSourcesRecipes({ operations, environment, onCapturingChange, highlightedFile,
|
|
56
|
+
export function useSourcesRecipes({ operations, environment, onCapturingChange, highlightedFile, }) {
|
|
57
|
+
const previewGeometry = usePreviewGeometry();
|
|
55
58
|
const homeDirectory = environment?.homeDirectory;
|
|
56
59
|
const appEnvironment = environment?.appEnvironment;
|
|
57
60
|
const apps = useMemo(() => createAppOperations({
|
|
@@ -90,7 +93,9 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
90
93
|
kind: "approve",
|
|
91
94
|
entry, removeAfterApproval, cursor: 0,
|
|
92
95
|
lines: [
|
|
93
|
-
...(members.length > 1
|
|
96
|
+
...(members.length > 1
|
|
97
|
+
? [textLine(`Recipes named ${appName(entry)}:`), ...members.map(row => textLine(` ${recipeMemberLine(row)}`)), textLine(" ")]
|
|
98
|
+
: []),
|
|
94
99
|
...appRecipePreviewLines(entry, status, steps),
|
|
95
100
|
],
|
|
96
101
|
});
|
|
@@ -106,6 +111,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
106
111
|
const flow = {
|
|
107
112
|
operations,
|
|
108
113
|
env: environment ?? {},
|
|
114
|
+
previewGeometry,
|
|
109
115
|
isMounted: () => mounted.current,
|
|
110
116
|
setAction: action => setMode(action.kind === "browse"
|
|
111
117
|
? { kind: "browse" }
|
|
@@ -161,7 +167,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
161
167
|
}
|
|
162
168
|
const removal = await planAppRecipeRemoval(apps, entry);
|
|
163
169
|
if (mounted.current)
|
|
164
|
-
setMode(
|
|
170
|
+
setMode(removal.installed ? { kind: "uninstall-question", removal } : { kind: "remove", removal, cursor: 0 });
|
|
165
171
|
}
|
|
166
172
|
catch (error) {
|
|
167
173
|
if (mounted.current) {
|
|
@@ -181,7 +187,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
181
187
|
if (result.status === "manual required")
|
|
182
188
|
setMode({ ...current, kind: "uninstall-manual" });
|
|
183
189
|
else if (result.status === "untracked")
|
|
184
|
-
setMode({ kind: "remove", removal: current.removal });
|
|
190
|
+
setMode({ kind: "remove", removal: current.removal, cursor: 0 });
|
|
185
191
|
else {
|
|
186
192
|
setError(`Uninstall failed: ${result.reason ?? result.status}; recipe retained`);
|
|
187
193
|
setMode({ kind: "browse" });
|
|
@@ -229,13 +235,13 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
229
235
|
return true;
|
|
230
236
|
}
|
|
231
237
|
if (current.kind === "approve") {
|
|
232
|
-
const viewport =
|
|
238
|
+
const viewport = previewGeometry(current.lines.map(lineText), current.cursor, recipeApprovalChrome(current.entry));
|
|
233
239
|
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
234
240
|
if (cursor !== undefined)
|
|
235
241
|
setMode({ ...current, cursor });
|
|
236
242
|
else if (input === "n" || key.escape)
|
|
237
243
|
cancel();
|
|
238
|
-
else if (input
|
|
244
|
+
else if (isYes(input, key) && viewport.reachedEnd) {
|
|
239
245
|
setBusy("Approving App recipe…");
|
|
240
246
|
void apps.approve(current.entry).then(async () => {
|
|
241
247
|
await reload();
|
|
@@ -268,18 +274,26 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
268
274
|
return true;
|
|
269
275
|
}
|
|
270
276
|
if (current.kind === "uninstall-confirm" || current.kind === "uninstall-manual") {
|
|
271
|
-
const lines =
|
|
272
|
-
const viewport =
|
|
277
|
+
const lines = appPlanLines(current.plan).map(lineText);
|
|
278
|
+
const viewport = previewGeometry(lines, current.cursor, uninstallChrome(current));
|
|
273
279
|
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
274
280
|
if (cursor !== undefined)
|
|
275
281
|
setMode({ ...current, cursor });
|
|
276
282
|
else if (input === "n" || key.escape)
|
|
277
283
|
cancel();
|
|
278
|
-
else if (input
|
|
284
|
+
else if (isYes(input, key) && viewport.reachedEnd)
|
|
279
285
|
void uninstall(current);
|
|
280
286
|
return true;
|
|
281
287
|
}
|
|
282
288
|
if (current.kind === "remove") {
|
|
289
|
+
const viewport = previewGeometry(removeLines(current.removal), current.cursor, removeChrome(current.removal));
|
|
290
|
+
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
291
|
+
if (cursor !== undefined) {
|
|
292
|
+
setMode({ ...current, cursor });
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
if (isYes(input, key) && !viewport.reachedEnd)
|
|
296
|
+
return true;
|
|
283
297
|
answerYesNo(input, key, () => {
|
|
284
298
|
setBusy("Removing App recipe…");
|
|
285
299
|
void removeAppRecipe(apps, current.removal, true).then(async () => {
|
|
@@ -344,15 +358,22 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
344
358
|
}
|
|
345
359
|
}, [apps, mounted]);
|
|
346
360
|
useEffect(() => { void reload(); }, [reload]);
|
|
347
|
-
|
|
361
|
+
// A paged preview sizes itself to the whole terminal, so no message row may sit above it.
|
|
362
|
+
const pagedPreview = (mode.kind === "install" && mode.action.kind === "confirm-install")
|
|
363
|
+
|| ["approve", "remove", "uninstall-confirm", "uninstall-manual"].includes(mode.kind);
|
|
364
|
+
const panel = (_jsxs(Box, { flexDirection: "column", children: [loadError && !pagedPreview ? _jsxs(Text, { color: theme.error, children: ["Error loading App recipes: ", loadError] }) : null, error && !pagedPreview ? _jsx(Text, { color: theme.error, children: error }) : null, notice && !pagedPreview ? _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(Working, { label: mode.label, since: mode.startedAt }) }) : null, mode.kind === "approve" ? (() => {
|
|
348
365
|
const chrome = recipeApprovalChrome(mode.entry);
|
|
349
|
-
const
|
|
350
|
-
return _jsx(
|
|
366
|
+
const lines = mode.lines.map(lineText);
|
|
367
|
+
return _jsx(PagedPreview, { chrome: chrome, lines: lines, styled: mode.lines, viewport: previewGeometry(lines, mode.cursor, chrome) });
|
|
351
368
|
})() : null, mode.kind === "pick" ? (_jsxs(BorderPanel, { title: "Remove which recipe?", children: [mode.members.map((row, index) => _jsxs(Text, { children: [index + 1, " ", recipeMemberLine(row)] }, row.entry.file)), _jsx(Text, { color: theme.legend, children: "Press a number to review that one file for removal \u00B7 n or Esc cancels" })] })) : 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" ? (() => {
|
|
352
369
|
const chrome = uninstallChrome(mode);
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
return _jsx(
|
|
356
|
-
})() : null, mode.kind === "remove" ? (
|
|
370
|
+
const styled = appPlanLines(mode.plan);
|
|
371
|
+
const lines = styled.map(lineText);
|
|
372
|
+
return _jsx(PagedPreview, { chrome: chrome, lines: lines, styled: styled, viewport: previewGeometry(lines, mode.cursor, chrome) });
|
|
373
|
+
})() : null, mode.kind === "remove" ? (() => {
|
|
374
|
+
const chrome = removeChrome(mode.removal);
|
|
375
|
+
const lines = removeLines(mode.removal);
|
|
376
|
+
return _jsx(PagedPreview, { chrome: chrome, lines: lines, viewport: previewGeometry(lines, mode.cursor, chrome) });
|
|
377
|
+
})() : null] }));
|
|
357
378
|
return { rows, loadingSince, handleKey, startAdd, reload, clearMessage: () => { setError(undefined); setNotice(undefined); }, panel, directory: apps.directory, capturing: mode.kind !== "browse" };
|
|
358
379
|
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box,
|
|
2
|
+
import { Box, useWindowSize } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
5
|
+
import { useCallback, useEffect, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
5
6
|
import { initialMode, modeReducer } from "./sources-mode.js";
|
|
6
7
|
import { rowStyle, theme } from "./theme.js";
|
|
7
8
|
import { computeWindow, pageStep, useListHeight } from "./window.js";
|
|
8
|
-
import { useKeys } from "./keys.js";
|
|
9
|
+
import { isYes, useKeys } from "./keys.js";
|
|
9
10
|
import { groupRecipeRows, useSourcesRecipes } from "./sources-recipes.js";
|
|
10
|
-
import { BorderPanel,
|
|
11
|
-
import {
|
|
11
|
+
import { BorderPanel, PagedPreview, Working } from "./panel-parts.js";
|
|
12
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
12
13
|
import { movePreviewCursor } from "./mode-keys.js";
|
|
14
|
+
import { formatAgo } from "./updates.js";
|
|
15
|
+
import { animationEnabled, formatElapsed } from "./working-model.js";
|
|
13
16
|
/** Footer keys for the selected row; App seeds the footer before the view reports a selection. */
|
|
14
17
|
export function sourceRowHints(row) {
|
|
15
18
|
if (row === undefined)
|
|
@@ -29,7 +32,7 @@ async function refreshEach(operations, targets) {
|
|
|
29
32
|
let failed = false;
|
|
30
33
|
for (const source of targets) {
|
|
31
34
|
try {
|
|
32
|
-
lines.push(`Refreshed Git Source: ${(await operations.refreshSource(source.id)).id}`);
|
|
35
|
+
lines.push(`Refreshed Git Source: ${(await operations.refreshSource(source.id, { force: true })).id}`);
|
|
33
36
|
}
|
|
34
37
|
catch (error) {
|
|
35
38
|
failed = true;
|
|
@@ -41,19 +44,67 @@ async function refreshEach(operations, targets) {
|
|
|
41
44
|
function errorText(error) {
|
|
42
45
|
return error instanceof Error ? error.message : String(error);
|
|
43
46
|
}
|
|
47
|
+
const NO_REFRESHES = new Map();
|
|
48
|
+
const NO_SUBSCRIPTION = () => () => undefined;
|
|
49
|
+
/** Re-renders on every session change and, when animation is on, every second so elapsed times move. */
|
|
50
|
+
function useRefreshStates(session) {
|
|
51
|
+
const states = useSyncExternalStore(session?.subscribe ?? NO_SUBSCRIPTION, session?.snapshot ?? (() => NO_REFRESHES));
|
|
52
|
+
const [, setTick] = useState(0);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (session === undefined || !animationEnabled())
|
|
55
|
+
return undefined;
|
|
56
|
+
const timer = setInterval(() => setTick((value) => value + 1), 1000);
|
|
57
|
+
// A redraw timer alone must never keep the process alive.
|
|
58
|
+
timer.unref();
|
|
59
|
+
return () => clearInterval(timer);
|
|
60
|
+
}, [session]);
|
|
61
|
+
return states;
|
|
62
|
+
}
|
|
63
|
+
/** The mark is text first; color only supports it. */
|
|
64
|
+
function RefreshMark({ state, now }) {
|
|
65
|
+
switch (state?.status) {
|
|
66
|
+
case undefined:
|
|
67
|
+
case "not-refreshed":
|
|
68
|
+
return _jsx(Text, { color: theme.muted, children: "\u00B7 not refreshed" });
|
|
69
|
+
case "refreshing":
|
|
70
|
+
return _jsxs(Text, { color: theme.warning, children: ["\u2026 refreshing ", formatElapsed(now - (state.startedAt ?? now))] });
|
|
71
|
+
case "refreshed":
|
|
72
|
+
return _jsxs(Text, { color: theme.success, children: ["\u2713 refreshed ", formatAgo(now - (state.finishedAt ?? now))] });
|
|
73
|
+
case "failed":
|
|
74
|
+
return _jsx(Text, { color: theme.error, children: "\u2717 refresh failed" });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
44
77
|
function refreshPreviewLines(sources) {
|
|
45
78
|
return sources.map(source => `Source: refresh Git Source ${source.id} from ${source.url}`);
|
|
46
79
|
}
|
|
47
|
-
function
|
|
48
|
-
return
|
|
80
|
+
function refreshChrome(count) {
|
|
81
|
+
return {
|
|
49
82
|
title: "Refresh Git Sources",
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
83
|
+
action: `Refresh ${count} ${count === 1 ? "Source" : "Sources"}`,
|
|
84
|
+
consequence: "Refresh these Sources? y/n",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function removeSourcePreviewLines(source) {
|
|
88
|
+
return [`Source: remove Git Source ${source.id} from ${source.url}`];
|
|
53
89
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
const REMOVE_SOURCE_CHROME = {
|
|
91
|
+
title: "Remove Git Source",
|
|
92
|
+
action: "Remove Git Source",
|
|
93
|
+
consequence: "Remove this Source? y/n",
|
|
94
|
+
};
|
|
95
|
+
/** The lines and chrome of the open confirmation preview, shared by key handling and rendering. */
|
|
96
|
+
function confirmPreview(mode) {
|
|
97
|
+
return mode.action === "refresh"
|
|
98
|
+
? { lines: refreshPreviewLines(mode.sources), chrome: refreshChrome(mode.sources.length) }
|
|
99
|
+
: { lines: removeSourcePreviewLines(mode.source), chrome: REMOVE_SOURCE_CHROME };
|
|
100
|
+
}
|
|
101
|
+
export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight, environment, onHintsChange, refreshSession }) {
|
|
102
|
+
const refreshStates = useRefreshStates(refreshSession);
|
|
103
|
+
const { rows: terminalRows } = useWindowSize();
|
|
104
|
+
const previewGeometry = usePreviewGeometry();
|
|
105
|
+
// With a refresh session, one more row is kept for the selected Source's refresh error; below 24 rows
|
|
106
|
+
// the list is already at its minimum, so that line is left out rather than pushing the footer away.
|
|
107
|
+
const height = useListHeight(listHeight, refreshSession === undefined ? 9 : 10);
|
|
57
108
|
const [state, setState] = useState(() => ({ status: "loading", startedAt: Date.now() }));
|
|
58
109
|
// The highlight follows the source id; the index is only the fallback once that source is gone.
|
|
59
110
|
// Selection and marks are mirrored in refs updated first, so keys delivered in one burst act on the latest state.
|
|
@@ -126,8 +177,6 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
126
177
|
const row = rows[resolveIndex(rows, selectionRef.current)];
|
|
127
178
|
return row?.kind === "app" ? row.group.primary.entry.file : undefined;
|
|
128
179
|
},
|
|
129
|
-
terminalRows: terminalRows || 24,
|
|
130
|
-
terminalColumns: columns || 80,
|
|
131
180
|
});
|
|
132
181
|
const rows = [
|
|
133
182
|
...sources.map((source) => ({ id: source.id, kind: "skills", source })),
|
|
@@ -136,8 +185,8 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
136
185
|
rowsRef.current = rows;
|
|
137
186
|
const selected = resolveIndex(rows, selection);
|
|
138
187
|
const selectedRow = rows[selected];
|
|
139
|
-
const hints = mode.kind === "confirm"
|
|
140
|
-
?
|
|
188
|
+
const hints = mode.kind === "confirm"
|
|
189
|
+
? `↑↓/Pg page · y ${mode.action} · n cancel`
|
|
141
190
|
: sourceRowHints(selectedRow);
|
|
142
191
|
useEffect(() => { onHintsChange?.(hints); }, [hints, onHintsChange]);
|
|
143
192
|
const select = (index) => {
|
|
@@ -240,17 +289,17 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
240
289
|
return;
|
|
241
290
|
}
|
|
242
291
|
if (mode.kind === "confirm") {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
if (input === "y" && !viewport.reachedEnd)
|
|
251
|
-
return;
|
|
292
|
+
const { lines, chrome } = confirmPreview(mode);
|
|
293
|
+
const viewport = previewGeometry(lines, mode.cursor, chrome);
|
|
294
|
+
const cursor = movePreviewCursor(mode.cursor, viewport.total, input, key, viewport.height);
|
|
295
|
+
if (cursor !== undefined) {
|
|
296
|
+
dispatch({ type: "preview-cursor", cursor });
|
|
297
|
+
return;
|
|
252
298
|
}
|
|
253
|
-
|
|
299
|
+
const yes = isYes(input, key);
|
|
300
|
+
if (yes && !viewport.reachedEnd)
|
|
301
|
+
return;
|
|
302
|
+
if (yes) {
|
|
254
303
|
if (mode.action === "refresh") {
|
|
255
304
|
const { sources: targets } = mode;
|
|
256
305
|
void run(targets.length === 1 ? "Refreshing Git Source…" : "Refreshing Git Sources…", async () => {
|
|
@@ -329,17 +378,19 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
329
378
|
if (state.status === "error")
|
|
330
379
|
return _jsx(BorderPanel, { title: "Source error", children: _jsx(Text, { color: theme.error, children: state.message }) });
|
|
331
380
|
const window = computeWindow(rows.length, selected, height);
|
|
381
|
+
const now = refreshSession?.now() ?? 0;
|
|
382
|
+
const selectedRefresh = selectedRow?.kind === "skills" ? refreshStates.get(selectedRow.source.id) : undefined;
|
|
332
383
|
if (mode.kind === "confirm")
|
|
333
|
-
return _jsx(Prompt, { mode: mode,
|
|
384
|
+
return _jsx(Prompt, { mode: mode, previewGeometry: previewGeometry });
|
|
334
385
|
if (recipes.capturing)
|
|
335
386
|
return _jsx(Box, { flexDirection: "column", children: recipes.panel });
|
|
336
387
|
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 && recipes.loadingSince === undefined ? _jsx(Text, { children: "No sources or App recipes \u00B7 n to add" }) : null, rows.slice(window.start, window.end).map((row, offset) => {
|
|
337
388
|
const index = window.start + offset;
|
|
338
389
|
const id = row.kind === "skills" ? row.source.id : row.group.primary.entry.recipe?.name ?? row.group.primary.entry.file.split(/[\\/]/u).pop();
|
|
339
390
|
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"
|
|
340
|
-
? _jsx(Text, { color: theme.value, children: row.source.url })
|
|
391
|
+
? _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: row.source.url }), refreshSession === undefined ? null : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsx(RefreshMark, { state: refreshStates.get(row.source.id), now: now })] })] })
|
|
341
392
|
: _jsx(RecipeStatus, { group: row.group })] }, row.id));
|
|
342
|
-
}), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : null, recipes.loadingSince !== undefined && mode.kind !== "busy" && !recipes.capturing
|
|
393
|
+
}), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedRefresh?.status === "failed" && (terminalRows || 24) >= 24 ? _jsxs(Text, { color: theme.error, wrap: "truncate-end", children: ["Refresh failed: ", selectedRefresh.error] }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : null, recipes.loadingSince !== undefined && mode.kind !== "busy" && !recipes.capturing
|
|
343
394
|
? _jsx(Working, { label: "Loading App recipes\u2026", since: recipes.loadingSince }) : 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] }));
|
|
344
395
|
}
|
|
345
396
|
/** Status of the recipe that applies here, led by its platform when it names one; other platforms follow, muted. */
|
|
@@ -348,20 +399,16 @@ function RecipeStatus({ group }) {
|
|
|
348
399
|
const platform = primary.entry.recipe?.platform;
|
|
349
400
|
return (_jsxs(_Fragment, { children: [platform === undefined ? null : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: platform }), _jsx(Text, { color: theme.legend, children: " \u00B7 " })] }), primary.entry.error === undefined || primary.entry.recipe === undefined ? null : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: path.basename(primary.entry.file) }), _jsx(Text, { color: theme.legend, children: " \u00B7 " })] }), _jsx(Text, { color: primary.status === "approved" ? theme.success : theme.warning, children: primary.status === "approved" ? "✓ approved" : primary.status }), members.slice(1).map(other => (_jsxs(Text, { color: theme.muted, children: [" ", other.entry.recipe?.platform ?? "all platforms", " (other platform)"] }, other.entry.file)))] }));
|
|
350
401
|
}
|
|
351
|
-
function Prompt({ mode,
|
|
402
|
+
function Prompt({ mode, previewGeometry }) {
|
|
352
403
|
if (mode.kind === "add-kind")
|
|
353
404
|
return _jsx(Text, { children: "Add: 1 Git Source (skills + Packages) \u00B7 2 App recipe (Esc cancel)" });
|
|
354
405
|
if (mode.kind === "input")
|
|
355
406
|
return _jsxs(Text, { children: ["URL: ", mode.value] });
|
|
356
407
|
if (mode.kind === "busy")
|
|
357
408
|
return _jsx(BorderPanel, { title: "Sources", children: _jsx(Working, { label: mode.label, since: mode.startedAt }) });
|
|
358
|
-
if (mode.kind === "confirm") {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
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
|
|
362
|
-
? _jsx(DecisionPrompt, { children: "Refresh these Sources?" })
|
|
363
|
-
: _jsx(Text, { color: theme.legend, children: "Scroll to end; n cancels." })] }));
|
|
364
|
-
})() : (_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?" })] }));
|
|
409
|
+
if (mode.kind === "confirm" && previewGeometry !== undefined) {
|
|
410
|
+
const { lines, chrome } = confirmPreview(mode);
|
|
411
|
+
return _jsx(PagedPreview, { chrome: chrome, lines: lines, viewport: previewGeometry(lines, mode.cursor, chrome) });
|
|
365
412
|
}
|
|
366
413
|
return null;
|
|
367
414
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { quoteText } from "../preview-model.js";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { describeUnmanagedRemoval, describeUnmanagedSymlinkRemoval, removeInspectedUnmanagedSkill, removeInspectedUnmanagedSymlink, } from "../unmanaged-removal.js";
|
|
4
5
|
import { inspectUserGlobalSkillRemoval, inspectUserGlobalSymlinkRemoval, } from "../user-global-skill-inventory.js";
|
|
5
|
-
import {
|
|
6
|
+
import { answerPaged, moveCursor } from "./mode-keys.js";
|
|
6
7
|
import { homeOf } from "./installations.js";
|
|
7
8
|
import { busy } from "./working-model.js";
|
|
8
9
|
export const UNMANAGED_KINDS = ["unmanaged-scope", "unmanaged-pick", "confirm-unmanaged"];
|
|
@@ -52,7 +53,7 @@ export async function prepareUnmanagedRemoval(operations, environment, group, lo
|
|
|
52
53
|
if (planned.has(link.path))
|
|
53
54
|
continue;
|
|
54
55
|
planned.set(link.path, link);
|
|
55
|
-
preview.push(`also removes link ${
|
|
56
|
+
preview.push(`also removes link ${quoteText(link.path)} → ${quoteText(chosen.path)} (it would dangle)`);
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
for (const location of linksFirst([...planned.values()])) {
|
|
@@ -105,7 +106,7 @@ async function startPreview(context, group, locations) {
|
|
|
105
106
|
const prepared = await prepareUnmanagedRemoval(context.operations, context.env, group, locations);
|
|
106
107
|
if (!context.isMounted())
|
|
107
108
|
return;
|
|
108
|
-
context.setMode({ kind: "confirm-unmanaged", group, prepared });
|
|
109
|
+
context.setMode({ kind: "confirm-unmanaged", group, prepared, cursor: 0 });
|
|
109
110
|
}
|
|
110
111
|
catch (error) {
|
|
111
112
|
if (!context.isMounted())
|
|
@@ -125,6 +126,11 @@ function cancel(context) {
|
|
|
125
126
|
context.setMessage({ kind: "ok", text: "Uninstall cancelled" });
|
|
126
127
|
context.browse();
|
|
127
128
|
}
|
|
129
|
+
/** What the confirmation shows, so the key handler and the panel page the same rows. */
|
|
130
|
+
export function unmanagedConfirmPreview(mode) {
|
|
131
|
+
const { name } = mode.group;
|
|
132
|
+
return { lines: mode.prepared.preview, chrome: { title: "Remove unmanaged skill", action: `Remove ${name}`, consequence: `Remove ${name}? y/n` } };
|
|
133
|
+
}
|
|
128
134
|
async function execute(context, group, prepared) {
|
|
129
135
|
context.setMode(busy(`Removing ${group.name}…`));
|
|
130
136
|
await context.finish(await runUnmanagedRemoval(prepared, context.operations, context.env));
|
|
@@ -170,9 +176,15 @@ export function handleUnmanagedKey(context, mode, input, key) {
|
|
|
170
176
|
}
|
|
171
177
|
return true;
|
|
172
178
|
}
|
|
173
|
-
case "confirm-unmanaged":
|
|
174
|
-
|
|
179
|
+
case "confirm-unmanaged": {
|
|
180
|
+
const { lines, chrome } = unmanagedConfirmPreview(current);
|
|
181
|
+
answerPaged(context.previewGeometry(lines, current.cursor, chrome), current.cursor, input, key, {
|
|
182
|
+
moveTo: (cursor) => context.setMode({ ...current, cursor }),
|
|
183
|
+
yes: () => void execute(context, current.group, current.prepared),
|
|
184
|
+
no: () => cancel(context),
|
|
185
|
+
});
|
|
175
186
|
return true;
|
|
187
|
+
}
|
|
176
188
|
default:
|
|
177
189
|
return false;
|
|
178
190
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { formatProgressLabel } from "../flow-progress.js";
|
|
2
3
|
import { errorText } from "./batch.js";
|
|
3
4
|
import { BROWSE } from "./updates-mode.js";
|
|
4
5
|
import { CHECKING, emptyData, PHASE_LABEL, REFRESHING } from "./updates-model.js";
|
|
@@ -12,7 +13,7 @@ export function useUpdatesFlow(inputs) {
|
|
|
12
13
|
useEffect(() => () => {
|
|
13
14
|
pendingAnswer.current?.(false);
|
|
14
15
|
}, []);
|
|
15
|
-
const load = useCallback(async (refresh) => {
|
|
16
|
+
const load = useCallback(async (refresh, force = false) => {
|
|
16
17
|
const token = ++loadToken.current;
|
|
17
18
|
// The check after an applied update continues that update's clock; a check from browsing starts one.
|
|
18
19
|
const running = busy(refresh ? REFRESHING : CHECKING, modeRef.current, clock);
|
|
@@ -26,7 +27,7 @@ export function useUpdatesFlow(inputs) {
|
|
|
26
27
|
setMode(running);
|
|
27
28
|
let next;
|
|
28
29
|
try {
|
|
29
|
-
next = { status: "ready", data: await loadUpdateRows(operations, env, { refresh, onPhase: phase, now: clock }) };
|
|
30
|
+
next = { status: "ready", data: await loadUpdateRows(operations, env, { refresh, force, onPhase: phase, now: clock }) };
|
|
30
31
|
}
|
|
31
32
|
catch (error) {
|
|
32
33
|
// The scopes are read separately, so this is unexpected; show it as the check's result.
|
|
@@ -62,9 +63,14 @@ export function useUpdatesFlow(inputs) {
|
|
|
62
63
|
}
|
|
63
64
|
};
|
|
64
65
|
const apply = async (prepared, confirmedPaths) => {
|
|
65
|
-
|
|
66
|
+
const running = busy("Applying updates…", undefined, clock);
|
|
67
|
+
setMode(running);
|
|
66
68
|
let result;
|
|
67
69
|
try {
|
|
70
|
+
const progress = (event) => {
|
|
71
|
+
if (mounted.current)
|
|
72
|
+
setMode(busy(formatProgressLabel(event), running, clock));
|
|
73
|
+
};
|
|
68
74
|
const outcome = await runUpdates(prepared, confirmedPaths, (plan, reason) => new Promise(resolve => {
|
|
69
75
|
if (!mounted.current) {
|
|
70
76
|
resolve(false);
|
|
@@ -75,8 +81,8 @@ export function useUpdatesFlow(inputs) {
|
|
|
75
81
|
resolve(done);
|
|
76
82
|
};
|
|
77
83
|
pendingAnswer.current = answer;
|
|
78
|
-
setMode({ kind: "manual", plan, reason, answer, cursor: 0 });
|
|
79
|
-
}));
|
|
84
|
+
setMode({ kind: "manual", plan, reason, answer, cursor: 0, startedAt: running.startedAt });
|
|
85
|
+
}), progress);
|
|
80
86
|
result = { kind: outcome.failed === 0 ? "ok" : "error", lines: outcome.lines };
|
|
81
87
|
}
|
|
82
88
|
catch (error) {
|
|
@@ -120,10 +120,10 @@ export function handleUpdatesKey(context, mode, input, key) {
|
|
|
120
120
|
answerYesNo(input, key, () => {
|
|
121
121
|
if (!context.previewAtEnd(mode))
|
|
122
122
|
return;
|
|
123
|
-
context.setMode(busy("Checking App version…",
|
|
123
|
+
context.setMode(busy("Checking App version…", mode, context.now));
|
|
124
124
|
mode.answer(true);
|
|
125
125
|
}, () => {
|
|
126
|
-
context.setMode(busy("Continuing updates…",
|
|
126
|
+
context.setMode(busy("Continuing updates…", mode, context.now));
|
|
127
127
|
mode.answer(false);
|
|
128
128
|
});
|
|
129
129
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildUpdateTree } from "./updates-tree.js";
|
|
2
2
|
import { flattenVisible } from "./tree.js";
|
|
3
|
+
import { formatAgo } from "./updates.js";
|
|
3
4
|
export const REFRESHING = "Refreshing sources…";
|
|
4
5
|
export const CHECKING = "Checking for updates…";
|
|
5
6
|
export const PHASE_LABEL = { refreshing: REFRESHING, checking: CHECKING };
|
|
@@ -40,7 +41,7 @@ export function viewOf(data, scope, showUnknown, expanded) {
|
|
|
40
41
|
return { shown, updatable, unknown, upToDate, entries };
|
|
41
42
|
}
|
|
42
43
|
export function describeRefresh(data) {
|
|
43
|
-
const { attempted, refreshed, failed, unreadable } = data.refresh;
|
|
44
|
+
const { attempted, refreshed, failed, unreadable, reusedAt } = data.refresh;
|
|
44
45
|
if (!attempted)
|
|
45
46
|
return "sources not fetched again";
|
|
46
47
|
if (unreadable.length > 0) {
|
|
@@ -49,7 +50,9 @@ export function describeRefresh(data) {
|
|
|
49
50
|
if (failed.length > 0) {
|
|
50
51
|
return `${failed.length} of ${failed.length + refreshed.length} sources could not be refreshed; their current data was used`;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
if (refreshed.length === 0)
|
|
54
|
+
return "no Git sources to refresh";
|
|
55
|
+
return reusedAt === undefined ? "sources refreshed" : `sources refreshed ${formatAgo(data.checkedAt - reusedAt)}`;
|
|
53
56
|
}
|
|
54
57
|
/** Data for a check that failed as a whole, so the view can show the reason. */
|
|
55
58
|
export function emptyData(reason, checkedAt) {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
3
4
|
import { rowStyle, theme } from "./theme.js";
|
|
4
|
-
import { BorderPanel, FieldLine,
|
|
5
|
-
import {
|
|
5
|
+
import { BorderPanel, FieldLine, PagedPreview } from "./panel-parts.js";
|
|
6
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
6
7
|
import { describeRefresh, describeUnknownReasons } from "./updates-model.js";
|
|
7
|
-
import { formatAgo
|
|
8
|
+
import { formatAgo } from "./updates.js";
|
|
9
|
+
import { lineText } from "./preview-render.js";
|
|
8
10
|
import { groupCheckState, packageLeafLabel } from "./updates-tree.js";
|
|
9
|
-
import { updateModePreviewChrome, updateModePreviewLines
|
|
11
|
+
import { updateModePreviewChrome, updateModePreviewLines } from "./updates-preview.js";
|
|
10
12
|
function updateResultField(line, defaultOk) {
|
|
11
13
|
if (line.startsWith("Update summary: ")) {
|
|
12
14
|
const successful = /^Update summary: \d+ updated, 0 failed$/.test(line);
|
|
@@ -54,11 +56,10 @@ export function UpdatesFooter({ indicator, upToDate, updatableCount, checkedCoun
|
|
|
54
56
|
}) })) : null] }));
|
|
55
57
|
}
|
|
56
58
|
function CapturedUpdatePreview({ mode }) {
|
|
57
|
-
const
|
|
58
|
-
const lines =
|
|
59
|
+
const styled = updateModePreviewLines(mode);
|
|
60
|
+
const lines = styled.map(lineText);
|
|
59
61
|
const chrome = updateModePreviewChrome(mode);
|
|
60
|
-
|
|
61
|
-
return (_jsx(PreviewConfirm, { title: chrome.title, lines: lines, cursor: mode.cursor, question: updateModeQuestion(mode, viewport.reachedEnd), chrome: chrome }));
|
|
62
|
+
return _jsx(PagedPreview, { chrome: chrome, lines: lines, styled: styled, viewport: usePreviewGeometry()(lines, mode.cursor, chrome) });
|
|
62
63
|
}
|
|
63
64
|
export function ModePanel({ mode }) {
|
|
64
65
|
switch (mode.kind) {
|