@iseoane/agent-depot 0.4.1 → 0.5.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 +29 -0
- package/README.es.md +969 -0
- package/README.md +244 -23
- package/dist/src/app-flow.js +52 -51
- package/dist/src/app-recipe-removal.js +63 -0
- package/dist/src/cli.js +122 -11
- package/dist/src/executable-resolution.js +54 -0
- package/dist/src/git-source.js +129 -6
- package/dist/src/package-bundles.js +614 -0
- package/dist/src/package-cli.js +412 -0
- package/dist/src/package-content.js +158 -0
- package/dist/src/package-flow.js +636 -0
- package/dist/src/package-host-plans.js +240 -0
- package/dist/src/package-host-state.js +349 -0
- package/dist/src/package-model.js +33 -0
- package/dist/src/package-owned-skills.js +122 -0
- package/dist/src/package-state.js +404 -0
- package/dist/src/profile-cli.js +15 -9
- package/dist/src/profile-export.js +44 -9
- package/dist/src/profile-import.js +80 -17
- package/dist/src/profile.js +78 -3
- package/dist/src/skill-discovery.js +2 -2
- package/dist/src/skill-install.js +16 -0
- package/dist/src/sources.js +11 -0
- package/dist/src/tui/app-actions.js +1 -1
- package/dist/src/tui/app.js +5 -6
- package/dist/src/tui/catalog-browse.js +10 -3
- package/dist/src/tui/catalog-panel.js +7 -3
- package/dist/src/tui/catalog-state.js +57 -6
- package/dist/src/tui/catalog-tree.js +52 -10
- package/dist/src/tui/catalog-view.js +27 -4
- package/dist/src/tui/environment.js +8 -1
- package/dist/src/tui/installations-browse.js +20 -2
- package/dist/src/tui/installations-rows.js +2 -1
- package/dist/src/tui/installations-tree.js +14 -1
- package/dist/src/tui/installations-view.js +8 -2
- package/dist/src/tui/installations.js +12 -2
- package/dist/src/tui/manage-actions.js +2 -0
- package/dist/src/tui/manage-panel.js +5 -0
- package/dist/src/tui/package-actions.js +165 -0
- package/dist/src/tui/package-rows.js +87 -0
- package/dist/src/tui/profile-view.js +36 -14
- package/dist/src/tui/sources-mode.js +2 -0
- package/dist/src/tui/sources-recipes.js +116 -53
- package/dist/src/tui/sources-view.js +57 -19
- package/dist/src/tui/tree.js +3 -4
- package/dist/src/tui/updates-flow.js +4 -1
- package/dist/src/tui/updates-panel.js +2 -2
- package/dist/src/tui/updates.js +72 -4
- package/dist/src/user-global-skill-inventory.js +7 -2
- package/package.json +2 -1
- package/skills/agent-depot-apprecipe/SKILL.md +29 -5
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describeCoordinates } from "../package-model.js";
|
|
3
|
+
import { errorText } from "./batch.js";
|
|
4
|
+
import { answerYesNo } from "./mode-keys.js";
|
|
5
|
+
import { installedEvidenceLabel, packagePlanLines } from "./package-rows.js";
|
|
6
|
+
import { PreviewConfirm } from "./panel-parts.js";
|
|
7
|
+
/** The per-row keys of a Package; Enter is approval, handled beside these. */
|
|
8
|
+
export const PACKAGE_ACTION_KEYS = new Map([
|
|
9
|
+
["i", "install"],
|
|
10
|
+
["U", "update"],
|
|
11
|
+
["u", "uninstall"],
|
|
12
|
+
["f", "forget"],
|
|
13
|
+
]);
|
|
14
|
+
export const PACKAGE_KINDS = ["package-approve", "package-confirm", "package-forget"];
|
|
15
|
+
/** Narrows the view's mode without asserting a partial mode is a Package mode. */
|
|
16
|
+
export function isPackageMode(mode) {
|
|
17
|
+
return PACKAGE_KINDS.some(kind => kind === mode.kind);
|
|
18
|
+
}
|
|
19
|
+
/** `install`, `update`, `uninstall` and `forget` plan a step; `approve` writes the install receipt. */
|
|
20
|
+
export async function startPackageAction(context, target, input) {
|
|
21
|
+
const { manage, packages } = context;
|
|
22
|
+
manage.setMessage(undefined);
|
|
23
|
+
try {
|
|
24
|
+
if (input === "approve") {
|
|
25
|
+
manage.setMode({ kind: "busy", label: `Preparing the declaration of ${target.name}...` });
|
|
26
|
+
const plan = await packages.planLifecycle(target.selection, "install");
|
|
27
|
+
if (manage.isMounted()) {
|
|
28
|
+
manage.setMode({ kind: "package-approve", target, lines: packagePlanLines(plan, "needs approval") });
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const action = PACKAGE_ACTION_KEYS.get(input);
|
|
33
|
+
if (action === "forget") {
|
|
34
|
+
manage.setMode({ kind: "busy", label: `Preparing to forget ${target.name}...` });
|
|
35
|
+
const inspection = await packages.inspect(target.selection);
|
|
36
|
+
if (manage.isMounted()) {
|
|
37
|
+
manage.setMode({
|
|
38
|
+
kind: "package-forget",
|
|
39
|
+
target,
|
|
40
|
+
lines: [
|
|
41
|
+
`Package: ${target.name} (${target.selection.host})`,
|
|
42
|
+
`Coordinates: ${describeCoordinates(target.selection)}`,
|
|
43
|
+
`Installed: ${installedEvidenceLabel(inspection.installed)}`,
|
|
44
|
+
"Forgetting drops the Agent Depot selection record only; the host install is left as it is and no host command runs.",
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (action !== undefined) {
|
|
51
|
+
manage.setMode({ kind: "busy", label: `Preparing the ${action} of ${target.name}...` });
|
|
52
|
+
const plan = await packages.planLifecycle(target.selection, action);
|
|
53
|
+
const approval = await packages.approvalStatus(target.selection, action);
|
|
54
|
+
if (manage.isMounted())
|
|
55
|
+
manage.setMode({ kind: "package-confirm", plan, approval });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await manage.finish({ kind: "error", text: `The action ${JSON.stringify(input)} is not available for Packages` });
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
await manage.finish({ kind: "error", text: errorText(error) });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** The `y` of a lifecycle confirmation whose receipt is missing approves it first, then asks again. */
|
|
65
|
+
async function approveThenConfirm(context, mode) {
|
|
66
|
+
const { manage, packages } = context;
|
|
67
|
+
manage.setMode({ kind: "busy", label: `Approving the ${mode.plan.action} declaration of ${mode.plan.descriptor.name}...` });
|
|
68
|
+
try {
|
|
69
|
+
await packages.approve(mode.plan.selection, mode.plan.action);
|
|
70
|
+
if (manage.isMounted())
|
|
71
|
+
manage.setMode({ ...mode, approval: "approved" });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (manage.isMounted())
|
|
75
|
+
await manage.finish({ kind: "error", text: errorText(error) });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function resultLine(plan, result) {
|
|
79
|
+
const name = plan.descriptor.name;
|
|
80
|
+
switch (result.status) {
|
|
81
|
+
case "installed":
|
|
82
|
+
return { ok: true, text: `${name}: installed (${installedEvidenceLabel(result.verified)})` };
|
|
83
|
+
case "updated":
|
|
84
|
+
return { ok: true, text: `${name}: updated (${installedEvidenceLabel(result.previous)} -> ${installedEvidenceLabel(result.verified)})` };
|
|
85
|
+
case "removed":
|
|
86
|
+
return { ok: true, text: `${name}: removed` };
|
|
87
|
+
case "selected":
|
|
88
|
+
return { ok: true, text: `${name}: selected` };
|
|
89
|
+
case "forgotten":
|
|
90
|
+
return { ok: true, text: `${name}: forgotten` };
|
|
91
|
+
case "not confirmed":
|
|
92
|
+
return { ok: true, text: `${name}: not changed` };
|
|
93
|
+
case "stale plan":
|
|
94
|
+
return { ok: false, text: `${name}: ${plan.action} not run: ${result.reason}` };
|
|
95
|
+
case "manual required":
|
|
96
|
+
return { ok: false, text: `${name}: ${plan.action} needs a manual step: ${result.reason}` };
|
|
97
|
+
case "failed":
|
|
98
|
+
return { ok: false, text: `${name}: failed: ${result.reason}${result.output === undefined ? "" : ` (${result.output})`}` };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function executePackagePlan(context, plan) {
|
|
102
|
+
const { manage, packages } = context;
|
|
103
|
+
manage.setMode({ kind: "busy", label: `Running the ${plan.action} of ${plan.descriptor.name}...` });
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
result = await packages.executeLifecycle(plan, true);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
await manage.finish({ kind: "error", text: errorText(error) });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const line = resultLine(plan, result);
|
|
113
|
+
await manage.finish({ kind: line.ok ? "ok" : "error", text: line.text });
|
|
114
|
+
}
|
|
115
|
+
async function forgetSelection(context, mode) {
|
|
116
|
+
const { manage, packages } = context;
|
|
117
|
+
manage.setMode({ kind: "busy", label: `Forgetting ${mode.target.name}...` });
|
|
118
|
+
try {
|
|
119
|
+
await packages.forget(mode.target.selection, true);
|
|
120
|
+
await manage.finish({ kind: "ok", text: `Forgot ${mode.target.name}; no host command ran` });
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
await manage.finish({ kind: "error", text: errorText(error) });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Applies the y/n keys of every Package confirmation; `n` and Esc run nothing. */
|
|
127
|
+
export function handlePackageKey(context, mode, input, key) {
|
|
128
|
+
const { manage, packages } = context;
|
|
129
|
+
const cancel = (text) => {
|
|
130
|
+
manage.setMessage({ kind: "ok", text });
|
|
131
|
+
manage.browse();
|
|
132
|
+
};
|
|
133
|
+
switch (mode.kind) {
|
|
134
|
+
case "package-approve":
|
|
135
|
+
answerYesNo(input, key, () => {
|
|
136
|
+
manage.setMode({ kind: "busy", label: `Approving the install declaration of ${mode.target.name}...` });
|
|
137
|
+
void packages.approve(mode.target.selection, "install").then(() => manage.finish({ kind: "ok", text: `Approved the install declaration of ${mode.target.name}` }), error => manage.finish({ kind: "error", text: errorText(error) }));
|
|
138
|
+
}, () => cancel("Package action cancelled"));
|
|
139
|
+
return;
|
|
140
|
+
case "package-forget":
|
|
141
|
+
answerYesNo(input, key, () => void forgetSelection(context, mode), () => cancel("Package action cancelled"));
|
|
142
|
+
return;
|
|
143
|
+
case "package-confirm":
|
|
144
|
+
answerYesNo(input, key, () => {
|
|
145
|
+
if (mode.approval === "needs approval")
|
|
146
|
+
void approveThenConfirm(context, mode);
|
|
147
|
+
else
|
|
148
|
+
void executePackagePlan(context, mode.plan);
|
|
149
|
+
}, () => cancel("Package action cancelled"));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Renders a Package prompt with the existing preview panel and y/n confirmation. */
|
|
154
|
+
export function PackagePanel({ mode }) {
|
|
155
|
+
switch (mode.kind) {
|
|
156
|
+
case "package-approve":
|
|
157
|
+
return _jsx(PreviewConfirm, { lines: mode.lines, question: `Approve this install declaration for ${mode.target.name}? y/n` });
|
|
158
|
+
case "package-forget":
|
|
159
|
+
return _jsx(PreviewConfirm, { lines: mode.lines, question: `Forget the selection record of ${mode.target.name}? y/n` });
|
|
160
|
+
case "package-confirm":
|
|
161
|
+
return (_jsx(PreviewConfirm, { lines: packagePlanLines(mode.plan, mode.approval), question: mode.approval === "needs approval"
|
|
162
|
+
? "Approve this declaration and continue? y/n"
|
|
163
|
+
: `${mode.plan.action} ${mode.plan.descriptor.name}? y/n` }));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { classifyUpdate } from "../package-host-plans.js";
|
|
2
|
+
import { describeCoordinates, } from "../package-model.js";
|
|
3
|
+
/** Version evidence as a short label; `unknown` when the host or the bundle states none. */
|
|
4
|
+
export function packageVersionLabel(evidence) {
|
|
5
|
+
if (evidence === undefined)
|
|
6
|
+
return "unknown";
|
|
7
|
+
return evidence.kind === "manifest-version" ? evidence.version : evidence.commit.slice(0, 7);
|
|
8
|
+
}
|
|
9
|
+
/** What the host reports about an install, as one phrase. Never a guess. */
|
|
10
|
+
export function installedEvidenceLabel(evidence) {
|
|
11
|
+
switch (evidence.kind) {
|
|
12
|
+
case "installed": {
|
|
13
|
+
if (evidence.version !== undefined)
|
|
14
|
+
return `installed ${evidence.version}`;
|
|
15
|
+
if (evidence.commit !== undefined)
|
|
16
|
+
return `installed at ${evidence.commit.slice(0, 7)}`;
|
|
17
|
+
return "installed with no version evidence";
|
|
18
|
+
}
|
|
19
|
+
case "absent": {
|
|
20
|
+
return "not installed";
|
|
21
|
+
}
|
|
22
|
+
case "unknown": {
|
|
23
|
+
return `unknown (${evidence.reason})`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** The shared update vocabulary; the Updates list prints `up to date`, not `current`. */
|
|
28
|
+
export function packageStatusLabel(status) {
|
|
29
|
+
if (status === "current")
|
|
30
|
+
return "up to date";
|
|
31
|
+
if (status === "unknown")
|
|
32
|
+
return "cannot assess";
|
|
33
|
+
return status;
|
|
34
|
+
}
|
|
35
|
+
/** What a selection record itself remembers, for lists that do not read host evidence. */
|
|
36
|
+
export function recordedVersionLabel(record) {
|
|
37
|
+
if (record.verified !== undefined)
|
|
38
|
+
return packageVersionLabel(record.verified);
|
|
39
|
+
if (record.lastDelegatedCommit !== undefined)
|
|
40
|
+
return record.lastDelegatedCommit.slice(0, 7);
|
|
41
|
+
return "unknown";
|
|
42
|
+
}
|
|
43
|
+
export function packageRow(record, inspection) {
|
|
44
|
+
return {
|
|
45
|
+
selection: record.selection,
|
|
46
|
+
name: inspection.descriptor?.name ?? describeCoordinates(record.selection),
|
|
47
|
+
installed: installedEvidenceLabel(inspection.installed),
|
|
48
|
+
available: packageVersionLabel(inspection.available),
|
|
49
|
+
status: classifyUpdate(inspection.installed, inspection.available),
|
|
50
|
+
...(inspection.drift === undefined ? {} : { drift: inspection.drift }),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Host, evidence and status of one row, as the Installations line prints it. */
|
|
54
|
+
export function describePackageRow(row) {
|
|
55
|
+
const drift = row.drift === undefined ? "" : ` drift: ${row.drift}`;
|
|
56
|
+
return `${row.selection.host} ${row.installed} available ${row.available} ${packageStatusLabel(row.status)}${drift}`;
|
|
57
|
+
}
|
|
58
|
+
/** One component of the inventory; the executable effect and a host-only owner are called out. */
|
|
59
|
+
export function describePackageComponent(component) {
|
|
60
|
+
const risk = component.effect === "executable" ? " runs a host process" : "";
|
|
61
|
+
const ownership = component.ownership === "host-only" ? " host-only: Agent Depot never manages it" : "";
|
|
62
|
+
const paths = component.paths.length === 0 ? "" : ` ${component.paths.join(", ")}`;
|
|
63
|
+
return `${component.kind} ${component.ownership} ${component.effect}${risk}${ownership}${paths}`;
|
|
64
|
+
}
|
|
65
|
+
/** The preview lines of one frozen plan: origin, inventory, argv, blast radius and approval state. */
|
|
66
|
+
export function packagePlanLines(plan, approval) {
|
|
67
|
+
const { descriptor, selection } = plan;
|
|
68
|
+
return [
|
|
69
|
+
`Environment: ${plan.environment}`,
|
|
70
|
+
`Package: ${descriptor.name} (${selection.host}) ${plan.action}`,
|
|
71
|
+
`Coordinates: ${describeCoordinates(selection)}`,
|
|
72
|
+
`Origin: manifest ${plan.origin.manifestDigest.slice(0, 12)}${plan.origin.resolvedCommit === undefined ? "" : ` commit ${plan.origin.resolvedCommit.slice(0, 7)}`}`,
|
|
73
|
+
`Available: ${packageVersionLabel(descriptor.version)}`,
|
|
74
|
+
`Components (${descriptor.components.length}):`,
|
|
75
|
+
...descriptor.components.map((component) => ` ${describePackageComponent(component)}`),
|
|
76
|
+
...(plan.commands.length === 0
|
|
77
|
+
? ["This action runs no host command."]
|
|
78
|
+
: plan.commands.flatMap((command, position) => [
|
|
79
|
+
`Step ${position + 1} (${command.purpose}): ${JSON.stringify([command.executable, ...command.args])}`,
|
|
80
|
+
`Executable: ${command.resolvedPath ?? command.executable}`,
|
|
81
|
+
])),
|
|
82
|
+
`Blast radius: ${plan.affects.map(describeCoordinates).join(", ")}`,
|
|
83
|
+
`Working directory: ${plan.cwd}`,
|
|
84
|
+
`Approval: ${approval === "approved" ? "approved" : "needs approval"}`,
|
|
85
|
+
...plan.warnings.map((warning) => `Warning: ${warning}`),
|
|
86
|
+
];
|
|
87
|
+
}
|
|
@@ -3,9 +3,10 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { createAppOperations } from "../app-flow.js";
|
|
6
|
-
import { buildProfileExport, profileExportPreview, writeProfileExport, } from "../profile-export.js";
|
|
6
|
+
import { buildProfileExport, describeProfilePackage, profileExportPreview, writeProfileExport, } from "../profile-export.js";
|
|
7
7
|
import { applyProfileImport, buildProfileImport, } from "../profile-import.js";
|
|
8
8
|
import { PROFILE_FORMAT } from "../profile.js";
|
|
9
|
+
import { packageOperationsOf } from "./environment.js";
|
|
9
10
|
import { useKeys } from "./keys.js";
|
|
10
11
|
import { answerYesNo, moveCursor, toggleChoice } from "./mode-keys.js";
|
|
11
12
|
import { ChecklistLines, PreviewConfirm, PreviewLines } from "./panel-parts.js";
|
|
@@ -21,6 +22,9 @@ function rowsFromExport(plan) {
|
|
|
21
22
|
...plan.profile.apps.map((value, index) => ({
|
|
22
23
|
block: "apps", index, label: `Apps: ${value.name} (${value.platform ?? "all platforms"})`, selectable: true,
|
|
23
24
|
})),
|
|
25
|
+
...plan.profile.packages.map((value, index) => ({
|
|
26
|
+
block: "packages", index, label: `Packages: ${describeProfilePackage(value)}`, selectable: true,
|
|
27
|
+
})),
|
|
24
28
|
...plan.exclusions.map((item, index) => ({
|
|
25
29
|
block: "excluded", index, label: `Excluded ${item.label}: ${item.reason}`, selectable: false,
|
|
26
30
|
})),
|
|
@@ -29,21 +33,35 @@ function rowsFromExport(plan) {
|
|
|
29
33
|
})),
|
|
30
34
|
];
|
|
31
35
|
}
|
|
36
|
+
/** The four import blocks in one indexed list, so the checklist and the confirmation share positions. */
|
|
37
|
+
function planEntries(plan) {
|
|
38
|
+
return [...plan.items, ...plan.packages];
|
|
39
|
+
}
|
|
32
40
|
function rowsFromPlan(plan) {
|
|
33
|
-
return plan.
|
|
41
|
+
return planEntries(plan).map((item, index) => ({
|
|
34
42
|
block: item.block, index,
|
|
35
43
|
label: `${item.status}: ${item.label}${item.difference ? `; ${item.difference}` : ""}`,
|
|
36
44
|
selectable: item.status === "add",
|
|
37
45
|
}));
|
|
38
46
|
}
|
|
39
|
-
function
|
|
40
|
-
return plan.
|
|
47
|
+
function selectedPlanEntries(plan, selected) {
|
|
48
|
+
return planEntries(plan).filter((item, index) => item.status !== "add" || selected.some(row => row.index === index));
|
|
49
|
+
}
|
|
50
|
+
/** A plan whose items are the chosen subset, split back into the block the writer consumes. */
|
|
51
|
+
function chosenPlan(plan, selected) {
|
|
52
|
+
const entries = selectedPlanEntries(plan, selected);
|
|
53
|
+
return {
|
|
54
|
+
...plan,
|
|
55
|
+
items: entries.filter((item) => item.block !== "packages"),
|
|
56
|
+
packages: entries.filter((item) => item.block === "packages"),
|
|
57
|
+
};
|
|
41
58
|
}
|
|
42
59
|
/** Collect choices, display shared-core previews and confirm portable Profile transfers. */
|
|
43
60
|
export function ProfileView({ operations, environment = {}, onCapturingChange, listHeight }) {
|
|
44
61
|
const apps = useMemo(() => createAppOperations({
|
|
45
62
|
homeDirectory: environment.homeDirectory, ...environment.appEnvironment,
|
|
46
63
|
}), [environment]);
|
|
64
|
+
const packages = useMemo(() => packageOperationsOf(environment), [environment]);
|
|
47
65
|
const [mode, setModeState] = useState("browse");
|
|
48
66
|
const modeRef = useRef("browse");
|
|
49
67
|
const mounted = useRef(true);
|
|
@@ -113,6 +131,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
113
131
|
sources: profile.sources.filter((_, index) => isChosen("sources", index)),
|
|
114
132
|
skills: profile.skills.filter((_, index) => isChosen("skills", index)),
|
|
115
133
|
apps: profile.apps.filter((_, index) => isChosen("apps", index)),
|
|
134
|
+
packages: profile.packages.filter((_, index) => isChosen("packages", index)),
|
|
116
135
|
};
|
|
117
136
|
};
|
|
118
137
|
useKeys((input, key) => {
|
|
@@ -123,7 +142,9 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
123
142
|
if (input === "e") {
|
|
124
143
|
action.current = "export";
|
|
125
144
|
void run(async () => {
|
|
126
|
-
const exported = await buildProfileExport(operations, apps, {
|
|
145
|
+
const exported = await buildProfileExport(operations, apps, {
|
|
146
|
+
homeDirectory: environment.homeDirectory, packageOperations: packages,
|
|
147
|
+
});
|
|
127
148
|
if (!mounted.current)
|
|
128
149
|
return;
|
|
129
150
|
setProfile(exported.profile);
|
|
@@ -156,7 +177,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
156
177
|
else
|
|
157
178
|
void run(async () => {
|
|
158
179
|
const raw = JSON.parse(await readFile(typed.current, "utf8"));
|
|
159
|
-
const imported = await buildProfileImport(raw, operations, apps);
|
|
180
|
+
const imported = await buildProfileImport(raw, operations, apps, {}, packages);
|
|
160
181
|
profileVersion.current = raw.agentDepotVersion;
|
|
161
182
|
if (!mounted.current)
|
|
162
183
|
return;
|
|
@@ -183,8 +204,8 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
183
204
|
result = [`Exported profile to ${typed.current}`];
|
|
184
205
|
}
|
|
185
206
|
else {
|
|
186
|
-
const confirmed =
|
|
187
|
-
const outcomes = await applyProfileImport(confirmed, operations, apps, true, () => undefined, { homeDirectory: environment.homeDirectory, ...environment.installationOptions });
|
|
207
|
+
const confirmed = chosenPlan(plan, selectedRef.current);
|
|
208
|
+
const outcomes = await applyProfileImport(confirmed, operations, apps, true, () => undefined, { homeDirectory: environment.homeDirectory, packageOperations: packages, ...environment.installationOptions });
|
|
188
209
|
result = outcomes.map(outcome => `${outcome.status}: ${outcome.item.label}${outcome.detail ? `; ${outcome.detail}` : ""}`);
|
|
189
210
|
}
|
|
190
211
|
if (mounted.current) {
|
|
@@ -225,13 +246,14 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
225
246
|
else
|
|
226
247
|
void run(async () => {
|
|
227
248
|
// Reuse the CLI's core preview for the exact selected subset, including argv and Source notes.
|
|
228
|
-
const
|
|
249
|
+
const entries = selectedPlanEntries(plan, selectedRef.current);
|
|
229
250
|
const preview = await buildProfileImport({
|
|
230
251
|
format: PROFILE_FORMAT, agentDepotVersion: profileVersion.current,
|
|
231
|
-
sources:
|
|
232
|
-
skills:
|
|
233
|
-
apps:
|
|
234
|
-
|
|
252
|
+
sources: entries.filter(item => item.block === "sources").map(item => item.value),
|
|
253
|
+
skills: entries.filter(item => item.block === "skills").map(item => item.value),
|
|
254
|
+
apps: entries.filter(item => item.block === "apps").map(item => item.value),
|
|
255
|
+
packages: entries.filter(item => item.block === "packages").map(item => item.value),
|
|
256
|
+
}, operations, apps, {}, packages);
|
|
235
257
|
if (mounted.current)
|
|
236
258
|
showLines(preview.preview, "confirm");
|
|
237
259
|
});
|
|
@@ -239,7 +261,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
239
261
|
});
|
|
240
262
|
const listing = mode === "export-list" || mode === "import-list";
|
|
241
263
|
const window = computeWindow(listing ? rows.length : lines.length, cursor, height);
|
|
242
|
-
return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Portable user-global Profile \u00B7 e export \u00B7 i import" }), mode === "busy" ? _jsx(Text, { children: "Loading Profile\u2026" }) : null, mode.endsWith("-path") ? _jsxs(Text, { children: ["Profile path: ", text, "_"] }) : null, listing ? ["sources", "skills", "apps", "excluded"].map(block => {
|
|
264
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Portable user-global Profile \u00B7 e export \u00B7 i import" }), mode === "busy" ? _jsx(Text, { children: "Loading Profile\u2026" }) : null, mode.endsWith("-path") ? _jsxs(Text, { children: ["Profile path: ", text, "_"] }) : null, listing ? ["sources", "skills", "apps", "packages", "excluded"].map(block => {
|
|
243
265
|
const choices = rows.slice(window.start, window.end).filter(row => row.block === block);
|
|
244
266
|
if (!choices.length)
|
|
245
267
|
return null;
|
|
@@ -2,6 +2,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { createAppOperations } from "../app-flow.js";
|
|
5
|
+
import { planAppRecipeRemoval, removeAppRecipe } from "../app-recipe-removal.js";
|
|
5
6
|
import { BUILT_IN_SOURCE } from "../sources.js";
|
|
6
7
|
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
7
8
|
import { ActionPanel } from "./catalog-panel.js";
|
|
@@ -11,13 +12,11 @@ import { errorText } from "./batch.js";
|
|
|
11
12
|
import { useMounted } from "./view-state.js";
|
|
12
13
|
import { PreviewConfirm } from "./panel-parts.js";
|
|
13
14
|
import { answerYesNo } from "./mode-keys.js";
|
|
14
|
-
import {
|
|
15
|
-
import { computeWindow, pageStep } from "./window.js";
|
|
15
|
+
import { theme } from "./theme.js";
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* and the Git list's share of the common height budget. Sources owns key dispatch.
|
|
17
|
+
* Recipe loading and captured actions; Sources owns the single list and cursor.
|
|
19
18
|
*/
|
|
20
|
-
export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
19
|
+
export function useSourcesRecipes({ operations, environment, onCapturingChange, highlightedFile, }) {
|
|
21
20
|
const homeDirectory = environment?.homeDirectory;
|
|
22
21
|
const appEnvironment = environment?.appEnvironment;
|
|
23
22
|
const apps = useMemo(() => createAppOperations({
|
|
@@ -27,12 +26,8 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
27
26
|
const mounted = useMounted();
|
|
28
27
|
const [rows, setRows] = useState([]);
|
|
29
28
|
const [error, setError] = useState();
|
|
29
|
+
const [notice, setNotice] = useState();
|
|
30
30
|
const [loadError, setLoadError] = useState();
|
|
31
|
-
const [focused, setFocused] = useState(false);
|
|
32
|
-
// Ref mirrors keep Tab and subsequent keys in a single burst on the same section.
|
|
33
|
-
const focusRef = useRef(false);
|
|
34
|
-
const [index, setIndex] = useState(0);
|
|
35
|
-
const indexRef = useRef(0);
|
|
36
31
|
const [mode, setModeState] = useState({ kind: "browse" });
|
|
37
32
|
const modeRef = useRef(mode);
|
|
38
33
|
const setMode = (next) => {
|
|
@@ -45,7 +40,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
45
40
|
if (mounted.current)
|
|
46
41
|
setMode({ kind: "browse" });
|
|
47
42
|
};
|
|
48
|
-
const preview = async (entry, status) => {
|
|
43
|
+
const preview = async (entry, status, removeAfterApproval = false) => {
|
|
49
44
|
setError(undefined);
|
|
50
45
|
setMode({ kind: "busy" });
|
|
51
46
|
try {
|
|
@@ -53,7 +48,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
53
48
|
if (mounted.current) {
|
|
54
49
|
setMode({
|
|
55
50
|
kind: "approve",
|
|
56
|
-
entry,
|
|
51
|
+
entry, removeAfterApproval,
|
|
57
52
|
lines: [
|
|
58
53
|
`File: ${entry.file}`,
|
|
59
54
|
`Platform: ${entry.recipe?.platform ?? "all"}`,
|
|
@@ -117,66 +112,142 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
117
112
|
}
|
|
118
113
|
}
|
|
119
114
|
};
|
|
120
|
-
|
|
115
|
+
const startRemoval = async (entry) => {
|
|
116
|
+
setError(undefined);
|
|
117
|
+
setMode({ kind: "busy" });
|
|
118
|
+
try {
|
|
119
|
+
const approval = await apps.approvalStatus(entry);
|
|
120
|
+
if (approval.status === "needs approval") {
|
|
121
|
+
await preview(entry, approval.status, true);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const removal = await planAppRecipeRemoval(apps, entry);
|
|
125
|
+
if (mounted.current)
|
|
126
|
+
setMode({ kind: removal.installed ? "uninstall-question" : "remove", removal });
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (mounted.current) {
|
|
130
|
+
setError(errorText(error));
|
|
131
|
+
setMode({ kind: "browse" });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const uninstall = async (current) => {
|
|
136
|
+
setMode({ kind: "busy" });
|
|
137
|
+
try {
|
|
138
|
+
const result = current.kind === "uninstall-manual"
|
|
139
|
+
? await apps.completeManual(current.plan, true)
|
|
140
|
+
: await apps.executeLifecycle(current.plan, true);
|
|
141
|
+
if (!mounted.current)
|
|
142
|
+
return;
|
|
143
|
+
if (result.status === "manual required")
|
|
144
|
+
setMode({ ...current, kind: "uninstall-manual" });
|
|
145
|
+
else if (result.status === "untracked")
|
|
146
|
+
setMode({ kind: "remove", removal: current.removal });
|
|
147
|
+
else {
|
|
148
|
+
setError(`Uninstall failed: ${result.reason ?? result.status}; recipe retained`);
|
|
149
|
+
setMode({ kind: "browse" });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (mounted.current) {
|
|
154
|
+
setError(`Uninstall failed: ${errorText(error)}; recipe retained`);
|
|
155
|
+
setMode({ kind: "browse" });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const startAdd = () => {
|
|
160
|
+
setError(undefined);
|
|
161
|
+
setMode({ kind: "guide" });
|
|
162
|
+
};
|
|
163
|
+
/** Captured actions take priority; browse actions use the unified cursor's recipe. */
|
|
121
164
|
const handleKey = (input, key) => {
|
|
122
165
|
const current = modeRef.current;
|
|
123
166
|
if (current.kind === "busy")
|
|
124
167
|
return true;
|
|
125
168
|
if (current.kind === "install") {
|
|
126
|
-
// Host/scope/version are selection prompts; version chooses latest or fixed
|
|
127
|
-
// by number. Only the separate fixed mode accepts free-text version input.
|
|
128
|
-
// n declines these selections; fixed input and in-flight installs are untouched.
|
|
129
169
|
const decline = input === "n" && ["host", "scope", "version"].includes(current.action.kind);
|
|
130
170
|
handleCatalogActionKey(flow, current.action, input, decline ? { ...key, escape: true } : key);
|
|
131
171
|
return true;
|
|
132
172
|
}
|
|
133
173
|
if (current.kind === "guide" || current.kind === "manual" || current.kind === "prompt") {
|
|
134
|
-
if (key.escape || input === "n")
|
|
174
|
+
if (key.escape || input === "n") {
|
|
135
175
|
setMode({ kind: "browse" });
|
|
176
|
+
void reload();
|
|
177
|
+
}
|
|
136
178
|
else if (current.kind === "guide" && input === "1")
|
|
137
179
|
void startAI();
|
|
138
180
|
else if (current.kind === "guide" && input === "2")
|
|
139
181
|
setMode({ kind: "manual" });
|
|
140
182
|
return true;
|
|
141
183
|
}
|
|
184
|
+
const cancel = () => setMode({ kind: "browse" });
|
|
142
185
|
if (current.kind === "approve") {
|
|
143
186
|
answerYesNo(input, key, () => {
|
|
144
187
|
setMode({ kind: "busy" });
|
|
145
|
-
void apps.approve(current.entry).then(
|
|
188
|
+
void apps.approve(current.entry).then(async () => {
|
|
189
|
+
await reload();
|
|
190
|
+
if (!mounted.current)
|
|
191
|
+
return;
|
|
192
|
+
if (current.removeAfterApproval)
|
|
193
|
+
await startRemoval(current.entry);
|
|
194
|
+
else
|
|
195
|
+
setMode({ kind: "browse" });
|
|
196
|
+
}, error => {
|
|
146
197
|
if (mounted.current) {
|
|
147
198
|
setError(errorText(error));
|
|
148
|
-
|
|
199
|
+
cancel();
|
|
149
200
|
}
|
|
150
201
|
});
|
|
151
|
-
},
|
|
202
|
+
}, cancel);
|
|
152
203
|
return true;
|
|
153
204
|
}
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
205
|
+
if (current.kind === "uninstall-question") {
|
|
206
|
+
answerYesNo(input, key, () => {
|
|
207
|
+
setMode({ kind: "busy" });
|
|
208
|
+
void apps.planLifecycle(current.removal.entry, "uninstall").then(plan => {
|
|
209
|
+
if (mounted.current)
|
|
210
|
+
setMode({ kind: !plan.argv && plan.manual ? "uninstall-manual" : "uninstall-confirm", removal: current.removal, plan });
|
|
211
|
+
}, error => { if (mounted.current) {
|
|
212
|
+
setError(errorText(error));
|
|
213
|
+
cancel();
|
|
214
|
+
} });
|
|
215
|
+
}, cancel);
|
|
158
216
|
return true;
|
|
159
217
|
}
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const step = key.pageDown || key.pageUp ? pageStep(recipeHeight) : 1;
|
|
164
|
-
const down = key.downArrow || input === "j" || key.pageDown;
|
|
165
|
-
indexRef.current = Math.min(Math.max(indexRef.current + (down ? step : -step), 0), Math.max(rows.length - 1, 0));
|
|
166
|
-
setIndex(indexRef.current);
|
|
218
|
+
if (current.kind === "uninstall-confirm" || current.kind === "uninstall-manual") {
|
|
219
|
+
answerYesNo(input, key, () => { void uninstall(current); }, cancel);
|
|
220
|
+
return true;
|
|
167
221
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
222
|
+
if (current.kind === "remove") {
|
|
223
|
+
answerYesNo(input, key, () => {
|
|
224
|
+
setMode({ kind: "busy" });
|
|
225
|
+
void removeAppRecipe(apps, current.removal, true).then(async () => {
|
|
226
|
+
if (mounted.current)
|
|
227
|
+
setNotice(`Removed App recipe: ${current.removal.file}`);
|
|
228
|
+
await finish();
|
|
229
|
+
}, error => {
|
|
230
|
+
if (mounted.current) {
|
|
231
|
+
setError(errorText(error));
|
|
232
|
+
cancel();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}, cancel);
|
|
236
|
+
return true;
|
|
171
237
|
}
|
|
172
|
-
|
|
238
|
+
const row = rows.find(row => row.entry.file === highlightedFile());
|
|
239
|
+
if (!row)
|
|
240
|
+
return false;
|
|
241
|
+
if (input === "n")
|
|
242
|
+
startAdd();
|
|
243
|
+
else if (input === "r")
|
|
173
244
|
void reload();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
245
|
+
else if (key.return)
|
|
246
|
+
void preview(row.entry, row.status);
|
|
247
|
+
else if (input === "d")
|
|
248
|
+
void startRemoval(row.entry);
|
|
249
|
+
else
|
|
250
|
+
return false;
|
|
180
251
|
return true;
|
|
181
252
|
};
|
|
182
253
|
/** Reloads parsing and approval receipts, never version/latest commands. */
|
|
@@ -201,16 +272,8 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
201
272
|
}
|
|
202
273
|
}, [apps, mounted]);
|
|
203
274
|
useEffect(() => { void reload(); }, [reload]);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const gitHeight = Math.max(1, budget - recipeHeight);
|
|
209
|
-
const at = Math.min(index, rows.length - 1);
|
|
210
|
-
const window = computeWindow(rows.length, at, recipeHeight);
|
|
211
|
-
const panel = (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { ...rowStyle(focused && rows.length === 0), children: ["App recipes \u00B7 ", apps.directory, " \u00B7 Tab switches section"] }), focused ? _jsx(Text, { color: theme.muted, children: "j/k move \u00B7 Enter preview \u00B7 n add recipe \u00B7 r reload" }) : null, loadError ? _jsxs(Text, { color: theme.error, children: ["Error loading App recipes: ", loadError] }) : null, error ? _jsx(Text, { color: theme.error, children: error }) : null, rows.slice(window.start, window.end).map(({ entry, status }, offset) => {
|
|
212
|
-
const selected = focused && window.start + offset === at;
|
|
213
|
-
return (_jsxs(Text, { ...rowStyle(selected), children: [selected ? "> " : " ", entry.file.split(/[\\/]/u).pop(), " \u00B7 ", status] }, entry.file));
|
|
214
|
-
}), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, mode.kind === "guide" ? (_jsxs(Text, { children: ["1 With the AI skill · install if missing, then copy the prompt\n", "2 Manual · write a recipe using the schema\nn or Esc closes"] })) : null, mode.kind === "manual" ? (_jsxs(Text, { children: ["Write a JSON recipe in ", apps.directory, "\n", "Schema: agent-depot app schema \u00B7 n or Esc closes"] })) : null, mode.kind === "prompt" ? (_jsxs(Text, { children: ["Use the agent-depot-apprecipe skill to create the recipe for <app>", "\n", "Recipes: ", apps.directory, " \u00B7 n or Esc closes"] })) : null, mode.kind === "install" ? _jsx(ActionPanel, { mode: mode.action }) : null, mode.kind === "busy" ? _jsx(Text, { children: "Working..." }) : null, mode.kind === "approve" ? (_jsx(PreviewConfirm, { lines: mode.lines, question: `Approve ${appName(mode.entry)}? y/n` })) : null] }));
|
|
215
|
-
return { focused, handleKey, gitHeight, panel };
|
|
275
|
+
const panel = (_jsxs(Box, { flexDirection: "column", children: [loadError ? _jsxs(Text, { color: theme.error, children: ["Error loading App recipes: ", loadError] }) : null, error ? _jsx(Text, { color: theme.error, children: error }) : null, notice ? _jsx(Text, { color: theme.success, children: notice }) : null, mode.kind === "guide" ? (_jsxs(Text, { children: ["1 With the AI skill · install if missing, then copy the prompt\n", "2 Manual · write a recipe using the schema\nn or Esc closes"] })) : null, mode.kind === "manual" ? (_jsxs(Text, { children: ["Write a JSON recipe in ", apps.directory, "\n", "Schema: agent-depot app schema \u00B7 n or Esc closes"] })) : null, mode.kind === "prompt" ? (_jsxs(Text, { children: ["Use the agent-depot-apprecipe skill to create the recipe for <app>", "\n", "Recipes: ", apps.directory, " \u00B7 n or Esc closes"] })) : null, mode.kind === "install" ? _jsx(ActionPanel, { mode: mode.action }) : null, mode.kind === "busy" ? _jsx(Text, { children: "Working..." }) : null, mode.kind === "approve" ? (_jsx(PreviewConfirm, { lines: mode.lines, question: `Approve ${appName(mode.entry)}? y/n` })) : null, mode.kind === "uninstall-question" ? (_jsxs(Text, { children: ["App ", appName(mode.removal.entry), " is installed or tracked", mode.removal.installedVersion ? ` (${mode.removal.installedVersion})` : "", ". Uninstall it before removing the recipe? y/n (n cancels removal)"] })) : null, mode.kind === "uninstall-confirm" || mode.kind === "uninstall-manual" ? (_jsx(PreviewConfirm, { lines: appStepLines(mode.plan), question: mode.kind === "uninstall-manual"
|
|
276
|
+
? "Complete the manual uninstall, then check now? y/n"
|
|
277
|
+
: `Uninstall ${appName(mode.removal.entry)}? y/n` })) : null, mode.kind === "remove" ? (_jsx(PreviewConfirm, { lines: [`Delete recipe: ${mode.removal.file}`, "Only this recipe file or symlink will be removed; App data and Host configuration are not deleted."], question: `Remove recipe for ${appName(mode.removal.entry)}? y/n` })) : null] }));
|
|
278
|
+
return { rows, handleKey, startAdd, reload, clearMessage: () => { setError(undefined); setNotice(undefined); }, panel, directory: apps.directory };
|
|
216
279
|
}
|