@iseoane/agent-depot 0.5.2 → 0.6.0
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 +19 -0
- package/README.es.md +51 -29
- package/README.md +44 -25
- 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/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 +38 -6
- package/dist/src/tui/preview-window.js +48 -0
- package/dist/src/tui/profile-view.js +261 -42
- 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/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 +6 -4
- package/package.json +4 -3
package/dist/src/profile.js
CHANGED
|
@@ -3,7 +3,9 @@ import { parseAppRecipe } from "./app-recipes.js";
|
|
|
3
3
|
import { canonicalizeGitSourceUrl } from "./git-source.js";
|
|
4
4
|
import { selectionKey } from "./package-model.js";
|
|
5
5
|
import { containsCredentialArgument, parseProjectManifest, ProjectManifestError } from "./project-manifest.js";
|
|
6
|
-
export const
|
|
6
|
+
export const PROFILE_FORMAT_V1 = "agent-depot-profile/v1";
|
|
7
|
+
export const PROFILE_FORMAT_V2 = "agent-depot-profile/v2";
|
|
8
|
+
export const PROFILE_FORMAT = PROFILE_FORMAT_V1;
|
|
7
9
|
function object(value, keys, field) {
|
|
8
10
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
9
11
|
throw new Error(`${field}: expected an object`);
|
|
@@ -127,9 +129,12 @@ function parseProfilePackage(value, field) {
|
|
|
127
129
|
}
|
|
128
130
|
/** Portable profile parsing shares the manifest and recipe safety contracts. */
|
|
129
131
|
export function parseProfile(value) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
133
|
+
throw new Error("profile: expected an object");
|
|
134
|
+
const format = value.format;
|
|
135
|
+
if (format !== PROFILE_FORMAT_V1 && format !== PROFILE_FORMAT_V2)
|
|
132
136
|
throw new Error("profile.format: unsupported profile version");
|
|
137
|
+
const root = object(value, ["format", "agentDepotVersion", "sources", "skills", "apps", "packages", "replicas"], "profile");
|
|
133
138
|
if (typeof root.agentDepotVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][\w.+-]+)?$/u.test(root.agentDepotVersion)) {
|
|
134
139
|
throw new Error("profile.agentDepotVersion: expected a package version");
|
|
135
140
|
}
|
|
@@ -179,8 +184,25 @@ export function parseProfile(value) {
|
|
|
179
184
|
assertPortableText(selection, field);
|
|
180
185
|
return selection;
|
|
181
186
|
});
|
|
187
|
+
const replicas = root.replicas === undefined
|
|
188
|
+
? []
|
|
189
|
+
: list(root.replicas, "replicas").map((value, index) => parseProfileReplica(value, `replicas[${index}]`));
|
|
190
|
+
if (format === PROFILE_FORMAT_V2 && root.replicas === undefined)
|
|
191
|
+
throw new Error("replicas: required for profile v2");
|
|
192
|
+
if (format === PROFILE_FORMAT_V1 && replicas.length > 0)
|
|
193
|
+
throw new Error("replicas: non-empty replica intent requires profile v2");
|
|
182
194
|
rejectDuplicates(sources, source => source.url, "sources");
|
|
183
195
|
rejectDuplicates(packages, selectionKey, "packages");
|
|
196
|
+
rejectDuplicates(replicas, replicaIdentity, "replicas");
|
|
197
|
+
replicas.forEach((replica, index) => {
|
|
198
|
+
const exists = replica.kind === "skill"
|
|
199
|
+
? parsedSkills.some(skill => skillIdentity(skill) === JSON.stringify([replica.source, replica.path]))
|
|
200
|
+
: replica.kind === "app"
|
|
201
|
+
? apps.some(app => app.name === replica.name && app.platform === replica.platform)
|
|
202
|
+
: packages.some(selection => selectionKey(selection) === selectionKey(replica.selection));
|
|
203
|
+
if (!exists)
|
|
204
|
+
throw new Error(`replicas[${index}]: does not reference a profile resource`);
|
|
205
|
+
});
|
|
184
206
|
apps.forEach((app, index) => {
|
|
185
207
|
if (apps.slice(0, index).some(previous => previous.name === app.name &&
|
|
186
208
|
(previous.platform === undefined || app.platform === undefined || previous.platform === app.platform))) {
|
|
@@ -192,14 +214,56 @@ export function parseProfile(value) {
|
|
|
192
214
|
const sortedSkills = [...parsedSkills].sort((a, b) => compare(JSON.stringify([a.source, a.path]), JSON.stringify([b.source, b.path])));
|
|
193
215
|
apps.sort((a, b) => compare(a.name, b.name) || compare(a.platform ?? "", b.platform ?? ""));
|
|
194
216
|
packages.sort((a, b) => compare(selectionKey(a), selectionKey(b)));
|
|
195
|
-
|
|
217
|
+
replicas.sort((a, b) => compare(replicaIdentity(a), replicaIdentity(b)));
|
|
218
|
+
return { format, agentDepotVersion: root.agentDepotVersion, sources, skills: sortedSkills, apps, packages, replicas };
|
|
196
219
|
}
|
|
197
220
|
/**
|
|
198
221
|
* An empty Package block is omitted, so an export with no Packages stays readable
|
|
199
222
|
* by an Agent Depot version that predates the block.
|
|
200
223
|
*/
|
|
224
|
+
function skillIdentity(skill) {
|
|
225
|
+
return JSON.stringify([skill.source, skill.path]);
|
|
226
|
+
}
|
|
227
|
+
function replicaIdentity(replica) {
|
|
228
|
+
switch (replica.kind) {
|
|
229
|
+
case "skill": return JSON.stringify([replica.kind, replica.source, replica.path]);
|
|
230
|
+
case "app": return JSON.stringify([replica.kind, replica.name, replica.platform ?? null]);
|
|
231
|
+
case "package": return JSON.stringify([replica.kind, selectionKey(replica.selection)]);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function parseProfileReplica(value, field) {
|
|
235
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
236
|
+
throw new Error(`${field}: expected an object`);
|
|
237
|
+
const kind = value.kind;
|
|
238
|
+
if (kind === "skill") {
|
|
239
|
+
const record = object(value, ["kind", "source", "path"], field);
|
|
240
|
+
const [skill] = parseProjectManifest({ version: 1, skills: [
|
|
241
|
+
{ source: record.source, path: record.path, version: { policy: "latest" }, hosts: ["pi"] },
|
|
242
|
+
] }, "profile").skills;
|
|
243
|
+
return { kind, source: skill.source, path: skill.path };
|
|
244
|
+
}
|
|
245
|
+
if (kind === "app") {
|
|
246
|
+
const record = object(value, ["kind", "name", "platform"], field);
|
|
247
|
+
if (typeof record.name !== "string" || record.name.length === 0)
|
|
248
|
+
throw new Error(`${field}.name: expected a non-empty string`);
|
|
249
|
+
const platform = record.platform;
|
|
250
|
+
if (platform !== undefined && platform !== "linux" && platform !== "windows" && platform !== "darwin") {
|
|
251
|
+
throw new Error(`${field}.platform: unsupported platform`);
|
|
252
|
+
}
|
|
253
|
+
return { kind, name: record.name, ...(platform === undefined ? {} : { platform }) };
|
|
254
|
+
}
|
|
255
|
+
if (kind === "package") {
|
|
256
|
+
const record = object(value, ["kind", "selection"], field);
|
|
257
|
+
return { kind, selection: parseProfilePackage(record.selection, `${field}.selection`) };
|
|
258
|
+
}
|
|
259
|
+
throw new Error(`${field}.kind: expected "skill", "app" or "package"`);
|
|
260
|
+
}
|
|
201
261
|
export function serializeProfile(profile) {
|
|
202
|
-
const
|
|
203
|
-
const {
|
|
204
|
-
|
|
262
|
+
const format = profile.replicas.length ? PROFILE_FORMAT_V2 : PROFILE_FORMAT_V1;
|
|
263
|
+
const parsed = parseProfile({ ...profile, format });
|
|
264
|
+
const { packages, replicas, ...rest } = parsed;
|
|
265
|
+
const output = { ...rest, format,
|
|
266
|
+
...(packages.length ? { packages } : {}),
|
|
267
|
+
...(replicas.length ? { replicas } : {}) };
|
|
268
|
+
return `${JSON.stringify(output, null, 2)}\n`;
|
|
205
269
|
}
|
|
@@ -1,27 +1,67 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text, useWindowSize } from "ink";
|
|
3
3
|
import { PROJECT_HOSTS } from "../project-manifest.js";
|
|
4
4
|
import { errorText, runBatch } from "./batch.js";
|
|
5
|
-
import {
|
|
6
|
-
import { HostChecklist, PreviewConfirm } from "./panel-parts.js";
|
|
7
|
-
|
|
5
|
+
import { hostChecklistKey, movePreviewCursor } from "./mode-keys.js";
|
|
6
|
+
import { BorderPanel, HostChecklist, PreviewConfirm } from "./panel-parts.js";
|
|
7
|
+
import { previewViewport } from "./preview-window.js";
|
|
8
|
+
export const APP_KINDS = ["app-busy", "app-approve", "app-hosts", "app-confirm", "app-manual"];
|
|
8
9
|
/** Narrows the view's mode without asserting a partial mode is an App mode. */
|
|
9
10
|
export function isAppMode(mode) {
|
|
10
11
|
return APP_KINDS.some(kind => kind === mode.kind);
|
|
11
12
|
}
|
|
12
13
|
/** Recipe name, falling back to the file identity for invalid recipes. */
|
|
13
14
|
export const appName = (entry) => entry.recipe?.name ?? entry.file;
|
|
15
|
+
function appPreviewChrome(mode) {
|
|
16
|
+
const title = mode.kind === "app-approve" ? "Recipe approval · does not install"
|
|
17
|
+
: mode.kind === "app-manual" ? "App manual step" : `App ${mode.plan.action} confirmation`;
|
|
18
|
+
const question = mode.kind === "app-approve" ? `Approve ${appName(mode.entry)}? y/n · recipe approval does not install the App.`
|
|
19
|
+
: mode.kind === "app-manual" ? "Manual step done, check now? y/n"
|
|
20
|
+
: `${mode.plan.action} ${appName(mode.plan.entry)}? y/n`;
|
|
21
|
+
return { title, questions: [question, "Scroll to end before y; n cancels."], footerRows: 2 };
|
|
22
|
+
}
|
|
14
23
|
export function appStepLines(step) {
|
|
24
|
+
const argv = step.argv;
|
|
15
25
|
return [
|
|
16
|
-
...(
|
|
26
|
+
...(argv ? [
|
|
27
|
+
"argv: [",
|
|
28
|
+
...argv.map((argument, index) => ` ${JSON.stringify(argument)}${index < argv.length - 1 ? "," : ""}`),
|
|
29
|
+
"]",
|
|
30
|
+
`Executable: ${step.executable ?? `${JSON.stringify(argv[0])} not found in PATH`}`,
|
|
31
|
+
] : []),
|
|
17
32
|
...(step.manual ? [`Manual: ${step.manual}`] : []),
|
|
33
|
+
...(step.declaration ? [step.declaration] : []),
|
|
34
|
+
...(step.pattern ? [`Version pattern: ${step.pattern}`] : []),
|
|
35
|
+
...(step.environment ? [`Environment: ${step.environment}`] : []),
|
|
18
36
|
`Working directory: ${step.cwd}`,
|
|
19
37
|
...(step.warning ? [step.warning] : []),
|
|
20
38
|
];
|
|
21
39
|
}
|
|
40
|
+
export function appRecipePreviewLines(entry, status, steps) {
|
|
41
|
+
const recipe = entry.recipe;
|
|
42
|
+
return [
|
|
43
|
+
`File: ${entry.file}`,
|
|
44
|
+
`Name: ${recipe?.name ?? entry.file}`,
|
|
45
|
+
`Platform: ${recipe?.platform ?? "all"}`,
|
|
46
|
+
`Status: ${status}`,
|
|
47
|
+
...(recipe?.homepage ? [`Homepage: ${recipe.homepage}`] : []),
|
|
48
|
+
...(recipe?.skills ? [`Owned Skills: ${JSON.stringify(recipe.skills)}`] : []),
|
|
49
|
+
...steps.flatMap(step => [step.step, ...appStepLines(step)]),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
function appPreviewLines(mode) {
|
|
53
|
+
if (mode.kind === "app-approve")
|
|
54
|
+
return mode.lines;
|
|
55
|
+
return [
|
|
56
|
+
`App: ${appName(mode.plan.entry)} ${mode.plan.action}${mode.plan.host ? ` (${mode.plan.host})` : ""}`,
|
|
57
|
+
...(mode.plan.previousVersion ? [`Previous version: ${mode.plan.previousVersion}`] : []),
|
|
58
|
+
...(mode.plan.latestVersion ? [`Latest version: ${mode.plan.latestVersion}`] : []),
|
|
59
|
+
...appStepLines(mode.plan),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
22
62
|
async function prepareNext(context, requests, results = [], failures = 0) {
|
|
23
63
|
const { manage, apps } = context;
|
|
24
|
-
manage.setMode({ kind: "busy", label: "Preparing App step..." });
|
|
64
|
+
manage.setMode({ kind: "app-busy", label: "Preparing App step..." });
|
|
25
65
|
// Independent planning failures never stop the remaining marked Apps.
|
|
26
66
|
const [request, ...remaining] = requests;
|
|
27
67
|
if (!request) {
|
|
@@ -33,7 +73,7 @@ async function prepareNext(context, requests, results = [], failures = 0) {
|
|
|
33
73
|
if (manage.isMounted()) {
|
|
34
74
|
manage.setMode({
|
|
35
75
|
kind: !plan.argv && plan.manual ? "app-manual" : "app-confirm",
|
|
36
|
-
plan, remaining, results, failures,
|
|
76
|
+
plan, remaining, results, failures, cursor: 0,
|
|
37
77
|
});
|
|
38
78
|
}
|
|
39
79
|
}
|
|
@@ -52,10 +92,10 @@ export async function startAppAction(context, entries, input) {
|
|
|
52
92
|
const entry = entries[0];
|
|
53
93
|
if (!entry)
|
|
54
94
|
throw new Error("No App selected");
|
|
55
|
-
manage.setMode({ kind: "busy", label: "Preparing recipe approval..." });
|
|
56
|
-
const steps = await apps.preview(entry);
|
|
95
|
+
manage.setMode({ kind: "app-busy", label: "Preparing recipe approval..." });
|
|
96
|
+
const [steps, approval] = await Promise.all([apps.preview(entry), apps.approvalStatus(entry)]);
|
|
57
97
|
if (manage.isMounted()) {
|
|
58
|
-
manage.setMode({ kind: "app-approve", entry, lines:
|
|
98
|
+
manage.setMode({ kind: "app-approve", entry, lines: appRecipePreviewLines(entry, approval.status, steps), cursor: 0 });
|
|
59
99
|
}
|
|
60
100
|
}
|
|
61
101
|
else if (input === "h") {
|
|
@@ -78,7 +118,7 @@ export async function startAppAction(context, entries, input) {
|
|
|
78
118
|
}
|
|
79
119
|
async function execute(context, mode) {
|
|
80
120
|
const { manage, apps } = context;
|
|
81
|
-
manage.setMode({ kind: "busy", label: "Running App step..." });
|
|
121
|
+
manage.setMode({ kind: "app-busy", label: "Running App step..." });
|
|
82
122
|
let manualRequired = false;
|
|
83
123
|
const [outcome] = await runBatch([mode.plan], async (plan) => {
|
|
84
124
|
const result = mode.kind === "app-manual"
|
|
@@ -102,6 +142,8 @@ async function execute(context, mode) {
|
|
|
102
142
|
/** Applies checklist and y/n keys, continuing independent batch steps after cancellation or failure. */
|
|
103
143
|
export function handleAppKey(context, mode, input, key) {
|
|
104
144
|
const { manage, apps } = context;
|
|
145
|
+
if (mode.kind === "app-busy")
|
|
146
|
+
return;
|
|
105
147
|
const cancel = () => {
|
|
106
148
|
manage.browse();
|
|
107
149
|
manage.setMessage({ kind: "ok", text: "App action cancelled" });
|
|
@@ -123,35 +165,42 @@ export function handleAppKey(context, mode, input, key) {
|
|
|
123
165
|
void prepareNext(context, requests);
|
|
124
166
|
}
|
|
125
167
|
}
|
|
126
|
-
else
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
168
|
+
else {
|
|
169
|
+
const lines = appPreviewLines(mode);
|
|
170
|
+
const viewport = previewViewport(lines, mode.cursor, context.terminalRows, context.terminalColumns, appPreviewChrome(mode));
|
|
171
|
+
const cursor = movePreviewCursor(mode.cursor, viewport.total, input, key, viewport.height);
|
|
172
|
+
if (cursor !== undefined) {
|
|
173
|
+
manage.setMode({ ...mode, cursor });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (input === "n" || key.escape) {
|
|
135
177
|
if (mode.kind === "app-confirm" || mode.kind === "app-manual") {
|
|
136
178
|
void prepareNext(context, mode.remaining, [...mode.results, `${appName(mode.plan.entry)}: cancelled`], mode.failures);
|
|
137
179
|
}
|
|
138
180
|
else
|
|
139
181
|
cancel();
|
|
140
|
-
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (input !== "y" || !viewport.reachedEnd)
|
|
185
|
+
return;
|
|
186
|
+
if (mode.kind !== "app-approve") {
|
|
187
|
+
void execute(context, mode);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
manage.setMode({ kind: "app-busy", label: "Approving recipe..." });
|
|
191
|
+
void apps.approve(mode.entry).then(() => manage.finish({ kind: "ok", text: `Approved ${appName(mode.entry)}` }), error => manage.finish({ kind: "error", text: errorText(error) }));
|
|
192
|
+
}
|
|
141
193
|
}
|
|
142
194
|
/** Renders App prompts using the existing checklist and preview-confirm panels. */
|
|
143
195
|
export function AppPanel({ mode }) {
|
|
196
|
+
const { rows, columns } = useWindowSize();
|
|
197
|
+
if (mode.kind === "app-busy")
|
|
198
|
+
return _jsx(BorderPanel, { title: "App lifecycle", children: _jsx(Text, { children: mode.label }) });
|
|
144
199
|
if (mode.kind === "app-hosts") {
|
|
145
|
-
return (_jsxs(
|
|
146
|
-
}
|
|
147
|
-
if (mode.kind === "app-approve") {
|
|
148
|
-
return _jsx(PreviewConfirm, { lines: mode.lines, question: `Approve ${appName(mode.entry)}? y/n` });
|
|
200
|
+
return (_jsxs(BorderPanel, { title: "App Host steps", children: [_jsx(Text, { children: "Checked = setup. Unchecked = teardown. Wiring is not tracked." }), _jsxs(Text, { children: ["Space/1-", mode.choices.length, " toggle \u00B7 j/k move \u00B7 Enter continue \u00B7 Esc cancel"] }), _jsx(HostChecklist, { choices: mode.choices, cursor: mode.cursor, selected: mode.selected })] }));
|
|
149
201
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
], question: mode.kind === "app-manual"
|
|
155
|
-
? "Manual step done, check now? y/n"
|
|
156
|
-
: `${mode.plan.action} ${appName(mode.plan.entry)}? y/n` }));
|
|
202
|
+
const lines = appPreviewLines(mode);
|
|
203
|
+
const chrome = appPreviewChrome(mode);
|
|
204
|
+
const viewport = previewViewport(lines, mode.cursor, rows || 24, columns || 80, chrome);
|
|
205
|
+
return (_jsx(PreviewConfirm, { title: chrome.title, lines: lines, cursor: mode.cursor, question: viewport.reachedEnd ? chrome.questions[0] : chrome.questions[1], chrome: chrome }));
|
|
157
206
|
}
|
package/dist/src/tui/app.js
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useApp, useWindowSize } from "ink";
|
|
3
3
|
import { useCallback, useRef, useState } from "react";
|
|
4
4
|
import { AGENT_DEPOT_PACKAGE_VERSION } from "../project-manifest.js";
|
|
5
5
|
import { CatalogView } from "./catalog-view.js";
|
|
6
6
|
import { InstallationsView } from "./installations-view.js";
|
|
7
|
+
import { headerTabRows, TABS } from "./header-layout.js";
|
|
7
8
|
import { useKeys } from "./keys.js";
|
|
8
9
|
import { ProfileView } from "./profile-view.js";
|
|
9
10
|
import { SourcesView, sourceRowHints } from "./sources-view.js";
|
|
10
|
-
import {
|
|
11
|
+
import { theme } from "./theme.js";
|
|
11
12
|
import { UpdatesView } from "./updates-view.js";
|
|
12
|
-
const TABS = [
|
|
13
|
-
["sources", "1 Sources"],
|
|
14
|
-
["catalog", "2 Catalog"],
|
|
15
|
-
["installations", "3 Installations"],
|
|
16
|
-
["updates", "4 Updates"],
|
|
17
|
-
["profile", "5 Import/Export"],
|
|
18
|
-
];
|
|
19
13
|
/** Sources derives its own hints from the selected row, so it is excluded here. */
|
|
20
14
|
function hintsOf(view, catalogFocusedOnSource) {
|
|
21
15
|
switch (view) {
|
|
@@ -26,9 +20,13 @@ function hintsOf(view, catalogFocusedOnSource) {
|
|
|
26
20
|
case "profile":
|
|
27
21
|
return "e export · i import · q quit";
|
|
28
22
|
case "updates":
|
|
29
|
-
return "j/k move · space mark · a all · Enter preview · r check again · q quit";
|
|
23
|
+
return "j/k move · space mark · a all · Enter preview · u update · r check again · q quit";
|
|
30
24
|
}
|
|
31
25
|
}
|
|
26
|
+
export function Header({ view, columns }) {
|
|
27
|
+
const innerWidth = Math.max(columns - 4, 1);
|
|
28
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: theme.accent, paddingX: 1, flexDirection: "column", width: columns, children: [_jsxs(Box, { width: innerWidth, justifyContent: "space-between", children: [_jsx(Text, { color: theme.accent, bold: true, children: "AGENT DEPOT" }), AGENT_DEPOT_PACKAGE_VERSION === undefined ? null : _jsxs(Text, { color: theme.value, children: ["v", AGENT_DEPOT_PACKAGE_VERSION] })] }), _jsx(Box, { flexDirection: "column", children: headerTabRows(innerWidth).map((row, rowIndex) => (_jsx(Text, { wrap: "truncate-end", children: row.map(([name, label], tabIndex) => (_jsxs(Text, { color: view === name ? theme.accent : theme.inactive, children: [tabIndex === 0 ? "" : " ", view === name ? "▶ " : "", label] }, name))) }, rowIndex))) })] }));
|
|
29
|
+
}
|
|
32
30
|
export function App({ operations, environment, onExit }) {
|
|
33
31
|
const { exit } = useApp();
|
|
34
32
|
const { columns, rows } = useWindowSize();
|
|
@@ -43,10 +41,15 @@ export function App({ operations, environment, onExit }) {
|
|
|
43
41
|
const [catalogSourceId, setCatalogSourceId] = useState();
|
|
44
42
|
const [sourcesHints, setSourcesHints] = useState(sourceRowHints(undefined));
|
|
45
43
|
const showView = (next) => setView(next);
|
|
46
|
-
useKeys((input) => {
|
|
44
|
+
useKeys((input, key) => {
|
|
47
45
|
if (capturingRef.current)
|
|
48
46
|
return;
|
|
49
|
-
if (
|
|
47
|
+
if (key.tab) {
|
|
48
|
+
const current = TABS.findIndex(([name]) => name === view);
|
|
49
|
+
const direction = key.shift ? -1 : 1;
|
|
50
|
+
showView(TABS[(current + direction + TABS.length) % TABS.length][0]);
|
|
51
|
+
}
|
|
52
|
+
else if (input === "q") {
|
|
50
53
|
onExit?.();
|
|
51
54
|
exit();
|
|
52
55
|
}
|
|
@@ -61,8 +64,10 @@ export function App({ operations, environment, onExit }) {
|
|
|
61
64
|
else if (input === "5")
|
|
62
65
|
showView("profile");
|
|
63
66
|
});
|
|
64
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [
|
|
67
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { view: view, columns: columns }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: view === "sources" ? (_jsx(SourcesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange, onHintsChange: setSourcesHints, onOpenCatalog: (source) => {
|
|
65
68
|
setCatalogSourceId(source.id);
|
|
66
69
|
setView("catalog");
|
|
67
|
-
} })) : view === "installations" ? (_jsx(InstallationsView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "profile" ? (_jsx(ProfileView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "updates" ? (_jsx(UpdatesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : (_jsx(CatalogView, { operations: operations, sourceId: catalogSourceId, environment: environment, onCapturingChange: onCapturingChange })) }), _jsx(Text, { color: theme.muted, children: capturing
|
|
70
|
+
} })) : view === "installations" ? (_jsx(InstallationsView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "profile" ? (_jsx(ProfileView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "updates" ? (_jsx(UpdatesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : (_jsx(CatalogView, { operations: operations, sourceId: catalogSourceId, environment: environment, onCapturingChange: onCapturingChange })) }), _jsx(Text, { color: theme.muted, children: capturing
|
|
71
|
+
? columns < 64 ? "Enter submit · Esc cancel" : "Enter submit · Esc cancel · y/n confirm · space toggle"
|
|
72
|
+
: view === "sources" ? sourcesHints : hintsOf(view, catalogSourceId !== undefined) })] }));
|
|
68
73
|
}
|
|
@@ -47,6 +47,11 @@ function markRepository(context, row, visible) {
|
|
|
47
47
|
/** `i`: installs the highlighted bundle, or the marked Skills that are listed, or the highlighted one. */
|
|
48
48
|
function chooseHosts(context, row) {
|
|
49
49
|
const data = row.node.data;
|
|
50
|
+
if (data.kind === "app") {
|
|
51
|
+
context.setMessage(undefined);
|
|
52
|
+
context.startApp(data.row.entry);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
50
55
|
// A bundle row is not markable, so it acts on itself and never picks up a Skill mark.
|
|
51
56
|
if (data.kind === "bundle") {
|
|
52
57
|
context.setMessage(undefined);
|
|
@@ -87,6 +92,6 @@ export function handleCatalogBrowseKey(context, input, key) {
|
|
|
87
92
|
markRow(context, row);
|
|
88
93
|
else if (input === "a")
|
|
89
94
|
markRepository(context, row, snapshot.visible);
|
|
90
|
-
else if (input === "i" && context.ready
|
|
95
|
+
else if (input === "i" && row && (context.ready || row.node.data.kind === "app"))
|
|
91
96
|
chooseHosts(context, row);
|
|
92
97
|
}
|
|
@@ -6,12 +6,12 @@ import { skillKey } from "./catalog-tree.js";
|
|
|
6
6
|
import { hasUnmanagedCopy } from "./catalog-installs.js";
|
|
7
7
|
import { describeRow, truncateDescription, UNMANAGED_MARKER } from "./catalog-tree.js";
|
|
8
8
|
import { isPackageMode, PackagePanel } from "./package-actions.js";
|
|
9
|
-
import { HostChecklist, PreviewLines } from "./panel-parts.js";
|
|
9
|
+
import { BorderPanel, DecisionPrompt, HostChecklist, PreviewLines } from "./panel-parts.js";
|
|
10
10
|
import { rowStyle } from "./theme.js";
|
|
11
11
|
import { theme } from "./theme.js";
|
|
12
12
|
/** Scope and counts, the filter line, and why the list may be empty. */
|
|
13
|
-
export function CatalogHeader({ scope, listed, installable, total, filter }) {
|
|
14
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["Scope: ", scope, " ", listed, " of ", installable] }), filter.editing || filter.query !== "" ? (_jsxs(Text, { children: ["Filter: ", filter.query, filter.editing ? "_" : ""] })) : null, total === 0 ? _jsx(Text, { children: "No skills" }) : null, total > 0 && installable === 0 ? _jsx(Text, { children: "All skills are installed" }) : null, installable > 0 && listed === 0 ? _jsx(Text, { children: "No matching skills" }) : null] }));
|
|
13
|
+
export function CatalogHeader({ scope, listed, installable, total, appCount, filter }) {
|
|
14
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["Scope: ", scope, " ", listed, " of ", installable, " Skills \u00B7 ", appCount, " Apps"] }), filter.editing || filter.query !== "" ? (_jsxs(Text, { children: ["Filter: ", filter.query, filter.editing ? "_" : ""] })) : null, total === 0 ? _jsx(Text, { children: "No skills" }) : null, total > 0 && installable === 0 ? _jsx(Text, { children: "All skills are installed" }) : null, installable > 0 && listed === 0 ? _jsx(Text, { children: "No matching skills" }) : null] }));
|
|
15
15
|
}
|
|
16
16
|
/** One line of the Catalog tree: cursor, indent, mark box, label and the ownership markers. */
|
|
17
17
|
export function CatalogRowLine({ row, selected, marked, installed }) {
|
|
@@ -20,7 +20,7 @@ export function CatalogRowLine({ row, selected, marked, installed }) {
|
|
|
20
20
|
const ownedBy = data.kind === "skill" ? data.ownedBy : undefined;
|
|
21
21
|
return (_jsxs(Text, { ...rowStyle(selected), color: isGroup ? theme.group : undefined, wrap: "truncate-end", children: [selected ? "> " : " ", " ".repeat(row.depth), data.kind === "skill" ? marked ? "[x] " : "[ ] " : "", data.kind === "skill" && ownedBy !== undefined
|
|
22
22
|
? _jsxs(_Fragment, { children: [data.skill.name, _jsxs(Text, { color: theme.marker, children: [" ", "[", packageOwnerBadge(ownedBy), "]"] }), " ", truncateDescription(data.skill.description)] })
|
|
23
|
-
: describeRow(row), data.kind === "skill" && hasUnmanagedCopy(data.skill, installed)
|
|
23
|
+
: data.kind === "app" ? `App ${data.row.entry.recipe?.name ?? data.row.entry.file}` : describeRow(row), data.kind === "app" ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: data.row.inspection.status === "not installed" ? theme.warning : theme.value, children: data.row.inspection.status })] })) : null, data.kind === "skill" && hasUnmanagedCopy(data.skill, installed)
|
|
24
24
|
? _jsxs(Text, { color: theme.marker, children: [" ", UNMANAGED_MARKER] })
|
|
25
25
|
: null] }));
|
|
26
26
|
}
|
|
@@ -37,23 +37,23 @@ export function ActionPanel({ mode }) {
|
|
|
37
37
|
case "browse":
|
|
38
38
|
return null;
|
|
39
39
|
case "host":
|
|
40
|
-
return (_jsxs(
|
|
40
|
+
return (_jsxs(BorderPanel, { title: `Skill selection · ${mode.skills.length} selected`, children: [_jsxs(Text, { children: ["Install ", describeSkills(mode.skills), ". Host (space/1-4 toggle \u00B7 j/k move \u00B7 Enter continue \u00B7 Esc cancel)"] }), _jsx(HostChecklist, { choices: HOST_CHOICES, cursor: mode.cursor, selected: mode.selected })] }));
|
|
41
41
|
case "scope":
|
|
42
|
-
return
|
|
42
|
+
return _jsx(BorderPanel, { title: "Skill installation scope", children: _jsxs(Text, { children: [SCOPE_CHOICES.map((scope, i) => `${i + 1} ${scope}`).join(" "), " (Esc cancel)"] }) });
|
|
43
43
|
case "version":
|
|
44
|
-
return _jsx(Text, { children: "
|
|
44
|
+
return _jsx(BorderPanel, { title: "Skill version policy", children: _jsx(Text, { children: "1 latest 2 fixed (Esc cancel)" }) });
|
|
45
45
|
case "fixed":
|
|
46
|
-
return
|
|
46
|
+
return _jsx(BorderPanel, { title: "Fixed Skill version", children: _jsxs(Text, { children: [mode.value, "_ (Enter confirm, Esc cancel)"] }) });
|
|
47
47
|
case "confirm-install": {
|
|
48
48
|
const ready = mode.items.filter((item) => item.prepared !== undefined);
|
|
49
49
|
const single = mode.items.length === 1;
|
|
50
|
-
return (_jsxs(
|
|
50
|
+
return (_jsxs(BorderPanel, { title: `Skill install preview · ${ready.length} ready`, children: [mode.items.map(({ skill, prepared, error }) => prepared === undefined ? (_jsxs(Text, { color: theme.error, children: ["Cannot install ", skill.name, ": ", error] }, skillKey(skill))) : (_jsxs(Box, { flexDirection: "column", children: [single ? null : _jsx(Text, { color: theme.group, children: skill.name }), _jsx(PreviewLines, { lines: prepared.preview })] }, skillKey(skill)))), ready.some((item) => item.prepared?.runsExternalCommand) ? _jsx(Text, { color: theme.warning, children: "This install runs an external command (see above)." }) : null, _jsxs(DecisionPrompt, { children: ["Install ", describeSkills(ready.map((item) => item.skill)), "?"] })] }));
|
|
51
51
|
}
|
|
52
52
|
case "confirm-exposure": {
|
|
53
53
|
const names = mode.items.filter((item) => item.prepared?.additionalHostExposure).map((item) => item.skill.name);
|
|
54
|
-
return (_jsxs(Text, { color: theme.warning, children: [names.length === 1 ? `An identical ${names[0]} already exists` : `Identical copies of ${names.join(", ")} already exist`, "; adding the missing Host location needs separate confirmation (
|
|
54
|
+
return (_jsx(BorderPanel, { title: `Skill Host exposure · ${names.length} confirmation`, children: _jsxs(Text, { color: theme.warning, children: [names.length === 1 ? `An identical ${names[0]} already exists` : `Identical copies of ${names.join(", ")} already exist`, "; adding the missing Host location needs separate confirmation. ", _jsx(DecisionPrompt, { children: "Confirm?" })] }) }));
|
|
55
55
|
}
|
|
56
56
|
case "busy":
|
|
57
|
-
return _jsx(Text, { children: mode.label });
|
|
57
|
+
return _jsx(BorderPanel, { title: "Skill install", children: _jsx(Text, { children: mode.label }) });
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -87,6 +87,34 @@ export function useCatalogSkills(operations, sourceId, all, env, reloadKey = 0)
|
|
|
87
87
|
}, [operations, sourceId, all, env, reloadKey]);
|
|
88
88
|
return state;
|
|
89
89
|
}
|
|
90
|
+
export function useCatalogApps(apps) {
|
|
91
|
+
const [state, setState] = useState({ status: "loading" });
|
|
92
|
+
const generation = useRef(0);
|
|
93
|
+
const reload = useCallback(async () => {
|
|
94
|
+
const current = ++generation.current;
|
|
95
|
+
setState({ status: "loading" });
|
|
96
|
+
try {
|
|
97
|
+
const rows = await Promise.all((await apps.load()).filter(entry => entry.applicable).map(async (entry) => ({
|
|
98
|
+
entry,
|
|
99
|
+
inspection: await apps.inspect(entry),
|
|
100
|
+
})));
|
|
101
|
+
if (generation.current === current)
|
|
102
|
+
setState({
|
|
103
|
+
status: "ready",
|
|
104
|
+
rows: rows.filter(row => row.inspection.status === "not installed"),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (generation.current === current)
|
|
109
|
+
setState({ status: "ready", rows: [], error: error instanceof Error ? error.message : String(error) });
|
|
110
|
+
}
|
|
111
|
+
}, [apps]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
void reload();
|
|
114
|
+
return () => { generation.current += 1; };
|
|
115
|
+
}, [reload]);
|
|
116
|
+
return { state, reload };
|
|
117
|
+
}
|
|
90
118
|
/** The Skills already installed, loaded on mount and on demand. */
|
|
91
119
|
export function useInstalledSkills(operations, env, mounted) {
|
|
92
120
|
const [installed, setInstalled] = useState(NO_INSTALLED);
|
|
@@ -23,7 +23,7 @@ export function sourceLabel(source) {
|
|
|
23
23
|
return url.hostname === "github.com" ? repository : `${url.host}/${repository}`;
|
|
24
24
|
}
|
|
25
25
|
/** Skills and their Source's Packages, in first-seen order; a Source with neither never appears. */
|
|
26
|
-
export function buildTree(skills, sources = [], bundles = [], activeDescriptors = []) {
|
|
26
|
+
export function buildTree(skills, sources = [], bundles = [], activeDescriptors = [], apps = []) {
|
|
27
27
|
const labels = new Map(sources.map((source) => [source.id, sourceLabel(source)]));
|
|
28
28
|
const active = new Set(activeDescriptors);
|
|
29
29
|
const bySource = new Map();
|
|
@@ -32,7 +32,7 @@ export function buildTree(skills, sources = [], bundles = [], activeDescriptors
|
|
|
32
32
|
const bundlesBySource = new Map();
|
|
33
33
|
for (const bundle of bundles)
|
|
34
34
|
bundlesBySource.set(bundle.sourceId, [...(bundlesBySource.get(bundle.sourceId) ?? []), bundle]);
|
|
35
|
-
|
|
35
|
+
const sourcesTree = [...new Set([...bySource.keys(), ...bundlesBySource.keys()])].map((sourceId) => {
|
|
36
36
|
const members = bySource.get(sourceId) ?? [];
|
|
37
37
|
const sourceBundles = bundlesBySource.get(sourceId) ?? [];
|
|
38
38
|
const owned = packageOwnedSkillMatcher(sourceBundles.map((entry) => entry.descriptor).filter((descriptor) => active.has(descriptor)));
|
|
@@ -52,13 +52,28 @@ export function buildTree(skills, sources = [], bundles = [], activeDescriptors
|
|
|
52
52
|
],
|
|
53
53
|
};
|
|
54
54
|
});
|
|
55
|
+
return apps.length === 0 ? sourcesTree : [...sourcesTree, {
|
|
56
|
+
id: "apps:user-global",
|
|
57
|
+
data: { kind: "group", label: `Apps (user-global) (${apps.length})` },
|
|
58
|
+
children: apps.map((row) => ({ id: `app:${row.entry.file}`, data: { kind: "app", row } })),
|
|
59
|
+
}];
|
|
55
60
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
61
|
+
function allExpandableIds(roots) {
|
|
62
|
+
const ids = new Set();
|
|
63
|
+
const visit = (nodes) => {
|
|
64
|
+
for (const node of nodes) {
|
|
65
|
+
if ((node.children?.length ?? 0) === 0)
|
|
66
|
+
continue;
|
|
67
|
+
ids.add(node.id);
|
|
68
|
+
visit(node.children);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
visit(roots);
|
|
72
|
+
return ids;
|
|
58
73
|
}
|
|
59
74
|
/** Filtering opens matching Sources; otherwise all groups start collapsed. */
|
|
60
75
|
export function openByDefault(nodes, query) {
|
|
61
|
-
return query === "" ? defaultExpanded() :
|
|
76
|
+
return query === "" ? defaultExpanded() : allExpandableIds(nodes);
|
|
62
77
|
}
|
|
63
78
|
export function describeRow(row) {
|
|
64
79
|
const { data } = row.node;
|
|
@@ -69,6 +84,8 @@ export function describeRow(row) {
|
|
|
69
84
|
if (data.kind === "bundle") {
|
|
70
85
|
return `Package ${data.entry.descriptor.name} (${data.entry.descriptor.host}) ${packageVersionLabel(data.entry.descriptor.version)}`;
|
|
71
86
|
}
|
|
87
|
+
if (data.kind === "app")
|
|
88
|
+
return `App ${data.row.entry.recipe?.name ?? data.row.entry.file} ${data.row.inspection.status}`;
|
|
72
89
|
return `${data.skill.name} ${truncateDescription(data.skill.description)}`;
|
|
73
90
|
}
|
|
74
91
|
/** The Skills under the tree's Sources, in display order. */
|