@iseoane/agent-depot 0.6.4 → 0.6.6
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 +94 -0
- package/README.es.md +14 -10
- package/README.md +22 -8
- package/dist/src/app-cli.js +10 -5
- package/dist/src/app-flow.js +2 -2
- package/dist/src/atomic-file.js +9 -0
- package/dist/src/cli.js +98 -67
- package/dist/src/executable-resolution.js +9 -3
- package/dist/src/git-source.js +2 -4
- package/dist/src/package-cli.js +9 -4
- package/dist/src/package-flow.js +2 -1
- package/dist/src/package-host-state.js +44 -6
- package/dist/src/package-state.js +2 -5
- package/dist/src/process-runner.js +234 -16
- package/dist/src/profile-cli.js +8 -6
- package/dist/src/progress.js +157 -0
- package/dist/src/self-update.js +6 -1
- package/dist/src/source-state.js +2 -5
- package/dist/src/tui/adoption-actions.js +4 -3
- package/dist/src/tui/app-actions.js +16 -9
- package/dist/src/tui/bulk-actions.js +7 -6
- package/dist/src/tui/catalog-flow.js +3 -2
- package/dist/src/tui/catalog-panel.js +2 -2
- package/dist/src/tui/catalog-view.js +26 -6
- package/dist/src/tui/file-picker.js +52 -20
- package/dist/src/tui/installations-view.js +44 -11
- package/dist/src/tui/manage-actions.js +7 -6
- package/dist/src/tui/manage-panel.js +2 -2
- package/dist/src/tui/package-actions.js +63 -31
- package/dist/src/tui/panel-parts.js +20 -0
- package/dist/src/tui/profile-view.js +84 -15
- package/dist/src/tui/sources-mode.js +2 -1
- package/dist/src/tui/sources-recipes.js +22 -10
- package/dist/src/tui/sources-view.js +13 -12
- package/dist/src/tui/text-wrap.js +13 -0
- package/dist/src/tui/unmanaged-actions.js +3 -2
- package/dist/src/tui/updates-flow.js +10 -7
- package/dist/src/tui/updates-keys.js +3 -2
- package/dist/src/tui/updates-model.js +2 -2
- package/dist/src/tui/updates-panel.js +3 -2
- package/dist/src/tui/updates-view.js +6 -3
- package/dist/src/tui/view-state.js +12 -0
- package/dist/src/tui/working-model.js +33 -0
- package/package.json +2 -2
|
@@ -5,6 +5,7 @@ import { errorText } from "./batch.js";
|
|
|
5
5
|
import { BULK_KINDS, handleBulkKey } from "./bulk-actions.js";
|
|
6
6
|
import { answerYesNo, applyChecklistOutcome, hostChecklistKey } from "./mode-keys.js";
|
|
7
7
|
import { handleUnmanagedKey, UNMANAGED_KINDS } from "./unmanaged-actions.js";
|
|
8
|
+
import { busy } from "./working-model.js";
|
|
8
9
|
const MANAGE_KINDS = new Set([
|
|
9
10
|
"remove-scope",
|
|
10
11
|
"hosts-add",
|
|
@@ -36,7 +37,7 @@ export function startUninstall(context, skill) {
|
|
|
36
37
|
void startFullUninstall(context, skill);
|
|
37
38
|
}
|
|
38
39
|
async function startFullUninstall(context, skill) {
|
|
39
|
-
context.setMode(
|
|
40
|
+
context.setMode(busy(`Preparing uninstall of ${skill.name}…`));
|
|
40
41
|
try {
|
|
41
42
|
const prepared = await prepareUninstall(context.operations, context.env, skill, context.installed);
|
|
42
43
|
if (!context.isMounted())
|
|
@@ -66,7 +67,7 @@ export function startHostAddition(context, skill) {
|
|
|
66
67
|
}
|
|
67
68
|
async function startHostChange(context, mode) {
|
|
68
69
|
const { operations, env, installed } = context;
|
|
69
|
-
context.setMode(
|
|
70
|
+
context.setMode(busy(`Preparing host change of ${mode.skill.name}…`));
|
|
70
71
|
try {
|
|
71
72
|
if (mode.kind === "hosts-add") {
|
|
72
73
|
const prepared = await prepareHostAddition(operations, env, mode.skill, installed, mode.selected);
|
|
@@ -90,7 +91,7 @@ async function startHostChange(context, mode) {
|
|
|
90
91
|
}
|
|
91
92
|
/** Runs a confirmed step in the busy mode and finishes with its outcome. */
|
|
92
93
|
async function execute(context, label, action) {
|
|
93
|
-
context.setMode(
|
|
94
|
+
context.setMode(busy(label));
|
|
94
95
|
let result;
|
|
95
96
|
try {
|
|
96
97
|
result = { kind: "ok", text: await action() };
|
|
@@ -126,17 +127,17 @@ function handleConfirmKey(context, mode, input, key) {
|
|
|
126
127
|
return;
|
|
127
128
|
case "confirm-host-exposure":
|
|
128
129
|
answerYesNo(input, key, () => {
|
|
129
|
-
void execute(context, `Changing hosts of ${mode.skill.name}
|
|
130
|
+
void execute(context, `Changing hosts of ${mode.skill.name}…`, () => runHostAddition(mode.prepared, operations, env));
|
|
130
131
|
}, cancelWith("Install cancelled"));
|
|
131
132
|
return;
|
|
132
133
|
case "confirm-host-remove":
|
|
133
134
|
answerYesNo(input, key, () => {
|
|
134
|
-
void execute(context, `Changing hosts of ${mode.skill.name}
|
|
135
|
+
void execute(context, `Changing hosts of ${mode.skill.name}…`, () => runHostRemoval(mode.prepared, operations, env, mode.skill));
|
|
135
136
|
}, cancelWith("Uninstall cancelled"));
|
|
136
137
|
return;
|
|
137
138
|
case "confirm-uninstall":
|
|
138
139
|
answerYesNo(input, key, () => {
|
|
139
|
-
void execute(context, `Removing ${mode.skill.name}
|
|
140
|
+
void execute(context, `Removing ${mode.skill.name}…`, async () => {
|
|
140
141
|
await runUninstall(mode.prepared, operations, env);
|
|
141
142
|
return `Removed ${mode.skill.name} (${mode.skill.path})`;
|
|
142
143
|
});
|
|
@@ -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, DecisionPrompt, HostChecklist, PreviewConfirm, PreviewLines } from "./panel-parts.js";
|
|
5
|
+
import { ChecklistLines, DecisionPrompt, HostChecklist, PreviewConfirm, PreviewLines, Working } 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 }) {
|
|
@@ -66,6 +66,6 @@ export function ManagePanel({ mode }) {
|
|
|
66
66
|
case "confirm-bulk-exposure":
|
|
67
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?" })] }));
|
|
68
68
|
case "busy":
|
|
69
|
-
return _jsx(
|
|
69
|
+
return _jsx(Working, { label: mode.label, since: mode.startedAt });
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { describeCoordinates } from "../package-model.js";
|
|
3
3
|
import { errorText } from "./batch.js";
|
|
4
|
-
import {
|
|
4
|
+
import { movePreviewCursor } from "./mode-keys.js";
|
|
5
5
|
import { installedEvidenceLabel, packagePlanLines } from "./package-rows.js";
|
|
6
6
|
import { PreviewConfirm } from "./panel-parts.js";
|
|
7
|
+
import { previewViewport } from "./preview-window.js";
|
|
8
|
+
import { busy } from "./working-model.js";
|
|
7
9
|
/** The per-row keys of a Package; Enter is approval, handled beside these. */
|
|
8
10
|
export const PACKAGE_ACTION_KEYS = new Map([
|
|
9
11
|
["i", "install"],
|
|
@@ -22,16 +24,16 @@ export async function startPackageAction(context, target, input) {
|
|
|
22
24
|
manage.setMessage(undefined);
|
|
23
25
|
try {
|
|
24
26
|
if (input === "approve") {
|
|
25
|
-
manage.setMode(
|
|
27
|
+
manage.setMode(busy(`Preparing the declaration of ${target.name}…`));
|
|
26
28
|
const plan = await packages.planLifecycle(target.selection, "install");
|
|
27
29
|
if (manage.isMounted()) {
|
|
28
|
-
manage.setMode({ kind: "package-approve", target, lines: packagePlanLines(plan, "needs approval") });
|
|
30
|
+
manage.setMode({ kind: "package-approve", target, lines: packagePlanLines(plan, "needs approval"), cursor: 0 });
|
|
29
31
|
}
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
34
|
const action = PACKAGE_ACTION_KEYS.get(input);
|
|
33
35
|
if (action === "forget") {
|
|
34
|
-
manage.setMode(
|
|
36
|
+
manage.setMode(busy(`Preparing to forget ${target.name}…`));
|
|
35
37
|
const inspection = await packages.inspect(target.selection);
|
|
36
38
|
if (manage.isMounted()) {
|
|
37
39
|
manage.setMode({
|
|
@@ -43,16 +45,17 @@ export async function startPackageAction(context, target, input) {
|
|
|
43
45
|
`Installed: ${installedEvidenceLabel(inspection.installed)}`,
|
|
44
46
|
"Forgetting drops the Agent Depot selection record only; the host install is left as it is and no host command runs.",
|
|
45
47
|
],
|
|
48
|
+
cursor: 0,
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
50
53
|
if (action !== undefined) {
|
|
51
|
-
manage.setMode(
|
|
54
|
+
manage.setMode(busy(`Preparing the ${action} of ${target.name}…`));
|
|
52
55
|
const plan = await packages.planLifecycle(target.selection, action);
|
|
53
56
|
const approval = await packages.approvalStatus(target.selection, action);
|
|
54
57
|
if (manage.isMounted())
|
|
55
|
-
manage.setMode({ kind: "package-confirm", plan, approval });
|
|
58
|
+
manage.setMode({ kind: "package-confirm", plan, approval, cursor: 0 });
|
|
56
59
|
return;
|
|
57
60
|
}
|
|
58
61
|
await manage.finish({ kind: "error", text: `The action ${JSON.stringify(input)} is not available for Packages` });
|
|
@@ -64,7 +67,7 @@ export async function startPackageAction(context, target, input) {
|
|
|
64
67
|
/** The `y` of a lifecycle confirmation whose receipt is missing approves it first, then asks again. */
|
|
65
68
|
async function approveThenConfirm(context, mode) {
|
|
66
69
|
const { manage, packages } = context;
|
|
67
|
-
manage.setMode(
|
|
70
|
+
manage.setMode(busy(`Approving the ${mode.plan.action} declaration of ${mode.plan.descriptor.name}…`));
|
|
68
71
|
try {
|
|
69
72
|
await packages.approve(mode.plan.selection, mode.plan.action);
|
|
70
73
|
if (manage.isMounted())
|
|
@@ -100,7 +103,7 @@ function resultLine(plan, result) {
|
|
|
100
103
|
}
|
|
101
104
|
async function executePackagePlan(context, plan) {
|
|
102
105
|
const { manage, packages } = context;
|
|
103
|
-
manage.setMode(
|
|
106
|
+
manage.setMode(busy(`Running the ${plan.action} of ${plan.descriptor.name}…`));
|
|
104
107
|
let result;
|
|
105
108
|
try {
|
|
106
109
|
result = await packages.executeLifecycle(plan, true);
|
|
@@ -114,7 +117,7 @@ async function executePackagePlan(context, plan) {
|
|
|
114
117
|
}
|
|
115
118
|
async function forgetSelection(context, mode) {
|
|
116
119
|
const { manage, packages } = context;
|
|
117
|
-
manage.setMode(
|
|
120
|
+
manage.setMode(busy(`Forgetting ${mode.target.name}…`));
|
|
118
121
|
try {
|
|
119
122
|
await packages.forget(mode.target.selection, true);
|
|
120
123
|
await manage.finish({ kind: "ok", text: `Forgot ${mode.target.name}; no host command ran` });
|
|
@@ -123,43 +126,72 @@ async function forgetSelection(context, mode) {
|
|
|
123
126
|
await manage.finish({ kind: "error", text: errorText(error) });
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
|
-
/**
|
|
129
|
+
/** What the preview shows for a mode, so the key handler and the panel page the same rows. */
|
|
130
|
+
function packagePreview(mode) {
|
|
131
|
+
switch (mode.kind) {
|
|
132
|
+
case "package-approve":
|
|
133
|
+
return {
|
|
134
|
+
lines: mode.lines,
|
|
135
|
+
chrome: reviewChrome("Package approval", `Approve this install declaration for ${mode.target.name}? y/n`),
|
|
136
|
+
};
|
|
137
|
+
case "package-forget":
|
|
138
|
+
return {
|
|
139
|
+
lines: mode.lines,
|
|
140
|
+
chrome: reviewChrome("Forget Package selection", `Forget the selection record of ${mode.target.name}? y/n`),
|
|
141
|
+
};
|
|
142
|
+
case "package-confirm":
|
|
143
|
+
return {
|
|
144
|
+
lines: packagePlanLines(mode.plan, mode.approval),
|
|
145
|
+
chrome: reviewChrome(`Package ${mode.plan.action} confirmation`, mode.approval === "needs approval"
|
|
146
|
+
? "Approve this declaration and continue? y/n"
|
|
147
|
+
: `${mode.plan.action} ${mode.plan.descriptor.name}? y/n`),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function reviewChrome(title, question) {
|
|
152
|
+
return { title, questions: [question], footerRows: 2 };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Pages the declaration with the arrows, PgUp/PgDn and j/k. `y` answers from any page: the panel shows
|
|
156
|
+
* the `Review N of M` position and the row window while more remains. `n` and Esc run nothing.
|
|
157
|
+
*/
|
|
127
158
|
export function handlePackageKey(context, mode, input, key) {
|
|
128
159
|
const { manage, packages } = context;
|
|
129
160
|
const cancel = (text) => {
|
|
130
161
|
manage.setMessage({ kind: "ok", text });
|
|
131
162
|
manage.browse();
|
|
132
163
|
};
|
|
164
|
+
const { lines, chrome } = packagePreview(mode);
|
|
165
|
+
const viewport = previewViewport(lines, mode.cursor, context.terminalRows, context.terminalColumns, chrome);
|
|
166
|
+
const cursor = movePreviewCursor(mode.cursor, viewport.total, input, key, viewport.height);
|
|
167
|
+
if (cursor !== undefined) {
|
|
168
|
+
manage.setMode({ ...mode, cursor });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (input === "n" || key.escape) {
|
|
172
|
+
cancel("Package action cancelled");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (input !== "y")
|
|
176
|
+
return;
|
|
133
177
|
switch (mode.kind) {
|
|
134
178
|
case "package-approve":
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
void packages.approve(mode.target.selection, "install").then(() => manage.finish({ kind: "ok", text: `Approved the install declaration of ${mode.target.name}` }), error => manage.finish({ kind: "error", text: errorText(error) }));
|
|
138
|
-
}, () => cancel("Package action cancelled"));
|
|
179
|
+
manage.setMode(busy(`Approving the install declaration of ${mode.target.name}…`));
|
|
180
|
+
void packages.approve(mode.target.selection, "install").then(() => manage.finish({ kind: "ok", text: `Approved the install declaration of ${mode.target.name}` }), error => manage.finish({ kind: "error", text: errorText(error) }));
|
|
139
181
|
return;
|
|
140
182
|
case "package-forget":
|
|
141
|
-
|
|
183
|
+
void forgetSelection(context, mode);
|
|
142
184
|
return;
|
|
143
185
|
case "package-confirm":
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
void executePackagePlan(context, mode.plan);
|
|
149
|
-
}, () => cancel("Package action cancelled"));
|
|
186
|
+
if (mode.approval === "needs approval")
|
|
187
|
+
void approveThenConfirm(context, mode);
|
|
188
|
+
else
|
|
189
|
+
void executePackagePlan(context, mode.plan);
|
|
150
190
|
return;
|
|
151
191
|
}
|
|
152
192
|
}
|
|
153
193
|
/** Renders a Package prompt with the existing preview panel and y/n confirmation. */
|
|
154
194
|
export function PackagePanel({ mode }) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return _jsx(PreviewConfirm, { lines: mode.lines, question: `Approve this install declaration for ${mode.target.name}? y/n` });
|
|
158
|
-
case "package-forget":
|
|
159
|
-
return _jsx(PreviewConfirm, { lines: mode.lines, question: `Forget the selection record of ${mode.target.name}? y/n` });
|
|
160
|
-
case "package-confirm":
|
|
161
|
-
return (_jsx(PreviewConfirm, { lines: packagePlanLines(mode.plan, mode.approval), question: mode.approval === "needs approval"
|
|
162
|
-
? "Approve this declaration and continue? y/n"
|
|
163
|
-
: `${mode.plan.action} ${mode.plan.descriptor.name}? y/n` }));
|
|
164
|
-
}
|
|
195
|
+
const { lines, chrome } = packagePreview(mode);
|
|
196
|
+
return _jsx(PreviewConfirm, { title: chrome.title, lines: lines, cursor: mode.cursor, question: chrome.questions[0], chrome: chrome });
|
|
165
197
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useWindowSize } from "ink";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
3
4
|
import { theme } from "./theme.js";
|
|
4
5
|
import { previewViewport } from "./preview-window.js";
|
|
6
|
+
import { animationEnabled, formatElapsed, SPINNER_FRAMES, SPINNER_TICK_MS, STATIC_GLYPH } from "./working-model.js";
|
|
5
7
|
/** A titled, color-independent boundary for a focused interaction or result. */
|
|
6
8
|
export function BorderPanel({ title, children }) {
|
|
7
9
|
return (_jsxs(Box, { borderStyle: "round", borderColor: theme.border, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.title, bold: true, children: title }), children] }));
|
|
@@ -9,6 +11,24 @@ export function BorderPanel({ title, children }) {
|
|
|
9
11
|
export function FieldLine({ label, value }) {
|
|
10
12
|
return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: label }), _jsxs(Text, { color: theme.value, children: [" ", value] })] });
|
|
11
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* A spinner, the label and, from one second on, the time since `since` (when the operation began, not
|
|
16
|
+
* when this line mounted, so the clock survives label changes). Without animation it is a static line.
|
|
17
|
+
*/
|
|
18
|
+
export function Working({ label, since, now, animate }) {
|
|
19
|
+
const animated = animate ?? animationEnabled();
|
|
20
|
+
const [tick, setTick] = useState(0);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!animated)
|
|
23
|
+
return undefined;
|
|
24
|
+
const timer = setInterval(() => setTick((value) => value + 1), SPINNER_TICK_MS);
|
|
25
|
+
// A redraw timer alone must never keep the process alive.
|
|
26
|
+
timer.unref();
|
|
27
|
+
return () => clearInterval(timer);
|
|
28
|
+
}, [animated]);
|
|
29
|
+
const elapsed = animated ? formatElapsed((now ?? Date.now)() - since) : "";
|
|
30
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.accent, children: animated ? SPINNER_FRAMES[tick % SPINNER_FRAMES.length] : STATIC_GLYPH }), _jsxs(Text, { color: theme.value, children: [" ", label] }), elapsed ? _jsxs(Text, { color: theme.legend, children: [" ", elapsed] }) : null] }));
|
|
31
|
+
}
|
|
12
32
|
export function DecisionPrompt({ children }) {
|
|
13
33
|
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
34
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { Box, Text, useWindowSize } from "ink";
|
|
4
4
|
import stringWidth from "string-width";
|
|
@@ -12,12 +12,14 @@ import { packageOperationsOf } from "./environment.js";
|
|
|
12
12
|
import { DEFAULT_PROFILE_FILE_NAME, exportDestination, fileNameNotice, pathExists, pickerStartDirectory, resolveExportFileName, } from "./file-picker-model.js";
|
|
13
13
|
import { FilePicker } from "./file-picker.js";
|
|
14
14
|
import { useKeys } from "./keys.js";
|
|
15
|
+
import { previewViewport } from "./preview-window.js";
|
|
15
16
|
import { answerYesNo, moveCursor, movePreviewCursor, toggleChoice } from "./mode-keys.js";
|
|
16
|
-
import { BorderPanel, PreviewConfirm, PreviewLines } from "./panel-parts.js";
|
|
17
|
-
import { fitLines, wrapText } from "./text-wrap.js";
|
|
17
|
+
import { BorderPanel, PreviewConfirm, PreviewLines, Working } from "./panel-parts.js";
|
|
18
|
+
import { elideMiddle, fitLines, wrapText } from "./text-wrap.js";
|
|
18
19
|
import { theme } from "./theme.js";
|
|
19
20
|
import { useMirrored, useMounted } from "./view-state.js";
|
|
20
21
|
import { computeWindow, useListHeight } from "./window.js";
|
|
22
|
+
import { busy } from "./working-model.js";
|
|
21
23
|
function rowsFromExport(plan) {
|
|
22
24
|
return [
|
|
23
25
|
...plan.profile.sources.map((value, index) => ({
|
|
@@ -105,6 +107,37 @@ function physicalListWindow(rows, rowHeights, selected, height) {
|
|
|
105
107
|
return { ...range, indicator: hidden ? `${range.start + 1}–${range.end} of ${rows.length}` : undefined, stickyGroup };
|
|
106
108
|
}
|
|
107
109
|
/** The four import blocks in one indexed list, so the checklist and the confirmation share positions. */
|
|
110
|
+
/** An error naming a long path keeps its first and last rows, where the path starts and the reason ends. */
|
|
111
|
+
const NAME_ERROR_ROWS = 3;
|
|
112
|
+
const NAME_HINT = "Type to edit · Ctrl+U clear · Enter continue · Esc back to folder";
|
|
113
|
+
/**
|
|
114
|
+
* A `Label: value` line of the export name stage, wrapped to the panel and kept within `rows` rows. The
|
|
115
|
+
* end of a path (its last folder, the file name) and the end of the typed name (where the cursor is) are
|
|
116
|
+
* what matter, so with fewer than three rows the field keeps its label and its tail behind a `…`; with
|
|
117
|
+
* three or more a path keeps its first row too, around a count of the rows left out.
|
|
118
|
+
*/
|
|
119
|
+
function NameField({ label, value, rows, width, tail = false }) {
|
|
120
|
+
const wrapped = wrapText(`${label}${value}`, width);
|
|
121
|
+
const cut = wrapped.length > rows;
|
|
122
|
+
const keepsMiddleCount = cut && !tail && rows > 2;
|
|
123
|
+
let shown = wrapped;
|
|
124
|
+
if (keepsMiddleCount)
|
|
125
|
+
shown = elideMiddle(wrapped, rows, width);
|
|
126
|
+
else if (cut) {
|
|
127
|
+
const characters = Array.from(value);
|
|
128
|
+
for (let dropped = 1; dropped <= characters.length; dropped += 1) {
|
|
129
|
+
shown = wrapText(`${label}…${characters.slice(dropped).join("")}`, width);
|
|
130
|
+
if (shown.length <= rows)
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return _jsx(_Fragment, { children: shown.map((line, index) => {
|
|
135
|
+
if (index === 0 && line.startsWith(label)) {
|
|
136
|
+
return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: label }), _jsx(Text, { color: theme.value, children: line.slice(label.length) })] }, index);
|
|
137
|
+
}
|
|
138
|
+
return _jsx(Text, { color: keepsMiddleCount && index === 1 ? theme.muted : theme.value, children: line }, index);
|
|
139
|
+
}) });
|
|
140
|
+
}
|
|
108
141
|
function planEntries(plan) {
|
|
109
142
|
return [...plan.items, ...plan.packages];
|
|
110
143
|
}
|
|
@@ -159,6 +192,9 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
159
192
|
const [cursor, cursorRef, cursorTo] = useMirrored(0);
|
|
160
193
|
const [text, typed, setText] = useMirrored("");
|
|
161
194
|
const [nameError, setNameError] = useState(undefined);
|
|
195
|
+
// What the busy mode is doing, and when the name check of the export file began, if one is running.
|
|
196
|
+
const [working, setWorking] = useState(undefined);
|
|
197
|
+
const [nameChecking, setNameChecking] = useState(undefined);
|
|
162
198
|
const [importNotice, setImportNotice] = useState(undefined);
|
|
163
199
|
const [exportDirectory, setExportDirectory] = useState(undefined);
|
|
164
200
|
const [importDirectory, setImportDirectory] = useState(undefined);
|
|
@@ -170,7 +206,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
170
206
|
const [profile, setProfile] = useState();
|
|
171
207
|
const [plan, setPlan] = useState();
|
|
172
208
|
const diagnostics = useRef([]);
|
|
173
|
-
const { columns: terminalColumns } = useWindowSize();
|
|
209
|
+
const { rows: terminalRows, columns: terminalColumns } = useWindowSize();
|
|
174
210
|
const panelWidth = Math.max((terminalColumns || 80) - 4, 1);
|
|
175
211
|
const hintCursorRow = (mode === "export-list" ? exportDisplayRows(rows, expandedGroupsRef.current) : rows)[cursorRef.current];
|
|
176
212
|
const enterHint = hintCursorRow === undefined ? undefined
|
|
@@ -184,13 +220,24 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
184
220
|
const titleRows = Math.max(Math.ceil(stringWidth("Export Profile") / panelWidth), 1);
|
|
185
221
|
const hintRows = Math.max(Math.ceil(stringWidth(hintText) / panelWidth), 1);
|
|
186
222
|
const height = useListHeight(listHeight, 4 + titleRows + hintRows);
|
|
223
|
+
const confirmChrome = () => ({
|
|
224
|
+
title: "Confirmation",
|
|
225
|
+
questions: [confirmRequest.current?.kind === "export"
|
|
226
|
+
? "Save/Export Profile? (y/n) · arrows/PgUp/PgDn page · j/k line"
|
|
227
|
+
: "Apply Profile? (y/n) · arrows/PgUp/PgDn page · j/k line"],
|
|
228
|
+
footerRows: 2,
|
|
229
|
+
});
|
|
187
230
|
const showLines = (next, nextMode) => {
|
|
188
231
|
setLines(next);
|
|
189
232
|
cursorTo(0);
|
|
190
233
|
setMode(nextMode);
|
|
191
234
|
};
|
|
192
|
-
const
|
|
235
|
+
const startBusy = (label) => {
|
|
236
|
+
setWorking(busy(label));
|
|
193
237
|
setMode("busy");
|
|
238
|
+
};
|
|
239
|
+
const run = async (label, work) => {
|
|
240
|
+
startBusy(label);
|
|
194
241
|
try {
|
|
195
242
|
await work();
|
|
196
243
|
}
|
|
@@ -240,7 +287,10 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
240
287
|
setNameError(undefined);
|
|
241
288
|
setMode("export-folder");
|
|
242
289
|
};
|
|
243
|
-
const abandonNameCheck = () => {
|
|
290
|
+
const abandonNameCheck = () => {
|
|
291
|
+
nameRequest.current += 1;
|
|
292
|
+
setNameChecking(undefined);
|
|
293
|
+
};
|
|
244
294
|
const chooseExportDirectory = (directory) => {
|
|
245
295
|
abandonNameCheck();
|
|
246
296
|
setExportDirectory(directory);
|
|
@@ -259,6 +309,8 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
259
309
|
const fileName = outcome.fileName;
|
|
260
310
|
const finalPath = exportDestination(directory, fileName);
|
|
261
311
|
const id = ++nameRequest.current;
|
|
312
|
+
// The check never blocks keys, so a line beside the name field shows it is running.
|
|
313
|
+
setNameChecking(Date.now());
|
|
262
314
|
void (async () => {
|
|
263
315
|
let exists;
|
|
264
316
|
try {
|
|
@@ -267,9 +319,12 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
267
319
|
catch (error) {
|
|
268
320
|
if (!mounted.current || nameRequest.current !== id || modeRef.current !== "export-name")
|
|
269
321
|
return;
|
|
322
|
+
setNameChecking(undefined);
|
|
270
323
|
setNameError(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
271
324
|
return;
|
|
272
325
|
}
|
|
326
|
+
if (mounted.current && nameRequest.current === id)
|
|
327
|
+
setNameChecking(undefined);
|
|
273
328
|
const current = resolveExportFileName(typed.current);
|
|
274
329
|
if (!mounted.current || nameRequest.current !== id || modeRef.current !== "export-name")
|
|
275
330
|
return;
|
|
@@ -288,7 +343,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
288
343
|
const request = confirmRequest.current;
|
|
289
344
|
if (request === undefined)
|
|
290
345
|
return;
|
|
291
|
-
void run(async () => {
|
|
346
|
+
void run(request.kind === "export" ? "Exporting…" : "Applying import…", async () => {
|
|
292
347
|
let result;
|
|
293
348
|
if (request.kind === "export") {
|
|
294
349
|
await writeProfileExport(request.file, chosenProfile());
|
|
@@ -304,7 +359,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
304
359
|
});
|
|
305
360
|
};
|
|
306
361
|
const previewImport = () => {
|
|
307
|
-
void run(async () => {
|
|
362
|
+
void run("Preparing import preview…", async () => {
|
|
308
363
|
// Reuse the CLI's core preview for the exact selected subset, including argv and Source notes.
|
|
309
364
|
const entries = selectedPlanEntries(plan, selectedRef.current);
|
|
310
365
|
const subset = selectedImportProfile(profile, entries);
|
|
@@ -320,7 +375,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
320
375
|
return;
|
|
321
376
|
setImportDirectory(selection.directory);
|
|
322
377
|
const id = ++importRequest.current;
|
|
323
|
-
|
|
378
|
+
startBusy("Loading Profile…");
|
|
324
379
|
void (async () => {
|
|
325
380
|
try {
|
|
326
381
|
const importedProfile = parseProfile(JSON.parse(await readFile(selection.file, "utf8")));
|
|
@@ -347,7 +402,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
347
402
|
return;
|
|
348
403
|
if (current === "browse" || current === "results") {
|
|
349
404
|
if (input === "e") {
|
|
350
|
-
void run(async () => {
|
|
405
|
+
void run("Loading Profile…", async () => {
|
|
351
406
|
const exported = await buildProfileExport(operations, apps, {
|
|
352
407
|
homeDirectory: environment.homeDirectory, packageOperations: packages,
|
|
353
408
|
});
|
|
@@ -416,7 +471,9 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
416
471
|
answerYesNo(input, key, confirmApply, () => setMode(confirmRequest.current?.kind === "import" ? "import-list" : "export-name"));
|
|
417
472
|
if (input === "y" || input === "n" || key.escape)
|
|
418
473
|
return;
|
|
419
|
-
|
|
474
|
+
// The preview cursor counts wrapped rows, not source lines, or a long destination would cap the paging.
|
|
475
|
+
const viewport = previewViewport(lines, cursorRef.current, terminalRows || 24, terminalColumns || 80, confirmChrome());
|
|
476
|
+
const next = movePreviewCursor(cursorRef.current, viewport.total, input, key, viewport.height);
|
|
420
477
|
if (next !== undefined)
|
|
421
478
|
cursorTo(Math.max(0, next));
|
|
422
479
|
return;
|
|
@@ -521,9 +578,21 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
521
578
|
const liveDestination = nameOutcome.kind === "ok"
|
|
522
579
|
? exportDestination(exportDirectory ?? startDirectory, nameOutcome.fileName) : undefined;
|
|
523
580
|
const nameNote = nameOutcome.kind === "ok" ? fileNameNotice(nameOutcome.fileName) : undefined;
|
|
581
|
+
const extensionNote = nameOutcome.kind === "ok" && nameOutcome.appendedExtension
|
|
582
|
+
? `A name without an extension is saved as ${nameOutcome.fileName}.` : undefined;
|
|
583
|
+
const textRows = (value) => value === undefined ? 0 : wrapText(value, panelWidth).length;
|
|
584
|
+
const nameErrorLines = nameError === undefined ? [] : wrapText(nameError, panelWidth);
|
|
585
|
+
// Keys type into the name, so the stage cannot scroll; instead each long field keeps its end (and, given
|
|
586
|
+
// three rows or more, its first row) within a share of the terminal. The confirmation pages the full destination.
|
|
587
|
+
const nameFixedRows = 3 + textRows(nameNote) + textRows(extensionNote) + Math.min(nameErrorLines.length, NAME_ERROR_ROWS)
|
|
588
|
+
+ (nameChecking === undefined ? 0 : 1) + textRows(NAME_HINT);
|
|
589
|
+
const fieldRows = useListHeight(listHeight, nameFixedRows, 3);
|
|
590
|
+
// Leftover rows go to the paths, which are the long fields, before the typed name.
|
|
591
|
+
const folderRows = Math.floor(fieldRows / 3) + (fieldRows % 3 >= 2 ? 1 : 0);
|
|
592
|
+
const destinationRows = Math.floor(fieldRows / 3) + (fieldRows % 3 >= 1 ? 1 : 0);
|
|
593
|
+
const nameRows = fieldRows - folderRows - destinationRows;
|
|
524
594
|
return _jsxs(Box, { flexDirection: "column", children: [mode === "export-folder" ? _jsx(FilePicker, { purpose: "directory", listHeight: listHeight, startDirectory: exportDirectory ?? startDirectory, onCancel: () => setMode("export-list"), onChoose: selection => { if (selection.kind === "directory")
|
|
525
|
-
chooseExportDirectory(selection.directory); } }) : null, mode === "import-browse" ? _jsx(FilePicker, { purpose: "file", listHeight: listHeight, startDirectory: importDirectory ?? startDirectory, notice: importNotice, onCancel: () => setMode("browse"), onChoose: chooseImportFile }) : null, listing ? _jsxs(BorderPanel, { title: mode === "export-list" ? "Export Profile" : "Import Profile", children: [listContent, _jsx(Text, { color: theme.legend, children: hintText }), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null] }) : null, mode === "browse" ? _jsx(BorderPanel, { title: "Portable Profile", children: _jsx(Text, { color: theme.legend, children: "e export \u00B7 i import" }) }) : null, mode === "busy" ? _jsx(
|
|
526
|
-
?
|
|
527
|
-
|
|
528
|
-
: "Apply Profile? (y/n) · arrows/PgUp/PgDn page · j/k line" }) : null, mode === "detail" ? _jsx(PreviewConfirm, { title: "Export note", lines: lines, cursor: cursor, question: "Esc close \u00B7 arrows/PgUp/PgDn page \u00B7 j/k line" }) : null, mode === "results" ? _jsxs(_Fragment, { children: [_jsx(PreviewLines, { lines: lines.slice(window.start, window.end) }), _jsx(Text, { children: "j/k scroll" })] }) : null, window.indicator && mode === "results" ? _jsx(Text, { children: window.indicator }) : null] });
|
|
595
|
+
chooseExportDirectory(selection.directory); } }) : null, mode === "import-browse" ? _jsx(FilePicker, { purpose: "file", listHeight: listHeight, startDirectory: importDirectory ?? startDirectory, notice: importNotice, onCancel: () => setMode("browse"), onChoose: chooseImportFile }) : null, listing ? _jsxs(BorderPanel, { title: mode === "export-list" ? "Export Profile" : "Import Profile", children: [listContent, _jsx(Text, { color: theme.legend, children: hintText }), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null] }) : null, mode === "browse" ? _jsx(BorderPanel, { title: "Portable Profile", children: _jsx(Text, { color: theme.legend, children: "e export \u00B7 i import" }) }) : null, mode === "busy" && working ? _jsx(Working, { label: working.label, since: working.startedAt }) : null, mode === "export-name" ? _jsxs(BorderPanel, { title: "Export file name", children: [_jsx(NameField, { label: "Folder: ", value: exportDirectory ?? startDirectory, rows: folderRows, width: panelWidth }), _jsx(NameField, { label: "Name: ", value: `${text}_`, rows: nameRows, width: panelWidth, tail: true }), liveDestination === undefined ? null : _jsx(NameField, { label: "Destination: ", value: liveDestination, rows: destinationRows, width: panelWidth }), nameNote === undefined ? null : _jsx(Text, { color: theme.warning, children: nameNote }), extensionNote === undefined ? null : _jsx(Text, { color: theme.muted, children: extensionNote }), nameError === undefined ? null : nameErrorLines.length <= NAME_ERROR_ROWS
|
|
596
|
+
? _jsx(Text, { color: theme.error, children: nameError })
|
|
597
|
+
: elideMiddle(nameErrorLines, NAME_ERROR_ROWS, panelWidth).map((line, index) => _jsx(Text, { color: theme.error, children: line }, index)), nameChecking === undefined ? null : _jsx(Working, { label: "Checking name\u2026", since: nameChecking }), _jsx(Text, { color: theme.legend, children: NAME_HINT })] }) : null, mode === "confirm" ? _jsx(PreviewConfirm, { lines: lines, cursor: cursor, chrome: confirmChrome(), question: confirmChrome().questions[0] }) : null, mode === "detail" ? _jsx(PreviewConfirm, { title: "Export note", lines: lines, cursor: cursor, question: "Esc close \u00B7 arrows/PgUp/PgDn page \u00B7 j/k line" }) : null, mode === "results" ? _jsxs(_Fragment, { children: [_jsx(PreviewLines, { lines: lines.slice(window.start, window.end) }), _jsx(Text, { children: "j/k scroll" })] }) : null, window.indicator && mode === "results" ? _jsx(Text, { children: window.indicator }) : null] });
|
|
529
598
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { busy } from "./working-model.js";
|
|
1
2
|
export const initialMode = { kind: "browse" };
|
|
2
3
|
export function modeReducer(mode, event) {
|
|
3
4
|
switch (event.type) {
|
|
@@ -14,7 +15,7 @@ export function modeReducer(mode, event) {
|
|
|
14
15
|
case "preview-cursor":
|
|
15
16
|
return mode.kind === "confirm" && mode.action === "refresh" ? { ...mode, cursor: event.cursor } : mode;
|
|
16
17
|
case "busy":
|
|
17
|
-
return
|
|
18
|
+
return busy(event.label, mode, () => event.startedAt);
|
|
18
19
|
case "done":
|
|
19
20
|
return initialMode;
|
|
20
21
|
}
|