@iseoane/agent-depot 0.6.13 → 0.6.14
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 +32 -0
- package/README.md +2 -1
- package/dist/src/flow-progress.js +6 -0
- package/dist/src/git-source.js +18 -6
- package/dist/src/preview-model.js +34 -0
- package/dist/src/process-runner.js +1 -0
- package/dist/src/profile-cli.js +12 -4
- package/dist/src/profile-export.js +23 -3
- package/dist/src/skill-update.js +1 -0
- package/dist/src/source-refresh-session.js +139 -0
- package/dist/src/sources.js +4 -4
- package/dist/src/tui/adoption-actions.js +21 -2
- package/dist/src/tui/app-actions.js +54 -45
- package/dist/src/tui/app.js +22 -4
- package/dist/src/tui/bulk-actions.js +42 -9
- package/dist/src/tui/catalog-actions.js +12 -0
- package/dist/src/tui/catalog-flow.js +31 -11
- package/dist/src/tui/catalog-panel.js +15 -13
- package/dist/src/tui/catalog-tree.js +7 -2
- package/dist/src/tui/catalog-view.js +12 -6
- package/dist/src/tui/file-picker.js +2 -1
- package/dist/src/tui/installations-panel.js +12 -4
- package/dist/src/tui/installations-rows.js +1 -1
- package/dist/src/tui/installations-view.js +16 -7
- package/dist/src/tui/keys.js +27 -4
- package/dist/src/tui/manage-actions.js +55 -27
- package/dist/src/tui/manage-panel.js +15 -29
- package/dist/src/tui/mode-keys.js +11 -1
- package/dist/src/tui/package-actions.js +25 -18
- package/dist/src/tui/package-rows.js +2 -1
- package/dist/src/tui/panel-parts.js +45 -36
- package/dist/src/tui/preview-render.js +93 -0
- package/dist/src/tui/preview-window.js +141 -10
- package/dist/src/tui/profile-view.js +49 -40
- package/dist/src/tui/render.js +11 -1
- package/dist/src/tui/safe-text.js +25 -0
- package/dist/src/tui/sources-mode.js +2 -2
- package/dist/src/tui/sources-recipes.js +53 -32
- package/dist/src/tui/sources-view.js +86 -39
- package/dist/src/tui/unmanaged-actions.js +17 -5
- package/dist/src/tui/updates-flow.js +11 -5
- package/dist/src/tui/updates-keys.js +2 -2
- package/dist/src/tui/updates-model.js +5 -2
- package/dist/src/tui/updates-panel.js +10 -9
- package/dist/src/tui/updates-preview.js +27 -44
- package/dist/src/tui/updates-view.js +8 -10
- package/dist/src/tui/updates.js +117 -33
- package/dist/src/update-flow.js +31 -15
- package/package.json +1 -1
|
@@ -1,65 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { quoteText } from "../preview-model.js";
|
|
2
|
+
import { appPlanLines } from "./app-actions.js";
|
|
3
|
+
import { fieldLine, renderPreviewDocument, textLine } from "./preview-render.js";
|
|
2
4
|
export function updateModePreviewLines(mode) {
|
|
3
5
|
switch (mode.kind) {
|
|
4
6
|
case "preview":
|
|
5
|
-
return
|
|
6
|
-
...mode.prepared.lines,
|
|
7
|
-
...mode.prepared.appFailures.map(previewText),
|
|
8
|
-
...mode.prepared.packageFailures.map(previewText),
|
|
9
|
-
...mode.prepared.failures.map(({ item, message }) => previewText(`Preview failed for ${JSON.stringify(item.id)}: ${message}`)),
|
|
10
|
-
];
|
|
7
|
+
return renderPreviewDocument(mode.prepared.document);
|
|
11
8
|
case "manual":
|
|
12
|
-
return [
|
|
13
|
-
{ kind: "field", label: "App:", value: mode.plan.entry.recipe?.name ?? mode.plan.entry.file },
|
|
14
|
-
{ kind: "field", label: "Environment:", value: mode.plan.environment },
|
|
15
|
-
{ kind: "field", label: "Working directory:", value: mode.plan.cwd },
|
|
16
|
-
{ kind: "field", label: "Manual:", value: mode.plan.manual ?? "" },
|
|
17
|
-
...(mode.reason === undefined ? [] : [previewText(mode.reason)]),
|
|
18
|
-
];
|
|
9
|
+
return [...appPlanLines(mode.plan), ...(mode.reason === undefined ? [] : [textLine(mode.reason)])];
|
|
19
10
|
case "confirm-paths":
|
|
20
11
|
return [
|
|
21
|
-
|
|
22
|
-
...mode.prepared.nonCanonicalPaths.map(({ scope, path }) => ({
|
|
23
|
-
kind: "field",
|
|
24
|
-
label: "Path:",
|
|
25
|
-
value: `[${scope}] ${JSON.stringify(path)}`,
|
|
26
|
-
})),
|
|
12
|
+
textLine("Outside .agents/skills and .claude/skills:"),
|
|
13
|
+
...mode.prepared.nonCanonicalPaths.map(({ scope, path }) => fieldLine("Path:", `[${scope}] ${quoteText(path)}`)),
|
|
27
14
|
];
|
|
28
15
|
default:
|
|
29
16
|
return [];
|
|
30
17
|
}
|
|
31
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* What else confirming does besides applying: replaced local changes and external commands. Kept to
|
|
21
|
+
* a bounded length so a narrow footer still fits the question after it; the warnings name every Skill.
|
|
22
|
+
*/
|
|
23
|
+
function previewEffects(prepared) {
|
|
24
|
+
const replaced = prepared.groups.flatMap((group) => group.applicable.filter((item) => group.overwriteShown.has(item.id)));
|
|
25
|
+
const commands = prepared.document.sections.flatMap(({ items }) => items.flatMap(({ rows }) => rows.filter((row) => row.kind === "command"))).length;
|
|
26
|
+
return [
|
|
27
|
+
...(replaced.length === 1 ? [`replaces ${replaced[0].selection.path}'s local changes`] : []),
|
|
28
|
+
...(replaced.length > 1 ? [`replaces local changes in ${replaced.length} Skills`] : []),
|
|
29
|
+
...(commands === 0 ? [] : [`runs ${commands} ${commands === 1 ? "command" : "commands"}`]),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
32
|
export function updateModePreviewChrome(mode) {
|
|
33
33
|
switch (mode.kind) {
|
|
34
34
|
case "preview": {
|
|
35
35
|
const count = mode.prepared.applicable.length;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
footerRows: 2,
|
|
41
|
-
};
|
|
36
|
+
if (count === 0)
|
|
37
|
+
return { title: "Update batch preview", consequence: "Nothing can be applied.", cancel: "[Enter/Esc] back" };
|
|
38
|
+
const action = mode.prepared.document.action;
|
|
39
|
+
return { title: "Update batch preview", action, consequence: [...previewEffects(mode.prepared), `${action}? y/n`].join(" · ") };
|
|
42
40
|
}
|
|
43
41
|
case "manual":
|
|
44
|
-
return {
|
|
45
|
-
title: "Manual App update",
|
|
46
|
-
questions: ["Scroll to end before confirming; n cancels.", "Manual step done, check now? y/n"],
|
|
47
|
-
footerRows: 2,
|
|
48
|
-
};
|
|
42
|
+
return { title: "Manual App update", action: "Check now", consequence: "Manual step done, check now? y/n" };
|
|
49
43
|
case "confirm-paths":
|
|
50
|
-
return {
|
|
51
|
-
title: "Confirm replacing non-canonical locations",
|
|
52
|
-
questions: ["Scroll to end before confirming; n cancels.", "Replace exactly these paths? y/n"],
|
|
53
|
-
footerRows: 2,
|
|
54
|
-
};
|
|
44
|
+
return { title: "Confirm replacing non-canonical locations", action: "Replace these paths", consequence: "Replace exactly these paths? y/n" };
|
|
55
45
|
default:
|
|
56
|
-
return { title: "Updates"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
export function updateModeQuestion(mode, reachedEnd) {
|
|
60
|
-
if (mode.kind === "preview" && mode.prepared.applicable.length === 0) {
|
|
61
|
-
return "Nothing can be applied. Enter or Esc to go back.";
|
|
46
|
+
return { title: "Updates" };
|
|
62
47
|
}
|
|
63
|
-
const chrome = updateModePreviewChrome(mode);
|
|
64
|
-
return reachedEnd ? chrome.questions.at(-1) ?? "Continue? y/n" : chrome.questions[0] ?? "Scroll to end before confirming; n cancels.";
|
|
65
48
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
3
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
5
|
import { theme } from "./theme.js";
|
|
5
|
-
import {
|
|
6
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
6
7
|
import { computeWindow, useListHeight } from "./window.js";
|
|
7
8
|
import { BROWSE } from "./updates-mode.js";
|
|
8
9
|
import { defaultExpanded } from "./tree.js";
|
|
@@ -11,7 +12,7 @@ import { useUpdatesFlow } from "./updates-flow.js";
|
|
|
11
12
|
import { handleUpdatesKey } from "./updates-keys.js";
|
|
12
13
|
import { entryId, viewOf } from "./updates-model.js";
|
|
13
14
|
import { CheckStatus, EntryLine, ModePanel, UpdatesFooter } from "./updates-panel.js";
|
|
14
|
-
import {
|
|
15
|
+
import { lineText } from "./preview-render.js";
|
|
15
16
|
import { updateModePreviewChrome, updateModePreviewLines } from "./updates-preview.js";
|
|
16
17
|
import { useKeys } from "./keys.js";
|
|
17
18
|
import { BorderPanel, Working } from "./panel-parts.js";
|
|
@@ -20,7 +21,6 @@ const NO_ENVIRONMENT = {};
|
|
|
20
21
|
const CLOCK_TICK_MS = 10_000;
|
|
21
22
|
export function UpdatesView({ operations, environment, onCapturingChange, listHeight, now }) {
|
|
22
23
|
const height = useListHeight(listHeight, 9);
|
|
23
|
-
const { rows: terminalRows, columns: terminalColumns } = useWindowSize();
|
|
24
24
|
const env = environment ?? NO_ENVIRONMENT;
|
|
25
25
|
const clock = now ?? Date.now;
|
|
26
26
|
// Scope, cursor, fold and selection are mirrored in refs updated first, so keys delivered in one burst act on the latest state.
|
|
@@ -73,7 +73,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
73
73
|
const { state, load, startPreview, apply } = useUpdatesFlow({
|
|
74
74
|
operations, env, clock, mounted, checkedRef, latest, setChecked, setCursor, setMode, modeRef, setMessage,
|
|
75
75
|
});
|
|
76
|
-
// Entering the view refreshes the Git sources first, like `r
|
|
76
|
+
// Entering the view refreshes the Git sources first, like `r`, but may reuse a recent refresh (ADR 0004).
|
|
77
77
|
useEffect(() => {
|
|
78
78
|
void load(true);
|
|
79
79
|
}, [load]);
|
|
@@ -81,10 +81,8 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
81
81
|
dataRef.current = data;
|
|
82
82
|
const { shown, updatable, unknown, upToDate, entries } = viewOf(data, scope, showUnknown, expanded);
|
|
83
83
|
const index = Math.min(cursor, Math.max(entries.length - 1, 0));
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
return previewViewport(updateModePreviewLines(current).map(previewLineText), cursor, terminalRows || 24, terminalColumns || 80, updateModePreviewChrome(current));
|
|
87
|
-
};
|
|
84
|
+
const geometry = usePreviewGeometry();
|
|
85
|
+
const previewGeometry = (current) => geometry(updateModePreviewLines(current).map(lineText), "cursor" in current ? current.cursor : 0, updateModePreviewChrome(current));
|
|
88
86
|
const keyContext = {
|
|
89
87
|
height,
|
|
90
88
|
now: clock,
|
|
@@ -105,7 +103,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
105
103
|
recheck: () => {
|
|
106
104
|
setMessage(undefined);
|
|
107
105
|
setShowUnknown(false);
|
|
108
|
-
void load(true);
|
|
106
|
+
void load(true, true);
|
|
109
107
|
},
|
|
110
108
|
startPreview: () => void startPreview(),
|
|
111
109
|
setMode,
|
package/dist/src/tui/updates.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { checkAppUpdates, createAppOperations } from "../app-flow.js";
|
|
2
|
-
import { displayPackageSelection, selectionKey } from "../package-model.js";
|
|
2
|
+
import { displayPackageCoordinates, displayPackageSelection, selectionKey } from "../package-model.js";
|
|
3
3
|
import { formatVersionPolicy } from "../skill-install.js";
|
|
4
|
-
import {
|
|
4
|
+
import { sourceIdForUrl } from "../git-source.js";
|
|
5
|
+
import { applyConfirmedUpdates, loadUpdates, previewUpdateCandidates, readScopeInstallations, skillUpdatePreviewItem, } from "../update-flow.js";
|
|
5
6
|
import { packageOperationsOf } from "./environment.js";
|
|
6
7
|
import { formatVersionEvidence } from "./installations.js";
|
|
7
|
-
import {
|
|
8
|
+
import { describePackageComponent, installedEvidenceLabel, packageStatusLabel, packageVersionLabel, recordedVersionLabel } from "./package-rows.js";
|
|
9
|
+
import { quoteText } from "../preview-model.js";
|
|
8
10
|
/** Every scope the view can assess, in display order. */
|
|
9
11
|
export const UPDATE_SCOPES = ["project", "user-global"];
|
|
10
12
|
const APP_STATUS_LABEL = {
|
|
@@ -32,18 +34,18 @@ function scopeInput(operations, environment, scope) {
|
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
/** Refreshes through the project-source operation the install flow uses; a registered Source is refreshed by id otherwise. */
|
|
35
|
-
async function refreshOne(operations, source) {
|
|
37
|
+
async function refreshOne(operations, source, options) {
|
|
36
38
|
if (operations.refreshProjectSource) {
|
|
37
|
-
await operations.refreshProjectSource(source);
|
|
39
|
+
await operations.refreshProjectSource(source, options);
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
42
|
const registered = (await operations.listSources()).find((candidate) => candidate.kind === "git" && candidate.url === source.url);
|
|
41
43
|
if (!registered)
|
|
42
44
|
throw new Error("the Source is not registered, so it cannot be refreshed here");
|
|
43
|
-
await operations.refreshSource(registered.id);
|
|
45
|
+
await operations.refreshSource(registered.id, options);
|
|
44
46
|
}
|
|
45
47
|
/** Refreshes every distinct Git source the installations use, one at a time; the built-in source is skipped. */
|
|
46
|
-
async function refreshSources(operations, selections) {
|
|
48
|
+
async function refreshSources(operations, selections, options, session) {
|
|
47
49
|
const distinct = new Map();
|
|
48
50
|
for (const { source } of selections) {
|
|
49
51
|
if (source.kind === "external" && "url" in source)
|
|
@@ -51,17 +53,24 @@ async function refreshSources(operations, selections) {
|
|
|
51
53
|
}
|
|
52
54
|
const refreshed = [];
|
|
53
55
|
const failed = [];
|
|
56
|
+
const startedAt = session?.now();
|
|
57
|
+
let reusedAt;
|
|
54
58
|
for (const source of distinct.values()) {
|
|
55
59
|
const label = source.ref === undefined ? source.url : `${source.url}@${source.ref}`;
|
|
56
60
|
try {
|
|
57
|
-
await refreshOne(operations, source);
|
|
61
|
+
await refreshOne(operations, source, options);
|
|
58
62
|
refreshed.push(label);
|
|
63
|
+
// A refresh that finished before this check began was reused, not made now.
|
|
64
|
+
const finishedAt = session?.snapshot().get(sourceIdForUrl(source.url))?.finishedAt;
|
|
65
|
+
if (startedAt !== undefined && finishedAt !== undefined && finishedAt < startedAt) {
|
|
66
|
+
reusedAt = Math.min(reusedAt ?? finishedAt, finishedAt);
|
|
67
|
+
}
|
|
59
68
|
}
|
|
60
69
|
catch (error) {
|
|
61
70
|
failed.push({ source: label, reason: errorText(error) });
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
|
-
return { attempted: true, refreshed, failed, unreadable: [] };
|
|
73
|
+
return { attempted: true, refreshed, failed, unreadable: [], ...(reusedAt === undefined ? {} : { reusedAt }) };
|
|
65
74
|
}
|
|
66
75
|
/**
|
|
67
76
|
* Refreshes the Git sources involved (unless told otherwise), then runs the
|
|
@@ -80,7 +89,7 @@ export async function loadUpdateRows(operations, environment, options) {
|
|
|
80
89
|
unreadable.push({ scope, reason: errorText(error) });
|
|
81
90
|
return [];
|
|
82
91
|
})));
|
|
83
|
-
refresh = { ...(await refreshSources(operations, installations.flat())), unreadable };
|
|
92
|
+
refresh = { ...(await refreshSources(operations, installations.flat(), { force: options.force === true }, environment.refreshSession)), unreadable };
|
|
84
93
|
}
|
|
85
94
|
options.onPhase?.("checking");
|
|
86
95
|
const results = await Promise.all(UPDATE_SCOPES.map(async (scope) => {
|
|
@@ -163,11 +172,50 @@ export function formatAgo(elapsedMs) {
|
|
|
163
172
|
return `${minutes} min ago`;
|
|
164
173
|
return `${Math.floor(minutes / 60)} h ago`;
|
|
165
174
|
}
|
|
166
|
-
|
|
167
|
-
|
|
175
|
+
const PROJECT_WARNING = /^ {2}WARNING: /u;
|
|
176
|
+
function counted(count, noun) {
|
|
177
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
168
178
|
}
|
|
169
|
-
|
|
170
|
-
|
|
179
|
+
function appPreviewItem(plan) {
|
|
180
|
+
const rows = [];
|
|
181
|
+
if (plan.argv) {
|
|
182
|
+
rows.push({ kind: "command", argv: plan.argv, cwd: plan.cwd, ...(plan.executable === undefined ? {} : { executable: plan.executable }) });
|
|
183
|
+
if (plan.executable === undefined)
|
|
184
|
+
rows.push({ kind: "warning", text: `${quoteText(plan.argv[0])} not found in PATH` });
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
rows.push({ kind: "field", label: "Working directory", value: plan.cwd });
|
|
188
|
+
}
|
|
189
|
+
if (plan.manual)
|
|
190
|
+
rows.push({ kind: "field", label: "Manual", value: plan.manual });
|
|
191
|
+
if (plan.warning)
|
|
192
|
+
rows.push({ kind: "warning", text: plan.warning });
|
|
193
|
+
return {
|
|
194
|
+
title: plan.entry.recipe?.name ?? plan.entry.file,
|
|
195
|
+
version: { from: plan.previousVersion ?? "unknown", to: plan.latestVersion ?? "unknown" },
|
|
196
|
+
rows,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** The frozen plan with every fact the approval covers: origin, inventory, commands, blast radius and approval state. */
|
|
200
|
+
function packagePreviewItem(row, { plan, approval }) {
|
|
201
|
+
const { descriptor, selection } = plan;
|
|
202
|
+
const rows = [
|
|
203
|
+
{ kind: "field", label: "Coordinates", value: displayPackageSelection(selection) },
|
|
204
|
+
{ kind: "field", label: "Origin", value: `manifest ${plan.origin.manifestDigest.slice(0, 12)}${plan.origin.resolvedCommit === undefined ? "" : ` commit ${plan.origin.resolvedCommit.slice(0, 7)}`}` },
|
|
205
|
+
{ kind: "field", label: `Components (${descriptor.components.length})`, value: "" },
|
|
206
|
+
...descriptor.components.map((component) => ({ kind: "text", text: describePackageComponent(component), indent: 1 })),
|
|
207
|
+
];
|
|
208
|
+
if (plan.commands.length === 0) {
|
|
209
|
+
rows.push({ kind: "text", text: "This action runs no host command." }, { kind: "field", label: "Working directory", value: plan.cwd });
|
|
210
|
+
}
|
|
211
|
+
plan.commands.forEach((command, position) => rows.push({ kind: "field", label: `Step ${position + 1}`, value: command.purpose }, { kind: "command", argv: [command.executable, ...command.args], executable: command.resolvedPath ?? command.executable, cwd: plan.cwd }));
|
|
212
|
+
rows.push({ kind: "field", label: "Blast radius", value: plan.affects.map((coordinates) => displayPackageCoordinates(selection.source, coordinates)).join(", ") }, { kind: "field", label: "Approval", value: approval }, ...plan.warnings.map((warning) => ({ kind: "warning", text: warning })), ...(approval === "needs approval" ? [{ kind: "text", text: "This apply approves the declaration above." }] : []));
|
|
213
|
+
return {
|
|
214
|
+
title: descriptor.name,
|
|
215
|
+
detail: selection.host,
|
|
216
|
+
version: { from: recordedVersionLabel(row.check.installed), to: packageVersionLabel(descriptor.version) },
|
|
217
|
+
rows,
|
|
218
|
+
};
|
|
171
219
|
}
|
|
172
220
|
/** Plan step: previews the selection from the assessed snapshots, scope by scope. Changes nothing. */
|
|
173
221
|
export async function prepareUpdates(data, selected) {
|
|
@@ -176,16 +224,28 @@ export async function prepareUpdates(data, selected) {
|
|
|
176
224
|
const appFailures = [];
|
|
177
225
|
const packagePlans = [];
|
|
178
226
|
const packageFailures = [];
|
|
179
|
-
const lines = [];
|
|
180
227
|
const failures = [];
|
|
228
|
+
const context = [];
|
|
229
|
+
const warnings = [];
|
|
230
|
+
const skillItems = [];
|
|
231
|
+
const appItems = [];
|
|
232
|
+
const packageItems = [];
|
|
181
233
|
for (const { scope, loaded } of data.results) {
|
|
182
|
-
const
|
|
183
|
-
if (!loaded ||
|
|
234
|
+
const rows = selected.filter((row) => row.kind === "skill" && row.scope === scope);
|
|
235
|
+
if (!loaded || rows.length === 0)
|
|
184
236
|
continue;
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
237
|
+
const outcome = await previewUpdateCandidates(rows.map((row) => row.item), loaded.context);
|
|
238
|
+
for (const line of loaded.projectTarget) {
|
|
239
|
+
if (PROJECT_WARNING.test(line))
|
|
240
|
+
warnings.push(line.replace(PROJECT_WARNING, ""));
|
|
241
|
+
else
|
|
242
|
+
context.push(line);
|
|
243
|
+
}
|
|
244
|
+
for (const plan of outcome.plans) {
|
|
245
|
+
const row = rows.find((candidate) => candidate.item === plan.item);
|
|
246
|
+
const item = skillUpdatePreviewItem(plan, scope, loaded.context.projectRoot, "prompt");
|
|
247
|
+
skillItems.push(row === undefined ? item : { ...item, version: { from: row.installed, to: row.available } });
|
|
248
|
+
}
|
|
189
249
|
failures.push(...outcome.failures);
|
|
190
250
|
groups.push({
|
|
191
251
|
scope,
|
|
@@ -201,10 +261,7 @@ export async function prepareUpdates(data, selected) {
|
|
|
201
261
|
try {
|
|
202
262
|
const plan = await data.apps.planLifecycle(row.check.entry, "update", undefined, row.check);
|
|
203
263
|
appPlans.push(plan);
|
|
204
|
-
|
|
205
|
-
{ kind: "field", label: "argv:", value: JSON.stringify(plan.argv) },
|
|
206
|
-
{ kind: "field", label: "Executable:", value: plan.executable ?? `${JSON.stringify(plan.argv[0])} not found in PATH` },
|
|
207
|
-
] : []), ...(plan.manual ? [{ kind: "field", label: "Manual:", value: plan.manual }] : []), { kind: "field", label: "Working directory:", value: plan.cwd }, ...(plan.warning ? [previewText(plan.warning)] : []));
|
|
264
|
+
appItems.push(appPreviewItem(plan));
|
|
208
265
|
}
|
|
209
266
|
catch (error) {
|
|
210
267
|
appFailures.push(`Failed ${row.path}: preview failed: ${errorText(error)}`);
|
|
@@ -217,19 +274,35 @@ export async function prepareUpdates(data, selected) {
|
|
|
217
274
|
const plan = await data.packages.planLifecycle(row.check.installed.selection, "update");
|
|
218
275
|
const approval = await data.packages.approvalStatus(row.check.installed.selection, "update");
|
|
219
276
|
packagePlans.push({ plan, approval });
|
|
220
|
-
|
|
221
|
-
if (approval === "needs approval")
|
|
222
|
-
lines.push(previewText("This apply approves the declaration above."));
|
|
277
|
+
packageItems.push(packagePreviewItem(row, { plan, approval }));
|
|
223
278
|
}
|
|
224
279
|
catch (error) {
|
|
225
280
|
packageFailures.push(`Failed ${row.path}: preview failed: ${errorText(error)}`);
|
|
226
281
|
}
|
|
227
282
|
}
|
|
283
|
+
const applicable = [...groups.flatMap((group) => group.applicable), ...appPlans, ...packagePlans.map(({ plan }) => plan)];
|
|
284
|
+
const sections = [
|
|
285
|
+
{ heading: "SKILLS", items: skillItems, noun: "Skill" },
|
|
286
|
+
{ heading: "APPS", items: appItems, noun: "App" },
|
|
287
|
+
{ heading: "PACKAGES", items: packageItems, noun: "Package" },
|
|
288
|
+
].filter(({ items }) => items.length > 0);
|
|
289
|
+
const environments = new Set([...appPlans, ...packagePlans.map(({ plan }) => plan)].map((plan) => plan.environment));
|
|
228
290
|
return {
|
|
229
291
|
groups, apps: data.apps, appPlans, appFailures,
|
|
230
292
|
packages: data.packages, packagePlans, packageFailures,
|
|
231
|
-
|
|
232
|
-
|
|
293
|
+
document: {
|
|
294
|
+
action: `Apply ${counted(applicable.length, "update")}`,
|
|
295
|
+
summary: sections.map(({ items, noun }) => counted(items.length, noun)).join(" · "),
|
|
296
|
+
context: [...context, ...[...environments].map((environment) => `Environment: ${environment}`)],
|
|
297
|
+
warnings,
|
|
298
|
+
sections: sections.map(({ heading, items }) => ({ heading, items })),
|
|
299
|
+
failures: [
|
|
300
|
+
...failures.map(({ item, message }) => `${item.selection.path}: ${message}`),
|
|
301
|
+
...appFailures,
|
|
302
|
+
...packageFailures,
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
applicable,
|
|
233
306
|
failures,
|
|
234
307
|
nonCanonicalPaths: groups.flatMap((group) => group.nonCanonicalPaths.map((location) => ({ scope: group.scope, path: location }))),
|
|
235
308
|
};
|
|
@@ -242,17 +315,27 @@ export async function prepareUpdates(data, selected) {
|
|
|
242
315
|
* answer (or absent callback) cancels and clears pending update evidence.
|
|
243
316
|
* A Skill that became locally modified after the preview fails instead of being overwritten.
|
|
244
317
|
*/
|
|
245
|
-
export async function runUpdates(prepared, confirmedPaths, confirmManual
|
|
318
|
+
export async function runUpdates(prepared, confirmedPaths, confirmManual,
|
|
319
|
+
/** Called before each Skill, App and Package runs, numbered across the whole batch. */
|
|
320
|
+
onProgress) {
|
|
246
321
|
const updated = [];
|
|
247
322
|
const failedLines = [];
|
|
323
|
+
const packagePlans = prepared.packagePlans ?? [];
|
|
324
|
+
const total = prepared.groups.reduce((sum, group) => sum + group.applicable.length, 0) + prepared.appPlans.length + packagePlans.length;
|
|
325
|
+
let position = 0;
|
|
326
|
+
const started = (kind, name) => {
|
|
327
|
+
position += 1;
|
|
328
|
+
onProgress?.({ kind, name, position, total, verb: "Updating" });
|
|
329
|
+
};
|
|
248
330
|
for (const group of prepared.groups) {
|
|
249
|
-
const result = await applyConfirmedUpdates(group.applicable, group.loaded.context, confirmedPaths
|
|
331
|
+
const result = await applyConfirmedUpdates(group.applicable, { ...group.loaded.context, onItem: (item) => started("skill", item.selection.path) }, confirmedPaths
|
|
250
332
|
.filter((confirmed) => confirmed.scope === group.scope && group.nonCanonicalPaths.includes(confirmed.path))
|
|
251
333
|
.map((confirmed) => confirmed.path), group.overwriteShown);
|
|
252
334
|
updated.push(...result.updated.map((item) => `Updated ${item.selection.path}`));
|
|
253
335
|
failedLines.push(...result.failed.map((item) => `Failed ${item.selection.path}: ${item.error ?? "unknown error"}`));
|
|
254
336
|
}
|
|
255
337
|
for (const plan of prepared.appPlans) {
|
|
338
|
+
started("app", plan.entry.recipe.name);
|
|
256
339
|
const name = `App: ${plan.entry.recipe.name}`;
|
|
257
340
|
try {
|
|
258
341
|
const apps = prepared.apps;
|
|
@@ -276,7 +359,8 @@ export async function runUpdates(prepared, confirmedPaths, confirmManual) {
|
|
|
276
359
|
failedLines.push(`Failed ${name}: ${errorText(error)}`);
|
|
277
360
|
}
|
|
278
361
|
}
|
|
279
|
-
for (const { plan, approval } of
|
|
362
|
+
for (const { plan, approval } of packagePlans) {
|
|
363
|
+
started("package", plan.descriptor.name);
|
|
280
364
|
const name = `Package: ${plan.descriptor.name} (${plan.selection.host}) from ${displayPackageSelection(plan.selection)}`;
|
|
281
365
|
try {
|
|
282
366
|
const packages = prepared.packages;
|
package/dist/src/update-flow.js
CHANGED
|
@@ -67,28 +67,44 @@ export async function previewUpdateCandidates(selected, context) {
|
|
|
67
67
|
}
|
|
68
68
|
return { plans, failures, nonCanonicalPaths };
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
71
|
-
export function
|
|
72
|
-
const
|
|
73
|
-
`Preview: update Skill ${JSON.stringify(plan.selection.path)} from ${plan.source.id} (scope: ${scope})`,
|
|
74
|
-
` target: ${plan.target}`,
|
|
75
|
-
];
|
|
70
|
+
/** One candidate's preview: target, warnings to confirm, proposed changes and any external command. */
|
|
71
|
+
export function skillUpdatePreviewItem(plan, scope, projectRoot, wording) {
|
|
72
|
+
const rows = [{ kind: "field", label: "target", value: plan.target }];
|
|
76
73
|
if (plan.nonCanonicalPath !== undefined) {
|
|
77
74
|
const relative = relativeProjectPath(projectRoot, plan.nonCanonicalPath);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
rows.push({ kind: "warning", text: wording === "flags"
|
|
76
|
+
? `non-canonical location ${JSON.stringify(relative)} (outside .agents/skills and .claude/skills); --yes together with --confirm-path ${relative} explicitly confirms replacing this exact path`
|
|
77
|
+
: `non-canonical location ${JSON.stringify(relative)} (outside .agents/skills and .claude/skills); a separate confirmation of this exact path is required to replace it` });
|
|
81
78
|
}
|
|
82
|
-
|
|
79
|
+
rows.push({ kind: "field", label: "proposed changes", value: `replace ${plan.snapshot.files.length} managed files and persist the new version/content baseline` });
|
|
83
80
|
if (plan.overwriteRequired) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
rows.push({ kind: "warning", text: wording === "flags"
|
|
82
|
+
? "the installed Skill has local modifications; --yes explicitly confirms replacing them"
|
|
83
|
+
: "the installed Skill has local modifications; confirming replaces them" });
|
|
87
84
|
}
|
|
88
85
|
if (plan.methodPreview) {
|
|
89
|
-
|
|
86
|
+
rows.push({ kind: "command", argv: plan.methodPreview.argv, cwd: plan.methodPreview.cwd }, { kind: "field", label: "external target", value: JSON.stringify(plan.methodPreview.target) }, { kind: "field", label: "declared changes", value: "" }, ...plan.methodPreview.declaredChanges.map((change) => ({ kind: "text", text: change, indent: 1 })), { kind: "warning", text: plan.methodPreview.warning });
|
|
87
|
+
}
|
|
88
|
+
return { title: plan.selection.path, detail: `${plan.source.id} · ${scope}`, rows };
|
|
89
|
+
}
|
|
90
|
+
/** How `update apply` prints one row of a Skill preview item. */
|
|
91
|
+
function cliPreviewRow(row) {
|
|
92
|
+
switch (row.kind) {
|
|
93
|
+
case "field": return [` ${row.label}:${row.value === "" ? "" : ` ${row.value}`}`];
|
|
94
|
+
case "text": return [` ${" ".repeat(row.indent ?? 0)}${row.text}`];
|
|
95
|
+
case "warning": return [` WARNING: ${row.text}`];
|
|
96
|
+
case "command": return [
|
|
97
|
+
` external command: argv=${JSON.stringify(row.argv)}`,
|
|
98
|
+
...(row.cwd === undefined ? [] : [` external cwd: ${JSON.stringify(row.cwd)}`]),
|
|
99
|
+
];
|
|
90
100
|
}
|
|
91
|
-
|
|
101
|
+
}
|
|
102
|
+
/** The preview `update apply` prints for one candidate. */
|
|
103
|
+
export function describeUpdatePreview(plan, scope, projectRoot, wording = "flags") {
|
|
104
|
+
return [
|
|
105
|
+
`Preview: update Skill ${JSON.stringify(plan.selection.path)} from ${plan.source.id} (scope: ${scope})`,
|
|
106
|
+
...skillUpdatePreviewItem(plan, scope, projectRoot, wording).rows.flatMap(cliPreviewRow),
|
|
107
|
+
];
|
|
92
108
|
}
|
|
93
109
|
/**
|
|
94
110
|
* `--yes` alone never confirms a repository-controlled non-canonical location:
|