@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
|
@@ -1,101 +1,189 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { Box, Text } from "ink";
|
|
4
|
-
import
|
|
3
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
4
|
+
import stringWidth from "string-width";
|
|
5
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
5
6
|
import { createAppOperations } from "../app-flow.js";
|
|
6
|
-
import { buildProfileExport, describeProfilePackage, profileExportPreview, writeProfileExport, } from "../profile-export.js";
|
|
7
|
+
import { buildProfileExport, describeProfileExportExclusion, describeProfilePackage, describeProfileReplica, describeProfileSkill, profileExportPreview, SKILL_EXPORT_EXPLANATION, writeProfileExport, } from "../profile-export.js";
|
|
7
8
|
import { applyProfileImport, buildProfileImport, } from "../profile-import.js";
|
|
8
|
-
import { PROFILE_FORMAT } from "../profile.js";
|
|
9
|
+
import { PROFILE_FORMAT, PROFILE_FORMAT_V2, parseProfile } from "../profile.js";
|
|
10
|
+
import { selectionKey } from "../package-model.js";
|
|
9
11
|
import { packageOperationsOf } from "./environment.js";
|
|
12
|
+
import { DEFAULT_PROFILE_FILE_NAME, exportDestination, fileNameNotice, pathExists, pickerStartDirectory, resolveExportFileName, } from "./file-picker-model.js";
|
|
13
|
+
import { FilePicker } from "./file-picker.js";
|
|
10
14
|
import { useKeys } from "./keys.js";
|
|
11
|
-
import { answerYesNo, moveCursor, toggleChoice } from "./mode-keys.js";
|
|
12
|
-
import {
|
|
15
|
+
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";
|
|
18
|
+
import { theme } from "./theme.js";
|
|
19
|
+
import { useMirrored, useMounted } from "./view-state.js";
|
|
13
20
|
import { computeWindow, useListHeight } from "./window.js";
|
|
14
21
|
function rowsFromExport(plan) {
|
|
15
22
|
return [
|
|
16
23
|
...plan.profile.sources.map((value, index) => ({
|
|
17
|
-
block: "sources", index,
|
|
24
|
+
id: `sources:${value.url}`, block: "sources", index, ordinal: index + 1,
|
|
25
|
+
label: value.url, selectable: true,
|
|
18
26
|
})),
|
|
19
27
|
...plan.profile.skills.map((value, index) => ({
|
|
20
|
-
block: "skills", index,
|
|
28
|
+
id: `skills:${JSON.stringify([value.source, value.path])}`, block: "skills", index, ordinal: index + 1,
|
|
29
|
+
label: describeProfileSkill(value), selectable: true,
|
|
21
30
|
})),
|
|
22
31
|
...plan.profile.apps.map((value, index) => ({
|
|
23
|
-
|
|
32
|
+
id: `apps:${JSON.stringify([value.name, value.platform ?? null])}`, block: "apps", index, ordinal: index + 1,
|
|
33
|
+
label: `App recipe ${value.name} (${value.platform ?? "all platforms"})`, selectable: true,
|
|
24
34
|
})),
|
|
25
35
|
...plan.profile.packages.map((value, index) => ({
|
|
26
|
-
block: "packages", index,
|
|
36
|
+
id: `packages:${JSON.stringify(value)}`, block: "packages", index, ordinal: index + 1,
|
|
37
|
+
label: describeProfilePackage(value), selectable: true,
|
|
38
|
+
})),
|
|
39
|
+
...plan.profile.replicas.map((value, index) => ({
|
|
40
|
+
id: `replicas:${JSON.stringify(value)}`, block: "replicas", index, ordinal: index + 1,
|
|
41
|
+
label: describeProfileReplica(value), selectable: true,
|
|
27
42
|
})),
|
|
28
43
|
...plan.exclusions.map((item, index) => ({
|
|
29
|
-
block: "excluded", index, label:
|
|
44
|
+
id: `excluded:${index}`, block: "excluded", index, label: describeProfileExportExclusion(item), selectable: false,
|
|
30
45
|
})),
|
|
46
|
+
{ id: "skill-export-explanation", block: "excluded", index: -1,
|
|
47
|
+
label: SKILL_EXPORT_EXPLANATION, selectable: false },
|
|
31
48
|
...plan.warnings.filter(warning => warning.kind === "load").map((warning, index) => ({
|
|
32
|
-
block: "excluded", index: plan.exclusions.length + index,
|
|
49
|
+
id: `warning:${index}`, block: "excluded", index: plan.exclusions.length + index,
|
|
50
|
+
label: warning.message, selectable: false,
|
|
33
51
|
})),
|
|
34
52
|
];
|
|
35
53
|
}
|
|
54
|
+
function exportDisplayRows(rows, expanded) {
|
|
55
|
+
const choices = (block) => rows.filter(row => row.block === block);
|
|
56
|
+
const group = (id, label, depth, children) => {
|
|
57
|
+
if (!children.length)
|
|
58
|
+
return [];
|
|
59
|
+
const header = { kind: "group", id, label, depth };
|
|
60
|
+
return expanded.has(id) ? [header, ...children] : [header];
|
|
61
|
+
};
|
|
62
|
+
const sourceChildren = [
|
|
63
|
+
...group("group:sources", `Source choices (${choices("sources").length})`, 1, choices("sources")),
|
|
64
|
+
...group("group:skills", `Skills (${choices("skills").length})`, 1, choices("skills")),
|
|
65
|
+
...group("group:apps", `Apps (${choices("apps").length})`, 1, choices("apps")),
|
|
66
|
+
...group("group:packages", `Packages (${choices("packages").length})`, 1, choices("packages")),
|
|
67
|
+
];
|
|
68
|
+
return [
|
|
69
|
+
...group("group:sources-root", "Sources", 0, sourceChildren),
|
|
70
|
+
...group("group:replicas", `Installed replicas (${choices("replicas").length})`, 0, choices("replicas")),
|
|
71
|
+
...group("group:excluded", `Export notes (${choices("excluded").length})`, 0, choices("excluded")),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
function physicalListWindow(rows, rowHeights, selected, height) {
|
|
75
|
+
if (!rows.length)
|
|
76
|
+
return { start: 0, end: 0, indicator: undefined, stickyGroup: false };
|
|
77
|
+
const cursor = Math.min(Math.max(selected, 0), rows.length - 1);
|
|
78
|
+
const build = (budget) => {
|
|
79
|
+
let start = cursor;
|
|
80
|
+
let end = cursor + 1;
|
|
81
|
+
let used = rowHeights[cursor] ?? 1;
|
|
82
|
+
while (start > 0 || end < rows.length) {
|
|
83
|
+
const previous = start > 0 ? rowHeights[start - 1] : Infinity;
|
|
84
|
+
const next = end < rows.length ? rowHeights[end] : Infinity;
|
|
85
|
+
if (used + Math.min(previous, next) > budget)
|
|
86
|
+
break;
|
|
87
|
+
if (previous <= next) {
|
|
88
|
+
start--;
|
|
89
|
+
used += previous;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
used += next;
|
|
93
|
+
end++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { start, end };
|
|
97
|
+
};
|
|
98
|
+
let range = build(height);
|
|
99
|
+
const needsStickyGroup = (start) => start > 0 && !("kind" in rows[start]) &&
|
|
100
|
+
(rowHeights[start] ?? 1) < height && rows.slice(0, start).some(row => "kind" in row);
|
|
101
|
+
if (needsStickyGroup(range.start))
|
|
102
|
+
range = build(Math.max(height - 1, 1));
|
|
103
|
+
const stickyGroup = needsStickyGroup(range.start);
|
|
104
|
+
const hidden = range.start > 0 || range.end < rows.length;
|
|
105
|
+
return { ...range, indicator: hidden ? `${range.start + 1}–${range.end} of ${rows.length}` : undefined, stickyGroup };
|
|
106
|
+
}
|
|
36
107
|
/** The four import blocks in one indexed list, so the checklist and the confirmation share positions. */
|
|
37
108
|
function planEntries(plan) {
|
|
38
109
|
return [...plan.items, ...plan.packages];
|
|
39
110
|
}
|
|
40
111
|
function rowsFromPlan(plan) {
|
|
41
112
|
return planEntries(plan).map((item, index) => ({
|
|
42
|
-
block: item.block, index,
|
|
113
|
+
id: `${item.block}:${index}`, block: item.block, index, ordinal: index + 1,
|
|
43
114
|
label: `${item.status}: ${item.label}${item.difference ? `; ${item.difference}` : ""}`,
|
|
44
115
|
selectable: item.status === "add",
|
|
45
116
|
}));
|
|
46
117
|
}
|
|
118
|
+
function hasChosenResource(profile) {
|
|
119
|
+
return profile.sources.length + profile.skills.length + profile.apps.length + profile.packages.length > 0;
|
|
120
|
+
}
|
|
47
121
|
function selectedPlanEntries(plan, selected) {
|
|
48
122
|
return planEntries(plan).filter((item, index) => item.status !== "add" || selected.some(row => row.index === index));
|
|
49
123
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
124
|
+
function selectedImportProfile(profile, entries) {
|
|
125
|
+
const sources = profile.sources.filter(value => entries.some(item => item.block === "sources" && item.value.url === value.url));
|
|
126
|
+
const skills = profile.skills.filter(value => entries.some(item => item.block === "skills" &&
|
|
127
|
+
item.value.path === value.path && JSON.stringify(item.value.source) === JSON.stringify(value.source)));
|
|
128
|
+
const apps = profile.apps.filter(value => entries.some(item => item.block === "apps" &&
|
|
129
|
+
item.value.name === value.name && item.value.platform === value.platform));
|
|
130
|
+
const packages = profile.packages.filter(value => entries.some(item => item.block === "packages" &&
|
|
131
|
+
selectionKey(item.value) === selectionKey(value)));
|
|
132
|
+
const replicas = profile.replicas.filter(replica => {
|
|
133
|
+
if (replica.kind === "skill")
|
|
134
|
+
return skills.some(skill => skill.path === replica.path &&
|
|
135
|
+
JSON.stringify(skill.source) === JSON.stringify(replica.source));
|
|
136
|
+
if (replica.kind === "app")
|
|
137
|
+
return apps.some(app => app.name === replica.name && app.platform === replica.platform);
|
|
138
|
+
return packages.some(selection => selectionKey(selection) === selectionKey(replica.selection));
|
|
139
|
+
});
|
|
140
|
+
return parseProfile({ ...profile, format: replicas.length ? PROFILE_FORMAT_V2 : PROFILE_FORMAT,
|
|
141
|
+
sources, skills, apps, packages, replicas });
|
|
58
142
|
}
|
|
59
143
|
/** Collect choices, display shared-core previews and confirm portable Profile transfers. */
|
|
60
|
-
export function ProfileView({ operations, environment = {}, onCapturingChange, listHeight }) {
|
|
144
|
+
export function ProfileView({ operations, environment = {}, onCapturingChange, listHeight, existsCheck = pathExists }) {
|
|
61
145
|
const apps = useMemo(() => createAppOperations({
|
|
62
146
|
homeDirectory: environment.homeDirectory, ...environment.appEnvironment,
|
|
63
147
|
}), [environment]);
|
|
64
148
|
const packages = useMemo(() => packageOperationsOf(environment), [environment]);
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
mounted.current = true;
|
|
70
|
-
return () => { mounted.current = false; };
|
|
71
|
-
}, []);
|
|
72
|
-
const setMode = (next) => {
|
|
73
|
-
modeRef.current = next;
|
|
149
|
+
const startDirectory = useMemo(() => pickerStartDirectory(environment), [environment]);
|
|
150
|
+
const mounted = useMounted();
|
|
151
|
+
const [mode, modeRef, setModeState] = useMirrored("browse");
|
|
152
|
+
const setMode = useCallback((next) => {
|
|
74
153
|
onCapturingChange?.(next !== "browse" && next !== "results");
|
|
75
154
|
setModeState(next);
|
|
76
|
-
};
|
|
155
|
+
}, [onCapturingChange, setModeState]);
|
|
77
156
|
const [rows, setRows] = useState([]);
|
|
78
|
-
const [
|
|
79
|
-
const selectedRef =
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const [
|
|
85
|
-
const
|
|
86
|
-
const cursorTo = (next) => {
|
|
87
|
-
cursorRef.current = next;
|
|
88
|
-
setCursorState(next);
|
|
89
|
-
};
|
|
90
|
-
const [text, setText] = useState("");
|
|
91
|
-
const typed = useRef("");
|
|
157
|
+
const [expandedGroups, expandedGroupsRef, setExpandedGroups] = useMirrored(new Set());
|
|
158
|
+
const [selected, selectedRef, select] = useMirrored([]);
|
|
159
|
+
const [cursor, cursorRef, cursorTo] = useMirrored(0);
|
|
160
|
+
const [text, typed, setText] = useMirrored("");
|
|
161
|
+
const [nameError, setNameError] = useState(undefined);
|
|
162
|
+
const [importNotice, setImportNotice] = useState(undefined);
|
|
163
|
+
const [exportDirectory, setExportDirectory] = useState(undefined);
|
|
164
|
+
const [importDirectory, setImportDirectory] = useState(undefined);
|
|
92
165
|
const [lines, setLines] = useState([]);
|
|
166
|
+
const detailReturnId = useRef(undefined);
|
|
167
|
+
const confirmRequest = useRef(undefined);
|
|
168
|
+
const nameRequest = useRef(0);
|
|
169
|
+
const importRequest = useRef(0);
|
|
93
170
|
const [profile, setProfile] = useState();
|
|
94
171
|
const [plan, setPlan] = useState();
|
|
95
172
|
const diagnostics = useRef([]);
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
173
|
+
const { columns: terminalColumns } = useWindowSize();
|
|
174
|
+
const panelWidth = Math.max((terminalColumns || 80) - 4, 1);
|
|
175
|
+
const hintCursorRow = (mode === "export-list" ? exportDisplayRows(rows, expandedGroupsRef.current) : rows)[cursorRef.current];
|
|
176
|
+
const enterHint = hintCursorRow === undefined ? undefined
|
|
177
|
+
: "kind" in hintCursorRow ? "Enter open"
|
|
178
|
+
: hintCursorRow.block === "excluded" ? "Enter note" : undefined;
|
|
179
|
+
const groupHint = hintCursorRow !== undefined && "kind" in hintCursorRow ? " · ←/→ group" : "";
|
|
180
|
+
const listingHint = mode === "export-list"
|
|
181
|
+
? `j/k move${groupHint}${enterHint === undefined ? "" : ` · ${enterHint}`} · space toggle · a all · e export · Esc back to menu`
|
|
182
|
+
: "j/k move · space toggle · a all · Enter preview · Esc back to menu";
|
|
183
|
+
const hintText = `${selected.length} selected · ${listingHint}`;
|
|
184
|
+
const titleRows = Math.max(Math.ceil(stringWidth("Export Profile") / panelWidth), 1);
|
|
185
|
+
const hintRows = Math.max(Math.ceil(stringWidth(hintText) / panelWidth), 1);
|
|
186
|
+
const height = useListHeight(listHeight, 4 + titleRows + hintRows);
|
|
99
187
|
const showLines = (next, nextMode) => {
|
|
100
188
|
setLines(next);
|
|
101
189
|
cursorTo(0);
|
|
@@ -112,15 +200,15 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
112
200
|
}
|
|
113
201
|
}
|
|
114
202
|
};
|
|
115
|
-
const prompt = (next) => {
|
|
116
|
-
typed.current = "";
|
|
117
|
-
setText("");
|
|
118
|
-
setMode(next);
|
|
119
|
-
};
|
|
120
203
|
const loadRows = (next, nextMode) => {
|
|
121
204
|
setRows(next);
|
|
122
205
|
select(next.filter(row => row.selectable));
|
|
123
|
-
|
|
206
|
+
const open = new Set();
|
|
207
|
+
setExpandedGroups(open);
|
|
208
|
+
const firstChoice = nextMode === "export-list"
|
|
209
|
+
? exportDisplayRows(next, open).findIndex(row => !("kind" in row) && row.selectable)
|
|
210
|
+
: 0;
|
|
211
|
+
cursorTo(Math.max(firstChoice, 0));
|
|
124
212
|
setMode(nextMode);
|
|
125
213
|
};
|
|
126
214
|
const chosenProfile = () => {
|
|
@@ -132,15 +220,133 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
132
220
|
skills: profile.skills.filter((_, index) => isChosen("skills", index)),
|
|
133
221
|
apps: profile.apps.filter((_, index) => isChosen("apps", index)),
|
|
134
222
|
packages: profile.packages.filter((_, index) => isChosen("packages", index)),
|
|
223
|
+
replicas: profile.replicas.filter((replica, index) => {
|
|
224
|
+
if (!isChosen("replicas", index))
|
|
225
|
+
return false;
|
|
226
|
+
if (replica.kind === "skill")
|
|
227
|
+
return profile.skills.some((skill, skillIndex) => isChosen("skills", skillIndex) && skill.path === replica.path && JSON.stringify(skill.source) === JSON.stringify(replica.source));
|
|
228
|
+
if (replica.kind === "app")
|
|
229
|
+
return profile.apps.some((app, appIndex) => isChosen("apps", appIndex) && app.name === replica.name && app.platform === replica.platform);
|
|
230
|
+
return profile.packages.some((selection, packageIndex) => isChosen("packages", packageIndex) && JSON.stringify(selection) === JSON.stringify(replica.selection));
|
|
231
|
+
}),
|
|
135
232
|
};
|
|
136
233
|
};
|
|
234
|
+
const beginExport = () => {
|
|
235
|
+
if (!hasChosenResource(chosenProfile())) {
|
|
236
|
+
showLines(["Nothing selected; nothing written."], "results");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
setExportDirectory(undefined);
|
|
240
|
+
setNameError(undefined);
|
|
241
|
+
setMode("export-folder");
|
|
242
|
+
};
|
|
243
|
+
const abandonNameCheck = () => { nameRequest.current += 1; };
|
|
244
|
+
const chooseExportDirectory = (directory) => {
|
|
245
|
+
abandonNameCheck();
|
|
246
|
+
setExportDirectory(directory);
|
|
247
|
+
setNameError(undefined);
|
|
248
|
+
if (typed.current === "")
|
|
249
|
+
setText(DEFAULT_PROFILE_FILE_NAME);
|
|
250
|
+
setMode("export-name");
|
|
251
|
+
};
|
|
252
|
+
const submitExportName = () => {
|
|
253
|
+
const outcome = resolveExportFileName(typed.current);
|
|
254
|
+
if (outcome.kind === "invalid") {
|
|
255
|
+
setNameError(outcome.message);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const directory = exportDirectory ?? startDirectory;
|
|
259
|
+
const fileName = outcome.fileName;
|
|
260
|
+
const finalPath = exportDestination(directory, fileName);
|
|
261
|
+
const id = ++nameRequest.current;
|
|
262
|
+
void (async () => {
|
|
263
|
+
let exists;
|
|
264
|
+
try {
|
|
265
|
+
exists = await existsCheck(finalPath);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (!mounted.current || nameRequest.current !== id || modeRef.current !== "export-name")
|
|
269
|
+
return;
|
|
270
|
+
setNameError(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const current = resolveExportFileName(typed.current);
|
|
274
|
+
if (!mounted.current || nameRequest.current !== id || modeRef.current !== "export-name")
|
|
275
|
+
return;
|
|
276
|
+
if (current.kind !== "ok" || current.fileName !== fileName)
|
|
277
|
+
return;
|
|
278
|
+
if (exists) {
|
|
279
|
+
setNameError(`${finalPath} already exists; choose another name`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
confirmRequest.current = { kind: "export", file: finalPath };
|
|
283
|
+
setNameError(undefined);
|
|
284
|
+
showLines([`Destination: ${finalPath}`, ...profileExportPreview(chosenProfile(), diagnostics.current)], "confirm");
|
|
285
|
+
})();
|
|
286
|
+
};
|
|
287
|
+
const confirmApply = () => {
|
|
288
|
+
const request = confirmRequest.current;
|
|
289
|
+
if (request === undefined)
|
|
290
|
+
return;
|
|
291
|
+
void run(async () => {
|
|
292
|
+
let result;
|
|
293
|
+
if (request.kind === "export") {
|
|
294
|
+
await writeProfileExport(request.file, chosenProfile());
|
|
295
|
+
result = [`Exported profile to ${request.file}`];
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const outcomes = await applyProfileImport(request.plan, operations, apps, true, () => undefined, { homeDirectory: environment.homeDirectory, packageOperations: packages, ...environment.installationOptions });
|
|
299
|
+
result = outcomes.map(outcome => `${outcome.status}: ${outcome.item.label}${outcome.detail ? `; ${outcome.detail}` : ""}`);
|
|
300
|
+
}
|
|
301
|
+
if (mounted.current) {
|
|
302
|
+
showLines(result.length ? result : ["Nothing selected; nothing written."], "results");
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
const previewImport = () => {
|
|
307
|
+
void run(async () => {
|
|
308
|
+
// Reuse the CLI's core preview for the exact selected subset, including argv and Source notes.
|
|
309
|
+
const entries = selectedPlanEntries(plan, selectedRef.current);
|
|
310
|
+
const subset = selectedImportProfile(profile, entries);
|
|
311
|
+
const imported = await buildProfileImport(subset, operations, apps, {}, packages);
|
|
312
|
+
if (!mounted.current)
|
|
313
|
+
return;
|
|
314
|
+
confirmRequest.current = { kind: "import", plan: imported };
|
|
315
|
+
showLines(imported.preview, "confirm");
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
const chooseImportFile = (selection) => {
|
|
319
|
+
if (selection.kind !== "file")
|
|
320
|
+
return;
|
|
321
|
+
setImportDirectory(selection.directory);
|
|
322
|
+
const id = ++importRequest.current;
|
|
323
|
+
setMode("busy");
|
|
324
|
+
void (async () => {
|
|
325
|
+
try {
|
|
326
|
+
const importedProfile = parseProfile(JSON.parse(await readFile(selection.file, "utf8")));
|
|
327
|
+
const imported = await buildProfileImport(importedProfile, operations, apps, {}, packages);
|
|
328
|
+
if (!mounted.current || importRequest.current !== id)
|
|
329
|
+
return;
|
|
330
|
+
setProfile(importedProfile);
|
|
331
|
+
setPlan(imported);
|
|
332
|
+
setImportNotice(undefined);
|
|
333
|
+
loadRows(rowsFromPlan(imported), "import-list");
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (!mounted.current || importRequest.current !== id)
|
|
337
|
+
return;
|
|
338
|
+
setImportNotice(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
339
|
+
setMode("import-browse");
|
|
340
|
+
}
|
|
341
|
+
})();
|
|
342
|
+
};
|
|
137
343
|
useKeys((input, key) => {
|
|
138
344
|
const current = modeRef.current;
|
|
139
|
-
|
|
345
|
+
// The FilePicker owns its keys while it is mounted, and async work ignores every key.
|
|
346
|
+
if (current === "export-folder" || current === "import-browse" || current === "busy")
|
|
140
347
|
return;
|
|
141
348
|
if (current === "browse" || current === "results") {
|
|
142
349
|
if (input === "e") {
|
|
143
|
-
action.current = "export";
|
|
144
350
|
void run(async () => {
|
|
145
351
|
const exported = await buildProfileExport(operations, apps, {
|
|
146
352
|
homeDirectory: environment.homeDirectory, packageOperations: packages,
|
|
@@ -153,8 +359,8 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
153
359
|
});
|
|
154
360
|
}
|
|
155
361
|
else if (input === "i") {
|
|
156
|
-
|
|
157
|
-
|
|
362
|
+
setImportNotice(undefined);
|
|
363
|
+
setMode("import-browse");
|
|
158
364
|
}
|
|
159
365
|
else if (current === "results") {
|
|
160
366
|
const next = moveCursor(cursorRef.current, lines.length, input, key);
|
|
@@ -163,59 +369,54 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
163
369
|
}
|
|
164
370
|
return;
|
|
165
371
|
}
|
|
166
|
-
if (current === "export-
|
|
372
|
+
if (current === "export-name") {
|
|
167
373
|
if (key.escape) {
|
|
168
|
-
|
|
374
|
+
abandonNameCheck();
|
|
375
|
+
setMode("export-folder");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (key.ctrl && input === "u") {
|
|
379
|
+
abandonNameCheck();
|
|
380
|
+
setText("");
|
|
381
|
+
setNameError(undefined);
|
|
169
382
|
return;
|
|
170
383
|
}
|
|
171
384
|
if (key.return) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const imported = await buildProfileImport(raw, operations, apps, {}, packages);
|
|
181
|
-
profileVersion.current = raw.agentDepotVersion;
|
|
182
|
-
if (!mounted.current)
|
|
183
|
-
return;
|
|
184
|
-
setPlan(imported);
|
|
185
|
-
loadRows(rowsFromPlan(imported), "import-list");
|
|
186
|
-
});
|
|
385
|
+
submitExportName();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (key.backspace || key.delete) {
|
|
389
|
+
abandonNameCheck();
|
|
390
|
+
setText(typed.current.slice(0, -1));
|
|
391
|
+
setNameError(undefined);
|
|
392
|
+
return;
|
|
187
393
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
setText(typed.current);
|
|
394
|
+
if (input && !key.ctrl && !key.meta) {
|
|
395
|
+
abandonNameCheck();
|
|
396
|
+
setText(typed.current + input);
|
|
397
|
+
setNameError(undefined);
|
|
191
398
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (current === "detail") {
|
|
402
|
+
if (key.escape) {
|
|
403
|
+
const returnIndex = exportDisplayRows(rows, expandedGroupsRef.current)
|
|
404
|
+
.findIndex(row => !("kind" in row) && row.id === detailReturnId.current);
|
|
405
|
+
cursorTo(Math.max(returnIndex, 0));
|
|
406
|
+
detailReturnId.current = undefined;
|
|
407
|
+
setMode("export-list");
|
|
408
|
+
return;
|
|
195
409
|
}
|
|
410
|
+
const next = movePreviewCursor(cursorRef.current, lines.length, input, key, height);
|
|
411
|
+
if (next !== undefined)
|
|
412
|
+
cursorTo(Math.max(0, next));
|
|
196
413
|
return;
|
|
197
414
|
}
|
|
198
415
|
if (current === "confirm") {
|
|
199
|
-
answerYesNo(input, key, () =>
|
|
200
|
-
void run(async () => {
|
|
201
|
-
let result;
|
|
202
|
-
if (action.current === "export") {
|
|
203
|
-
await writeProfileExport(typed.current, chosenProfile());
|
|
204
|
-
result = [`Exported profile to ${typed.current}`];
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
const confirmed = chosenPlan(plan, selectedRef.current);
|
|
208
|
-
const outcomes = await applyProfileImport(confirmed, operations, apps, true, () => undefined, { homeDirectory: environment.homeDirectory, packageOperations: packages, ...environment.installationOptions });
|
|
209
|
-
result = outcomes.map(outcome => `${outcome.status}: ${outcome.item.label}${outcome.detail ? `; ${outcome.detail}` : ""}`);
|
|
210
|
-
}
|
|
211
|
-
if (mounted.current) {
|
|
212
|
-
showLines(result.length ? result : ["Nothing selected; nothing written."], "results");
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
}, () => setMode("browse"));
|
|
416
|
+
answerYesNo(input, key, confirmApply, () => setMode(confirmRequest.current?.kind === "import" ? "import-list" : "export-name"));
|
|
216
417
|
if (input === "y" || input === "n" || key.escape)
|
|
217
418
|
return;
|
|
218
|
-
const next =
|
|
419
|
+
const next = movePreviewCursor(cursorRef.current, lines.length, input, key, height);
|
|
219
420
|
if (next !== undefined)
|
|
220
421
|
cursorTo(Math.max(0, next));
|
|
221
422
|
return;
|
|
@@ -224,48 +425,105 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
224
425
|
setMode("browse");
|
|
225
426
|
return;
|
|
226
427
|
}
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
const next = moveCursor(cursorRef.current,
|
|
231
|
-
if (next !== undefined)
|
|
428
|
+
// export-list and import-list
|
|
429
|
+
const visibleRows = current === "export-list" ? exportDisplayRows(rows, expandedGroupsRef.current) : rows;
|
|
430
|
+
const currentRow = visibleRows[cursorRef.current];
|
|
431
|
+
const next = moveCursor(cursorRef.current, visibleRows.length, input, key);
|
|
432
|
+
if (next !== undefined) {
|
|
232
433
|
cursorTo(Math.max(0, next));
|
|
233
|
-
|
|
234
|
-
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (input === " ") {
|
|
437
|
+
if (currentRow && !("kind" in currentRow) && currentRow.selectable)
|
|
438
|
+
select(toggleChoice(rows, selectedRef.current, currentRow));
|
|
439
|
+
return;
|
|
235
440
|
}
|
|
236
|
-
|
|
441
|
+
if (input === "a") {
|
|
237
442
|
select(selectedRef.current.length ? [] : rows.filter(row => row.selectable));
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (current === "export-list" && input === "e") {
|
|
446
|
+
beginExport();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (currentRow && "kind" in currentRow && currentRow.kind === "group") {
|
|
450
|
+
if (key.return || key.rightArrow || key.leftArrow) {
|
|
451
|
+
const open = new Set(expandedGroupsRef.current);
|
|
452
|
+
if (key.return) {
|
|
453
|
+
if (open.has(currentRow.id))
|
|
454
|
+
open.delete(currentRow.id);
|
|
455
|
+
else
|
|
456
|
+
open.add(currentRow.id);
|
|
242
457
|
}
|
|
458
|
+
else if (key.rightArrow)
|
|
459
|
+
open.add(currentRow.id);
|
|
243
460
|
else
|
|
244
|
-
|
|
461
|
+
open.delete(currentRow.id);
|
|
462
|
+
setExpandedGroups(open);
|
|
245
463
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}, operations, apps, {}, packages);
|
|
257
|
-
if (mounted.current)
|
|
258
|
-
showLines(preview.preview, "confirm");
|
|
259
|
-
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (key.return && currentRow && !("kind" in currentRow) && currentRow.block === "excluded") {
|
|
467
|
+
detailReturnId.current = currentRow.id;
|
|
468
|
+
showLines(wrapText(currentRow.label, Math.max((terminalColumns || 80) - 8, 1)), "detail");
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (key.return && current === "import-list") {
|
|
472
|
+
previewImport();
|
|
473
|
+
return;
|
|
260
474
|
}
|
|
261
475
|
});
|
|
262
476
|
const listing = mode === "export-list" || mode === "import-list";
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
477
|
+
const displayRows = mode === "export-list" ? exportDisplayRows(rows, expandedGroups) : rows;
|
|
478
|
+
const rowHeights = displayRows.map(row => {
|
|
479
|
+
const prefixWidth = "kind" in row ? stringWidth(`> ${" ".repeat(row.depth * 2)}▸ `) : row.block === "excluded" ? 2
|
|
480
|
+
: stringWidth(`> [ ] ${row.ordinal ?? row.index + 1} `);
|
|
481
|
+
const label = !("kind" in row) && row.block !== "excluded" && !row.selectable
|
|
482
|
+
? `${row.label} (not selectable)` : row.label;
|
|
483
|
+
return wrapText(label, Math.max(panelWidth - prefixWidth, 1)).length;
|
|
484
|
+
});
|
|
485
|
+
const listWindow = physicalListWindow(displayRows, rowHeights, cursor, height);
|
|
486
|
+
const window = listing ? listWindow : computeWindow(lines.length, cursor, height);
|
|
487
|
+
const visibleRows = displayRows.slice(window.start, window.end);
|
|
488
|
+
const precedingGroup = displayRows.slice(0, window.start).filter((row) => "kind" in row).at(-1);
|
|
489
|
+
const indexedRows = visibleRows.map((row, index) => ({ row, index: window.start + index }));
|
|
490
|
+
const renderedRows = listWindow.stickyGroup && precedingGroup
|
|
491
|
+
? [{ row: precedingGroup, index: undefined }, ...indexedRows]
|
|
492
|
+
: indexedRows;
|
|
493
|
+
let remainingRows = height;
|
|
494
|
+
const listContent = renderedRows.map(({ row, index }) => {
|
|
495
|
+
const marker = index === cursor ? "> " : " ";
|
|
496
|
+
if ("kind" in row) {
|
|
497
|
+
const open = expandedGroups.has(row.id);
|
|
498
|
+
const prefix = `${marker}${" ".repeat(row.depth * 2)}${open ? "▾" : "▸"} `;
|
|
499
|
+
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
500
|
+
const label = fitLines(wrapText(row.label, width), remainingRows, width)
|
|
501
|
+
.join(`\n${" ".repeat(stringWidth(prefix))}`);
|
|
502
|
+
remainingRows -= label.split("\n").length;
|
|
503
|
+
return _jsxs(Text, { color: theme.group, bold: true, children: [prefix, label] }, row.id);
|
|
504
|
+
}
|
|
505
|
+
if (row.block === "excluded") {
|
|
506
|
+
const width = Math.max(panelWidth - stringWidth(marker), 1);
|
|
507
|
+
const label = fitLines(wrapText(row.label, width), remainingRows, width)
|
|
508
|
+
.join(`\n${" ".repeat(stringWidth(marker))}`);
|
|
509
|
+
remainingRows -= label.split("\n").length;
|
|
510
|
+
return _jsxs(Text, { color: theme.value, children: [marker, label] }, row.id);
|
|
511
|
+
}
|
|
512
|
+
const prefix = `${marker}[${selected.some(choice => choice.id === row.id) ? "x" : " "}] ${row.ordinal ?? row.index + 1} `;
|
|
513
|
+
const value = `${row.label}${row.selectable ? "" : " (not selectable)"}`;
|
|
514
|
+
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
515
|
+
const label = fitLines(wrapText(value, width), remainingRows, width)
|
|
516
|
+
.join(`\n${" ".repeat(stringWidth(prefix))}`);
|
|
517
|
+
remainingRows -= label.split("\n").length;
|
|
518
|
+
return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: prefix }), _jsx(Text, { color: theme.value, children: label })] }, row.id);
|
|
519
|
+
});
|
|
520
|
+
const nameOutcome = resolveExportFileName(text);
|
|
521
|
+
const liveDestination = nameOutcome.kind === "ok"
|
|
522
|
+
? exportDestination(exportDirectory ?? startDirectory, nameOutcome.fileName) : undefined;
|
|
523
|
+
const nameNote = nameOutcome.kind === "ok" ? fileNameNotice(nameOutcome.fileName) : undefined;
|
|
524
|
+
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(Text, { children: "Loading Profile\u2026" }) : null, mode === "export-name" ? _jsxs(BorderPanel, { title: "Export file name", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: "Folder: " }), _jsx(Text, { color: theme.value, children: exportDirectory ?? startDirectory })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: "Name: " }), _jsxs(Text, { color: theme.value, children: [text, "_"] })] }), liveDestination === undefined ? null : _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: "Destination: " }), _jsx(Text, { color: theme.value, children: liveDestination })] }), nameNote === undefined ? null : _jsx(Text, { color: theme.warning, children: nameNote }), nameOutcome.kind === "ok" && nameOutcome.appendedExtension
|
|
526
|
+
? _jsxs(Text, { color: theme.muted, children: ["A name without an extension is saved as ", nameOutcome.fileName, "."] }) : null, nameError === undefined ? null : _jsx(Text, { color: theme.error, children: nameError }), _jsx(Text, { color: theme.legend, children: "Type to edit \u00B7 Ctrl+U clear \u00B7 Enter continue \u00B7 Esc back to folder" })] }) : null, mode === "confirm" ? _jsx(PreviewConfirm, { lines: lines, cursor: cursor, question: confirmRequest.current?.kind === "export"
|
|
527
|
+
? "Save/Export Profile? (y/n) · arrows/PgUp/PgDn page · j/k line"
|
|
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] });
|
|
271
529
|
}
|