@iseoane/agent-depot 0.4.0 → 0.5.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.
- package/CHANGELOG.md +37 -0
- package/README.es.md +969 -0
- package/README.md +254 -25
- package/dist/src/app-flow.js +52 -51
- package/dist/src/app-recipe-removal.js +63 -0
- package/dist/src/cli.js +122 -11
- package/dist/src/executable-resolution.js +54 -0
- package/dist/src/git-source.js +129 -6
- package/dist/src/package-bundles.js +614 -0
- package/dist/src/package-cli.js +412 -0
- package/dist/src/package-content.js +158 -0
- package/dist/src/package-flow.js +636 -0
- package/dist/src/package-host-plans.js +240 -0
- package/dist/src/package-host-state.js +349 -0
- package/dist/src/package-model.js +33 -0
- package/dist/src/package-owned-skills.js +122 -0
- package/dist/src/package-state.js +404 -0
- package/dist/src/profile-cli.js +15 -9
- package/dist/src/profile-export.js +44 -9
- package/dist/src/profile-import.js +80 -17
- package/dist/src/profile.js +78 -3
- package/dist/src/skill-discovery.js +2 -2
- package/dist/src/skill-install.js +16 -0
- package/dist/src/sources.js +11 -0
- package/dist/src/tui/app-actions.js +1 -1
- package/dist/src/tui/app.js +5 -6
- package/dist/src/tui/catalog-browse.js +10 -3
- package/dist/src/tui/catalog-panel.js +7 -3
- package/dist/src/tui/catalog-state.js +57 -6
- package/dist/src/tui/catalog-tree.js +52 -10
- package/dist/src/tui/catalog-view.js +27 -4
- package/dist/src/tui/environment.js +8 -1
- package/dist/src/tui/installations-browse.js +20 -2
- package/dist/src/tui/installations-rows.js +2 -1
- package/dist/src/tui/installations-tree.js +14 -1
- package/dist/src/tui/installations-view.js +8 -2
- package/dist/src/tui/installations.js +12 -2
- package/dist/src/tui/manage-actions.js +2 -0
- package/dist/src/tui/manage-panel.js +5 -0
- package/dist/src/tui/package-actions.js +165 -0
- package/dist/src/tui/package-rows.js +87 -0
- package/dist/src/tui/profile-view.js +36 -14
- package/dist/src/tui/sources-mode.js +2 -0
- package/dist/src/tui/sources-recipes.js +116 -53
- package/dist/src/tui/sources-view.js +57 -19
- package/dist/src/tui/tree.js +3 -4
- package/dist/src/tui/updates-flow.js +4 -1
- package/dist/src/tui/updates-panel.js +2 -2
- package/dist/src/tui/updates.js +72 -4
- package/dist/src/user-global-skill-inventory.js +7 -2
- package/package.json +2 -1
- package/skills/agent-depot-apprecipe/SKILL.md +29 -5
package/README.md
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
# Agent Depot
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
@@ -75,10 +106,38 @@ agent-depot uninstall --cli
|
|
|
75
106
|
agent-depot uninstall --skills --yes
|
|
76
107
|
agent-depot uninstall --unmanaged-skill <exact-global-path> --yes
|
|
77
108
|
agent-depot uninstall --data --yes
|
|
109
|
+
agent-depot app list
|
|
110
|
+
agent-depot app approve <name> --yes
|
|
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>
|
|
119
|
+
agent-depot export --out profile.json
|
|
120
|
+
agent-depot import profile.json --yes
|
|
78
121
|
agent-depot tui
|
|
79
122
|
agent-depot --version
|
|
80
123
|
```
|
|
81
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
|
+
|
|
82
141
|
Refresh first prints the registered URL as a preview. It requires explicit
|
|
83
142
|
`--yes` confirmation before any network access and otherwise fails closed. The
|
|
84
143
|
package-owned `builtin:agent-depot` Source is listed as read-only and cannot be
|
|
@@ -185,8 +244,16 @@ index, or an unambiguous Skill path:
|
|
|
185
244
|
```sh
|
|
186
245
|
agent-depot update apply --scope project --all --yes
|
|
187
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
|
|
188
248
|
```
|
|
189
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
|
+
|
|
190
257
|
A Skill tracked at a non-canonical project path (outside `.agents/skills/<name>` and
|
|
191
258
|
`.claude/skills/<name>`) is repository-controlled, so `--yes` alone does not update it. The
|
|
192
259
|
preview prints the relative path; repeat it with `--confirm-path` (once per such path):
|
|
@@ -385,23 +452,29 @@ taller than the terminal. `j`/`k` or the arrow keys move.
|
|
|
385
452
|
|
|
386
453
|
| View | Purpose |
|
|
387
454
|
| --- | --- |
|
|
388
|
-
| 1 Sources |
|
|
389
|
-
| 2 Catalog | Skills that can be installed: those not installed yet, as Source, then Skills. |
|
|
390
|
-
| 3 Installations | What is on disk: managed installations (user-global and project) and unmanaged user-global skills. |
|
|
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
|
|
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. |
|
|
393
460
|
|
|
394
461
|
Keys per view (prompts also accept `Esc` to cancel and `y`/`n` to confirm):
|
|
395
462
|
|
|
396
463
|
| View | Keys |
|
|
397
464
|
| --- | --- |
|
|
398
|
-
| Sources | `
|
|
399
|
-
| Catalog | `Enter`/arrows expand and collapse, `space` mark, `a` mark all listed, `i` install the
|
|
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) |
|
|
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 |
|
|
402
|
-
|
|
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. |
|
|
403
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 |
|
|
404
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
|
+
|
|
405
478
|
Safety model:
|
|
406
479
|
|
|
407
480
|
- Nothing is written before a preview and an explicit `y`. Exposing a skill to an
|
|
@@ -409,6 +482,11 @@ Safety model:
|
|
|
409
482
|
- An install refreshes its Git Source before the preview, so the content you
|
|
410
483
|
confirm is the content that is applied. The Updates view refreshes
|
|
411
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.
|
|
412
490
|
- The built-in Source is fixed: it is shown as an unselectable row and cannot be
|
|
413
491
|
refreshed or removed.
|
|
414
492
|
- Removal is user-global only. Project installations are listed, but project
|
|
@@ -438,6 +516,11 @@ Git mirrors are cached separately from the catalog:
|
|
|
438
516
|
|
|
439
517
|
## Limitations
|
|
440
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
|
+
|
|
441
524
|
Only Git repository URLs using `git:`, `http:`, `https:`, or `ssh:` are
|
|
442
525
|
accepted. Refresh uses the registered URL and runs Git with argument arrays,
|
|
443
526
|
without a shell. It creates or fetches a bare mirror, explicitly synchronizes
|
|
@@ -459,8 +542,11 @@ node dist/src/cli.js --version
|
|
|
459
542
|
|
|
460
543
|
[Fallow](https://docs.fallow.tools) 3.30.0 runs through `npx --yes fallow@3.30.0`
|
|
461
544
|
(pinned, no dependency installed) and reads `.fallowrc.json`, which defines the
|
|
462
|
-
entry points and the module-layer boundaries (`cli` -> `
|
|
463
|
-
-> `infrastructure`; imports may only point downward
|
|
545
|
+
entry points and the module-layer boundaries (`cli` -> `tui` -> `application`
|
|
546
|
+
-> `core` -> `infrastructure`; imports may only point downward, and `cli` may
|
|
547
|
+
also use the lower layers directly). `scripts/**` forms an isolated `tooling`
|
|
548
|
+
zone. Every source file must belong to a zone; App and Profile modules sit in
|
|
549
|
+
`application`, except `atomic-file.ts` in `infrastructure`.
|
|
464
550
|
|
|
465
551
|
- `pnpm audit:dead-code`: unused files, exports and dependencies, plus
|
|
466
552
|
boundary violations. Must be clean.
|
|
@@ -581,6 +667,135 @@ remaining Hosts. For manual completion, rerun the selected Host with
|
|
|
581
667
|
`--manual-done --yes`. This checks the App version, not Host wiring. Host actions
|
|
582
668
|
never change installed-App tracking.
|
|
583
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
|
+
|
|
584
799
|
## Export a user-global Profile
|
|
585
800
|
|
|
586
801
|
Export portable choices to another machine or keep them in git:
|
|
@@ -590,25 +805,30 @@ agent-depot export > profile.json
|
|
|
590
805
|
agent-depot export --out profile.json
|
|
591
806
|
agent-depot export --no-apps --source <source-id> --skill doctor-md-agents
|
|
592
807
|
agent-depot export --no-sources --no-skills --app my-tool --app another-tool
|
|
808
|
+
agent-depot export --package git:<source-id>::pi:.
|
|
593
809
|
```
|
|
594
810
|
|
|
595
|
-
Everything portable is selected by default. `--no-sources`, `--no-skills
|
|
596
|
-
`--no-apps` exclude blocks; repeatable `--source <id|url>`,
|
|
597
|
-
|
|
598
|
-
combining a block's inclusion filter with its
|
|
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.
|
|
599
816
|
|
|
600
817
|
Stdout contains JSON only; stderr shows the preview and exclusion reasons.
|
|
601
818
|
`--out` requires a new file (existing files and symlinks are not replaced).
|
|
602
819
|
Profiles contain canonical Git URLs with discovery inclusion, recorded Skill
|
|
603
|
-
version policies/Hosts/user methods,
|
|
604
|
-
|
|
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
|
|
605
823
|
the TUI may pass explicit inclusion choices to the shared export core.
|
|
606
|
-
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.
|
|
607
826
|
|
|
608
827
|
Local paths and obvious credentials are not portable. Nonportable/invalid items
|
|
609
828
|
are excluded with reasons, as are unmanaged/App-owned and project Skills,
|
|
610
|
-
installation locations/evidence, approval receipts, installed-App state
|
|
611
|
-
|
|
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
|
|
612
832
|
profiles before sharing them—arbitrary secrets cannot be detected automatically.
|
|
613
833
|
|
|
614
834
|
## Import a user-global Profile
|
|
@@ -621,19 +841,26 @@ agent-depot import profile.json --yes
|
|
|
621
841
|
agent-depot import profile.json --no-apps --skill doctor-md-agents --yes
|
|
622
842
|
```
|
|
623
843
|
|
|
624
|
-
Import uses the same independent block/name filters as export
|
|
844
|
+
Import uses the same independent block/name filters as export, including
|
|
845
|
+
`--no-packages` and `--package <selection>`. It reports **add**,
|
|
625
846
|
**same** (skipped), and **conflict** (difference shown, skipped). Without `--yes`,
|
|
626
847
|
nothing is written. Existing choices and conflicting files are never replaced.
|
|
627
848
|
A profile produced by a newer Agent Depot version gives a warning, not an error.
|
|
628
849
|
|
|
629
850
|
Confirmed additions run in order: Source registration, user-global Skills through
|
|
630
|
-
normal refreshed install previews and safety checks, then App recipes
|
|
631
|
-
Hosts, version policies and user methods are retained. Even
|
|
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`,
|
|
632
854
|
each add-Skill preview shows its Source, Hosts, version policy and full argv for
|
|
633
855
|
every user-provided method, marked as running only after confirmation. The normal
|
|
634
856
|
install preview still appears before execution. Each failure is reported independently and remaining items
|
|
635
857
|
continue; any failed item makes the command exit non-zero.
|
|
636
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
|
+
|
|
637
864
|
Recipes, including those for other platforms, arrive **unapproved** in `apps/`.
|
|
638
865
|
A same-name destination recipe blocks import only when its platform overlaps the
|
|
639
866
|
incoming recipe: equal platforms overlap, and a missing platform overlaps every
|
|
@@ -653,3 +880,5 @@ The **5 Import/Export** TUI tab offers grouped checklists and the same shared-co
|
|
|
653
880
|
previews as the CLI. Exclusions and conflicts cannot be selected. Export creates
|
|
654
881
|
a new file only; empty exports write nothing (CLI reports an error). Import adds missing choices without replacing existing ones.
|
|
655
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.
|
package/dist/src/app-flow.js
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|