@iseoane/agent-depot 0.6.4 → 0.6.5

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 (43) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.es.md +14 -10
  3. package/README.md +22 -8
  4. package/dist/src/app-cli.js +10 -5
  5. package/dist/src/app-flow.js +2 -2
  6. package/dist/src/atomic-file.js +9 -0
  7. package/dist/src/cli.js +98 -67
  8. package/dist/src/executable-resolution.js +9 -3
  9. package/dist/src/git-source.js +2 -4
  10. package/dist/src/package-cli.js +9 -4
  11. package/dist/src/package-flow.js +2 -1
  12. package/dist/src/package-host-state.js +44 -6
  13. package/dist/src/package-state.js +2 -5
  14. package/dist/src/process-runner.js +234 -16
  15. package/dist/src/profile-cli.js +8 -6
  16. package/dist/src/progress.js +157 -0
  17. package/dist/src/source-state.js +2 -5
  18. package/dist/src/tui/adoption-actions.js +4 -3
  19. package/dist/src/tui/app-actions.js +16 -9
  20. package/dist/src/tui/bulk-actions.js +7 -6
  21. package/dist/src/tui/catalog-flow.js +3 -2
  22. package/dist/src/tui/catalog-panel.js +2 -2
  23. package/dist/src/tui/catalog-view.js +26 -6
  24. package/dist/src/tui/file-picker.js +52 -20
  25. package/dist/src/tui/installations-view.js +44 -11
  26. package/dist/src/tui/manage-actions.js +7 -6
  27. package/dist/src/tui/manage-panel.js +2 -2
  28. package/dist/src/tui/package-actions.js +63 -31
  29. package/dist/src/tui/panel-parts.js +20 -0
  30. package/dist/src/tui/profile-view.js +84 -15
  31. package/dist/src/tui/sources-mode.js +2 -1
  32. package/dist/src/tui/sources-recipes.js +22 -10
  33. package/dist/src/tui/sources-view.js +13 -12
  34. package/dist/src/tui/text-wrap.js +13 -0
  35. package/dist/src/tui/unmanaged-actions.js +3 -2
  36. package/dist/src/tui/updates-flow.js +10 -7
  37. package/dist/src/tui/updates-keys.js +3 -2
  38. package/dist/src/tui/updates-model.js +2 -2
  39. package/dist/src/tui/updates-panel.js +3 -2
  40. package/dist/src/tui/updates-view.js +6 -3
  41. package/dist/src/tui/view-state.js +12 -0
  42. package/dist/src/tui/working-model.js +33 -0
  43. package/package.json +2 -2
package/dist/src/cli.js CHANGED
@@ -6,12 +6,14 @@ import path from "node:path";
6
6
  import { checkAppUpdates, createAppOperations } from "./app-flow.js";
7
7
  import { describeAppResult, describeManualCompletion, runAppCommand } from "./app-cli.js";
8
8
  import { createPackageOperations } from "./package-flow.js";
9
- import { describeLifecyclePlan, describePackageResult, describePackageUpdateCheck, formatPackageSelection, packageSelectionMatches, runPackageCommand } from "./package-cli.js";
9
+ import { describeLifecyclePlan, describePackageResult, describePackageUpdateCheck, formatPackageSelection, packageSelectionMatches, planWithRefresh, runPackageCommand } from "./package-cli.js";
10
10
  import { selectionKey } from "./package-model.js";
11
11
  import { EXPORT_USAGE, IMPORT_USAGE, runProfileExport, runProfileImport } from "./profile-cli.js";
12
12
  import { pathsOverlap } from "./path-safety.js";
13
+ import { withChildOutput } from "./process-runner.js";
14
+ import { createProgress, NO_PROGRESS } from "./progress.js";
13
15
  import { skillTreeBaseline } from "./skill-discovery.js";
14
- import { BuiltInSourceError, createSourceOperations, resolveProjectSource, SourceMetadataError, SourceNotFoundError, } from "./sources.js";
16
+ import { BUILT_IN_SOURCE, BuiltInSourceError, createSourceOperations, resolveProjectSource, SourceMetadataError, SourceNotFoundError, } from "./sources.js";
15
17
  import { AGENT_DEPOT_PACKAGE_NAME, AGENT_DEPOT_PACKAGE_VERSION, builtinPinMismatch, defaultProjectManifestPath, parseProjectManifest, ProjectManifestStore, PROJECT_HOSTS, } from "./project-manifest.js";
16
18
  import { inspectProjectSkillInstallation, assertNoOverlappingProjectSkillRemovalTargets, } from "./project-installation.js";
17
19
  import { selectUpdateBatch, UpdateBatchSelectionError, } from "./update-batch.js";
@@ -57,6 +59,10 @@ const USAGE = [
57
59
  ` ${IMPORT_USAGE}`,
58
60
  " agent-depot tui",
59
61
  " agent-depot --version",
62
+ "",
63
+ "Options for every command, anywhere on the line:",
64
+ " --verbose stream the full child output (npm, pi, git, App commands) to stderr line by line as it runs,",
65
+ " instead of a spinner and the last few lines; stdout is unchanged.",
60
66
  ].join("\n");
61
67
  const INSTALL_USAGE = "Usage: agent-depot install --scope <project|user-global> --source <id> --skill <path> --host <host>... --version <latest|version> [--ref <git-ref>] [--method <json>] --portable-v1 [--overwrite --yes] [--confirm-additional-host]";
62
68
  const DISCOVER_USAGE = "Usage: agent-depot discover <source-id> [source-id...]\nSelect at least one Source ID explicitly; run `agent-depot source list` to see registered Sources";
@@ -74,11 +80,12 @@ const SOURCE_SUBCOMMANDS = new Map([
74
80
  }],
75
81
  ["add", async (values, { operations, output }) => {
76
82
  requireArgumentCount(values, 1, "source add <url>");
83
+ // Registration only: nothing is cloned until the first refresh, so there is no long step to report.
77
84
  const source = await operations.addGitSource(values[0]);
78
85
  output(`Added Git Source: ${source.id}\t${source.url}`);
79
86
  }],
80
87
  ["refresh", runSourceRefresh],
81
- ["remove", (values, { operations, dependencies, output }) => runSourceRemoval(values, operations, dependencies, output)],
88
+ ["remove", (values, { operations, dependencies, output, progress }) => runSourceRemoval(values, operations, dependencies, output, progress)],
82
89
  ["migrate", (values, { operations, dependencies, output }) => runSourceMigration(values, operations, dependencies, output)],
83
90
  ]);
84
91
  function packageOperationsFor(dependencies, operations) {
@@ -89,34 +96,34 @@ function packageOperationsFor(dependencies, operations) {
89
96
  });
90
97
  }
91
98
  const COMMANDS = new Map([
92
- ["import", (values, { operations, dependencies, output }) => runProfileImport(values, operations, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output, { ...dependencies, packageOperations: packageOperationsFor(dependencies, operations) })],
93
- ["export", (values, { operations, dependencies, output }) => runProfileExport(values, operations, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output, dependencies.stderr ?? (line => console.error(line)), dependencies.homeDirectory, packageOperationsFor(dependencies, operations))],
94
- ["app", (values, { dependencies, output }) => runAppCommand(values, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output)],
95
- ["package", (values, { operations, dependencies, output }) => runPackageCommand(values, packageOperationsFor(dependencies, operations), operations, output)],
96
- ["install", async (values, { operations, dependencies, output }) => {
97
- await runInstall(values, operations, dependencies, output);
99
+ ["import", (values, { operations, dependencies, output, progress }) => runProfileImport(values, operations, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output, { ...dependencies, packageOperations: packageOperationsFor(dependencies, operations) }, progress)],
100
+ ["export", (values, { operations, dependencies, output, errorOutput, progress }) => runProfileExport(values, operations, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output, errorOutput, dependencies.homeDirectory, packageOperationsFor(dependencies, operations), progress)],
101
+ ["app", (values, { dependencies, output, progress }) => runAppCommand(values, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output, progress)],
102
+ ["package", (values, { operations, dependencies, output, progress }) => runPackageCommand(values, packageOperationsFor(dependencies, operations), operations, output, progress)],
103
+ ["install", async (values, { operations, dependencies, output, progress }) => {
104
+ await runInstall(values, operations, dependencies, output, progress);
98
105
  return 0;
99
106
  }],
100
- ["update", (values, { operations, dependencies, output }) => runUpdate(values, operations, dependencies, output)],
101
- ["self", async (values, { dependencies, output }) => {
107
+ ["update", (values, { operations, dependencies, output, progress }) => runUpdate(values, operations, dependencies, output, progress)],
108
+ ["self", async (values, { dependencies, output, progress }) => {
102
109
  if (values[0] !== "update")
103
110
  throw new CliUsageError("Usage: agent-depot self update [--yes]");
104
- return runSelfUpdate(values.slice(1), dependencies, output);
111
+ return runSelfUpdate(values.slice(1), dependencies, output, progress);
105
112
  }],
106
- ["skill", async (values, { operations, dependencies, output }) => {
113
+ ["skill", async (values, { operations, dependencies, output, progress }) => {
107
114
  if (values[0] === "remove") {
108
- await runSkillRemoval(values.slice(1), operations, dependencies, output);
115
+ await runSkillRemoval(values.slice(1), operations, dependencies, output, progress);
109
116
  }
110
117
  else if (values[0] === "host" && values[1] === "add") {
111
- await runSkillHostAdd(values.slice(2), operations, dependencies, output);
118
+ await runSkillHostAdd(values.slice(2), operations, dependencies, output, progress);
112
119
  }
113
120
  else {
114
121
  throw new CliUsageError(USAGE);
115
122
  }
116
123
  return 0;
117
124
  }],
118
- ["uninstall", async (values, { operations, dependencies, output }) => {
119
- await runUninstall(values, operations, dependencies, output);
125
+ ["uninstall", async (values, { operations, dependencies, output, progress }) => {
126
+ await runUninstall(values, operations, dependencies, output, progress);
120
127
  return 0;
121
128
  }],
122
129
  ["discover", async (values, { operations, output }) => {
@@ -162,7 +169,7 @@ const COMMANDS = new Map([
162
169
  return 0;
163
170
  }],
164
171
  ]);
165
- async function runSourceRefresh(values, { operations, output }) {
172
+ async function runSourceRefresh(values, { operations, output, progress }) {
166
173
  if (values.length < 1 || values.length > 2 || (values.length === 2 && values[1] !== "--yes")) {
167
174
  throw new CliUsageError("Usage: agent-depot source refresh <id> [--yes]");
168
175
  }
@@ -174,13 +181,21 @@ async function runSourceRefresh(values, { operations, output }) {
174
181
  if (values[1] !== "--yes") {
175
182
  throw new CliUsageError("Refresh not confirmed; rerun with --yes to continue");
176
183
  }
177
- const refreshed = await operations.refreshSource(source.id);
184
+ // `git clone`/`fetch` may ask for credentials on the terminal, so this step never animates.
185
+ const refreshed = await progress.run(`Refreshing Git Source ${source.id}`, () => operations.refreshSource(source.id), { mayPrompt: true });
178
186
  output(`Refreshed Git Source: ${refreshed.id}\t${refreshed.url}`);
179
187
  }
180
188
  /** Runs the CLI application and returns a process exit code. */
181
- export async function runCli(argv = process.argv.slice(2), dependencies = {}) {
182
- const output = dependencies.stdout ?? ((line) => console.log(line));
183
- const errorOutput = dependencies.stderr ?? ((line) => console.error(line));
189
+ export async function runCli(rawArgv = process.argv.slice(2), dependencies = {}) {
190
+ // A global flag, so every command's own option parser never sees it.
191
+ const verbose = rawArgv.includes("--verbose");
192
+ const argv = verbose ? rawArgv.filter((argument) => argument !== "--verbose") : rawArgv;
193
+ const progress = dependencies.progress ?? (dependencies.stderr === undefined ? createProgress({ verbose, tapChildOutput: withChildOutput }) : NO_PROGRESS);
194
+ // Every line is written after erasing any spinner, so a step that prints while it runs is never garbled.
195
+ const writeStdout = dependencies.stdout ?? ((line) => console.log(line));
196
+ const writeStderr = dependencies.stderr ?? ((line) => console.error(line));
197
+ const output = (line) => { progress.clear(); writeStdout(line); };
198
+ const errorOutput = (line) => { progress.clear(); writeStderr(line); };
184
199
  const operations = dependencies.operations ?? createSourceOperations({ homeDirectory: dependencies.homeDirectory });
185
200
  try {
186
201
  if (argv[0] === "--version" || argv[0] === "-v") {
@@ -201,7 +216,7 @@ export async function runCli(argv = process.argv.slice(2), dependencies = {}) {
201
216
  if (!command) {
202
217
  throw new CliUsageError(USAGE);
203
218
  }
204
- return await command(argv.slice(1), { operations, dependencies, output });
219
+ return await command(argv.slice(1), { operations, dependencies, output, errorOutput, progress });
205
220
  }
206
221
  catch (error) {
207
222
  errorOutput(`Error: ${error instanceof Error ? error.message : "unknown error"}`);
@@ -210,7 +225,7 @@ export async function runCli(argv = process.argv.slice(2), dependencies = {}) {
210
225
  }
211
226
  /** Alias retained as a discoverable public CLI seam for embedding and tests. */
212
227
  export const main = runCli;
213
- async function runSourceRemoval(argv, operations, dependencies, output) {
228
+ async function runSourceRemoval(argv, operations, dependencies, output, progress) {
214
229
  const options = parseRemoveSourceOptions(argv);
215
230
  const source = await findSource(operations, options.sourceId);
216
231
  if (source.kind === "builtin") {
@@ -232,17 +247,19 @@ async function runSourceRemoval(argv, operations, dependencies, output) {
232
247
  output(` [${index}] ${selection.path} -> ${location}${selected.includes(selection) ? " (selected for removal)" : " (kept)"}`);
233
248
  }
234
249
  output(" Default: keep all dependent Skills tracked by default after Source removal");
235
- const plan = await inspectSkillRemovals(selected, removalOptions, (selection, inspection) => {
250
+ const plan = await progress.run(`Inspecting ${selected.length} Skill${selected.length === 1 ? "" : "s"} for removal`, () => inspectSkillRemovals(selected, removalOptions, (selection, inspection) => {
236
251
  for (const line of describeSkillRemoval(selection, inspection, "Skill "))
237
252
  output(line);
238
- });
253
+ }));
239
254
  assertNoOverlappingProjectSkillRemovalTargets(plan.inspections);
240
255
  requireConfirmation(options.confirmed, "Removal not confirmed; rerun with --yes after reviewing every Source and Skill preview");
241
256
  // Recheck every selected target before deleting the first one. A changed later
242
257
  // target must never result in a partially removed batch.
243
- const rechecked = await recheckSkillRemoval(plan, removalOptions);
244
- await removeSkillFiles(plan, rechecked, removalOptions);
245
- await operations.removeGitSource(source.id, selected);
258
+ await progress.run("Removing Skill files", async () => {
259
+ const rechecked = await recheckSkillRemoval(plan, removalOptions);
260
+ await removeSkillFiles(plan, rechecked, removalOptions);
261
+ });
262
+ await progress.run(`Removing Git Source ${source.id}`, () => operations.removeGitSource(source.id, selected));
246
263
  output(`Removed Git Source: ${source.id}`);
247
264
  if (selected.length > 0) {
248
265
  output(`Removed ${selected.length} dependent user-global Skill${selected.length === 1 ? "" : "s"}; other dependent Skills remain tracked`);
@@ -418,7 +435,7 @@ function resolveRemovalSelections(dependent, requested) {
418
435
  }
419
436
  return Object.freeze(selected);
420
437
  }
421
- async function runSkillRemoval(argv, operations, dependencies, output) {
438
+ async function runSkillRemoval(argv, operations, dependencies, output, progress) {
422
439
  const requested = [];
423
440
  const hosts = [];
424
441
  let confirmed = false;
@@ -446,7 +463,7 @@ async function runSkillRemoval(argv, operations, dependencies, output) {
446
463
  const selected = selectManagedSkills(installations, requested);
447
464
  const removalOptions = userGlobalRemovalOptions(path.resolve(dependencies.homeDirectory ?? homedir()), dependencies.installationOptions);
448
465
  if (hosts.length > 0) {
449
- await runSkillHostRemoval(selected[0], hosts, installations, confirmed, operations, removalOptions, output);
466
+ await runSkillHostRemoval(selected[0], hosts, installations, confirmed, operations, removalOptions, output, progress);
450
467
  return;
451
468
  }
452
469
  output(`Preview: remove ${selected.length} user-global Skill${selected.length === 1 ? "" : "s"}`);
@@ -455,7 +472,7 @@ async function runSkillRemoval(argv, operations, dependencies, output) {
455
472
  output(line);
456
473
  output(" Installation records are reconciled after each successful Skill removal");
457
474
  requireConfirmation(confirmed, "Removal not confirmed; rerun with --yes after reviewing every Skill preview");
458
- await executeSkillRemoval(plan, operations, removalOptions, installations);
475
+ await progress.run(`Removing ${selected.length} user-global Skill${selected.length === 1 ? "" : "s"}`, () => executeSkillRemoval(plan, operations, removalOptions, installations));
459
476
  output(`Removed ${selected.length} user-global Skill${selected.length === 1 ? "" : "s"}`);
460
477
  }
461
478
  /** Resolves `<id|path>` arguments to managed installations, reporting selection problems as usage errors. */
@@ -469,7 +486,7 @@ function selectManagedSkills(installations, requested) {
469
486
  throw error;
470
487
  }
471
488
  }
472
- async function runSkillHostRemoval(selection, hosts, installations, confirmed, operations, options, output) {
489
+ async function runSkillHostRemoval(selection, hosts, installations, confirmed, operations, options, output, progress) {
473
490
  const plan = await planHostRemoval(selection, hosts, options).catch(rethrowSelectionError);
474
491
  output(plan.kind === "full"
475
492
  ? "Preview: remove 1 user-global Skill (every recorded Host is removed)"
@@ -478,12 +495,12 @@ async function runSkillHostRemoval(selection, hosts, installations, confirmed, o
478
495
  output(line);
479
496
  output(" The installation record is reconciled after the locations are removed");
480
497
  requireConfirmation(confirmed, "Removal not confirmed; rerun with --yes after reviewing the preview");
481
- await executeHostRemoval(plan, operations, options, installations);
498
+ await progress.run(plan.kind === "full" ? "Removing 1 user-global Skill" : `Removing Hosts from ${selection.path}`, () => executeHostRemoval(plan, operations, options, installations));
482
499
  output(plan.kind === "full"
483
500
  ? "Removed 1 user-global Skill"
484
501
  : `Removed hosts ${plan.removed.join(", ")} from ${JSON.stringify(selection.path)}`);
485
502
  }
486
- async function runSkillHostAdd(argv, operations, dependencies, output) {
503
+ async function runSkillHostAdd(argv, operations, dependencies, output, progress) {
487
504
  const requested = [];
488
505
  const hosts = [];
489
506
  let confirmed = false;
@@ -520,7 +537,7 @@ async function runSkillHostAdd(argv, operations, dependencies, output) {
520
537
  output(line);
521
538
  requireConfirmation(confirmed, "Host change not confirmed; rerun with --yes after reviewing the preview");
522
539
  requireConfirmation(confirmAdditionalHost, "Adding a Host exposes the installed Skill to more hosts; rerun with --yes --confirm-additional-host");
523
- await executeHostAddition(plan, operations, options, installations, true);
540
+ await progress.run(`Adding Hosts to ${plan.selection.path}`, () => executeHostAddition(plan, operations, options, installations, true));
524
541
  output(`Added hosts ${plan.hosts.join(", ")} to ${JSON.stringify(plan.selection.path)}`);
525
542
  }
526
543
  function rethrowSelectionError(error) {
@@ -662,7 +679,7 @@ function outputUninstallSummary(options, removedManagedCount, output) {
662
679
  output(" npx/pnpm dlx runs need no uninstall (only a cache entry)");
663
680
  }
664
681
  }
665
- async function runUninstall(argv, operations, dependencies, output) {
682
+ async function runUninstall(argv, operations, dependencies, output, progress) {
666
683
  const options = parseUninstallOptions(argv);
667
684
  assertUninstallCapabilities(options, operations);
668
685
  const homeDirectory = path.resolve(dependencies.homeDirectory ?? homedir());
@@ -685,7 +702,8 @@ async function runUninstall(argv, operations, dependencies, output) {
685
702
  if (options.skills || options.unmanagedSkills.length > 0 || options.data) {
686
703
  requireConfirmation(options.confirmed, "Uninstall not confirmed; rerun with --yes after reviewing every user-global deletion preview");
687
704
  }
688
- const managedRechecked = await recheckSkillRemoval(managedPlan, removalOptions);
705
+ const recheckManaged = () => recheckSkillRemoval(managedPlan, removalOptions);
706
+ const managedRechecked = options.skills ? await progress.run("Rechecking Skills before removal", recheckManaged) : await recheckManaged();
689
707
  const rechecked = managedRechecked.inspections;
690
708
  const unmanagedRechecked = [];
691
709
  for (const inspection of unmanagedInspections) {
@@ -697,11 +715,13 @@ async function runUninstall(argv, operations, dependencies, output) {
697
715
  assertNoOverlappingUserGlobalSkillRemovals(unmanagedRechecked);
698
716
  assertNoManagedUnmanagedOverlap(rechecked, unmanagedRechecked, () => "An unmanaged Skill target changed into an overlap with a selected managed Skill; no path was changed");
699
717
  const expectedManagedInstallations = options.skills
700
- ? await removeSkillsAndRecords(managedPlan, managedRechecked, operations, removalOptions, selectedInstallations)
718
+ ? await progress.run(`Removing ${selectedInstallations.length} managed Skill${selectedInstallations.length === 1 ? "" : "s"}`, () => removeSkillsAndRecords(managedPlan, managedRechecked, operations, removalOptions, selectedInstallations))
701
719
  : selectedInstallations;
702
- await removeInspectedUnmanagedSkills(context, unmanagedInspections, expectedManagedInstallations);
720
+ if (unmanagedInspections.length > 0) {
721
+ await progress.run(`Removing ${unmanagedInspections.length} unmanaged Skill${unmanagedInspections.length === 1 ? "" : "s"}`, () => removeInspectedUnmanagedSkills(context, unmanagedInspections, expectedManagedInstallations));
722
+ }
703
723
  if (options.data) {
704
- await operations.removeCatalogData();
724
+ await progress.run("Removing catalog data", () => operations.removeCatalogData());
705
725
  }
706
726
  outputUninstallSummary(options, selectedInstallations.length, output);
707
727
  }
@@ -717,13 +737,13 @@ function selfUpdateOperationsFor(dependencies) {
717
737
  packageRoot: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."),
718
738
  });
719
739
  }
720
- async function runSelfUpdate(argv, dependencies, output) {
740
+ async function runSelfUpdate(argv, dependencies, output, progress) {
721
741
  if (argv.length > 1 || (argv.length === 1 && argv[0] !== "--yes")) {
722
742
  throw new CliUsageError("Usage: agent-depot self update [--yes]");
723
743
  }
724
744
  const confirmed = argv[0] === "--yes";
725
745
  const selfUpdate = selfUpdateOperationsFor(dependencies);
726
- const status = await selfUpdate.check();
746
+ const status = await progress.run("Checking for Agent Depot updates", () => selfUpdate.check());
727
747
  switch (status.kind) {
728
748
  case "unsupported":
729
749
  output(`Self-update is unavailable: ${status.reason}`);
@@ -744,7 +764,7 @@ async function runSelfUpdate(argv, dependencies, output) {
744
764
  output(` command: npm install --global --no-audit --no-fund --no-update-notifier --logs-max=0 --ignore-scripts --registry=https://registry.npmjs.org/ ${AGENT_DEPOT_PACKAGE_NAME ?? "@iseoane/agent-depot"}@${status.latest}`);
745
765
  output(" npm package scripts will not run; no elevated privileges will be requested");
746
766
  requireConfirmation(confirmed, "Self-update not confirmed; rerun with --yes after reviewing the preview");
747
- await selfUpdate.apply(status.latest);
767
+ await progress.run(`Updating Agent Depot to ${status.latest}`, () => selfUpdate.apply(status.latest));
748
768
  output(`Updated Agent Depot to ${status.latest}; restart the CLI to use the new version`);
749
769
  return 0;
750
770
  default: {
@@ -753,7 +773,7 @@ async function runSelfUpdate(argv, dependencies, output) {
753
773
  }
754
774
  }
755
775
  }
756
- async function runUpdate(argv, operations, dependencies, output) {
776
+ async function runUpdate(argv, operations, dependencies, output, progress) {
757
777
  const options = parseUpdateOptions(argv);
758
778
  const apps = dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory });
759
779
  const packages = packageOperationsFor(dependencies, operations);
@@ -766,14 +786,14 @@ async function runUpdate(argv, operations, dependencies, output) {
766
786
  let packageCheckFailed = false;
767
787
  if (options.scope === "user-global") {
768
788
  try {
769
- appUpdates = await checkAppUpdates(apps);
789
+ appUpdates = await progress.run("Checking App updates", () => checkAppUpdates(apps));
770
790
  }
771
791
  catch (error) {
772
792
  appCheckFailed = true;
773
793
  output(`Error (Apps): ${error instanceof Error ? error.message : String(error)}`);
774
794
  }
775
795
  try {
776
- packageUpdates = await packages.checkUpdates(requestedPackages.length === 0 ? undefined : requestedPackages);
796
+ packageUpdates = await progress.run("Checking Package updates", () => packages.checkUpdates(requestedPackages.length === 0 ? undefined : requestedPackages));
777
797
  }
778
798
  catch (error) {
779
799
  packageCheckFailed = true;
@@ -782,7 +802,7 @@ async function runUpdate(argv, operations, dependencies, output) {
782
802
  }
783
803
  let loaded;
784
804
  try {
785
- loaded = await loadUpdates({
805
+ loaded = await progress.run(`Checking ${options.scope} Skill updates`, () => loadUpdates({
786
806
  scope: options.scope,
787
807
  operations,
788
808
  ...(dependencies.homeDirectory === undefined ? {} : { homeDirectory: dependencies.homeDirectory }),
@@ -790,7 +810,7 @@ async function runUpdate(argv, operations, dependencies, output) {
790
810
  ...(dependencies.sourceAccess === undefined ? {} : { sourceAccess: dependencies.sourceAccess }),
791
811
  ...(dependencies.projectManifestStore === undefined ? {} : { projectManifestStore: dependencies.projectManifestStore }),
792
812
  ...(dependencies.installationOptions?.fileSystem === undefined ? {} : { installationFileSystem: dependencies.installationOptions.fileSystem }),
793
- });
813
+ }));
794
814
  }
795
815
  catch (error) {
796
816
  output(`Error (Skills, ${options.scope}): ${error instanceof Error ? error.message : String(error)}`);
@@ -846,7 +866,7 @@ async function runUpdate(argv, operations, dependencies, output) {
846
866
  }
847
867
  for (const item of selectedPackages) {
848
868
  try {
849
- const plan = await packages.planLifecycle(item.installed.selection, "update");
869
+ const plan = await planWithRefresh(packages, item.installed.selection, "update", progress);
850
870
  const approval = await packages.approvalStatus(item.installed.selection, "update");
851
871
  describeLifecyclePlan(plan, approval, output);
852
872
  if (approval === "approved") {
@@ -863,7 +883,9 @@ async function runUpdate(argv, operations, dependencies, output) {
863
883
  }
864
884
  }
865
885
  const selected = selectUpdateCandidates(assessment, options);
866
- const { plans, failures: previewFailures, nonCanonicalPaths } = await previewUpdateCandidates(selected, context);
886
+ const { plans, failures: previewFailures, nonCanonicalPaths } = selected.length === 0
887
+ ? await previewUpdateCandidates(selected, context)
888
+ : await progress.run("Previewing Skill updates", () => previewUpdateCandidates(selected, context));
867
889
  // Successes and failures are reported in candidate order, as the preview runs.
868
890
  const failedIds = new Set(previewFailures.map(({ item }) => item.id));
869
891
  const plansById = new Map(plans.map((plan) => [plan.item.id, plan]));
@@ -882,7 +904,9 @@ async function runUpdate(argv, operations, dependencies, output) {
882
904
  const pathProblem = findPathConfirmationProblem(nonCanonicalPaths, options.confirmedPaths);
883
905
  if (pathProblem !== undefined)
884
906
  throw new CliUsageError(pathProblem);
885
- const result = await applyConfirmedUpdates(selected, context, options.confirmedPaths);
907
+ const result = selected.length === 0
908
+ ? await applyConfirmedUpdates(selected, context, options.confirmedPaths)
909
+ : await progress.run(`Updating ${selected.length} Skill${selected.length === 1 ? "" : "s"}`, () => applyConfirmedUpdates(selected, context, options.confirmedPaths));
886
910
  for (const { item } of previewFailures) {
887
911
  if (!result.failed.some((failed) => failed.id === item.id)) {
888
912
  output(`Preview failure was not selected for application: ${JSON.stringify(item.id)}`);
@@ -897,7 +921,7 @@ async function runUpdate(argv, operations, dependencies, output) {
897
921
  let appUpdated = 0;
898
922
  for (const plan of appPlans) {
899
923
  try {
900
- const outcome = await apps.executeLifecycle(plan, true);
924
+ const outcome = await progress.run(`Updating App ${plan.entry.recipe.name}`, () => apps.executeLifecycle(plan, true));
901
925
  output(`App ${plan.entry.recipe.name}: ${describeAppResult(outcome)}${outcome.reason ? `: ${outcome.reason}` : ""}`);
902
926
  if (outcome.status === "installed")
903
927
  appUpdated++;
@@ -917,7 +941,7 @@ async function runUpdate(argv, operations, dependencies, output) {
917
941
  let packageUpdated = 0;
918
942
  for (const plan of packagePlans) {
919
943
  try {
920
- const outcome = await packages.executeLifecycle(plan, true);
944
+ const outcome = await progress.run(`Updating Package ${formatPackageSelection(plan.selection)}`, () => packages.executeLifecycle(plan, true));
921
945
  output(`Package ${formatPackageSelection(plan.selection)}: ${describePackageResult(outcome)}`);
922
946
  if (outcome.status === "updated") {
923
947
  packageUpdated++;
@@ -1097,16 +1121,16 @@ function outputUpdateItem(item, output) {
1097
1121
  const selection = item.selection;
1098
1122
  output(` [${item.index}] ${item.id}${typeof selection?.path === "string" ? ` path=${selection.path}` : ""}: ${item.reason}`);
1099
1123
  }
1100
- async function runInstall(argv, operations, dependencies, output) {
1124
+ async function runInstall(argv, operations, dependencies, output, progress) {
1101
1125
  const options = parseInstallOptions(argv);
1102
1126
  const sourceAccess = dependencies.sourceAccess ?? defaultProjectSkillTreeAccess();
1103
1127
  if (options.scope === "user-global") {
1104
- await runUserGlobalInstall(options, operations, dependencies, sourceAccess, output);
1128
+ await runUserGlobalInstall(options, operations, dependencies, sourceAccess, output, progress);
1105
1129
  return;
1106
1130
  }
1107
1131
  const projectRoot = path.resolve(dependencies.projectRoot ?? process.cwd());
1108
1132
  const manifestStore = dependencies.projectManifestStore ?? new ProjectManifestStore(defaultProjectManifestPath(projectRoot));
1109
- const context = { options, operations, dependencies, sourceAccess, output, projectRoot, manifestStore };
1133
+ const context = { options, operations, dependencies, sourceAccess, output, progress, projectRoot, manifestStore };
1110
1134
  if (options.fromManifest) {
1111
1135
  await runManifestInstall(context);
1112
1136
  return;
@@ -1114,7 +1138,7 @@ async function runInstall(argv, operations, dependencies, output) {
1114
1138
  await runSingleProjectInstall(context);
1115
1139
  }
1116
1140
  async function runManifestInstall(context) {
1117
- const { options, operations, dependencies, sourceAccess, output, projectRoot, manifestStore } = context;
1141
+ const { options, operations, dependencies, sourceAccess, output, progress, projectRoot, manifestStore } = context;
1118
1142
  const manifest = await manifestStore.load();
1119
1143
  if (manifest.skills.length === 0) {
1120
1144
  throw new CliUsageError("The project manifest contains no selected Skills");
@@ -1141,7 +1165,7 @@ async function runManifestInstall(context) {
1141
1165
  if (skipped > 0) {
1142
1166
  output(`Skipped ${skipped} of ${manifest.skills.length} Skills pinned to another Agent Depot version`);
1143
1167
  }
1144
- const resolved = await resolveManifestSelections(installable, operations, sourceAccess, options.confirmed);
1168
+ const resolved = await progress.run(`Resolving ${installable.length} manifest Skill${installable.length === 1 ? "" : "s"}`, () => resolveManifestSelections(installable, operations, sourceAccess, options.confirmed), { mayPrompt: options.confirmed && installable.some((selection) => selection.source.kind !== "builtin") });
1145
1169
  if (resolved.some((item) => item.method !== undefined) && resolved.length > 1) {
1146
1170
  throw new SourceMetadataError("A batch install containing an external method is refused because command side effects cannot be rolled back safely; install that Skill separately");
1147
1171
  }
@@ -1163,7 +1187,7 @@ async function runManifestInstall(context) {
1163
1187
  try {
1164
1188
  for (let index = 0; index < resolved.length; index += 1) {
1165
1189
  const item = resolved[index];
1166
- const transaction = await installOne(item.selection, item.source, sourceAccess, projectRoot, dependencies, item.method, options.portableV1, item.files, item.resolvedVersion, options.confirmAdditionalHostExposure, false, inspections[index]);
1190
+ const transaction = await progress.run(`Installing ${item.selection.path}`, () => installOne(item.selection, item.source, sourceAccess, projectRoot, dependencies, item.method, options.portableV1, item.files, item.resolvedVersion, options.confirmAdditionalHostExposure, false, inspections[index]));
1167
1191
  if (transaction) {
1168
1192
  transactions.push(transaction);
1169
1193
  installed.push({ item, result: transaction.result });
@@ -1198,20 +1222,21 @@ async function runManifestInstall(context) {
1198
1222
  }
1199
1223
  }
1200
1224
  async function runSingleProjectInstall(context) {
1201
- const { options, operations, dependencies, sourceAccess, output, projectRoot, manifestStore } = context;
1225
+ const { options, operations, dependencies, sourceAccess, output, progress, projectRoot, manifestStore } = context;
1202
1226
  if (!options.sourceId || !options.skillPath || !options.version || options.hosts.length === 0) {
1203
1227
  throw new CliUsageError(INSTALL_USAGE);
1204
1228
  }
1205
1229
  const existing = await manifestStore.load();
1206
- const plan = await planSingleInstall({ ...options, scope: "project", sourceId: options.sourceId, skillPath: options.skillPath, version: options.version }, { operations, sourceAccess, environment: dependencies, root: projectRoot, existing: existing.skills, recordedIn: manifestStore.path });
1230
+ const request = { ...options, scope: "project", sourceId: options.sourceId, skillPath: options.skillPath, version: options.version };
1231
+ const plan = await progress.run(`Resolving ${options.skillPath}`, () => planSingleInstall(request, { operations, sourceAccess, environment: dependencies, root: projectRoot, existing: existing.skills, recordedIn: manifestStore.path }), { mayPrompt: refreshesSource(options) });
1207
1232
  outputSingleInstallPreview(plan, output);
1208
1233
  requireConfirmation(options.confirmed, "Installation not confirmed; rerun with --yes to continue");
1209
- await executeSingleInstall(plan, async (record) => {
1234
+ await progress.run(`Installing ${options.skillPath}`, () => executeSingleInstall(plan, async (record) => {
1210
1235
  await manifestStore.save(parseProjectManifest({ version: 1, skills: [...existing.skills, record] }));
1211
- }, output);
1236
+ }, output));
1212
1237
  output(`Saved project manifest: ${manifestStore.path}`);
1213
1238
  }
1214
- async function runUserGlobalInstall(options, operations, dependencies, sourceAccess, output) {
1239
+ async function runUserGlobalInstall(options, operations, dependencies, sourceAccess, output, progress) {
1215
1240
  if (options.fromManifest || !options.sourceId || !options.skillPath || !options.version || options.hosts.length === 0) {
1216
1241
  throw new CliUsageError(INSTALL_USAGE);
1217
1242
  }
@@ -1220,12 +1245,18 @@ async function runUserGlobalInstall(options, operations, dependencies, sourceAcc
1220
1245
  throw new Error("Configured Source operations cannot manage user-global installation state");
1221
1246
  }
1222
1247
  const homeDirectory = path.resolve(dependencies.homeDirectory ?? homedir());
1223
- const plan = await planSingleInstall({ ...options, scope: "user-global", sourceId: options.sourceId, skillPath: options.skillPath, version: options.version }, { operations, sourceAccess, environment: dependencies, root: homeDirectory, existing: await listUserGlobalInstallations.call(operations), recordedIn: "user-global state" });
1248
+ const existing = await listUserGlobalInstallations.call(operations);
1249
+ const request = { ...options, scope: "user-global", sourceId: options.sourceId, skillPath: options.skillPath, version: options.version };
1250
+ const plan = await progress.run(`Resolving ${options.skillPath}`, () => planSingleInstall(request, { operations, sourceAccess, environment: dependencies, root: homeDirectory, existing, recordedIn: "user-global state" }), { mayPrompt: refreshesSource(options) });
1224
1251
  outputSingleInstallPreview(plan, output);
1225
1252
  requireConfirmation(options.confirmed, "Installation not confirmed; rerun with --yes to continue");
1226
- await executeSingleInstall(plan, (record) => addUserGlobalInstallation.call(operations, record), output);
1253
+ await progress.run(`Installing ${options.skillPath}`, () => executeSingleInstall(plan, (record) => addUserGlobalInstallation.call(operations, record), output));
1227
1254
  output("Saved user-global installation state: shared Source state");
1228
1255
  }
1256
+ /** A confirmed install of a non-built-in Source refreshes it with `git`, which may ask for credentials. */
1257
+ function refreshesSource(options) {
1258
+ return options.confirmed && options.sourceId !== BUILT_IN_SOURCE.id;
1259
+ }
1229
1260
  /** Appends the comma-separated `--host` value, rejecting unknown or repeated Hosts. */
1230
1261
  function collectHosts(hosts, value) {
1231
1262
  for (const host of value.split(",")) {
@@ -25,9 +25,15 @@ export function createPortableExecutableResolver(options = {}) {
25
25
  return { executable, blocked: target !== undefined && isWindowsExecutableOnWsl(target, wslEnvironment) };
26
26
  }
27
27
  let blockedExecutable;
28
- const suffixes = platform === "win32"
29
- ? ["", ...(process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")]
30
- : [""];
28
+ // On Windows only PATHEXT candidates are runnable: npm installs an
29
+ // extensionless sh shim beside `name.cmd`, and spawning it fails (ENOENT).
30
+ const configuredExtensions = (process.env.PATHEXT ?? "").split(";").map((extension) => extension.trim()).filter(Boolean);
31
+ const extensions = configuredExtensions.length > 0 ? configuredExtensions : [".COM", ".EXE", ".BAT", ".CMD"];
32
+ const suffixes = platform !== "win32"
33
+ ? [""]
34
+ : extensions.some((extension) => name.toLowerCase().endsWith(extension.toLowerCase()))
35
+ ? [""]
36
+ : extensions;
31
37
  for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
32
38
  if (!path.isAbsolute(directory)) {
33
39
  continue;
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { lstat, mkdir, rename, rm, rmdir } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
+ import { isLockContention } from "./atomic-file.js";
5
6
  import { assertNoSymlinkPath } from "./path-safety.js";
6
7
  import { runProcess } from "./process-runner.js";
7
8
  export const SKILL_TREE_LIMITS = Object.freeze({
@@ -424,7 +425,7 @@ async function acquireSourceLock(lockPath) {
424
425
  };
425
426
  }
426
427
  catch (error) {
427
- if (!isAlreadyExists(error)) {
428
+ if (!isLockContention(error)) {
428
429
  throw error;
429
430
  }
430
431
  if ((await pathType(lockPath)) === "symlink") {
@@ -481,9 +482,6 @@ function isSafeSourcePath(candidate) {
481
482
  function isMissing(error) {
482
483
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
483
484
  }
484
- function isAlreadyExists(error) {
485
- return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
486
- }
487
485
  const GIT_PROTOCOLS = new Set(["git:", "http:", "https:", "ssh:"]);
488
486
  export function canonicalizeGitSourceUrl(input) {
489
487
  if (typeof input !== "string" || input.length === 0 || input.trim() !== input) {
@@ -1,9 +1,10 @@
1
1
  import { sourceIdForUrl } from "./git-source.js";
2
2
  import { PACKAGE_HOSTS, bundleRoot, describeCoordinates, selectionKey } from "./package-model.js";
3
+ import { NO_PROGRESS } from "./progress.js";
3
4
  import { projectSourceOf } from "./sources.js";
4
5
  import { CliUsageError } from "./usage-error.js";
5
6
  export const PACKAGE_USAGE = "Usage: agent-depot package list; package discover <source-id>; package inspect <selection> --host <host>; package approve <selection> --host <host> [--action <install|update|uninstall>]; package install|update|uninstall <selection> --host <host> [--yes]; package forget <selection> --host <host> --yes";
6
- export async function runPackageCommand(values, packages, sources, output) {
7
+ export async function runPackageCommand(values, packages, sources, output, progress = NO_PROGRESS) {
7
8
  const [command, ...args] = values;
8
9
  if (command === "list") {
9
10
  requireNoArgs(args, "package list");
@@ -35,7 +36,7 @@ export async function runPackageCommand(values, packages, sources, output) {
35
36
  throw new CliUsageError("Package approval only applies to install, update, and uninstall actions");
36
37
  }
37
38
  const selection = await resolveSelection(parsed, packages, sources);
38
- const plan = await packages.planLifecycle(selection, action);
39
+ const plan = await planWithRefresh(packages, selection, action, progress);
39
40
  describeLifecyclePlan(plan, await packages.approvalStatus(selection, action), output);
40
41
  await packages.approve(selection, action);
41
42
  output(`Approved Package ${action}: ${formatPackageSelection(selection)}`);
@@ -44,7 +45,7 @@ export async function runPackageCommand(values, packages, sources, output) {
44
45
  if (command === "install" || command === "update" || command === "uninstall") {
45
46
  const parsed = parseSelectionArgs(args, { allowConfirmation: true });
46
47
  const selection = await resolveSelection(parsed, packages, sources);
47
- const plan = await packages.planLifecycle(selection, command);
48
+ const plan = await planWithRefresh(packages, selection, command, progress);
48
49
  const approval = await packages.approvalStatus(selection, command);
49
50
  describeLifecyclePlan(plan, approval, output);
50
51
  if (approval !== "approved") {
@@ -53,7 +54,7 @@ export async function runPackageCommand(values, packages, sources, output) {
53
54
  if (!parsed.confirmed) {
54
55
  throw new Error(`Package ${command} not confirmed; rerun with --yes after reviewing the preview`);
55
56
  }
56
- const result = await packages.executeLifecycle(plan, true);
57
+ const result = await progress.run(`${command === "install" ? "Installing" : command === "update" ? "Updating" : "Uninstalling"} Package ${formatPackageSelection(selection)}`, () => packages.executeLifecycle(plan, true));
57
58
  output(`Package ${formatPackageSelection(selection)}: ${describePackageResult(result)}`);
58
59
  return packageResultSucceeded(result) ? 0 : 1;
59
60
  }
@@ -71,6 +72,10 @@ export async function runPackageCommand(values, packages, sources, output) {
71
72
  }
72
73
  throw new CliUsageError(PACKAGE_USAGE);
73
74
  }
75
+ /** Planning refreshes the Package's Git Source (ADR 0004), and git may ask for credentials, so the step never animates. */
76
+ export function planWithRefresh(packages, selection, action, progress) {
77
+ return progress.run(`Refreshing Source for Package ${formatPackageSelection(selection)}`, () => packages.planLifecycle(selection, action), { mayPrompt: true });
78
+ }
74
79
  export function formatPackageSelection(selection) {
75
80
  return `${sourceToken(selection.source)}::${describeSelectionCoordinate(selection)}`;
76
81
  }
@@ -357,13 +357,14 @@ export function createPackageOperations(environment = {}) {
357
357
  result = await runner(resolution.executable, command.args, {
358
358
  cwd: plan.cwd,
359
359
  captureStdout: true,
360
+ streamStdout: true,
360
361
  maxOutputBytes: PACKAGE_OUTPUT_LIMIT,
361
362
  maxStderrBytes: PACKAGE_OUTPUT_LIMIT,
362
363
  });
363
364
  }
364
365
  catch (error) {
365
366
  const code = error?.code;
366
- if (code === undefined || !["ENOENT", "EACCES", "ENOEXEC", "EPERM"].includes(code)) {
367
+ if (code === undefined || !["ENOENT", "EACCES", "ENOEXEC", "EPERM", "EINVAL"].includes(code)) {
367
368
  throw error;
368
369
  }
369
370
  const detail = error instanceof Error ? error.message : String(error);