claudekit-codex-sync 0.2.0 → 0.2.2
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 +1 -1
- package/docs/codebase-summary.md +4 -4
- package/docs/system-architecture.md +10 -4
- package/package.json +7 -2
- package/src/claudekit_codex_sync/asset_sync_dir.py +22 -0
- package/src/claudekit_codex_sync/clean_target.py +10 -0
- package/src/claudekit_codex_sync/constants.py +2 -2
- package/src/claudekit_codex_sync/dep_bootstrapper.py +22 -18
- package/src/claudekit_codex_sync/runtime_verifier.py +17 -7
- package/tests/test_clean_target.py +26 -6
package/AGENTS.md
CHANGED
|
@@ -35,11 +35,8 @@ Codex working profile for this workspace, adapted from ClaudeKit rules and workf
|
|
|
35
35
|
- Activate relevant skills intentionally per task.
|
|
36
36
|
- For legacy ClaudeKit command intents (`/ck-help`, `/coding-level`, `/ask`, `/docs/*`, `/journal`, `/watzup`), use `$claudekit-command-bridge`.
|
|
37
37
|
|
|
38
|
-
## Reference Material
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
- `~/.codex/claudekit/rules/orchestration-protocol.md`
|
|
44
|
-
- `~/.codex/claudekit/rules/documentation-management.md`
|
|
45
|
-
- `~/.codex/claudekit/rules/team-coordination-rules.md`
|
|
38
|
+
## Reference Material
|
|
39
|
+
|
|
40
|
+
- `README.md`
|
|
41
|
+
- `docs/`
|
|
42
|
+
- `plans/`
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ ClaudeKit uses `~/.claude/` conventions and agent markdown frontmatter. Codex us
|
|
|
13
13
|
| **Scope select** | Sync to project `./.codex/` (default) or global `~/.codex/` (`-g`) |
|
|
14
14
|
| **Fresh clean** | Optional `-f` cleanup of target dirs before sync |
|
|
15
15
|
| **Source resolve** | Uses live `~/.claude/` or `--zip` export |
|
|
16
|
-
| **Asset sync** | Copies commands/output-styles/scripts
|
|
16
|
+
| **Asset sync** | Copies agents `.md` → `agents/` (for TOML conversion), commands/output-styles/rules/scripts → `codex_home/claudekit/` |
|
|
17
17
|
| **Skill sync** | Copies skills into `codex_home/skills/` |
|
|
18
18
|
| **Path normalize** | Rewrites `.claude` references to `.codex` |
|
|
19
19
|
| **Config enforce** | Ensures `config.toml`, feature flags, and agent registration |
|
package/docs/codebase-summary.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
### Core Orchestration
|
|
19
19
|
|
|
20
20
|
- **`cli.py`** — Main entry point with 8-flag interface (`-g`, `-f`, `--force`, `--zip`, `--source`, `--mcp`, `--no-deps`, `-n`).
|
|
21
|
-
- **`clean_target.py`** — Fresh-sync cleaner for `--fresh` (
|
|
21
|
+
- **`clean_target.py`** — Fresh-sync cleaner for `--fresh` (deletes real `.venv`, keeps symlinks).
|
|
22
22
|
|
|
23
23
|
### Source Resolution
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
### Asset & Skill Sync
|
|
28
28
|
|
|
29
|
-
- **`asset_sync_dir.py`** — Live sync for assets and skills
|
|
29
|
+
- **`asset_sync_dir.py`** — Live sync for assets, agents, and skills. Copies agents directly to `codex_home/agents/` for TOML conversion. Registry-aware overwrite/backup.
|
|
30
30
|
- **`asset_sync_zip.py`** — Zip-based sync path.
|
|
31
31
|
|
|
32
32
|
### Normalization & Conversion
|
|
@@ -75,6 +75,6 @@ Configured Codex home (.codex project scope or ~/.codex global)
|
|
|
75
75
|
|---|---|
|
|
76
76
|
| `tests/test_config_enforcer.py` | 4 |
|
|
77
77
|
| `tests/test_path_normalizer.py` | 7 |
|
|
78
|
-
| `tests/test_clean_target.py` |
|
|
78
|
+
| `tests/test_clean_target.py` | 5 |
|
|
79
79
|
| `tests/test_cli_args.py` | 6 |
|
|
80
|
-
| **Total** | **
|
|
80
|
+
| **Total** | **22** |
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
┌─────────────────────┐ ┌──────────────────────────┐
|
|
7
7
|
│ ClaudeKit Source │ │ Codex Target │
|
|
8
8
|
│ ~/.claude/ or zip │───▶│ ./.codex or ~/.codex │
|
|
9
|
+
│ agents/*.md │ │ agents/*.toml (convert) │
|
|
9
10
|
│ skills/* │ │ skills/* │
|
|
10
11
|
│ commands/ │ │ claudekit/commands/ │
|
|
11
12
|
│ output-styles/ │ │ claudekit/output-styles/│
|
|
13
|
+
│ rules/ │ │ claudekit/rules/ │
|
|
12
14
|
│ scripts/ │ │ claudekit/scripts/ │
|
|
13
15
|
└─────────────────────┘ │ prompts/* (generated) │
|
|
14
16
|
│ config.toml │
|
|
@@ -25,12 +27,15 @@ Workspace baseline: `./AGENTS.md` is ensured in the current working directory.
|
|
|
25
27
|
- Select source: live (`~/.claude/`) or zip (`--zip`)
|
|
26
28
|
|
|
27
29
|
2. **Asset/skill sync**
|
|
28
|
-
- Copy
|
|
30
|
+
- Copy agents `.md` directly to `codex_home/agents/` (for TOML conversion)
|
|
31
|
+
- Copy managed assets (commands, output-styles, rules, scripts) to `codex_home/claudekit/`
|
|
32
|
+
- Copy skills to `codex_home/skills/`
|
|
29
33
|
- Apply registry-aware overwrite behavior (`--force`)
|
|
30
34
|
|
|
31
35
|
3. **Normalization**
|
|
32
36
|
- Rewrite `.claude` references to `.codex`
|
|
33
|
-
-
|
|
37
|
+
- Convert agent `.md` (YAML frontmatter) → `.toml` with model mapping
|
|
38
|
+
- Normalize existing agent TOMLs and compatibility patches
|
|
34
39
|
|
|
35
40
|
4. **Config enforcement**
|
|
36
41
|
- Enforce `config.toml` defaults
|
|
@@ -64,6 +69,7 @@ Workspace baseline: `./AGENTS.md` is ensured in the current working directory.
|
|
|
64
69
|
## Design Notes
|
|
65
70
|
|
|
66
71
|
- Project-first scope reduces accidental global writes while developing.
|
|
67
|
-
- `
|
|
72
|
+
- Agents copied to top-level `agents/` (not `claudekit/agents/`) to match conversion function expectations.
|
|
73
|
+
- `clean_target.py` deletes real `.venv` dirs (keeps symlinks) so re-symlink works on refresh.
|
|
74
|
+
- Bootstrap skips pip install when venv is symlinked — packages already present.
|
|
68
75
|
- Registry writes are defensive (`mkdir` before save) to avoid missing-path failures.
|
|
69
|
-
- Bootstrap path prefers symlink reuse for speed; fallback path ensures portability.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudekit-codex-sync",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Sync ClaudeKit skills, agents, and config to Codex CLI",
|
|
5
5
|
"main": "bin/ck-codex-sync.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"test": "PYTHONPATH=src python3 -m pytest tests/",
|
|
13
13
|
"lint": "python3 -m py_compile src/claudekit_codex_sync/*.py"
|
|
14
14
|
},
|
|
15
|
-
"keywords": [
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claudekit",
|
|
17
|
+
"codex",
|
|
18
|
+
"sync",
|
|
19
|
+
"skills"
|
|
20
|
+
],
|
|
16
21
|
"license": "MIT"
|
|
17
22
|
}
|
|
@@ -112,6 +112,28 @@ def sync_assets_from_dir(
|
|
|
112
112
|
if registry and not dry_run and dst.exists():
|
|
113
113
|
update_entry(registry, rel_path, src, dst)
|
|
114
114
|
|
|
115
|
+
# Copy agents directly to codex_home/agents/ (NOT claudekit/agents/)
|
|
116
|
+
# so convert_agents_md_to_toml() can find and convert them
|
|
117
|
+
agents_src = source / "agents"
|
|
118
|
+
if agents_src.exists():
|
|
119
|
+
agents_dst = codex_home / "agents"
|
|
120
|
+
if not dry_run:
|
|
121
|
+
agents_dst.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
for src_file in sorted(agents_src.rglob("*.md")):
|
|
123
|
+
if not src_file.is_file():
|
|
124
|
+
continue
|
|
125
|
+
rel = src_file.relative_to(agents_src)
|
|
126
|
+
dst = agents_dst / rel
|
|
127
|
+
data = src_file.read_bytes()
|
|
128
|
+
changed, is_added = write_bytes_if_changed(dst, data, mode=None, dry_run=dry_run)
|
|
129
|
+
if changed:
|
|
130
|
+
if is_added:
|
|
131
|
+
added += 1
|
|
132
|
+
print(f"add: agents/{rel}")
|
|
133
|
+
else:
|
|
134
|
+
updated += 1
|
|
135
|
+
print(f"update: agents/{rel}")
|
|
136
|
+
|
|
115
137
|
return {"added": added, "updated": updated, "removed": 0, "skipped": skipped}
|
|
116
138
|
|
|
117
139
|
|
|
@@ -23,6 +23,16 @@ def clean_target(codex_home: Path, *, dry_run: bool) -> int:
|
|
|
23
23
|
if skills.exists():
|
|
24
24
|
for item in skills.iterdir():
|
|
25
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)")
|
|
26
36
|
continue
|
|
27
37
|
if item.is_dir():
|
|
28
38
|
count = sum(1 for path in item.rglob("*") if path.is_file())
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import List, Set, Tuple
|
|
4
4
|
|
|
5
|
-
ASSET_DIRS = {"commands", "output-styles", "scripts"}
|
|
6
|
-
ASSET_FILES = {".env.example"}
|
|
5
|
+
ASSET_DIRS = {"commands", "output-styles", "rules", "scripts"}
|
|
6
|
+
ASSET_FILES = {".env.example", ".ck.json"}
|
|
7
7
|
ASSET_MANIFEST = ".sync-manifest-assets.txt"
|
|
8
8
|
PROMPT_MANIFEST = ".claudekit-generated-prompts.txt"
|
|
9
9
|
REGISTRY_FILE = ".claudekit-sync-registry.json"
|
|
@@ -88,24 +88,28 @@ def bootstrap_deps(
|
|
|
88
88
|
run_cmd(["python3", "-m", "venv", str(venv_dir)], dry_run=dry_run)
|
|
89
89
|
run_cmd([str(py_bin), "-m", "pip", "install", "--upgrade", "pip"], dry_run=dry_run)
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
# Skip dependency install when venv is symlinked — packages already in source
|
|
92
|
+
if not symlinked:
|
|
93
|
+
req_files = sorted(skills_dir.rglob("requirements*.txt"))
|
|
94
|
+
for req in req_files:
|
|
95
|
+
if is_excluded_path(req.parts):
|
|
96
|
+
continue
|
|
97
|
+
if not include_mcp and ("mcp-builder" in req.parts or "mcp-management" in req.parts):
|
|
98
|
+
continue
|
|
99
|
+
try:
|
|
100
|
+
run_cmd([str(py_bin), "-m", "pip", "install", "-r", str(req)], dry_run=dry_run)
|
|
101
|
+
py_ok += 1
|
|
102
|
+
except subprocess.CalledProcessError:
|
|
103
|
+
py_fail += 1
|
|
104
|
+
eprint(f"python deps failed: {req}")
|
|
105
|
+
|
|
106
|
+
node_ok, node_fail = _install_node_deps(
|
|
107
|
+
skills_dir=skills_dir,
|
|
108
|
+
include_mcp=include_mcp,
|
|
109
|
+
dry_run=dry_run,
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
print("skip: deps install (venv symlinked, packages shared)")
|
|
109
113
|
|
|
110
114
|
return {
|
|
111
115
|
"python_ok": py_ok,
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
5
7
|
from pathlib import Path
|
|
6
|
-
from typing import
|
|
7
|
-
|
|
8
|
-
from .utils import run_cmd
|
|
8
|
+
from typing import Any, Dict
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def verify_runtime(*, codex_home: Path, dry_run: bool) -> Dict[str, Any]:
|
|
@@ -13,19 +13,29 @@ def verify_runtime(*, codex_home: Path, dry_run: bool) -> Dict[str, Any]:
|
|
|
13
13
|
if dry_run:
|
|
14
14
|
return {"skipped": True}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# Silent codex check — just verify it exists and runs
|
|
17
|
+
codex_bin = shutil.which("codex")
|
|
18
|
+
codex_ok = False
|
|
19
|
+
if codex_bin:
|
|
20
|
+
result = subprocess.run(
|
|
21
|
+
[codex_bin, "--version"], capture_output=True, timeout=10
|
|
22
|
+
)
|
|
23
|
+
codex_ok = result.returncode == 0
|
|
17
24
|
|
|
25
|
+
# Silent copywriting check
|
|
18
26
|
copy_script = codex_home / "skills" / "copywriting" / "scripts" / "extract-writing-styles.py"
|
|
19
27
|
py_bin = codex_home / "skills" / ".venv" / "bin" / "python3"
|
|
20
28
|
copywriting_ok = False
|
|
21
29
|
if copy_script.exists() and py_bin.exists():
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
result = subprocess.run(
|
|
31
|
+
[str(py_bin), str(copy_script), "--list"], capture_output=True, timeout=30
|
|
32
|
+
)
|
|
33
|
+
copywriting_ok = result.returncode == 0
|
|
24
34
|
|
|
25
35
|
prompts_count = len(list((codex_home / "prompts").glob("*.md")))
|
|
26
36
|
skills_count = len(list((codex_home / "skills").rglob("SKILL.md")))
|
|
27
37
|
return {
|
|
28
|
-
"
|
|
38
|
+
"codex": "ok" if codex_ok else "missing",
|
|
29
39
|
"copywriting": "ok" if copywriting_ok else "skipped",
|
|
30
40
|
"prompts": prompts_count,
|
|
31
41
|
"skills": skills_count,
|
|
@@ -18,23 +18,43 @@ def test_clean_removes_agents(tmp_path: Path):
|
|
|
18
18
|
assert removed >= 2
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def
|
|
22
|
-
"""Clean keeps
|
|
21
|
+
def test_clean_keeps_venv_symlink(tmp_path: Path):
|
|
22
|
+
"""Clean keeps symlinked .venv but deletes real .venv dirs."""
|
|
23
23
|
skills = tmp_path / "skills"
|
|
24
24
|
skills.mkdir()
|
|
25
|
+
|
|
26
|
+
# Create a real source venv to symlink to
|
|
27
|
+
source_venv = tmp_path / "source_venv"
|
|
28
|
+
source_venv.mkdir()
|
|
29
|
+
(source_venv / "bin").mkdir()
|
|
30
|
+
(source_venv / "bin" / "python3").write_text("#!/usr/bin/env python3")
|
|
31
|
+
|
|
32
|
+
# Symlink .venv → source_venv (simulates symlink to ~/.claude/skills/.venv)
|
|
25
33
|
venv = skills / ".venv"
|
|
26
|
-
venv.
|
|
27
|
-
|
|
28
|
-
(venv / "bin" / "python3").write_text("#!/usr/bin/env python3")
|
|
34
|
+
venv.symlink_to(source_venv)
|
|
35
|
+
|
|
29
36
|
skill = skills / "my-skill"
|
|
30
37
|
skill.mkdir()
|
|
31
38
|
(skill / "SKILL.md").write_text("# test")
|
|
32
39
|
|
|
33
40
|
clean_target(tmp_path, dry_run=False)
|
|
34
|
-
assert venv.
|
|
41
|
+
assert venv.is_symlink(), "symlinked .venv should survive cleaning"
|
|
35
42
|
assert not skill.exists(), "skill dirs should be removed"
|
|
36
43
|
|
|
37
44
|
|
|
45
|
+
def test_clean_deletes_real_venv(tmp_path: Path):
|
|
46
|
+
"""Clean deletes real (non-symlink) .venv for re-symlinking."""
|
|
47
|
+
skills = tmp_path / "skills"
|
|
48
|
+
skills.mkdir()
|
|
49
|
+
venv = skills / ".venv"
|
|
50
|
+
venv.mkdir()
|
|
51
|
+
(venv / "bin").mkdir()
|
|
52
|
+
(venv / "bin" / "python3").write_text("#!/usr/bin/env python3")
|
|
53
|
+
|
|
54
|
+
clean_target(tmp_path, dry_run=False)
|
|
55
|
+
assert not venv.exists(), "real .venv should be deleted for re-symlinking"
|
|
56
|
+
|
|
57
|
+
|
|
38
58
|
def test_clean_dry_run(tmp_path: Path):
|
|
39
59
|
"""Dry run counts but doesn't delete."""
|
|
40
60
|
agents = tmp_path / "agents"
|