@iseoane/agent-depot 0.4.1 → 0.5.1

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 (56) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.es.md +969 -0
  3. package/README.md +244 -23
  4. package/dist/src/app-flow.js +52 -51
  5. package/dist/src/app-recipe-removal.js +63 -0
  6. package/dist/src/cli.js +122 -11
  7. package/dist/src/executable-resolution.js +54 -0
  8. package/dist/src/git-source.js +150 -6
  9. package/dist/src/package-bundles.js +614 -0
  10. package/dist/src/package-cli.js +412 -0
  11. package/dist/src/package-content.js +158 -0
  12. package/dist/src/package-flow.js +645 -0
  13. package/dist/src/package-host-plans.js +243 -0
  14. package/dist/src/package-host-state.js +473 -0
  15. package/dist/src/package-model.js +33 -0
  16. package/dist/src/package-owned-skills.js +124 -0
  17. package/dist/src/package-state.js +404 -0
  18. package/dist/src/process-runner.js +9 -1
  19. package/dist/src/profile-cli.js +15 -9
  20. package/dist/src/profile-export.js +44 -9
  21. package/dist/src/profile-import.js +80 -17
  22. package/dist/src/profile.js +78 -3
  23. package/dist/src/skill-discovery.js +7 -2
  24. package/dist/src/skill-install.js +16 -0
  25. package/dist/src/sources.js +11 -0
  26. package/dist/src/tui/app-actions.js +1 -1
  27. package/dist/src/tui/app.js +10 -11
  28. package/dist/src/tui/catalog-browse.js +46 -13
  29. package/dist/src/tui/catalog-ownership.js +12 -0
  30. package/dist/src/tui/catalog-panel.js +16 -4
  31. package/dist/src/tui/catalog-state.js +62 -6
  32. package/dist/src/tui/catalog-tree.js +58 -12
  33. package/dist/src/tui/catalog-view.js +53 -7
  34. package/dist/src/tui/environment.js +8 -1
  35. package/dist/src/tui/installations-browse.js +20 -2
  36. package/dist/src/tui/installations-rows.js +2 -1
  37. package/dist/src/tui/installations-tree.js +14 -1
  38. package/dist/src/tui/installations-view.js +8 -2
  39. package/dist/src/tui/installations.js +12 -2
  40. package/dist/src/tui/manage-actions.js +2 -0
  41. package/dist/src/tui/manage-panel.js +5 -0
  42. package/dist/src/tui/package-actions.js +165 -0
  43. package/dist/src/tui/package-rows.js +91 -0
  44. package/dist/src/tui/profile-view.js +36 -14
  45. package/dist/src/tui/render.js +1 -1
  46. package/dist/src/tui/sources-mode.js +2 -0
  47. package/dist/src/tui/sources-recipes.js +116 -53
  48. package/dist/src/tui/sources-view.js +57 -19
  49. package/dist/src/tui/tree.js +3 -4
  50. package/dist/src/tui/updates-flow.js +4 -1
  51. package/dist/src/tui/updates-panel.js +2 -2
  52. package/dist/src/tui/updates.js +72 -4
  53. package/dist/src/tui/window.js +3 -4
  54. package/dist/src/user-global-skill-inventory.js +7 -2
  55. package/package.json +2 -1
  56. package/skills/agent-depot-apprecipe/SKILL.md +29 -5
package/README.md CHANGED
@@ -1,6 +1,37 @@
1
1
  # Agent Depot
2
2
 
3
- Agent Depot keeps a global catalog of Git Sources for future skill discovery.
3
+ [**English**](README.md) · [Español](README.es.md)
4
+
5
+ Agent Depot is a cross-platform manager for the reusable resources that coding agents
6
+ use. It discovers Skills from Git Sources, installs them into one or more Hosts
7
+ (Pi, Claude Code, Codex and OpenCode), manages user-global Apps and host-native
8
+ Packages, keeps track of what it installed and updates it safely, all from a CLI or
9
+ an interactive TUI.
10
+
11
+ What it does:
12
+
13
+ - **Sources and catalog**: register Git repositories (plus the built-in
14
+ `builtin:agent-depot` Source) and discover the Skills they contain.
15
+ - **Install Skills** user-global or per project, for several Hosts at once, pinned
16
+ to a version or following the latest. A project manifest lets a fresh checkout
17
+ reinstall the same Skills.
18
+ - **Updates**: check and apply updates in batch; every change is previewed first and
19
+ needs explicit confirmation.
20
+ - **Existing installations**: report unmanaged Skills already on disk, adopt the
21
+ ones that exactly match a Source, or remove them by explicit path.
22
+ - **Apps**: install, update and uninstall user-global applications through
23
+ user-written App recipes that must be approved before they run.
24
+ - **Packages**: install, update and uninstall host-native bundles (Pi packages and
25
+ Claude Code plugins) by delegating the lifecycle to each Host's own CLI and
26
+ reading the installed version from the Host's own state files. A bundle reserves
27
+ its Skills only while you have selected it or a Host proves it installed, so an
28
+ unselected bundle's Skills stay installable on their own.
29
+ - **Profiles**: export your user-global Sources, Skill selections, App recipes and
30
+ Package selections to a portable file and import them, add-only, on another
31
+ machine.
32
+
33
+ Agent Depot never overwrites or deletes content without a preview and confirmation,
34
+ and refuses unsafe paths and links.
4
35
 
5
36
  Requires Node.js >= 24 (Active LTS). The [interactive TUI](#interactive-tui)
6
37
  needs an interactive terminal.
@@ -78,12 +109,35 @@ agent-depot uninstall --data --yes
78
109
  agent-depot app list
79
110
  agent-depot app approve <name> --yes
80
111
  agent-depot app install <name> --yes
112
+ agent-depot package list
113
+ agent-depot package discover <source-id>
114
+ agent-depot package inspect <source-id>::<bundle-path> --host <host>
115
+ agent-depot package approve <source-id>::<bundle-path> --host <host> --action install
116
+ agent-depot package install <source-id>::<bundle-path> --host <host> --yes
117
+ agent-depot package forget <source-id>::<bundle-path> --host <host> --yes
118
+ agent-depot update check --scope user-global --package <selection>
81
119
  agent-depot export --out profile.json
82
120
  agent-depot import profile.json --yes
83
121
  agent-depot tui
84
122
  agent-depot --version
85
123
  ```
86
124
 
125
+ GitHub directory URLs such as `https://github.com/cursor/plugins/tree/main/pstack`
126
+ are also supported: Agent Depot clones the repository, reads the URL's ref, and
127
+ only discovers Skills inside that directory. Skill paths stay repository-relative.
128
+ The full URL remains the Source identity, so an already registered directory URL
129
+ can simply be refreshed. The ref occupies one URL segment; encode any `/` in a
130
+ branch name as `%2F` (for example `tree/release%2Fstable/pstack`). An explicit
131
+ fixed-version ref overrides the URL ref while keeping the directory scope.
132
+
133
+ Catalog groups show repository names instead of opaque Git IDs. Catalog and
134
+ Installations start with all groups collapsed; use Enter or the right arrow to
135
+ open a group. A Catalog filter automatically expands matching Sources.
136
+
137
+ The TUI Catalog reports unavailable Sources individually and keeps other Sources
138
+ browsable. To create a missing mirror, go to Sources (`1`) and refresh with `r`;
139
+ opening the Catalog does not initiate network access.
140
+
87
141
  Refresh first prints the registered URL as a preview. It requires explicit
88
142
  `--yes` confirmation before any network access and otherwise fails closed. The
89
143
  package-owned `builtin:agent-depot` Source is listed as read-only and cannot be
@@ -190,8 +244,16 @@ index, or an unambiguous Skill path:
190
244
  ```sh
191
245
  agent-depot update apply --scope project --all --yes
192
246
  agent-depot update apply --scope user-global --skill 0 --yes
247
+ agent-depot update apply --scope user-global --package git:<source-id>::pi:. --yes
193
248
  ```
194
249
 
250
+ For `--scope user-global` only, the check also assesses every recorded Package and
251
+ `update apply` accepts repeatable `--package <selection>` or includes updateable
252
+ Packages under `--all`. A Package with `unknown` version status is never offered as
253
+ an update. Packages run after Skills and Apps, each selected Package previews its
254
+ frozen plan, and a Package whose update receipt is missing requires a separate `package approve --action update` before CLI execution. Project
255
+ scope never reads or selects a Package.
256
+
195
257
  A Skill tracked at a non-canonical project path (outside `.agents/skills/<name>` and
196
258
  `.claude/skills/<name>`) is repository-controlled, so `--yes` alone does not update it. The
197
259
  preview prints the relative path; repeat it with `--confirm-path` (once per such path):
@@ -390,23 +452,29 @@ taller than the terminal. `j`/`k` or the arrow keys move.
390
452
 
391
453
  | View | Purpose |
392
454
  | --- | --- |
393
- | 1 Sources | Where skills come from: the fixed built-in Source plus registered Git Sources. |
394
- | 2 Catalog | Skills that can be installed: those not installed yet, as Source, then Skills. |
395
- | 3 Installations | What is on disk: managed installations (user-global and project) and unmanaged user-global skills. |
396
- | 4 Updates | Managed skills with a newer version available, for user-global and project scope. |
397
- | 5 Import/Export | Transfer portable user-global Source choices, Skill selections and App recipes. |
455
+ | 1 Sources | Git Sources (their skills and Packages) and user-owned App recipes in one list, with `skills`/`app` kinds and contextual actions. |
456
+ | 2 Catalog | Skills that can be installed: those not installed yet, as Source, then Skills. Each Source also shows a Packages group, and the Skills a bundle owns are marked and dimmed. |
457
+ | 3 Installations | What is on disk: managed installations (user-global and project), Package selections and unmanaged user-global skills. |
458
+ | 4 Updates | Managed skills and Packages with a newer version available, for user-global and project scope. |
459
+ | 5 Import/Export | Transfer portable user-global Source choices, Skill selections, App recipes and Package selections. |
398
460
 
399
461
  Keys per view (prompts also accept `Esc` to cancel and `y`/`n` to confirm):
400
462
 
401
463
  | View | Keys |
402
464
  | --- | --- |
403
- | Sources | `space` mark, `a` mark all, `Enter` open the Source in the Catalog, `n` add a Git Source, `r` refresh the marked (or highlighted) Sources, `d` remove |
404
- | Catalog | `Enter`/arrows expand and collapse, `space` mark, `a` mark all listed, `i` install the marks (or the highlighted skill), `/` filter, `s` toggle one Source or all (when opened from a Source) |
405
- | 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) |
406
- | 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 |
407
-
465
+ | Sources | `j`/`k` move through Git Sources and App recipes (no Tab section switch). `Enter` opens the Catalog for a Git Source (its skills and Packages) or previews/approves an App recipe. `n` adds a reusable Git Source or guides recipe authoring; `r` refreshes Sources or reloads recipe status; `d` removes. `space`/`a` mark Git Sources only. |
466
+ | Catalog | `Enter`/arrows expand and collapse, `space` mark, `a` mark all listed, `i` install the marked Skills (or the highlighted skill) or preview the highlighted Package bundle, `/` filter, `s` toggle one Source or all (when opened from a Source) |
467
+ | 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). On a Package row: `i` previews the frozen install plan and runs it, approving the receipt first when it is missing, `Enter` approves the install declaration only, `U` update, `u` uninstall, `f` forget the selection record. Package rows are not markable for bulk actions. |
468
+ | 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. A marked Package joins the batch and is approved before its update runs. |
408
469
  | 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 |
409
470
 
471
+ Removing an App recipe checks its approved version command and tracked state.
472
+ If the App is installed or tracked, the TUI offers the existing uninstall flow;
473
+ `n` cancels removal and a failed uninstall keeps the recipe. After verified
474
+ uninstall (or when not installed), a separate preview confirms deletion of only
475
+ the recipe file or symlink. App data and Host configuration are not implicitly
476
+ deleted. Invalid recipes must be repaired before their App state can be checked.
477
+
410
478
  Safety model:
411
479
 
412
480
  - Nothing is written before a preview and an explicit `y`. Exposing a skill to an
@@ -414,6 +482,11 @@ Safety model:
414
482
  - An install refreshes its Git Source before the preview, so the content you
415
483
  confirm is the content that is applied. The Updates view refreshes
416
484
  the Git Sources in use when you enter it and on `r`.
485
+ - A Package action previews the frozen declaration and the exact Host commands.
486
+ When the action's receipt is missing, the first `y` approves it and a second `y`
487
+ runs the Host command; an already approved action runs on the first `y`. `n` or
488
+ `Esc` at either gate cancels without approving or running anything. Listing and
489
+ reloading Packages never run a Host command.
417
490
  - The built-in Source is fixed: it is shown as an unselectable row and cannot be
418
491
  refreshed or removed.
419
492
  - Removal is user-global only. Project installations are listed, but project
@@ -443,6 +516,11 @@ Git mirrors are cached separately from the catalog:
443
516
 
444
517
  ## Limitations
445
518
 
519
+ Packages are limited to the Pi and Claude Code Hosts in V1. Codex and OpenCode
520
+ plugins, npm-sourced Pi packages, project-scoped Packages and external Claude
521
+ marketplace sources are deferred, and a fixed-version Claude selection is refused;
522
+ see [Packages](#packages).
523
+
446
524
  Only Git repository URLs using `git:`, `http:`, `https:`, or `ssh:` are
447
525
  accepted. Refresh uses the registered URL and runs Git with argument arrays,
448
526
  without a shell. It creates or fetches a bare mirror, explicitly synchronizes
@@ -589,6 +667,135 @@ remaining Hosts. For manual completion, rerun the selected Host with
589
667
  `--manual-done --yes`. This checks the App version, not Host wiring. Host actions
590
668
  never change installed-App tracking.
591
669
 
670
+ ## Packages
671
+
672
+ A Package is a host-native bundle discovered inside a registered Source: a Pi
673
+ package (a `package.json` with a `pi` key, or a `pi-package` keyword or a
674
+ Pi-specific `extensions/` or `prompts/` directory) or a Claude Code plugin (a
675
+ `.claude-plugin/plugin.json` bound by an in-repo `.claude-plugin/marketplace.json`).
676
+ Because `skills/` and `themes/` are also Claude Code default locations, a
677
+ conventional Pi package needs a Pi-specific directory, so an ordinary repository
678
+ of shared skills is not read as a Pi package. One repository can be both, as
679
+ `pstack-claude` is. V1 supports the Pi and Claude Code Hosts only; Codex and
680
+ OpenCode packaging, npm-sourced Pi packages and project-scoped Packages are
681
+ deferred.
682
+
683
+ Agent Depot does not copy a bundle's contents. It reads the bundle manifest into a
684
+ typed component inventory, derives the Host's own commands from that declaration,
685
+ runs them through the no-shell command runner after approval, and reads the
686
+ installed version back from the Host's state files. The Host owns what is
687
+ installed. Agent Depot owns the selection record and the receipt for what it
688
+ delegated. A bundle's own Skills are never offered for adoption or installed a
689
+ second time, because the Host owns their files and locations.
690
+
691
+ ### Add a Source and discover its bundles
692
+
693
+ Discovery reads the cached Git mirror and never refreshes it. The first discovery
694
+ of a Source whose mirror is missing fails until you refresh that Source:
695
+
696
+ ```sh
697
+ agent-depot source add https://github.com/michael-denyer/pstack-claude.git
698
+ agent-depot source list
699
+ agent-depot source refresh git:<source-id> --yes
700
+ agent-depot package discover git:<source-id>
701
+ ```
702
+
703
+ `package discover` prints one line per bundle in the Source plus its component
704
+ inventory. `pstack-claude` yields a Pi bundle at the repository root and a Claude
705
+ bundle from its in-repo marketplace:
706
+
707
+ ```text
708
+ Package git:<source-id>::pi:.: pstack
709
+ Package git:<source-id>::claude:.#pstack: pstack
710
+ ```
711
+
712
+ Each component line names a kind, an ownership (`skill-category` or `host-only`),
713
+ an effect and the declared paths. Agent definitions and MCP servers appear as
714
+ `host-only`: Agent Depot reports them and leaves them to the Host.
715
+
716
+ ### Select, approve and run a lifecycle action
717
+
718
+ A selection is `<source-id>::<bundle-path>` plus `--host`, or a host-qualified
719
+ `<source-id>::<host>:<bundle-path>`. A Claude bundle path ends in `#<plugin-name>`
720
+ when the Source offers more than one plugin.
721
+
722
+ `approve` is per action. It writes the receipt for the exact declaration of that
723
+ one action, so approving `install` does not authorize `update`. `--yes` confirms
724
+ execution and never substitutes for approval. Planning a lifecycle action
725
+ refreshes the selection's Git Source first, so the previewed manifest is the one
726
+ the commands derive from.
727
+
728
+ ```sh
729
+ # Pi package at the repository root
730
+ agent-depot package inspect git:<source-id>::pi:. --host pi
731
+ agent-depot package install git:<source-id>::pi:. --host pi
732
+ agent-depot package approve git:<source-id>::pi:. --host pi --action install
733
+ agent-depot package install git:<source-id>::pi:. --host pi --yes
734
+
735
+ # Claude Code plugin from the in-repo marketplace
736
+ agent-depot package inspect git:<source-id>::claude:.#pstack --host claude
737
+ agent-depot package approve git:<source-id>::claude:.#pstack --host claude --action install
738
+ agent-depot package install git:<source-id>::claude:.#pstack --host claude --yes
739
+ ```
740
+
741
+ The `install`, `update` and `uninstall` commands print the full preview, then exit
742
+ non-zero until both the action's receipt exists and `--yes` is given. The preview
743
+ shows the resolved origin and commit, the descriptor, the component inventory with
744
+ ownership and effect, every Host command with its executable name, the
745
+ recorded coordinates the action can change (`affects`), the working directory, the
746
+ environment, and any warning. Immediately before execution, Agent Depot re-derives
747
+ the declaration and stops with `stale plan` if the manifest, commit, install
748
+ identity or ordered commands changed since the preview. When the Host already
749
+ reports the bundle installed, the plan runs no command and only records the
750
+ selection.
751
+
752
+ The Host commands for the two V1 Hosts:
753
+
754
+ | Host | Install | Update | Uninstall |
755
+ | --- | --- | --- | --- |
756
+ | Pi | `pi install <source>` | `pi update <source>` | `pi remove <source>` |
757
+ | Claude Code | `claude plugin marketplace add <source>`, then `claude plugin install <plugin>@<marketplace>` | `claude plugin marketplace update <marketplace>`, then `claude plugin update <plugin>@<marketplace>` | `claude plugin uninstall <plugin>@<marketplace>` |
758
+
759
+ Agent Depot omits the `marketplace add` step when the Host already knows the
760
+ marketplace, and always names the marketplace on the scoped update verb. Every V1
761
+ action affects only its own selection.
762
+
763
+ ### Installed state, versions and removal
764
+
765
+ `package list` shows every recorded selection with its install id and the version
766
+ evidence Agent Depot recorded. Live evidence comes from
767
+ `~/.pi/agent/settings.json` and `~/.claude/plugins/installed_plugins.json`, which
768
+ Agent Depot reads and never writes. A missing Host file is an empty view. A
769
+ drifted or undecodable file leaves the affected lookup `unknown` and never throws.
770
+ An unversioned Pi git package is reported as `unknown`, never as `current`.
771
+
772
+ An update records a new version only when the Host state changed in a way Agent
773
+ Depot can verify, meaning the two observations agree on a version or a
774
+ hexadecimal commit and differ, or the Host went from nothing installed to
775
+ installed. An unverifiable update fails and leaves the record untouched. Uninstall
776
+ delegates the Host command and then drops the selection record:
777
+
778
+ ```sh
779
+ agent-depot package approve git:<source-id>::pi:. --host pi --action uninstall
780
+ agent-depot package uninstall git:<source-id>::pi:. --host pi --yes
781
+ agent-depot package forget git:<source-id>::pi:. --host pi --yes
782
+ ```
783
+
784
+ `forget` removes only the Agent Depot selection record. It runs no Host command
785
+ and leaves the Host install as it is.
786
+
787
+ ### Package limits
788
+
789
+ - A fixed-version Claude selection cannot be delegated. `claude plugin install`
790
+ takes `<plugin>@<marketplace>` and has no pin slot, so planning a Claude
791
+ `install` or `update` with a `fixed` version policy fails closed instead of
792
+ claiming a pin the Host would ignore. Use `latest` for a Claude selection, or a
793
+ fixed pin for a Pi selection, until a supported Claude pin form is verified.
794
+ - A Claude plugin with no in-repo marketplace binding is reported but not
795
+ installable.
796
+ - Agent Depot does not read Host CLI prose output, including `pi list`. Installed
797
+ evidence comes from the Host state files only.
798
+
592
799
  ## Export a user-global Profile
593
800
 
594
801
  Export portable choices to another machine or keep them in git:
@@ -598,25 +805,30 @@ agent-depot export > profile.json
598
805
  agent-depot export --out profile.json
599
806
  agent-depot export --no-apps --source <source-id> --skill doctor-md-agents
600
807
  agent-depot export --no-sources --no-skills --app my-tool --app another-tool
808
+ agent-depot export --package git:<source-id>::pi:.
601
809
  ```
602
810
 
603
- Everything portable is selected by default. `--no-sources`, `--no-skills` and
604
- `--no-apps` exclude blocks; repeatable `--source <id|url>`, `--skill <path|name>`
605
- and `--app <name>` restrict each block independently. Unknown selections and
606
- combining a block's inclusion filter with its `--no-*` flag are errors.
811
+ Everything portable is selected by default. `--no-sources`, `--no-skills`,
812
+ `--no-apps` and `--no-packages` exclude blocks; repeatable `--source <id|url>`,
813
+ `--skill <path|name>`, `--app <name>` and `--package <selection>` restrict each block
814
+ independently. Unknown selections and combining a block's inclusion filter with its
815
+ `--no-*` flag are errors.
607
816
 
608
817
  Stdout contains JSON only; stderr shows the preview and exclusion reasons.
609
818
  `--out` requires a new file (existing files and symlinks are not replaced).
610
819
  Profiles contain canonical Git URLs with discovery inclusion, recorded Skill
611
- version policies/Hosts/user methods, and complete App recipes, including recipes
612
- for other platforms. CLI export always writes `included: true`; discovery choices are transient, while
820
+ version policies/Hosts/user methods, complete App recipes, including recipes for
821
+ other platforms, and Package selections as declarations only: their Source, host
822
+ coordinates and version policy. CLI export always writes `included: true`; discovery choices are transient, while
613
823
  the TUI may pass explicit inclusion choices to the shared export core.
614
- Export neither refreshes Sources nor runs App commands.
824
+ Export neither refreshes Sources nor runs App commands, and a Package receipt never
825
+ travels in a Profile.
615
826
 
616
827
  Local paths and obvious credentials are not portable. Nonportable/invalid items
617
828
  are excluded with reasons, as are unmanaged/App-owned and project Skills,
618
- installation locations/evidence, approval receipts, installed-App state and
619
- pending updates. Free-text recipe portability checks are conservative; review
829
+ installation locations/evidence, approval receipts, installed-App state, pending
830
+ updates and Package receipts/installed state. Free-text recipe portability checks
831
+ are conservative; review
620
832
  profiles before sharing them—arbitrary secrets cannot be detected automatically.
621
833
 
622
834
  ## Import a user-global Profile
@@ -629,19 +841,26 @@ agent-depot import profile.json --yes
629
841
  agent-depot import profile.json --no-apps --skill doctor-md-agents --yes
630
842
  ```
631
843
 
632
- Import uses the same independent block/name filters as export. It reports **add**,
844
+ Import uses the same independent block/name filters as export, including
845
+ `--no-packages` and `--package <selection>`. It reports **add**,
633
846
  **same** (skipped), and **conflict** (difference shown, skipped). Without `--yes`,
634
847
  nothing is written. Existing choices and conflicting files are never replaced.
635
848
  A profile produced by a newer Agent Depot version gives a warning, not an error.
636
849
 
637
850
  Confirmed additions run in order: Source registration, user-global Skills through
638
- normal refreshed install previews and safety checks, then App recipes. Recorded
639
- Hosts, version policies and user methods are retained. Even without `--yes`,
851
+ normal refreshed install previews and safety checks, then App recipes, then Package
852
+ selections. Recorded Hosts, version policies and user methods are retained. Even
853
+ without `--yes`,
640
854
  each add-Skill preview shows its Source, Hosts, version policy and full argv for
641
855
  every user-provided method, marked as running only after confirmation. The normal
642
856
  install preview still appears before execution. Each failure is reported independently and remaining items
643
857
  continue; any failed item makes the command exit non-zero.
644
858
 
859
+ A Package selection is a selection only. Import replays it through the flow's
860
+ `select` action, which writes a receipt for that action alone, whose declaration
861
+ carries no argv. No install or update is authorized and no Host command runs, so
862
+ import never installs a Package.
863
+
645
864
  Recipes, including those for other platforms, arrive **unapproved** in `apps/`.
646
865
  A same-name destination recipe blocks import only when its platform overlaps the
647
866
  incoming recipe: equal platforms overlap, and a missing platform overlaps every
@@ -661,3 +880,5 @@ The **5 Import/Export** TUI tab offers grouped checklists and the same shared-co
661
880
  previews as the CLI. Exclusions and conflicts cannot be selected. Export creates
662
881
  a new file only; empty exports write nothing (CLI reports an error). Import adds missing choices without replacing existing ones.
663
882
  Imported recipes remain unapproved and Apps are never installed by import.
883
+ Imported Package selections are recorded without installing a bundle and without
884
+ running a Host command.
@@ -1,5 +1,4 @@
1
1
  import { access, chmod, mkdir, readFile, readdir, realpath, unlink } from "node:fs/promises";
2
- import { constants } from "node:fs";
3
2
  import { createHash } from "node:crypto";
4
3
  import { homedir } from "node:os";
5
4
  import path from "node:path";
@@ -7,6 +6,7 @@ import { compareAppVersions, sameAppVersion } from "./app-version.js";
7
6
  import { fetchAppLatest } from "./app-latest.js";
8
7
  import { writeFileAtomically } from "./atomic-file.js";
9
8
  import { parseAppRecipe } from "./app-recipes.js";
9
+ import { createPortableExecutableResolver } from "./executable-resolution.js";
10
10
  import { runProcess } from "./process-runner.js";
11
11
  import { defaultSourceStatePath } from "./source-state.js";
12
12
  const APP_OUTPUT_LIMIT = 1024 * 1024;
@@ -21,42 +21,12 @@ export function createAppOperations(environment = {}) {
21
21
  const isWsl = environment.isWsl ?? (platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP));
22
22
  const mountRoot = environment.wslMountRoot ?? "/mnt";
23
23
  const runner = environment.runner ?? runProcess;
24
- function isWindowsExecutable(executable) {
25
- const relative = path.posix.relative(mountRoot, executable);
26
- return isWsl && /^[a-z]\//iu.test(relative);
27
- }
28
- async function resolve(name) {
29
- if (environment.resolveExecutable) {
30
- const executable = await environment.resolveExecutable(name);
31
- const target = executable ? await realpath(executable).catch(() => executable) : undefined;
32
- return { executable, blocked: target !== undefined && isWindowsExecutable(target) };
33
- }
34
- let blockedExecutable;
35
- const suffixes = platform === "win32"
36
- ? ["", ...(process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")]
37
- : [""];
38
- for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
39
- if (!path.isAbsolute(dir))
40
- continue;
41
- for (const suffix of suffixes) {
42
- const candidate = path.join(dir, name + suffix);
43
- try {
44
- await access(candidate, platform === "win32" ? constants.F_OK : constants.X_OK);
45
- const target = await realpath(candidate);
46
- if (isWindowsExecutable(target)) {
47
- blockedExecutable ??= candidate;
48
- continue;
49
- }
50
- // Preserve shim/multicall identity when spawning; realpath is only a safety check.
51
- return { executable: candidate, blocked: false };
52
- }
53
- catch {
54
- // Try the next PATH entry.
55
- }
56
- }
57
- }
58
- return { executable: blockedExecutable, blocked: blockedExecutable !== undefined };
59
- }
24
+ const resolve = createPortableExecutableResolver({
25
+ platform,
26
+ isWsl,
27
+ wslMountRoot: mountRoot,
28
+ ...(environment.resolveExecutable === undefined ? {} : { resolveExecutable: environment.resolveExecutable }),
29
+ });
60
30
  const receiptPath = (entry) => path.join(receipts, createHash("sha256").update(entry.canonicalFile).digest("hex") + ".json");
61
31
  async function read(file) {
62
32
  try {
@@ -178,25 +148,28 @@ export function createAppOperations(environment = {}) {
178
148
  return approval;
179
149
  if (cached)
180
150
  return cached.installedVersion
181
- ? { status: "installed", installedVersion: cached.installedVersion } : { status: "not installed" };
151
+ ? { status: "installed", installedVersion: cached.installedVersion, evidence: "recorded" }
152
+ : { status: "not installed" };
182
153
  const { executable, blocked } = await resolve(entry.recipe.version.argv[0]);
183
154
  if (!executable || blocked) {
184
155
  return { status: "not installed", executable, reason: blocked ? WINDOWS_EXECUTABLE_WARNING : undefined };
185
156
  }
157
+ let installedVersion;
186
158
  try {
187
159
  const result = await runner(executable, entry.recipe.version.argv.slice(1), {
188
160
  cwd: home, captureStdout: true, maxOutputBytes: APP_OUTPUT_LIMIT, maxStderrBytes: APP_OUTPUT_LIMIT,
189
161
  });
190
- const installedVersion = result.code === 0 && !result.signal && !result.outputTooLarge
162
+ installedVersion = result.code === 0 && !result.signal && !result.outputTooLarge
191
163
  ? new RegExp(entry.recipe.version.pattern).exec(result.stdout.toString("utf8"))?.[1]
192
164
  : undefined;
193
- return installedVersion
194
- ? { status: "installed", executable, installedVersion }
195
- : { status: "not installed", executable };
196
165
  }
197
166
  catch {
198
167
  return { status: "not installed", executable };
199
168
  }
169
+ if (!installedVersion)
170
+ return { status: "not installed", executable };
171
+ await reconcileTracked(entry, installedVersion);
172
+ return { status: "installed", executable, installedVersion, evidence: "verified" };
200
173
  }
201
174
  async function checkUpdate(entry) {
202
175
  const inspection = await inspect(entry);
@@ -273,6 +246,25 @@ export function createAppOperations(environment = {}) {
273
246
  }
274
247
  return records;
275
248
  }
249
+ async function writeTracked(name, recipeFile, installedVersion) {
250
+ await mkdir(installations, { recursive: true, mode: 0o700 });
251
+ await chmod(installations, 0o700);
252
+ await writeFileAtomically(installationPath(name), JSON.stringify({ name, recipeFile, installedVersion }), { mode: 0o600 });
253
+ }
254
+ /**
255
+ * A live probe that verifies a version ahead of tracking rewrites that existing record,
256
+ * so a reload shows the version the App actually reports. Only a record whose recipe file
257
+ * matches is touched; an untracked App, an equal or older version and a failed probe write nothing.
258
+ */
259
+ async function reconcileTracked(entry, installedVersion) {
260
+ const name = entry.recipe?.name;
261
+ if (!name || !entry.canonicalFile)
262
+ return;
263
+ const tracked = (await trackedApps(() => { })).find(record => record.name === name && record.recipeFile === entry.canonicalFile);
264
+ if (!tracked || compareAppVersions(installedVersion, tracked.installedVersion) !== 1)
265
+ return;
266
+ await writeTracked(name, entry.canonicalFile, installedVersion);
267
+ }
276
268
  async function previewForget(name) {
277
269
  const file = installationPath(name);
278
270
  try {
@@ -336,11 +328,7 @@ export function createAppOperations(environment = {}) {
336
328
  if (plan.action === "update" && (!plan.previousVersion || sameAppVersion(inspection.installedVersion, plan.previousVersion))) {
337
329
  return { status: "failed", reason: `version unchanged (${plan.previousVersion ?? "unknown"}); expected ${plan.latestVersion ?? "unknown"}` };
338
330
  }
339
- await mkdir(installations, { recursive: true, mode: 0o700 });
340
- await chmod(installations, 0o700);
341
- await writeFileAtomically(installationPath(name), JSON.stringify({
342
- name, recipeFile: plan.entry.canonicalFile, installedVersion: inspection.installedVersion,
343
- }), { mode: 0o600 });
331
+ await writeTracked(name, plan.entry.canonicalFile, inspection.installedVersion);
344
332
  if (plan.action === "update")
345
333
  await clearPendingUpdate(name);
346
334
  return { status: "installed", previousVersion: plan.previousVersion,
@@ -358,8 +346,12 @@ export function createAppOperations(environment = {}) {
358
346
  if (!confirmed)
359
347
  throw new Error("App step not confirmed; review and confirm first");
360
348
  await recheckPlan(plan);
361
- if (!plan.argv || !plan.executable || plan.warning)
362
- return manualFallback(plan);
349
+ if (!plan.argv)
350
+ return manualFallback(plan, `The ${plan.action} step requires manual completion`);
351
+ if (plan.warning)
352
+ return manualFallback(plan, `Cannot run ${JSON.stringify(plan.argv[0])}: ${plan.warning}`);
353
+ if (!plan.executable)
354
+ return manualFallback(plan, missingExecutableReason(plan.argv[0], plan.action));
363
355
  let result;
364
356
  try {
365
357
  result = await runner(plan.executable, plan.argv.slice(1), {
@@ -370,7 +362,10 @@ export function createAppOperations(environment = {}) {
370
362
  const code = error?.code;
371
363
  if (!code || !["ENOENT", "EACCES", "ENOEXEC", "EPERM"].includes(code))
372
364
  throw error;
373
- return manualFallback(plan, error instanceof Error ? error.message : String(error));
365
+ const detail = error instanceof Error ? error.message : String(error);
366
+ return manualFallback(plan, code === "ENOENT"
367
+ ? `${missingExecutableReason(plan.argv[0], plan.action)} (${detail})`
368
+ : `Cannot run ${JSON.stringify(plan.argv[0])} for ${plan.action} (${code}): ${detail}. Check the executable's permissions and runtime.`);
374
369
  }
375
370
  if (result.code !== 0 || result.signal || result.outputTooLarge) {
376
371
  const reason = result.outputTooLarge ? `output exceeded ${APP_OUTPUT_LIMIT} bytes`
@@ -389,7 +384,13 @@ export function createAppOperations(environment = {}) {
389
384
  throw error;
390
385
  });
391
386
  }
392
- async function manualFallback(plan, reason = "Executable unavailable") {
387
+ function missingExecutableReason(executable, action) {
388
+ const guidance = executable === "brew"
389
+ ? "Install Homebrew and make sure brew is on the PATH used by Agent Depot, then retry."
390
+ : `Install the required executable or make it available on the PATH used by Agent Depot, then retry.`;
391
+ return `Required executable ${JSON.stringify(executable)} for ${action} was not found or could not be started. ${guidance}`;
392
+ }
393
+ async function manualFallback(plan, reason) {
393
394
  if (plan.action === "update" && plan.manual) {
394
395
  await mkdir(pendingUpdates, { recursive: true, mode: 0o700 });
395
396
  await chmod(pendingUpdates, 0o700);
@@ -0,0 +1,63 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile, readlink, realpath, unlink } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { assertNoSymlinkPath } from "./path-safety.js";
5
+ function sameIdentity(left, right) {
6
+ return left.device === right.device && left.inode === right.inode && left.hash === right.hash && left.link === right.link;
7
+ }
8
+ async function recipeIdentity(file) {
9
+ await assertNoSymlinkPath(path.dirname(file), "App recipes directory");
10
+ const info = await lstat(file);
11
+ if (!info.isFile() && !info.isSymbolicLink())
12
+ throw new Error("App recipe is not a file or symbolic link");
13
+ // A symlink is removed as a link, never by unlinking its target.
14
+ const link = info.isSymbolicLink() ? await readlink(file) : undefined;
15
+ const bytes = await readFile(file);
16
+ return { device: info.dev, inode: info.ino, hash: createHash("sha256").update(bytes).digest("hex"),
17
+ ...(link === undefined ? {} : { link }) };
18
+ }
19
+ /** Explicit removal may check version, but never runs an unapproved recipe. */
20
+ export async function planAppRecipeRemoval(apps, entry) {
21
+ const file = path.resolve(entry.file);
22
+ if (path.dirname(file) !== path.resolve(apps.directory))
23
+ throw new Error("App recipe is outside the recipes directory");
24
+ const identity = await recipeIdentity(file);
25
+ const [current] = await apps.load([file]);
26
+ if (current.error || !current.recipe)
27
+ throw new Error(`Cannot verify App state: ${current.error ?? "invalid recipe"}. Repair the recipe before removing it.`);
28
+ if (current.hash !== entry.hash || current.canonicalFile !== entry.canonicalFile)
29
+ throw new Error("App recipe changed; review it again");
30
+ const tracked = (await apps.trackedApps()).find((record) => record.name === current.recipe.name);
31
+ const inspection = await apps.inspect(current);
32
+ if (inspection.status !== "installed" && inspection.status !== "not installed" && inspection.status !== "not applicable here") {
33
+ throw new Error(`Cannot verify App state (${inspection.status}); approve the recipe before removing it`);
34
+ }
35
+ // Nothing resolved a version executable, so installed state was never verified.
36
+ if (inspection.status === "not installed" && (inspection.executable === undefined || inspection.reason !== undefined)) {
37
+ throw new Error(`Cannot verify App state: the version command for ${current.recipe.name} did not run. Make it available, then remove the recipe.`);
38
+ }
39
+ const installed = inspection.status === "installed" || tracked !== undefined;
40
+ return Object.freeze({ entry: current, file, identity, installed,
41
+ installedVersion: inspection.installedVersion ?? tracked?.installedVersion });
42
+ }
43
+ /** Rechecks content, inode and symlink target before any unlink; the live version read is a window. */
44
+ async function assertRecipeUnchanged(plan) {
45
+ const identity = await recipeIdentity(plan.file);
46
+ const canonicalFile = await realpath(plan.file);
47
+ if (!sameIdentity(identity, plan.identity) || canonicalFile !== plan.entry.canonicalFile) {
48
+ throw new Error("App recipe changed after preview; removal cancelled");
49
+ }
50
+ }
51
+ /** Rechecks the preview and installed state before unlinking only the recipe path. */
52
+ export async function removeAppRecipe(apps, plan, confirmed) {
53
+ if (!confirmed)
54
+ throw new Error("App recipe removal must be confirmed");
55
+ if (path.dirname(plan.file) !== path.resolve(apps.directory))
56
+ throw new Error("App recipe is outside the recipes directory");
57
+ await assertRecipeUnchanged(plan);
58
+ const current = await planAppRecipeRemoval(apps, plan.entry);
59
+ if (current.installed)
60
+ throw new Error("App is still installed or tracked; uninstall it before removing its recipe");
61
+ await assertRecipeUnchanged(plan);
62
+ await unlink(plan.file);
63
+ }