@iseoane/agent-depot 0.2.0 → 0.3.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +117 -3
  3. package/dist/src/app-cli.js +172 -0
  4. package/dist/src/app-flow.js +457 -0
  5. package/dist/src/app-latest.js +63 -0
  6. package/dist/src/app-owned-skills.js +42 -0
  7. package/dist/src/app-recipes.js +116 -0
  8. package/dist/src/app-schema.js +45 -0
  9. package/dist/src/app-version.js +46 -0
  10. package/dist/src/atomic-file.js +26 -0
  11. package/dist/src/cli.js +1256 -0
  12. package/dist/src/git-source.js +409 -0
  13. package/dist/src/path-safety.js +31 -0
  14. package/dist/src/process-runner.js +41 -0
  15. package/dist/src/project-installation.js +1411 -0
  16. package/dist/src/project-manifest.js +511 -0
  17. package/dist/src/skill-adoption.js +77 -0
  18. package/dist/src/skill-discovery.js +467 -0
  19. package/dist/src/skill-hosts.js +103 -0
  20. package/dist/src/skill-install.js +374 -0
  21. package/dist/src/skill-removal.js +119 -0
  22. package/dist/src/skill-update.js +244 -0
  23. package/dist/src/source-state.js +240 -0
  24. package/dist/src/sources.js +391 -0
  25. package/dist/src/tui/adoption-actions.js +144 -0
  26. package/dist/src/tui/app-actions.js +157 -0
  27. package/dist/src/tui/app.js +63 -0
  28. package/dist/src/tui/batch.js +17 -0
  29. package/dist/src/tui/bulk-actions.js +243 -0
  30. package/dist/src/tui/catalog-actions.js +9 -0
  31. package/dist/src/tui/catalog-browse.js +59 -0
  32. package/dist/src/tui/catalog-filter.js +20 -0
  33. package/dist/src/tui/catalog-flow.js +107 -0
  34. package/dist/src/tui/catalog-installs.js +193 -0
  35. package/dist/src/tui/catalog-panel.js +47 -0
  36. package/dist/src/tui/catalog-state.js +92 -0
  37. package/dist/src/tui/catalog-tree.js +35 -0
  38. package/dist/src/tui/catalog-view.js +89 -0
  39. package/dist/src/tui/environment.js +1 -0
  40. package/dist/src/tui/installations-browse.js +136 -0
  41. package/dist/src/tui/installations-mode.js +1 -0
  42. package/dist/src/tui/installations-panel.js +27 -0
  43. package/dist/src/tui/installations-rows.js +11 -0
  44. package/dist/src/tui/installations-tree.js +80 -0
  45. package/dist/src/tui/installations-view.js +154 -0
  46. package/dist/src/tui/installations.js +172 -0
  47. package/dist/src/tui/keys.js +31 -0
  48. package/dist/src/tui/manage-actions.js +174 -0
  49. package/dist/src/tui/manage-panel.js +65 -0
  50. package/dist/src/tui/mode-keys.js +61 -0
  51. package/dist/src/tui/panel-parts.js +23 -0
  52. package/dist/src/tui/render.js +8 -0
  53. package/dist/src/tui/sources-mode.js +17 -0
  54. package/dist/src/tui/sources-view.js +276 -0
  55. package/dist/src/tui/theme.js +29 -0
  56. package/dist/src/tui/tree-navigation.js +68 -0
  57. package/dist/src/tui/tree.js +53 -0
  58. package/dist/src/tui/unmanaged-actions.js +178 -0
  59. package/dist/src/tui/updates-flow.js +89 -0
  60. package/dist/src/tui/updates-keys.js +104 -0
  61. package/dist/src/tui/updates-mode.js +1 -0
  62. package/dist/src/tui/updates-model.js +48 -0
  63. package/dist/src/tui/updates-panel.js +44 -0
  64. package/dist/src/tui/updates-view.js +101 -0
  65. package/dist/src/tui/updates.js +230 -0
  66. package/dist/src/tui/view-state.js +42 -0
  67. package/dist/src/tui/window.js +31 -0
  68. package/dist/src/unmanaged-removal.js +60 -0
  69. package/dist/src/update-batch.js +376 -0
  70. package/dist/src/update-flow.js +135 -0
  71. package/dist/src/usage-error.js +7 -0
  72. package/dist/src/user-global-skill-inventory.js +565 -0
  73. package/dist/src/user-method.js +14 -0
  74. package/package.json +7 -4
  75. package/skills/agent-depot-apprecipe/SKILL.md +44 -0
  76. package/skills/agent-depot-apprecipe/assets/example.md +25 -0
  77. package/skills/doctor-md-agents/scripts/test_collect_sessions.py +0 -780
  78. package/skills/doctor-md-agents/scripts/test_extract_rules.py +0 -530
  79. package/skills/doctor-md-agents/scripts/test_render_report.py +0 -383
  80. package/skills/doctor-md-agents/scripts/test_scan_corrections.py +0 -343
  81. package/skills/doctor-md-agents/scripts/test_verify_citations.py +0 -236
  82. package/skills/doctor-md-agents/scripts/testdata/old_extract_rules_pre_t1.py.txt +0 -507
  83. package/skills/doctor-md-skill/scripts/test_collect_sessions.py +0 -593
  84. package/skills/doctor-md-skill/scripts/test_detect_machine_sessions.py +0 -181
  85. package/skills/doctor-md-skill/scripts/test_render_report.py +0 -241
  86. package/skills/doctor-md-skill/scripts/test_verify_citations.py +0 -196
@@ -0,0 +1,174 @@
1
+ import { APP_KINDS } from "./app-actions.js";
2
+ import { globalRecord, missingHosts, prepareHostAddition, prepareHostRemoval, prepareUninstall, runHostAddition, runHostRemoval, runUninstall, UNINSTALL_REFUSALS, } from "./catalog-installs.js";
3
+ import { errorText } from "./batch.js";
4
+ import { BULK_KINDS, handleBulkKey } from "./bulk-actions.js";
5
+ import { answerYesNo, applyChecklistOutcome, hostChecklistKey } from "./mode-keys.js";
6
+ import { handleUnmanagedKey, UNMANAGED_KINDS } from "./unmanaged-actions.js";
7
+ const MANAGE_KINDS = new Set([
8
+ "remove-scope",
9
+ "hosts-add",
10
+ "hosts-remove",
11
+ "confirm-host-add",
12
+ "confirm-host-exposure",
13
+ "confirm-host-remove",
14
+ "confirm-uninstall",
15
+ ...APP_KINDS,
16
+ ...UNMANAGED_KINDS,
17
+ ...BULK_KINDS,
18
+ "busy",
19
+ ]);
20
+ function isManageMode(mode) {
21
+ return MANAGE_KINDS.has(mode.kind);
22
+ }
23
+ export { errorText };
24
+ function cancel(context, text) {
25
+ context.setMessage({ kind: "ok", text });
26
+ context.browse();
27
+ }
28
+ /** `u`: one host goes straight to full removal; several ask whether to remove all or choose. */
29
+ export function startUninstall(context, skill) {
30
+ const record = globalRecord(skill, context.installed);
31
+ if (record && record.hosts.length > 1)
32
+ context.setMode({ kind: "remove-scope", skill, hosts: record.hosts });
33
+ else
34
+ void startFullUninstall(context, skill);
35
+ }
36
+ async function startFullUninstall(context, skill) {
37
+ context.setMode({ kind: "busy", label: `Preparing uninstall of ${skill.name}...` });
38
+ try {
39
+ const prepared = await prepareUninstall(context.operations, context.env, skill, context.installed);
40
+ if (!context.isMounted())
41
+ return;
42
+ if (prepared.kind === "ready") {
43
+ context.setMode({ kind: "confirm-uninstall", skill, prepared });
44
+ return;
45
+ }
46
+ context.setMessage({ kind: "error", text: UNINSTALL_REFUSALS[prepared.kind] });
47
+ }
48
+ catch (error) {
49
+ if (!context.isMounted())
50
+ return;
51
+ context.setMessage({ kind: "error", text: errorText(error) });
52
+ }
53
+ context.browse();
54
+ }
55
+ /** Opens the host checklist for the hosts the installed Skill does not serve yet. */
56
+ export function startHostAddition(context, skill) {
57
+ const record = globalRecord(skill, context.installed);
58
+ const missing = record ? missingHosts(record) : [];
59
+ if (missing.length === 0) {
60
+ context.setMessage({ kind: "error", text: record ? `${skill.name} already has every host` : "Skill is not installed" });
61
+ return;
62
+ }
63
+ context.setMode({ kind: "hosts-add", skill, choices: missing, cursor: 0, selected: [] });
64
+ }
65
+ async function startHostChange(context, mode) {
66
+ const { operations, env, installed } = context;
67
+ context.setMode({ kind: "busy", label: `Preparing host change of ${mode.skill.name}...` });
68
+ try {
69
+ if (mode.kind === "hosts-add") {
70
+ const prepared = await prepareHostAddition(operations, env, mode.skill, installed, mode.selected);
71
+ if (!context.isMounted())
72
+ return;
73
+ context.setMode({ kind: "confirm-host-add", skill: mode.skill, prepared });
74
+ }
75
+ else {
76
+ const prepared = await prepareHostRemoval(operations, env, mode.skill, installed, mode.selected);
77
+ if (!context.isMounted())
78
+ return;
79
+ context.setMode({ kind: "confirm-host-remove", skill: mode.skill, prepared });
80
+ }
81
+ }
82
+ catch (error) {
83
+ if (!context.isMounted())
84
+ return;
85
+ context.setMessage({ kind: "error", text: errorText(error) });
86
+ context.browse();
87
+ }
88
+ }
89
+ /** Runs a confirmed step in the busy mode and finishes with its outcome. */
90
+ async function execute(context, label, action) {
91
+ context.setMode({ kind: "busy", label });
92
+ let result;
93
+ try {
94
+ result = { kind: "ok", text: await action() };
95
+ }
96
+ catch (error) {
97
+ result = { kind: "error", text: errorText(error) };
98
+ }
99
+ await context.finish(result);
100
+ }
101
+ function handleHostChecklistKey(context, mode, input, key) {
102
+ applyChecklistOutcome(hostChecklistKey(mode, mode.choices, input, key), {
103
+ cancel: () => cancel(context, mode.kind === "hosts-add" ? "Install cancelled" : "Uninstall cancelled"),
104
+ update: (state) => context.setMode({ ...mode, ...state }),
105
+ submit: () => void startHostChange(context, mode),
106
+ setMessage: context.setMessage,
107
+ });
108
+ }
109
+ function handleRemoveScopeKey(context, mode, input, key) {
110
+ if (key.escape)
111
+ cancel(context, "Uninstall cancelled");
112
+ else if (input === "1")
113
+ void startFullUninstall(context, mode.skill);
114
+ else if (input === "2")
115
+ context.setMode({ kind: "hosts-remove", skill: mode.skill, choices: mode.hosts, cursor: 0, selected: [] });
116
+ }
117
+ /** Keys of the confirmation modes: `y` runs the step (or asks for exposure consent first), `n`/Esc cancels. */
118
+ function handleConfirmKey(context, mode, input, key) {
119
+ const { operations, env } = context;
120
+ const cancelWith = (text) => () => cancel(context, text);
121
+ switch (mode.kind) {
122
+ case "confirm-host-add":
123
+ answerYesNo(input, key, () => context.setMode({ ...mode, kind: "confirm-host-exposure" }), cancelWith("Install cancelled"));
124
+ return;
125
+ case "confirm-host-exposure":
126
+ answerYesNo(input, key, () => {
127
+ void execute(context, `Changing hosts of ${mode.skill.name}...`, () => runHostAddition(mode.prepared, operations, env));
128
+ }, cancelWith("Install cancelled"));
129
+ return;
130
+ case "confirm-host-remove":
131
+ answerYesNo(input, key, () => {
132
+ void execute(context, `Changing hosts of ${mode.skill.name}...`, () => runHostRemoval(mode.prepared, operations, env, mode.skill));
133
+ }, cancelWith("Uninstall cancelled"));
134
+ return;
135
+ case "confirm-uninstall":
136
+ answerYesNo(input, key, () => {
137
+ void execute(context, `Removing ${mode.skill.name}...`, async () => {
138
+ await runUninstall(mode.prepared, operations, env);
139
+ return `Removed ${mode.skill.name} (${mode.skill.path})`;
140
+ });
141
+ }, cancelWith("Uninstall cancelled"));
142
+ return;
143
+ }
144
+ }
145
+ /**
146
+ * Handles a key in one of the manage modes. Returns false when the mode is not
147
+ * a manage mode, so the host view can handle its own modes.
148
+ */
149
+ export function handleManageKey(context, mode, input, key) {
150
+ if (!isManageMode(mode))
151
+ return false;
152
+ if (handleUnmanagedKey(context, mode, input, key))
153
+ return true;
154
+ if (handleBulkKey(context, mode, input, key))
155
+ return true;
156
+ switch (mode.kind) {
157
+ case "remove-scope":
158
+ handleRemoveScopeKey(context, mode, input, key);
159
+ break;
160
+ case "hosts-add":
161
+ case "hosts-remove":
162
+ handleHostChecklistKey(context, mode, input, key);
163
+ break;
164
+ case "confirm-host-add":
165
+ case "confirm-host-exposure":
166
+ case "confirm-host-remove":
167
+ case "confirm-uninstall":
168
+ handleConfirmKey(context, mode, input, key);
169
+ break;
170
+ default:
171
+ break;
172
+ }
173
+ return true;
174
+ }
@@ -0,0 +1,65 @@
1
+ import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { AppPanel } from "./app-actions.js";
4
+ import { ChecklistLines, HostChecklist, PreviewConfirm, PreviewLines } from "./panel-parts.js";
5
+ import { theme } from "./theme.js";
6
+ const plural = (n) => `${n} ${n === 1 ? "item" : "items"}`;
7
+ function SkippedLines({ skipped }) {
8
+ return (_jsx(_Fragment, { children: skipped.map(({ id, label, reason }) => _jsxs(Text, { color: theme.warning, children: ["Skipped ", label, ": ", reason] }, id)) }));
9
+ }
10
+ /** One block per planned item, the skipped ones, then the y/n question. */
11
+ function PlanPreview({ entries, skipped, question }) {
12
+ return (_jsxs(Box, { flexDirection: "column", children: [entries.map((entry) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.group, children: entry.label }), _jsx(PreviewLines, { lines: entry.lines, warnFirst: entry.warnFirst })] }, entry.id))), _jsx(SkippedLines, { skipped: skipped }), _jsx(Text, { children: question })] }));
13
+ }
14
+ function UninstallPreview({ plan }) {
15
+ const entries = plan.items.map((item) => ({
16
+ id: item.id,
17
+ label: item.label,
18
+ lines: item.kind === "managed" ? item.prepared.plan.preview : item.prepared.preview,
19
+ warnFirst: item.kind === "unmanaged",
20
+ }));
21
+ return _jsx(PlanPreview, { entries: entries, skipped: plan.skipped, question: `Uninstall ${plural(plan.items.length)}? y/n` });
22
+ }
23
+ function HostsPreview({ plan }) {
24
+ const entries = plan.items.map((item) => ({ id: item.id, label: item.label, lines: item.prepared.preview, warnFirst: false }));
25
+ return _jsx(PlanPreview, { entries: entries, skipped: plan.skipped, question: `Add hosts to ${plural(plan.items.length)}? y/n` });
26
+ }
27
+ /** Renders the prompt of a manage mode: host lists, previews and confirmations. */
28
+ export function ManagePanel({ mode }) {
29
+ switch (mode.kind) {
30
+ case "app-approve":
31
+ case "app-hosts":
32
+ case "app-confirm":
33
+ case "app-manual":
34
+ return _jsx(AppPanel, { mode: mode });
35
+ case "remove-scope":
36
+ return _jsxs(Text, { children: ["Uninstall ", mode.skill.name, " (hosts: ", mode.hosts.join(", "), "): 1 all hosts 2 choose hosts (Esc cancel)"] });
37
+ case "hosts-add":
38
+ case "hosts-remove":
39
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [mode.kind === "hosts-add" ? "Add hosts to" : "Remove hosts from", " ", mode.skill.name, ". Host (space/1-", mode.choices.length, " toggle, j/k move, Enter continue, Esc cancel):"] }), _jsx(HostChecklist, { choices: mode.choices, cursor: mode.cursor, selected: mode.selected })] }));
40
+ case "confirm-host-add":
41
+ return _jsx(PreviewConfirm, { lines: mode.prepared.preview, question: `Add hosts to ${mode.skill.name}? y/n` });
42
+ case "confirm-host-exposure":
43
+ return (_jsxs(Text, { color: theme.warning, children: ["Adding hosts exposes the installed ", mode.skill.name, " to more hosts and needs separate confirmation (CLI: --confirm-additional-host). Confirm? y/n"] }));
44
+ case "confirm-host-remove":
45
+ return _jsx(PreviewConfirm, { lines: mode.prepared.preview, question: `Remove hosts from ${mode.skill.name}? y/n` });
46
+ case "confirm-uninstall":
47
+ return _jsx(PreviewConfirm, { lines: mode.prepared.plan.preview, question: `Uninstall ${mode.skill.name}? y/n` });
48
+ case "unmanaged-scope":
49
+ return _jsxs(Text, { children: ["Remove ", mode.group.name, " (", mode.group.locations.length, " locations): 1 all locations 2 choose locations (Esc cancel)"] });
50
+ case "unmanaged-pick":
51
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Remove locations of ", mode.group.name, ". Location (space/1-", mode.group.locations.length, " toggle, j/k move, Enter continue, Esc cancel):"] }), _jsx(ChecklistLines, { choices: mode.group.locations, cursor: mode.cursor, isSelected: (location) => mode.selected.includes(location.path), keyOf: (location) => location.path, label: (location) => `${location.path}${location.linkTarget === undefined ? "" : ` symlink → ${location.linkTarget}`}` })] }));
52
+ case "confirm-unmanaged":
53
+ return _jsx(PreviewConfirm, { lines: mode.prepared.preview, question: `Remove ${mode.group.name}? y/n`, warnFirst: true });
54
+ case "bulk-hosts":
55
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Add hosts to ", plural(mode.targets.length), ". Host (space/1-", mode.choices.length, " toggle, j/k move, Enter continue, Esc cancel):"] }), _jsx(HostChecklist, { choices: mode.choices, cursor: mode.cursor, selected: mode.selected }), _jsx(SkippedLines, { skipped: mode.skipped })] }));
56
+ case "confirm-bulk-uninstall":
57
+ return _jsx(UninstallPreview, { plan: mode.plan });
58
+ case "confirm-bulk-hosts":
59
+ return _jsx(HostsPreview, { plan: mode.plan });
60
+ case "confirm-bulk-exposure":
61
+ return (_jsxs(Text, { color: theme.warning, children: ["Adding hosts exposes the installed ", mode.plan.items.map((item) => item.label).join(", "), " to more hosts and needs separate confirmation (CLI: --confirm-additional-host). Confirm? y/n"] }));
62
+ case "busy":
63
+ return _jsx(Text, { children: mode.label });
64
+ }
65
+ }
@@ -0,0 +1,61 @@
1
+ import { isValidFixedVersion } from "../skill-install.js";
2
+ /** The cursor after a down/up key (arrows or j/k), or undefined when the key does not move it. */
3
+ export function moveCursor(cursor, length, input, key) {
4
+ if (key.downArrow || input === "j")
5
+ return Math.min(cursor + 1, length - 1);
6
+ if (key.upArrow || input === "k")
7
+ return Math.max(cursor - 1, 0);
8
+ return undefined;
9
+ }
10
+ /** `selected` with `choice` flipped, always in the order of `choices`. */
11
+ export function toggleChoice(choices, selected, choice) {
12
+ return choices.filter((candidate) => candidate === choice ? !selected.includes(candidate) : selected.includes(candidate));
13
+ }
14
+ /**
15
+ * Keys of a host checklist: Esc cancels, j/k or arrows move, space toggles the
16
+ * cursor row, a digit toggles that row, Enter submits a non-empty selection.
17
+ */
18
+ export function hostChecklistKey(state, choices, input, key) {
19
+ if (key.escape)
20
+ return { kind: "cancel" };
21
+ const cursor = moveCursor(state.cursor, choices.length, input, key);
22
+ if (cursor !== undefined)
23
+ return { kind: "update", state: { ...state, cursor } };
24
+ const flipped = input === " " ? choices[state.cursor] : choices[Number(input) - 1];
25
+ if (input === " " || flipped !== undefined) {
26
+ return { kind: "update", state: { ...state, selected: toggleChoice(choices, state.selected, flipped) } };
27
+ }
28
+ if (key.return)
29
+ return state.selected.length > 0 ? { kind: "submit", selected: state.selected } : { kind: "empty" };
30
+ return { kind: "ignore" };
31
+ }
32
+ /** Runs the handler for what a key did to a host checklist; an empty Enter shows the shared message. */
33
+ export function applyChecklistOutcome(outcome, handlers) {
34
+ if (outcome.kind === "cancel")
35
+ handlers.cancel();
36
+ else if (outcome.kind === "update")
37
+ handlers.update(outcome.state);
38
+ else if (outcome.kind === "submit")
39
+ handlers.submit(outcome.selected);
40
+ else if (outcome.kind === "empty")
41
+ handlers.setMessage({ kind: "error", text: "Select at least one host" });
42
+ }
43
+ /** The y/n confirmation keys: `y` confirms, `n` or Esc declines, anything else is ignored. */
44
+ export function answerYesNo(input, key, onYes, onNo) {
45
+ if (input === "y")
46
+ onYes();
47
+ else if (input === "n" || key.escape)
48
+ onNo();
49
+ }
50
+ /** Esc cancels, Enter submits a valid version, backspace deletes, any other plain key is typed. */
51
+ export function versionInputKey(value, input, key) {
52
+ if (key.escape)
53
+ return { kind: "cancel" };
54
+ if (key.return)
55
+ return isValidFixedVersion(value) ? { kind: "submit" } : { kind: "ignore" };
56
+ if (key.backspace || key.delete)
57
+ return { kind: "edit", value: value.slice(0, -1) };
58
+ if (input !== "" && !key.ctrl && !key.meta)
59
+ return { kind: "edit", value: value + input };
60
+ return { kind: "ignore" };
61
+ }
@@ -0,0 +1,23 @@
1
+ import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { theme } from "./theme.js";
4
+ /** The `> [x] 1 name` rows of a checklist; the caller renders the header. */
5
+ export function ChecklistLines({ choices, cursor, isSelected, keyOf, label }) {
6
+ return (_jsx(_Fragment, { children: choices.map((choice, position) => (_jsxs(Text, { children: [position === cursor ? "> " : " ", "[", isSelected(choice) ? "x" : " ", "] ", position + 1, " ", label(choice)] }, keyOf(choice)))) }));
7
+ }
8
+ /** The host rows of a checklist. */
9
+ export function HostChecklist({ choices, cursor, selected }) {
10
+ return _jsx(ChecklistLines, { choices: choices, cursor: cursor, isSelected: (host) => selected.includes(host), keyOf: (host) => host, label: (host) => host });
11
+ }
12
+ /** Preview lines; `warnFirst` colors the first one as a warning. */
13
+ export function PreviewLines({ lines, warnFirst = false }) {
14
+ return (_jsx(_Fragment, { children: lines.map((line, position) => _jsx(Text, { color: warnFirst && position === 0 ? theme.warning : undefined, children: line }, position)) }));
15
+ }
16
+ /** Under a list: the window indicator, how many items are marked, and the last result. */
17
+ export function ListFooter({ indicator, selectedCount, message }) {
18
+ return (_jsxs(_Fragment, { children: [indicator ? _jsx(Text, { color: theme.muted, children: indicator }) : null, selectedCount > 0 ? _jsxs(Text, { color: theme.muted, children: [selectedCount, " selected"] }) : null, message ? _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) : null] }));
19
+ }
20
+ /** A preview followed by a y/n question. */
21
+ export function PreviewConfirm({ lines, question, warnFirst }) {
22
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PreviewLines, { lines: lines, warnFirst: warnFirst }), _jsx(Text, { children: question })] }));
23
+ }
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from "ink";
3
+ import { App } from "./app.js";
4
+ /** Renders the TUI and resolves once the user exits it. */
5
+ export async function renderTui(operations, environment) {
6
+ const instance = render(_jsx(App, { operations: operations, environment: environment }));
7
+ await instance.waitUntilExit();
8
+ }
@@ -0,0 +1,17 @@
1
+ export const initialMode = { kind: "browse" };
2
+ export function modeReducer(mode, event) {
3
+ switch (event.type) {
4
+ case "input":
5
+ return { kind: "input", value: "" };
6
+ case "edit":
7
+ return mode.kind === "input" ? { kind: "input", value: event.value } : mode;
8
+ case "confirm":
9
+ return event.action === "refresh"
10
+ ? { kind: "confirm", action: "refresh", sources: event.sources }
11
+ : { kind: "confirm", action: "remove", source: event.source };
12
+ case "busy":
13
+ return { kind: "busy" };
14
+ case "done":
15
+ return initialMode;
16
+ }
17
+ }
@@ -0,0 +1,276 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { useCallback, useEffect, useReducer, useRef, useState } from "react";
4
+ import { initialMode, modeReducer } from "./sources-mode.js";
5
+ import { rowStyle, theme } from "./theme.js";
6
+ import { computeWindow, pageStep, useListHeight } from "./window.js";
7
+ import { useKeys } from "./keys.js";
8
+ /** The highlighted index: by stable id while that source exists, otherwise the bounded fallback index. */
9
+ function resolveIndex(sources, selection) {
10
+ const found = sources.findIndex((source) => source.id === selection.id);
11
+ return found >= 0 ? found : Math.min(selection.index, Math.max(sources.length - 1, 0));
12
+ }
13
+ /** Refreshes each source in order; one failure is reported and does not stop the others. */
14
+ async function refreshEach(operations, targets) {
15
+ const lines = [];
16
+ let failed = false;
17
+ for (const source of targets) {
18
+ try {
19
+ lines.push(`Refreshed Git Source: ${(await operations.refreshSource(source.id)).id}`);
20
+ }
21
+ catch (error) {
22
+ failed = true;
23
+ lines.push(`Failed ${source.id}: ${errorText(error)}`);
24
+ }
25
+ }
26
+ return { kind: failed ? "error" : "ok", text: lines.join("\n") };
27
+ }
28
+ function errorText(error) {
29
+ return error instanceof Error ? error.message : String(error);
30
+ }
31
+ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight }) {
32
+ const height = useListHeight(listHeight, 7);
33
+ const [state, setState] = useState({ status: "loading" });
34
+ // The highlight follows the source id; the index is only the fallback once that source is gone.
35
+ // Selection and marks are mirrored in refs updated first, so keys delivered in one burst act on the latest state.
36
+ const [selection, setSelectionState] = useState({ index: 0 });
37
+ const selectionRef = useRef({ index: 0 });
38
+ const setSelection = (next) => {
39
+ selectionRef.current = next;
40
+ setSelectionState(next);
41
+ };
42
+ const [mode, dispatchMode] = useReducer(modeReducer, initialMode);
43
+ // Mirrors the mode synchronously so the shell and later keystrokes never see a stale capturing state.
44
+ const modeRef = useRef(initialMode);
45
+ const mounted = useRef(true);
46
+ const [message, setMessage] = useState();
47
+ // Git sources marked for a combined refresh; the built-in source can never be marked.
48
+ const [marked, setMarkedState] = useState(new Set());
49
+ const markedRef = useRef(new Set());
50
+ const setMarked = (next) => {
51
+ markedRef.current = next;
52
+ setMarkedState(next);
53
+ };
54
+ // Mirrors the typed URL synchronously so keystrokes delivered in one burst are not lost to stale closures.
55
+ const typed = useRef("");
56
+ const reload = useCallback(async () => {
57
+ try {
58
+ const sources = await operations.listSources();
59
+ if (mounted.current)
60
+ setState({ status: "ready", sources });
61
+ }
62
+ catch (error) {
63
+ if (mounted.current)
64
+ setState({ status: "error", message: errorText(error) });
65
+ }
66
+ }, [operations]);
67
+ useEffect(() => {
68
+ mounted.current = true;
69
+ return () => {
70
+ mounted.current = false;
71
+ };
72
+ }, []);
73
+ const dispatch = (event) => {
74
+ modeRef.current = modeReducer(modeRef.current, event);
75
+ onCapturingChange?.(modeRef.current.kind !== "browse");
76
+ dispatchMode(event);
77
+ };
78
+ useEffect(() => {
79
+ let cancelled = false;
80
+ operations.listSources().then((sources) => {
81
+ if (!cancelled)
82
+ setState({ status: "ready", sources });
83
+ }, (error) => {
84
+ if (!cancelled)
85
+ setState({ status: "error", message: errorText(error) });
86
+ });
87
+ return () => {
88
+ cancelled = true;
89
+ };
90
+ }, [operations]);
91
+ const allSources = state.status === "ready" ? state.sources : [];
92
+ const builtin = allSources.find((source) => source.kind === "builtin");
93
+ // The built-in source is a fixed row; only Git sources are selectable and navigable.
94
+ const sources = allSources.filter((source) => source.kind === "git");
95
+ const sourcesRef = useRef(sources);
96
+ sourcesRef.current = sources;
97
+ const selected = resolveIndex(sources, selection);
98
+ const select = (index) => {
99
+ const listed = sourcesRef.current;
100
+ const bounded = Math.min(Math.max(index, 0), Math.max(listed.length - 1, 0));
101
+ setSelection({ id: listed[bounded]?.id, index: bounded });
102
+ };
103
+ /** Runs a mutating action in the busy mode, reports its outcome and reloads the list. */
104
+ const run = async (action) => {
105
+ dispatch({ type: "busy" });
106
+ setMessage(undefined);
107
+ let outcome;
108
+ try {
109
+ const result = await action();
110
+ outcome = typeof result === "string" ? { kind: "ok", text: result } : result;
111
+ }
112
+ catch (error) {
113
+ outcome = { kind: "error", text: `Error: ${errorText(error)}` };
114
+ }
115
+ if (!mounted.current)
116
+ return;
117
+ setMessage(outcome);
118
+ await reload();
119
+ if (mounted.current)
120
+ dispatch({ type: "done" });
121
+ };
122
+ /** Marks the given ids, or unmarks them all when every one is already marked. */
123
+ const toggle = (ids) => {
124
+ const next = new Set(markedRef.current);
125
+ if (ids.every((id) => next.has(id)))
126
+ ids.forEach((id) => next.delete(id));
127
+ else
128
+ ids.forEach((id) => next.add(id));
129
+ setMarked(next);
130
+ };
131
+ const stop = (text) => setMessage({ kind: "error", text });
132
+ const startRemoval = async (source) => {
133
+ if (!operations.listUserGlobalInstallations || !operations.removeGitSource) {
134
+ stop("Removing sources is not supported by the configured operations");
135
+ return;
136
+ }
137
+ dispatch({ type: "busy" });
138
+ setMessage(undefined);
139
+ try {
140
+ const installations = await operations.listUserGlobalInstallations();
141
+ if (!mounted.current)
142
+ return;
143
+ const dependent = installations.some((selection) => selection.source.kind === "external" && "url" in selection.source && selection.source.url === source.url);
144
+ if (dependent) {
145
+ stop(`Dependent user-global Skills exist; run \`agent-depot source remove ${source.id}\` in the CLI`);
146
+ dispatch({ type: "done" });
147
+ }
148
+ else {
149
+ dispatch({ type: "confirm", action: "remove", source });
150
+ }
151
+ }
152
+ catch (error) {
153
+ if (!mounted.current)
154
+ return;
155
+ stop(`Error: ${errorText(error)}`);
156
+ dispatch({ type: "done" });
157
+ }
158
+ };
159
+ useKeys((input, key) => {
160
+ const mode = modeRef.current;
161
+ if (mode.kind === "busy")
162
+ return;
163
+ if (mode.kind === "input") {
164
+ if (key.escape)
165
+ dispatch({ type: "done" });
166
+ else if (key.return) {
167
+ const url = typed.current.trim();
168
+ if (url === "")
169
+ return;
170
+ void run(async () => `Added Git Source: ${(await operations.addGitSource(url)).id}`);
171
+ }
172
+ else if (key.backspace || key.delete) {
173
+ typed.current = typed.current.slice(0, -1);
174
+ dispatch({ type: "edit", value: typed.current });
175
+ }
176
+ else if (input !== "" && !key.ctrl && !key.meta) {
177
+ typed.current += input;
178
+ dispatch({ type: "edit", value: typed.current });
179
+ }
180
+ return;
181
+ }
182
+ if (mode.kind === "confirm") {
183
+ if (input === "y") {
184
+ if (mode.action === "refresh") {
185
+ const { sources: targets } = mode;
186
+ void run(async () => {
187
+ const result = await refreshEach(operations, targets);
188
+ setMarked(new Set());
189
+ return result;
190
+ });
191
+ }
192
+ else {
193
+ const { source } = mode;
194
+ void run(async () => {
195
+ await operations.removeGitSource?.(source.id);
196
+ return `Removed Git Source: ${source.id}`;
197
+ });
198
+ }
199
+ }
200
+ else if (input === "n" || key.escape)
201
+ dispatch({ type: "done" });
202
+ return;
203
+ }
204
+ const listed = sourcesRef.current;
205
+ const at = resolveIndex(listed, selectionRef.current);
206
+ const highlighted = listed[at];
207
+ if (key.downArrow || input === "j") {
208
+ select(at + 1);
209
+ }
210
+ else if (key.upArrow || input === "k") {
211
+ select(at - 1);
212
+ }
213
+ else if (key.pageDown) {
214
+ select(at + pageStep(height));
215
+ }
216
+ else if (key.pageUp) {
217
+ select(at - pageStep(height));
218
+ }
219
+ else if (key.return && highlighted) {
220
+ onOpenCatalog?.(highlighted);
221
+ }
222
+ else if (input === "n") {
223
+ setMessage(undefined);
224
+ typed.current = "";
225
+ dispatch({ type: "input" });
226
+ }
227
+ else if (input === " " && highlighted) {
228
+ setMessage(undefined);
229
+ toggle([highlighted.id]);
230
+ }
231
+ else if (input === "a") {
232
+ if (listed.length === 0)
233
+ stop("There are no Git sources to select");
234
+ else {
235
+ setMessage(undefined);
236
+ toggle(listed.map((source) => source.id));
237
+ }
238
+ }
239
+ else if (input === "r") {
240
+ const targets = markedRef.current.size > 0
241
+ ? listed.filter((source) => markedRef.current.has(source.id))
242
+ : highlighted ? [highlighted] : [];
243
+ if (targets.length > 0) {
244
+ setMessage(undefined);
245
+ dispatch({ type: "confirm", action: "refresh", sources: targets });
246
+ }
247
+ }
248
+ else if (input === "d" && highlighted) {
249
+ void startRemoval(highlighted);
250
+ }
251
+ });
252
+ // Sources removed since they were marked no longer count.
253
+ const selectedCount = sources.filter((source) => marked.has(source.id)).length;
254
+ if (state.status === "loading")
255
+ return _jsx(Text, { children: "Loading sources..." });
256
+ if (state.status === "error")
257
+ return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
258
+ const window = computeWindow(sources.length, selected, height);
259
+ return (_jsxs(Box, { flexDirection: "column", children: [builtin ? _jsxs(Text, { color: theme.muted, children: [" ", builtin.id, " \u00B7 included"] }) : null, sources.length === 0 ? _jsx(Text, { children: "No Git sources \u00B7 n to add" }) : null, sources.length > 0 ? _jsxs(Text, { color: theme.muted, children: [" ", "ID KIND URL"] }) : null, sources.slice(window.start, window.end).map((source, offset) => {
260
+ const index = window.start + offset;
261
+ return (_jsxs(Text, { ...rowStyle(index === selected), children: [index === selected ? "> " : " ", marked.has(source.id) ? "[x] " : "[ ] ", source.id, " ", source.kind, " ", source.url] }, source.id));
262
+ }), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : null, _jsx(Prompt, { mode: mode }), message ? _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) : null] }));
263
+ }
264
+ function Prompt({ mode }) {
265
+ if (mode.kind === "input")
266
+ return _jsxs(Text, { children: ["URL: ", mode.value] });
267
+ if (mode.kind === "busy")
268
+ return _jsx(Text, { children: "Working..." });
269
+ if (mode.kind === "confirm") {
270
+ const lines = mode.action === "refresh"
271
+ ? mode.sources.map((source) => `refresh Git Source ${source.id} from ${source.url}`)
272
+ : [`remove Git Source ${mode.source.id} from ${mode.source.url}`];
273
+ return _jsxs(Text, { children: [lines.join("\n"), "\n", "[y/n]"] });
274
+ }
275
+ return null;
276
+ }
@@ -0,0 +1,29 @@
1
+ /** Central xeo palette for the TUI; views take every colour from here. */
2
+ export const theme = {
3
+ /** Title and active tab. */
4
+ accent: "#ff2e9a",
5
+ /** Group nodes and selection text. */
6
+ group: "#7aa2f7",
7
+ /** Markers such as hosts and versions. */
8
+ marker: "#449dab",
9
+ success: "#9ece6a",
10
+ warning: "#e0af68",
11
+ error: "#f7768e",
12
+ /** Hints and footer. */
13
+ muted: "#414868",
14
+ /** Inactive tabs. */
15
+ inactive: "#565f89",
16
+ /** Background of the selected row. */
17
+ selection: "#292e42",
18
+ };
19
+ /** Columns assumed when the terminal width is unknown. */
20
+ export const FALLBACK_TERMINAL_COLUMNS = 80;
21
+ /** Double-line rule as wide as the terminal. */
22
+ export function separatorLine(columns) {
23
+ const width = columns !== undefined && columns > 0 ? Math.floor(columns) : FALLBACK_TERMINAL_COLUMNS;
24
+ return "═".repeat(width);
25
+ }
26
+ /** Text props for a list row: bold on the selection background when selected. */
27
+ export function rowStyle(selected) {
28
+ return selected ? { bold: true, backgroundColor: theme.selection } : { bold: false };
29
+ }