Initial commit: Flutter 无书应用项目
This commit is contained in:
294
.trae/skills/planning-with-files/scripts/sync-ide-folders.py
Normal file
294
.trae/skills/planning-with-files/scripts/sync-ide-folders.py
Normal file
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
sync-ide-folders.py — Syncs shared files from the canonical source
|
||||
(skills/planning-with-files/) to all IDE-specific folders.
|
||||
|
||||
Run this from the repo root before releases:
|
||||
python scripts/sync-ide-folders.py
|
||||
|
||||
What it syncs:
|
||||
- Templates (findings.md, progress.md, task_plan.md)
|
||||
- References (examples.md, reference.md)
|
||||
- Scripts (check-complete.sh/.ps1, init-session.sh/.ps1, session-catchup.py)
|
||||
|
||||
What it NEVER touches:
|
||||
- SKILL.md (IDE-specific frontmatter differs per IDE)
|
||||
- IDE-specific files (hooks, prompts, package.json, steering files)
|
||||
|
||||
Use --dry-run to preview changes without writing anything.
|
||||
Use --verify to check for drift without making changes (exits 1 if drift found).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import sys
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
# ─── Canonical source ──────────────────────────────────────────────
|
||||
CANONICAL = Path("skills/planning-with-files")
|
||||
|
||||
# ─── Shared source files (relative to CANONICAL) ──────────────────
|
||||
TEMPLATES = [
|
||||
"templates/findings.md",
|
||||
"templates/progress.md",
|
||||
"templates/task_plan.md",
|
||||
]
|
||||
|
||||
REFERENCES = [
|
||||
"examples.md",
|
||||
"reference.md",
|
||||
]
|
||||
|
||||
SCRIPTS = [
|
||||
"scripts/check-complete.sh",
|
||||
"scripts/check-complete.ps1",
|
||||
"scripts/init-session.sh",
|
||||
"scripts/init-session.ps1",
|
||||
"scripts/session-catchup.py",
|
||||
]
|
||||
|
||||
# ─── IDE sync manifests ───────────────────────────────────────────
|
||||
# Each IDE maps: canonical_source_file -> target_path (relative to repo root)
|
||||
# Only files listed here are synced. Everything else is untouched.
|
||||
|
||||
def _build_manifest(base, *, ref_style="flat", template_dirs=None,
|
||||
include_scripts=True, extra_template_dirs=None):
|
||||
"""Build a sync manifest for an IDE folder.
|
||||
|
||||
Args:
|
||||
base: IDE skill folder path (e.g. ".gemini/skills/planning-with-files")
|
||||
ref_style: "flat" = examples.md at root, "subdir" = references/examples.md
|
||||
template_dirs: list of template subdirs (default: ["templates/"])
|
||||
include_scripts: whether to sync scripts
|
||||
extra_template_dirs: additional dirs to also receive template copies
|
||||
"""
|
||||
manifest = {}
|
||||
b = Path(base)
|
||||
|
||||
# Templates
|
||||
if template_dirs is None:
|
||||
template_dirs = ["templates/"]
|
||||
for tdir in template_dirs:
|
||||
for t in TEMPLATES:
|
||||
filename = Path(t).name # e.g. "findings.md"
|
||||
manifest[t] = str(b / tdir / filename)
|
||||
|
||||
# Extra template locations (e.g. assets/templates/ in codex, codebuddy)
|
||||
if extra_template_dirs:
|
||||
for tdir in extra_template_dirs:
|
||||
for t in TEMPLATES:
|
||||
filename = Path(t).name
|
||||
manifest[f"{t}__extra_{tdir}"] = str(b / tdir / filename)
|
||||
|
||||
# References
|
||||
if ref_style == "flat":
|
||||
for r in REFERENCES:
|
||||
manifest[r] = str(b / r)
|
||||
elif ref_style == "subdir":
|
||||
for r in REFERENCES:
|
||||
manifest[r] = str(b / "references" / r)
|
||||
# ref_style == "skip" means don't sync references (IDE uses custom format)
|
||||
|
||||
# Scripts
|
||||
if include_scripts:
|
||||
for s in SCRIPTS:
|
||||
manifest[s] = str(b / s)
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
IDE_MANIFESTS = {
|
||||
".cursor": _build_manifest(
|
||||
".cursor/skills/planning-with-files",
|
||||
ref_style="flat",
|
||||
include_scripts=False,
|
||||
# Cursor hooks are IDE-specific, not synced
|
||||
),
|
||||
|
||||
".gemini": _build_manifest(
|
||||
".gemini/skills/planning-with-files",
|
||||
ref_style="subdir",
|
||||
include_scripts=True,
|
||||
),
|
||||
|
||||
".codex": _build_manifest(
|
||||
".codex/skills/planning-with-files",
|
||||
ref_style="subdir",
|
||||
include_scripts=True,
|
||||
),
|
||||
|
||||
# .openclaw, .kilocode, .adal, .agent removed in v2.24.0 (IDE audit)
|
||||
# These IDEs use the standard Agent Skills spec — install via npx skills add
|
||||
|
||||
".pi": _build_manifest(
|
||||
".pi/skills/planning-with-files",
|
||||
ref_style="flat",
|
||||
include_scripts=True,
|
||||
# package.json and README.md are IDE-specific, not synced
|
||||
),
|
||||
|
||||
".continue": _build_manifest(
|
||||
".continue/skills/planning-with-files",
|
||||
ref_style="flat",
|
||||
template_dirs=[], # Continue has no templates dir
|
||||
include_scripts=True,
|
||||
# .continue/prompts/ is IDE-specific, not synced
|
||||
),
|
||||
|
||||
".codebuddy": _build_manifest(
|
||||
".codebuddy/skills/planning-with-files",
|
||||
ref_style="subdir",
|
||||
include_scripts=True,
|
||||
),
|
||||
|
||||
".factory": _build_manifest(
|
||||
".factory/skills/planning-with-files",
|
||||
ref_style="skip", # Uses combined references.md, not synced
|
||||
include_scripts=True,
|
||||
),
|
||||
|
||||
".opencode": _build_manifest(
|
||||
".opencode/skills/planning-with-files",
|
||||
ref_style="flat",
|
||||
include_scripts=False,
|
||||
),
|
||||
|
||||
# Kiro: maintained under .kiro/ (skill + wrappers); not synced from canonical scripts/.
|
||||
".kiro": {},
|
||||
}
|
||||
|
||||
|
||||
# ─── Utility functions ─────────────────────────────────────────────
|
||||
|
||||
def file_hash(path):
|
||||
"""Return SHA-256 hash of a file, or None if it doesn't exist."""
|
||||
try:
|
||||
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def sync_file(src, dst, *, dry_run=False):
|
||||
"""Copy src to dst. Returns (action, detail) tuple.
|
||||
|
||||
Actions: "updated", "created", "skipped" (already identical), "missing_src"
|
||||
"""
|
||||
if not src.exists():
|
||||
return "missing_src", f"Canonical file not found: {src}"
|
||||
|
||||
src_hash = file_hash(src)
|
||||
dst_hash = file_hash(dst)
|
||||
|
||||
if src_hash == dst_hash:
|
||||
return "skipped", "Already up to date"
|
||||
|
||||
action = "created" if dst_hash is None else "updated"
|
||||
|
||||
if not dry_run:
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
return action, f"{'Would ' if dry_run else ''}{action}: {dst}"
|
||||
|
||||
|
||||
# ─── Main ──────────────────────────────────────────────────────────
|
||||
|
||||
def parse_args(argv=None):
|
||||
"""Parse CLI arguments for sync behavior."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Sync shared planning-with-files assets from canonical source "
|
||||
"to IDE-specific folders."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Preview changes without writing files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verify",
|
||||
action="store_true",
|
||||
help="Check for drift only; exit with code 1 if drift is found.",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = parse_args(argv)
|
||||
dry_run = args.dry_run
|
||||
verify = args.verify
|
||||
|
||||
# Must run from repo root
|
||||
if not CANONICAL.exists():
|
||||
print(f"Error: Canonical source not found at {CANONICAL}/")
|
||||
print("Run this script from the repo root.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"{'[DRY RUN] ' if dry_run else ''}{'[VERIFY] ' if verify else ''}"
|
||||
f"Syncing from {CANONICAL}/\n")
|
||||
|
||||
stats = {"updated": 0, "created": 0, "skipped": 0, "missing_src": 0, "drift": 0}
|
||||
|
||||
for ide_name, manifest in sorted(IDE_MANIFESTS.items()):
|
||||
# Skip IDEs whose base directory doesn't exist
|
||||
ide_root = Path(ide_name)
|
||||
if not ide_root.exists():
|
||||
continue
|
||||
|
||||
print(f" {ide_name}/")
|
||||
ide_changes = 0
|
||||
|
||||
for canonical_key, target_path in sorted(manifest.items()):
|
||||
# Handle __extra_ keys (canonical key contains __extra_ suffix)
|
||||
canonical_rel = canonical_key.split("__extra_")[0]
|
||||
src = CANONICAL / canonical_rel
|
||||
dst = Path(target_path)
|
||||
|
||||
if verify:
|
||||
# Verify mode: just check for drift
|
||||
src_hash = file_hash(src)
|
||||
dst_hash = file_hash(dst)
|
||||
if src_hash and dst_hash and src_hash != dst_hash:
|
||||
print(f" DRIFT: {dst}")
|
||||
stats["drift"] += 1
|
||||
ide_changes += 1
|
||||
elif src_hash and not dst_hash:
|
||||
print(f" MISSING: {dst}")
|
||||
stats["drift"] += 1
|
||||
ide_changes += 1
|
||||
else:
|
||||
action, detail = sync_file(src, dst, dry_run=dry_run)
|
||||
stats[action] += 1
|
||||
if action in ("updated", "created"):
|
||||
print(f" {action.upper()}: {dst}")
|
||||
ide_changes += 1
|
||||
|
||||
if ide_changes == 0:
|
||||
print(" (up to date)")
|
||||
|
||||
# Summary
|
||||
print(f"\n{'-' * 50}")
|
||||
if verify:
|
||||
total_drift = stats["drift"]
|
||||
if total_drift > 0:
|
||||
print(f"DRIFT DETECTED: {total_drift} file(s) out of sync.")
|
||||
print("Run 'python scripts/sync-ide-folders.py' to fix.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("All IDE folders are in sync.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f" Updated: {stats['updated']}")
|
||||
print(f" Created: {stats['created']}")
|
||||
print(f" Skipped: {stats['skipped']} (already up to date)")
|
||||
if stats["missing_src"] > 0:
|
||||
print(f" Missing: {stats['missing_src']} (canonical source not found)")
|
||||
if dry_run:
|
||||
print("\n This was a dry run. No files were modified.")
|
||||
print(" Run without --dry-run to apply changes.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user