@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const TABS = [
|
|
2
|
+
["sources", "[1] Sources"],
|
|
3
|
+
["catalog", "[2] Catalog"],
|
|
4
|
+
["installations", "[3] Installations"],
|
|
5
|
+
["updates", "[4] Updates"],
|
|
6
|
+
["profile", "[5] Profile"],
|
|
7
|
+
];
|
|
8
|
+
export function headerTabRows(width) {
|
|
9
|
+
const rows = [];
|
|
10
|
+
let row = [];
|
|
11
|
+
let rowWidth = 0;
|
|
12
|
+
for (const tab of TABS) {
|
|
13
|
+
const separatorWidth = row.length === 0 ? 0 : 2;
|
|
14
|
+
// Each rendered row has at most one active marker, regardless of tab count.
|
|
15
|
+
const nextWidth = rowWidth + separatorWidth + tab[1].length;
|
|
16
|
+
if (row.length > 0 && nextWidth + 2 > width) {
|
|
17
|
+
rows.push(row);
|
|
18
|
+
row = [];
|
|
19
|
+
rowWidth = 0;
|
|
20
|
+
}
|
|
21
|
+
rowWidth += (row.length === 0 ? 0 : 2) + tab[1].length;
|
|
22
|
+
row.push(tab);
|
|
23
|
+
}
|
|
24
|
+
if (row.length > 0)
|
|
25
|
+
rows.push(row);
|
|
26
|
+
return rows;
|
|
27
|
+
}
|
|
28
|
+
export function headerTabRowCount(width) {
|
|
29
|
+
return headerTabRows(width).length;
|
|
30
|
+
}
|
|
31
|
+
export function formatHeaderTabs(view, width) {
|
|
32
|
+
return headerTabRows(width).map(row => row.map(([name, label]) => `${view === name ? "▶ " : ""}${label}`).join(" ")).join("\n");
|
|
33
|
+
}
|
|
@@ -2,7 +2,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { HOST_CHOICES } from "./catalog-actions.js";
|
|
4
4
|
import { ManagePanel } from "./manage-panel.js";
|
|
5
|
-
import { HostChecklist, PreviewLines } from "./panel-parts.js";
|
|
5
|
+
import { DecisionPrompt, HostChecklist, PreviewLines } from "./panel-parts.js";
|
|
6
6
|
import { theme } from "./theme.js";
|
|
7
7
|
/** The prompt of the adoption and manage modes; browsing shows nothing. */
|
|
8
8
|
export function ModePanel({ mode }) {
|
|
@@ -18,9 +18,9 @@ export function ModePanel({ mode }) {
|
|
|
18
18
|
case "fixed":
|
|
19
19
|
return _jsxs(Text, { children: ["Fixed version: ", mode.value, "_ (Enter confirm, Esc cancel)"] });
|
|
20
20
|
case "confirm":
|
|
21
|
-
return mode.adoption.verdict.adoptable ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(PreviewLines, { lines: mode.adoption.prepared.preview }), mode.adoption.prepared.runsExternalCommand ? _jsx(Text, { color: theme.warning, children: "This install runs an external command (see above)." }) : null, _jsxs(
|
|
21
|
+
return mode.adoption.verdict.adoptable ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(PreviewLines, { lines: mode.adoption.prepared.preview }), mode.adoption.prepared.runsExternalCommand ? _jsx(Text, { color: theme.warning, children: "This install runs an external command (see above)." }) : null, _jsxs(DecisionPrompt, { children: ["Adopt ", mode.entry.name, "?"] })] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.error, children: mode.adoption.verdict.reason }), _jsx(Text, { children: "Enter/Esc to go back" })] }));
|
|
22
22
|
case "confirm-exposure":
|
|
23
|
-
return (_jsxs(Text, { color: theme.warning, children: ["Adopting ", mode.entry.name, " adds the missing Host location and exposes it to more hosts; this needs separate confirmation (CLI: --confirm-additional-host). Confirm?
|
|
23
|
+
return (_jsxs(Text, { color: theme.warning, children: ["Adopting ", mode.entry.name, " adds the missing Host location and exposes it to more hosts; this needs separate confirmation (CLI: --confirm-additional-host). ", _jsx(DecisionPrompt, { children: "Confirm?" })] }));
|
|
24
24
|
default:
|
|
25
25
|
return _jsx(ManagePanel, { mode: mode });
|
|
26
26
|
}
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Text } from "ink";
|
|
3
|
-
import {
|
|
4
|
-
import { describePackageRow } from "./package-rows.js";
|
|
3
|
+
import { describeLinks, describeNode, markersOf } from "./installations-tree.js";
|
|
5
4
|
import { rowStyle, theme } from "./theme.js";
|
|
5
|
+
function AppDetails({ row }) {
|
|
6
|
+
const version = row.inspection.installedVersion;
|
|
7
|
+
const statusColor = row.inspection.status === "invalid" ? theme.error : theme.warning;
|
|
8
|
+
const invalid = row.inspection.status === "invalid" ? ` · invalid recipe: ${row.inspection.reason ?? "unknown error"}` : "";
|
|
9
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: version === undefined ? statusColor : theme.version, children: version ?? row.inspection.status }), row.inspection.evidence === "recorded" ? _jsx(Text, { color: theme.value, children: " (recorded)" }) : null, invalid === "" ? null : _jsx(Text, { color: theme.error, children: invalid })] }));
|
|
10
|
+
}
|
|
11
|
+
function PackageDetails({ row }) {
|
|
12
|
+
const version = row.installedVersion;
|
|
13
|
+
const installedPrefix = version === undefined ? "" : row.installed.startsWith("installed at ") ? "installed at " : "installed ";
|
|
14
|
+
const installedSuffix = version === undefined ? "" : row.installed.slice(installedPrefix.length + version.length);
|
|
15
|
+
const status = row.status.kind === "unknown" && row.status.reason === "local changes"
|
|
16
|
+
? "update not checked · local changes"
|
|
17
|
+
: row.status.kind === "unknown" ? "cannot assess" : row.status.kind === "current" ? "up to date" : "update available";
|
|
18
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsxs(Text, { color: theme.value, children: [row.selection.host, " "] }), version === undefined ? _jsx(Text, { color: theme.warning, children: row.installed }) : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: installedPrefix }), _jsx(Text, { color: theme.version, children: version }), _jsx(Text, { color: theme.value, children: installedSuffix })] }), _jsx(Text, { color: theme.value, children: " available " }), _jsx(Text, { color: row.available === "unknown" ? theme.warning : theme.version, children: row.available }), _jsxs(Text, { color: theme.warning, children: [" ", status] }), row.drift === undefined ? null : _jsxs(Text, { color: theme.warning, children: [" drift: ", row.drift] })] }));
|
|
19
|
+
}
|
|
6
20
|
/** One line of the Installations tree: cursor, indent, mark box, label and the leaf's details. */
|
|
7
21
|
export function TreeRowLine({ row, selected, marked }) {
|
|
8
22
|
const item = row.node.data;
|
|
9
|
-
const
|
|
23
|
+
const markable = item.kind !== "group" && item.kind !== "package";
|
|
24
|
+
const leaf = !row.expandable && markable;
|
|
10
25
|
const prefix = `${selected ? "> " : " "}${" ".repeat(row.depth)}${leaf ? (marked ? "[x] " : "[ ] ") : ""}`;
|
|
11
|
-
return (_jsxs(Text, { ...rowStyle(selected), color: item.kind === "group" ? theme.group : undefined, children: [prefix, describeNode(row), item.kind === "installation" ? (_jsxs(Text, { color: theme.
|
|
26
|
+
return (_jsxs(Text, { ...rowStyle(selected), color: item.kind === "group" ? theme.group : undefined, children: [prefix, item.kind === "app" ? item.row.entry.recipe?.name ?? item.row.name ?? item.row.entry.file : describeNode(row), item.kind === "installation" ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: item.row.installed === "unknown" ? theme.warning : theme.version, children: item.row.installed }), _jsxs(Text, { color: theme.field, children: [" (", item.row.policy, ") [", item.row.selection.hosts.join(", "), "]"] }), markersOf(item.row) === "" ? null : _jsxs(Text, { color: theme.marker, children: [" ", markersOf(item.row)] })] })) : null, item.kind === "app" ? _jsx(AppDetails, { row: item.row }) : null, item.kind === "package" ? _jsx(PackageDetails, { row: item.row }) : null, item.kind === "unmanaged" && describeLinks(item.group) !== "" ? (_jsxs(Text, { color: theme.success, children: [" ", describeLinks(item.group)] })) : null] }));
|
|
12
27
|
}
|
|
@@ -49,10 +49,6 @@ export function buildTree(data) {
|
|
|
49
49
|
});
|
|
50
50
|
return roots;
|
|
51
51
|
}
|
|
52
|
-
/** Version, policy and hosts of a leaf; `adopted` / `modified` only when true (the legend explains them). */
|
|
53
|
-
export function describeDetails({ selection, policy, installed }) {
|
|
54
|
-
return `${installed} (${policy}) [${selection.hosts.join(", ")}]`;
|
|
55
|
-
}
|
|
56
52
|
export function markersOf({ adopted, modified }) {
|
|
57
53
|
return [adopted ? "adopted" : "", modified ? "modified" : ""].filter((flag) => flag !== "").join(" ");
|
|
58
54
|
}
|
|
@@ -78,11 +74,12 @@ export function describeNode(row) {
|
|
|
78
74
|
return data.row.name;
|
|
79
75
|
if (data.kind === "app") {
|
|
80
76
|
const { entry, inspection } = data.row;
|
|
81
|
-
const name = entry.recipe?.name ?? entry.file;
|
|
82
|
-
if (inspection.status === "invalid")
|
|
77
|
+
const name = entry.recipe?.name ?? data.row.name ?? entry.file;
|
|
78
|
+
if (inspection.status === "invalid" && inspection.evidence !== "recorded")
|
|
83
79
|
return `${name} invalid recipe: ${inspection.reason}`;
|
|
84
80
|
const version = inspection.installedVersion ?? inspection.status;
|
|
85
|
-
|
|
81
|
+
const invalid = inspection.status === "invalid" ? ` · invalid recipe: ${inspection.reason}` : "";
|
|
82
|
+
return `${name} ${version}${inspection.evidence === "recorded" ? " (recorded)" : ""}${invalid}`;
|
|
86
83
|
}
|
|
87
84
|
return `${data.group.name} [${data.group.hosts.join(", ")}]`;
|
|
88
85
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
2
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { createAppOperations } from "../app-flow.js";
|
|
5
|
-
import { handleAppKey, isAppMode, startAppAction } from "./app-actions.js";
|
|
5
|
+
import { AppPanel, handleAppKey, isAppMode, startAppAction } from "./app-actions.js";
|
|
6
6
|
import { handlePackageKey, isPackageMode } from "./package-actions.js";
|
|
7
7
|
import { packageOperationsOf } from "./environment.js";
|
|
8
8
|
import { loadInstallations } from "./installations.js";
|
|
@@ -24,6 +24,7 @@ import { useKeys } from "./keys.js";
|
|
|
24
24
|
const NO_ENVIRONMENT = {};
|
|
25
25
|
const LEGEND = "adopted: tracked in place · modified: changed on disk";
|
|
26
26
|
export function InstallationsView({ operations, environment, onCapturingChange, listHeight }) {
|
|
27
|
+
const { rows: terminalRows, columns: terminalColumns } = useWindowSize();
|
|
27
28
|
const height = useListHeight(listHeight, 7);
|
|
28
29
|
const env = environment ?? NO_ENVIRONMENT;
|
|
29
30
|
const apps = useMemo(() => createAppOperations({ homeDirectory: env.homeDirectory, ...env.appEnvironment }), [env]);
|
|
@@ -71,9 +72,15 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
71
72
|
return;
|
|
72
73
|
checked.current.add(identity);
|
|
73
74
|
const generation = loadGeneration.current;
|
|
74
|
-
void apps.
|
|
75
|
-
if (
|
|
75
|
+
void apps.approvalStatus(entry).then(approval => {
|
|
76
|
+
if (approval.status !== "approved")
|
|
77
|
+
return undefined;
|
|
78
|
+
return apps.inspect(entry);
|
|
79
|
+
}).then(inspection => {
|
|
80
|
+
if (!inspection || !mounted.current || generation !== loadGeneration.current)
|
|
76
81
|
return;
|
|
82
|
+
if (inspection.status === "unknown")
|
|
83
|
+
checked.current.delete(identity);
|
|
77
84
|
setState(previous => previous.status !== "ready" ? previous : {
|
|
78
85
|
...previous,
|
|
79
86
|
data: {
|
|
@@ -131,7 +138,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
131
138
|
packages,
|
|
132
139
|
startAdoption: (group) => void startAdoption(adoption, group),
|
|
133
140
|
setMessage,
|
|
134
|
-
startApps: (entries, input) => void startAppAction({ apps, manage }, entries, input),
|
|
141
|
+
startApps: (entries, input) => void startAppAction({ apps, manage, terminalRows: terminalRows || 24, terminalColumns: terminalColumns || 80 }, entries, input),
|
|
135
142
|
};
|
|
136
143
|
useKeys((input, key) => {
|
|
137
144
|
const current = modeRef.current;
|
|
@@ -140,7 +147,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
140
147
|
else if (isPackageMode(current))
|
|
141
148
|
handlePackageKey({ packages, manage }, current, input, key);
|
|
142
149
|
else if (isAppMode(current))
|
|
143
|
-
handleAppKey({ apps, manage }, current, input, key);
|
|
150
|
+
handleAppKey({ apps, manage, terminalRows: terminalRows || 24, terminalColumns: terminalColumns || 80 }, current, input, key);
|
|
144
151
|
else if (!handleManageKey(manage, current, input, key))
|
|
145
152
|
handleAdoptionKey(adoption, current, input, key);
|
|
146
153
|
});
|
|
@@ -148,6 +155,8 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
148
155
|
return _jsx(Text, { children: "Loading installations..." });
|
|
149
156
|
if (state.status === "error")
|
|
150
157
|
return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
|
|
158
|
+
if (isAppMode(mode))
|
|
159
|
+
return _jsx(AppPanel, { mode: mode });
|
|
151
160
|
const ready = state.data;
|
|
152
161
|
const window = computeWindow(rows.length, index, height);
|
|
153
162
|
const listed = rows.slice(window.start, window.end);
|
|
@@ -79,13 +79,15 @@ export async function loadInstallations(operations, environment, appOperations =
|
|
|
79
79
|
let appsError;
|
|
80
80
|
try {
|
|
81
81
|
const tracked = await appOperations.trackedApps();
|
|
82
|
-
apps = await Promise.all((await appOperations.load()).filter(entry => entry.applicable).map(async (entry) =>
|
|
83
|
-
entry
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
apps = (await Promise.all((await appOperations.load()).filter(entry => entry.applicable).map(async (entry) => {
|
|
83
|
+
const record = tracked.find(item => item.recipeFile === entry.canonicalFile
|
|
84
|
+
&& (item.name === entry.recipe?.name || entry.recipe === undefined));
|
|
85
|
+
const inspection = record && (entry.error || !entry.recipe)
|
|
86
|
+
? { status: "invalid", installedVersion: record.installedVersion, evidence: "recorded", reason: entry.error }
|
|
87
|
+
: await appOperations.inspect(entry, record === undefined ? undefined : { installedVersion: record.installedVersion });
|
|
88
|
+
return { entry, inspection, ...(record === undefined ? {} : { name: record.name }) };
|
|
89
|
+
}))).filter(row => row.inspection.status === "installed"
|
|
90
|
+
|| (row.inspection.status === "invalid" && row.inspection.evidence === "recorded"));
|
|
89
91
|
}
|
|
90
92
|
catch (error) {
|
|
91
93
|
appsError = errorText(error);
|
|
@@ -2,7 +2,7 @@ import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-run
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { AppPanel } from "./app-actions.js";
|
|
4
4
|
import { PackagePanel } from "./package-actions.js";
|
|
5
|
-
import { ChecklistLines, HostChecklist, PreviewConfirm, PreviewLines } from "./panel-parts.js";
|
|
5
|
+
import { ChecklistLines, DecisionPrompt, HostChecklist, PreviewConfirm, PreviewLines } from "./panel-parts.js";
|
|
6
6
|
import { theme } from "./theme.js";
|
|
7
7
|
const plural = (n) => `${n} ${n === 1 ? "item" : "items"}`;
|
|
8
8
|
function SkippedLines({ skipped }) {
|
|
@@ -10,7 +10,7 @@ function SkippedLines({ skipped }) {
|
|
|
10
10
|
}
|
|
11
11
|
/** One block per planned item, the skipped ones, then the y/n question. */
|
|
12
12
|
function PlanPreview({ entries, skipped, question }) {
|
|
13
|
-
return (_jsxs(Box, { flexDirection: "column", children: [entries.map((entry) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.group, children: entry.label }), _jsx(PreviewLines, { lines: entry.lines, warnFirst: entry.warnFirst })] }, entry.id))), _jsx(SkippedLines, { skipped: skipped }), _jsx(
|
|
13
|
+
return (_jsxs(Box, { flexDirection: "column", children: [entries.map((entry) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.group, children: entry.label }), _jsx(PreviewLines, { lines: entry.lines, warnFirst: entry.warnFirst })] }, entry.id))), _jsx(SkippedLines, { skipped: skipped }), _jsx(DecisionPrompt, { children: question })] }));
|
|
14
14
|
}
|
|
15
15
|
function UninstallPreview({ plan }) {
|
|
16
16
|
const entries = plan.items.map((item) => ({
|
|
@@ -19,15 +19,16 @@ function UninstallPreview({ plan }) {
|
|
|
19
19
|
lines: item.kind === "managed" ? item.prepared.plan.preview : item.prepared.preview,
|
|
20
20
|
warnFirst: item.kind === "unmanaged",
|
|
21
21
|
}));
|
|
22
|
-
return _jsx(PlanPreview, { entries: entries, skipped: plan.skipped, question: `Uninstall ${plural(plan.items.length)}
|
|
22
|
+
return _jsx(PlanPreview, { entries: entries, skipped: plan.skipped, question: `Uninstall ${plural(plan.items.length)}?` });
|
|
23
23
|
}
|
|
24
24
|
function HostsPreview({ plan }) {
|
|
25
25
|
const entries = plan.items.map((item) => ({ id: item.id, label: item.label, lines: item.prepared.preview, warnFirst: false }));
|
|
26
|
-
return _jsx(PlanPreview, { entries: entries, skipped: plan.skipped, question: `Add hosts to ${plural(plan.items.length)}
|
|
26
|
+
return _jsx(PlanPreview, { entries: entries, skipped: plan.skipped, question: `Add hosts to ${plural(plan.items.length)}?` });
|
|
27
27
|
}
|
|
28
28
|
/** Renders the prompt of a manage mode: host lists, previews and confirmations. */
|
|
29
29
|
export function ManagePanel({ mode }) {
|
|
30
30
|
switch (mode.kind) {
|
|
31
|
+
case "app-busy":
|
|
31
32
|
case "app-approve":
|
|
32
33
|
case "app-hosts":
|
|
33
34
|
case "app-confirm":
|
|
@@ -45,7 +46,7 @@ export function ManagePanel({ mode }) {
|
|
|
45
46
|
case "confirm-host-add":
|
|
46
47
|
return _jsx(PreviewConfirm, { lines: mode.prepared.preview, question: `Add hosts to ${mode.skill.name}? y/n` });
|
|
47
48
|
case "confirm-host-exposure":
|
|
48
|
-
return (_jsxs(Text, { color: theme.warning, children: ["Adding hosts exposes the installed ", mode.skill.name, " to more hosts and needs separate confirmation (CLI: --confirm-additional-host). Confirm?
|
|
49
|
+
return (_jsxs(Text, { color: theme.warning, children: ["Adding hosts exposes the installed ", mode.skill.name, " to more hosts and needs separate confirmation (CLI: --confirm-additional-host). ", _jsx(DecisionPrompt, { children: "Confirm?" })] }));
|
|
49
50
|
case "confirm-host-remove":
|
|
50
51
|
return _jsx(PreviewConfirm, { lines: mode.prepared.preview, question: `Remove hosts from ${mode.skill.name}? y/n` });
|
|
51
52
|
case "confirm-uninstall":
|
|
@@ -63,7 +64,7 @@ export function ManagePanel({ mode }) {
|
|
|
63
64
|
case "confirm-bulk-hosts":
|
|
64
65
|
return _jsx(HostsPreview, { plan: mode.plan });
|
|
65
66
|
case "confirm-bulk-exposure":
|
|
66
|
-
return (_jsxs(Text, { color: theme.warning, children: ["Adding hosts exposes the installed ", mode.plan.items.map((item) => item.label).join(", "), " to more hosts and needs separate confirmation (CLI: --confirm-additional-host). Confirm?
|
|
67
|
+
return (_jsxs(Text, { color: theme.warning, children: ["Adding hosts exposes the installed ", mode.plan.items.map((item) => item.label).join(", "), " to more hosts and needs separate confirmation (CLI: --confirm-additional-host). ", _jsx(DecisionPrompt, { children: "Confirm?" })] }));
|
|
67
68
|
case "busy":
|
|
68
69
|
return _jsx(Text, { children: mode.label });
|
|
69
70
|
}
|
|
@@ -7,6 +7,15 @@ export function moveCursor(cursor, length, input, key) {
|
|
|
7
7
|
return Math.max(cursor - 1, 0);
|
|
8
8
|
return undefined;
|
|
9
9
|
}
|
|
10
|
+
/** Read-only preview navigation: arrows and page keys move a viewport; j/k move one line. */
|
|
11
|
+
export function movePreviewCursor(cursor, length, input, key, pageSize) {
|
|
12
|
+
const step = key.pageDown || key.downArrow ? pageSize : key.pageUp || key.upArrow ? -pageSize : 0;
|
|
13
|
+
if (step !== 0)
|
|
14
|
+
return Math.min(Math.max(cursor + step, 0), Math.max(length - 1, 0));
|
|
15
|
+
if (input === "j" || input === "k")
|
|
16
|
+
return moveCursor(cursor, length, input, key);
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
10
19
|
/** `selected` with `choice` flipped, always in the order of `choices`. */
|
|
11
20
|
export function toggleChoice(choices, selected, choice) {
|
|
12
21
|
return choices.filter((candidate) => candidate === choice ? !selected.includes(candidate) : selected.includes(candidate));
|
|
@@ -51,15 +51,27 @@ export function packageRow(record, inspection) {
|
|
|
51
51
|
selection: record.selection,
|
|
52
52
|
name: inspection.descriptor?.name ?? describeCoordinates(record.selection),
|
|
53
53
|
installed: installedEvidenceLabel(inspection.installed),
|
|
54
|
+
...(inspection.installed.kind === "installed" && inspection.installed.version !== undefined
|
|
55
|
+
? { installedVersion: inspection.installed.version }
|
|
56
|
+
: inspection.installed.kind === "installed" && inspection.installed.commit !== undefined
|
|
57
|
+
? { installedVersion: inspection.installed.commit.slice(0, 7) }
|
|
58
|
+
: {}),
|
|
54
59
|
available: packageVersionLabel(inspection.available),
|
|
55
|
-
status:
|
|
60
|
+
status: packageRowStatus(inspection),
|
|
56
61
|
...(inspection.drift === undefined ? {} : { drift: inspection.drift }),
|
|
57
62
|
};
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
function packageRowStatus(inspection) {
|
|
65
|
+
const status = classifyUpdate(inspection.installed, inspection.available, inspection.availableCommit, inspection.installedCommitIsAncestor);
|
|
66
|
+
if (status === "unknown") {
|
|
67
|
+
return {
|
|
68
|
+
kind: "unknown",
|
|
69
|
+
reason: inspection.installed.kind === "installed" && /local modifications|local changes/iu.test(inspection.installed.reason ?? "")
|
|
70
|
+
? "local changes"
|
|
71
|
+
: "unknown",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { kind: status };
|
|
63
75
|
}
|
|
64
76
|
/** One component of the inventory; the executable effect and a host-only owner are called out. */
|
|
65
77
|
export function describePackageComponent(component) {
|
|
@@ -1,6 +1,17 @@
|
|
|
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 { theme } from "./theme.js";
|
|
4
|
+
import { previewViewport } from "./preview-window.js";
|
|
5
|
+
/** A titled, color-independent boundary for a focused interaction or result. */
|
|
6
|
+
export function BorderPanel({ title, children }) {
|
|
7
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: theme.border, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.title, bold: true, children: title }), children] }));
|
|
8
|
+
}
|
|
9
|
+
export function FieldLine({ label, value }) {
|
|
10
|
+
return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: label }), _jsxs(Text, { color: theme.value, children: [" ", value] })] });
|
|
11
|
+
}
|
|
12
|
+
export function DecisionPrompt({ children }) {
|
|
13
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.warning, children: children }), " ", _jsx(Text, { color: theme.success, bold: true, children: "y" }), _jsx(Text, { color: theme.legend, children: "/" }), _jsx(Text, { color: theme.error, children: "n" })] }));
|
|
14
|
+
}
|
|
4
15
|
/** The `> [x] 1 name` rows of a checklist; the caller renders the header. */
|
|
5
16
|
export function ChecklistLines({ choices, cursor, isSelected, keyOf, label }) {
|
|
6
17
|
return (_jsx(_Fragment, { children: choices.map((choice, position) => (_jsxs(Text, { children: [position === cursor ? "> " : " ", "[", isSelected(choice) ? "x" : " ", "] ", position + 1, " ", label(choice)] }, keyOf(choice)))) }));
|
|
@@ -10,14 +21,116 @@ export function HostChecklist({ choices, cursor, selected }) {
|
|
|
10
21
|
return _jsx(ChecklistLines, { choices: choices, cursor: cursor, isSelected: (host) => selected.includes(host), keyOf: (host) => host, label: (host) => host });
|
|
11
22
|
}
|
|
12
23
|
/** Preview lines; `warnFirst` colors the first one as a warning. */
|
|
24
|
+
const PREVIEW_FIELD_LABELS = [
|
|
25
|
+
"File:", "Name:", "Platform:", "Status:", "Homepage:", "Owned Skills:",
|
|
26
|
+
"App:", "Environment:", "Version:", "argv:", "Executable:", "Manual:",
|
|
27
|
+
"Declaration:", "Version pattern:", "Working directory:", "Previous version:",
|
|
28
|
+
"Latest version:", "Package:", "Host:", "Source:", "Scope:", "Coordinates:", "Origin:",
|
|
29
|
+
"Available:", "Approval:", "Path:", "Blast radius:", "Warning:", " target:", " proposed changes:",
|
|
30
|
+
" external command:", " external cwd:", " external target:",
|
|
31
|
+
];
|
|
32
|
+
function previewField(line) {
|
|
33
|
+
const label = PREVIEW_FIELD_LABELS.find(candidate => line.startsWith(candidate));
|
|
34
|
+
return label === undefined ? undefined : { label, value: line.slice(label.length) };
|
|
35
|
+
}
|
|
36
|
+
function previewSpan(text, color, bold = false) {
|
|
37
|
+
return { text, color, bold };
|
|
38
|
+
}
|
|
39
|
+
function profilePreviewSpans(line) {
|
|
40
|
+
if (line.startsWith("Destination: ")) {
|
|
41
|
+
return [previewSpan("Destination:", theme.field), previewSpan(line.slice("Destination:".length), theme.value)];
|
|
42
|
+
}
|
|
43
|
+
if (line === "Preview: export portable user-global profile" || line === "Preview: import portable user-global profile") {
|
|
44
|
+
return [previewSpan("Preview:", theme.field), previewSpan(line.slice("Preview:".length), theme.value)];
|
|
45
|
+
}
|
|
46
|
+
if (line.startsWith("WARNING:"))
|
|
47
|
+
return [previewSpan(line, theme.warning)];
|
|
48
|
+
const source = /^Source (git:[^ ]+) (.+) \(included: (true|false)\)$/u.exec(line);
|
|
49
|
+
if (source)
|
|
50
|
+
return [previewSpan("Source", theme.field), previewSpan(" ", theme.value),
|
|
51
|
+
previewSpan(source[1], theme.muted), previewSpan(" ", theme.value), previewSpan(source[2], theme.value),
|
|
52
|
+
previewSpan(" (", theme.value), previewSpan("included:", theme.field), previewSpan(` ${source[3]})`, theme.value)];
|
|
53
|
+
const skill = /^Skill (.+) \[([^\]]*)\] \((latest|fixed(?: [^)]+)?)\)$/u.exec(line);
|
|
54
|
+
if (skill)
|
|
55
|
+
return [previewSpan("Skill", theme.field), previewSpan(" ", theme.value), previewSpan(skill[1], theme.value, true),
|
|
56
|
+
previewSpan(" [", theme.value), previewSpan(skill[2], theme.marker), previewSpan("] (", theme.value),
|
|
57
|
+
previewSpan(skill[3], theme.version), previewSpan(")", theme.value)];
|
|
58
|
+
const app = /^App recipe (.+) \(([^;]+); approval not transferred\)$/u.exec(line);
|
|
59
|
+
if (app)
|
|
60
|
+
return [previewSpan("App recipe", theme.field), previewSpan(" ", theme.value), previewSpan(app[1], theme.value, true),
|
|
61
|
+
previewSpan(" (", theme.value), previewSpan(app[2], theme.marker), previewSpan("; approval not transferred)", theme.value)];
|
|
62
|
+
const packageLine = /^Package (.+) from (.+) \((latest|fixed [^;]+); saved selection\)(.*)$/u.exec(line);
|
|
63
|
+
if (packageLine)
|
|
64
|
+
return [previewSpan("Package", theme.field), previewSpan(" ", theme.value),
|
|
65
|
+
previewSpan(packageLine[1], theme.value, true), previewSpan(" from ", theme.value), previewSpan(packageLine[2], theme.value),
|
|
66
|
+
previewSpan(" (", theme.value), previewSpan(packageLine[3], theme.version),
|
|
67
|
+
previewSpan(`; saved selection)${packageLine[4]}`, theme.value)];
|
|
68
|
+
const replica = /^Replica intent (Installed Skill|Installed App|Installed Package) (.+?)(; .*)?$/u.exec(line);
|
|
69
|
+
if (replica)
|
|
70
|
+
return [previewSpan("Replica intent", theme.field), previewSpan(" ", theme.value),
|
|
71
|
+
previewSpan(replica[1], theme.field), previewSpan(" ", theme.value), previewSpan(replica[2], theme.value, true),
|
|
72
|
+
previewSpan(replica[3] ?? "", theme.value)];
|
|
73
|
+
if (line.startsWith(" Source "))
|
|
74
|
+
return [previewSpan(" Source", theme.field), previewSpan(line.slice(" Source".length), theme.value)];
|
|
75
|
+
if (line.startsWith("Origin replica intent:"))
|
|
76
|
+
return [previewSpan("Origin replica intent:", theme.field),
|
|
77
|
+
previewSpan(line.slice("Origin replica intent:".length), theme.value)];
|
|
78
|
+
const imported = /^(add|same|conflict|skip)( \(discovery inclusion is not imported\))?: (Source|Skill|App recipe|Package) (.+)$/u.exec(line);
|
|
79
|
+
if (imported)
|
|
80
|
+
return [previewSpan(`${imported[1]}${imported[2] ?? ""}:`, theme.field), previewSpan(` ${imported[3]}`, theme.field),
|
|
81
|
+
previewSpan(" ", theme.value), previewSpan(imported[4], theme.value, true)];
|
|
82
|
+
const hosts = /^ {2}hosts: ([^;]*); version policy: (latest|fixed(?: .+)?)$/u.exec(line);
|
|
83
|
+
if (hosts)
|
|
84
|
+
return [previewSpan(" hosts:", theme.field), previewSpan(" ", theme.value),
|
|
85
|
+
previewSpan(hosts[1], theme.marker), previewSpan("; ", theme.value), previewSpan("version policy:", theme.field),
|
|
86
|
+
previewSpan(` ${hosts[2]}`, theme.version)];
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
function visiblePreviewSpans(spans, offset, length) {
|
|
90
|
+
const end = offset + length;
|
|
91
|
+
let spanStart = 0;
|
|
92
|
+
const visible = [];
|
|
93
|
+
for (const span of spans) {
|
|
94
|
+
const spanEnd = spanStart + span.text.length;
|
|
95
|
+
const start = Math.max(offset, spanStart);
|
|
96
|
+
const stop = Math.min(end, spanEnd);
|
|
97
|
+
if (start < stop)
|
|
98
|
+
visible.push({ ...span, text: span.text.slice(start - spanStart, stop - spanStart) });
|
|
99
|
+
spanStart = spanEnd;
|
|
100
|
+
}
|
|
101
|
+
return visible;
|
|
102
|
+
}
|
|
103
|
+
function PreviewLine({ line, position, warnFirst, logicalLine, offset = 0 }) {
|
|
104
|
+
const profileSpans = logicalLine === undefined ? profilePreviewSpans(line) : profilePreviewSpans(logicalLine);
|
|
105
|
+
if (profileSpans) {
|
|
106
|
+
const spans = logicalLine === undefined ? profileSpans : visiblePreviewSpans(profileSpans, offset, line.length);
|
|
107
|
+
return _jsx(Text, { children: spans.map((span, index) => _jsx(Text, { color: span.color, bold: span.bold, children: span.text }, index)) }, position);
|
|
108
|
+
}
|
|
109
|
+
const field = previewField(logicalLine ?? line);
|
|
110
|
+
if (field) {
|
|
111
|
+
const fieldSpans = [previewSpan(field.label, theme.field), previewSpan(field.value, theme.value)];
|
|
112
|
+
const spans = logicalLine === undefined ? fieldSpans : visiblePreviewSpans(fieldSpans, offset, line.length);
|
|
113
|
+
return _jsx(Text, { children: spans.map((span, index) => _jsx(Text, { color: span.color, children: span.text }, index)) }, position);
|
|
114
|
+
}
|
|
115
|
+
return _jsx(Text, { color: warnFirst && position === 0 ? theme.warning : theme.value, children: line }, position);
|
|
116
|
+
}
|
|
13
117
|
export function PreviewLines({ lines, warnFirst = false }) {
|
|
14
|
-
return
|
|
118
|
+
return _jsx(_Fragment, { children: lines.map((line, position) => _jsx(PreviewLine, { line: line, position: position, warnFirst: warnFirst }, position)) });
|
|
15
119
|
}
|
|
16
120
|
/** Under a list: the window indicator, how many items are marked, and the last result. */
|
|
17
121
|
export function ListFooter({ indicator, selectedCount, message }) {
|
|
18
|
-
return (_jsxs(_Fragment, { children: [indicator ? _jsx(Text, { color: theme.muted, children: indicator }) : null, selectedCount > 0 ? _jsxs(Text, { color: theme.muted, children: [selectedCount, " selected"] }) : null, message ? _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) : null] }));
|
|
122
|
+
return (_jsxs(_Fragment, { children: [indicator ? _jsx(Text, { color: theme.muted, children: indicator }) : null, selectedCount > 0 ? _jsxs(Text, { color: theme.muted, children: [selectedCount, " selected"] }) : null, message ? (_jsx(BorderPanel, { title: message.kind === "error" ? "Result · failed" : "Result · complete", children: _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) })) : null] }));
|
|
19
123
|
}
|
|
20
124
|
/** A preview followed by a y/n question. */
|
|
21
|
-
export function PreviewConfirm({ lines, question, warnFirst }) {
|
|
22
|
-
|
|
125
|
+
export function PreviewConfirm({ title = "Confirmation", lines, question, warnFirst, cursor, chrome }) {
|
|
126
|
+
const { rows, columns } = useWindowSize();
|
|
127
|
+
const viewport = cursor === undefined ? undefined : previewViewport(lines, cursor, rows || 24, columns || 80, chrome ?? { title, questions: [question], footerRows: 2 });
|
|
128
|
+
const shown = viewport?.rows ?? lines;
|
|
129
|
+
return (_jsxs(BorderPanel, { title: title, children: [viewport ? shown.map((line, position) => {
|
|
130
|
+
const sourceLine = viewport.sourceLines[position];
|
|
131
|
+
return _jsx(PreviewLine, { line: line, position: position, warnFirst: Boolean(warnFirst && viewport.start === 0), logicalLine: lines[sourceLine], offset: viewport.sourceOffsets[position] }, position);
|
|
132
|
+
}) : _jsx(PreviewLines, { lines: shown, warnFirst: warnFirst }), viewport && viewport.total > viewport.height ? _jsxs(Text, { color: theme.muted, children: [viewport.start + 1, "\u2013", viewport.end, " of ", viewport.total] }) : null, viewport ? _jsxs(Text, { color: theme.legend, children: ["Review ", viewport.cursor + 1, " of ", viewport.total, " \u00B7 arrows/Pg page"] }) : null, question.includes("y/n") ? (() => {
|
|
133
|
+
const marker = question.indexOf("y/n");
|
|
134
|
+
return _jsxs(Text, { children: [_jsx(Text, { color: theme.warning, children: question.slice(0, marker) }), _jsx(Text, { color: theme.success, bold: true, children: "y" }), _jsx(Text, { color: theme.legend, children: "/" }), _jsx(Text, { color: theme.error, children: "n" }), _jsx(Text, { color: theme.legend, children: question.slice(marker + 3) })] });
|
|
135
|
+
})() : _jsx(Text, { color: theme.legend, children: question })] }));
|
|
23
136
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import stringWidth from "string-width";
|
|
2
|
+
import { headerTabRowCount } from "./header-layout.js";
|
|
3
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
4
|
+
function wrapLine(line, width) {
|
|
5
|
+
const segments = [...graphemes.segment(line)];
|
|
6
|
+
if (segments.length === 0)
|
|
7
|
+
return [{ row: "", offset: 0 }];
|
|
8
|
+
const wrapped = [];
|
|
9
|
+
let row = "";
|
|
10
|
+
let rowWidth = 0;
|
|
11
|
+
let rowOffset = 0;
|
|
12
|
+
for (const { segment, index } of segments) {
|
|
13
|
+
const segmentWidth = stringWidth(segment);
|
|
14
|
+
if (row !== "" && rowWidth + segmentWidth > width) {
|
|
15
|
+
wrapped.push({ row, offset: rowOffset });
|
|
16
|
+
row = "";
|
|
17
|
+
rowWidth = 0;
|
|
18
|
+
rowOffset = index;
|
|
19
|
+
}
|
|
20
|
+
row += segment;
|
|
21
|
+
rowWidth += segmentWidth;
|
|
22
|
+
}
|
|
23
|
+
wrapped.push({ row, offset: rowOffset });
|
|
24
|
+
return wrapped;
|
|
25
|
+
}
|
|
26
|
+
export function previewViewport(lines, cursor, terminalRows, terminalColumns, chrome = { title: "Confirmation", questions: ["Scroll to end before y; n cancels.", "Confirm? y/n"] }) {
|
|
27
|
+
const contentWidth = Math.max(terminalColumns - 8, 1);
|
|
28
|
+
const wrapped = lines.flatMap((line, sourceLine) => wrapLine(line, contentWidth).map(({ row, offset }) => ({ row, sourceLine, offset })));
|
|
29
|
+
const previewRows = wrapped.map(({ row }) => row);
|
|
30
|
+
const headerRows = headerTabRowCount(contentWidth) + 3;
|
|
31
|
+
const titleRows = wrapLine(chrome.title, contentWidth).length;
|
|
32
|
+
const questionRows = Math.max(...chrome.questions.map(question => wrapLine(question, contentWidth).length));
|
|
33
|
+
const panelChromeRows = 2 + titleRows + questionRows + (chrome.footerRows ?? 1);
|
|
34
|
+
const reservedRows = chrome.reservedRows !== undefined && chrome.reservedRows > 0 ? Math.ceil(chrome.reservedRows) : 0;
|
|
35
|
+
const availableRows = Math.max(terminalRows - headerRows - 1 - panelChromeRows - reservedRows, 1);
|
|
36
|
+
const height = previewRows.length > availableRows ? Math.max(availableRows - 1, 1) : availableRows;
|
|
37
|
+
const selected = Math.min(Math.max(cursor, 0), Math.max(previewRows.length - 1, 0));
|
|
38
|
+
const start = Math.min(Math.max(selected - Math.floor(height / 2), 0), Math.max(previewRows.length - height, 0));
|
|
39
|
+
const end = Math.min(start + height, previewRows.length);
|
|
40
|
+
return {
|
|
41
|
+
rows: previewRows.slice(start, end),
|
|
42
|
+
sourceLines: wrapped.slice(start, end).map(({ sourceLine }) => sourceLine),
|
|
43
|
+
sourceOffsets: wrapped.slice(start, end).map(({ offset }) => offset),
|
|
44
|
+
start,
|
|
45
|
+
end,
|
|
46
|
+
total: previewRows.length,
|
|
47
|
+
cursor: selected,
|
|
48
|
+
height,
|
|
49
|
+
reachedEnd: end === previewRows.length,
|
|
50
|
+
};
|
|
51
|
+
}
|