@iseoane/agent-depot 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +186 -1
  3. package/dist/src/app-cli.js +172 -0
  4. package/dist/src/app-flow.js +464 -0
  5. package/dist/src/app-latest.js +63 -0
  6. package/dist/src/app-owned-skills.js +42 -0
  7. package/dist/src/app-recipes.js +116 -0
  8. package/dist/src/app-schema.js +45 -0
  9. package/dist/src/app-version.js +46 -0
  10. package/dist/src/atomic-file.js +26 -0
  11. package/dist/src/cli.js +117 -20
  12. package/dist/src/process-runner.js +10 -1
  13. package/dist/src/profile-cli.js +88 -0
  14. package/dist/src/profile-export.js +135 -0
  15. package/dist/src/profile-import.js +218 -0
  16. package/dist/src/profile.js +130 -0
  17. package/dist/src/project-manifest.js +6 -0
  18. package/dist/src/skill-install.js +12 -0
  19. package/dist/src/source-state.js +3 -28
  20. package/dist/src/tui/app-actions.js +157 -0
  21. package/dist/src/tui/app.js +10 -4
  22. package/dist/src/tui/catalog-installs.js +5 -1
  23. package/dist/src/tui/installations-browse.js +24 -2
  24. package/dist/src/tui/installations-tree.js +13 -1
  25. package/dist/src/tui/installations-view.js +46 -6
  26. package/dist/src/tui/installations.js +26 -3
  27. package/dist/src/tui/manage-actions.js +2 -0
  28. package/dist/src/tui/manage-panel.js +6 -0
  29. package/dist/src/tui/profile-view.js +249 -0
  30. package/dist/src/tui/sources-recipes.js +216 -0
  31. package/dist/src/tui/sources-view.js +16 -7
  32. package/dist/src/tui/unmanaged-actions.js +2 -2
  33. package/dist/src/tui/updates-flow.js +18 -3
  34. package/dist/src/tui/updates-keys.js +9 -0
  35. package/dist/src/tui/updates-model.js +3 -3
  36. package/dist/src/tui/updates-panel.js +4 -2
  37. package/dist/src/tui/updates-view.js +1 -1
  38. package/dist/src/tui/updates.js +76 -5
  39. package/dist/src/unmanaged-removal.js +3 -0
  40. package/dist/src/user-global-skill-inventory.js +7 -1
  41. package/package.json +4 -4
  42. package/skills/agent-depot-apprecipe/SKILL.md +44 -0
  43. package/skills/agent-depot-apprecipe/assets/example.md +25 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,38 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-10-01
9
+
10
+ ### Added
11
+
12
+ - **Profiles**: a portable, versioned `agent-depot-profile/v1` JSON file to carry a user-global setup to another Agent Depot installation (ADR 0008). It holds Git Sources, user-global Skill selections (Source, path, version policy, Hosts, methods) and App recipe contents, plus the producing `agentDepotVersion`. Local-path and built-in Sources, unmanaged and App-owned Skills, approvals and installed state are never exported; credentials in URLs and absolute paths are rejected, and output is canonically ordered.
13
+ - `agent-depot export [--out <file>]` with `--no-sources`, `--no-skills`, `--no-apps` and repeatable `--source`, `--skill`, `--app`; refuses an existing output file and an empty selection.
14
+ - `agent-depot import <file> [--yes]`: an add-only plan (add / same / conflict with per-field differences) that never removes or changes anything present. Without `--yes` nothing is written and every Skill's Hosts, version policy and method argv are shown. Imported App recipes arrive unapproved and Apps are never installed; a recipe conflicts only with a same-name recipe whose platform overlaps.
15
+ - TUI: a fifth tab **5 Import/Export** with checklists for both directions, previews, y/n confirmation and per-item results.
16
+ - TUI: an **App recipes** section in Sources (`Tab` to switch) lists every recipe with its status (approved, needs approval, invalid, not applicable), previews and approves it with Enter, and `n` shows how to add one: install the `agent-depot-apprecipe` skill from the TUI and paste the suggested prompt, or write the recipe by hand in the shown `apps/` directory.
17
+
18
+ ## [0.3.0] - 2026-10-01
19
+
20
+ ### Added
21
+
22
+ - **Apps**: user-global applications (for example Engram, CodeGraph, GitNexus) managed through user-declared JSON recipes in the per-user `apps/` directory. Agent Depot delegates the lifecycle to the App's own commands, tracks the installed version and never tracks what an App writes, clones App repositories or ships built-in recipes (ADR 0007).
23
+ - `agent-depot app schema | validate | approve | list`: recipe JSON Schema, validation per platform (WSL counts as Linux), content-hash approval required before any recipe command runs, and listing with installed version and resolved executable.
24
+ - `agent-depot app install | uninstall | update <name>` and `app setup | teardown <name> --host <host>...`: previewed and confirmed steps run without a shell from the home directory; manual steps are shown and confirmed with `--manual-done`; installed state is recorded only when `version` confirms it; `app uninstall --forget` drops tracking explicitly.
25
+ - Latest-version lookup from GitHub Releases, the npm registry or an approved argv; Apps join `update check` and `update apply` (`--app <name>`); an update succeeds only when the version changes, and semver downgrades are never offered.
26
+ - TUI: an "Apps (user-global)" group in Installations (approve, install, uninstall, Host setup/teardown, bulk marks) and App rows in the Updates batch, with lazy version checks so start-up stays fast.
27
+ - Recipe-declared `skills` names/globs exclude App-owned skills from the unmanaged adopt and remove flows.
28
+ - Built-in skill `agent-depot-apprecipe` guides an AI agent to research an App and write valid, platform-aware recipes.
29
+
30
+ ### Changed
31
+
32
+ - `runProcess` accepts an opt-in `maxStderrBytes`; Source and Skill commands keep their previous behaviour.
33
+
34
+ ### Internal
35
+
36
+ - Hermetic end-to-end coverage for Apps in the CLI and in a real terminal.
37
+ - Test processes ignore an exported `XDG_STATE_HOME`, so a developer's own state no longer affects the suite.
38
+ - Test processes ignore `FORCE_COLOR`, which `node --test` sets when run from a real terminal; the TUI tests no longer fail under `npm publish` in a TTY.
39
+
8
40
  ## [0.2.1] - 2026-09-30
9
41
 
10
42
  ### Fixed
package/README.md CHANGED
@@ -380,7 +380,7 @@ agent-depot tui
380
380
  The TUI is an [Ink](https://github.com/vadimdemedes/ink) (React for terminals)
381
381
  front end over the same operations as the CLI. It needs Node.js >= 24 and an
382
382
  interactive terminal; otherwise `tui` fails with an error. Switch views with
383
- `1`-`4`, quit with `q` (not while a prompt is open). Every list scrolls when it is
383
+ `1`-`5`, quit with `q` (not while a prompt is open). Every list scrolls when it is
384
384
  taller than the terminal. `j`/`k` or the arrow keys move.
385
385
 
386
386
  | View | Purpose |
@@ -389,6 +389,7 @@ taller than the terminal. `j`/`k` or the arrow keys move.
389
389
  | 2 Catalog | Skills that can be installed: those not installed yet, as Source, then Skills. |
390
390
  | 3 Installations | What is on disk: managed installations (user-global and project) and unmanaged user-global skills. |
391
391
  | 4 Updates | Managed skills with a newer version available, for user-global and project scope. |
392
+ | 5 Import/Export | Transfer portable user-global Source choices, Skill selections and App recipes. |
392
393
 
393
394
  Keys per view (prompts also accept `Esc` to cancel and `y`/`n` to confirm):
394
395
 
@@ -399,6 +400,8 @@ Keys per view (prompts also accept `Esc` to cancel and `y`/`n` to confirm):
399
400
  | Installations | `Enter`/arrows expand and collapse, `u` uninstall (all Hosts or chosen Hosts) or remove an unmanaged skill, `h` or `i` add Hosts to an installation, `A` or `Enter` adopt an unmanaged skill, `space` marks a leaf and `a` marks all visible leaves: with marks, `u` and `h` act on all of them with one combined preview and one confirmation (project installations are skipped with a reason) |
400
401
  | Updates | `space` mark, `a` mark all, `t`/`p`/`g` show all, project or user-global, `Enter` preview the marks, `r` check again, arrows fold the "cannot assess" line |
401
402
 
403
+ | Import/Export | `e` export, `i` import; enter a file path, `space` toggle choices, `a` toggle all, `Enter` preview, `y` apply; `j`/`k` scroll previews and results |
404
+
402
405
  Safety model:
403
406
 
404
407
  - Nothing is written before a preview and an explicit `y`. Exposing a skill to an
@@ -468,3 +471,185 @@ entry points and the module-layer boundaries (`cli` -> `application` -> `core`
468
471
  - `pnpm test:coverage`: runs the tests with built-in V8 coverage
469
472
  (`NODE_V8_COVERAGE`, source maps) into `coverage/v8`, which Fallow reads.
470
473
  It rebuilds `dist/` with source maps; no extra dependencies are needed.
474
+
475
+ ## User-global App recipes
476
+
477
+ Place user-owned JSON recipes in `apps/` next to the per-user `sources.json`
478
+ (Linux/WSL: `${XDG_STATE_HOME:-~/.local/state}/agent-depot/apps`; Windows:
479
+ `%APPDATA%\Agent Depot\apps`). Recipes are never rewritten or supplied by Sources.
480
+
481
+ ```sh
482
+ agent-depot app schema
483
+ agent-depot app validate /path/to/example.json
484
+ agent-depot app list
485
+ agent-depot app approve example --yes
486
+ ```
487
+
488
+ Validation previews every declared argv and its resolved executable but never
489
+ approves recipes (`validate --yes` is rejected). Review the preview before using
490
+ `app approve <name> --yes`: approval authorizes version commands, not just
491
+ read-only behavior. Private, atomically written approval receipts live in sibling
492
+ `app-approvals/` and are keyed to the canonical file path and exact content hash.
493
+ Changed bytes require approval again; restoring approved bytes restores approval.
494
+ `app list` never approves recipes. Invalid recipes are shown with
495
+ reasons; validation exits non-zero for invalid recipes. Other-platform recipes
496
+ are reported as not applicable and never executed. WSL counts as Linux and never
497
+ runs executables whose realpath is under `/mnt/<drive>/`; it skips those PATH
498
+ candidates while looking for Linux executables. Commands spawn the PATH candidate
499
+ rather than the realpath so shims and multicall binaries retain their identity.
500
+
501
+ Required fields are `name`, `install`, `update`, `uninstall`, and `version`.
502
+ Lifecycle steps contain `argv`, `manual`, or both; manual text is never executed.
503
+ `version` contains safe no-shell `argv` and a JavaScript regex `pattern` with one
504
+ capture group. Commands run from the user's home directory. An unsuccessful or
505
+ unresolvable version check means not installed. The schema describes structure;
506
+ `app validate` additionally enforces argv safety and regex semantics.
507
+
508
+ ### Install, update and uninstall
509
+
510
+ ```sh
511
+ agent-depot app install example # preview only; exits non-zero until confirmed
512
+ agent-depot app install example --yes
513
+ agent-depot app update example --yes
514
+ agent-depot app uninstall example --yes
515
+ agent-depot app setup example --host pi --host codex --yes
516
+ agent-depot app teardown example --host pi --yes
517
+ ```
518
+
519
+ Each step previews its argv/manual text, resolved executable, home working
520
+ directory and environment. Manual fallback occurs only when a process cannot
521
+ launch, never after a non-zero exit (reported with stdout/stderr). Run the shown
522
+ manual instructions yourself, then attest completion without rerunning argv:
523
+
524
+ ```sh
525
+ agent-depot app install example --manual-done --yes
526
+ agent-depot app uninstall example --manual-done --yes
527
+ ```
528
+
529
+ `--yes` alone does not attest manual completion. Install records a version only
530
+ when the version check succeeds. Uninstall retains tracking while version still
531
+ succeeds; `agent-depot app uninstall example --forget --yes` explicitly removes
532
+ tracking without running any recipe command, even if the recipe is gone.
533
+ Atomic per-App records live in sibling `app-installations/`. Uninstall does not
534
+ implicitly run Host teardown.
535
+
536
+ `app list` and `update check --scope user-global` resolve latest versions from
537
+ GitHub Releases, npm, or approved `latest.argv`. Missing or failed lookups are
538
+ unknown, never offered as updates. Public HTTP requests send no credentials,
539
+ reject redirects, cap responses at 1 MiB and time out after five seconds.
540
+
541
+ ```sh
542
+ agent-depot update apply --scope user-global --all --yes
543
+ agent-depot update apply --scope user-global --skill 0 --app example --yes
544
+ agent-depot app update example --manual-done --yes
545
+ ```
546
+
547
+ `--all` includes Apps and Skills; repeat `--app <name>` to select Apps alongside
548
+ Skills. App updates run **after Skills** in the same batch. Apps are user-global
549
+ only; `--all --scope project` skips Apps. Every selected step is previewed before the
550
+ batch confirmation; independent failures do not stop the remaining updates.
551
+ Manual batch outcomes are completed separately using `app update --manual-done`.
552
+ Updates record the version confirmed by the recipe's version command, not the
553
+ registry response. Updates require a changed version; unchanged versions fail
554
+ without rewriting installed tracking. Leading `v` and SemVer build-metadata-only
555
+ changes count as unchanged, just as in availability checks. Successful results show
556
+ `installed <old> -> <new> (latest <latest>)`, even when new differs from latest.
557
+ A confirmed manual update preview/fallback saves private version evidence in
558
+ sibling `app-pending-updates/`, so a later `--manual-done` invocation can compare
559
+ against the pre-update version. An attested manual completion consumes this
560
+ evidence even on failure; get a fresh confirmed preview before retrying.
561
+
562
+ Strict SemVer comparison ignores a leading `v`, orders prereleases and does not
563
+ offer a downgrade when installed is at or ahead of latest. Non-SemVer versions
564
+ use opaque inequality and **may offer a downgrade**. GitHub `/releases/latest`
565
+ excludes drafts and prereleases; npm uses package metadata's `dist-tags.latest`.
566
+ `app list` checks Apps concurrently, preserving listing order. One App check
567
+ failure does not hide other Apps or Skill updates. Lookup failures show reasons
568
+ (redirect, rate limit, HTTP status, timeout, oversized or invalid response).
569
+
570
+
571
+ App lifecycle commands are non-interactive: stdin is ignored (EOF), as in the
572
+ existing Skill command runner, and there is no execution timeout. Recipes must
573
+ use non-interactive flags; installers requiring a terminal belong in `manual`
574
+ steps. EOF lets stdin-based prompts fail rather than wait for input, but a
575
+ process ignoring EOF can still hang; cancel it manually. Captured App stdout
576
+ and stderr are each capped at 1 MiB; failures show only their last 4 KiB.
577
+
578
+ App setup/teardown accepts only Hosts declared for that action. All selected steps
579
+ are previewed before execution; each Host gets an outcome and failures do not stop
580
+ remaining Hosts. For manual completion, rerun the selected Host with
581
+ `--manual-done --yes`. This checks the App version, not Host wiring. Host actions
582
+ never change installed-App tracking.
583
+
584
+ ## Export a user-global Profile
585
+
586
+ Export portable choices to another machine or keep them in git:
587
+
588
+ ```sh
589
+ agent-depot export > profile.json
590
+ agent-depot export --out profile.json
591
+ agent-depot export --no-apps --source <source-id> --skill doctor-md-agents
592
+ agent-depot export --no-sources --no-skills --app my-tool --app another-tool
593
+ ```
594
+
595
+ Everything portable is selected by default. `--no-sources`, `--no-skills` and
596
+ `--no-apps` exclude blocks; repeatable `--source <id|url>`, `--skill <path|name>`
597
+ and `--app <name>` restrict each block independently. Unknown selections and
598
+ combining a block's inclusion filter with its `--no-*` flag are errors.
599
+
600
+ Stdout contains JSON only; stderr shows the preview and exclusion reasons.
601
+ `--out` requires a new file (existing files and symlinks are not replaced).
602
+ Profiles contain canonical Git URLs with discovery inclusion, recorded Skill
603
+ version policies/Hosts/user methods, and complete App recipes, including recipes
604
+ for other platforms. CLI export always writes `included: true`; discovery choices are transient, while
605
+ the TUI may pass explicit inclusion choices to the shared export core.
606
+ Export neither refreshes Sources nor runs App commands.
607
+
608
+ Local paths and obvious credentials are not portable. Nonportable/invalid items
609
+ are excluded with reasons, as are unmanaged/App-owned and project Skills,
610
+ installation locations/evidence, approval receipts, installed-App state and
611
+ pending updates. Free-text recipe portability checks are conservative; review
612
+ profiles before sharing them—arbitrary secrets cannot be detected automatically.
613
+
614
+ ## Import a user-global Profile
615
+
616
+ Preview first, then explicitly confirm the same selection:
617
+
618
+ ```sh
619
+ agent-depot import profile.json
620
+ agent-depot import profile.json --yes
621
+ agent-depot import profile.json --no-apps --skill doctor-md-agents --yes
622
+ ```
623
+
624
+ Import uses the same independent block/name filters as export. It reports **add**,
625
+ **same** (skipped), and **conflict** (difference shown, skipped). Without `--yes`,
626
+ nothing is written. Existing choices and conflicting files are never replaced.
627
+ A profile produced by a newer Agent Depot version gives a warning, not an error.
628
+
629
+ Confirmed additions run in order: Source registration, user-global Skills through
630
+ normal refreshed install previews and safety checks, then App recipes. Recorded
631
+ Hosts, version policies and user methods are retained. Even without `--yes`,
632
+ each add-Skill preview shows its Source, Hosts, version policy and full argv for
633
+ every user-provided method, marked as running only after confirmation. The normal
634
+ install preview still appears before execution. Each failure is reported independently and remaining items
635
+ continue; any failed item makes the command exit non-zero.
636
+
637
+ Recipes, including those for other platforms, arrive **unapproved** in `apps/`.
638
+ A same-name destination recipe blocks import only when its platform overlaps the
639
+ incoming recipe: equal platforms overlap, and a missing platform overlaps every
640
+ platform. Disjoint platform recipes can both be added. Invalid recipe JSON uses
641
+ its raw name and platform for the same check; unknown-name files are reported
642
+ with read/parse details without blocking unrelated Apps. A profile cannot contain
643
+ same-name recipes whose platform applicability overlaps. Import never installs Apps or runs
644
+ recipe commands. Review and approve applicable
645
+ recipes with `agent-depot app approve <name> --yes` before using their lifecycle
646
+ commands. Source discovery inclusion remains a transient frontend choice, not a
647
+ persisted change to an existing Source. Skills carry their own Source identity,
648
+ so omitting their Source block does not force registration; the preview names
649
+ unregistered Sources that will be fetched without registering. Existing Source
650
+ rows explain when discovery inclusion differs but is not imported.
651
+
652
+ The **5 Import/Export** TUI tab offers grouped checklists and the same shared-core
653
+ previews as the CLI. Exclusions and conflicts cannot be selected. Export creates
654
+ a new file only; empty exports write nothing (CLI reports an error). Import adds missing choices without replacing existing ones.
655
+ Imported recipes remain unapproved and Apps are never installed by import.
@@ -0,0 +1,172 @@
1
+ import { PROJECT_HOSTS } from "./project-manifest.js";
2
+ import { APP_RECIPE_SCHEMA } from "./app-schema.js";
3
+ import { checkAppUpdates } from "./app-flow.js";
4
+ import { CliUsageError } from "./usage-error.js";
5
+ export async function runAppCommand(values, apps, output) {
6
+ const [command, ...args] = values;
7
+ const usage = "Usage: agent-depot app schema|list; app validate <file>...; app approve <name> [--yes]; app install|update|uninstall <name> [--yes] [--manual-done]; app setup|teardown <name> --host <host>... [--yes] [--manual-done]; app uninstall <name> --forget [--yes] (--manual-done requires a declared manual step)";
8
+ if (command === "schema" && args.length === 0) {
9
+ output(JSON.stringify(APP_RECIPE_SCHEMA, null, 2));
10
+ return 0;
11
+ }
12
+ if (command === "list" && args.length === 0) {
13
+ for (const update of await checkAppUpdates(apps))
14
+ await show(update.entry, update);
15
+ return 0;
16
+ }
17
+ if (command === "install" || command === "update" || command === "uninstall" || command === "setup" || command === "teardown") {
18
+ return runLifecycle(command, args, apps, output, usage);
19
+ }
20
+ if (command !== "validate" && command !== "approve")
21
+ throw new CliUsageError(usage);
22
+ const confirmations = args.filter((arg) => arg === "--yes").length;
23
+ const selections = command === "approve" ? args.filter((arg) => arg !== "--yes") : args;
24
+ if (!selections.length || selections.some((arg) => arg.startsWith("--"))
25
+ || confirmations > 1 || (command === "approve" && selections.length !== 1)) {
26
+ throw new CliUsageError(usage);
27
+ }
28
+ const entries = command === "validate"
29
+ ? await apps.load(selections)
30
+ : (await apps.load()).filter((entry) => entry.recipe?.name === selections[0] && entry.applicable);
31
+ if (!entries.length)
32
+ throw new Error("No applicable recipe found");
33
+ let failed = false;
34
+ for (const entry of entries) {
35
+ if (entry.error) {
36
+ await show(entry);
37
+ failed = true;
38
+ continue;
39
+ }
40
+ if (!entry.applicable) {
41
+ await show(entry);
42
+ continue;
43
+ }
44
+ output(`Recipe approval preview: ${entry.recipe.name} (${entry.file})`);
45
+ for (const preview of await apps.preview(entry)) {
46
+ const argv = preview.argv ? JSON.stringify(preview.argv) : "manual only";
47
+ output(`${preview.step}: ${argv}\t${preview.executable ?? "unresolved"}\tcwd: ${preview.cwd}`);
48
+ if (preview.warning)
49
+ output(preview.warning);
50
+ if ("manual" in preview && preview.manual) {
51
+ output(`Manual (never executed): ${preview.manual}`);
52
+ }
53
+ }
54
+ if (command === "approve" && confirmations === 1) {
55
+ await apps.approve(entry);
56
+ }
57
+ else {
58
+ output(`Approve explicitly with app approve ${JSON.stringify(entry.recipe.name)} --yes after reviewing every argv.`);
59
+ }
60
+ await show(entry);
61
+ }
62
+ return failed ? 1 : 0;
63
+ async function show(entry, update) {
64
+ const result = update?.inspection ?? await apps.inspect(entry);
65
+ const columns = [
66
+ entry.recipe?.name ?? entry.file, result.status,
67
+ result.installedVersion ?? "unknown", result.executable ?? "unresolved",
68
+ ];
69
+ if (update) {
70
+ columns.push(`latest: ${update.latestVersion ?? "unknown"}`, update.status);
71
+ if (update.reason && update.reason !== result.reason)
72
+ columns.push(update.reason);
73
+ }
74
+ if (result.reason)
75
+ columns.push(result.reason);
76
+ output(columns.join("\t"));
77
+ }
78
+ }
79
+ async function runLifecycle(action, args, apps, output, usage) {
80
+ const hostAction = action === "setup" || action === "teardown";
81
+ const hosts = [];
82
+ const remaining = [];
83
+ for (let index = 0; index < args.length; index++) {
84
+ if (args[index] !== "--host") {
85
+ remaining.push(args[index]);
86
+ continue;
87
+ }
88
+ const host = args[++index];
89
+ if (!hostAction || !PROJECT_HOSTS.some(value => value === host) || hosts.includes(host)) {
90
+ throw new CliUsageError(usage);
91
+ }
92
+ hosts.push(host);
93
+ }
94
+ if (hostAction && !hosts.length)
95
+ throw new CliUsageError(usage);
96
+ const flags = remaining.filter(arg => arg.startsWith("--"));
97
+ const names = remaining.filter(arg => !arg.startsWith("--"));
98
+ const confirmed = flags.includes("--yes");
99
+ const done = flags.includes("--manual-done");
100
+ const forget = flags.includes("--forget");
101
+ const invalidSelection = names.length !== 1;
102
+ const duplicateFlags = new Set(flags).size !== flags.length;
103
+ const unknownFlag = flags.some(flag => !["--yes", "--manual-done", "--forget"].includes(flag));
104
+ const incompatibleForget = forget && (action !== "uninstall" || done);
105
+ if (invalidSelection || duplicateFlags || unknownFlag || incompatibleForget) {
106
+ throw new CliUsageError(usage);
107
+ }
108
+ const name = names[0];
109
+ if (forget) {
110
+ const record = await apps.previewForget(name);
111
+ output(`Forget App tracking only: ${name} (${record.file}); no recipe commands will run`);
112
+ await apps.forgetApp(name, confirmed);
113
+ output(`${name}: untracked`);
114
+ return 0;
115
+ }
116
+ const entries = (await apps.load()).filter(entry => entry.applicable && entry.recipe?.name === name);
117
+ if (entries.length !== 1)
118
+ throw new Error("No unique applicable recipe found");
119
+ // Validate and preview the whole selection before executing any Host.
120
+ const plans = await Promise.all((hostAction ? hosts : [undefined]).map(host => apps.planLifecycle(entries[0], action, host)));
121
+ for (const plan of plans)
122
+ showPlan(plan);
123
+ if (!confirmed)
124
+ throw new Error("App step not confirmed; rerun with --yes after reviewing the preview");
125
+ let failed = false;
126
+ for (const plan of plans) {
127
+ try {
128
+ if (await applyPlan(plan) !== 0)
129
+ failed = true;
130
+ }
131
+ catch (error) {
132
+ if (!hostAction)
133
+ throw error;
134
+ output(`${name} (${plan.host}): failed`);
135
+ output(error instanceof Error ? error.message : String(error));
136
+ failed = true;
137
+ }
138
+ }
139
+ return failed ? 1 : 0;
140
+ function showPlan(plan) {
141
+ output(`App ${name} ${action}${plan.host ? ` (${plan.host})` : ""}: ${plan.argv ? JSON.stringify(plan.argv) : "manual only"}\t${plan.executable ?? "unresolved"}\tcwd: ${plan.cwd}\tenvironment: ${plan.environment}`);
142
+ if (plan.warning)
143
+ output(plan.warning);
144
+ if (plan.manual)
145
+ output(`Manual (never executed): ${plan.manual}`);
146
+ }
147
+ async function applyPlan(plan) {
148
+ const result = done ? await apps.completeManual(plan, true) : await apps.executeLifecycle(plan, true);
149
+ output(`${name}${plan.host ? ` (${plan.host})` : ""}: ${describeAppResult(result)}`);
150
+ if (result.reason)
151
+ output(result.reason);
152
+ if (result.status === "manual required") {
153
+ output(`Manual (never executed): ${result.manual}`);
154
+ output(describeManualCompletion(plan));
155
+ }
156
+ return result.status === "installed" || result.status === "untracked" || result.status === "completed" ? 0 : 1;
157
+ }
158
+ }
159
+ /** Copyable completion command for the platform's documented shell. */
160
+ export function describeManualCompletion(plan) {
161
+ const name = plan.entry.recipe.name;
162
+ const quotedName = process.platform === "win32"
163
+ ? "'" + name.replaceAll("'", "''") + "'"
164
+ : "'" + name.replaceAll("'", "'\\''") + "'";
165
+ const shell = process.platform === "win32" ? "PowerShell" : "POSIX shell";
166
+ return `After completing it, run app ${plan.action} ${quotedName}${plan.host ? ` --host ${plan.host}` : ""} --manual-done --yes to check version (${shell}).`;
167
+ }
168
+ export function describeAppResult(result) {
169
+ return result.status === "installed" && result.previousVersion
170
+ ? `installed ${result.previousVersion} -> ${result.installedVersion} (latest ${result.latestVersion ?? "unknown"})`
171
+ : result.status;
172
+ }