claudekit-codex-sync 0.2.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 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 (Imported from ClaudeKit)
39
-
40
- - `~/.codex/claudekit/CLAUDE.md`
41
- - `~/.codex/claudekit/rules/development-rules.md`
42
- - `~/.codex/claudekit/rules/primary-workflow.md`
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudekit-codex-sync",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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": ["claudekit", "codex", "sync", "skills"],
15
+ "keywords": [
16
+ "claudekit",
17
+ "codex",
18
+ "sync",
19
+ "skills"
20
+ ],
16
21
  "license": "MIT"
17
22
  }
@@ -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())
@@ -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
- req_files = sorted(skills_dir.rglob("requirements*.txt"))
92
- for req in req_files:
93
- if is_excluded_path(req.parts):
94
- continue
95
- if not include_mcp and ("mcp-builder" in req.parts or "mcp-management" in req.parts):
96
- continue
97
- try:
98
- run_cmd([str(py_bin), "-m", "pip", "install", "-r", str(req)], dry_run=dry_run)
99
- py_ok += 1
100
- except subprocess.CalledProcessError:
101
- py_fail += 1
102
- eprint(f"python deps failed: {req}")
103
-
104
- node_ok, node_fail = _install_node_deps(
105
- skills_dir=skills_dir,
106
- include_mcp=include_mcp,
107
- dry_run=dry_run,
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 Dict, Optional, Any
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
- run_cmd(["codex", "--help"], dry_run=False)
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
- run_cmd([str(py_bin), str(copy_script), "--list"], dry_run=False)
23
- copywriting_ok = True
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
- "codex_help": "ok",
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 test_clean_keeps_venv(tmp_path: Path):
22
- """Clean keeps skills/.venv intact."""
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.mkdir()
27
- (venv / "bin").mkdir()
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.exists(), ".venv should survive cleaning"
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"