@iseoane/agent-depot 0.2.1 → 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.
@@ -0,0 +1,157 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { PROJECT_HOSTS } from "../project-manifest.js";
4
+ import { errorText, runBatch } from "./batch.js";
5
+ import { answerYesNo, hostChecklistKey } from "./mode-keys.js";
6
+ import { HostChecklist, PreviewConfirm } from "./panel-parts.js";
7
+ export const APP_KINDS = ["app-approve", "app-hosts", "app-confirm", "app-manual"];
8
+ /** Narrows the view's mode without asserting a partial mode is an App mode. */
9
+ export function isAppMode(mode) {
10
+ return APP_KINDS.some(kind => kind === mode.kind);
11
+ }
12
+ /** Recipe name, falling back to the file identity for invalid recipes. */
13
+ export const appName = (entry) => entry.recipe?.name ?? entry.file;
14
+ function stepLines(step) {
15
+ return [
16
+ ...(step.argv ? [`argv: ${JSON.stringify(step.argv)}`, `Executable: ${step.executable ?? "unavailable"}`] : []),
17
+ ...(step.manual ? [`Manual: ${step.manual}`] : []),
18
+ `Working directory: ${step.cwd}`,
19
+ ...(step.warning ? [step.warning] : []),
20
+ ];
21
+ }
22
+ async function prepareNext(context, requests, results = [], failures = 0) {
23
+ const { manage, apps } = context;
24
+ manage.setMode({ kind: "busy", label: "Preparing App step..." });
25
+ // Independent planning failures never stop the remaining marked Apps.
26
+ const [request, ...remaining] = requests;
27
+ if (!request) {
28
+ await manage.finish({ kind: failures > 0 ? "error" : "ok", text: results.join("\n") });
29
+ return;
30
+ }
31
+ try {
32
+ const plan = await apps.planLifecycle(request.entry, request.action, request.host);
33
+ if (manage.isMounted()) {
34
+ manage.setMode({
35
+ kind: !plan.argv && plan.manual ? "app-manual" : "app-confirm",
36
+ plan, remaining, results, failures,
37
+ });
38
+ }
39
+ }
40
+ catch (error) {
41
+ if (manage.isMounted()) {
42
+ await prepareNext(context, remaining, [...results, `${appName(request.entry)}: failed: ${errorText(error)}`], failures + 1);
43
+ }
44
+ }
45
+ }
46
+ /** Opens approval, the Host checklist, or the first lifecycle preview for selected Apps. */
47
+ export async function startAppAction(context, entries, input) {
48
+ const { manage, apps } = context;
49
+ manage.setMessage(undefined);
50
+ try {
51
+ if (input === "approve") {
52
+ const entry = entries[0];
53
+ if (!entry)
54
+ throw new Error("No App selected");
55
+ manage.setMode({ kind: "busy", label: "Preparing recipe approval..." });
56
+ const steps = await apps.preview(entry);
57
+ if (manage.isMounted()) {
58
+ manage.setMode({ kind: "app-approve", entry, lines: steps.flatMap(step => [step.step, ...stepLines(step)]) });
59
+ }
60
+ }
61
+ else if (input === "h") {
62
+ const choices = PROJECT_HOSTS.filter(host => entries.some(entry => entry.recipe?.setup?.[host] || entry.recipe?.teardown?.[host]));
63
+ if (!choices.length)
64
+ throw new Error("No declared Host steps");
65
+ // This is an action selector, not a claim about persisted Host wiring.
66
+ manage.setMode({
67
+ kind: "app-hosts", entries, choices, cursor: 0,
68
+ selected: choices.filter(host => entries.some(entry => entry.recipe?.teardown?.[host])),
69
+ });
70
+ }
71
+ else {
72
+ await prepareNext(context, entries.map(entry => ({ entry, action: input === "u" ? "uninstall" : "install" })));
73
+ }
74
+ }
75
+ catch (error) {
76
+ await manage.finish({ kind: "error", text: errorText(error) });
77
+ }
78
+ }
79
+ async function execute(context, mode) {
80
+ const { manage, apps } = context;
81
+ manage.setMode({ kind: "busy", label: "Running App step..." });
82
+ let manualRequired = false;
83
+ const [outcome] = await runBatch([mode.plan], async (plan) => {
84
+ const result = mode.kind === "app-manual"
85
+ ? await apps.completeManual(plan, true)
86
+ : await apps.executeLifecycle(plan, true);
87
+ if (result.status === "manual required") {
88
+ manualRequired = true;
89
+ if (manage.isMounted())
90
+ manage.setMode({ ...mode, kind: "app-manual" });
91
+ return "manual required";
92
+ }
93
+ if (result.status === "failed")
94
+ throw new Error(result.reason);
95
+ manage.unmark([`app:${plan.entry.file}`]);
96
+ return `${appName(plan.entry)}: ${result.status}${result.installedVersion ? ` ${result.installedVersion}` : ""}`;
97
+ }, (plan, error) => `${appName(plan.entry)}: failed: ${errorText(error)}`);
98
+ if (manage.isMounted() && outcome && !manualRequired) {
99
+ await prepareNext(context, mode.remaining, [...mode.results, outcome.line], mode.failures + (outcome.ok ? 0 : 1));
100
+ }
101
+ }
102
+ /** Applies checklist and y/n keys, continuing independent batch steps after cancellation or failure. */
103
+ export function handleAppKey(context, mode, input, key) {
104
+ const { manage, apps } = context;
105
+ const cancel = () => {
106
+ manage.browse();
107
+ manage.setMessage({ kind: "ok", text: "App action cancelled" });
108
+ };
109
+ if (mode.kind === "app-hosts") {
110
+ const outcome = hostChecklistKey(mode, mode.choices, input, key);
111
+ if (outcome.kind === "cancel")
112
+ cancel();
113
+ else if (outcome.kind === "update")
114
+ manage.setMode({ ...mode, ...outcome.state });
115
+ else if (key.return) {
116
+ const requests = mode.entries.flatMap(entry => mode.choices.flatMap(host => {
117
+ const action = mode.selected.includes(host) ? "setup" : "teardown";
118
+ return entry.recipe?.[action]?.[host] ? [{ entry, action, host }] : [];
119
+ }));
120
+ if (!requests.length)
121
+ manage.setMessage({ kind: "error", text: "No declared steps for this selection" });
122
+ else
123
+ void prepareNext(context, requests);
124
+ }
125
+ }
126
+ else
127
+ answerYesNo(input, key, () => {
128
+ if (mode.kind !== "app-approve") {
129
+ void execute(context, mode);
130
+ return;
131
+ }
132
+ manage.setMode({ kind: "busy", label: "Approving recipe..." });
133
+ void apps.approve(mode.entry).then(() => manage.finish({ kind: "ok", text: `Approved ${appName(mode.entry)}` }), error => manage.finish({ kind: "error", text: errorText(error) }));
134
+ }, () => {
135
+ if (mode.kind === "app-confirm" || mode.kind === "app-manual") {
136
+ void prepareNext(context, mode.remaining, [...mode.results, `${appName(mode.plan.entry)}: cancelled`], mode.failures);
137
+ }
138
+ else
139
+ cancel();
140
+ });
141
+ }
142
+ /** Renders App prompts using the existing checklist and preview-confirm panels. */
143
+ export function AppPanel({ mode }) {
144
+ if (mode.kind === "app-hosts") {
145
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["App Host steps: checked = setup, unchecked = teardown. Wiring is not tracked. Space/1-", mode.choices.length, " toggle, j/k move, Enter continue, Esc cancel."] }), _jsx(HostChecklist, { choices: mode.choices, cursor: mode.cursor, selected: mode.selected })] }));
146
+ }
147
+ if (mode.kind === "app-approve") {
148
+ return _jsx(PreviewConfirm, { lines: mode.lines, question: `Approve ${appName(mode.entry)}? y/n` });
149
+ }
150
+ return (_jsx(PreviewConfirm, { lines: [
151
+ `Environment: ${mode.plan.environment}`,
152
+ `App: ${appName(mode.plan.entry)} ${mode.plan.action}${mode.plan.host ? ` (${mode.plan.host})` : ""}`,
153
+ ...stepLines(mode.plan),
154
+ ], question: mode.kind === "app-manual"
155
+ ? "Manual step done, check now? y/n"
156
+ : `${mode.plan.action} ${appName(mode.plan.entry)}? y/n` }));
157
+ }
@@ -20,7 +20,11 @@ export async function loadInstalledSkills(operations, environment) {
20
20
  manifestStoreOf(environment).load().then((manifest) => manifest.skills, () => []),
21
21
  operations.listSources().catch(() => []),
22
22
  ]);
23
- const unmanagedNames = await scanUserGlobalSkillInventory({ homeDirectory: homeOf(environment), managedInstallations: global }).then((inventory) => new Set([...inventory.unmanaged, ...inventory.symlinks].map((entry) => entry.name)), () => new Set());
23
+ const unmanagedNames = await scanUserGlobalSkillInventory({
24
+ homeDirectory: homeOf(environment),
25
+ appEnvironment: environment.appEnvironment,
26
+ managedInstallations: global,
27
+ }).then((inventory) => new Set([...inventory.unmanaged, ...inventory.symlinks].map((entry) => entry.name)), () => new Set());
24
28
  return { global, project, sources, unmanagedNames };
25
29
  }
26
30
  /** Whether an unmanaged copy with the Skill's directory name exists on disk (the name an install would use). */
@@ -1,3 +1,4 @@
1
+ import { appName } from "./app-actions.js";
1
2
  import { startBulkHostAddition, startBulkUninstall } from "./bulk-actions.js";
2
3
  import { skillOf, sourceIdOf } from "./installations.js";
3
4
  import { leavesOf, targetOf } from "./installations-tree.js";
@@ -86,7 +87,21 @@ function actOnRow(context, snapshot, row, input, key) {
86
87
  context.setMessage(undefined);
87
88
  if (item.kind === "unmanaged")
88
89
  actOnUnmanaged(context, item.group, input, key);
89
- else
90
+ else if (item.kind === "app") {
91
+ if (input === "A")
92
+ context.setMessage({ kind: "error", text: "Adoption applies to unmanaged Skills, not Apps" });
93
+ else if (key.return && item.row.inspection.status !== "needs approval") {
94
+ const { entry, inspection } = item.row;
95
+ const approved = inspection.status === "installed" || inspection.status === "not installed";
96
+ context.setMessage({
97
+ kind: approved ? "ok" : "error",
98
+ text: approved ? `${appName(entry)} is already approved` : inspection.reason ?? inspection.status,
99
+ });
100
+ }
101
+ else
102
+ context.startApps([item.row.entry], key.return ? "approve" : input);
103
+ }
104
+ else if (item.kind === "installation")
90
105
  actOnInstallation(context, data, item, input, key);
91
106
  }
92
107
  /** Keys while browsing: navigation, marks, bulk actions and the per-row actions. */
@@ -96,7 +111,14 @@ export function handleBrowseKey(context, input, key) {
96
111
  return;
97
112
  const row = snapshot.visible[snapshot.at];
98
113
  const rowAction = input === "A" || key.return || input === "u" || input === "h" || input === "i";
99
- if (input === " ")
114
+ const appMarks = leavesOf(context.navigation.latestRoots()).filter(node => context.marks.markedRef.current.has(node.id) && node.data.kind === "app");
115
+ if ((input === "u" || input === "h" || input === "i") && appMarks.length) {
116
+ if (markedTargets(context).length)
117
+ context.setMessage({ kind: "error", text: "Select Apps or Skills separately for bulk actions" });
118
+ else
119
+ context.startApps(appMarks.flatMap(node => node.data.kind === "app" ? [node.data.row.entry] : []), input);
120
+ }
121
+ else if (input === " ")
100
122
  markRow(context, row);
101
123
  else if (input === "a")
102
124
  markAllListed(context, snapshot.visible);
@@ -29,9 +29,17 @@ export function buildTree(data) {
29
29
  });
30
30
  roots.push({
31
31
  id: "scope:unmanaged",
32
- data: { kind: "group", label: `Unmanaged (user-global) (${data.unmanaged.length})` },
32
+ data: {
33
+ kind: "group",
34
+ label: `Unmanaged (user-global) (${data.unmanaged.length})${data.unmanagedWarnings?.length ? ` — ${data.unmanagedWarnings.join("; ")}` : ""}`,
35
+ },
33
36
  children: data.unmanaged.map((group) => ({ id: `unmanaged:${group.name}`, data: { kind: "unmanaged", group } })),
34
37
  });
38
+ if (data.apps?.length || data.appsError)
39
+ roots.push({
40
+ id: "scope:apps", data: { kind: "group", label: `Apps (user-global) (${data.apps?.length ?? 0})${data.appsError ? ` — ${data.appsError}` : ""}` },
41
+ children: (data.apps ?? []).map(row => ({ id: `app:${row.entry.file}`, data: { kind: "app", row } })),
42
+ });
35
43
  return roots;
36
44
  }
37
45
  /** Version, policy and hosts of a leaf; `adopted` / `modified` only when true (the legend explains them). */
@@ -59,6 +67,10 @@ export function describeNode(row) {
59
67
  return `${row.expandable ? (row.expanded ? "▾" : "▸") : " "} ${data.label}`;
60
68
  if (data.kind === "installation")
61
69
  return data.row.selection.path;
70
+ if (data.kind === "app") {
71
+ const { entry, inspection } = data.row;
72
+ return `${entry.recipe?.name ?? entry.file} ${inspection.status === "invalid" ? `invalid recipe: ${inspection.reason}` : inspection.installedVersion ?? inspection.status}`;
73
+ }
62
74
  return `${data.group.name} [${data.group.hosts.join(", ")}]`;
63
75
  }
64
76
  /** `symlink → <target>` for the locations that are links, one entry per distinct target. */
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { createAppOperations } from "../app-flow.js";
5
+ import { handleAppKey, isAppMode, startAppAction } from "./app-actions.js";
4
6
  import { loadInstallations } from "./installations.js";
5
7
  import { BROWSE } from "./installations-mode.js";
6
8
  import { errorText } from "./batch.js";
@@ -22,6 +24,7 @@ const LEGEND = "adopted: tracked in place · modified: changed on disk";
22
24
  export function InstallationsView({ operations, environment, onCapturingChange, listHeight }) {
23
25
  const height = useListHeight(listHeight, 7);
24
26
  const env = environment ?? NO_ENVIRONMENT;
27
+ const apps = useMemo(() => createAppOperations({ homeDirectory: env.homeDirectory, ...env.appEnvironment }), [env]);
25
28
  const [state, setState] = useState({ status: "loading" });
26
29
  const [mode, setModeState] = useState(BROWSE);
27
30
  // Mirrors the mode synchronously so later keystrokes and the shell never see stale state.
@@ -34,17 +37,19 @@ export function InstallationsView({ operations, environment, onCapturingChange,
34
37
  onCapturingChange?.(next.kind !== "browse");
35
38
  setModeState(next);
36
39
  };
40
+ const loadGeneration = useRef(0);
37
41
  const reload = useCallback(async () => {
42
+ const generation = ++loadGeneration.current;
38
43
  try {
39
- const data = await loadInstallations(operations, env);
40
- if (mounted.current)
44
+ const data = await loadInstallations(operations, env, apps);
45
+ if (mounted.current && generation === loadGeneration.current)
41
46
  setState({ status: "ready", data });
42
47
  }
43
48
  catch (error) {
44
- if (mounted.current)
49
+ if (mounted.current && generation === loadGeneration.current)
45
50
  setState({ status: "error", message: errorText(error) });
46
51
  }
47
- }, [operations, env, mounted]);
52
+ }, [operations, env, apps, mounted]);
48
53
  useEffect(() => {
49
54
  void reload();
50
55
  }, [reload]);
@@ -52,6 +57,35 @@ export function InstallationsView({ operations, environment, onCapturingChange,
52
57
  const roots = useMemo(() => (data ? buildTree(data) : []), [data]);
53
58
  const navigation = useTreeNavigation(roots, defaultExpanded);
54
59
  const { rows, index } = navigation;
60
+ const checked = useRef(new Set());
61
+ const highlighted = rows[index]?.node.data;
62
+ useEffect(() => {
63
+ if (highlighted?.kind !== "app")
64
+ return;
65
+ const { entry } = highlighted.row;
66
+ const identity = `${entry.file}:${entry.hash}`;
67
+ if (checked.current.has(identity))
68
+ return;
69
+ checked.current.add(identity);
70
+ const generation = loadGeneration.current;
71
+ void apps.inspect(entry).then(inspection => {
72
+ if (!mounted.current || generation !== loadGeneration.current)
73
+ return;
74
+ setState(previous => previous.status !== "ready" ? previous : {
75
+ ...previous,
76
+ data: {
77
+ ...previous.data,
78
+ apps: previous.data.apps?.map(row => row.entry.file === entry.file && row.entry.hash === entry.hash
79
+ ? { entry, inspection } : row),
80
+ },
81
+ });
82
+ }).catch(error => {
83
+ if (!mounted.current || generation !== loadGeneration.current)
84
+ return;
85
+ checked.current.delete(identity);
86
+ setMessage({ kind: "error", text: errorText(error) });
87
+ });
88
+ }, [highlighted, apps, mounted]);
55
89
  const installed = useMemo(() => ({
56
90
  global: (data?.global ?? []).map((row) => row.selection),
57
91
  project: (data?.project ?? []).map((row) => row.selection),
@@ -68,6 +102,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
68
102
  isMounted: () => mounted.current,
69
103
  unmark: (ids) => marks.setMarked(new Set([...marks.markedRef.current].filter((id) => !ids.includes(id)))),
70
104
  finish: async (result) => {
105
+ checked.current.clear();
71
106
  await reload();
72
107
  if (!mounted.current)
73
108
  return;
@@ -92,11 +127,14 @@ export function InstallationsView({ operations, environment, onCapturingChange,
92
127
  manage,
93
128
  startAdoption: (group) => void startAdoption(adoption, group),
94
129
  setMessage,
130
+ startApps: (entries, input) => void startAppAction({ apps, manage }, entries, input),
95
131
  };
96
132
  useKeys((input, key) => {
97
133
  const current = modeRef.current;
98
134
  if (current.kind === "browse")
99
135
  handleBrowseKey(browse, input, key);
136
+ else if (isAppMode(current))
137
+ handleAppKey({ apps, manage }, current, input, key);
100
138
  else if (!handleManageKey(manage, current, input, key))
101
139
  handleAdoptionKey(adoption, current, input, key);
102
140
  });
@@ -107,8 +145,10 @@ export function InstallationsView({ operations, environment, onCapturingChange,
107
145
  const ready = state.data;
108
146
  const window = computeWindow(rows.length, index, height);
109
147
  const listed = rows.slice(window.start, window.end);
110
- const selectedCount = leavesOf(roots).filter((node) => marks.marked.has(node.id)).length;
148
+ const markedLeaves = leavesOf(roots).filter(node => marks.marked.has(node.id));
149
+ const selectedCount = markedLeaves.length;
150
+ const markedApps = markedLeaves.filter(node => node.data.kind === "app").length;
111
151
  // The legend explains the markers, so it shows only while a rendered row carries one.
112
152
  const legendNeeded = listed.some((row) => row.node.data.kind === "installation" && markersOf(row.node.data.row) !== "");
113
- return (_jsxs(Box, { flexDirection: "column", children: [ready.global === undefined ? _jsx(Text, { children: "User-global installations are not supported by the configured operations" }) : null, ready.projectError === undefined ? null : _jsxs(Text, { color: theme.error, children: ["Error: ", ready.projectError] }), listed.map((row, offset) => (_jsx(TreeRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id) }, row.node.id))), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), legendNeeded ? _jsx(Text, { color: theme.muted, children: LEGEND }) : null, _jsx(ModePanel, { mode: mode })] }));
153
+ return (_jsxs(Box, { flexDirection: "column", children: [ready.global === undefined ? _jsx(Text, { children: "User-global installations are not supported by the configured operations" }) : null, ready.projectError === undefined ? null : _jsxs(Text, { color: theme.error, children: ["Error: ", ready.projectError] }), listed.map((row, offset) => (_jsx(TreeRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id) }, row.node.id))), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), markedApps > 0 ? _jsxs(Text, { color: theme.muted, children: [markedApps, " Apps marked"] }) : null, legendNeeded ? _jsx(Text, { color: theme.muted, children: LEGEND }) : null, _jsx(ModePanel, { mode: mode })] }));
114
154
  }
@@ -1,9 +1,11 @@
1
1
  import { homedir } from "node:os";
2
2
  import path from "node:path";
3
+ import { createAppOperations } from "../app-flow.js";
3
4
  import { defaultProjectManifestPath, ProjectManifestStore, } from "../project-manifest.js";
4
5
  import { formatVersionPolicy } from "../skill-install.js";
5
6
  import { inspectSkillRemovals, userGlobalRemovalOptions } from "../skill-removal.js";
6
7
  import { scanUserGlobalSkillInventory, } from "../user-global-skill-inventory.js";
8
+ import { errorText } from "./batch.js";
7
9
  import { prepareInstall } from "./catalog-installs.js";
8
10
  /** Hosts in display order; the Claude root serves Claude Code, the shared root serves the others. */
9
11
  const HOST_DISPLAY_ORDER = ["claude", "pi", "codex", "opencode"];
@@ -69,8 +71,23 @@ async function toRow(scope, selection, inspectionOptions) {
69
71
  };
70
72
  }
71
73
  /** Loads both scopes and the unmanaged user-global inventory the CLI's `update check` uses. */
72
- export async function loadInstallations(operations, environment) {
74
+ export async function loadInstallations(operations, environment, appOperations = createAppOperations({ homeDirectory: homeOf(environment), ...environment.appEnvironment })) {
73
75
  const home = homeOf(environment);
76
+ let apps = [];
77
+ let appsError;
78
+ try {
79
+ const tracked = await appOperations.trackedApps();
80
+ apps = await Promise.all((await appOperations.load()).filter(entry => entry.applicable).map(async (entry) => ({
81
+ entry,
82
+ inspection: await appOperations.inspect(entry, {
83
+ installedVersion: tracked.find(record => record.name === entry.recipe?.name
84
+ && record.recipeFile === entry.canonicalFile)?.installedVersion,
85
+ }),
86
+ })));
87
+ }
88
+ catch (error) {
89
+ appsError = errorText(error);
90
+ }
74
91
  const globalRecords = operations.listUserGlobalInstallations
75
92
  ? await operations.listUserGlobalInstallations()
76
93
  : undefined;
@@ -94,11 +111,17 @@ export async function loadInstallations(operations, environment) {
94
111
  ...environment.installationOptions,
95
112
  };
96
113
  const project = await Promise.all(projectRecords.map((record) => toRow("project", record, projectOptions)));
114
+ const unmanagedWarnings = [];
97
115
  const unmanaged = globalRecords
98
- ? groupUnmanaged(await scanUserGlobalSkillInventory({ homeDirectory: home, managedInstallations: globalRecords }))
116
+ ? groupUnmanaged(await scanUserGlobalSkillInventory({
117
+ homeDirectory: home,
118
+ managedInstallations: globalRecords,
119
+ appEnvironment: environment.appEnvironment,
120
+ reportWarning: warning => unmanagedWarnings.push(warning),
121
+ }))
99
122
  : [];
100
123
  const sources = await operations.listSources().catch(() => []);
101
- return { global, project, ...(projectError === undefined ? {} : { projectError }), unmanaged, sources };
124
+ return { unmanagedWarnings, global, project, ...(projectError === undefined ? {} : { projectError }), unmanaged, sources, apps, ...(appsError === undefined ? {} : { appsError }) };
102
125
  }
103
126
  /** Source id of an installation's Source, when that Source is still registered. */
104
127
  export function sourceIdOf(selection, sources) {
@@ -1,3 +1,4 @@
1
+ import { APP_KINDS } from "./app-actions.js";
1
2
  import { globalRecord, missingHosts, prepareHostAddition, prepareHostRemoval, prepareUninstall, runHostAddition, runHostRemoval, runUninstall, UNINSTALL_REFUSALS, } from "./catalog-installs.js";
2
3
  import { errorText } from "./batch.js";
3
4
  import { BULK_KINDS, handleBulkKey } from "./bulk-actions.js";
@@ -11,6 +12,7 @@ const MANAGE_KINDS = new Set([
11
12
  "confirm-host-exposure",
12
13
  "confirm-host-remove",
13
14
  "confirm-uninstall",
15
+ ...APP_KINDS,
14
16
  ...UNMANAGED_KINDS,
15
17
  ...BULK_KINDS,
16
18
  "busy",
@@ -1,5 +1,6 @@
1
1
  import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
+ import { AppPanel } from "./app-actions.js";
3
4
  import { ChecklistLines, HostChecklist, PreviewConfirm, PreviewLines } from "./panel-parts.js";
4
5
  import { theme } from "./theme.js";
5
6
  const plural = (n) => `${n} ${n === 1 ? "item" : "items"}`;
@@ -26,6 +27,11 @@ function HostsPreview({ plan }) {
26
27
  /** Renders the prompt of a manage mode: host lists, previews and confirmations. */
27
28
  export function ManagePanel({ mode }) {
28
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 });
29
35
  case "remove-scope":
30
36
  return _jsxs(Text, { children: ["Uninstall ", mode.skill.name, " (hosts: ", mode.hosts.join(", "), "): 1 all hosts 2 choose hosts (Esc cancel)"] });
31
37
  case "hosts-add":
@@ -36,7 +36,7 @@ export async function prepareUnmanagedRemoval(operations, environment, group, lo
36
36
  if (!operations.listUserGlobalInstallations)
37
37
  throw new Error("Uninstall is not supported by the configured operations");
38
38
  const installations = await operations.listUserGlobalInstallations();
39
- const options = { homeDirectory: homeOf(environment), managedInstallations: installations };
39
+ const options = { homeDirectory: homeOf(environment), appEnvironment: environment.appEnvironment, managedInstallations: installations };
40
40
  const items = [];
41
41
  const preview = [`WARNING: agent-depot did not create these files (unmanaged skill ${group.name})`];
42
42
  // A chosen directory takes the links that point at it along, so none is left dangling.
@@ -70,7 +70,7 @@ export async function prepareUnmanagedRemoval(operations, environment, group, lo
70
70
  }
71
71
  /** Execute step: each location is rechecked and removed on its own, so one failure does not stop the others. */
72
72
  export async function runUnmanagedRemoval(prepared, operations, environment) {
73
- const context = { operations, homeDirectory: homeOf(environment) };
73
+ const context = { operations, homeDirectory: homeOf(environment), appEnvironment: environment.appEnvironment };
74
74
  const lines = [];
75
75
  let failed = false;
76
76
  const notRemoved = new Set();
@@ -1,4 +1,4 @@
1
- import { useCallback, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { errorText } from "./batch.js";
3
3
  import { BROWSE } from "./updates-mode.js";
4
4
  import { CHECKING, emptyData, PHASE_LABEL, REFRESHING } from "./updates-model.js";
@@ -7,6 +7,10 @@ export function useUpdatesFlow(inputs) {
7
7
  const { operations, env, clock, mounted, checkedRef, latest, setChecked, setCursor, setMode, setMessage } = inputs;
8
8
  const [state, setState] = useState({ status: "loading", label: REFRESHING });
9
9
  const loadToken = useRef(0);
10
+ const pendingAnswer = useRef(undefined);
11
+ useEffect(() => () => {
12
+ pendingAnswer.current?.(false);
13
+ }, []);
10
14
  const load = useCallback(async (refresh) => {
11
15
  const token = ++loadToken.current;
12
16
  const phase = (next) => {
@@ -45,7 +49,7 @@ export function useUpdatesFlow(inputs) {
45
49
  try {
46
50
  const prepared = await prepareUpdates(current, selected);
47
51
  if (mounted.current)
48
- setMode({ kind: "preview", selected: selected.map((row) => row.item), prepared });
52
+ setMode({ kind: "preview", prepared });
49
53
  }
50
54
  catch (error) {
51
55
  if (!mounted.current)
@@ -58,7 +62,18 @@ export function useUpdatesFlow(inputs) {
58
62
  setMode({ kind: "busy", label: "Applying updates..." });
59
63
  let result;
60
64
  try {
61
- const outcome = await runUpdates(prepared, confirmedPaths);
65
+ const outcome = await runUpdates(prepared, confirmedPaths, (plan, reason) => new Promise(resolve => {
66
+ if (!mounted.current) {
67
+ resolve(false);
68
+ return;
69
+ }
70
+ const answer = (done) => {
71
+ pendingAnswer.current = undefined;
72
+ resolve(done);
73
+ };
74
+ pendingAnswer.current = answer;
75
+ setMode({ kind: "manual", plan, reason, answer });
76
+ }));
62
77
  result = { kind: outcome.failed === 0 ? "ok" : "error", lines: outcome.lines };
63
78
  }
64
79
  catch (error) {
@@ -80,6 +80,15 @@ function handlePreviewKey(context, mode, input, key) {
80
80
  /** Handles a key in the current Updates mode; `busy` ignores keys. */
81
81
  export function handleUpdatesKey(context, mode, input, key) {
82
82
  switch (mode.kind) {
83
+ case "manual":
84
+ answerYesNo(input, key, () => {
85
+ context.setMode({ kind: "busy", label: "Checking App version..." });
86
+ mode.answer(true);
87
+ }, () => {
88
+ context.setMode({ kind: "busy", label: "Continuing updates..." });
89
+ mode.answer(false);
90
+ });
91
+ return;
83
92
  case "browse":
84
93
  handleBrowseKey(context, input, key);
85
94
  return;
@@ -15,9 +15,9 @@ const matchesScope = (row, filter) => filter === "all" || row.scope === filter;
15
15
  /** The rows the list shows for a scope filter and fold state, and the cursor positions they make up. */
16
16
  export function viewOf(data, scope, showUnknown) {
17
17
  const shown = (data?.rows ?? []).filter((row) => matchesScope(row, scope));
18
- const updatable = shown.filter((row) => row.item.status === "updateable");
19
- const unknown = shown.filter((row) => row.item.status === "unknown");
20
- const upToDate = shown.filter((row) => row.item.status === "current").length;
18
+ const updatable = shown.filter((row) => row.status === "update available");
19
+ const unknown = shown.filter((row) => row.status === "cannot assess");
20
+ const upToDate = shown.filter((row) => row.status === "up to date").length;
21
21
  const entries = [
22
22
  ...updatable.map((row) => ({ kind: "update", row })),
23
23
  ...(unknown.length > 0 ? [{ kind: "summary" }] : []),
@@ -7,7 +7,7 @@ import { PreviewLines } from "./panel-parts.js";
7
7
  /** What the last check found out: its age, how the sources were refreshed, and what failed. */
8
8
  export function CheckStatus({ data, now }) {
9
9
  const errors = data.results.filter((result) => result.error !== undefined);
10
- return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["checked ", formatAgo(now - data.checkedAt), " \u00B7 ", describeRefresh(data)] }), data.refresh.failed.map((failure) => (_jsxs(Text, { color: theme.warning, children: ["refresh failed for ", failure.source, ": ", failure.reason] }, failure.source))), errors.map((result) => _jsxs(Text, { color: theme.error, children: ["Error (", result.scope, "): ", result.error] }, result.scope))] }));
10
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["checked ", formatAgo(now - data.checkedAt), " \u00B7 ", describeRefresh(data)] }), data.refresh.failed.map((failure) => (_jsxs(Text, { color: theme.warning, children: ["refresh failed for ", failure.source, ": ", failure.reason] }, failure.source))), data.appError ? _jsxs(Text, { color: theme.error, children: ["Error (Apps): ", data.appError] }) : null, errors.map((result) => _jsxs(Text, { color: theme.error, children: ["Error (", result.scope, "): ", result.error] }, result.scope))] }));
11
11
  }
12
12
  /** One line of the list: an update, the fold of unassessable items, or one of those when unfolded. */
13
13
  export function EntryLine({ entry, selected, checked, view, showUnknown }) {
@@ -28,12 +28,14 @@ export function UpdatesFooter({ indicator, upToDate, updatableCount, checkedCoun
28
28
  /** The prompt of the preview and confirmation modes; browsing and the check itself show nothing. */
29
29
  export function ModePanel({ mode }) {
30
30
  switch (mode.kind) {
31
+ case "manual":
32
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["App: ", mode.plan.entry.recipe.name] }), _jsxs(Text, { children: ["Environment: ", mode.plan.environment] }), _jsxs(Text, { children: ["Working directory: ", mode.plan.cwd] }), _jsxs(Text, { children: ["Manual: ", mode.plan.manual] }), mode.reason ? _jsx(Text, { color: theme.warning, children: mode.reason }) : null, _jsx(Text, { children: "Manual step done, check now? y/n" })] }));
31
33
  case "browse":
32
34
  return null;
33
35
  case "busy":
34
36
  return mode.label === REFRESHING || mode.label === CHECKING ? null : _jsx(Text, { children: mode.label });
35
37
  case "preview":
36
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PreviewLines, { lines: mode.prepared.lines }), mode.prepared.failures.map(({ item, message }) => (_jsxs(Text, { color: theme.error, children: ["Preview failed for ", JSON.stringify(item.id), ": ", message] }, item.id))), mode.prepared.applicable.length === 0
38
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PreviewLines, { lines: mode.prepared.lines }), _jsx(PreviewLines, { lines: mode.prepared.appFailures }), mode.prepared.failures.map(({ item, message }) => (_jsxs(Text, { color: theme.error, children: ["Preview failed for ", JSON.stringify(item.id), ": ", message] }, item.id))), mode.prepared.applicable.length === 0
37
39
  ? _jsx(Text, { children: "Nothing can be applied. Enter/Esc to go back" })
38
40
  : _jsxs(Text, { children: ["Apply ", mode.prepared.applicable.length, " ", mode.prepared.applicable.length === 1 ? "update" : "updates", "? y/n"] })] }));
39
41
  case "confirm-paths":
@@ -96,6 +96,6 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
96
96
  useKeys((input, key) => handleUpdatesKey(keyContext, modeRef.current, input, key));
97
97
  const window = computeWindow(entries.length, index, height);
98
98
  const view = { shown, updatable, unknown, upToDate, entries };
99
- const hasErrors = (data?.results ?? []).some((result) => result.error !== undefined);
99
+ const hasErrors = data?.appError !== undefined || (data?.results ?? []).some((result) => result.error !== undefined);
100
100
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.accent, bold: true, children: ["Updates (scope: ", scope, ") t all \u00B7 p project \u00B7 g user-global"] }), state.status === "loading" ? _jsx(Text, { children: state.label }) : null, data ? _jsx(CheckStatus, { data: data, now: clock() }) : null, data && !hasErrors && shown.length === 0 ? _jsx(Text, { children: "No installations" }) : null, entries.slice(window.start, window.end).map((entry, offset) => (_jsx(EntryLine, { entry: entry, selected: window.start + offset === index, checked: entry.kind === "update" && checked.includes(entry.row.key), view: view, showUnknown: showUnknown }, entry.kind === "summary" ? "summary" : entry.row.key))), _jsx(UpdatesFooter, { indicator: window.indicator, upToDate: upToDate, updatableCount: updatable.length, checkedCount: checked.length, message: message }), _jsx(ModePanel, { mode: mode })] }));
101
101
  }