@event4u/agent-config 2.4.0 → 2.6.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +29 -0
- package/package.json +1 -1
- package/scripts/_cli/cmd_uninstall.py +16 -6
- package/scripts/_cli/cmd_update.py +7 -6
- package/scripts/_lib/claude_desktop_bundler.py +94 -6
- package/scripts/_lib/installed_lock.py +23 -4
- package/scripts/install.py +33 -26
package/CHANGELOG.md
CHANGED
|
@@ -343,6 +343,35 @@ our recommendation order, not its support status.
|
|
|
343
343
|
users" tension without removing any path that an existing user
|
|
344
344
|
might rely on.
|
|
345
345
|
|
|
346
|
+
## [2.6.0](https://github.com/event4u-app/agent-config/compare/2.4.1...2.6.0) (2026-05-13)
|
|
347
|
+
|
|
348
|
+
### Features
|
|
349
|
+
|
|
350
|
+
* **claude-desktop:** bundle commands as desktop skills ([50cd319](https://github.com/event4u-app/agent-config/commit/50cd31988ac571ecdf883c5d2c6d62c209820ebc))
|
|
351
|
+
|
|
352
|
+
### Other
|
|
353
|
+
|
|
354
|
+
* 2.5.0 ([1da0e10](https://github.com/event4u-app/agent-config/commit/1da0e1014b9cf24cf41d820795f96f2668ce738c))
|
|
355
|
+
|
|
356
|
+
Tests: 3530 (+9 since 2.4.1)
|
|
357
|
+
|
|
358
|
+
## [2.5.0](https://github.com/event4u-app/agent-config/compare/2.4.1...2.5.0) (2026-05-13)
|
|
359
|
+
|
|
360
|
+
### Features
|
|
361
|
+
|
|
362
|
+
* **claude-desktop:** bundle commands as desktop skills ([50cd319](https://github.com/event4u-app/agent-config/commit/50cd31988ac571ecdf883c5d2c6d62c209820ebc))
|
|
363
|
+
|
|
364
|
+
Tests: 3530 (+9 since 2.4.1)
|
|
365
|
+
|
|
366
|
+
## [2.4.1](https://github.com/event4u-app/agent-config/compare/2.4.0...2.4.1) (2026-05-13)
|
|
367
|
+
|
|
368
|
+
### Bug Fixes
|
|
369
|
+
|
|
370
|
+
* **install:** write lockfile to canonical event4u namespace ([ca57607](https://github.com/event4u-app/agent-config/commit/ca5760785f150903bcad74e278b59e534f83634d))
|
|
371
|
+
* **install:** source global deploy from .agent-src/ subdirectories ([f151caf](https://github.com/event4u-app/agent-config/commit/f151caf854d9ce8519bc244d441a7c8bf73e7fde))
|
|
372
|
+
|
|
373
|
+
Tests: 3521 (+0 since 2.4.0)
|
|
374
|
+
|
|
346
375
|
## [2.4.0](https://github.com/event4u-app/agent-config/compare/2.3.0...2.4.0) (2026-05-13)
|
|
347
376
|
|
|
348
377
|
### Features
|
package/package.json
CHANGED
|
@@ -422,6 +422,7 @@ def _uninstall_project(opts: argparse.Namespace) -> int:
|
|
|
422
422
|
|
|
423
423
|
def _uninstall_global(opts: argparse.Namespace) -> int:
|
|
424
424
|
lock_path = installed_lock.lockfile_path()
|
|
425
|
+
write_path = installed_lock.lockfile_write_path()
|
|
425
426
|
lock = installed_lock.read_lockfile(lock_path)
|
|
426
427
|
if lock is None and not opts.force:
|
|
427
428
|
print(f"❌ no global lockfile at {lock_path}", file=sys.stderr)
|
|
@@ -442,15 +443,24 @@ def _uninstall_global(opts: argparse.Namespace) -> int:
|
|
|
442
443
|
removed_names.append(tool)
|
|
443
444
|
if lock is not None and not opts.dry_run:
|
|
444
445
|
remaining = [t for t in lock.get("tools", []) if t not in tools]
|
|
446
|
+
version = lock.get("agent_config_version", "")
|
|
445
447
|
if remaining:
|
|
446
|
-
installed_lock.write_lockfile(remaining,
|
|
448
|
+
installed_lock.write_lockfile(version, remaining, path=write_path)
|
|
449
|
+
# Drop the legacy file if it differs from the canonical write
|
|
450
|
+
# target so the namespace migration completes on uninstall.
|
|
451
|
+
if lock_path != write_path:
|
|
452
|
+
try:
|
|
453
|
+
lock_path.unlink()
|
|
454
|
+
except OSError:
|
|
455
|
+
pass
|
|
447
456
|
print(f"✅ lockfile updated ({len(tools)} entries removed, {len(remaining)} kept)")
|
|
448
457
|
else:
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
458
|
+
for target in {lock_path, write_path}:
|
|
459
|
+
try:
|
|
460
|
+
target.unlink()
|
|
461
|
+
except OSError:
|
|
462
|
+
pass
|
|
463
|
+
print(f"✅ lockfile deleted ({write_path})")
|
|
454
464
|
return 0
|
|
455
465
|
|
|
456
466
|
|
|
@@ -243,17 +243,18 @@ def _refresh_global_lockfile(version: str, *, out=sys.stdout) -> None:
|
|
|
243
243
|
when ``update`` flips the pin. Atomic write goes through
|
|
244
244
|
``installed_lock.write_lockfile``.
|
|
245
245
|
"""
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
read_path = installed_lock.lockfile_path()
|
|
247
|
+
write_path = installed_lock.lockfile_write_path()
|
|
248
|
+
existing = installed_lock.read_lockfile(path=read_path)
|
|
248
249
|
if existing is None:
|
|
249
250
|
return
|
|
250
251
|
recorded = existing.get("agent_config_version")
|
|
251
252
|
tools = list(existing.get("tools", []))
|
|
252
|
-
if recorded == version:
|
|
253
|
-
print(f"ℹ️ {
|
|
253
|
+
if recorded == version and read_path == write_path:
|
|
254
|
+
print(f"ℹ️ {write_path} already records {version}.", file=out)
|
|
254
255
|
return
|
|
255
|
-
installed_lock.write_lockfile(version, tools, path=
|
|
256
|
-
print(f"✅ Refreshed global lockfile at {
|
|
256
|
+
installed_lock.write_lockfile(version, tools, path=write_path)
|
|
257
|
+
print(f"✅ Refreshed global lockfile at {write_path}.", file=out)
|
|
257
258
|
|
|
258
259
|
|
|
259
260
|
def _detect_installed_version() -> str:
|
|
@@ -2,21 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Claude Desktop has no filesystem convention for skills; the Customize →
|
|
4
4
|
Skills UI accepts a ZIP per skill via the Upload button. This module
|
|
5
|
-
walks ``<package_root>/.
|
|
6
|
-
``<skill-name>.zip`` per directory into ``dest_dir``.
|
|
5
|
+
walks ``<package_root>/.agent-src/skills/*`` and produces one
|
|
6
|
+
``<skill-name>.zip`` per directory into ``dest_dir``. It additionally
|
|
7
|
+
walks ``<package_root>/.agent-src/commands/`` and produces one
|
|
8
|
+
``<command-slug>.zip`` per command ``.md`` file so Claude Desktop sees
|
|
9
|
+
the same surface that Claude Code exposes via the ``.claude/skills/``
|
|
10
|
+
symlink wrapper layer.
|
|
7
11
|
|
|
8
12
|
Contract:
|
|
9
13
|
|
|
10
14
|
- Each ZIP contains ``SKILL.md`` plus every sibling file under the same
|
|
11
15
|
directory (recursive). Symlinks are dereferenced so the ZIP is
|
|
12
16
|
self-contained.
|
|
17
|
+
- Command bundles wrap a single ``.agent-src/commands/<path>.md`` file
|
|
18
|
+
as ``SKILL.md`` inside the ZIP. Nested commands flatten to
|
|
19
|
+
``<cluster>-<leaf>`` slugs (e.g. ``council/default.md`` →
|
|
20
|
+
``council-default.zip``) to mirror ``compress.py``.
|
|
13
21
|
- Exclusions: ``.git*``, ``__pycache__``, ``*.pyc`` — matched on the
|
|
14
22
|
basename of any path component.
|
|
15
23
|
- A skill folder without a ``SKILL.md`` is skipped (defensive: avoids
|
|
16
24
|
shipping Claude-Code orchestrator stubs that don't follow the
|
|
17
25
|
Anthropic skill schema).
|
|
26
|
+
- Command files named ``AGENTS.md`` are skipped (cluster authoring docs,
|
|
27
|
+
not invocable commands).
|
|
28
|
+
- A command slug that collides with an existing skill name is skipped —
|
|
29
|
+
the real skill bundle wins, matching ``compress.generate_claude_commands``.
|
|
18
30
|
- Writes are atomic via tempfile → ``os.replace``.
|
|
19
|
-
- Idempotent: each ZIP gets a sibling ``<
|
|
31
|
+
- Idempotent: each ZIP gets a sibling ``<slug>.sha256`` recording
|
|
20
32
|
the manifest digest. If the recomputed digest matches the recorded
|
|
21
33
|
one, the existing ZIP is left untouched (unless ``force=True``).
|
|
22
34
|
"""
|
|
@@ -51,8 +63,8 @@ def _walk_skill_files(skill_dir: Path) -> list[tuple[Path, tuple[str, ...]]]:
|
|
|
51
63
|
"""Return ``[(abs_path, rel_parts), ...]`` for every file in the skill.
|
|
52
64
|
|
|
53
65
|
Symlinks are followed (``os.walk(..., followlinks=True)``) so a
|
|
54
|
-
bundle from a symlinked entry under ``.
|
|
55
|
-
actual target content, not a dangling symlink.
|
|
66
|
+
bundle from a symlinked entry under ``.agent-src/skills/`` contains
|
|
67
|
+
the actual target content, not a dangling symlink.
|
|
56
68
|
"""
|
|
57
69
|
out: list[tuple[Path, tuple[str, ...]]] = []
|
|
58
70
|
resolved = skill_dir.resolve()
|
|
@@ -121,7 +133,7 @@ def build_skill_bundles(
|
|
|
121
133
|
``curation`` optionally restricts the build to the given skill
|
|
122
134
|
names; ``None`` bundles every skill folder containing ``SKILL.md``.
|
|
123
135
|
"""
|
|
124
|
-
skills_root = package_root / ".
|
|
136
|
+
skills_root = package_root / ".agent-src" / "skills"
|
|
125
137
|
if not skills_root.is_dir():
|
|
126
138
|
return []
|
|
127
139
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -148,3 +160,79 @@ def build_skill_bundles(
|
|
|
148
160
|
digest_path.write_text(digest + "\n", encoding="utf-8")
|
|
149
161
|
written.append(zip_path)
|
|
150
162
|
return written
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _command_slug(source_file: Path, commands_root: Path) -> str:
|
|
166
|
+
"""Return the flat slug for a command source file.
|
|
167
|
+
|
|
168
|
+
Mirrors ``scripts/compress.py::_command_slug``: top-level commands
|
|
169
|
+
keep their stem (``commit.md`` → ``commit``); nested commands flatten
|
|
170
|
+
the relative path with ``-`` (``council/default.md`` →
|
|
171
|
+
``council-default``).
|
|
172
|
+
"""
|
|
173
|
+
rel = source_file.relative_to(commands_root)
|
|
174
|
+
return "-".join(rel.with_suffix("").parts)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _iter_command_files(commands_root: Path) -> Iterable[Path]:
|
|
178
|
+
"""Yield every command ``.md`` file under ``commands_root`` (recursive).
|
|
179
|
+
|
|
180
|
+
Skips ``AGENTS.md`` cluster authoring docs, matching
|
|
181
|
+
``scripts/compress.py::_iter_commands``.
|
|
182
|
+
"""
|
|
183
|
+
for source_file in sorted(commands_root.rglob("*.md")):
|
|
184
|
+
if source_file.name == "AGENTS.md":
|
|
185
|
+
continue
|
|
186
|
+
yield source_file
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def build_command_bundles(
|
|
190
|
+
package_root: Path,
|
|
191
|
+
dest_dir: Path,
|
|
192
|
+
force: bool = False,
|
|
193
|
+
curation: Optional[list[str]] = None,
|
|
194
|
+
) -> list[Path]:
|
|
195
|
+
"""Build per-command ZIPs under ``dest_dir``.
|
|
196
|
+
|
|
197
|
+
Each ZIP contains a single ``SKILL.md`` whose bytes are the source
|
|
198
|
+
command ``.md`` file — same wrapping pattern that
|
|
199
|
+
``compress.generate_claude_commands`` uses for Claude Code via
|
|
200
|
+
``.claude/skills/<slug>/SKILL.md`` symlinks.
|
|
201
|
+
|
|
202
|
+
Slugs that collide with an existing skill folder under
|
|
203
|
+
``<package_root>/.agent-src/skills/`` are skipped so the real skill
|
|
204
|
+
bundle wins.
|
|
205
|
+
|
|
206
|
+
Returns the list of ZIP paths that were (re-)written this call. ZIPs
|
|
207
|
+
skipped because their content digest matched the existing sidecar
|
|
208
|
+
are not in the returned list (but remain on disk).
|
|
209
|
+
|
|
210
|
+
``curation`` optionally restricts the build to the given command
|
|
211
|
+
slugs; ``None`` bundles every command file.
|
|
212
|
+
"""
|
|
213
|
+
commands_root = package_root / ".agent-src" / "commands"
|
|
214
|
+
if not commands_root.is_dir():
|
|
215
|
+
return []
|
|
216
|
+
skills_root = package_root / ".agent-src" / "skills"
|
|
217
|
+
skill_names: set[str] = set()
|
|
218
|
+
if skills_root.is_dir():
|
|
219
|
+
skill_names = {entry.name for entry in skills_root.iterdir() if entry.is_dir()}
|
|
220
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
written: list[Path] = []
|
|
222
|
+
for source_file in _iter_command_files(commands_root):
|
|
223
|
+
slug = _command_slug(source_file, commands_root)
|
|
224
|
+
if slug in skill_names:
|
|
225
|
+
continue
|
|
226
|
+
if curation is not None and slug not in curation:
|
|
227
|
+
continue
|
|
228
|
+
files = [(source_file.resolve(), ("SKILL.md",))]
|
|
229
|
+
digest = _manifest_digest(files)
|
|
230
|
+
zip_path = dest_dir / f"{slug}.zip"
|
|
231
|
+
digest_path = dest_dir / f"{slug}.sha256"
|
|
232
|
+
recorded = digest_path.read_text(encoding="utf-8").strip() if digest_path.exists() else ""
|
|
233
|
+
if not force and recorded == digest and zip_path.exists():
|
|
234
|
+
continue
|
|
235
|
+
_atomic_write_zip(zip_path, files)
|
|
236
|
+
digest_path.write_text(digest + "\n", encoding="utf-8")
|
|
237
|
+
written.append(zip_path)
|
|
238
|
+
return written
|
|
@@ -50,7 +50,7 @@ _TOOL_RE = re.compile(r"^\s*-\s*([A-Za-z0-9_\-.]+)\s*$")
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def lockfile_path(env: Optional[dict] = None) -> Path:
|
|
53
|
-
"""Return the active lockfile path
|
|
53
|
+
"""Return the active lockfile path for **reads**, honoring overrides.
|
|
54
54
|
|
|
55
55
|
Resolution order:
|
|
56
56
|
|
|
@@ -59,9 +59,10 @@ def lockfile_path(env: Optional[dict] = None) -> Path:
|
|
|
59
59
|
3. ``~/.config/agent-config/installed.lock`` (legacy fallback, read-only).
|
|
60
60
|
4. Canonical write target under the new namespace (Step 2 fallthrough).
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
Readers benefit from (3) so pre-2.4 installs keep working while the
|
|
63
|
+
migration shim has not yet run. Writers must use
|
|
64
|
+
:func:`lockfile_write_path` so a stale legacy file does not anchor
|
|
65
|
+
subsequent writes to the deprecated location.
|
|
65
66
|
"""
|
|
66
67
|
env = env if env is not None else os.environ
|
|
67
68
|
override = env.get(LOCKFILE_ENV)
|
|
@@ -73,6 +74,24 @@ def lockfile_path(env: Optional[dict] = None) -> Path:
|
|
|
73
74
|
return user_global_paths.write_target("installed.lock", env=env)
|
|
74
75
|
|
|
75
76
|
|
|
77
|
+
def lockfile_write_path(env: Optional[dict] = None) -> Path:
|
|
78
|
+
"""Return the canonical write target for the lockfile.
|
|
79
|
+
|
|
80
|
+
Unlike :func:`lockfile_path`, this never falls back to the legacy
|
|
81
|
+
``~/.config/agent-config/`` location. Honors the
|
|
82
|
+
``$AGENT_CONFIG_INSTALLED_LOCK`` override for tests, otherwise pins
|
|
83
|
+
to ``~/.event4u/agent-config/installed.lock``. Callers in
|
|
84
|
+
``init``, ``update``, and ``uninstall`` use this so writes always
|
|
85
|
+
land in the new namespace regardless of whether a stale legacy
|
|
86
|
+
lockfile is still present.
|
|
87
|
+
"""
|
|
88
|
+
env = env if env is not None else os.environ
|
|
89
|
+
override = env.get(LOCKFILE_ENV)
|
|
90
|
+
if override:
|
|
91
|
+
return Path(override).expanduser()
|
|
92
|
+
return user_global_paths.write_target("installed.lock", env=env)
|
|
93
|
+
|
|
94
|
+
|
|
76
95
|
def read_lockfile(path: Optional[Path] = None) -> Optional[dict]:
|
|
77
96
|
"""Parse ``path`` (or the default) into a dict; return ``None`` if absent.
|
|
78
97
|
|
package/scripts/install.py
CHANGED
|
@@ -2092,36 +2092,41 @@ PROJECT_BRIDGE_MARKERS = {
|
|
|
2092
2092
|
# ``_write_claude_desktop_marker`` rather than via this map.
|
|
2093
2093
|
#
|
|
2094
2094
|
# Tools that follow the markdown-skills convention (anchors lifted from
|
|
2095
|
-
# nextlevelbuilder/ui-ux-pro-max-skill) deploy
|
|
2096
|
-
#
|
|
2097
|
-
#
|
|
2095
|
+
# nextlevelbuilder/ui-ux-pro-max-skill) deploy the universal Anthropic-
|
|
2096
|
+
# shaped skill bundle — sourced from ``.agent-src/`` (the npm-shipped
|
|
2097
|
+
# canonical asset tree) — into ``<anchor>/skills/`` (or
|
|
2098
|
+
# ``<anchor>/steering/`` for kiro). ``.agent-src/rules`` is also copied
|
|
2098
2099
|
# where the destination is a true rules-aware tool root.
|
|
2100
|
+
#
|
|
2101
|
+
# All source paths reference ``.agent-src/<subdir>`` because that is the
|
|
2102
|
+
# only asset tree included in the npm tarball (see ``package.json#files``).
|
|
2103
|
+
# The legacy ``.augment/``, ``.claude/``, ``.cursor/`` projections only
|
|
2104
|
+
# exist in the development checkout — they are not shipped.
|
|
2099
2105
|
_CLAUDE_SKILL_BUNDLE: list[tuple[str, str]] = [
|
|
2100
|
-
(".
|
|
2101
|
-
(".
|
|
2102
|
-
(".
|
|
2106
|
+
(".agent-src/rules", "rules"),
|
|
2107
|
+
(".agent-src/skills", "skills"),
|
|
2108
|
+
(".agent-src/personas", "personas"),
|
|
2103
2109
|
]
|
|
2104
2110
|
GLOBAL_DEPLOY_SOURCES: dict[str, list[tuple[str, str]]] = {
|
|
2105
2111
|
"claude-code": _CLAUDE_SKILL_BUNDLE,
|
|
2106
2112
|
"augment": [
|
|
2107
|
-
(".
|
|
2108
|
-
(".
|
|
2109
|
-
(".
|
|
2110
|
-
(".
|
|
2111
|
-
(".
|
|
2112
|
-
(".
|
|
2113
|
+
(".agent-src/rules", "rules"),
|
|
2114
|
+
(".agent-src/skills", "skills"),
|
|
2115
|
+
(".agent-src/commands", "commands"),
|
|
2116
|
+
(".agent-src/contexts", "contexts"),
|
|
2117
|
+
(".agent-src/personas", "personas"),
|
|
2118
|
+
(".agent-src/templates", "templates"),
|
|
2113
2119
|
],
|
|
2114
2120
|
"cursor": [
|
|
2115
|
-
(".
|
|
2116
|
-
(".
|
|
2117
|
-
(".
|
|
2121
|
+
(".agent-src/rules", "rules"),
|
|
2122
|
+
(".agent-src/commands", "commands"),
|
|
2123
|
+
(".agent-src/personas", "personas"),
|
|
2118
2124
|
],
|
|
2119
2125
|
"windsurf": [
|
|
2120
|
-
(".
|
|
2121
|
-
(".windsurf/workflows", "workflows"),
|
|
2126
|
+
(".agent-src/rules", "rules"),
|
|
2122
2127
|
],
|
|
2123
2128
|
"cline": [
|
|
2124
|
-
(".
|
|
2129
|
+
(".agent-src/rules", ""),
|
|
2125
2130
|
],
|
|
2126
2131
|
# Markdown-skills tools — mirror the universal skill bundle into the
|
|
2127
2132
|
# tool-specific anchor. Subpath matches the reference repo's
|
|
@@ -2142,9 +2147,9 @@ GLOBAL_DEPLOY_SOURCES: dict[str, list[tuple[str, str]]] = {
|
|
|
2142
2147
|
# Kiro reads from `steering/` not `skills/` (per
|
|
2143
2148
|
# platforms/kiro.json#folderStructure.skillPath).
|
|
2144
2149
|
"kiro": [
|
|
2145
|
-
(".
|
|
2146
|
-
(".
|
|
2147
|
-
(".
|
|
2150
|
+
(".agent-src/rules", "rules"),
|
|
2151
|
+
(".agent-src/skills", "steering"),
|
|
2152
|
+
(".agent-src/personas", "personas"),
|
|
2148
2153
|
],
|
|
2149
2154
|
}
|
|
2150
2155
|
|
|
@@ -2883,6 +2888,7 @@ def _deploy_claude_desktop(
|
|
|
2883
2888
|
bundler = _load_claude_desktop_bundler_module()
|
|
2884
2889
|
bundles_dir = _claude_desktop_bundles_dir()
|
|
2885
2890
|
bundler.build_skill_bundles(package_root, bundles_dir, force=force)
|
|
2891
|
+
bundler.build_command_bundles(package_root, bundles_dir, force=force)
|
|
2886
2892
|
# Count total existing ZIPs (idempotent runs may not rewrite any).
|
|
2887
2893
|
bundle_count = sum(1 for _ in bundles_dir.glob("*.zip")) if bundles_dir.is_dir() else 0
|
|
2888
2894
|
_, _, marker_paths = _write_claude_desktop_marker(
|
|
@@ -2988,14 +2994,15 @@ def install_global(
|
|
|
2988
2994
|
|
|
2989
2995
|
lock_mod = _load_installed_lock_module()
|
|
2990
2996
|
installed_version = lock_mod.current_package_version()
|
|
2991
|
-
|
|
2992
|
-
|
|
2997
|
+
read_path = lock_mod.lockfile_path()
|
|
2998
|
+
write_path = lock_mod.lockfile_write_path()
|
|
2999
|
+
ok, recorded = lock_mod.check_version(installed_version, path=read_path)
|
|
2993
3000
|
|
|
2994
3001
|
if not ok and not force:
|
|
2995
3002
|
if not QUIET:
|
|
2996
3003
|
print()
|
|
2997
3004
|
warn("Refusing global install: lockfile version mismatch.")
|
|
2998
|
-
info(f" Lockfile: {
|
|
3005
|
+
info(f" Lockfile: {read_path}")
|
|
2999
3006
|
info(f" Recorded version: {recorded}")
|
|
3000
3007
|
info(f" Current package: {installed_version}")
|
|
3001
3008
|
info(" Fix: run `agent-config update`")
|
|
@@ -3013,10 +3020,10 @@ def install_global(
|
|
|
3013
3020
|
continue
|
|
3014
3021
|
print(f" {tool_id:<15} → {anchor}")
|
|
3015
3022
|
|
|
3016
|
-
existing = lock_mod.read_lockfile(path=
|
|
3023
|
+
existing = lock_mod.read_lockfile(path=read_path) or {}
|
|
3017
3024
|
existing_tools = list(existing.get("tools", []))
|
|
3018
3025
|
merged_tools = sorted(set(existing_tools) | set(tools))
|
|
3019
|
-
written = lock_mod.write_lockfile(installed_version, merged_tools, path=
|
|
3026
|
+
written = lock_mod.write_lockfile(installed_version, merged_tools, path=write_path)
|
|
3020
3027
|
|
|
3021
3028
|
if not QUIET:
|
|
3022
3029
|
print()
|