@softspark/ai-toolkit 4.32.0 → 4.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +19 -24
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/skills/analyze/scripts/complexity.py +3 -0
- package/app/skills/deploy/scripts/pre_deploy_check.py +13 -6
- package/app/skills/docs/scripts/doc-inventory.py +3 -0
- package/app/skills/explain/scripts/dependency-graph.py +3 -0
- package/app/skills/migrate/scripts/migration-status.py +3 -0
- package/app/skills/refactor/scripts/refactor-scan.py +3 -0
- package/kb/reference/plugin-pack-conventions.md +3 -3
- package/llms-full.txt +3 -3
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/plugin.py +128 -16
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,38 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.32.1 - Plugin removal leaves nothing behind (2026-09-04)
|
|
11
|
+
|
|
12
|
+
Both fixes come from running the post-release SOPs on v4.32.0 in an isolated
|
|
13
|
+
sandbox; both defects predate that release (identical on v4.31.0).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **`plugin remove` deletes the hooks and scripts a pack installed.** The
|
|
18
|
+
Claude and Codex install paths copied `hooks/plugin-<pack>-*` and
|
|
19
|
+
`plugin-scripts/<pack>/*` into `~/.softspark/ai-toolkit` but never recorded
|
|
20
|
+
them in `shared_asset_ownership`, so removal found no owner and preserved
|
|
21
|
+
every file as "untracked": each pack left 4-5 files behind. Install now
|
|
22
|
+
records sha256/mode/inode for what it copied; removal deletes only entries
|
|
23
|
+
this editor consumes that no other editor still uses and that are unchanged
|
|
24
|
+
on disk, preserves an edited file with a `WARN preserved changed plugin
|
|
25
|
+
asset` line, and drops the empty `plugin-scripts/<pack>/` directory.
|
|
26
|
+
`tests/test_plugin_asset_removal.bats` covers install, zero-residue removal,
|
|
27
|
+
user-edited preservation, two-editor sharing, and reinstall.
|
|
28
|
+
- **Five skill scripts answer `--help`.** `doc-inventory.py`,
|
|
29
|
+
`dependency-graph.py`, `migration-status.py`, `refactor-scan.py`, and
|
|
30
|
+
`complexity.py` treated `--help` as a path and returned a JSON error.
|
|
31
|
+
`tests/test_skill_scripts_help.bats` now probes every documented
|
|
32
|
+
`${CLAUDE_SKILL_DIR}` invocation with `--help` (exit 0, no traceback), the
|
|
33
|
+
same check as post-release SOP Phase 4b. The probe uses `perl alarm` as its
|
|
34
|
+
hang guard, not GNU `timeout`, which the macOS CI runner does not have.
|
|
35
|
+
- **`pre_deploy_check.py` no longer crashes on Linux.** It probed for docker
|
|
36
|
+
by running the shell builtin `command -v` through `subprocess`, which only
|
|
37
|
+
works on macOS (a `/usr/bin/command` binary exists there); on Linux it raised
|
|
38
|
+
`FileNotFoundError` before any check ran. Now `shutil.which`. Caught by the
|
|
39
|
+
new `--help` probe on the Ubuntu CI runner, which is the point of that test.
|
|
40
|
+
- **Test count.** Bats increased from 1966 to 1972.
|
|
41
|
+
|
|
10
42
|
## v4.32.0 - Context budget: scoped rules, scoped language skills, doctor budget checks (2026-09-04)
|
|
11
43
|
|
|
12
44
|
### Added
|
package/README.md
CHANGED
|
@@ -6,30 +6,25 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
|
+
|
|
11
|
+
## What's New in v4.32.1
|
|
12
|
+
|
|
13
|
+
**v4.32.1** fixes two defects the post-release SOPs found on v4.32.0 (both
|
|
14
|
+
older than that release):
|
|
15
|
+
|
|
16
|
+
- `ai-toolkit plugin remove` now deletes the hooks and scripts a pack
|
|
17
|
+
installed under `~/.softspark/ai-toolkit`. Install records what it copied;
|
|
18
|
+
removal deletes only unchanged, unshared files and names anything it
|
|
19
|
+
preserves. Every pack used to leave 4-5 files behind.
|
|
20
|
+
- Five skill scripts (`doc-inventory`, `dependency-graph`, `migration-status`,
|
|
21
|
+
`refactor-scan`, `complexity`) answer `--help` instead of treating it as a
|
|
22
|
+
path; a new test probes every documented skill script with `--help`.
|
|
23
|
+
- Still in this train from v4.32.0: path-scoped common rules, language skills
|
|
24
|
+
scoped to your registered projects (`--language-skills`), `doctor` Context
|
|
25
|
+
Budget and Permission Rules checks, one strict frontmatter parser, and the
|
|
26
|
+
`git-team` rule for `--profile strict`. Test count: 1966 -> 1972 bats +
|
|
27
|
+
348 pytest.
|
|
33
28
|
|
|
34
29
|
See [CHANGELOG.md](CHANGELOG.md) for full history.
|
|
35
30
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "ai-toolkit",
|
|
4
4
|
"displayName": "AI Toolkit",
|
|
5
5
|
"description": "Professional-grade engineering skills, agents, rules, and lifecycle guardrails for Claude Code, Claude Chat, and Cowork.",
|
|
6
|
-
"version": "4.32.
|
|
6
|
+
"version": "4.32.1",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SoftSpark",
|
|
9
9
|
"url": "https://github.com/softspark"
|
|
@@ -125,6 +125,9 @@ def _summary(root: Path, all_files: list[Path]) -> tuple[int, int]:
|
|
|
125
125
|
|
|
126
126
|
def main() -> None:
|
|
127
127
|
"""Entry point: analyse directory and print text report to stdout."""
|
|
128
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
129
|
+
print("Usage: complexity.py [directory]\n\nReport per-file code complexity for directory (default: cwd).")
|
|
130
|
+
return
|
|
128
131
|
root = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd().resolve()
|
|
129
132
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
130
133
|
|
|
@@ -16,6 +16,7 @@ Usage::
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import json
|
|
19
|
+
import shutil
|
|
19
20
|
import subprocess
|
|
20
21
|
import sys
|
|
21
22
|
from datetime import datetime, timezone
|
|
@@ -50,12 +51,15 @@ def _git_branch() -> str:
|
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
def _docker_available() -> bool:
|
|
53
|
-
"""Return True if the ``docker`` command is on PATH.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
"""Return True if the ``docker`` command is on PATH.
|
|
55
|
+
|
|
56
|
+
``command -v`` is a shell builtin; running it through ``subprocess`` only
|
|
57
|
+
worked on macOS, which ships a ``/usr/bin/command`` binary. On Linux it
|
|
58
|
+
raised ``FileNotFoundError`` before any check ran (v4.32.1).
|
|
59
|
+
"""
|
|
60
|
+
if shutil.which("docker") is None:
|
|
61
|
+
return False
|
|
62
|
+
return _run(["docker", "--version"]).returncode == 0
|
|
59
63
|
|
|
60
64
|
|
|
61
65
|
def _docker_services_running() -> bool:
|
|
@@ -155,6 +159,9 @@ def _run_checks(env: str) -> _CheckCollector:
|
|
|
155
159
|
|
|
156
160
|
def main() -> None:
|
|
157
161
|
"""Entry point: run checks and print JSON result to stdout."""
|
|
162
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
163
|
+
print("Usage: pre_deploy_check.py [environment]\n\nRun pre-deployment checks for environment (default: staging) and print JSON.")
|
|
164
|
+
return
|
|
158
165
|
env = sys.argv[1] if len(sys.argv) > 1 else "staging"
|
|
159
166
|
branch = _git_branch()
|
|
160
167
|
collector = _run_checks(env)
|
|
@@ -130,6 +130,9 @@ def get_mtime(filepath: str) -> float:
|
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
def main():
|
|
133
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
134
|
+
print("Usage: doc-inventory.py [directory]\n\nInventory documentation files under directory (default: cwd) as JSON.")
|
|
135
|
+
return
|
|
133
136
|
root = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.getcwd()
|
|
134
137
|
if not os.path.isdir(root):
|
|
135
138
|
print(json.dumps({"error": f"Not a directory: {root}"}))
|
|
@@ -159,6 +159,9 @@ def generate_mermaid(target_name: str, imports: list, imported_by: list) -> str:
|
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
def main():
|
|
162
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
163
|
+
print("Usage: dependency-graph.py <file_or_directory>\n\nPrint the import/dependency graph of a file or directory as JSON.")
|
|
164
|
+
return
|
|
162
165
|
if len(sys.argv) < 2:
|
|
163
166
|
print(json.dumps({"error": "Usage: dependency-graph.py <file_or_directory>"}))
|
|
164
167
|
sys.exit(1)
|
|
@@ -182,6 +182,9 @@ def detect(project_dir: Path) -> dict[str, object]:
|
|
|
182
182
|
|
|
183
183
|
def main() -> None:
|
|
184
184
|
"""Entry point: detect migration tool and print JSON result to stdout."""
|
|
185
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
186
|
+
print("Usage: migration-status.py [project_dir]\n\nDetect the migration tool in project_dir (default: cwd) and report its status as JSON.")
|
|
187
|
+
return
|
|
185
188
|
project_dir = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd()
|
|
186
189
|
if not project_dir.is_dir():
|
|
187
190
|
print(json.dumps({"error": f"Not a directory: {project_dir}"}))
|
|
@@ -154,6 +154,9 @@ def analyze_file(filepath: str, project_root: str) -> dict:
|
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
def main():
|
|
157
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
158
|
+
print("Usage: refactor-scan.py <file_or_directory>\n\nScan a file or directory for refactoring candidates and print JSON.")
|
|
159
|
+
return
|
|
157
160
|
if len(sys.argv) < 2:
|
|
158
161
|
print(json.dumps({"error": "Usage: refactor-scan.py <file_or_directory>"}))
|
|
159
162
|
sys.exit(1)
|
|
@@ -3,9 +3,9 @@ title: "Plugin Pack Conventions"
|
|
|
3
3
|
category: reference
|
|
4
4
|
service: ai-toolkit
|
|
5
5
|
tags: [plugins, plugin-packs, conventions, manifests, hooks, policy-packs]
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.4.0"
|
|
7
7
|
created: "2026-03-28"
|
|
8
|
-
last_updated: "2026-
|
|
8
|
+
last_updated: "2026-09-04"
|
|
9
9
|
description: "Conventions for experimental ai-toolkit plugin packs, policy packs, hook packs, and plugin-creator scaffolding across supported editors."
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -167,7 +167,7 @@ ai-toolkit plugin status --editor all # show installed packs with r
|
|
|
167
167
|
1. **Claude Code target**: strips plugin hook entries from `~/.claude/settings.json` and removes plugin-local rule sections from `~/.claude/CLAUDE.md`
|
|
168
168
|
2. **Codex target**: strips only command handlers carrying the exact `AI_TOOLKIT_HOOK_OWNER=ai-toolkit-plugin-<pack>` marker from `$CODEX_HOME/hooks.json`, removes owned `$CODEX_HOME/ai-toolkit-hooks/plugin-<pack>-*` assets, and removes the pack's marker-bounded sections from `$CODEX_HOME/AGENTS.md`
|
|
169
169
|
3. **Cursor/Gemini rules**: removes only exact content recorded in `rule_ownership`; foreign or user-modified files/sections are preserved with a warning
|
|
170
|
-
4. **Claude/shared assets** (`~/.softspark/ai-toolkit/hooks/plugin-*`, `plugin-scripts/<pack>/`) are removed only when no remaining runtime still uses that pack
|
|
170
|
+
4. **Claude/shared assets** (`~/.softspark/ai-toolkit/hooks/plugin-*`, `plugin-scripts/<pack>/`) are removed only when no remaining runtime still uses that pack, and only when the file on disk is byte-, mode- and inode-identical to what install recorded in `shared_asset_ownership`; a file the user edited is preserved and named in a `WARN preserved changed plugin asset` line. Before v4.32.1 the Claude and Codex install paths never recorded that ownership, so removal preserved every hook and script as "untracked" and each pack left 4-5 files behind (found by the post-release SOP on v4.32.0)
|
|
171
171
|
5. **Updates** `plugins.json` state per runtime
|
|
172
172
|
6. **Leaves** core agents/skills untouched (they belong to the base install)
|
|
173
173
|
7. **Leaves** plugin data intact (e.g. `memory.db` — use `clean` to prune)
|
package/llms-full.txt
CHANGED
|
@@ -15855,9 +15855,9 @@ title: "Plugin Pack Conventions"
|
|
|
15855
15855
|
category: reference
|
|
15856
15856
|
service: ai-toolkit
|
|
15857
15857
|
tags: [plugins, plugin-packs, conventions, manifests, hooks, policy-packs]
|
|
15858
|
-
version: "1.
|
|
15858
|
+
version: "1.4.0"
|
|
15859
15859
|
created: "2026-03-28"
|
|
15860
|
-
last_updated: "2026-
|
|
15860
|
+
last_updated: "2026-09-04"
|
|
15861
15861
|
description: "Conventions for experimental ai-toolkit plugin packs, policy packs, hook packs, and plugin-creator scaffolding across supported editors."
|
|
15862
15862
|
---
|
|
15863
15863
|
|
|
@@ -16019,7 +16019,7 @@ ai-toolkit plugin status --editor all # show installed packs with r
|
|
|
16019
16019
|
1. **Claude Code target**: strips plugin hook entries from `~/.claude/settings.json` and removes plugin-local rule sections from `~/.claude/CLAUDE.md`
|
|
16020
16020
|
2. **Codex target**: strips only command handlers carrying the exact `AI_TOOLKIT_HOOK_OWNER=ai-toolkit-plugin-<pack>` marker from `$CODEX_HOME/hooks.json`, removes owned `$CODEX_HOME/ai-toolkit-hooks/plugin-<pack>-*` assets, and removes the pack's marker-bounded sections from `$CODEX_HOME/AGENTS.md`
|
|
16021
16021
|
3. **Cursor/Gemini rules**: removes only exact content recorded in `rule_ownership`; foreign or user-modified files/sections are preserved with a warning
|
|
16022
|
-
4. **Claude/shared assets** (`~/.softspark/ai-toolkit/hooks/plugin-*`, `plugin-scripts/<pack>/`) are removed only when no remaining runtime still uses that pack
|
|
16022
|
+
4. **Claude/shared assets** (`~/.softspark/ai-toolkit/hooks/plugin-*`, `plugin-scripts/<pack>/`) are removed only when no remaining runtime still uses that pack, and only when the file on disk is byte-, mode- and inode-identical to what install recorded in `shared_asset_ownership`; a file the user edited is preserved and named in a `WARN preserved changed plugin asset` line. Before v4.32.1 the Claude and Codex install paths never recorded that ownership, so removal preserved every hook and script as "untracked" and each pack left 4-5 files behind (found by the post-release SOP on v4.32.0)
|
|
16023
16023
|
5. **Updates** `plugins.json` state per runtime
|
|
16024
16024
|
6. **Leaves** core agents/skills untouched (they belong to the base install)
|
|
16025
16025
|
7. **Leaves** plugin data intact (e.g. `memory.db` — use `clean` to prune)
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softspark/ai-toolkit",
|
|
3
|
-
"version": "4.32.
|
|
3
|
+
"version": "4.32.1",
|
|
4
4
|
"description": "AI coding toolkit: 109 skills, 44 agents, 14 developer-tool integrations, recoverable native tool-output filtering, Claude Chat/Cowork export, safety constitution, SARIF audit, and signed npm provenance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/scripts/plugin.py
CHANGED
|
@@ -2006,6 +2006,16 @@ def _apply_asset_install(
|
|
|
2006
2006
|
"inode": version.inode,
|
|
2007
2007
|
}
|
|
2008
2008
|
print(f" Installed plugin {spec.kind}: {spec.path.name}")
|
|
2009
|
+
return _merge_asset_ownership(previous_ownership, name, editor, produced_entries)
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
def _merge_asset_ownership(
|
|
2013
|
+
previous_ownership: dict | None,
|
|
2014
|
+
name: str,
|
|
2015
|
+
editor: str,
|
|
2016
|
+
produced_entries: dict[str, dict],
|
|
2017
|
+
) -> dict:
|
|
2018
|
+
"""Fold one editor's freshly written assets into the pack's ownership record."""
|
|
2009
2019
|
entries = _asset_entries(previous_ownership, name)
|
|
2010
2020
|
entries.update(produced_entries)
|
|
2011
2021
|
consumers = _shared_asset_consumers(previous_ownership, name)
|
|
@@ -2022,6 +2032,37 @@ def _apply_asset_install(
|
|
|
2022
2032
|
}
|
|
2023
2033
|
|
|
2024
2034
|
|
|
2035
|
+
def _ownership_from_copied_assets(
|
|
2036
|
+
name: str,
|
|
2037
|
+
editor: str,
|
|
2038
|
+
pack_dir: Path,
|
|
2039
|
+
hook_specs: list[dict],
|
|
2040
|
+
previous_ownership: dict | None,
|
|
2041
|
+
) -> dict:
|
|
2042
|
+
"""Record what the non-transactional Claude/Codex install just copied.
|
|
2043
|
+
|
|
2044
|
+
``_copy_plugin_hook_scripts`` and ``_copy_plugin_scripts`` write the same
|
|
2045
|
+
paths ``_prepare_asset_specs`` describes but never recorded them, so
|
|
2046
|
+
removal had nothing to verify against and preserved every file as
|
|
2047
|
+
"untracked" (v4.32.1). Reading the versions back from disk after the copy
|
|
2048
|
+
gives removal the same sha256/mode/inode contract the JSON runtimes get.
|
|
2049
|
+
"""
|
|
2050
|
+
produced: dict[str, dict] = {}
|
|
2051
|
+
for spec in _prepare_asset_specs(name, pack_dir, hook_specs):
|
|
2052
|
+
version = _read_file_version(spec.path)
|
|
2053
|
+
if version is None:
|
|
2054
|
+
continue
|
|
2055
|
+
produced[spec.key] = {
|
|
2056
|
+
"path": str(spec.path),
|
|
2057
|
+
"kind": spec.kind,
|
|
2058
|
+
"sha256": hashlib.sha256(version.content).hexdigest(),
|
|
2059
|
+
"mode": version.mode,
|
|
2060
|
+
"device": version.device,
|
|
2061
|
+
"inode": version.inode,
|
|
2062
|
+
}
|
|
2063
|
+
return _merge_asset_ownership(previous_ownership, name, editor, produced)
|
|
2064
|
+
|
|
2065
|
+
|
|
2025
2066
|
def _preflight_asset_removal(
|
|
2026
2067
|
transaction: PluginFileTransaction,
|
|
2027
2068
|
ownership: dict | None,
|
|
@@ -2075,6 +2116,74 @@ def _apply_asset_removal(
|
|
|
2075
2116
|
print(f" WARN preserved changed or user-owned plugin asset: {path}")
|
|
2076
2117
|
|
|
2077
2118
|
|
|
2119
|
+
def _remove_owned_plugin_assets(state: dict | None, name: str, editor: str) -> None:
|
|
2120
|
+
"""Delete the hook and script files this pack installed for ``editor``.
|
|
2121
|
+
|
|
2122
|
+
Claude and Codex removal never consulted ``shared_asset_ownership``: the
|
|
2123
|
+
files were written on install, recorded, and then reported as "untracked"
|
|
2124
|
+
on remove because nothing looked the record up. Every pack left its hooks
|
|
2125
|
+
and ``plugin-scripts/<name>/`` behind (v4.32.1).
|
|
2126
|
+
|
|
2127
|
+
Same rules as the transactional runtimes: an entry is deleted only when
|
|
2128
|
+
this editor consumes it, no other editor still does, and the file on disk
|
|
2129
|
+
is byte- and inode-identical to what install recorded. Anything else
|
|
2130
|
+
(user edits, a file install never wrote, a symlink) is preserved and named.
|
|
2131
|
+
"""
|
|
2132
|
+
ownership = _shared_asset_ownership_for(state or {}, name)
|
|
2133
|
+
entries = _asset_entries(ownership, name)
|
|
2134
|
+
consumers = _shared_asset_consumers(ownership, name)
|
|
2135
|
+
editor_keys = set(consumers.get(editor, ()))
|
|
2136
|
+
other_keys = {
|
|
2137
|
+
key
|
|
2138
|
+
for consumer, keys in consumers.items()
|
|
2139
|
+
if consumer != editor
|
|
2140
|
+
for key in keys
|
|
2141
|
+
}
|
|
2142
|
+
scripts_root = TOOLKIT_DATA_DIR / "plugin-scripts" / name
|
|
2143
|
+
hook_prefix = f"plugin-{name}-"
|
|
2144
|
+
handled: set[Path] = set()
|
|
2145
|
+
|
|
2146
|
+
for key, entry in entries.items():
|
|
2147
|
+
raw_path = entry.get("path")
|
|
2148
|
+
if not isinstance(raw_path, str):
|
|
2149
|
+
continue
|
|
2150
|
+
path = Path(raw_path)
|
|
2151
|
+
handled.add(path.absolute())
|
|
2152
|
+
if key not in editor_keys or key in other_keys:
|
|
2153
|
+
continue
|
|
2154
|
+
kind = entry.get("kind")
|
|
2155
|
+
if kind == "hook":
|
|
2156
|
+
safe = path.parent.absolute() == HOOKS_DIR.absolute() and path.name.startswith(hook_prefix)
|
|
2157
|
+
elif kind == "script":
|
|
2158
|
+
safe = path.parent.absolute() == scripts_root.absolute()
|
|
2159
|
+
else:
|
|
2160
|
+
safe = False
|
|
2161
|
+
if not safe:
|
|
2162
|
+
print(f" WARN refused to remove plugin asset outside its owned root: {path}")
|
|
2163
|
+
continue
|
|
2164
|
+
if path.is_symlink():
|
|
2165
|
+
print(f" WARN preserved symlinked plugin asset: {path}")
|
|
2166
|
+
continue
|
|
2167
|
+
version = _read_file_version(path)
|
|
2168
|
+
if version is None:
|
|
2169
|
+
continue
|
|
2170
|
+
if not _asset_entry_matches(path, version, entry):
|
|
2171
|
+
print(f" WARN preserved changed plugin asset: {path}")
|
|
2172
|
+
continue
|
|
2173
|
+
path.unlink()
|
|
2174
|
+
print(f" Removed plugin asset: {path.name}")
|
|
2175
|
+
|
|
2176
|
+
for hook in sorted(HOOKS_DIR.glob(f"{hook_prefix}*")):
|
|
2177
|
+
if hook.absolute() not in handled:
|
|
2178
|
+
print(f" WARN preserved untracked plugin hook: {hook}")
|
|
2179
|
+
if scripts_root.is_dir():
|
|
2180
|
+
for leftover in sorted(scripts_root.iterdir()):
|
|
2181
|
+
if leftover.absolute() not in handled:
|
|
2182
|
+
print(f" WARN preserved untracked plugin script: {leftover}")
|
|
2183
|
+
if not any(scripts_root.iterdir()):
|
|
2184
|
+
scripts_root.rmdir()
|
|
2185
|
+
|
|
2186
|
+
|
|
2078
2187
|
def _state_after_removal(
|
|
2079
2188
|
state: dict,
|
|
2080
2189
|
editor: str,
|
|
@@ -2616,19 +2725,15 @@ def _remove_claude_pack_links(pack: dict, pack_dir: Path) -> None:
|
|
|
2616
2725
|
|
|
2617
2726
|
|
|
2618
2727
|
def remove_pack_claude(
|
|
2619
|
-
name: str, pack: dict, pack_dir: Path, *, keep_shared_assets: bool
|
|
2728
|
+
name: str, pack: dict, pack_dir: Path, *, keep_shared_assets: bool,
|
|
2729
|
+
state: dict | None = None,
|
|
2620
2730
|
) -> bool:
|
|
2621
2731
|
hook_specs = _resolve_pack_hooks(pack, pack_dir)
|
|
2622
2732
|
rule_specs = _resolve_pack_rules(pack, pack_dir)
|
|
2623
2733
|
_remove_claude_pack_links(pack, pack_dir)
|
|
2624
2734
|
|
|
2625
2735
|
if not keep_shared_assets:
|
|
2626
|
-
|
|
2627
|
-
print(f" WARN preserved untracked plugin hook: {hook}")
|
|
2628
|
-
|
|
2629
|
-
scripts_dir = TOOLKIT_DATA_DIR / "plugin-scripts" / name
|
|
2630
|
-
if scripts_dir.is_dir():
|
|
2631
|
-
print(f" WARN preserved untracked plugin scripts: {scripts_dir}")
|
|
2736
|
+
_remove_owned_plugin_assets(state, name, "claude")
|
|
2632
2737
|
|
|
2633
2738
|
if any(not spec["is_core"] for spec in hook_specs):
|
|
2634
2739
|
_strip_claude_hooks(name)
|
|
@@ -3000,7 +3105,8 @@ def install_pack_codex(name: str, pack: dict, pack_dir: Path) -> bool:
|
|
|
3000
3105
|
|
|
3001
3106
|
|
|
3002
3107
|
def remove_pack_codex(
|
|
3003
|
-
name: str, pack: dict, pack_dir: Path, *, keep_shared_assets: bool
|
|
3108
|
+
name: str, pack: dict, pack_dir: Path, *, keep_shared_assets: bool,
|
|
3109
|
+
state: dict | None = None,
|
|
3004
3110
|
) -> bool:
|
|
3005
3111
|
_assert_safe_codex_surface()
|
|
3006
3112
|
_strip_codex_hooks(name)
|
|
@@ -3016,14 +3122,10 @@ def remove_pack_codex(
|
|
|
3016
3122
|
print(f" WARN preserved user-owned Codex hook: {hook.name}")
|
|
3017
3123
|
|
|
3018
3124
|
if not keep_shared_assets:
|
|
3019
|
-
#
|
|
3020
|
-
#
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
scripts_dir = TOOLKIT_DATA_DIR / "plugin-scripts" / name
|
|
3025
|
-
if scripts_dir.is_dir():
|
|
3026
|
-
print(f" WARN preserved untracked plugin scripts: {scripts_dir}")
|
|
3125
|
+
# Paths used by releases before native Codex plugin assets moved under
|
|
3126
|
+
# $CODEX_HOME. Claude still owns these when installed for both, which
|
|
3127
|
+
# the ownership consumers encode; only Codex-consumed entries go.
|
|
3128
|
+
_remove_owned_plugin_assets(state, name, "codex")
|
|
3027
3129
|
|
|
3028
3130
|
_remove_codex_rules(name)
|
|
3029
3131
|
print(f" Done: removed {name} from codex")
|
|
@@ -3202,6 +3304,14 @@ def _install_pack_locked(
|
|
|
3202
3304
|
if transaction_snapshots is not None:
|
|
3203
3305
|
_restore_file_snapshots(transaction_snapshots)
|
|
3204
3306
|
return False
|
|
3307
|
+
if editor in ("claude", "codex"):
|
|
3308
|
+
asset_ownership = _ownership_from_copied_assets(
|
|
3309
|
+
name,
|
|
3310
|
+
editor,
|
|
3311
|
+
pack_dir,
|
|
3312
|
+
_resolve_pack_hooks(pack, pack_dir),
|
|
3313
|
+
_shared_asset_ownership_for(state, name),
|
|
3314
|
+
)
|
|
3205
3315
|
|
|
3206
3316
|
if hook_config_update is not None:
|
|
3207
3317
|
if transaction_snapshots is not None:
|
|
@@ -3362,6 +3472,7 @@ def _remove_pack_locked(
|
|
|
3362
3472
|
pack,
|
|
3363
3473
|
pack_dir,
|
|
3364
3474
|
keep_shared_assets=keep_shared_assets,
|
|
3475
|
+
state=state,
|
|
3365
3476
|
)
|
|
3366
3477
|
elif editor == "codex":
|
|
3367
3478
|
ok = remove_pack_codex(
|
|
@@ -3369,6 +3480,7 @@ def _remove_pack_locked(
|
|
|
3369
3480
|
pack,
|
|
3370
3481
|
pack_dir,
|
|
3371
3482
|
keep_shared_assets=keep_shared_assets,
|
|
3483
|
+
state=state,
|
|
3372
3484
|
)
|
|
3373
3485
|
elif editor in JSON_HOOK_RUNTIMES:
|
|
3374
3486
|
ok = remove_pack_json_runtime(
|