agentbundle 0.9.0__tar.gz → 0.10.0__tar.gz

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 (126) hide show
  1. {agentbundle-0.9.0 → agentbundle-0.10.0}/PKG-INFO +9 -1
  2. {agentbundle-0.9.0 → agentbundle-0.10.0}/README.md +8 -0
  3. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/cli.py +44 -0
  4. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/_common.py +40 -0
  5. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/diff.py +4 -2
  6. agentbundle-0.10.0/agentbundle/commands/list_installed.py +223 -0
  7. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/uninstall.py +4 -1
  8. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/upgrade.py +72 -11
  9. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/version.py +1 -1
  10. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle.egg-info/PKG-INFO +9 -1
  11. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle.egg-info/SOURCES.txt +1 -0
  12. {agentbundle-0.9.0 → agentbundle-0.10.0}/pyproject.toml +1 -1
  13. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/__init__.py +0 -0
  14. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/__main__.py +0 -0
  15. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/_data/adapter.schema.json +0 -0
  16. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/_data/adapter.toml +0 -0
  17. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/_data/install-defaults.toml +0 -0
  18. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/_data/install-marker.py +0 -0
  19. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/_data/pack.schema.json +0 -0
  20. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/_data/plugin-manifest.derived.schema.json +0 -0
  21. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/_data/plugin-manifest.schema.json +0 -0
  22. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/_data/profile.schema.json +0 -0
  23. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/__init__.py +0 -0
  24. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/__main__.py +0 -0
  25. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/adapter_root_bins.py +0 -0
  26. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/adapters/__init__.py +0 -0
  27. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/adapters/claude_code.py +0 -0
  28. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/adapters/codex.py +0 -0
  29. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/adapters/copilot.py +0 -0
  30. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/adapters/cursor.py +0 -0
  31. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/adapters/gemini.py +0 -0
  32. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/adapters/kiro.py +0 -0
  33. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/adapters/kiro_cli.py +0 -0
  34. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/adapters/kiro_ide.py +0 -0
  35. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/contract.py +0 -0
  36. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/lint_packs.py +0 -0
  37. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/main.py +0 -0
  38. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/phase_order.py +0 -0
  39. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/projections/__init__.py +0 -0
  40. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/projections/codex_agent_toml.py +0 -0
  41. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/projections/copilot_agent_md.py +0 -0
  42. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/projections/copilot_hooks_json.py +0 -0
  43. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/projections/direct_directory.py +0 -0
  44. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/projections/gemini_command_toml.py +0 -0
  45. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/projections/hook_id.py +0 -0
  46. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/projections/kiro_ide_hook.py +0 -0
  47. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/projections/merge_into_agent_json.py +0 -0
  48. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/projections/merge_json.py +0 -0
  49. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/projections/user_merge_json.py +0 -0
  50. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/scope_rails.py +0 -0
  51. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/self_host.py +0 -0
  52. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/shared_libs.py +0 -0
  53. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/target_resolver.py +0 -0
  54. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/__init__.py +0 -0
  55. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_adapter_claude_code.py +0 -0
  56. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_adapter_codex.py +0 -0
  57. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_adapter_copilot.py +0 -0
  58. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_adapter_cursor.py +0 -0
  59. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_adapter_gemini.py +0 -0
  60. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_adapter_kiro.py +0 -0
  61. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_adapter_kiro_alias.py +0 -0
  62. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_adapter_kiro_cli.py +0 -0
  63. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_adapter_kiro_ide.py +0 -0
  64. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_adapter_root_bins_projection.py +0 -0
  65. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_architect_design_reviewer_projection.py +0 -0
  66. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_architect_design_reviewer_rubric_parity.py +0 -0
  67. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_build_ships_seeds.py +0 -0
  68. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_contract.py +0 -0
  69. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_contract_scope.py +0 -0
  70. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_contract_v07.py +0 -0
  71. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_contract_v08.py +0 -0
  72. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_direct_directory_cleanup.py +0 -0
  73. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_end_to_end_build.py +0 -0
  74. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_lint_agents_md_diataxis_block.py +0 -0
  75. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_lint_agents_md_legacy_block.py +0 -0
  76. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_lint_agents_md_risk_block.py +0 -0
  77. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_lint_packs.py +0 -0
  78. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +0 -0
  79. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_pack_schema.py +0 -0
  80. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_pack_schema_allowed_adapters.py +0 -0
  81. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_pack_schema_install.py +0 -0
  82. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_pipeline.py +0 -0
  83. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_plugin_manifest_schema.py +0 -0
  84. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_projectable_subset.py +0 -0
  85. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_projections_merge_json.py +0 -0
  86. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_scope_rails.py +0 -0
  87. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_security.py +0 -0
  88. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_self_host_check.py +0 -0
  89. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_self_host_fixture_guard.py +0 -0
  90. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_self_host_recipe_config.py +0 -0
  91. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_shared_libs_projection.py +0 -0
  92. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_shared_prefix_contract.py +0 -0
  93. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_shipped_packs_v07_declarations.py +0 -0
  94. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_shipped_packs_v08_declarations.py +0 -0
  95. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_user_libs_projection.py +0 -0
  96. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_validate.py +0 -0
  97. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/tests/test_writers_emit_lf.py +0 -0
  98. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/user_libs.py +0 -0
  99. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/build/validate.py +0 -0
  100. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/catalogue.py +0 -0
  101. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/categories.py +0 -0
  102. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/__init__.py +0 -0
  103. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/_drop_warning.py +0 -0
  104. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/adapt.py +0 -0
  105. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/config.py +0 -0
  106. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/init_state.py +0 -0
  107. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/install.py +0 -0
  108. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/list_packs.py +0 -0
  109. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/list_profiles.py +0 -0
  110. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/list_targets.py +0 -0
  111. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/profile.py +0 -0
  112. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/reconcile.py +0 -0
  113. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/render.py +0 -0
  114. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/scaffold.py +0 -0
  115. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/commands/validate.py +0 -0
  116. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/config.py +0 -0
  117. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/render.py +0 -0
  118. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/safety.py +0 -0
  119. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/scope.py +0 -0
  120. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/source_defaults.py +0 -0
  121. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/statelock.py +0 -0
  122. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle/user_config.py +0 -0
  123. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle.egg-info/dependency_links.txt +0 -0
  124. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle.egg-info/entry_points.txt +0 -0
  125. {agentbundle-0.9.0 → agentbundle-0.10.0}/agentbundle.egg-info/top_level.txt +0 -0
  126. {agentbundle-0.9.0 → agentbundle-0.10.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentbundle
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: npm for your coding agent. Install packs of skills, subagents, and hooks into any repo, for every major agent.
5
5
  Author-email: eugenelim <eugenelim@users.noreply.github.com>
6
6
  License: Apache-2.0 OR MIT
@@ -55,6 +55,12 @@ The install auto-detects your agent (`--adapter` overrides). Multi-IDE? Install
55
55
  agentbundle list-packs
56
56
  agentbundle list-profiles
57
57
 
58
+ # See what YOU have installed — pack, adapter, scope, version, and whether
59
+ # an upgrade is available (both scopes by default)
60
+ agentbundle list-installed
61
+ agentbundle list-installed --no-check # skip the catalogue check (offline, fast)
62
+ agentbundle list-installed --check-drift # also count locally edited files
63
+
58
64
  # Install a whole curated profile — a single-scope set of packs — in one command
59
65
  agentbundle install --profile inception
60
66
 
@@ -70,6 +76,8 @@ agentbundle uninstall --pack core --dry-run
70
76
  agentbundle uninstall --pack core --yes
71
77
  ```
72
78
 
79
+ **`list-installed`** reads your state files (not the catalogue) and reports every installed `(pack, adapter)` at each scope with its version and an `up-to-date` / `upgrade-available` / `unknown` status; it degrades to `unknown` (never an error) when the catalogue can't be resolved, and `--no-check` skips the check entirely.
80
+
73
81
  A **profile** is a catalogue-curated, single-scope set of packs you install in one command — it declares its own scope, so `--scope` doesn't apply. **Upgrade takes no version** — the target is whatever the catalogue you point at declares; to pin a past version, point the catalogue at that git ref. Install a pack that's **already there** and `agentbundle` offers to `upgrade` it instead (`--yes` runs it straight away).
74
82
 
75
83
  **Mutating commands ask first.** `uninstall`, the `--force` cleanup, and the upgrade offer all preview what they'll do and confirm before touching anything; `--dry-run` previews without writing, and `--yes` skips the prompt for non-interactive / CI use (where, without it, they refuse rather than hang).
@@ -37,6 +37,12 @@ The install auto-detects your agent (`--adapter` overrides). Multi-IDE? Install
37
37
  agentbundle list-packs
38
38
  agentbundle list-profiles
39
39
 
40
+ # See what YOU have installed — pack, adapter, scope, version, and whether
41
+ # an upgrade is available (both scopes by default)
42
+ agentbundle list-installed
43
+ agentbundle list-installed --no-check # skip the catalogue check (offline, fast)
44
+ agentbundle list-installed --check-drift # also count locally edited files
45
+
40
46
  # Install a whole curated profile — a single-scope set of packs — in one command
41
47
  agentbundle install --profile inception
42
48
 
@@ -52,6 +58,8 @@ agentbundle uninstall --pack core --dry-run
52
58
  agentbundle uninstall --pack core --yes
53
59
  ```
54
60
 
61
+ **`list-installed`** reads your state files (not the catalogue) and reports every installed `(pack, adapter)` at each scope with its version and an `up-to-date` / `upgrade-available` / `unknown` status; it degrades to `unknown` (never an error) when the catalogue can't be resolved, and `--no-check` skips the check entirely.
62
+
55
63
  A **profile** is a catalogue-curated, single-scope set of packs you install in one command — it declares its own scope, so `--scope` doesn't apply. **Upgrade takes no version** — the target is whatever the catalogue you point at declares; to pin a past version, point the catalogue at that git ref. Install a pack that's **already there** and `agentbundle` offers to `upgrade` it instead (`--yes` runs it straight away).
56
64
 
57
65
  **Mutating commands ask first.** `uninstall`, the `--force` cleanup, and the upgrade offer all preview what they'll do and confirm before touching anything; `--dry-run` previews without writing, and `--yes` skips the prompt for non-interactive / CI use (where, without it, they refuse rather than hang).
@@ -230,6 +230,50 @@ def _build_parser() -> argparse.ArgumentParser:
230
230
  )
231
231
  sp.set_defaults(func=_lazy("list_targets"))
232
232
 
233
+ # --- list-installed --- (state-file reader; both scopes by default)
234
+ sp = subparsers.add_parser(
235
+ "list-installed",
236
+ help=(
237
+ "List installed packs across user and repo scope with version and "
238
+ "up-to-date status (vs. a catalogue). Read-only."
239
+ ),
240
+ )
241
+ sp.add_argument(
242
+ "catalogue",
243
+ nargs="?",
244
+ default=None,
245
+ help=(
246
+ "Catalogue URI to compare installed versions against. Optional: when "
247
+ "omitted, the source is resolved from your config, an editable clone, "
248
+ "or the packaged default (RFC-0047). Ignored under --no-check."
249
+ ),
250
+ )
251
+ sp.add_argument("--root", default=".")
252
+ sp.add_argument(
253
+ "--scope",
254
+ choices=("repo", "user"),
255
+ help="Limit the listing to one scope. Default: both user and repo.",
256
+ )
257
+ sp.add_argument(
258
+ "--no-check",
259
+ "--offline",
260
+ dest="no_check",
261
+ action="store_true",
262
+ help=(
263
+ "Skip the catalogue up-to-date check (no network): print only "
264
+ "pack/adapter/scope/installed, omitting LATEST and STATUS."
265
+ ),
266
+ )
267
+ sp.add_argument(
268
+ "--check-drift",
269
+ action="store_true",
270
+ help=(
271
+ "Add a DRIFT column counting installed files locally edited since "
272
+ "install (on-disk SHA differs from the recorded SHA)."
273
+ ),
274
+ )
275
+ sp.set_defaults(func=_lazy("list_installed"))
276
+
233
277
  # --- scaffold --- (no --scope; always repo-targeted)
234
278
  sp = subparsers.add_parser(
235
279
  "scaffold",
@@ -16,6 +16,7 @@ from agentbundle.version import SPEC_VERSION
16
16
  if TYPE_CHECKING:
17
17
  import argparse
18
18
 
19
+ from agentbundle.config import PackState
19
20
  from agentbundle.safety import Tier
20
21
 
21
22
 
@@ -199,6 +200,45 @@ def _major(version: str) -> str:
199
200
  return v.split(".")[0]
200
201
 
201
202
 
203
+ def format_adapter_versions(rows: "dict[str, PackState]") -> str:
204
+ """Render sorted ``adapter (version)`` pairs for a multi-adapter
205
+ disambiguator message (RFC-0052).
206
+
207
+ ``rows`` is a ``{adapter: PackState}`` mapping (the shape
208
+ ``State.rows_for_pack`` returns). Output e.g. ``claude-code (0.9.0),
209
+ codex (0.9.0)`` — so a "pass --adapter" refusal names not just *which*
210
+ adapters are installed but *at what version*, making the next command
211
+ actionable without a second lookup.
212
+ """
213
+ return ", ".join(
214
+ f"{adapter} ({rows[adapter].installed_version})" for adapter in sorted(rows)
215
+ )
216
+
217
+
218
+ def count_drifted_files(pack_state: "PackState", root: Path) -> int:
219
+ """Count *pack_state*'s files whose on-disk SHA differs from the recorded SHA.
220
+
221
+ Row-scoped drift (Tier-2): compares each file against this row's own
222
+ recorded SHA — **not** ``safety.classify``, which resolves against the SHA
223
+ set across *all* rows and would undercount a co-owned path. A file absent on
224
+ disk is not drift (it is Tier-1, "about to (re)write"). Render-free: needs
225
+ only the loaded state plus on-disk bytes, so it is cheap enough to run
226
+ before an upgrade confirm. Shared by ``list-installed --check-drift`` and
227
+ the upgrade upfront drift notice.
228
+ """
229
+ from agentbundle.safety import sha256_file
230
+
231
+ count = 0
232
+ for relpath in pack_state.files:
233
+ on_disk = root / relpath
234
+ if not on_disk.exists():
235
+ continue
236
+ recorded = pack_state.file_sha(relpath)
237
+ if recorded is not None and sha256_file(on_disk) != recorded:
238
+ count += 1
239
+ return count
240
+
241
+
202
242
  # ---------------------------------------------------------------------------
203
243
  # Dry-run plan formatting (shared by `install --dry-run` and `upgrade --dry-run`)
204
244
  # ---------------------------------------------------------------------------
@@ -113,10 +113,12 @@ def run(args: argparse.Namespace) -> int:
113
113
  elif len(_rows) == 1:
114
114
  pack_state = next(iter(_rows.values()))
115
115
  elif len(_rows) > 1:
116
+ from agentbundle.commands._common import format_adapter_versions
117
+
116
118
  print(
117
119
  f"diff: {pack_name} installed for multiple adapters at "
118
- f"{effective_scope} scope ({', '.join(sorted(_rows))}); "
119
- f"pass --adapter",
120
+ f"{effective_scope} scope; pass --adapter to pick one: "
121
+ f"{format_adapter_versions(_rows)}",
120
122
  file=sys.stderr,
121
123
  )
122
124
  return 1
@@ -0,0 +1,223 @@
1
+ """``agentbundle list-installed`` subcommand.
2
+
3
+ Lists every installed ``(pack, adapter)`` row across the user and repo scope —
4
+ the state-file complement to ``list-packs`` (which queries a *catalogue* of
5
+ what is *available*). Reads the state files **read-only**; never writes.
6
+
7
+ By default it also resolves the catalogue once and joins each row against the
8
+ catalogue's ``pack.toml`` version to report a status (``up-to-date`` /
9
+ ``upgrade-available`` / ``unknown``). ``--no-check`` (alias ``--offline``) skips
10
+ that join and prints only the state-only columns. ``--check-drift`` adds a
11
+ per-row count of locally edited files (on-disk SHA ≠ the SHA recorded in state).
12
+
13
+ Exit codes:
14
+ 0 — success (including "no packs installed" and an unresolvable catalogue).
15
+ 1 — only on an argument/environment error the listing genuinely can't survive.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING
22
+
23
+ if TYPE_CHECKING:
24
+ import argparse
25
+
26
+ from agentbundle.config import State
27
+
28
+
29
+ def run(args: "argparse.Namespace") -> int:
30
+ """Entry point for ``agentbundle list-installed``."""
31
+ import sys
32
+
33
+ from agentbundle import scope as scope_mod
34
+ from agentbundle.config import ConfigError, load_state
35
+
36
+ requested_scope = getattr(args, "scope", None)
37
+ scopes = [requested_scope] if requested_scope else ["user", "repo"]
38
+ check = not getattr(args, "no_check", False)
39
+ want_drift = getattr(args, "check_drift", False)
40
+
41
+ # ── Gather (scope, root, State) read-only ─────────────────────────────────
42
+ repo_root = Path(getattr(args, "root", ".")).resolve()
43
+ scope_states: list[tuple[str, Path, "State"]] = []
44
+ for sc in scopes:
45
+ if sc == "repo":
46
+ base, state_path = repo_root, repo_root / ".agentbundle-state.toml"
47
+ else: # user
48
+ try:
49
+ base = scope_mod.resolve_user_root()
50
+ except scope_mod.UserScopeUnresolvable:
51
+ # No resolvable home → no user-scope state to list. Not an error.
52
+ continue
53
+ state_path = base / ".agentbundle" / "state.toml"
54
+ try:
55
+ state = load_state(state_path)
56
+ except ConfigError as exc:
57
+ # An incompatible (e.g. legacy-schema) state file is a hard refusal
58
+ # on read (RFC-0052). Warn and skip that scope rather than abort —
59
+ # the other scope stays listable.
60
+ print(f"list-installed: skipping {sc} scope: {exc}", file=sys.stderr)
61
+ continue
62
+ scope_states.append((sc, base, state))
63
+
64
+ rows = _collect_rows(scope_states)
65
+ if not rows:
66
+ where = f"{requested_scope} scope" if requested_scope else "user or repo scope"
67
+ print(f"no packs installed at {where}.")
68
+ return 0
69
+
70
+ # ── Catalogue join for LATEST / STATUS (unless --no-check) ─────────────────
71
+ catalogue_resolved = False
72
+ latest_by_pack: dict[str, str | None] = {}
73
+ if check:
74
+ catalogue_resolved, latest_by_pack = _resolve_latest(args)
75
+
76
+ # ── Drift count (only when requested) ─────────────────────────────────────
77
+ if want_drift:
78
+ from agentbundle.commands._common import count_drifted_files
79
+
80
+ for row in rows:
81
+ row["drift"] = count_drifted_files(row["_pack_state"], row["_root"])
82
+
83
+ _print_table(rows, check=check, want_drift=want_drift,
84
+ catalogue_resolved=catalogue_resolved, latest_by_pack=latest_by_pack)
85
+ return 0
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Helpers
90
+ # ---------------------------------------------------------------------------
91
+
92
+
93
+ def _collect_rows(
94
+ scope_states: list[tuple[str, Path, "State"]],
95
+ ) -> list[dict]:
96
+ """Flatten every ``(pack, adapter)`` row across scopes into sorted dicts.
97
+
98
+ Each row carries the display fields plus ``_pack_state`` / ``_root`` for a
99
+ later (optional) drift pass. Sorted by (pack, adapter, scope) so output is
100
+ deterministic.
101
+ """
102
+ rows: list[dict] = []
103
+ for scope_name, root, state in scope_states:
104
+ for (pack, adapter), ps in state.packs.items():
105
+ rows.append(
106
+ {
107
+ "pack": pack,
108
+ "adapter": adapter,
109
+ "scope": scope_name,
110
+ "installed": ps.installed_version,
111
+ "_pack_state": ps,
112
+ "_root": root,
113
+ }
114
+ )
115
+ rows.sort(key=lambda r: (r["pack"], r["adapter"], r["scope"]))
116
+ return rows
117
+
118
+
119
+ def _resolve_latest(args: "argparse.Namespace") -> tuple[bool, dict[str, str | None]]:
120
+ """Resolve the catalogue once and map each pack name to its latest version.
121
+
122
+ Returns ``(catalogue_resolved, {pack_name: version_or_None})``. On an
123
+ unresolvable catalogue returns ``(False, {})`` — the caller degrades every
124
+ row to ``unknown`` rather than failing. A pack present in the catalogue but
125
+ declaring an incompatible major version maps to ``None`` (also ``unknown``),
126
+ detected directly (``pack_spec_version`` + ``_major``) so no refusal is
127
+ printed — unlike ``check_spec_version_gate``, which prints and aborts.
128
+ """
129
+ from agentbundle.catalogue import CatalogueError, resolve_catalogue
130
+ from agentbundle.commands._common import _major, resolve_catalogue_uri
131
+ from agentbundle.config import ConfigError, load_pack_toml, pack_spec_version
132
+ from agentbundle.version import SPEC_VERSION
133
+
134
+ try:
135
+ catalogue_dir = resolve_catalogue(resolve_catalogue_uri(args))
136
+ except CatalogueError:
137
+ return False, {}
138
+
139
+ # Reuse list-packs' discovery so the layouts accepted stay identical.
140
+ from agentbundle.commands.list_packs import _discover_pack_dirs
141
+
142
+ cli_major = _major(SPEC_VERSION)
143
+ latest: dict[str, str | None] = {}
144
+ for pack_dir in _discover_pack_dirs(catalogue_dir):
145
+ try:
146
+ toml = load_pack_toml(pack_dir / "pack.toml")
147
+ except ConfigError:
148
+ continue
149
+ pack = toml.get("pack", {})
150
+ name = pack.get("name") or pack_dir.name
151
+ declared = pack_spec_version(toml)
152
+ if declared is not None and _major(declared) != cli_major:
153
+ latest[name] = None # incompatible major → unknown, don't compare
154
+ continue
155
+ latest[name] = pack.get("version") or None
156
+ return True, latest
157
+
158
+
159
+ def _version_key(version: str) -> tuple[int, ...] | None:
160
+ """Parse a dotted version into a comparable int tuple, or None if not numeric.
161
+
162
+ A non-numeric segment (a pre-release / build tag like ``1.2.0-rc1`` or
163
+ ``1.2.post1``) intentionally yields ``None`` → the row reads ``unknown``
164
+ rather than risking a wrong comparison; packs ship plain numeric versions.
165
+ """
166
+ parts = version.lstrip("v").split(".")
167
+ try:
168
+ return tuple(int(p) for p in parts)
169
+ except ValueError:
170
+ return None
171
+
172
+
173
+ def _status_for(installed: str, latest: str | None, *, catalogue_resolved: bool) -> str:
174
+ """Compute a row's status from (installed, latest, resolvable?).
175
+
176
+ ``up-to-date`` when installed is at or beyond latest; ``upgrade-available``
177
+ when latest is strictly greater; ``unknown`` when the catalogue could not be
178
+ resolved, the pack is absent from it, or either version isn't comparable.
179
+ """
180
+ if not catalogue_resolved or latest is None:
181
+ return "unknown"
182
+ a, b = _version_key(installed), _version_key(latest)
183
+ if a is None or b is None:
184
+ return "unknown"
185
+ # Zero-pad to equal length so `0.9` vs `0.9.0` compares equal rather than
186
+ # `(0,9) < (0,9,0)` reading as upgrade-available.
187
+ width = max(len(a), len(b))
188
+ a += (0,) * (width - len(a))
189
+ b += (0,) * (width - len(b))
190
+ return "upgrade-available" if b > a else "up-to-date"
191
+
192
+
193
+ def _print_table(
194
+ rows: list[dict],
195
+ *,
196
+ check: bool,
197
+ want_drift: bool,
198
+ catalogue_resolved: bool,
199
+ latest_by_pack: dict[str, str | None],
200
+ ) -> None:
201
+ """Render the installed-packs table to stdout."""
202
+ from agentbundle.commands._common import render_table
203
+
204
+ headers = ["PACK", "ADAPTER", "SCOPE", "INSTALLED"]
205
+ if check:
206
+ headers += ["LATEST", "STATUS"]
207
+ if want_drift:
208
+ headers += ["DRIFT"]
209
+
210
+ table_rows: list[list[str]] = []
211
+ for r in rows:
212
+ cells = [r["pack"], r["adapter"], r["scope"], r["installed"]]
213
+ if check:
214
+ latest = latest_by_pack.get(r["pack"])
215
+ status = _status_for(
216
+ r["installed"], latest, catalogue_resolved=catalogue_resolved
217
+ )
218
+ cells += [latest or "—", status]
219
+ if want_drift:
220
+ cells += [str(r.get("drift", 0))]
221
+ table_rows.append(cells)
222
+
223
+ render_table(headers, table_rows)
@@ -141,9 +141,12 @@ def run(args: "argparse.Namespace") -> int:
141
141
  elif len(rows) == 1:
142
142
  target_adapter = next(iter(rows))
143
143
  else:
144
+ from agentbundle.commands._common import format_adapter_versions
145
+
144
146
  print(
145
147
  f"uninstall: {pack_name} installed for multiple adapters at "
146
- f"{effective_scope} scope ({', '.join(sorted(rows))}); pass --adapter",
148
+ f"{effective_scope} scope; pass --adapter to pick one: "
149
+ f"{format_adapter_versions(rows)}",
147
150
  file=sys.stderr,
148
151
  )
149
152
  return 1
@@ -38,7 +38,6 @@ from typing import TYPE_CHECKING
38
38
  if TYPE_CHECKING:
39
39
  import argparse
40
40
 
41
- from agentbundle.config import State
42
41
  from agentbundle.user_config import UserConfig
43
42
 
44
43
 
@@ -182,9 +181,12 @@ def run(args: "argparse.Namespace") -> int:
182
181
  elif len(_rows) == 1:
183
182
  target_adapter = next(iter(_rows))
184
183
  elif len(_rows) > 1:
184
+ from agentbundle.commands._common import format_adapter_versions
185
+
185
186
  print(
186
187
  f"upgrade: {pack_name} installed for multiple adapters at "
187
- f"{effective_scope} scope ({', '.join(sorted(_rows))}); pass --adapter",
188
+ f"{effective_scope} scope; pass --adapter to pick one: "
189
+ f"{format_adapter_versions(_rows)}",
188
190
  file=sys.stderr,
189
191
  )
190
192
  return 1
@@ -322,6 +324,25 @@ def run(args: "argparse.Namespace") -> int:
322
324
  # write happens either way) and rare; left as-is to keep the confirm above
323
325
  # the expensive render.
324
326
 
327
+ # Upfront drift notice (whole-pack only): before the user confirms, tell
328
+ # them how many installed files have local edits that re-applying will
329
+ # preserve as `*.upstream` companions. Computed from on-disk-vs-state SHAs
330
+ # (no render needed); suppressed at zero and on a dry run (which writes
331
+ # nothing and already names companion actions in its plan). Gated to the
332
+ # whole-pack case: a per-primitive upgrade re-applies only that primitive's
333
+ # files, so a whole-pack count would mislead.
334
+ if not is_per_primitive and not getattr(args, "dry_run", False):
335
+ from agentbundle.commands._common import count_drifted_files
336
+
337
+ _drifted = count_drifted_files(pack_state, root)
338
+ if _drifted:
339
+ print(
340
+ f"upgrade: {_drifted} installed file(s) have local edits; your "
341
+ f"edits are preserved as .upstream companions where the file "
342
+ f"still ships.",
343
+ file=sys.stderr,
344
+ )
345
+
325
346
  # Confirm before the first write — unless ``--yes`` or ``--dry-run`` (which
326
347
  # writes nothing). A non-interactive stdin cannot answer the prompt, so
327
348
  # refuse and explain rather than block on ``input()``. ``--dry-run``
@@ -330,8 +351,10 @@ def run(args: "argparse.Namespace") -> int:
330
351
  if not getattr(args, "yes", False) and not getattr(args, "dry_run", False):
331
352
  if already_current:
332
353
  question = (
333
- f"{confirm_label} is already at {to_version}. Re-apply at "
334
- f"{effective_scope} scope (repairs local drift)? [y/N] "
354
+ f"{confirm_label} is already at {to_version} at "
355
+ f"{effective_scope} scope. Re-apply to restore any missing or "
356
+ f"reset any unmodified bundle files? Your local edits are kept "
357
+ f"as .upstream companions. [y/N] "
335
358
  )
336
359
  else:
337
360
  question = (
@@ -727,15 +750,19 @@ def run(args: "argparse.Namespace") -> int:
727
750
 
728
751
  if is_per_primitive:
729
752
  ptype, _src_dir = _PRIMITIVE_FLAG_MAP[prim_flag]
730
- print(
731
- f"upgraded: {pack_name} {ptype}/{prim_name} @ "
732
- f"{effective_scope} {from_version} -> {to_version}"
733
- )
753
+ recap_label = f"{pack_name} {ptype}/{prim_name}"
734
754
  else:
735
- print(
736
- f"upgraded: {pack_name} @ {effective_scope} "
737
- f"{from_version} -> {to_version}"
755
+ recap_label = pack_name
756
+ print(
757
+ _format_recap(
758
+ recap_label,
759
+ effective_scope,
760
+ from_version,
761
+ to_version,
762
+ already_current=already_current,
763
+ companion_count=len(companions),
738
764
  )
765
+ )
739
766
  return 0
740
767
 
741
768
 
@@ -744,6 +771,40 @@ def run(args: "argparse.Namespace") -> int:
744
771
  # ---------------------------------------------------------------------------
745
772
 
746
773
 
774
+ def _format_recap(
775
+ label: str,
776
+ scope: str,
777
+ from_version: str,
778
+ to_version: str,
779
+ *,
780
+ already_current: bool,
781
+ companion_count: int,
782
+ ) -> str:
783
+ """Render the post-upgrade recap line, honestly distinguishing the cases.
784
+
785
+ Three verdicts, driven by the only signals available without changing what
786
+ the walk writes (the walk re-writes bundle-owned files unconditionally, so
787
+ "nothing changed on disk" is not knowable):
788
+
789
+ - version changed → ``upgraded: <label> @ <scope> <from> -> <to>``
790
+ - same version, no local edits preserved →
791
+ ``re-applied: <label> @ <scope> <version> (already current)``
792
+ - same version, local edits preserved as companions →
793
+ ``re-applied: <label> @ <scope> <version> — N file(s) had local edits,
794
+ kept as .upstream companions``
795
+
796
+ It never prints ``upgraded: … X -> X`` for a same-version re-apply.
797
+ """
798
+ if not already_current:
799
+ return f"upgraded: {label} @ {scope} {from_version} -> {to_version}"
800
+ if companion_count:
801
+ return (
802
+ f"re-applied: {label} @ {scope} {to_version} — "
803
+ f"{companion_count} file(s) had local edits, kept as .upstream companions"
804
+ )
805
+ return f"re-applied: {label} @ {scope} {to_version} (already current)"
806
+
807
+
747
808
  def _compute_new_wiring_rows(
748
809
  pack_dir: Path,
749
810
  pack_name: str,
@@ -14,7 +14,7 @@ import tomllib
14
14
  from importlib.resources import files
15
15
  from pathlib import Path
16
16
 
17
- CLI_VERSION = "0.9.0"
17
+ CLI_VERSION = "0.10.0"
18
18
 
19
19
  _HERE = Path(__file__).resolve().parent
20
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentbundle
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: npm for your coding agent. Install packs of skills, subagents, and hooks into any repo, for every major agent.
5
5
  Author-email: eugenelim <eugenelim@users.noreply.github.com>
6
6
  License: Apache-2.0 OR MIT
@@ -55,6 +55,12 @@ The install auto-detects your agent (`--adapter` overrides). Multi-IDE? Install
55
55
  agentbundle list-packs
56
56
  agentbundle list-profiles
57
57
 
58
+ # See what YOU have installed — pack, adapter, scope, version, and whether
59
+ # an upgrade is available (both scopes by default)
60
+ agentbundle list-installed
61
+ agentbundle list-installed --no-check # skip the catalogue check (offline, fast)
62
+ agentbundle list-installed --check-drift # also count locally edited files
63
+
58
64
  # Install a whole curated profile — a single-scope set of packs — in one command
59
65
  agentbundle install --profile inception
60
66
 
@@ -70,6 +76,8 @@ agentbundle uninstall --pack core --dry-run
70
76
  agentbundle uninstall --pack core --yes
71
77
  ```
72
78
 
79
+ **`list-installed`** reads your state files (not the catalogue) and reports every installed `(pack, adapter)` at each scope with its version and an `up-to-date` / `upgrade-available` / `unknown` status; it degrades to `unknown` (never an error) when the catalogue can't be resolved, and `--no-check` skips the check entirely.
80
+
73
81
  A **profile** is a catalogue-curated, single-scope set of packs you install in one command — it declares its own scope, so `--scope` doesn't apply. **Upgrade takes no version** — the target is whatever the catalogue you point at declares; to pin a past version, point the catalogue at that git ref. Install a pack that's **already there** and `agentbundle` offers to `upgrade` it instead (`--yes` runs it straight away).
74
82
 
75
83
  **Mutating commands ask first.** `uninstall`, the `--force` cleanup, and the upgrade offer all preview what they'll do and confirm before touching anything; `--dry-run` previews without writing, and `--yes` skips the prompt for non-interactive / CI use (where, without it, they refuse rather than hang).
@@ -111,6 +111,7 @@ agentbundle/commands/config.py
111
111
  agentbundle/commands/diff.py
112
112
  agentbundle/commands/init_state.py
113
113
  agentbundle/commands/install.py
114
+ agentbundle/commands/list_installed.py
114
115
  agentbundle/commands/list_packs.py
115
116
  agentbundle/commands/list_profiles.py
116
117
  agentbundle/commands/list_targets.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentbundle"
7
- version = "0.9.0"
7
+ version = "0.10.0"
8
8
  description = "npm for your coding agent. Install packs of skills, subagents, and hooks into any repo, for every major agent."
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []
File without changes