@iseoane/agent-depot 0.6.12 → 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 +45 -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/self-update.js +9 -2
- 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 { jsx as _jsx, jsxs as _jsxs } 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 { AppPanel, handleAppKey, isAppMode, startAppAction } from "./app-actions.js";
|
|
@@ -10,8 +11,9 @@ import { BROWSE } from "./installations-mode.js";
|
|
|
10
11
|
import { errorText } from "./batch.js";
|
|
11
12
|
import { handleAdoptionKey, startAdoption } from "./adoption-actions.js";
|
|
12
13
|
import { handleBrowseKey } from "./installations-browse.js";
|
|
13
|
-
import { handleManageKey } from "./manage-actions.js";
|
|
14
|
-
import {
|
|
14
|
+
import { handleManageKey, isPagedConfirmMode } from "./manage-actions.js";
|
|
15
|
+
import { PagedConfirmPanel } from "./manage-panel.js";
|
|
16
|
+
import { AdoptionConfirmPanel, ModePanel } from "./installations-panel.js";
|
|
15
17
|
import { TreeRowLine } from "./installations-rows.js";
|
|
16
18
|
import { buildTree, leavesOf, markersOf } from "./installations-tree.js";
|
|
17
19
|
import { ListFooter, Working } from "./panel-parts.js";
|
|
@@ -23,10 +25,11 @@ import { useLoadingSince, useMarks, useMounted } from "./view-state.js";
|
|
|
23
25
|
import { computeWindow, useListHeight } from "./window.js";
|
|
24
26
|
import { useKeys } from "./keys.js";
|
|
25
27
|
import { invalidateRetention } from "./view-retention.js";
|
|
28
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
26
29
|
const NO_ENVIRONMENT = {};
|
|
27
30
|
const LEGEND = "adopted: tracked in place · modified: changed on disk";
|
|
28
31
|
export function InstallationsView({ operations, environment, onCapturingChange, listHeight, retention }) {
|
|
29
|
-
const
|
|
32
|
+
const previewGeometry = usePreviewGeometry();
|
|
30
33
|
const height = useListHeight(listHeight, 7);
|
|
31
34
|
const env = environment ?? NO_ENVIRONMENT;
|
|
32
35
|
const apps = useMemo(() => createAppOperations({ homeDirectory: env.homeDirectory, ...env.appEnvironment }), [env]);
|
|
@@ -148,6 +151,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
148
151
|
operations,
|
|
149
152
|
env,
|
|
150
153
|
installed,
|
|
154
|
+
previewGeometry,
|
|
151
155
|
setMode,
|
|
152
156
|
browse: () => setMode(BROWSE),
|
|
153
157
|
setMessage,
|
|
@@ -165,6 +169,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
165
169
|
const adoption = {
|
|
166
170
|
operations,
|
|
167
171
|
env,
|
|
172
|
+
previewGeometry,
|
|
168
173
|
setMode,
|
|
169
174
|
browse: manage.browse,
|
|
170
175
|
setMessage,
|
|
@@ -180,16 +185,16 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
180
185
|
packages,
|
|
181
186
|
startAdoption: (group) => void startAdoption(adoption, group),
|
|
182
187
|
setMessage,
|
|
183
|
-
startApps: (entries, input) => void startAppAction({ apps, manage,
|
|
188
|
+
startApps: (entries, input) => void startAppAction({ apps, manage, previewGeometry }, entries, input),
|
|
184
189
|
};
|
|
185
190
|
useKeys((input, key) => {
|
|
186
191
|
const current = modeRef.current;
|
|
187
192
|
if (current.kind === "browse")
|
|
188
193
|
handleBrowseKey(browse, input, key);
|
|
189
194
|
else if (isPackageMode(current))
|
|
190
|
-
handlePackageKey({ packages, manage,
|
|
195
|
+
handlePackageKey({ packages, manage, previewGeometry }, current, input, key);
|
|
191
196
|
else if (isAppMode(current))
|
|
192
|
-
handleAppKey({ apps, manage,
|
|
197
|
+
handleAppKey({ apps, manage, previewGeometry }, current, input, key);
|
|
193
198
|
else if (!handleManageKey(manage, current, input, key))
|
|
194
199
|
handleAdoptionKey(adoption, current, input, key);
|
|
195
200
|
});
|
|
@@ -202,6 +207,10 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
202
207
|
// A paged declaration owns the view: rows of the tree above it would not be counted in its height.
|
|
203
208
|
if (isPackageMode(mode))
|
|
204
209
|
return _jsx(PackagePanel, { mode: mode });
|
|
210
|
+
if (isPagedConfirmMode(mode))
|
|
211
|
+
return _jsx(PagedConfirmPanel, { mode: mode });
|
|
212
|
+
if (mode.kind === "confirm" && mode.adoption.verdict.adoptable)
|
|
213
|
+
return _jsx(AdoptionConfirmPanel, { mode: mode });
|
|
205
214
|
const ready = state.data;
|
|
206
215
|
const window = computeWindow(rows.length, index, height);
|
|
207
216
|
const listed = rows.slice(window.start, window.end);
|
package/dist/src/tui/keys.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useInput } from "ink";
|
|
2
|
+
import { useRef } from "react";
|
|
2
3
|
/** Plain printable characters only: control characters and escape sequences are never split. */
|
|
3
4
|
const PRINTABLE = /^[^\p{Cc}]+$/u;
|
|
4
5
|
/**
|
|
@@ -10,22 +11,44 @@ const PRINTABLE = /^[^\p{Cc}]+$/u;
|
|
|
10
11
|
*/
|
|
11
12
|
const ESCAPE_REMNANT = /^(?:\[[\d;<:?]*[A-DFHIMOPQRSZm~]|O[A-DFHPQRS])$/;
|
|
12
13
|
const PASTE_MARKERS = new Set(["[200~", "[201~"]);
|
|
14
|
+
/** Keys that did not arrive alone: split off a multi-character block, or read after another key in the same turn. */
|
|
15
|
+
const groupedKeys = new WeakSet();
|
|
16
|
+
/**
|
|
17
|
+
* Whether this key press is a `y` that answers a confirmation. A `y` that arrived together with other
|
|
18
|
+
* keys never does: they may have opened the question or moved the cursor to its end before it was drawn.
|
|
19
|
+
* Text fields read `input` directly, so pasted text still reaches them.
|
|
20
|
+
*/
|
|
21
|
+
export function isYes(input, key) {
|
|
22
|
+
return input === "y" && !groupedKeys.has(key);
|
|
23
|
+
}
|
|
24
|
+
function grouped(key) {
|
|
25
|
+
const copy = { ...key };
|
|
26
|
+
groupedKeys.add(copy);
|
|
27
|
+
return copy;
|
|
28
|
+
}
|
|
13
29
|
/**
|
|
14
30
|
* `useInput` that treats several plain characters delivered in one write (fast
|
|
15
|
-
* typing, a paste) as the separate key presses they are, in order.
|
|
31
|
+
* typing, a paste) as the separate key presses they are, in order. Ink also hands
|
|
32
|
+
* the escape sequences of one read over as separate events within one turn; every key
|
|
33
|
+
* after the first of a turn counts as grouped, as do all characters of a split block.
|
|
16
34
|
*/
|
|
17
35
|
export function useKeys(handler) {
|
|
36
|
+
const eventsThisTurn = useRef(0);
|
|
18
37
|
useInput((input, key) => {
|
|
38
|
+
if (eventsThisTurn.current++ === 0)
|
|
39
|
+
queueMicrotask(() => { eventsThisTurn.current = 0; });
|
|
40
|
+
const later = eventsThisTurn.current > 1;
|
|
19
41
|
if (ESCAPE_REMNANT.test(input)) {
|
|
20
42
|
if (!PASTE_MARKERS.has(input))
|
|
21
|
-
handler(input, key);
|
|
43
|
+
handler(input, later ? grouped(key) : key);
|
|
22
44
|
return;
|
|
23
45
|
}
|
|
24
46
|
if (input.length > 1 && !key.ctrl && !key.meta && !key.escape && PRINTABLE.test(input)) {
|
|
47
|
+
const blockKey = grouped(key);
|
|
25
48
|
for (const character of input)
|
|
26
|
-
handler(character,
|
|
49
|
+
handler(character, blockKey);
|
|
27
50
|
return;
|
|
28
51
|
}
|
|
29
|
-
handler(input, key);
|
|
52
|
+
handler(input, later ? grouped(key) : key);
|
|
30
53
|
});
|
|
31
54
|
}
|
|
@@ -2,9 +2,9 @@ import { APP_KINDS } from "./app-actions.js";
|
|
|
2
2
|
import { PACKAGE_KINDS } from "./package-actions.js";
|
|
3
3
|
import { globalRecord, missingHosts, prepareHostAddition, prepareHostRemoval, prepareUninstall, runHostAddition, runHostRemoval, runUninstall, UNINSTALL_REFUSALS, } from "./catalog-installs.js";
|
|
4
4
|
import { errorText } from "./batch.js";
|
|
5
|
-
import { BULK_KINDS, handleBulkKey } from "./bulk-actions.js";
|
|
6
|
-
import { answerYesNo, applyChecklistOutcome, hostChecklistKey } from "./mode-keys.js";
|
|
7
|
-
import { handleUnmanagedKey, UNMANAGED_KINDS } from "./unmanaged-actions.js";
|
|
5
|
+
import { BULK_KINDS, bulkConfirmPreview, handleBulkKey } from "./bulk-actions.js";
|
|
6
|
+
import { answerYesNo, applyChecklistOutcome, hostChecklistKey, movePreviewCursor } from "./mode-keys.js";
|
|
7
|
+
import { handleUnmanagedKey, UNMANAGED_KINDS, unmanagedConfirmPreview } from "./unmanaged-actions.js";
|
|
8
8
|
import { busy } from "./working-model.js";
|
|
9
9
|
const MANAGE_KINDS = new Set([
|
|
10
10
|
"remove-scope",
|
|
@@ -43,7 +43,7 @@ async function startFullUninstall(context, skill) {
|
|
|
43
43
|
if (!context.isMounted())
|
|
44
44
|
return;
|
|
45
45
|
if (prepared.kind === "ready") {
|
|
46
|
-
context.setMode({ kind: "confirm-uninstall", skill, prepared });
|
|
46
|
+
context.setMode({ kind: "confirm-uninstall", skill, prepared, cursor: 0 });
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
context.setMessage({ kind: "error", text: UNINSTALL_REFUSALS[prepared.kind] });
|
|
@@ -73,13 +73,13 @@ async function startHostChange(context, mode) {
|
|
|
73
73
|
const prepared = await prepareHostAddition(operations, env, mode.skill, installed, mode.selected);
|
|
74
74
|
if (!context.isMounted())
|
|
75
75
|
return;
|
|
76
|
-
context.setMode({ kind: "confirm-host-add", skill: mode.skill, prepared });
|
|
76
|
+
context.setMode({ kind: "confirm-host-add", skill: mode.skill, prepared, cursor: 0 });
|
|
77
77
|
}
|
|
78
78
|
else {
|
|
79
79
|
const prepared = await prepareHostRemoval(operations, env, mode.skill, installed, mode.selected);
|
|
80
80
|
if (!context.isMounted())
|
|
81
81
|
return;
|
|
82
|
-
context.setMode({ kind: "confirm-host-remove", skill: mode.skill, prepared });
|
|
82
|
+
context.setMode({ kind: "confirm-host-remove", skill: mode.skill, prepared, cursor: 0 });
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
catch (error) {
|
|
@@ -117,35 +117,61 @@ function handleRemoveScopeKey(context, mode, input, key) {
|
|
|
117
117
|
else if (input === "2")
|
|
118
118
|
context.setMode({ kind: "hosts-remove", skill: mode.skill, choices: mode.hosts, cursor: 0, selected: [] });
|
|
119
119
|
}
|
|
120
|
-
/**
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
/** What a paged confirmation shows, so the key handler and the panel page the same rows. */
|
|
121
|
+
export function pagedConfirmPreview(mode) {
|
|
122
|
+
if (mode.kind === "confirm-unmanaged")
|
|
123
|
+
return unmanagedConfirmPreview(mode);
|
|
124
|
+
if (mode.kind === "confirm-bulk-uninstall" || mode.kind === "confirm-bulk-hosts")
|
|
125
|
+
return bulkConfirmPreview(mode);
|
|
126
|
+
const name = mode.skill.name;
|
|
124
127
|
switch (mode.kind) {
|
|
125
128
|
case "confirm-host-add":
|
|
126
|
-
|
|
127
|
-
return;
|
|
128
|
-
case "confirm-host-exposure":
|
|
129
|
-
answerYesNo(input, key, () => {
|
|
130
|
-
void execute(context, `Changing hosts of ${mode.skill.name}…`, () => runHostAddition(mode.prepared, operations, env));
|
|
131
|
-
}, cancelWith("Install cancelled"));
|
|
132
|
-
return;
|
|
129
|
+
return { lines: mode.prepared.preview, chrome: { title: "Add hosts", action: `Add hosts to ${name}`, consequence: `Add hosts to ${name}? y/n` } };
|
|
133
130
|
case "confirm-host-remove":
|
|
134
|
-
|
|
135
|
-
void execute(context, `Changing hosts of ${mode.skill.name}…`, () => runHostRemoval(mode.prepared, operations, env, mode.skill));
|
|
136
|
-
}, cancelWith("Uninstall cancelled"));
|
|
137
|
-
return;
|
|
131
|
+
return { lines: mode.prepared.preview, chrome: { title: "Remove hosts", action: `Remove hosts from ${name}`, consequence: `Remove hosts from ${name}? y/n` } };
|
|
138
132
|
case "confirm-uninstall":
|
|
139
|
-
|
|
133
|
+
return { lines: mode.prepared.plan.preview, chrome: { title: "Uninstall", action: `Uninstall ${name}`, consequence: `Uninstall ${name}? y/n` } };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function isPagedConfirmMode(mode) {
|
|
137
|
+
return ["confirm-host-add", "confirm-host-remove", "confirm-uninstall", "confirm-unmanaged", "confirm-bulk-uninstall", "confirm-bulk-hosts"].includes(mode.kind);
|
|
138
|
+
}
|
|
139
|
+
/** Pages the preview like the other footer confirmations: `y` answers only once it has all been displayed. */
|
|
140
|
+
function handlePagedConfirmKey(context, mode, input, key) {
|
|
141
|
+
const { operations, env } = context;
|
|
142
|
+
const { lines, chrome } = pagedConfirmPreview(mode);
|
|
143
|
+
const viewport = context.previewGeometry(lines, mode.cursor, chrome);
|
|
144
|
+
const cursor = movePreviewCursor(mode.cursor, viewport.total, input, key, viewport.height);
|
|
145
|
+
if (cursor !== undefined) {
|
|
146
|
+
context.setMode({ ...mode, cursor });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
answerYesNo(input, key, () => {
|
|
150
|
+
if (!viewport.reachedEnd)
|
|
151
|
+
return;
|
|
152
|
+
switch (mode.kind) {
|
|
153
|
+
case "confirm-host-add":
|
|
154
|
+
context.setMode({ kind: "confirm-host-exposure", skill: mode.skill, prepared: mode.prepared });
|
|
155
|
+
return;
|
|
156
|
+
case "confirm-host-remove":
|
|
157
|
+
void execute(context, `Changing hosts of ${mode.skill.name}…`, () => runHostRemoval(mode.prepared, operations, env, mode.skill));
|
|
158
|
+
return;
|
|
159
|
+
case "confirm-uninstall":
|
|
140
160
|
void execute(context, `Removing ${mode.skill.name}…`, async () => {
|
|
141
161
|
await runUninstall(mode.prepared, operations, env);
|
|
142
162
|
return mode.prepared.plan.inspections.every((inspection) => inspection.recordOnly)
|
|
143
163
|
? `Forgot missing installation record for ${mode.skill.path}; no files were removed`
|
|
144
164
|
: `Removed ${mode.skill.name} (${mode.skill.path})`;
|
|
145
165
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}, () => cancel(context, mode.kind === "confirm-host-add" ? "Install cancelled" : "Uninstall cancelled"));
|
|
169
|
+
}
|
|
170
|
+
/** The exposure question has no preview: `y` adds the hosts, `n`/Esc cancels. */
|
|
171
|
+
function handleExposureKey(context, mode, input, key) {
|
|
172
|
+
answerYesNo(input, key, () => {
|
|
173
|
+
void execute(context, `Changing hosts of ${mode.skill.name}…`, () => runHostAddition(mode.prepared, context.operations, context.env));
|
|
174
|
+
}, () => cancel(context, "Install cancelled"));
|
|
149
175
|
}
|
|
150
176
|
/**
|
|
151
177
|
* Handles a key in one of the manage modes. Returns false when the mode is not
|
|
@@ -167,10 +193,12 @@ export function handleManageKey(context, mode, input, key) {
|
|
|
167
193
|
handleHostChecklistKey(context, mode, input, key);
|
|
168
194
|
break;
|
|
169
195
|
case "confirm-host-add":
|
|
170
|
-
case "confirm-host-exposure":
|
|
171
196
|
case "confirm-host-remove":
|
|
172
197
|
case "confirm-uninstall":
|
|
173
|
-
|
|
198
|
+
handlePagedConfirmKey(context, mode, input, key);
|
|
199
|
+
break;
|
|
200
|
+
case "confirm-host-exposure":
|
|
201
|
+
handleExposureKey(context, mode, input, key);
|
|
174
202
|
break;
|
|
175
203
|
default:
|
|
176
204
|
break;
|
|
@@ -1,29 +1,20 @@
|
|
|
1
1
|
import { jsxs as _jsxs, Fragment as _Fragment, 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 { AppPanel } from "./app-actions.js";
|
|
4
5
|
import { PackagePanel } from "./package-actions.js";
|
|
5
|
-
import {
|
|
6
|
+
import { pagedConfirmPreview } from "./manage-actions.js";
|
|
7
|
+
import { ChecklistLines, DecisionPrompt, HostChecklist, PagedPreview, Working } from "./panel-parts.js";
|
|
8
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
6
9
|
import { theme } from "./theme.js";
|
|
7
10
|
const plural = (n) => `${n} ${n === 1 ? "item" : "items"}`;
|
|
8
11
|
function SkippedLines({ skipped }) {
|
|
9
12
|
return (_jsx(_Fragment, { children: skipped.map(({ id, label, reason }) => _jsxs(Text, { color: theme.warning, children: ["Skipped ", label, ": ", reason] }, id)) }));
|
|
10
13
|
}
|
|
11
|
-
/**
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
function UninstallPreview({ plan }) {
|
|
16
|
-
const entries = plan.items.map((item) => ({
|
|
17
|
-
id: item.id,
|
|
18
|
-
label: item.label,
|
|
19
|
-
lines: item.kind === "managed" ? item.prepared.plan.preview : item.prepared.preview,
|
|
20
|
-
warnFirst: item.kind === "unmanaged",
|
|
21
|
-
}));
|
|
22
|
-
return _jsx(PlanPreview, { entries: entries, skipped: plan.skipped, question: `Uninstall ${plural(plan.items.length)}?` });
|
|
23
|
-
}
|
|
24
|
-
function HostsPreview({ plan }) {
|
|
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)}?` });
|
|
14
|
+
/** A Skill confirmation with the shared footer; the view shows it alone so no tree rows count against its height. */
|
|
15
|
+
export function PagedConfirmPanel({ mode }) {
|
|
16
|
+
const { lines, styled, chrome } = pagedConfirmPreview(mode);
|
|
17
|
+
return _jsx(PagedPreview, { chrome: chrome, lines: lines, styled: styled, viewport: usePreviewGeometry()(lines, mode.cursor, chrome) });
|
|
27
18
|
}
|
|
28
19
|
/** Renders the prompt of a manage mode: host lists, previews and confirmations. */
|
|
29
20
|
export function ManagePanel({ mode }) {
|
|
@@ -44,25 +35,20 @@ export function ManagePanel({ mode }) {
|
|
|
44
35
|
case "hosts-remove":
|
|
45
36
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [mode.kind === "hosts-add" ? "Add hosts to" : "Remove hosts from", " ", mode.skill.name, ". Host (space/1-", mode.choices.length, " toggle, j/k move, Enter continue, Esc cancel):"] }), _jsx(HostChecklist, { choices: mode.choices, cursor: mode.cursor, selected: mode.selected })] }));
|
|
46
37
|
case "confirm-host-add":
|
|
47
|
-
return _jsx(PreviewConfirm, { lines: mode.prepared.preview, question: `Add hosts to ${mode.skill.name}? y/n` });
|
|
48
|
-
case "confirm-host-exposure":
|
|
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?" })] }));
|
|
50
38
|
case "confirm-host-remove":
|
|
51
|
-
return _jsx(PreviewConfirm, { lines: mode.prepared.preview, question: `Remove hosts from ${mode.skill.name}? y/n` });
|
|
52
39
|
case "confirm-uninstall":
|
|
53
|
-
|
|
40
|
+
case "confirm-unmanaged":
|
|
41
|
+
case "confirm-bulk-uninstall":
|
|
42
|
+
case "confirm-bulk-hosts":
|
|
43
|
+
return _jsx(PagedConfirmPanel, { mode: mode });
|
|
44
|
+
case "confirm-host-exposure":
|
|
45
|
+
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?" })] }));
|
|
54
46
|
case "unmanaged-scope":
|
|
55
47
|
return _jsxs(Text, { children: ["Remove ", mode.group.name, " (", mode.group.locations.length, " locations): 1 all locations 2 choose locations (Esc cancel)"] });
|
|
56
48
|
case "unmanaged-pick":
|
|
57
49
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Remove locations of ", mode.group.name, ". Location (space/1-", mode.group.locations.length, " toggle, j/k move, Enter continue, Esc cancel):"] }), _jsx(ChecklistLines, { choices: mode.group.locations, cursor: mode.cursor, isSelected: (location) => mode.selected.includes(location.path), keyOf: (location) => location.path, label: (location) => `${location.path}${location.linkTarget === undefined ? "" : ` symlink → ${location.linkTarget}`}` })] }));
|
|
58
|
-
case "confirm-unmanaged":
|
|
59
|
-
return _jsx(PreviewConfirm, { lines: mode.prepared.preview, question: `Remove ${mode.group.name}? y/n`, warnFirst: true });
|
|
60
50
|
case "bulk-hosts":
|
|
61
51
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Add hosts to ", plural(mode.targets.length), ". Host (space/1-", mode.choices.length, " toggle, j/k move, Enter continue, Esc cancel):"] }), _jsx(HostChecklist, { choices: mode.choices, cursor: mode.cursor, selected: mode.selected }), _jsx(SkippedLines, { skipped: mode.skipped })] }));
|
|
62
|
-
case "confirm-bulk-uninstall":
|
|
63
|
-
return _jsx(UninstallPreview, { plan: mode.plan });
|
|
64
|
-
case "confirm-bulk-hosts":
|
|
65
|
-
return _jsx(HostsPreview, { plan: mode.plan });
|
|
66
52
|
case "confirm-bulk-exposure":
|
|
67
53
|
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?" })] }));
|
|
68
54
|
case "busy":
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isValidFixedVersion } from "../skill-install.js";
|
|
2
|
+
import { isYes } from "./keys.js";
|
|
2
3
|
/** The cursor after a down/up key (arrows or j/k), or undefined when the key does not move it. */
|
|
3
4
|
export function moveCursor(cursor, length, input, key) {
|
|
4
5
|
if (key.downArrow || input === "j")
|
|
@@ -51,11 +52,20 @@ export function applyChecklistOutcome(outcome, handlers) {
|
|
|
51
52
|
}
|
|
52
53
|
/** The y/n confirmation keys: `y` confirms, `n` or Esc declines, anything else is ignored. */
|
|
53
54
|
export function answerYesNo(input, key, onYes, onNo) {
|
|
54
|
-
if (input
|
|
55
|
+
if (isYes(input, key))
|
|
55
56
|
onYes();
|
|
56
57
|
else if (input === "n" || key.escape)
|
|
57
58
|
onNo();
|
|
58
59
|
}
|
|
60
|
+
/** A paged preview answers `y` only once `viewport` has been read to the end; paging keys move the cursor. */
|
|
61
|
+
export function answerPaged(viewport, cursor, input, key, outcome) {
|
|
62
|
+
const moved = movePreviewCursor(cursor, viewport.total, input, key, viewport.height);
|
|
63
|
+
if (moved !== undefined)
|
|
64
|
+
outcome.moveTo(moved);
|
|
65
|
+
else
|
|
66
|
+
answerYesNo(input, key, () => { if (viewport.reachedEnd)
|
|
67
|
+
outcome.yes(); }, outcome.no);
|
|
68
|
+
}
|
|
59
69
|
/** Esc cancels, Enter submits a valid version, backspace deletes, any other plain key is typed. */
|
|
60
70
|
export function versionInputKey(value, input, key) {
|
|
61
71
|
if (key.escape)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { quoteText } from "../preview-model.js";
|
|
2
3
|
import { displayPackageSelection } from "../package-model.js";
|
|
3
4
|
import { errorText } from "./batch.js";
|
|
5
|
+
import { isYes } from "./keys.js";
|
|
4
6
|
import { movePreviewCursor } from "./mode-keys.js";
|
|
5
7
|
import { installedEvidenceLabel, packagePlanLines } from "./package-rows.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
+
import { PagedPreview } from "./panel-parts.js";
|
|
9
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
8
10
|
import { busy } from "./working-model.js";
|
|
9
11
|
/** The per-row keys of a Package; Enter is approval, handled beside these. */
|
|
10
12
|
export const PACKAGE_ACTION_KEYS = new Map([
|
|
@@ -58,7 +60,7 @@ export async function startPackageAction(context, target, input) {
|
|
|
58
60
|
manage.setMode({ kind: "package-confirm", plan, approval, cursor: 0 });
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
61
|
-
await manage.finish({ kind: "error", text: `The action ${
|
|
63
|
+
await manage.finish({ kind: "error", text: `The action ${quoteText(input)} is not available for Packages` });
|
|
62
64
|
}
|
|
63
65
|
catch (error) {
|
|
64
66
|
await manage.finish({ kind: "error", text: errorText(error) });
|
|
@@ -132,28 +134,32 @@ function packagePreview(mode) {
|
|
|
132
134
|
case "package-approve":
|
|
133
135
|
return {
|
|
134
136
|
lines: mode.lines,
|
|
135
|
-
chrome:
|
|
137
|
+
chrome: { title: "Package approval", action: "Approve the install declaration",
|
|
138
|
+
consequence: `Approve this install declaration for ${mode.target.name}? y/n` },
|
|
136
139
|
};
|
|
137
140
|
case "package-forget":
|
|
138
141
|
return {
|
|
139
142
|
lines: mode.lines,
|
|
140
|
-
chrome:
|
|
143
|
+
chrome: { title: "Forget Package selection", action: "Forget the selection record",
|
|
144
|
+
consequence: `Forget the selection record of ${mode.target.name}? y/n` },
|
|
141
145
|
};
|
|
142
|
-
case "package-confirm":
|
|
146
|
+
case "package-confirm": {
|
|
147
|
+
const { action, descriptor } = mode.plan;
|
|
148
|
+
const needsApproval = mode.approval === "needs approval";
|
|
143
149
|
return {
|
|
144
150
|
lines: packagePlanLines(mode.plan, mode.approval),
|
|
145
|
-
chrome:
|
|
146
|
-
|
|
147
|
-
: `${
|
|
151
|
+
chrome: {
|
|
152
|
+
title: `Package ${action} confirmation`,
|
|
153
|
+
action: needsApproval ? `Approve and ${action} ${descriptor.name}` : `${action[0].toUpperCase()}${action.slice(1)} ${descriptor.name}`,
|
|
154
|
+
consequence: needsApproval ? "Approve this declaration and continue? y/n" : `${action} ${descriptor.name}? y/n`,
|
|
155
|
+
},
|
|
148
156
|
};
|
|
157
|
+
}
|
|
149
158
|
}
|
|
150
159
|
}
|
|
151
|
-
function reviewChrome(title, question) {
|
|
152
|
-
return { title, questions: [question], footerRows: 2 };
|
|
153
|
-
}
|
|
154
160
|
/**
|
|
155
|
-
* Pages the declaration with the arrows, PgUp/PgDn and j/k. `y` answers
|
|
156
|
-
*
|
|
161
|
+
* Pages the declaration with the arrows, PgUp/PgDn and j/k. `y` answers only once the whole declaration
|
|
162
|
+
* has been displayed. `n` and Esc run nothing.
|
|
157
163
|
*/
|
|
158
164
|
export function handlePackageKey(context, mode, input, key) {
|
|
159
165
|
const { manage, packages } = context;
|
|
@@ -162,7 +168,7 @@ export function handlePackageKey(context, mode, input, key) {
|
|
|
162
168
|
manage.browse();
|
|
163
169
|
};
|
|
164
170
|
const { lines, chrome } = packagePreview(mode);
|
|
165
|
-
const viewport =
|
|
171
|
+
const viewport = context.previewGeometry(lines, mode.cursor, chrome);
|
|
166
172
|
const cursor = movePreviewCursor(mode.cursor, viewport.total, input, key, viewport.height);
|
|
167
173
|
if (cursor !== undefined) {
|
|
168
174
|
manage.setMode({ ...mode, cursor });
|
|
@@ -172,7 +178,7 @@ export function handlePackageKey(context, mode, input, key) {
|
|
|
172
178
|
cancel("Package action cancelled");
|
|
173
179
|
return;
|
|
174
180
|
}
|
|
175
|
-
if (input
|
|
181
|
+
if (!isYes(input, key) || !viewport.reachedEnd)
|
|
176
182
|
return;
|
|
177
183
|
switch (mode.kind) {
|
|
178
184
|
case "package-approve":
|
|
@@ -190,8 +196,9 @@ export function handlePackageKey(context, mode, input, key) {
|
|
|
190
196
|
return;
|
|
191
197
|
}
|
|
192
198
|
}
|
|
193
|
-
/** Renders a Package prompt with the
|
|
199
|
+
/** Renders a Package prompt with the shared paged preview. */
|
|
194
200
|
export function PackagePanel({ mode }) {
|
|
195
201
|
const { lines, chrome } = packagePreview(mode);
|
|
196
|
-
|
|
202
|
+
const viewport = usePreviewGeometry()(lines, mode.cursor, chrome);
|
|
203
|
+
return _jsx(PagedPreview, { chrome: chrome, lines: lines, viewport: viewport });
|
|
197
204
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { classifyUpdate } from "../package-host-plans.js";
|
|
2
|
+
import { quoteText } from "../preview-model.js";
|
|
2
3
|
import { displayPackageCoordinates, displayPackageSelection, } from "../package-model.js";
|
|
3
4
|
/** Version evidence as a short label; `unknown` when the host or the bundle states none. */
|
|
4
5
|
export function packageVersionLabel(evidence, commit) {
|
|
@@ -98,7 +99,7 @@ export function packagePlanLines(plan, approval) {
|
|
|
98
99
|
...(plan.commands.length === 0
|
|
99
100
|
? ["This action runs no host command."]
|
|
100
101
|
: plan.commands.flatMap((command, position) => [
|
|
101
|
-
`Step ${position + 1} (${command.purpose}): ${
|
|
102
|
+
`Step ${position + 1} (${command.purpose}): [${[command.executable, ...command.args].map(quoteText).join(",")}]`,
|
|
102
103
|
`Executable: ${command.resolvedPath ?? command.executable}`,
|
|
103
104
|
])),
|
|
104
105
|
`Blast radius: ${plan.affects.map((coordinates) => displayPackageCoordinates(selection.source, coordinates)).join(", ")}`,
|
|
@@ -1,8 +1,9 @@
|
|
|
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 { PreEscapedText, Text } from "./safe-text.js";
|
|
3
4
|
import { useEffect, useState } from "react";
|
|
4
5
|
import { theme } from "./theme.js";
|
|
5
|
-
import {
|
|
6
|
+
import { escapeControlText, sliceSpans } from "./preview-window.js";
|
|
6
7
|
import { animationEnabled, formatElapsed, SPINNER_FRAMES, SPINNER_TICK_MS, STATIC_GLYPH } from "./working-model.js";
|
|
7
8
|
/** A titled, color-independent boundary for a focused interaction or result. */
|
|
8
9
|
export function BorderPanel({ title, children }) {
|
|
@@ -40,7 +41,6 @@ export function ChecklistLines({ choices, cursor, isSelected, keyOf, label }) {
|
|
|
40
41
|
export function HostChecklist({ choices, cursor, selected }) {
|
|
41
42
|
return _jsx(ChecklistLines, { choices: choices, cursor: cursor, isSelected: (host) => selected.includes(host), keyOf: (host) => host, label: (host) => host });
|
|
42
43
|
}
|
|
43
|
-
/** Preview lines; `warnFirst` colors the first one as a warning. */
|
|
44
44
|
const PREVIEW_FIELD_LABELS = [
|
|
45
45
|
"File:", "Name:", "Platform:", "Status:", "Homepage:", "Owned Skills:",
|
|
46
46
|
"App:", "Environment:", "Version:", "argv:", "Executable:", "Manual:",
|
|
@@ -106,51 +106,60 @@ function profilePreviewSpans(line) {
|
|
|
106
106
|
previewSpan(` ${hosts[2]}`, theme.version)];
|
|
107
107
|
return undefined;
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const start = Math.max(offset, spanStart);
|
|
116
|
-
const stop = Math.min(end, spanEnd);
|
|
117
|
-
if (start < stop)
|
|
118
|
-
visible.push({ ...span, text: span.text.slice(start - spanStart, stop - spanStart) });
|
|
119
|
-
spanStart = spanEnd;
|
|
120
|
-
}
|
|
121
|
-
return visible;
|
|
109
|
+
/** The colour of a warning, host exposure notice or refusal, which the flows indent to different depths. */
|
|
110
|
+
function noticeColor(line) {
|
|
111
|
+
const text = line.trimStart();
|
|
112
|
+
if (text.startsWith("WARNING:") || text.startsWith("ADDITIONAL HOST EXPOSURE:") || text.startsWith("Skipped "))
|
|
113
|
+
return theme.warning;
|
|
114
|
+
return text.startsWith("Cannot install ") ? theme.error : undefined;
|
|
122
115
|
}
|
|
123
|
-
function PreviewLine({ line, position,
|
|
116
|
+
function PreviewLine({ line, position, logicalLine, offset = 0 }) {
|
|
124
117
|
const profileSpans = logicalLine === undefined ? profilePreviewSpans(line) : profilePreviewSpans(logicalLine);
|
|
125
118
|
if (profileSpans) {
|
|
126
|
-
const spans = logicalLine === undefined ? profileSpans :
|
|
127
|
-
return _jsx(
|
|
119
|
+
const spans = logicalLine === undefined ? profileSpans : sliceSpans(profileSpans, offset, line.length);
|
|
120
|
+
return _jsx(PreEscapedText, { children: spans.map((span, index) => _jsx(PreEscapedText, { color: span.color, bold: span.bold, children: span.text }, index)) }, position);
|
|
128
121
|
}
|
|
129
122
|
const field = previewField(logicalLine ?? line);
|
|
130
123
|
if (field) {
|
|
131
124
|
const fieldSpans = [previewSpan(field.label, theme.field), previewSpan(field.value, theme.value)];
|
|
132
|
-
const spans = logicalLine === undefined ? fieldSpans :
|
|
133
|
-
return _jsx(
|
|
125
|
+
const spans = logicalLine === undefined ? fieldSpans : sliceSpans(fieldSpans, offset, line.length);
|
|
126
|
+
return _jsx(PreEscapedText, { children: spans.map((span, index) => _jsx(PreEscapedText, { color: span.color, children: span.text }, index)) }, position);
|
|
134
127
|
}
|
|
135
|
-
return _jsx(
|
|
128
|
+
return _jsx(PreEscapedText, { color: noticeColor(logicalLine ?? line) ?? theme.value, children: line }, position);
|
|
136
129
|
}
|
|
137
|
-
export function PreviewLines({ lines
|
|
138
|
-
return _jsx(_Fragment, { children: lines.map((line, position) => _jsx(PreviewLine, { line: line, position: position
|
|
130
|
+
export function PreviewLines({ lines }) {
|
|
131
|
+
return _jsx(_Fragment, { children: lines.map((line, position) => _jsx(PreviewLine, { line: escapeControlText(line), position: position }, position)) });
|
|
139
132
|
}
|
|
140
133
|
/** Under a list: the window indicator, how many items are marked, and the last result. */
|
|
141
134
|
export function ListFooter({ indicator, selectedCount, message }) {
|
|
142
135
|
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] }));
|
|
143
136
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
137
|
+
function footerColor(role, reachedEnd) {
|
|
138
|
+
switch (role) {
|
|
139
|
+
case "read": return { color: theme.success };
|
|
140
|
+
case "reading": return { color: theme.warning };
|
|
141
|
+
case "consequence": return { color: theme.warning };
|
|
142
|
+
case "yes": return { color: theme.success, bold: true };
|
|
143
|
+
case "no": return { color: theme.error };
|
|
144
|
+
case "action": return reachedEnd ? { color: theme.success, bold: true } : { color: theme.muted };
|
|
145
|
+
case "position":
|
|
146
|
+
case "cancel": return { color: theme.legend };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A paged preview with the shared two-row footer: reading progress, then what `y` does. `[y]` stays
|
|
151
|
+
* dimmed until `viewport` reaches the end; callers ignore `y` until then using the same viewport.
|
|
152
|
+
*/
|
|
153
|
+
export function PagedPreview({ chrome, lines: rawLines, styled: rawStyled, viewport }) {
|
|
154
|
+
// The same escape `previewViewport` applied, so rows, offsets and spans describe the same text. The rows themselves are already
|
|
155
|
+
// escaped, hence `PreEscapedText`; only the title goes through `BorderPanel`.
|
|
156
|
+
const lines = rawLines.map(escapeControlText);
|
|
157
|
+
const styled = rawStyled?.map((line) => line?.map((span) => ({ ...span, text: escapeControlText(span.text) })));
|
|
158
|
+
return (_jsxs(BorderPanel, { title: chrome.title, children: [viewport.rows.map((line, position) => {
|
|
159
|
+
const logical = viewport.sourceLines[position];
|
|
160
|
+
const custom = styled?.[logical];
|
|
161
|
+
return custom === undefined
|
|
162
|
+
? _jsx(PreviewLine, { line: line, position: position, logicalLine: lines[logical], offset: viewport.sourceOffsets[position] }, position)
|
|
163
|
+
: _jsx(PreEscapedText, { children: sliceSpans(custom, viewport.sourceOffsets[position], line.length).map((span, index) => _jsx(PreEscapedText, { color: span.color, bold: span.bold, children: span.text }, index)) }, position);
|
|
164
|
+
}), viewport.footer.map((spans, row) => _jsx(PreEscapedText, { children: spans.map((span, index) => _jsx(PreEscapedText, { ...footerColor(span.role, viewport.reachedEnd), children: span.text }, index)) }, `footer-${row}`))] }));
|
|
156
165
|
}
|