claudekit-codex-sync 0.1.0 → 0.2.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/AGENTS.md +5 -8
- package/README.md +76 -59
- package/docs/codebase-summary.md +40 -43
- package/docs/codex-vs-claude-agents.md +53 -42
- package/docs/installation-guide.md +67 -23
- package/docs/project-overview-pdr.md +20 -26
- package/docs/project-roadmap.md +26 -32
- package/docs/system-architecture.md +45 -82
- package/package.json +10 -4
- package/plans/260223-1310-v02-cli-redesign-cleanup/phase-01-delete-dead-code.md +88 -0
- package/plans/260223-1310-v02-cli-redesign-cleanup/phase-02-cli-redesign.md +316 -0
- package/plans/260223-1310-v02-cli-redesign-cleanup/phase-03-symlink-venv.md +148 -0
- package/plans/260223-1310-v02-cli-redesign-cleanup/phase-04-wire-unused-functions.md +151 -0
- package/plans/260223-1310-v02-cli-redesign-cleanup/phase-05-safety-tests-docs.md +206 -0
- package/plans/260223-1310-v02-cli-redesign-cleanup/plan.md +18 -0
- package/plans/reports/planner-260223-v02-cli-redesign-cleanup-validation.md +95 -0
- package/plans/reports/project-manager-260223-v02-cli-redesign-cleanup-finalization.md +28 -0
- package/src/claudekit_codex_sync/asset_sync_dir.py +60 -9
- package/src/claudekit_codex_sync/asset_sync_zip.py +16 -5
- package/src/claudekit_codex_sync/clean_target.py +59 -0
- package/src/claudekit_codex_sync/cli.py +113 -81
- package/src/claudekit_codex_sync/constants.py +2 -13
- package/src/claudekit_codex_sync/dep_bootstrapper.py +82 -36
- package/src/claudekit_codex_sync/path_normalizer.py +2 -0
- package/src/claudekit_codex_sync/runtime_verifier.py +17 -7
- package/src/claudekit_codex_sync/source_resolver.py +10 -2
- package/src/claudekit_codex_sync/sync_registry.py +1 -0
- package/templates/agents-md.md +5 -8
- package/tests/test_clean_target.py +75 -0
- package/tests/test_cli_args.py +57 -0
- package/reports/brainstorm-260222-2051-claudekit-codex-community-sync.md +0 -113
- package/scripts/bootstrap-claudekit-skill-scripts.sh +0 -150
- package/scripts/claudekit-sync-all.py +0 -1150
- package/scripts/export-claudekit-prompts.sh +0 -221
- package/scripts/normalize-claudekit-for-codex.sh +0 -261
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Phase 5 — Safety Fixes + Tests + Docs
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
- Priority: P1
|
|
5
|
+
- Status: [x] Implemented (release ops pending)
|
|
6
|
+
- Estimated: 30 min
|
|
7
|
+
|
|
8
|
+
## Code Changes
|
|
9
|
+
|
|
10
|
+
### [MODIFY] `src/claudekit_codex_sync/path_normalizer.py`
|
|
11
|
+
|
|
12
|
+
**Add defensive mkdir in `convert_agents_md_to_toml()` (after line ~113):**
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
def convert_agents_md_to_toml(*, codex_home: Path, dry_run: bool) -> int:
|
|
16
|
+
"""Convert ClaudeKit agent .md files to Codex .toml format."""
|
|
17
|
+
from .constants import (
|
|
18
|
+
CLAUDE_MODEL_REASONING_EFFORT,
|
|
19
|
+
CLAUDE_TO_CODEX_MODELS,
|
|
20
|
+
READ_ONLY_AGENT_ROLES,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
agents_dir = codex_home / "agents"
|
|
24
|
+
if not agents_dir.exists():
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
# Defensive: ensure target dir exists for writing .toml
|
|
28
|
+
if not dry_run:
|
|
29
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
# ... rest unchanged
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### [NEW] `tests/test_clean_target.py`
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
"""Tests for clean_target module."""
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
from claudekit_codex_sync.clean_target import clean_target
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_clean_removes_agents(tmp_path: Path):
|
|
46
|
+
"""Clean removes agents dir."""
|
|
47
|
+
agents = tmp_path / "agents"
|
|
48
|
+
agents.mkdir()
|
|
49
|
+
(agents / "planner.toml").write_text("model = 'test'")
|
|
50
|
+
(agents / "researcher.toml").write_text("model = 'test'")
|
|
51
|
+
|
|
52
|
+
removed = clean_target(tmp_path, dry_run=False)
|
|
53
|
+
assert not agents.exists()
|
|
54
|
+
assert removed >= 2
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_clean_keeps_venv(tmp_path: Path):
|
|
58
|
+
"""Clean keeps skills/.venv intact."""
|
|
59
|
+
skills = tmp_path / "skills"
|
|
60
|
+
skills.mkdir()
|
|
61
|
+
venv = skills / ".venv"
|
|
62
|
+
venv.mkdir()
|
|
63
|
+
(venv / "bin").mkdir()
|
|
64
|
+
(venv / "bin" / "python3").write_text("#!/usr/bin/env python3")
|
|
65
|
+
skill = skills / "my-skill"
|
|
66
|
+
skill.mkdir()
|
|
67
|
+
(skill / "SKILL.md").write_text("# test")
|
|
68
|
+
|
|
69
|
+
clean_target(tmp_path, dry_run=False)
|
|
70
|
+
assert venv.exists(), ".venv should survive cleaning"
|
|
71
|
+
assert not skill.exists(), "skill dirs should be removed"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_clean_dry_run(tmp_path: Path):
|
|
75
|
+
"""Dry run counts but doesn't delete."""
|
|
76
|
+
agents = tmp_path / "agents"
|
|
77
|
+
agents.mkdir()
|
|
78
|
+
(agents / "test.toml").write_text("x = 1")
|
|
79
|
+
|
|
80
|
+
removed = clean_target(tmp_path, dry_run=True)
|
|
81
|
+
assert removed >= 1
|
|
82
|
+
assert agents.exists(), "dry-run should not delete"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_clean_removes_registry(tmp_path: Path):
|
|
86
|
+
"""Clean clears sync registry."""
|
|
87
|
+
registry = tmp_path / ".claudekit-sync-registry.json"
|
|
88
|
+
registry.write_text(json.dumps({"version": 1}))
|
|
89
|
+
|
|
90
|
+
clean_target(tmp_path, dry_run=False)
|
|
91
|
+
assert not registry.exists()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### [NEW] `tests/test_cli_args.py`
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
"""Tests for CLI argument parsing."""
|
|
98
|
+
|
|
99
|
+
import sys
|
|
100
|
+
from unittest.mock import patch
|
|
101
|
+
|
|
102
|
+
from claudekit_codex_sync.cli import parse_args
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_default_args():
|
|
106
|
+
"""Default: project scope, no flags."""
|
|
107
|
+
with patch.object(sys, "argv", ["ckc-sync"]):
|
|
108
|
+
args = parse_args()
|
|
109
|
+
assert not args.global_scope
|
|
110
|
+
assert not args.fresh
|
|
111
|
+
assert not args.force
|
|
112
|
+
assert not args.mcp
|
|
113
|
+
assert not args.no_deps
|
|
114
|
+
assert not args.dry_run
|
|
115
|
+
assert args.zip_path is None
|
|
116
|
+
assert args.source is None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_global_flag():
|
|
120
|
+
"""'-g' enables global scope."""
|
|
121
|
+
with patch.object(sys, "argv", ["ckc-sync", "-g"]):
|
|
122
|
+
args = parse_args()
|
|
123
|
+
assert args.global_scope
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_fresh_flag():
|
|
127
|
+
"""'-f' enables fresh clean."""
|
|
128
|
+
with patch.object(sys, "argv", ["ckc-sync", "-f"]):
|
|
129
|
+
args = parse_args()
|
|
130
|
+
assert args.fresh
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_combined_short_flags():
|
|
134
|
+
"""'-g -f -n' all work together."""
|
|
135
|
+
with patch.object(sys, "argv", ["ckc-sync", "-g", "-f", "-n"]):
|
|
136
|
+
args = parse_args()
|
|
137
|
+
assert args.global_scope
|
|
138
|
+
assert args.fresh
|
|
139
|
+
assert args.dry_run
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_zip_flag():
|
|
143
|
+
"""'--zip' sets zip path and implies zip mode."""
|
|
144
|
+
with patch.object(sys, "argv", ["ckc-sync", "--zip", "test.zip"]):
|
|
145
|
+
args = parse_args()
|
|
146
|
+
assert str(args.zip_path) == "test.zip"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_mcp_flag():
|
|
150
|
+
"""'--mcp' includes MCP skills."""
|
|
151
|
+
with patch.object(sys, "argv", ["ckc-sync", "--mcp"]):
|
|
152
|
+
args = parse_args()
|
|
153
|
+
assert args.mcp
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### [MODIFY] Docs
|
|
157
|
+
|
|
158
|
+
Update these files to reflect new CLI:
|
|
159
|
+
- `README.md` — new command name, flags, examples
|
|
160
|
+
- `docs/installation-guide.md` — new command, npm install
|
|
161
|
+
- `docs/codebase-summary.md` — new modules (clean_target.py), removed scripts/
|
|
162
|
+
|
|
163
|
+
**README.md Quick Start update:**
|
|
164
|
+
```markdown
|
|
165
|
+
## Quick Start
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npm install -g claudekit-codex-sync
|
|
169
|
+
|
|
170
|
+
# Project sync (to ./.codex/)
|
|
171
|
+
ckc-sync
|
|
172
|
+
|
|
173
|
+
# Global sync (to ~/.codex/)
|
|
174
|
+
ckc-sync -g
|
|
175
|
+
|
|
176
|
+
# Fresh re-sync
|
|
177
|
+
ckc-sync -g -f
|
|
178
|
+
|
|
179
|
+
# Preview
|
|
180
|
+
ckc-sync -g -n
|
|
181
|
+
```
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Verification
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Run all tests
|
|
188
|
+
PYTHONPATH=src python3 -m pytest tests/ -v
|
|
189
|
+
|
|
190
|
+
# Expected: 11 existing + 10 new = 21 tests passing
|
|
191
|
+
|
|
192
|
+
# Lint
|
|
193
|
+
python3 -m py_compile src/claudekit_codex_sync/*.py
|
|
194
|
+
|
|
195
|
+
# Full sync test
|
|
196
|
+
ckc-sync -g -f # fresh global sync
|
|
197
|
+
ckc-sync -n # project dry-run
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Todo
|
|
201
|
+
- [x] Add defensive mkdir in `path_normalizer.py`
|
|
202
|
+
- [x] Create `tests/test_clean_target.py` (4 tests)
|
|
203
|
+
- [x] Create `tests/test_cli_args.py` (6 tests)
|
|
204
|
+
- [x] Update `README.md`, `docs/installation-guide.md`, `docs/codebase-summary.md`
|
|
205
|
+
- [x] Run all tests
|
|
206
|
+
- [ ] `git commit && git push && npm publish`
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# v0.2 Plan — CLI Redesign + Cleanup
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Phase | What | Files | Status |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| 1 | Delete dead code + trim constants | `scripts/`, `constants.py` | [x] Complete |
|
|
8
|
+
| 2 | CLI redesign (ckc-sync, 8 flags) | `cli.py`, `clean_target.py`, `package.json`, `bin/` | [x] Implemented (local install step pending) |
|
|
9
|
+
| 3 | Symlink venv | `dep_bootstrapper.py` | [x] Complete |
|
|
10
|
+
| 4 | Wire unused functions + backup | `cli.py`, `asset_sync_dir.py`, `sync_registry.py` | [x] Complete |
|
|
11
|
+
| 5 | Safety fixes + tests + docs | `path_normalizer.py`, tests, docs | [x] Implemented (release ops pending) |
|
|
12
|
+
|
|
13
|
+
Details: see phase files.
|
|
14
|
+
|
|
15
|
+
## Tracking Notes
|
|
16
|
+
- Validation run on 2026-02-23: `python3 -m py_compile src/claudekit_codex_sync/*.py` passed.
|
|
17
|
+
- Validation run on 2026-02-23: `PYTHONPATH=src python3 -m pytest tests/ -q` passed (`21 passed`).
|
|
18
|
+
- Pending non-implementation ops: `npm install -g .`, `git commit && git push && npm publish`.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Validation + Execution Checklist
|
|
2
|
+
|
|
3
|
+
Date: 2026-02-23
|
|
4
|
+
Plan: `plans/260223-1310-v02-cli-redesign-cleanup/`
|
|
5
|
+
|
|
6
|
+
## Validation Verdict
|
|
7
|
+
Conditionally executable. Use with preflight fixes first.
|
|
8
|
+
|
|
9
|
+
### Baseline (current repo)
|
|
10
|
+
- `PYTHONPATH=src python3 -m pytest tests/ -q` -> 11 passed.
|
|
11
|
+
- `python3 -m py_compile src/claudekit_codex_sync/*.py` -> pass.
|
|
12
|
+
|
|
13
|
+
## Blocking Conflicts (Resolve First)
|
|
14
|
+
1. **Agent pipeline mismatch (high)**
|
|
15
|
+
Plan says remove `agents` from assets, but conversion reads only `codex_home/agents`. Current sync writes assets under `codex_home/claudekit/*`. If `agents` removed, agent conversion/registration becomes impossible.
|
|
16
|
+
Refs: `plans/260223-1310-v02-cli-redesign-cleanup/phase-01-delete-dead-code.md:53`, `plans/260223-1310-v02-cli-redesign-cleanup/phase-01-delete-dead-code.md:58`, `src/claudekit_codex_sync/asset_sync_dir.py:21`, `src/claudekit_codex_sync/path_normalizer.py:83`, `src/claudekit_codex_sync/config_enforcer.py:112`.
|
|
17
|
+
2. **Rules/CLAUDE removal conflicts with downstream references (high)**
|
|
18
|
+
Plan drops `rules` + `CLAUDE.md`, but generated `AGENTS.md` and path replacements still reference them.
|
|
19
|
+
Refs: `plans/260223-1310-v02-cli-redesign-cleanup/phase-01-delete-dead-code.md:53`, `plans/260223-1310-v02-cli-redesign-cleanup/phase-01-delete-dead-code.md:60`, `templates/agents-md.md:40`, `src/claudekit_codex_sync/constants.py:29`, `src/claudekit_codex_sync/constants.py:68`.
|
|
20
|
+
3. **`--force` semantic collision (high)**
|
|
21
|
+
Phase 2 maps `--force` to `include_conflicts`, mixing overwrite semantics with conflict-skill inclusion.
|
|
22
|
+
Refs: `plans/260223-1310-v02-cli-redesign-cleanup/phase-02-cli-redesign.md:114`, `src/claudekit_codex_sync/cli.py:40`, `src/claudekit_codex_sync/asset_sync_dir.py:73`, `src/claudekit_codex_sync/asset_sync_zip.py:90`.
|
|
23
|
+
4. **Registry wiring incomplete in plan text (medium)**
|
|
24
|
+
Phase 4 adds `registry`/`force` params to `sync_assets_from_dir`, but phase flow does not require passing these from CLI callsite; backup logic can remain inert.
|
|
25
|
+
Refs: `plans/260223-1310-v02-cli-redesign-cleanup/phase-04-wire-unused-functions.md:39`, `src/claudekit_codex_sync/cli.py:83`.
|
|
26
|
+
5. **Phase-order runtime break risk (medium)**
|
|
27
|
+
Phase 2 removes `include_test_deps` call before Phase 3 changes function signature. If implemented separately, bootstrap call breaks.
|
|
28
|
+
Refs: `plans/260223-1310-v02-cli-redesign-cleanup/phase-02-cli-redesign.md:203`, `src/claudekit_codex_sync/dep_bootstrapper.py:17`.
|
|
29
|
+
6. **Test count in plan is wrong (low)**
|
|
30
|
+
Plan expects 17 tests after adding 10; actual target should be 21 (11 existing + 10 new).
|
|
31
|
+
Ref: `plans/260223-1310-v02-cli-redesign-cleanup/phase-05-safety-tests-docs.md:190`.
|
|
32
|
+
|
|
33
|
+
## Operationalized Execution Checklist
|
|
34
|
+
|
|
35
|
+
### Gate 0: Scope Lock (must pass before coding)
|
|
36
|
+
- [ ] Decide asset contract for `agents`, `rules`, `CLAUDE.md` (keep vs remove) and update all dependent docs/templates/replacements consistently.
|
|
37
|
+
- [ ] Separate flags: keep `--force` for overwrite behavior only; do not reuse for conflict-skill include.
|
|
38
|
+
- [ ] Decide compatibility policy for legacy flags (`--source-mode`, `--skip-verify`, `--skip-agent-toml`, `--include-hooks`, `--respect-edits`).
|
|
39
|
+
|
|
40
|
+
Dependencies: none.
|
|
41
|
+
Risk: High if skipped (functional regression + broken agent/rules references).
|
|
42
|
+
|
|
43
|
+
### Phase A: CLI Redesign Skeleton (safe-first)
|
|
44
|
+
- [ ] Add new command surface (`ckc-sync` alias) in `package.json` while retaining `ck-codex-sync`.
|
|
45
|
+
- [ ] Introduce `clean_target.py` and `-f/--fresh` flow.
|
|
46
|
+
- [ ] Wire `register_agents()` call in CLI.
|
|
47
|
+
- [ ] Keep bootstrap signature compatibility until Phase B complete (or ship both changes atomically).
|
|
48
|
+
|
|
49
|
+
Dependencies: Gate 0.
|
|
50
|
+
Risk: Medium (breaking CLI behavior if compatibility policy unclear).
|
|
51
|
+
|
|
52
|
+
### Phase B: Dependency Bootstrap Symlink
|
|
53
|
+
- [ ] Implement symlink-first venv strategy with fallback creation.
|
|
54
|
+
- [ ] Ensure broken symlink handling + existing real `.venv` short-circuit.
|
|
55
|
+
- [ ] Validate Linux/WSL behavior with dry-run and real run.
|
|
56
|
+
|
|
57
|
+
Dependencies: Phase A (if CLI flags change), or atomic with Phase A for signature safety.
|
|
58
|
+
Risk: Medium (environment-specific symlink behavior).
|
|
59
|
+
|
|
60
|
+
### Phase C: Registry + Respect-Edits Wiring
|
|
61
|
+
- [ ] Update `sync_assets_from_dir()` to call `maybe_backup()`/`update_entry()`.
|
|
62
|
+
- [ ] Pass `registry` and overwrite policy from CLI callsite explicitly.
|
|
63
|
+
- [ ] Add defensive mkdir in `save_registry()`.
|
|
64
|
+
- [ ] Validate skip/backup/overwrite paths with deterministic fixtures.
|
|
65
|
+
|
|
66
|
+
Dependencies: Gate 0 flag semantics + Phase A CLI args.
|
|
67
|
+
Risk: High (data-loss risk if overwrite path wrong).
|
|
68
|
+
|
|
69
|
+
### Phase D: Safety + Tests + Docs
|
|
70
|
+
- [ ] Add `tests/test_clean_target.py` (4 tests) and `tests/test_cli_args.py` (6 tests).
|
|
71
|
+
- [ ] Correct expected total test count to 21.
|
|
72
|
+
- [ ] Update `README.md`, `docs/installation-guide.md`, `docs/codebase-summary.md`, `docs/system-architecture.md` to exact final behavior.
|
|
73
|
+
- [ ] Run: `pytest`, `py_compile`, and CLI smoke matrix for both command aliases.
|
|
74
|
+
|
|
75
|
+
Dependencies: Phases A-C complete.
|
|
76
|
+
Risk: Medium (doc/runtime drift if done early).
|
|
77
|
+
|
|
78
|
+
### Phase E: Release Gate
|
|
79
|
+
- [ ] Confirm no stale references to removed flags/assets.
|
|
80
|
+
- [ ] Confirm backward compatibility decision is documented.
|
|
81
|
+
- [ ] Package/install smoke: `npm install -g .`, `ckc-sync --help`, `ck-codex-sync --help`.
|
|
82
|
+
|
|
83
|
+
Dependencies: Phase D complete.
|
|
84
|
+
Risk: Low.
|
|
85
|
+
|
|
86
|
+
## Risk Notes (Condensed)
|
|
87
|
+
- **Data safety:** Overwrite logic currently under-specified; treat as release blocker.
|
|
88
|
+
- **Architecture consistency:** Asset contract changes touch constants, templates, docs, runtime behavior; enforce single source of truth.
|
|
89
|
+
- **Breaking CLI UX:** Removing flags without transition policy may break existing automation.
|
|
90
|
+
- **Cross-platform:** Symlink behavior varies; keep fallback path mandatory.
|
|
91
|
+
|
|
92
|
+
## Unresolved Questions
|
|
93
|
+
1. Should `rules` and `CLAUDE.md` remain first-class synced assets in v0.2, given `templates/agents-md.md` references?
|
|
94
|
+
2. Should agent `.md` files be synced to `codex_home/agents` (for conversion) instead of `codex_home/claudekit/agents`, or should conversion read from `claudekit/agents`?
|
|
95
|
+
3. Should legacy flags remain temporarily as deprecated aliases, or be removed immediately in v0.2?
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Completion Report
|
|
2
|
+
|
|
3
|
+
Date: 2026-02-23
|
|
4
|
+
Plan: `plans/260223-1310-v02-cli-redesign-cleanup/`
|
|
5
|
+
Role: project-manager fallback
|
|
6
|
+
|
|
7
|
+
## What Updated
|
|
8
|
+
- Marked implemented phases and todos complete in:
|
|
9
|
+
- `plans/260223-1310-v02-cli-redesign-cleanup/plan.md`
|
|
10
|
+
- `plans/260223-1310-v02-cli-redesign-cleanup/phase-01-delete-dead-code.md`
|
|
11
|
+
- `plans/260223-1310-v02-cli-redesign-cleanup/phase-02-cli-redesign.md`
|
|
12
|
+
- `plans/260223-1310-v02-cli-redesign-cleanup/phase-03-symlink-venv.md`
|
|
13
|
+
- `plans/260223-1310-v02-cli-redesign-cleanup/phase-04-wire-unused-functions.md`
|
|
14
|
+
- `plans/260223-1310-v02-cli-redesign-cleanup/phase-05-safety-tests-docs.md`
|
|
15
|
+
- Corrected Phase 5 expected test count from 17 to 21.
|
|
16
|
+
|
|
17
|
+
## Validation Evidence
|
|
18
|
+
- `python3 -m py_compile src/claudekit_codex_sync/*.py` -> pass
|
|
19
|
+
- `PYTHONPATH=src python3 -m pytest tests/ -q` -> `21 passed`
|
|
20
|
+
- Symlink smoke check for Phase 3 (`_try_symlink_venv`) -> source exists, symlink created in temp codex home
|
|
21
|
+
|
|
22
|
+
## Remaining Pending (Non-implementation Ops)
|
|
23
|
+
- Phase 2: `npm install -g .` (local command registration step)
|
|
24
|
+
- Phase 5: `git commit && git push && npm publish` (release workflow)
|
|
25
|
+
|
|
26
|
+
## Unresolved Questions
|
|
27
|
+
1. Do you want plan tracking to treat local install/release commands as required completion gates, or keep them tracked as post-implementation ops?
|
|
28
|
+
2. Should I also update roadmap/changelog docs to mirror this finalized plan status?
|
|
@@ -7,7 +7,8 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Dict
|
|
8
8
|
|
|
9
9
|
from .constants import ASSET_DIRS, ASSET_FILES, CONFLICT_SKILLS, EXCLUDED_SKILLS_ALWAYS, MCP_SKILLS
|
|
10
|
-
from .
|
|
10
|
+
from .sync_registry import check_user_edit, maybe_backup, update_entry
|
|
11
|
+
from .utils import compute_hash, create_backup, is_excluded_path, write_bytes_if_changed
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def sync_assets_from_dir(
|
|
@@ -16,11 +17,14 @@ def sync_assets_from_dir(
|
|
|
16
17
|
codex_home: Path,
|
|
17
18
|
include_hooks: bool,
|
|
18
19
|
dry_run: bool,
|
|
20
|
+
registry: dict | None = None,
|
|
21
|
+
force: bool = True,
|
|
19
22
|
) -> Dict[str, int]:
|
|
20
23
|
"""Sync non-skill assets from live directory."""
|
|
21
24
|
claudekit_dir = codex_home / "claudekit"
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
if not dry_run:
|
|
26
|
+
claudekit_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
added = updated = skipped = 0
|
|
24
28
|
|
|
25
29
|
for dirname in ASSET_DIRS:
|
|
26
30
|
src_dir = source / dirname
|
|
@@ -34,35 +38,81 @@ def sync_assets_from_dir(
|
|
|
34
38
|
if not src_file.is_file() or is_excluded_path(src_file.parts):
|
|
35
39
|
continue
|
|
36
40
|
rel = src_file.relative_to(src_dir)
|
|
41
|
+
rel_path = f"claudekit/{dirname}/{rel}"
|
|
37
42
|
dst = dst_dir / rel
|
|
43
|
+
|
|
44
|
+
if not force and registry and dst.exists():
|
|
45
|
+
entry = registry.get("entries", {}).get(rel_path)
|
|
46
|
+
if entry:
|
|
47
|
+
if dry_run and check_user_edit(entry, dst):
|
|
48
|
+
skipped += 1
|
|
49
|
+
print(f"skip(user-edit): {rel_path}")
|
|
50
|
+
continue
|
|
51
|
+
backup = maybe_backup(registry, rel_path, dst, respect_edits=True)
|
|
52
|
+
if backup:
|
|
53
|
+
skipped += 1
|
|
54
|
+
print(f"skip(user-edit): {rel_path}")
|
|
55
|
+
continue
|
|
56
|
+
elif compute_hash(src_file) != compute_hash(dst):
|
|
57
|
+
if dry_run:
|
|
58
|
+
print(f"[dry-run] backup(untracked): {rel_path}")
|
|
59
|
+
else:
|
|
60
|
+
backup = create_backup(dst)
|
|
61
|
+
print(f"backup(untracked): {rel_path} -> {backup.name}")
|
|
62
|
+
|
|
38
63
|
data = src_file.read_bytes()
|
|
39
64
|
mode = src_file.stat().st_mode & 0o777 if src_file.stat().st_mode & 0o111 else None
|
|
40
65
|
changed, is_added = write_bytes_if_changed(dst, data, mode=mode, dry_run=dry_run)
|
|
41
66
|
if changed:
|
|
42
67
|
if is_added:
|
|
43
68
|
added += 1
|
|
44
|
-
print(f"add:
|
|
69
|
+
print(f"add: {rel_path}")
|
|
45
70
|
else:
|
|
46
71
|
updated += 1
|
|
47
|
-
print(f"update:
|
|
72
|
+
print(f"update: {rel_path}")
|
|
73
|
+
if registry and not dry_run and dst.exists():
|
|
74
|
+
update_entry(registry, rel_path, src_file, dst)
|
|
48
75
|
|
|
49
76
|
for filename in ASSET_FILES:
|
|
50
77
|
src = source / filename
|
|
51
78
|
if not src.exists():
|
|
52
79
|
continue
|
|
80
|
+
rel_path = f"claudekit/{filename}"
|
|
53
81
|
dst = claudekit_dir / filename
|
|
82
|
+
|
|
83
|
+
if not force and registry and dst.exists():
|
|
84
|
+
entry = registry.get("entries", {}).get(rel_path)
|
|
85
|
+
if entry:
|
|
86
|
+
if dry_run and check_user_edit(entry, dst):
|
|
87
|
+
skipped += 1
|
|
88
|
+
print(f"skip(user-edit): {rel_path}")
|
|
89
|
+
continue
|
|
90
|
+
backup = maybe_backup(registry, rel_path, dst, respect_edits=True)
|
|
91
|
+
if backup:
|
|
92
|
+
skipped += 1
|
|
93
|
+
print(f"skip(user-edit): {rel_path}")
|
|
94
|
+
continue
|
|
95
|
+
elif compute_hash(src) != compute_hash(dst):
|
|
96
|
+
if dry_run:
|
|
97
|
+
print(f"[dry-run] backup(untracked): {rel_path}")
|
|
98
|
+
else:
|
|
99
|
+
backup = create_backup(dst)
|
|
100
|
+
print(f"backup(untracked): {rel_path} -> {backup.name}")
|
|
101
|
+
|
|
54
102
|
data = src.read_bytes()
|
|
55
103
|
mode = src.stat().st_mode & 0o777 if src.stat().st_mode & 0o111 else None
|
|
56
104
|
changed, is_added = write_bytes_if_changed(dst, data, mode=mode, dry_run=dry_run)
|
|
57
105
|
if changed:
|
|
58
106
|
if is_added:
|
|
59
107
|
added += 1
|
|
60
|
-
print(f"add:
|
|
108
|
+
print(f"add: {rel_path}")
|
|
61
109
|
else:
|
|
62
110
|
updated += 1
|
|
63
|
-
print(f"update:
|
|
111
|
+
print(f"update: {rel_path}")
|
|
112
|
+
if registry and not dry_run and dst.exists():
|
|
113
|
+
update_entry(registry, rel_path, src, dst)
|
|
64
114
|
|
|
65
|
-
return {"added": added, "updated": updated, "removed": 0, "
|
|
115
|
+
return {"added": added, "updated": updated, "removed": 0, "skipped": skipped}
|
|
66
116
|
|
|
67
117
|
|
|
68
118
|
def sync_skills_from_dir(
|
|
@@ -120,6 +170,7 @@ def sync_skills_from_dir(
|
|
|
120
170
|
ignore = shutil.ignore_patterns("*.pyc", "__pycache__", ".venv", "node_modules", "dist", "build")
|
|
121
171
|
shutil.copytree(skill_dir, dst, ignore=ignore)
|
|
122
172
|
|
|
123
|
-
|
|
173
|
+
if not dry_run:
|
|
174
|
+
skills_dst.mkdir(parents=True, exist_ok=True)
|
|
124
175
|
total_skills = len(list(skills_dst.rglob("SKILL.md")))
|
|
125
176
|
return {"added": added, "updated": updated, "skipped": skipped, "total_skills": total_skills}
|
|
@@ -16,7 +16,16 @@ from .constants import (
|
|
|
16
16
|
MCP_SKILLS,
|
|
17
17
|
)
|
|
18
18
|
from .source_resolver import collect_skill_entries, zip_mode
|
|
19
|
-
from .utils import load_manifest, save_manifest, write_bytes_if_changed
|
|
19
|
+
from .utils import SyncError, load_manifest, save_manifest, write_bytes_if_changed
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _validate_zip_relpath(rel: str, zip_name: str) -> str:
|
|
23
|
+
"""Validate zip relative path and return normalized form."""
|
|
24
|
+
normalized = rel.replace("\\", "/")
|
|
25
|
+
path = Path(normalized)
|
|
26
|
+
if path.is_absolute() or ".." in path.parts:
|
|
27
|
+
raise SyncError(f"Unsafe zip entry path: {zip_name}")
|
|
28
|
+
return normalized
|
|
20
29
|
|
|
21
30
|
|
|
22
31
|
def sync_assets(
|
|
@@ -35,7 +44,7 @@ def sync_assets(
|
|
|
35
44
|
for name in zf.namelist():
|
|
36
45
|
if name.endswith("/") or not name.startswith(".claude/"):
|
|
37
46
|
continue
|
|
38
|
-
rel = name[len(".claude/") :]
|
|
47
|
+
rel = _validate_zip_relpath(name[len(".claude/") :], name)
|
|
39
48
|
first = rel.split("/", 1)[0]
|
|
40
49
|
if first == "hooks" and include_hooks:
|
|
41
50
|
selected.append((name, rel))
|
|
@@ -47,10 +56,11 @@ def sync_assets(
|
|
|
47
56
|
added = updated = removed = 0
|
|
48
57
|
|
|
49
58
|
for rel in sorted(old_manifest - new_manifest):
|
|
50
|
-
|
|
59
|
+
safe_rel = _validate_zip_relpath(rel, rel)
|
|
60
|
+
target = claudekit_dir / safe_rel
|
|
51
61
|
if target.exists():
|
|
52
62
|
removed += 1
|
|
53
|
-
print(f"remove: {
|
|
63
|
+
print(f"remove: {safe_rel}")
|
|
54
64
|
if not dry_run:
|
|
55
65
|
target.unlink()
|
|
56
66
|
|
|
@@ -135,6 +145,7 @@ def sync_skills(
|
|
|
135
145
|
dst = dst_skill_dir / inner
|
|
136
146
|
write_bytes_if_changed(dst, data, mode=zip_mode(info), dry_run=False)
|
|
137
147
|
|
|
138
|
-
|
|
148
|
+
if not dry_run:
|
|
149
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
139
150
|
total_skills = len(list(skills_dir.rglob("SKILL.md")))
|
|
140
151
|
return {"added": added, "updated": updated, "skipped": skipped, "total_skills": total_skills}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Clean target directories for --fresh sync."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def clean_target(codex_home: Path, *, dry_run: bool) -> int:
|
|
10
|
+
"""Remove agents, skills (keep .venv), prompts, claudekit before fresh sync."""
|
|
11
|
+
removed = 0
|
|
12
|
+
|
|
13
|
+
for subdir in ("agents", "prompts", "claudekit"):
|
|
14
|
+
target = codex_home / subdir
|
|
15
|
+
if target.exists():
|
|
16
|
+
count = sum(1 for item in target.rglob("*") if item.is_file())
|
|
17
|
+
removed += count
|
|
18
|
+
print(f"fresh: rm {subdir}/ ({count} files)")
|
|
19
|
+
if not dry_run:
|
|
20
|
+
shutil.rmtree(target)
|
|
21
|
+
|
|
22
|
+
skills = codex_home / "skills"
|
|
23
|
+
if skills.exists():
|
|
24
|
+
for item in skills.iterdir():
|
|
25
|
+
if item.name == ".venv":
|
|
26
|
+
# Keep symlinks (pointing to ~/.claude/skills/.venv)
|
|
27
|
+
# Delete real venv dirs so symlink can be created on next bootstrap
|
|
28
|
+
if item.is_symlink():
|
|
29
|
+
continue
|
|
30
|
+
# Real venv dir → delete so symlink-first strategy works
|
|
31
|
+
count = sum(1 for p in item.rglob("*") if p.is_file())
|
|
32
|
+
removed += count
|
|
33
|
+
if not dry_run:
|
|
34
|
+
shutil.rmtree(item)
|
|
35
|
+
print("fresh: rm skills/.venv (real dir, will be re-symlinked)")
|
|
36
|
+
continue
|
|
37
|
+
if item.is_dir():
|
|
38
|
+
count = sum(1 for path in item.rglob("*") if path.is_file())
|
|
39
|
+
removed += count
|
|
40
|
+
if not dry_run:
|
|
41
|
+
shutil.rmtree(item)
|
|
42
|
+
else:
|
|
43
|
+
removed += 1
|
|
44
|
+
if not dry_run:
|
|
45
|
+
item.unlink()
|
|
46
|
+
print("fresh: rm skills/* (kept .venv)")
|
|
47
|
+
|
|
48
|
+
for name in (
|
|
49
|
+
".claudekit-sync-registry.json",
|
|
50
|
+
".sync-manifest-assets.txt",
|
|
51
|
+
".claudekit-generated-prompts.txt",
|
|
52
|
+
):
|
|
53
|
+
target = codex_home / name
|
|
54
|
+
if target.exists():
|
|
55
|
+
removed += 1
|
|
56
|
+
if not dry_run:
|
|
57
|
+
target.unlink()
|
|
58
|
+
|
|
59
|
+
return removed
|