@iseoane/agent-depot 0.2.0 → 0.3.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 +47 -0
- package/README.md +117 -3
- package/dist/src/app-cli.js +172 -0
- package/dist/src/app-flow.js +457 -0
- package/dist/src/app-latest.js +63 -0
- package/dist/src/app-owned-skills.js +42 -0
- package/dist/src/app-recipes.js +116 -0
- package/dist/src/app-schema.js +45 -0
- package/dist/src/app-version.js +46 -0
- package/dist/src/atomic-file.js +26 -0
- package/dist/src/cli.js +1256 -0
- package/dist/src/git-source.js +409 -0
- package/dist/src/path-safety.js +31 -0
- package/dist/src/process-runner.js +41 -0
- package/dist/src/project-installation.js +1411 -0
- package/dist/src/project-manifest.js +511 -0
- package/dist/src/skill-adoption.js +77 -0
- package/dist/src/skill-discovery.js +467 -0
- package/dist/src/skill-hosts.js +103 -0
- package/dist/src/skill-install.js +374 -0
- package/dist/src/skill-removal.js +119 -0
- package/dist/src/skill-update.js +244 -0
- package/dist/src/source-state.js +240 -0
- package/dist/src/sources.js +391 -0
- package/dist/src/tui/adoption-actions.js +144 -0
- package/dist/src/tui/app-actions.js +157 -0
- package/dist/src/tui/app.js +63 -0
- package/dist/src/tui/batch.js +17 -0
- package/dist/src/tui/bulk-actions.js +243 -0
- package/dist/src/tui/catalog-actions.js +9 -0
- package/dist/src/tui/catalog-browse.js +59 -0
- package/dist/src/tui/catalog-filter.js +20 -0
- package/dist/src/tui/catalog-flow.js +107 -0
- package/dist/src/tui/catalog-installs.js +193 -0
- package/dist/src/tui/catalog-panel.js +47 -0
- package/dist/src/tui/catalog-state.js +92 -0
- package/dist/src/tui/catalog-tree.js +35 -0
- package/dist/src/tui/catalog-view.js +89 -0
- package/dist/src/tui/environment.js +1 -0
- package/dist/src/tui/installations-browse.js +136 -0
- package/dist/src/tui/installations-mode.js +1 -0
- package/dist/src/tui/installations-panel.js +27 -0
- package/dist/src/tui/installations-rows.js +11 -0
- package/dist/src/tui/installations-tree.js +80 -0
- package/dist/src/tui/installations-view.js +154 -0
- package/dist/src/tui/installations.js +172 -0
- package/dist/src/tui/keys.js +31 -0
- package/dist/src/tui/manage-actions.js +174 -0
- package/dist/src/tui/manage-panel.js +65 -0
- package/dist/src/tui/mode-keys.js +61 -0
- package/dist/src/tui/panel-parts.js +23 -0
- package/dist/src/tui/render.js +8 -0
- package/dist/src/tui/sources-mode.js +17 -0
- package/dist/src/tui/sources-view.js +276 -0
- package/dist/src/tui/theme.js +29 -0
- package/dist/src/tui/tree-navigation.js +68 -0
- package/dist/src/tui/tree.js +53 -0
- package/dist/src/tui/unmanaged-actions.js +178 -0
- package/dist/src/tui/updates-flow.js +89 -0
- package/dist/src/tui/updates-keys.js +104 -0
- package/dist/src/tui/updates-mode.js +1 -0
- package/dist/src/tui/updates-model.js +48 -0
- package/dist/src/tui/updates-panel.js +44 -0
- package/dist/src/tui/updates-view.js +101 -0
- package/dist/src/tui/updates.js +230 -0
- package/dist/src/tui/view-state.js +42 -0
- package/dist/src/tui/window.js +31 -0
- package/dist/src/unmanaged-removal.js +60 -0
- package/dist/src/update-batch.js +376 -0
- package/dist/src/update-flow.js +135 -0
- package/dist/src/usage-error.js +7 -0
- package/dist/src/user-global-skill-inventory.js +565 -0
- package/dist/src/user-method.js +14 -0
- package/package.json +7 -4
- package/skills/agent-depot-apprecipe/SKILL.md +44 -0
- package/skills/agent-depot-apprecipe/assets/example.md +25 -0
- package/skills/doctor-md-agents/scripts/test_collect_sessions.py +0 -780
- package/skills/doctor-md-agents/scripts/test_extract_rules.py +0 -530
- package/skills/doctor-md-agents/scripts/test_render_report.py +0 -383
- package/skills/doctor-md-agents/scripts/test_scan_corrections.py +0 -343
- package/skills/doctor-md-agents/scripts/test_verify_citations.py +0 -236
- package/skills/doctor-md-agents/scripts/testdata/old_extract_rules_pre_t1.py.txt +0 -507
- package/skills/doctor-md-skill/scripts/test_collect_sessions.py +0 -593
- package/skills/doctor-md-skill/scripts/test_detect_machine_sessions.py +0 -181
- package/skills/doctor-md-skill/scripts/test_render_report.py +0 -241
- package/skills/doctor-md-skill/scripts/test_verify_citations.py +0 -196
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,53 @@ 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.3.0] - 2026-10-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **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).
|
|
13
|
+
- `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.
|
|
14
|
+
- `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.
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- Recipe-declared `skills` names/globs exclude App-owned skills from the unmanaged adopt and remove flows.
|
|
18
|
+
- Built-in skill `agent-depot-apprecipe` guides an AI agent to research an App and write valid, platform-aware recipes.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `runProcess` accepts an opt-in `maxStderrBytes`; Source and Skill commands keep their previous behaviour.
|
|
23
|
+
|
|
24
|
+
### Internal
|
|
25
|
+
|
|
26
|
+
- Hermetic end-to-end coverage for Apps in the CLI and in a real terminal.
|
|
27
|
+
- Test processes ignore an exported `XDG_STATE_HOME`, so a developer's own state no longer affects the suite.
|
|
28
|
+
- 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.
|
|
29
|
+
|
|
30
|
+
## [0.2.1] - 2026-09-30
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- A project manifest that pins a built-in Skill to another Agent Depot version stays loadable; `install --manifest` skips that Skill with its reason and installs the rest.
|
|
35
|
+
- `install --manifest` now fails with exit code 1 ("Nothing to install") when every Skill is skipped, and prints a summary line when only some are.
|
|
36
|
+
- `--help`, `-h` and `help` print the usage and exit 0.
|
|
37
|
+
- The Updates view names the reason on the collapsed "cannot be checked" row.
|
|
38
|
+
- Unmanaged symlink identity includes birth time and size, so a replaced symlink is no longer mistaken for the original.
|
|
39
|
+
- Bulk actions in the TUI detect drift strictly: a change to the installation records outside the batch stops the affected item and leaves it untouched.
|
|
40
|
+
- The TUI only treats real terminal escape sequences as escape remnants, so pasting text such as `Ok` into an input keeps it.
|
|
41
|
+
- Updates scope keys (`t`, `p`, `g`) are looked up in a Map, so prototype property names can never match.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- The npm tarball is smaller: source maps and Skill tests are excluded.
|
|
46
|
+
- README links are absolute GitHub URLs so they work on npm.
|
|
47
|
+
|
|
48
|
+
### Internal
|
|
49
|
+
|
|
50
|
+
- Every source file is covered by a dependency-boundary zone (fallow audit); the version test path was fixed.
|
|
51
|
+
- Duplicated blocks removed (18 clone groups down to 3) in project installation and the TUI views.
|
|
52
|
+
- Cognitive complexity reduced in the catalog, updates and installations views and their key handlers; `installations-view.tsx` and `catalog-view.tsx` split into focused modules.
|
|
53
|
+
- Characterization tests for the complex functions in `src/path-safety.ts`.
|
|
54
|
+
|
|
8
55
|
## [0.2.0] - 2026-09-30
|
|
9
56
|
|
|
10
57
|
### BREAKING
|
package/README.md
CHANGED
|
@@ -47,7 +47,9 @@ Agent Depot is reported as not updatable (update Agent Depot first).
|
|
|
47
47
|
|
|
48
48
|
## Usage
|
|
49
49
|
|
|
50
|
-
With the package installed (or via `npx @iseoane/agent-depot`)
|
|
50
|
+
With the package installed (or via `npx @iseoane/agent-depot`). `agent-depot --help`
|
|
51
|
+
(also `-h` or `help`) prints the usage and exits 0; running it without arguments
|
|
52
|
+
or with an unknown command is a usage error and exits 1.
|
|
51
53
|
|
|
52
54
|
```sh
|
|
53
55
|
agent-depot source list
|
|
@@ -249,7 +251,10 @@ Sources require a fixed 40- or 64-character Git commit. For the built-in
|
|
|
249
251
|
Source, `latest` tracks the installed Agent Depot package, while a fixed policy
|
|
250
252
|
is accepted only when its value exactly equals that package's current version;
|
|
251
253
|
other values fail closed because the requested package version cannot be
|
|
252
|
-
reproduced.
|
|
254
|
+
reproduced. A manifest written by another Agent Depot stays loadable: a built-in
|
|
255
|
+
Skill pinned to a different version shows as unknown in `update check` ("pinned
|
|
256
|
+
to Agent Depot X; running Y") and is skipped by `install --manifest`, while the
|
|
257
|
+
other Skills keep working. The built-in Source does not support `--ref`, and the CLI rejects it
|
|
253
258
|
explicitly. External `latest` may track an explicit mutable safe ref, but an
|
|
254
259
|
immutable commit ref must use a fixed policy matching that commit. The
|
|
255
260
|
`--portable-v1` flag requires the validated portable format rule; compatibility
|
|
@@ -410,7 +415,7 @@ Safety model:
|
|
|
410
415
|
- Batch installs and removals apply item by item; one failure does not stop the
|
|
411
416
|
others and each item reports its own result.
|
|
412
417
|
|
|
413
|
-
See [CONTEXT.md](CONTEXT.md) for the domain terms and [docs/adr](docs/adr) for the
|
|
418
|
+
See [CONTEXT.md](https://github.com/iseoane/agent-depot/blob/main/CONTEXT.md) for the domain terms and [docs/adr](https://github.com/iseoane/agent-depot/tree/main/docs/adr) for the
|
|
414
419
|
decisions behind the TUI.
|
|
415
420
|
|
|
416
421
|
## Configuration and cache
|
|
@@ -463,3 +468,112 @@ entry points and the module-layer boundaries (`cli` -> `application` -> `core`
|
|
|
463
468
|
- `pnpm test:coverage`: runs the tests with built-in V8 coverage
|
|
464
469
|
(`NODE_V8_COVERAGE`, source maps) into `coverage/v8`, which Fallow reads.
|
|
465
470
|
It rebuilds `dist/` with source maps; no extra dependencies are needed.
|
|
471
|
+
|
|
472
|
+
## User-global App recipes
|
|
473
|
+
|
|
474
|
+
Place user-owned JSON recipes in `apps/` next to the per-user `sources.json`
|
|
475
|
+
(Linux/WSL: `${XDG_STATE_HOME:-~/.local/state}/agent-depot/apps`; Windows:
|
|
476
|
+
`%APPDATA%\Agent Depot\apps`). Recipes are never rewritten or supplied by Sources.
|
|
477
|
+
|
|
478
|
+
```sh
|
|
479
|
+
agent-depot app schema
|
|
480
|
+
agent-depot app validate /path/to/example.json
|
|
481
|
+
agent-depot app list
|
|
482
|
+
agent-depot app approve example --yes
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Validation previews every declared argv and its resolved executable but never
|
|
486
|
+
approves recipes (`validate --yes` is rejected). Review the preview before using
|
|
487
|
+
`app approve <name> --yes`: approval authorizes version commands, not just
|
|
488
|
+
read-only behavior. Private, atomically written approval receipts live in sibling
|
|
489
|
+
`app-approvals/` and are keyed to the canonical file path and exact content hash.
|
|
490
|
+
Changed bytes require approval again; restoring approved bytes restores approval.
|
|
491
|
+
`app list` never approves recipes. Invalid recipes are shown with
|
|
492
|
+
reasons; validation exits non-zero for invalid recipes. Other-platform recipes
|
|
493
|
+
are reported as not applicable and never executed. WSL counts as Linux and never
|
|
494
|
+
runs executables whose realpath is under `/mnt/<drive>/`; it skips those PATH
|
|
495
|
+
candidates while looking for Linux executables. Commands spawn the PATH candidate
|
|
496
|
+
rather than the realpath so shims and multicall binaries retain their identity.
|
|
497
|
+
|
|
498
|
+
Required fields are `name`, `install`, `update`, `uninstall`, and `version`.
|
|
499
|
+
Lifecycle steps contain `argv`, `manual`, or both; manual text is never executed.
|
|
500
|
+
`version` contains safe no-shell `argv` and a JavaScript regex `pattern` with one
|
|
501
|
+
capture group. Commands run from the user's home directory. An unsuccessful or
|
|
502
|
+
unresolvable version check means not installed. The schema describes structure;
|
|
503
|
+
`app validate` additionally enforces argv safety and regex semantics.
|
|
504
|
+
|
|
505
|
+
### Install, update and uninstall
|
|
506
|
+
|
|
507
|
+
```sh
|
|
508
|
+
agent-depot app install example # preview only; exits non-zero until confirmed
|
|
509
|
+
agent-depot app install example --yes
|
|
510
|
+
agent-depot app update example --yes
|
|
511
|
+
agent-depot app uninstall example --yes
|
|
512
|
+
agent-depot app setup example --host pi --host codex --yes
|
|
513
|
+
agent-depot app teardown example --host pi --yes
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Each step previews its argv/manual text, resolved executable, home working
|
|
517
|
+
directory and environment. Manual fallback occurs only when a process cannot
|
|
518
|
+
launch, never after a non-zero exit (reported with stdout/stderr). Run the shown
|
|
519
|
+
manual instructions yourself, then attest completion without rerunning argv:
|
|
520
|
+
|
|
521
|
+
```sh
|
|
522
|
+
agent-depot app install example --manual-done --yes
|
|
523
|
+
agent-depot app uninstall example --manual-done --yes
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
`--yes` alone does not attest manual completion. Install records a version only
|
|
527
|
+
when the version check succeeds. Uninstall retains tracking while version still
|
|
528
|
+
succeeds; `agent-depot app uninstall example --forget --yes` explicitly removes
|
|
529
|
+
tracking without running any recipe command, even if the recipe is gone.
|
|
530
|
+
Atomic per-App records live in sibling `app-installations/`. Uninstall does not
|
|
531
|
+
implicitly run Host teardown.
|
|
532
|
+
|
|
533
|
+
`app list` and `update check --scope user-global` resolve latest versions from
|
|
534
|
+
GitHub Releases, npm, or approved `latest.argv`. Missing or failed lookups are
|
|
535
|
+
unknown, never offered as updates. Public HTTP requests send no credentials,
|
|
536
|
+
reject redirects, cap responses at 1 MiB and time out after five seconds.
|
|
537
|
+
|
|
538
|
+
```sh
|
|
539
|
+
agent-depot update apply --scope user-global --all --yes
|
|
540
|
+
agent-depot update apply --scope user-global --skill 0 --app example --yes
|
|
541
|
+
agent-depot app update example --manual-done --yes
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
`--all` includes Apps and Skills; repeat `--app <name>` to select Apps alongside
|
|
545
|
+
Skills. App updates run **after Skills** in the same batch. Apps are user-global
|
|
546
|
+
only; `--all --scope project` skips Apps. Every selected step is previewed before the
|
|
547
|
+
batch confirmation; independent failures do not stop the remaining updates.
|
|
548
|
+
Manual batch outcomes are completed separately using `app update --manual-done`.
|
|
549
|
+
Updates record the version confirmed by the recipe's version command, not the
|
|
550
|
+
registry response. Updates require a changed version; unchanged versions fail
|
|
551
|
+
without rewriting installed tracking. Leading `v` and SemVer build-metadata-only
|
|
552
|
+
changes count as unchanged, just as in availability checks. Successful results show
|
|
553
|
+
`installed <old> -> <new> (latest <latest>)`, even when new differs from latest.
|
|
554
|
+
A confirmed manual update preview/fallback saves private version evidence in
|
|
555
|
+
sibling `app-pending-updates/`, so a later `--manual-done` invocation can compare
|
|
556
|
+
against the pre-update version. An attested manual completion consumes this
|
|
557
|
+
evidence even on failure; get a fresh confirmed preview before retrying.
|
|
558
|
+
|
|
559
|
+
Strict SemVer comparison ignores a leading `v`, orders prereleases and does not
|
|
560
|
+
offer a downgrade when installed is at or ahead of latest. Non-SemVer versions
|
|
561
|
+
use opaque inequality and **may offer a downgrade**. GitHub `/releases/latest`
|
|
562
|
+
excludes drafts and prereleases; npm uses package metadata's `dist-tags.latest`.
|
|
563
|
+
`app list` checks Apps concurrently, preserving listing order. One App check
|
|
564
|
+
failure does not hide other Apps or Skill updates. Lookup failures show reasons
|
|
565
|
+
(redirect, rate limit, HTTP status, timeout, oversized or invalid response).
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
App lifecycle commands are non-interactive: stdin is ignored (EOF), as in the
|
|
569
|
+
existing Skill command runner, and there is no execution timeout. Recipes must
|
|
570
|
+
use non-interactive flags; installers requiring a terminal belong in `manual`
|
|
571
|
+
steps. EOF lets stdin-based prompts fail rather than wait for input, but a
|
|
572
|
+
process ignoring EOF can still hang; cancel it manually. Captured App stdout
|
|
573
|
+
and stderr are each capped at 1 MiB; failures show only their last 4 KiB.
|
|
574
|
+
|
|
575
|
+
App setup/teardown accepts only Hosts declared for that action. All selected steps
|
|
576
|
+
are previewed before execution; each Host gets an outcome and failures do not stop
|
|
577
|
+
remaining Hosts. For manual completion, rerun the selected Host with
|
|
578
|
+
`--manual-done --yes`. This checks the App version, not Host wiring. Host actions
|
|
579
|
+
never change installed-App tracking.
|
|
@@ -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
|
+
}
|