File size: 8,140 Bytes
216004d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
"""Bootstrap a compile-first knowledge system into a target repo.

The actual file contents live as real files under
`skills/knowledge-system-bootstrap/templates/`. This script copies them
into the target with sentinel-string substitution. That keeps every
generated script lintable, testable, and reviewable on its own.

Substitution sentinels (plain string.replace, no Template machinery so
templates can contain literal `$`):
  __PROJECT_NAME__    — human-readable project name passed on the CLI
  __RAW_ROOT_NAME__   — folder name for the local raw root
  __TODAY__           — today's date in YYYY-MM-DD
"""
from __future__ import annotations

import argparse
import re
import shutil
from datetime import date, datetime
from pathlib import Path

__version__ = "2.0.0"  # bumped for v2 scripts (wiki_lint, supersede_check, crystallize, hybrid_search, graph_export, intake_filter) merged under SKILL_ROOT/scripts/

SKILL_ROOT = Path(__file__).resolve().parents[1]
TEMPLATES = SKILL_ROOT / "templates"


# (template-relative path, target-relative path)
TEMPLATE_TO_TARGET: list[tuple[str, str]] = [
    ("configs/AGENTS.md", "AGENTS.md"),
    ("configs/CLAUDE.md", "CLAUDE.md"),
    ("configs/.cursorrules", ".cursorrules"),
    ("configs/.windsurfrules", ".windsurfrules"),
    ("configs/.gitignore", ".gitignore"),
    ("wiki/README.md", "docs/wiki/README.md"),
    ("wiki/SCHEMA.md", "docs/wiki/SCHEMA.md"),
    ("wiki/index.md", "docs/wiki/index.md"),
    ("wiki/log.md", "docs/wiki/log.md"),
    ("wiki/project-overview.md", "docs/wiki/project-overview.md"),
    ("wiki/current-status.md", "docs/wiki/current-status.md"),
    ("wiki/sources-and-data.md", "docs/wiki/sources-and-data.md"),
    ("wiki/github-and-raw-strategy.md", "docs/wiki/github-and-raw-strategy.md"),
    ("wiki/runtime-profile.md", "docs/wiki/runtime-profile.md"),
    ("manifests/README.md", "manifests/README.md"),
    ("manifests/raw_sources.csv", "manifests/raw_sources.csv"),
    ("manifests/raw_sources.meta.json", "manifests/raw_sources.meta.json"),
    ("scripts/wiki_check.py", "scripts/wiki_check.py"),
    ("scripts/raw_manifest_check.py", "scripts/raw_manifest_check.py"),
    ("scripts/ingest_raw.py", "scripts/ingest_raw.py"),
    ("scripts/untracked_raw_check.py", "scripts/untracked_raw_check.py"),
    ("scripts/provenance_check.py", "scripts/provenance_check.py"),
    ("scripts/stale_report.py", "scripts/stale_report.py"),
    ("scripts/delta_compile.py", "scripts/delta_compile.py"),
    ("scripts/version_check.py", "scripts/version_check.py"),
    ("scripts/wiki_size_report.py", "scripts/wiki_size_report.py"),
    ("scripts/init_raw_root.py", "scripts/init_raw_root.py"),
    ("scripts/export_memory_repo.py", "scripts/export_memory_repo.py"),
    ("scripts/upgrade.sh", "scripts/upgrade.sh"),
    # v2.0.0 — new scripts merged under scripts/, no longer in templates/scripts/
    ("scripts/wiki_lint.py", "scripts/wiki_lint.py"),
    ("scripts/supersede_check.py", "scripts/supersede_check.py"),
    ("scripts/crystallize.py", "scripts/crystallize.py"),
    ("scripts/hybrid_search.py", "scripts/hybrid_search.py"),
    ("scripts/graph_export.py", "scripts/graph_export.py"),
    ("scripts/intake_filter.py", "scripts/intake_filter.py"),
    ("claude-commands/wiki-check.md", ".claude/commands/wiki-check.md"),
    ("claude-commands/wiki-upgrade.md", ".claude/commands/wiki-upgrade.md"),
    ("claude-commands/wiki-status.md", ".claude/commands/wiki-status.md"),
    ("github/workflows/wiki-lint.yml", ".github/workflows/wiki-lint.yml"),
]


def slugify(value: str) -> str:
    value = value.strip().lower()
    value = re.sub(r"[^a-z0-9]+", "_", value)
    return value.strip("_") or "project"


def render(body: str, vars: dict[str, str]) -> str:
    out = body
    for sentinel, value in vars.items():
        out = out.replace(sentinel, value)
    unresolved = re.findall(r"__[A-Z][A-Z0-9_]*__", out)
    expected = {"__main__", "__init__", "__name__", "__file__"}
    real = [s for s in unresolved if s not in expected]
    if real:
        raise RuntimeError(f"unresolved sentinels: {set(real)}")
    return out


def write(path: Path, content: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")


def backup(path: Path) -> Path:
    stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    dest = path.with_suffix(path.suffix + f".bak.{stamp}")
    shutil.copy2(path, dest)
    return dest


def main() -> int:
    parser = argparse.ArgumentParser(description="Bootstrap a compile-first knowledge system into another repo.")
    parser.add_argument("target_dir", help="Target repository root")
    parser.add_argument("project_name", help="Human-readable project name")
    parser.add_argument("--force", action="store_true", help="Overwrite existing files (a .bak.<timestamp> copy is kept unless --no-backup)")
    parser.add_argument("--no-backup", action="store_true", help="When overwriting, skip the .bak file (only effective with --force)")
    parser.add_argument("--dry-run", action="store_true", help="Show what would happen without writing anything")
    parser.add_argument("--raw-root-name", help="Folder name for the local raw root")
    args = parser.parse_args()

    target = Path(args.target_dir).expanduser().resolve()
    project_name = args.project_name.strip()
    slug = slugify(project_name)
    raw_root_name = args.raw_root_name or f"{slug}_raw"
    today = date.today().isoformat()

    vars = {
        "__PROJECT_NAME__": project_name,
        "__RAW_ROOT_NAME__": raw_root_name,
        "__TODAY__": today,
    }

    created: list[Path] = []
    overwritten: list[tuple[Path, Path | None]] = []
    skipped: list[Path] = []
    unchanged: list[Path] = []

    for tmpl_rel, target_rel in TEMPLATE_TO_TARGET:
        # v2.0.0: scripts/ under SKILL_ROOT/scripts/ (not templates/scripts/)
        if tmpl_rel.startswith("scripts/"):
            src = SKILL_ROOT / tmpl_rel
        else:
            src = TEMPLATES / tmpl_rel
        if not src.exists():
            print(f"WARNING: template missing: {tmpl_rel}")
            continue
        body = src.read_text(encoding="utf-8")
        try:
            content = render(body, vars)
        except RuntimeError as exc:
            raise SystemExit(f"template {tmpl_rel}: {exc}")

        dest = target / target_rel
        if not dest.exists():
            if not args.dry_run:
                write(dest, content)
            created.append(dest)
        elif dest.read_text(encoding="utf-8") == content:
            unchanged.append(dest)
        elif args.force:
            bak_path: Path | None = None
            if not args.dry_run:
                if not args.no_backup:
                    bak_path = backup(dest)
                write(dest, content)
            overwritten.append((dest, bak_path))
        else:
            skipped.append(dest)

    prefix = "[DRY RUN] " if args.dry_run else ""
    print(f"{prefix}Bootstrapped knowledge system for: {project_name}")
    print(f"Target repo: {target}")
    print(f"Default raw root name: {raw_root_name}")

    if created:
        print(f"\nCreated ({len(created)}):")
        for p in created:
            print(f"  + {p.relative_to(target)}")
    if overwritten:
        print(f"\nOverwritten ({len(overwritten)}):")
        for p, bak in overwritten:
            suffix = f"  (backup: {bak.name})" if bak else "  (no backup)"
            print(f"  ! {p.relative_to(target)}{suffix}")
    if unchanged:
        print(f"\nUnchanged ({len(unchanged)})")
    if skipped:
        print(f"\nSkipped — already exists, content differs ({len(skipped)}):")
        print("Use --force to overwrite.")
        for p in skipped:
            print(f"  ~ {p.relative_to(target)}")

    if not args.dry_run and (created or overwritten):
        print("\nNext steps:")
        print(f"1. cd {target}")
        print("2. python3 scripts/init_raw_root.py")
        print("3. python3 scripts/wiki_check.py")
        print("4. python3 scripts/raw_manifest_check.py")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())