claude-code-arcane 2.9.9 → 2.11.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 +29 -0
- package/README.md +45 -43
- package/dist/cli.js +79 -33
- package/docs/SKILLS-CATALOG.md +4 -2
- package/docs/USER-GUIDE.md +24 -22
- package/docs/templates/skill-test-spec.md +0 -1
- package/package.json +1 -1
- package/profiles/ai.yaml +3 -0
- package/profiles/android-native.yaml +2 -0
- package/profiles/backend-dotnet.yaml +2 -0
- package/profiles/backend-go.yaml +2 -8
- package/profiles/backend-nestjs.yaml +2 -0
- package/profiles/backend-nextjs.yaml +2 -0
- package/profiles/backend-ts.yaml +4 -8
- package/profiles/backend-typescript.yaml +53 -0
- package/profiles/blender.yaml +1 -0
- package/profiles/flutter.yaml +2 -0
- package/profiles/frontend-react-vite.yaml +39 -0
- package/profiles/frontend.yaml +2 -0
- package/profiles/infra.yaml +2 -0
- package/profiles/integrations.yaml +6 -1
- package/profiles/ios-native.yaml +2 -0
- package/profiles/meshy.yaml +1 -0
- package/profiles/mobile-android-kotlin.yaml +36 -0
- package/profiles/mobile-flutter.yaml +37 -0
- package/profiles/mobile-ios-swift.yaml +37 -0
- package/profiles/mobile-react-native.yaml +39 -0
- package/profiles/mobile.yaml +2 -0
- package/rules/blender-files.md +72 -0
- package/rules/blender-pipeline.md +1 -63
- package/rules/gamedev/narrative.md +1 -1
- package/rules/meshy-assets.md +9 -78
- package/rules/meshy-files.md +83 -0
- package/skills/agent-protocol/SKILL.md +1 -1
- package/skills/agent-protocol/references/communication-standard.md +2 -2
- package/skills/ai-security/SKILL.md +4 -0
- package/skills/arcane-add/SKILL.md +1 -1
- package/skills/arcane-list/SKILL.md +3 -3
- package/skills/arcane-status/SKILL.md +1 -1
- package/skills/audit-dev/SKILL.md +1 -1
- package/skills/blender-animation/SKILL.md +1 -1
- package/skills/blender-export/SKILL.md +1 -1
- package/skills/blender-materials/SKILL.md +1 -1
- package/skills/blender-modeling/SKILL.md +1 -1
- package/skills/check/SKILL.md +1 -1
- package/skills/chief-of-staff/SKILL.md +1 -1
- package/skills/cloud-security/SKILL.md +4 -0
- package/skills/freelance-kickoff/SKILL.md +1 -1
- package/skills/isms-audit-expert/SKILL.md +2 -0
- package/skills/meshy-derive/SKILL.md +1 -1
- package/skills/meshy-generate/SKILL.md +1 -1
- package/skills/meshy-setup/SKILL.md +1 -1
- package/skills/owasp-top10-check/SKILL.md +1 -1
- package/skills/qms-audit-expert/SKILL.md +2 -0
- package/skills/security-pen-testing/SKILL.md +1 -1
- package/skills/seo-audit/SKILL.md +1 -1
- package/skills/skill-auditor/SKILL.md +0 -1
- package/skills/skill-improve/SKILL.md +6 -7
- package/skills/skill-test/SKILL.md +3 -3
- package/skills/skill-test/references/static-checks.md +13 -8
- package/skills/standup-report/SKILL.md +1 -1
- package/skills/vn-gdd/SKILL.md +2 -0
- package/skills/web-security/SKILL.md +4 -0
- package/skills/worktree-isolation/README.md +6 -6
- package/skills/worktree-isolation/SKILL.md +5 -2
- package/skills/worktree-isolation-setup/README.md +7 -11
- package/skills/worktree-isolation-setup/SKILL.md +15 -6
- package/skills/worktree-isolation-setup/scripts/apply.py +0 -178
- package/skills/worktree-isolation-setup/scripts/install_hook.py +0 -118
- package/skills/worktree-isolation-setup/scripts/lib.py +0 -371
|
@@ -8,22 +8,22 @@ This README explains **how worktree isolation works under the hood**. For usage,
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
+
Ships with the `infra` and backend profiles. Elsewhere:
|
|
12
|
+
|
|
11
13
|
```bash
|
|
12
|
-
npx
|
|
14
|
+
npx claude-code-arcane add worktree-isolation
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
After install, invoke with phrases like:
|
|
17
|
+
Then invoke with phrases like:
|
|
18
18
|
|
|
19
19
|
- `isolate worktree`
|
|
20
20
|
- `regenerate worktree ports`
|
|
21
21
|
- `which ports is this worktree using?`
|
|
22
22
|
|
|
23
|
-
If you also want
|
|
23
|
+
If you also want it to run automatically on every Claude Code session start, install the SessionStart hook once per machine:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
npx
|
|
26
|
+
npx claude-code-arcane global
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
---
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: worktree-isolation
|
|
3
|
-
description:
|
|
3
|
+
description: "Regenerate or show this worktree's Docker ports (.env.worktree + compose overrides) so parallel worktrees don't collide. Leaves repo files alone; to prepare a repo once, use worktree-isolation-setup."
|
|
4
|
+
argument-hint: "[apply | status]"
|
|
5
|
+
user-invocable: true
|
|
6
|
+
allowed-tools: Read, Glob, Grep, Bash
|
|
4
7
|
---
|
|
5
8
|
|
|
6
9
|
# Worktree Isolation — On-Demand
|
|
@@ -81,7 +84,7 @@ This skill does not auto-rewrite `CLAUDE.md` or scripts (zero-touch on project f
|
|
|
81
84
|
|
|
82
85
|
## Out of scope
|
|
83
86
|
|
|
84
|
-
- Does not install the SessionStart hook
|
|
87
|
+
- Does not install the SessionStart hook. `npx claude-code-arcane global` does that, once per machine.
|
|
85
88
|
- Does not modify the repo's compose files / `CLAUDE.md` / `.gitignore`. Use [`worktree-isolation-setup`](../worktree-isolation-setup/) for that.
|
|
86
89
|
- Does not clean up orphan stacks from deleted worktrees. For now, `docker compose ls` and `docker compose -p <project-name> down -v` manually.
|
|
87
90
|
- Does not handle non-docker dev servers (Next.js, uvicorn directly on host).
|
|
@@ -8,13 +8,13 @@ This README explains **what setup does and why**. For step-by-step instructions,
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
+
Ships with the `infra` and backend profiles, next to `worktree-isolation`. Elsewhere:
|
|
12
|
+
|
|
11
13
|
```bash
|
|
12
|
-
npx
|
|
14
|
+
npx claude-code-arcane add worktree-isolation-setup
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
After install, run the skill once per repo:
|
|
17
|
+
Then run the skill once per repo:
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
setup worktree isolation in this project
|
|
@@ -80,7 +80,7 @@ After setup, both work in parallel.
|
|
|
80
80
|
│ │
|
|
81
81
|
│ user → "setup worktree isolation in this project" │
|
|
82
82
|
│ → this skill runs │
|
|
83
|
-
│ ├─
|
|
83
|
+
│ ├─ arcane global (one-time per machine; idempotent) │
|
|
84
84
|
│ ├─ rewrites compose files (port + container_name params) │
|
|
85
85
|
│ ├─ updates .gitignore │
|
|
86
86
|
│ └─ amends CLAUDE.md │
|
|
@@ -127,11 +127,7 @@ For deep-dive details on how the runtime script works (port allocation, override
|
|
|
127
127
|
```
|
|
128
128
|
skills/worktree-isolation-setup/
|
|
129
129
|
├── SKILL.md # entry point — step-by-step setup procedure
|
|
130
|
-
|
|
131
|
-
└── scripts/
|
|
132
|
-
├── install_hook.py # idempotent installer for the hook + scripts
|
|
133
|
-
├── apply.py # copied into ~/.claude/scripts/ by install_hook.py
|
|
134
|
-
└── lib.py # ditto
|
|
130
|
+
└── README.md # this file
|
|
135
131
|
```
|
|
136
132
|
|
|
137
|
-
|
|
133
|
+
This skill has no scripts of its own. The runtime scripts live in [`worktree-isolation`](../worktree-isolation/scripts/), and `npx claude-code-arcane global` copies them from there into `~/.claude/scripts/`.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: worktree-isolation-setup
|
|
3
|
-
description:
|
|
3
|
+
description: "Prepare a repo once for parallel worktrees: parameterize docker-compose ports and container names, update .gitignore and CLAUDE.md, install the SessionStart hook. Edits repo files; to regenerate or show ports, use worktree-isolation."
|
|
4
|
+
argument-hint: ""
|
|
5
|
+
user-invocable: true
|
|
6
|
+
allowed-tools: Read, Glob, Grep, Bash, Edit
|
|
4
7
|
---
|
|
5
8
|
|
|
6
9
|
# Worktree Isolation — Setup (one-time)
|
|
@@ -20,17 +23,23 @@ For an in-depth explanation of how isolation works (hook lifecycle, override gen
|
|
|
20
23
|
|
|
21
24
|
## Step 1 — Ensure the global hook is installed
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
The hook is per machine, so check first:
|
|
24
27
|
|
|
25
28
|
```bash
|
|
26
|
-
|
|
29
|
+
npx claude-code-arcane global --status
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If the SessionStart hook is not active, install it. It is idempotent — safe to re-run:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx claude-code-arcane global
|
|
27
36
|
```
|
|
28
37
|
|
|
29
38
|
What it does:
|
|
30
|
-
1. Copies `
|
|
31
|
-
2. Adds
|
|
39
|
+
1. Copies `apply.py` + `lib.py` into `~/.claude/scripts/worktree-isolation/`
|
|
40
|
+
2. Adds a `SessionStart` hook in `~/.claude/settings.json` that invokes `~/.claude/scripts/worktree-isolation/apply.py --quiet`
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
It refuses to touch a malformed `settings.json`; if it stops there, tell the user and continue with Step 2 — the repo changes do not depend on the hook.
|
|
34
43
|
|
|
35
44
|
---
|
|
36
45
|
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S uv run --script --quiet
|
|
2
|
-
# /// script
|
|
3
|
-
# requires-python = ">=3.11"
|
|
4
|
-
# dependencies = ["pyyaml"]
|
|
5
|
-
# ///
|
|
6
|
-
"""Worktree isolation entry point.
|
|
7
|
-
|
|
8
|
-
Two callers:
|
|
9
|
-
1. Claude Code SessionStart hook → `apply.py --quiet` (emits hook JSON).
|
|
10
|
-
2. worktree-isolation skill → `apply.py --verbose --force` or `--status`.
|
|
11
|
-
|
|
12
|
-
Reads the project's docker-compose*.yml files, generates per-worktree override
|
|
13
|
-
files (sibling .worktree.yml) with deterministic randomized host ports + suffixed
|
|
14
|
-
container_names, writes .env.worktree, and registers the generated files in the
|
|
15
|
-
per-worktree git exclude list.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from __future__ import annotations
|
|
19
|
-
|
|
20
|
-
import argparse
|
|
21
|
-
import json
|
|
22
|
-
import sys
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
|
|
25
|
-
sys.path.insert(0, str(Path(__file__).parent))
|
|
26
|
-
|
|
27
|
-
import yaml # noqa: E402
|
|
28
|
-
|
|
29
|
-
from lib import ( # noqa: E402
|
|
30
|
-
PortMapping,
|
|
31
|
-
collect_container_names,
|
|
32
|
-
collect_port_mappings,
|
|
33
|
-
ensure_git_exclude,
|
|
34
|
-
find_compose_files,
|
|
35
|
-
overrides_up_to_date,
|
|
36
|
-
override_path_for,
|
|
37
|
-
render_env_worktree,
|
|
38
|
-
render_override_yaml,
|
|
39
|
-
worktree_id,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _build_summary(
|
|
44
|
-
project_name: str,
|
|
45
|
-
worktree: str,
|
|
46
|
-
compose_chain: list[tuple[Path, Path | None]],
|
|
47
|
-
mappings: list[PortMapping],
|
|
48
|
-
) -> str:
|
|
49
|
-
lines = [
|
|
50
|
-
f"Worktree isolation active: project='{project_name}' (id={worktree}).",
|
|
51
|
-
"",
|
|
52
|
-
"Port mappings (legacy → worktree host):",
|
|
53
|
-
]
|
|
54
|
-
for m in sorted(mappings, key=lambda x: (x.service, x.container_port)):
|
|
55
|
-
lines.append(
|
|
56
|
-
f" {m.service} container :{m.container_port} → host {m.new_host_port} "
|
|
57
|
-
f"(was {m.original_host_port})"
|
|
58
|
-
)
|
|
59
|
-
lines += [
|
|
60
|
-
"",
|
|
61
|
-
"To bring up the stack from this worktree:",
|
|
62
|
-
" set -a; source .env.worktree; set +a",
|
|
63
|
-
" docker compose up -d",
|
|
64
|
-
"(COMPOSE_FILE in .env.worktree auto-chains base + override files.)",
|
|
65
|
-
"",
|
|
66
|
-
"When running ad-hoc commands (e.g. test commands in CLAUDE.md) that hardcode",
|
|
67
|
-
"legacy host ports like " + ", ".join(sorted({str(m.original_host_port) for m in mappings}))
|
|
68
|
-
+ ", substitute the matching new host port from the table above before executing.",
|
|
69
|
-
]
|
|
70
|
-
return "\n".join(lines)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _run(args: argparse.Namespace) -> int:
|
|
74
|
-
cwd = args.cwd.resolve()
|
|
75
|
-
|
|
76
|
-
if args.status:
|
|
77
|
-
env_file = cwd / ".env.worktree"
|
|
78
|
-
if not env_file.exists():
|
|
79
|
-
print(f"No .env.worktree at {cwd}. Run with --force to generate.", file=sys.stderr)
|
|
80
|
-
return 1
|
|
81
|
-
print(env_file.read_text())
|
|
82
|
-
return 0
|
|
83
|
-
|
|
84
|
-
compose_files = find_compose_files(cwd)
|
|
85
|
-
if not compose_files:
|
|
86
|
-
if args.quiet:
|
|
87
|
-
print(json.dumps({"continue": True}))
|
|
88
|
-
elif args.verbose:
|
|
89
|
-
print(f"No docker-compose files in {cwd}. Nothing to isolate.")
|
|
90
|
-
return 0
|
|
91
|
-
|
|
92
|
-
wid = worktree_id(cwd)
|
|
93
|
-
project_name = f"{cwd.name}-{wid}"
|
|
94
|
-
env_file = cwd / ".env.worktree"
|
|
95
|
-
|
|
96
|
-
compose_chain: list[tuple[Path, Path | None]] = []
|
|
97
|
-
all_mappings: list[PortMapping] = []
|
|
98
|
-
regenerated_any = False
|
|
99
|
-
|
|
100
|
-
for base in compose_files:
|
|
101
|
-
mappings = collect_port_mappings(base, wid)
|
|
102
|
-
container_names = collect_container_names(base)
|
|
103
|
-
|
|
104
|
-
if not mappings and not container_names:
|
|
105
|
-
# Nothing to override for this file — still include in chain for completeness? No,
|
|
106
|
-
# if there's nothing to override, the base alone is fine. Skip.
|
|
107
|
-
continue
|
|
108
|
-
|
|
109
|
-
override_path = override_path_for(base)
|
|
110
|
-
|
|
111
|
-
if not args.force and overrides_up_to_date(base, override_path, env_file):
|
|
112
|
-
compose_chain.append((base, override_path))
|
|
113
|
-
all_mappings.extend(mappings)
|
|
114
|
-
continue
|
|
115
|
-
|
|
116
|
-
override_yaml_data = render_override_yaml(mappings, container_names, project_name, wid)
|
|
117
|
-
with open(override_path, "w") as f:
|
|
118
|
-
f.write("# Auto-generated by ~/.claude/scripts/worktree-isolation/apply.py\n")
|
|
119
|
-
f.write("# DO NOT COMMIT. Regenerate with the worktree-isolation skill.\n")
|
|
120
|
-
yaml.safe_dump(override_yaml_data, f, sort_keys=False, default_flow_style=False)
|
|
121
|
-
compose_chain.append((base, override_path))
|
|
122
|
-
all_mappings.extend(mappings)
|
|
123
|
-
regenerated_any = True
|
|
124
|
-
|
|
125
|
-
if not compose_chain:
|
|
126
|
-
if args.quiet:
|
|
127
|
-
print(json.dumps({"continue": True}))
|
|
128
|
-
elif args.verbose:
|
|
129
|
-
print(f"No services with ports or container_name in {cwd}. Nothing to isolate.")
|
|
130
|
-
return 0
|
|
131
|
-
|
|
132
|
-
# Always write env file when we have a chain — covers fresh dirs even if overrides exist.
|
|
133
|
-
env_content = render_env_worktree(project_name, wid, compose_chain, all_mappings)
|
|
134
|
-
if not env_file.exists() or env_file.read_text() != env_content:
|
|
135
|
-
env_file.write_text(env_content)
|
|
136
|
-
regenerated_any = True
|
|
137
|
-
|
|
138
|
-
ensure_git_exclude(cwd, [".env.worktree", "*.worktree.yml", "*.worktree.yaml"])
|
|
139
|
-
|
|
140
|
-
summary = _build_summary(project_name, wid, compose_chain, all_mappings)
|
|
141
|
-
|
|
142
|
-
if args.quiet:
|
|
143
|
-
out = {
|
|
144
|
-
"hookSpecificOutput": {
|
|
145
|
-
"hookEventName": "SessionStart",
|
|
146
|
-
"additionalContext": summary,
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
print(json.dumps(out))
|
|
150
|
-
else:
|
|
151
|
-
print(summary)
|
|
152
|
-
if not regenerated_any:
|
|
153
|
-
print("\n(no changes — overrides already up to date)")
|
|
154
|
-
return 0
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def main() -> int:
|
|
158
|
-
parser = argparse.ArgumentParser(description="Per-worktree docker-compose isolation.")
|
|
159
|
-
parser.add_argument("--cwd", type=Path, default=Path.cwd(), help="Working directory")
|
|
160
|
-
parser.add_argument("--quiet", action="store_true", help="Hook mode: emit JSON only")
|
|
161
|
-
parser.add_argument("--verbose", action="store_true", help="Skill mode: human output")
|
|
162
|
-
parser.add_argument("--force", action="store_true", help="Regenerate even if up-to-date")
|
|
163
|
-
parser.add_argument("--status", action="store_true", help="Print current .env.worktree only")
|
|
164
|
-
args = parser.parse_args()
|
|
165
|
-
|
|
166
|
-
try:
|
|
167
|
-
return _run(args)
|
|
168
|
-
except Exception as exc: # noqa: BLE001 — fail loud; hook captures stderr
|
|
169
|
-
if args.quiet:
|
|
170
|
-
# Don't break the session — emit a minimal valid hook response and log to stderr.
|
|
171
|
-
print(json.dumps({"continue": True}))
|
|
172
|
-
print(f"worktree-isolation error: {exc}", file=sys.stderr)
|
|
173
|
-
return 0
|
|
174
|
-
raise
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if __name__ == "__main__":
|
|
178
|
-
sys.exit(main())
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Install the worktree-isolation SessionStart hook globally.
|
|
3
|
-
|
|
4
|
-
Idempotent. Safe to re-run.
|
|
5
|
-
|
|
6
|
-
What it does:
|
|
7
|
-
1. Copies `apply.py` + `lib.py` from this skill's `scripts/` dir into
|
|
8
|
-
`~/.claude/scripts/worktree-isolation/` (creating the dir if needed).
|
|
9
|
-
2. Ensures `~/.claude/settings.json` has a `SessionStart` hook entry that
|
|
10
|
-
invokes the copied `apply.py --quiet`.
|
|
11
|
-
|
|
12
|
-
Re-running is a no-op if the hook + scripts are already in place.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
import os
|
|
19
|
-
import shutil
|
|
20
|
-
import stat
|
|
21
|
-
import sys
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
HOME = Path.home()
|
|
25
|
-
TARGET_DIR = HOME / ".claude" / "scripts" / "worktree-isolation"
|
|
26
|
-
SETTINGS_PATH = HOME / ".claude" / "settings.json"
|
|
27
|
-
HOOK_COMMAND = f"{TARGET_DIR}/apply.py --quiet"
|
|
28
|
-
|
|
29
|
-
SCRIPTS_TO_COPY = ("apply.py", "lib.py")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def copy_scripts(source_dir: Path) -> list[str]:
|
|
33
|
-
"""Copy hook scripts into ~/.claude/scripts/worktree-isolation/. Returns list of actions taken."""
|
|
34
|
-
TARGET_DIR.mkdir(parents=True, exist_ok=True)
|
|
35
|
-
actions: list[str] = []
|
|
36
|
-
for name in SCRIPTS_TO_COPY:
|
|
37
|
-
src = source_dir / name
|
|
38
|
-
dst = TARGET_DIR / name
|
|
39
|
-
if not src.exists():
|
|
40
|
-
raise FileNotFoundError(f"Source script missing: {src}")
|
|
41
|
-
if dst.exists() and dst.read_bytes() == src.read_bytes():
|
|
42
|
-
actions.append(f" • {name}: already up-to-date")
|
|
43
|
-
continue
|
|
44
|
-
shutil.copy2(src, dst)
|
|
45
|
-
# Ensure apply.py is executable.
|
|
46
|
-
if name == "apply.py":
|
|
47
|
-
mode = dst.stat().st_mode
|
|
48
|
-
dst.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
49
|
-
actions.append(f" • {name}: installed → {dst}")
|
|
50
|
-
return actions
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def hook_already_installed(settings: dict) -> bool:
|
|
54
|
-
for entry in settings.get("hooks", {}).get("SessionStart", []):
|
|
55
|
-
for hook in entry.get("hooks", []):
|
|
56
|
-
cmd = hook.get("command", "")
|
|
57
|
-
if "worktree-isolation/apply.py" in cmd and "--quiet" in cmd:
|
|
58
|
-
return True
|
|
59
|
-
return False
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def install_hook() -> str:
|
|
63
|
-
"""Add SessionStart hook to ~/.claude/settings.json. Returns a status string."""
|
|
64
|
-
if SETTINGS_PATH.exists():
|
|
65
|
-
try:
|
|
66
|
-
settings = json.loads(SETTINGS_PATH.read_text())
|
|
67
|
-
except json.JSONDecodeError as exc:
|
|
68
|
-
raise SystemExit(
|
|
69
|
-
f"Refusing to modify malformed {SETTINGS_PATH}: {exc}. Fix manually and re-run."
|
|
70
|
-
)
|
|
71
|
-
else:
|
|
72
|
-
settings = {}
|
|
73
|
-
|
|
74
|
-
if hook_already_installed(settings):
|
|
75
|
-
return " • SessionStart hook: already present (no change)"
|
|
76
|
-
|
|
77
|
-
hooks = settings.setdefault("hooks", {})
|
|
78
|
-
session_start = hooks.setdefault("SessionStart", [])
|
|
79
|
-
|
|
80
|
-
new_entry = {
|
|
81
|
-
"matcher": "",
|
|
82
|
-
"hooks": [{"type": "command", "command": HOOK_COMMAND}],
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
# Prefer to attach to an existing matcher="" entry rather than appending a new one.
|
|
86
|
-
for entry in session_start:
|
|
87
|
-
if entry.get("matcher", "") == "":
|
|
88
|
-
entry.setdefault("hooks", []).append(new_entry["hooks"][0])
|
|
89
|
-
break
|
|
90
|
-
else:
|
|
91
|
-
session_start.append(new_entry)
|
|
92
|
-
|
|
93
|
-
SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
-
SETTINGS_PATH.write_text(json.dumps(settings, indent=2) + "\n")
|
|
95
|
-
return f" • SessionStart hook: added to {SETTINGS_PATH}"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def main() -> int:
|
|
99
|
-
here = Path(__file__).resolve().parent
|
|
100
|
-
print(f"Installing worktree-isolation hook from {here}")
|
|
101
|
-
print()
|
|
102
|
-
|
|
103
|
-
print("1) Copying hook scripts:")
|
|
104
|
-
for line in copy_scripts(here):
|
|
105
|
-
print(line)
|
|
106
|
-
|
|
107
|
-
print()
|
|
108
|
-
print("2) Wiring SessionStart hook:")
|
|
109
|
-
print(install_hook())
|
|
110
|
-
|
|
111
|
-
print()
|
|
112
|
-
print("Done. Open a new Claude Code session inside any git worktree to verify.")
|
|
113
|
-
print(f" .env.worktree should appear in the worktree root automatically.")
|
|
114
|
-
return 0
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if __name__ == "__main__":
|
|
118
|
-
sys.exit(main())
|