@iseoane/agent-depot 0.6.13 → 0.6.15

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 (55) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +3 -1
  3. package/dist/src/app-flow.js +8 -1
  4. package/dist/src/atomic-file.js +29 -3
  5. package/dist/src/cli.js +2 -0
  6. package/dist/src/flow-progress.js +9 -0
  7. package/dist/src/git-source.js +257 -15
  8. package/dist/src/preview-model.js +37 -0
  9. package/dist/src/process-runner.js +1 -0
  10. package/dist/src/profile-cli.js +12 -4
  11. package/dist/src/profile-export.js +23 -3
  12. package/dist/src/skill-update.js +1 -0
  13. package/dist/src/source-refresh-session.js +139 -0
  14. package/dist/src/sources.js +4 -4
  15. package/dist/src/tui/adoption-actions.js +21 -2
  16. package/dist/src/tui/app-actions.js +57 -47
  17. package/dist/src/tui/app.js +22 -4
  18. package/dist/src/tui/bulk-actions.js +45 -11
  19. package/dist/src/tui/catalog-actions.js +12 -0
  20. package/dist/src/tui/catalog-flow.js +31 -11
  21. package/dist/src/tui/catalog-panel.js +15 -13
  22. package/dist/src/tui/catalog-tree.js +7 -2
  23. package/dist/src/tui/catalog-view.js +12 -6
  24. package/dist/src/tui/file-picker.js +2 -1
  25. package/dist/src/tui/installations-browse.js +1 -1
  26. package/dist/src/tui/installations-panel.js +12 -4
  27. package/dist/src/tui/installations-rows.js +2 -2
  28. package/dist/src/tui/installations-tree.js +1 -1
  29. package/dist/src/tui/installations-view.js +16 -7
  30. package/dist/src/tui/keys.js +35 -4
  31. package/dist/src/tui/manage-actions.js +55 -27
  32. package/dist/src/tui/manage-panel.js +15 -29
  33. package/dist/src/tui/mode-keys.js +11 -1
  34. package/dist/src/tui/package-actions.js +25 -18
  35. package/dist/src/tui/package-rows.js +9 -4
  36. package/dist/src/tui/panel-parts.js +54 -37
  37. package/dist/src/tui/preview-render.js +93 -0
  38. package/dist/src/tui/preview-window.js +185 -20
  39. package/dist/src/tui/profile-view.js +54 -44
  40. package/dist/src/tui/render.js +13 -1
  41. package/dist/src/tui/safe-text.js +25 -0
  42. package/dist/src/tui/sources-mode.js +2 -2
  43. package/dist/src/tui/sources-recipes.js +53 -32
  44. package/dist/src/tui/sources-view.js +85 -39
  45. package/dist/src/tui/text-wrap.js +47 -3
  46. package/dist/src/tui/unmanaged-actions.js +17 -5
  47. package/dist/src/tui/updates-flow.js +11 -5
  48. package/dist/src/tui/updates-keys.js +2 -2
  49. package/dist/src/tui/updates-model.js +5 -2
  50. package/dist/src/tui/updates-panel.js +10 -9
  51. package/dist/src/tui/updates-preview.js +27 -44
  52. package/dist/src/tui/updates-view.js +15 -11
  53. package/dist/src/tui/updates.js +117 -33
  54. package/dist/src/update-flow.js +31 -15
  55. package/package.json +1 -1
@@ -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 { applyConfirmedUpdates, describeUpdatePreview, loadUpdates, previewUpdateCandidates, readScopeInstallations, } from "../update-flow.js";
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 { installedEvidenceLabel, packagePlanLines, packageStatusLabel, packageVersionLabel, recordedVersionLabel } from "./package-rows.js";
8
+ import { describePackageComponent, installedEvidenceLabel, packageStatusLabel, packageVersionLabel, installedVersionLabel, 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
- export function previewText(text) {
167
- return { kind: "text", text };
175
+ const PROJECT_WARNING = /^ {2}WARNING: /u;
176
+ function counted(count, noun) {
177
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
168
178
  }
169
- export function previewLineText(line) {
170
- return line.kind === "field" ? `${line.label} ${line.value}` : line.text;
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: installedVersionLabel(row.check), 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 items = selected.filter((row) => row.kind !== "app" && row.kind !== "package" && row.scope === scope).map((row) => row.item);
183
- if (!loaded || items.length === 0)
234
+ const rows = selected.filter((row) => row.kind === "skill" && row.scope === scope);
235
+ if (!loaded || rows.length === 0)
184
236
  continue;
185
- const { context } = loaded;
186
- const outcome = await previewUpdateCandidates(items, context);
187
- lines.push(...loaded.projectTarget.map(previewText));
188
- lines.push(...outcome.plans.flatMap((plan) => describeUpdatePreview(plan, scope, context.projectRoot, "prompt").map(previewText)));
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
- lines.push({ kind: "field", label: "Environment:", value: plan.environment }, previewText(row.path), { kind: "field", label: "Version:", value: `${plan.previousVersion} -> ${plan.latestVersion ?? "unknown"}` }, ...(plan.argv ? [
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
- lines.push(...packagePlanLines(plan, approval).map(previewText));
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
- lines,
232
- applicable: [...groups.flatMap((group) => group.applicable), ...appPlans, ...packagePlans.map(({ plan }) => plan)],
293
+ document: {
294
+ action: applicable.length === 0 ? "No updates can be applied" : `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 prepared.packagePlans ?? []) {
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;
@@ -67,28 +67,44 @@ export async function previewUpdateCandidates(selected, context) {
67
67
  }
68
68
  return { plans, failures, nonCanonicalPaths };
69
69
  }
70
- /** The preview `update apply` prints for one candidate. */
71
- export function describeUpdatePreview(plan, scope, projectRoot, wording = "flags") {
72
- const lines = [
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
- lines.push(wording === "flags"
79
- ? ` WARNING: non-canonical location ${JSON.stringify(relative)} (outside .agents/skills and .claude/skills); --yes together with --confirm-path ${relative} explicitly confirms replacing this exact path`
80
- : ` WARNING: non-canonical location ${JSON.stringify(relative)} (outside .agents/skills and .claude/skills); a separate confirmation of this exact path is required to replace it`);
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
- lines.push(` proposed changes: replace ${plan.snapshot.files.length} managed files and persist the new version/content baseline`);
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
- lines.push(wording === "flags"
85
- ? " WARNING: the installed Skill has local modifications; --yes explicitly confirms replacing them"
86
- : " WARNING: the installed Skill has local modifications; confirming replaces them");
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
- lines.push(` external command: argv=${JSON.stringify(plan.methodPreview.argv)}`, ` external cwd: ${JSON.stringify(plan.methodPreview.cwd)}`, ` external target: ${JSON.stringify(plan.methodPreview.target)}`, " declared changes:", ...plan.methodPreview.declaredChanges.map((change) => ` ${change}`), ` WARNING: ${plan.methodPreview.warning}`);
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
- return lines;
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iseoane/agent-depot",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "A cross-platform catalog for reusable coding-agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "iseoane",