@iseoane/agent-depot 0.5.1 → 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 +37 -0
- package/README.es.md +120 -53
- package/README.md +101 -41
- package/dist/src/app-flow.js +56 -24
- package/dist/src/cli.js +55 -0
- package/dist/src/package-cli.js +2 -1
- package/dist/src/package-flow.js +1 -1
- package/dist/src/package-host-state.js +10 -6
- 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/self-update.js +141 -0
- 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 +21 -7
- 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 +35 -14
- package/resources/pi/agents-panel/contract.ts +62 -0
- package/resources/pi/agents-panel/index.ts +471 -0
- package/skills/doctor-md-agents/SKILL.md +2 -1
- package/skills/doctor-md-agents/scripts/scan_corrections.py +141 -13
- package/skills/doctor-md-skill/references/supported-harnesses.md +2 -0
- package/skills/doctor-md-skill/scripts/collect_sessions.py +26 -2
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { compareAppVersions } from "./app-version.js";
|
|
4
|
+
import { createPortableExecutableResolver } from "./executable-resolution.js";
|
|
5
|
+
import { runProcess } from "./process-runner.js";
|
|
6
|
+
const REGISTRY_URL = "https://registry.npmjs.org";
|
|
7
|
+
const MAX_REGISTRY_RESPONSE_BYTES = 8 * 1024;
|
|
8
|
+
const MAX_NPM_STDERR_BYTES = 16 * 1024;
|
|
9
|
+
const COMMAND_TIMEOUT_MS = 30_000;
|
|
10
|
+
function compareVersions(left, right) {
|
|
11
|
+
const comparison = compareAppVersions(left, right);
|
|
12
|
+
if (comparison === undefined)
|
|
13
|
+
throw new Error(`Invalid semantic version comparison ${JSON.stringify(left)} and ${JSON.stringify(right)}`);
|
|
14
|
+
return comparison;
|
|
15
|
+
}
|
|
16
|
+
async function latestVersion(fetcher, packageName) {
|
|
17
|
+
const encodedName = encodeURIComponent(packageName).replace(/^%40/u, "@");
|
|
18
|
+
const response = await fetcher(`${REGISTRY_URL}/${encodedName}/latest`, {
|
|
19
|
+
signal: AbortSignal.timeout(10_000),
|
|
20
|
+
credentials: "omit",
|
|
21
|
+
redirect: "error",
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok)
|
|
24
|
+
throw new Error(`npm registry returned HTTP ${response.status}`);
|
|
25
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
26
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_REGISTRY_RESPONSE_BYTES) {
|
|
27
|
+
throw new Error("npm registry response exceeded the size limit");
|
|
28
|
+
}
|
|
29
|
+
if (!response.body)
|
|
30
|
+
throw new Error("npm registry response did not include a body");
|
|
31
|
+
const reader = response.body.getReader();
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let responseBytes = 0;
|
|
34
|
+
try {
|
|
35
|
+
for (;;) {
|
|
36
|
+
const chunk = await reader.read();
|
|
37
|
+
if (chunk.done)
|
|
38
|
+
break;
|
|
39
|
+
responseBytes += chunk.value.byteLength;
|
|
40
|
+
if (responseBytes > MAX_REGISTRY_RESPONSE_BYTES) {
|
|
41
|
+
await reader.cancel();
|
|
42
|
+
throw new Error("npm registry response exceeded the size limit");
|
|
43
|
+
}
|
|
44
|
+
chunks.push(chunk.value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
reader.releaseLock();
|
|
49
|
+
}
|
|
50
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
51
|
+
let payload;
|
|
52
|
+
try {
|
|
53
|
+
payload = JSON.parse(body);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error("npm registry returned invalid JSON");
|
|
57
|
+
}
|
|
58
|
+
if (typeof payload !== "object" || payload === null || !("version" in payload) || typeof payload.version !== "string") {
|
|
59
|
+
throw new Error("npm registry response did not contain a version");
|
|
60
|
+
}
|
|
61
|
+
compareVersions(payload.version, payload.version);
|
|
62
|
+
return payload.version;
|
|
63
|
+
}
|
|
64
|
+
function successfulStdout(result) {
|
|
65
|
+
if (result.code !== 0 || result.signal !== null || result.timedOut || result.outputTooLarge)
|
|
66
|
+
return undefined;
|
|
67
|
+
return result.stdout.toString("utf8").trim();
|
|
68
|
+
}
|
|
69
|
+
export function createSelfUpdateOperations(options) {
|
|
70
|
+
const resolveExecutable = options.resolveExecutable ?? createPortableExecutableResolver();
|
|
71
|
+
const run = options.run ?? runProcess;
|
|
72
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
73
|
+
async function npmGlobalExecutable() {
|
|
74
|
+
const npm = await resolveExecutable("npm");
|
|
75
|
+
if (npm.blocked || npm.executable === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const target = await realpath(npm.executable).catch(() => undefined);
|
|
78
|
+
if (target === undefined || path.basename(target) !== "npm-cli.js" || path.basename(path.dirname(target)) !== "bin" || path.basename(path.dirname(path.dirname(target))) !== "npm")
|
|
79
|
+
return undefined;
|
|
80
|
+
let rootResult;
|
|
81
|
+
try {
|
|
82
|
+
rootResult = await run(npm.executable, ["root", "--global", "--silent", "--logs-max=0", "--no-update-notifier"], {
|
|
83
|
+
captureStdout: true,
|
|
84
|
+
maxOutputBytes: 4096,
|
|
85
|
+
maxStderrBytes: MAX_NPM_STDERR_BYTES,
|
|
86
|
+
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const globalRoot = successfulStdout(rootResult);
|
|
93
|
+
if (globalRoot === undefined || !path.isAbsolute(globalRoot))
|
|
94
|
+
return undefined;
|
|
95
|
+
const installedPackagePath = path.join(globalRoot, ...options.packageName.split("/"));
|
|
96
|
+
const installedPackage = await lstat(installedPackagePath).catch(() => undefined);
|
|
97
|
+
if (installedPackage === undefined || !installedPackage.isDirectory())
|
|
98
|
+
return undefined;
|
|
99
|
+
const actualRoot = await realpath(options.packageRoot).catch(() => undefined);
|
|
100
|
+
const reportedRoot = await realpath(installedPackagePath).catch(() => undefined);
|
|
101
|
+
return actualRoot !== undefined && actualRoot === reportedRoot ? npm.executable : undefined;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
async check() {
|
|
105
|
+
compareVersions(options.packageVersion, options.packageVersion);
|
|
106
|
+
const npm = await npmGlobalExecutable();
|
|
107
|
+
if (npm === undefined) {
|
|
108
|
+
return { kind: "unsupported", reason: "this CLI is not verified in npm's global package directory" };
|
|
109
|
+
}
|
|
110
|
+
const latest = await latestVersion(fetcher, options.packageName);
|
|
111
|
+
const comparison = compareVersions(latest, options.packageVersion);
|
|
112
|
+
if (comparison === 0)
|
|
113
|
+
return { kind: "current", current: options.packageVersion, latest };
|
|
114
|
+
if (comparison < 0)
|
|
115
|
+
return { kind: "ahead", current: options.packageVersion, latest };
|
|
116
|
+
return { kind: "available", current: options.packageVersion, latest };
|
|
117
|
+
},
|
|
118
|
+
async apply(version) {
|
|
119
|
+
let comparison;
|
|
120
|
+
try {
|
|
121
|
+
comparison = compareVersions(version, options.packageVersion);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new Error(`Refusing invalid Agent Depot version ${JSON.stringify(version)}`);
|
|
125
|
+
}
|
|
126
|
+
if (comparison <= 0)
|
|
127
|
+
throw new Error(`Refusing to downgrade Agent Depot from ${options.packageVersion} to ${version}`);
|
|
128
|
+
const npm = await npmGlobalExecutable();
|
|
129
|
+
if (npm === undefined)
|
|
130
|
+
throw new Error("this CLI is not verified in npm's global package directory");
|
|
131
|
+
const result = await run(npm, [
|
|
132
|
+
"install", "--global", "--no-audit", "--no-fund", "--no-update-notifier", "--logs-max=0", "--ignore-scripts",
|
|
133
|
+
"--registry=https://registry.npmjs.org/", `${options.packageName}@${version}`,
|
|
134
|
+
], { maxStderrBytes: MAX_NPM_STDERR_BYTES, timeoutMs: COMMAND_TIMEOUT_MS });
|
|
135
|
+
if (result.code !== 0 || result.signal !== null || result.timedOut || result.outputTooLarge) {
|
|
136
|
+
const detail = result.timedOut ? "timed out" : result.outputTooLarge ? "produced too much output" : result.signal ? `stopped by ${result.signal}` : `exited with code ${result.code ?? "unknown"}`;
|
|
137
|
+
throw new Error(`npm global update ${detail}${result.stderr ? `: ${result.stderr.trim()}` : ""}`);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -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. */
|