@kennethsolomon/shipkit 1.0.0

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.
Files changed (117) hide show
  1. package/README.md +321 -0
  2. package/bin/shipkit.js +146 -0
  3. package/commands/sk/brainstorm.md +63 -0
  4. package/commands/sk/branch.md +35 -0
  5. package/commands/sk/config.md +96 -0
  6. package/commands/sk/execute-plan.md +85 -0
  7. package/commands/sk/features.md +238 -0
  8. package/commands/sk/finish-feature.md +154 -0
  9. package/commands/sk/help.md +103 -0
  10. package/commands/sk/hotfix.md +61 -0
  11. package/commands/sk/plan.md +30 -0
  12. package/commands/sk/release.md +72 -0
  13. package/commands/sk/security-check.md +188 -0
  14. package/commands/sk/set-profile.md +71 -0
  15. package/commands/sk/status.md +25 -0
  16. package/commands/sk/update-task.md +35 -0
  17. package/commands/sk/write-plan.md +72 -0
  18. package/package.json +23 -0
  19. package/skills/sk:accessibility/LICENSE.txt +177 -0
  20. package/skills/sk:accessibility/SKILL.md +150 -0
  21. package/skills/sk:api-design/LICENSE.txt +177 -0
  22. package/skills/sk:api-design/SKILL.md +158 -0
  23. package/skills/sk:brainstorming/SKILL.md +124 -0
  24. package/skills/sk:debug/SKILL.md +252 -0
  25. package/skills/sk:debug/debug_conductor.py +177 -0
  26. package/skills/sk:debug/lib/__init__.py +1 -0
  27. package/skills/sk:debug/lib/bug_gatherer.py +55 -0
  28. package/skills/sk:debug/lib/context_reader.py +139 -0
  29. package/skills/sk:debug/lib/findings_writer.py +76 -0
  30. package/skills/sk:debug/lib/lessons_writer.py +165 -0
  31. package/skills/sk:debug/lib/step_runner.py +326 -0
  32. package/skills/sk:features/SKILL.md +238 -0
  33. package/skills/sk:frontend-design/LICENSE.txt +177 -0
  34. package/skills/sk:frontend-design/SKILL.md +191 -0
  35. package/skills/sk:laravel-init/SKILL.md +37 -0
  36. package/skills/sk:laravel-new/SKILL.md +68 -0
  37. package/skills/sk:lint/SKILL.md +113 -0
  38. package/skills/sk:perf/LICENSE.txt +177 -0
  39. package/skills/sk:perf/SKILL.md +188 -0
  40. package/skills/sk:release/SKILL.md +113 -0
  41. package/skills/sk:release/references/android-checklist.md +269 -0
  42. package/skills/sk:release/references/ios-checklist.md +339 -0
  43. package/skills/sk:release/release.sh +378 -0
  44. package/skills/sk:review/SKILL.md +346 -0
  45. package/skills/sk:review/references/security-checklist.md +223 -0
  46. package/skills/sk:schema-migrate/SKILL.md +125 -0
  47. package/skills/sk:schema-migrate/orms/drizzle.md +546 -0
  48. package/skills/sk:schema-migrate/orms/laravel.md +367 -0
  49. package/skills/sk:schema-migrate/orms/prisma.md +357 -0
  50. package/skills/sk:schema-migrate/orms/rails.md +351 -0
  51. package/skills/sk:schema-migrate/orms/sqlalchemy.md +385 -0
  52. package/skills/sk:schema-migrate/references/detection.md +110 -0
  53. package/skills/sk:setup-claude/SKILL.md +365 -0
  54. package/skills/sk:setup-claude/references/detection.md +6 -0
  55. package/skills/sk:setup-claude/references/templates.md +11 -0
  56. package/skills/sk:setup-claude/scripts/apply_setup_claude.py +443 -0
  57. package/skills/sk:setup-claude/scripts/detect_arch_changes.py +437 -0
  58. package/skills/sk:setup-claude/templates/.claude/docs/arch-changelog-guide.md.template +6 -0
  59. package/skills/sk:setup-claude/templates/.claude/docs/changelog-guide.md.template +12 -0
  60. package/skills/sk:setup-claude/templates/CHANGELOG.md.template +21 -0
  61. package/skills/sk:setup-claude/templates/CLAUDE.md.template +299 -0
  62. package/skills/sk:setup-claude/templates/arch-changelog-guide.md.template +3 -0
  63. package/skills/sk:setup-claude/templates/changelog-guide.md.template +3 -0
  64. package/skills/sk:setup-claude/templates/commands/brainstorm.md.template +74 -0
  65. package/skills/sk:setup-claude/templates/commands/execute-plan.md.template +57 -0
  66. package/skills/sk:setup-claude/templates/commands/features.md.template +238 -0
  67. package/skills/sk:setup-claude/templates/commands/finish-feature.md.template +155 -0
  68. package/skills/sk:setup-claude/templates/commands/plan.md.template +30 -0
  69. package/skills/sk:setup-claude/templates/commands/re-setup.md.template +38 -0
  70. package/skills/sk:setup-claude/templates/commands/release.md.template +74 -0
  71. package/skills/sk:setup-claude/templates/commands/security-check.md.template +172 -0
  72. package/skills/sk:setup-claude/templates/commands/status.md.template +17 -0
  73. package/skills/sk:setup-claude/templates/commands/write-plan.md.template +34 -0
  74. package/skills/sk:setup-claude/templates/finish-feature.md.template +3 -0
  75. package/skills/sk:setup-claude/templates/plan.md.template +3 -0
  76. package/skills/sk:setup-claude/templates/status.md.template +3 -0
  77. package/skills/sk:setup-claude/templates/tasks/findings.md.template +19 -0
  78. package/skills/sk:setup-claude/templates/tasks/lessons.md.template +26 -0
  79. package/skills/sk:setup-claude/templates/tasks/progress.md.template +20 -0
  80. package/skills/sk:setup-claude/templates/tasks/security-findings.md.template +5 -0
  81. package/skills/sk:setup-claude/templates/tasks/todo.md.template +26 -0
  82. package/skills/sk:setup-claude/templates/tasks/workflow-status.md.template +31 -0
  83. package/skills/sk:setup-claude/templates/tasks-findings.md.template +3 -0
  84. package/skills/sk:setup-claude/templates/tasks-lessons.md.template +3 -0
  85. package/skills/sk:setup-claude/templates/tasks-progress.md.template +3 -0
  86. package/skills/sk:setup-claude/templates/tasks-todo.md.template +3 -0
  87. package/skills/sk:setup-claude/tests/test_apply_setup_claude.py +193 -0
  88. package/skills/sk:setup-optimizer/SKILL.md +184 -0
  89. package/skills/sk:setup-optimizer/lib/__init__.py +24 -0
  90. package/skills/sk:setup-optimizer/lib/detect.py +205 -0
  91. package/skills/sk:setup-optimizer/lib/discover.py +221 -0
  92. package/skills/sk:setup-optimizer/lib/enrich.py +163 -0
  93. package/skills/sk:setup-optimizer/lib/merge.py +277 -0
  94. package/skills/sk:setup-optimizer/lib/sidecar.py +129 -0
  95. package/skills/sk:setup-optimizer/optimize_claude.py +174 -0
  96. package/skills/sk:setup-optimizer/templates/CLAUDE.md.template +105 -0
  97. package/skills/sk:skill-creator/LICENSE.txt +202 -0
  98. package/skills/sk:skill-creator/SKILL.md +479 -0
  99. package/skills/sk:skill-creator/agents/analyzer.md +274 -0
  100. package/skills/sk:skill-creator/agents/comparator.md +202 -0
  101. package/skills/sk:skill-creator/agents/grader.md +223 -0
  102. package/skills/sk:skill-creator/assets/eval_review.html +146 -0
  103. package/skills/sk:skill-creator/eval-viewer/generate_review.py +471 -0
  104. package/skills/sk:skill-creator/eval-viewer/viewer.html +1325 -0
  105. package/skills/sk:skill-creator/references/schemas.md +430 -0
  106. package/skills/sk:skill-creator/scripts/aggregate_benchmark.py +401 -0
  107. package/skills/sk:skill-creator/scripts/generate_report.py +326 -0
  108. package/skills/sk:skill-creator/scripts/improve_description.py +248 -0
  109. package/skills/sk:skill-creator/scripts/package_skill.py +136 -0
  110. package/skills/sk:skill-creator/scripts/quick_validate.py +103 -0
  111. package/skills/sk:skill-creator/scripts/run_eval.py +310 -0
  112. package/skills/sk:skill-creator/scripts/run_loop.py +332 -0
  113. package/skills/sk:skill-creator/scripts/utils.py +47 -0
  114. package/skills/sk:smart-commit/SKILL.md +175 -0
  115. package/skills/sk:test/SKILL.md +171 -0
  116. package/skills/sk:write-tests/SKILL.md +195 -0
  117. package/skills/sk:write-tests/references/patterns.md +209 -0
@@ -0,0 +1,443 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import re
10
+ import sys
11
+ from dataclasses import asdict, dataclass
12
+ from pathlib import Path
13
+ from typing import Dict, Iterable, List, Optional, Tuple
14
+
15
+
16
+ GENERATED_MARKER = "<!-- Generated by /setup-claude -->"
17
+ TEMPLATE_HASH_MARKER = "<!-- Template Hash: "
18
+ TEMPLATE_HASH_END = " -->"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Detection:
23
+ project_name: str
24
+ description: str
25
+ language: str
26
+ framework: str
27
+ database: str
28
+ ui: str
29
+ testing: str
30
+ ai: str
31
+ browser_automation: str
32
+ dev_cmd: str
33
+ build_cmd: str
34
+ lint_cmd: str
35
+ test_cmd: str
36
+ arch_changelog_dir: str
37
+
38
+
39
+ def _read_json(path: Path) -> Optional[dict]:
40
+ try:
41
+ return json.loads(path.read_text(encoding="utf-8"))
42
+ except FileNotFoundError:
43
+ return None
44
+ except Exception:
45
+ return None
46
+
47
+
48
+ def _has_dep(pkg: dict, name: str) -> bool:
49
+ deps = {}
50
+ for key in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
51
+ deps.update(pkg.get(key, {}) or {})
52
+ return name in deps
53
+
54
+
55
+ def _any_dep_prefix(pkg: dict, prefix: str) -> bool:
56
+ deps = {}
57
+ for key in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
58
+ deps.update(pkg.get(key, {}) or {})
59
+ return any(k.startswith(prefix) for k in deps.keys())
60
+
61
+
62
+ def detect(repo_root: Path) -> Detection:
63
+ package_json = _read_json(repo_root / "package.json") or {}
64
+ scripts = (package_json.get("scripts") or {}) if isinstance(package_json, dict) else {}
65
+
66
+ project_name = str(package_json.get("name") or repo_root.name)
67
+ description = str(package_json.get("description") or "Project instructions for Claude Code.")
68
+
69
+ # Language
70
+ if (repo_root / "tsconfig.json").exists() or _has_dep(package_json, "typescript"):
71
+ language = "TypeScript"
72
+ elif (repo_root / "package.json").exists():
73
+ language = "JavaScript"
74
+ elif (repo_root / "pyproject.toml").exists() or (repo_root / "requirements.txt").exists():
75
+ language = "Python"
76
+ elif (repo_root / "go.mod").exists():
77
+ language = "Go"
78
+ elif (repo_root / "Cargo.toml").exists():
79
+ language = "Rust"
80
+ elif (repo_root / "composer.json").exists():
81
+ language = "PHP"
82
+ else:
83
+ language = "Unknown"
84
+
85
+ # Framework (keep simple; expand later)
86
+ if _has_dep(package_json, "next"):
87
+ framework = "Next.js (App Router)"
88
+ elif _has_dep(package_json, "react"):
89
+ framework = "React"
90
+ elif _has_dep(package_json, "express"):
91
+ framework = "Express"
92
+ else:
93
+ framework = "Unknown"
94
+
95
+ # Database / ORM
96
+ if _has_dep(package_json, "drizzle-orm"):
97
+ if _has_dep(package_json, "better-sqlite3"):
98
+ database = "Drizzle ORM + SQLite"
99
+ elif _has_dep(package_json, "pg"):
100
+ database = "Drizzle ORM + PostgreSQL"
101
+ else:
102
+ database = "Drizzle ORM"
103
+ elif _has_dep(package_json, "prisma") or _has_dep(package_json, "@prisma/client"):
104
+ database = "Prisma"
105
+ elif _has_dep(package_json, "@supabase/supabase-js") or (repo_root / "supabase" / "config.toml").exists():
106
+ database = "Supabase (Postgres)"
107
+ else:
108
+ database = "Unknown"
109
+
110
+ # UI
111
+ if _has_dep(package_json, "tailwindcss"):
112
+ if (repo_root / "src" / "components" / "ui").exists():
113
+ ui = "Tailwind CSS + shadcn/ui"
114
+ else:
115
+ ui = "Tailwind CSS"
116
+ elif _has_dep(package_json, "bootstrap"):
117
+ ui = "Bootstrap"
118
+ else:
119
+ ui = "Unknown"
120
+
121
+ # Testing
122
+ if _has_dep(package_json, "vitest"):
123
+ testing = "Vitest"
124
+ elif _has_dep(package_json, "jest"):
125
+ testing = "Jest"
126
+ elif _has_dep(package_json, "@playwright/test") or _has_dep(package_json, "playwright"):
127
+ testing = "Playwright"
128
+ else:
129
+ testing = "Unknown"
130
+
131
+ # AI
132
+ if _has_dep(package_json, "openai"):
133
+ ai = "OpenAI-compatible"
134
+ elif _has_dep(package_json, "@anthropic-ai/sdk"):
135
+ ai = "Anthropic"
136
+ else:
137
+ ai = "Unknown"
138
+
139
+ # Browser automation
140
+ if _has_dep(package_json, "@playwright/test") or _has_dep(package_json, "playwright"):
141
+ browser_automation = "Playwright"
142
+ elif _has_dep(package_json, "puppeteer"):
143
+ browser_automation = "Puppeteer"
144
+ else:
145
+ browser_automation = "None"
146
+
147
+ dev_cmd = scripts.get("dev") and "npm run dev" or "npm run dev"
148
+ build_cmd = scripts.get("build") and "npm run build" or "npm run build"
149
+ lint_cmd = scripts.get("lint") and "npm run lint" or "npm run lint"
150
+ test_cmd = scripts.get("test") and "npm test" or "npm test"
151
+
152
+ typo_dir = repo_root / ".claude" / "docs" / "achritectural_change_log"
153
+ correct_dir = repo_root / ".claude" / "docs" / "architectural_change_log"
154
+ if typo_dir.exists():
155
+ arch_dir = ".claude/docs/achritectural_change_log/"
156
+ else:
157
+ arch_dir = ".claude/docs/architectural_change_log/"
158
+
159
+ return Detection(
160
+ project_name=project_name,
161
+ description=description,
162
+ language=language,
163
+ framework=framework,
164
+ database=database,
165
+ ui=ui,
166
+ testing=testing,
167
+ ai=ai,
168
+ browser_automation=browser_automation,
169
+ dev_cmd=dev_cmd,
170
+ build_cmd=build_cmd,
171
+ lint_cmd=lint_cmd,
172
+ test_cmd=test_cmd,
173
+ arch_changelog_dir=arch_dir,
174
+ )
175
+
176
+
177
+ def _get_template_hash(template_path: Path) -> str:
178
+ """Calculate SHA256 hash of template file."""
179
+ try:
180
+ content = template_path.read_text(encoding="utf-8")
181
+ return hashlib.sha256(content.encode()).hexdigest()[:12]
182
+ except Exception:
183
+ return "unknown"
184
+
185
+
186
+ def _extract_template_hash(content: str) -> Optional[str]:
187
+ """Extract template hash from generated file."""
188
+ match = re.search(rf"{re.escape(TEMPLATE_HASH_MARKER)}([a-f0-9]{{12}}){re.escape(TEMPLATE_HASH_END)}", content)
189
+ return match.group(1) if match else None
190
+
191
+
192
+ def render_template(text: str, detection: Detection, template_path: Optional[Path] = None) -> str:
193
+ replacements = {
194
+ "[PROJECT_NAME]": detection.project_name,
195
+ "[ONE_SENTENCE_DESCRIPTION]": detection.description,
196
+ "[LANGUAGE]": detection.language,
197
+ "[FRAMEWORK]": detection.framework,
198
+ "[DATABASE]": detection.database,
199
+ "[UI]": detection.ui,
200
+ "[TESTING]": detection.testing,
201
+ "[AI]": detection.ai,
202
+ "[BROWSER_AUTOMATION]": detection.browser_automation,
203
+ "[DEV_COMMAND]": detection.dev_cmd,
204
+ "[BUILD_COMMAND]": detection.build_cmd,
205
+ "[LINT_COMMAND]": detection.lint_cmd,
206
+ "[TEST_COMMAND]": detection.test_cmd,
207
+ "[ARCH_CHANGELOG_DIR]": detection.arch_changelog_dir,
208
+ }
209
+ for k, v in replacements.items():
210
+ text = text.replace(k, v)
211
+
212
+ # Add template hash for future update detection
213
+ if template_path:
214
+ template_hash = _get_template_hash(template_path)
215
+ # Insert hash after the first line (GENERATED_MARKER)
216
+ lines = text.split('\n')
217
+ if lines and GENERATED_MARKER in lines[0]:
218
+ lines.insert(1, f"{TEMPLATE_HASH_MARKER}{template_hash}{TEMPLATE_HASH_END}")
219
+ text = '\n'.join(lines)
220
+
221
+ return text
222
+
223
+
224
+ def _write_file_if_missing(dest: Path, content: str) -> Tuple[str, Path]:
225
+ if dest.exists():
226
+ return ("skipped", dest)
227
+ dest.parent.mkdir(parents=True, exist_ok=True)
228
+ dest.write_text(content, encoding="utf-8")
229
+ return ("created", dest)
230
+
231
+
232
+ def _write_file_if_generated(dest: Path, content: str, template_path: Optional[Path] = None) -> Tuple[str, Path]:
233
+ if not dest.exists():
234
+ dest.parent.mkdir(parents=True, exist_ok=True)
235
+ dest.write_text(content, encoding="utf-8")
236
+ return ("created", dest)
237
+ try:
238
+ existing = dest.read_text(encoding="utf-8")
239
+ except Exception:
240
+ return ("skipped", dest)
241
+ if GENERATED_MARKER in existing:
242
+ # Check if content is identical (fast path)
243
+ if existing == content:
244
+ return ("skipped", dest)
245
+ # Check if template has changed (slow path)
246
+ if template_path:
247
+ old_hash = _extract_template_hash(existing)
248
+ new_hash = _get_template_hash(template_path)
249
+ if old_hash == new_hash:
250
+ # Template hasn't changed, but rendered content differs
251
+ # This shouldn't happen; keep existing file
252
+ return ("skipped", dest)
253
+ # Template has changed or hash check failed, update
254
+ dest.write_text(content, encoding="utf-8")
255
+ return ("updated", dest)
256
+ return ("skipped", dest)
257
+
258
+
259
+ def _plan_file_if_missing(dest: Path) -> str:
260
+ return "skipped" if dest.exists() else "created"
261
+
262
+
263
+ def _plan_file_if_generated(dest: Path, content: str) -> str:
264
+ if not dest.exists():
265
+ return "created"
266
+ try:
267
+ existing = dest.read_text(encoding="utf-8")
268
+ except Exception:
269
+ return "skipped"
270
+ if GENERATED_MARKER not in existing:
271
+ return "skipped"
272
+ return "skipped" if existing == content else "updated"
273
+
274
+
275
+ def apply(
276
+ repo_root: Path,
277
+ skill_root: Path,
278
+ *,
279
+ update_generated: bool,
280
+ dry_run: bool,
281
+ detection: Optional[Detection] = None,
282
+ ) -> int:
283
+ detection = detection or detect(repo_root)
284
+
285
+ arch_dir_path = repo_root / detection.arch_changelog_dir.rstrip("/")
286
+ if not dry_run:
287
+ # Ensure arch dir exists if we selected the correct spelling.
288
+ arch_dir_path.mkdir(parents=True, exist_ok=True)
289
+
290
+ mapping: List[Tuple[Path, Path, str]] = []
291
+ notes: List[str] = []
292
+
293
+ def add(template_rel: str, dest_rel: str, mode: str) -> None:
294
+ mapping.append((skill_root / template_rel, repo_root / dest_rel, mode))
295
+
296
+ # tasks (create-only)
297
+ add("templates/tasks/todo.md.template", "tasks/todo.md", "missing")
298
+ add("templates/tasks/findings.md.template", "tasks/findings.md", "missing")
299
+ add("templates/tasks/progress.md.template", "tasks/progress.md", "missing")
300
+ add("templates/tasks/lessons.md.template", "tasks/lessons.md", "missing")
301
+ add("templates/tasks/security-findings.md.template", "tasks/security-findings.md", "missing")
302
+ add("templates/tasks/workflow-status.md.template", "tasks/workflow-status.md", "missing")
303
+
304
+ # commands (update if generated)
305
+ add("templates/commands/brainstorm.md.template", ".claude/commands/brainstorm.md", "generated")
306
+ add("templates/commands/write-plan.md.template", ".claude/commands/write-plan.md", "generated")
307
+ add("templates/commands/execute-plan.md.template", ".claude/commands/execute-plan.md", "generated")
308
+ add("templates/commands/plan.md.template", ".claude/commands/plan.md", "generated")
309
+ add("templates/commands/status.md.template", ".claude/commands/status.md", "generated")
310
+ add("templates/commands/finish-feature.md.template", ".claude/commands/finish-feature.md", "generated")
311
+ add("templates/commands/re-setup.md.template", ".claude/commands/re-setup.md", "generated")
312
+ add("templates/commands/release.md.template", ".claude/commands/release.md", "generated")
313
+ add("templates/commands/features.md.template", ".claude/commands/features.md", "generated")
314
+ add("templates/commands/security-check.md.template", ".claude/commands/security-check.md", "generated")
315
+
316
+ # docs (generated-update)
317
+ add("templates/.claude/docs/changelog-guide.md.template", ".claude/docs/changelog-guide.md", "generated")
318
+ add("templates/.claude/docs/arch-changelog-guide.md.template", ".claude/docs/arch-changelog-guide.md", "generated")
319
+
320
+ # root files
321
+ claude_md_path = repo_root / "CLAUDE.md"
322
+ sidecar_claude_md_path = repo_root / "CLAUDE.setup-claude.md"
323
+ use_sidecar_claude_md = False
324
+ if claude_md_path.exists():
325
+ try:
326
+ existing = claude_md_path.read_text(encoding="utf-8")
327
+ use_sidecar_claude_md = GENERATED_MARKER not in existing
328
+ except Exception:
329
+ use_sidecar_claude_md = True
330
+ if use_sidecar_claude_md:
331
+ notes.append("Found existing custom CLAUDE.md; writing CLAUDE.setup-claude.md instead.")
332
+
333
+ if use_sidecar_claude_md:
334
+ add("templates/CLAUDE.md.template", "CLAUDE.setup-claude.md", "generated")
335
+ else:
336
+ add("templates/CLAUDE.md.template", "CLAUDE.md", "generated")
337
+ add("templates/CHANGELOG.md.template", "CHANGELOG.md", "missing")
338
+
339
+ created: List[str] = []
340
+ updated: List[str] = []
341
+ skipped: List[str] = []
342
+
343
+ arch_dir_rel = str(arch_dir_path.relative_to(repo_root))
344
+ if dry_run:
345
+ if arch_dir_path.exists():
346
+ skipped.append(arch_dir_rel)
347
+ else:
348
+ created.append(arch_dir_rel)
349
+
350
+ for template_path, dest_path, mode in mapping:
351
+ try:
352
+ template_text = template_path.read_text(encoding="utf-8")
353
+ except FileNotFoundError:
354
+ skipped.append(str(dest_path))
355
+ continue
356
+ rendered = render_template(template_text, detection, template_path if mode == "generated" else None)
357
+
358
+ if mode == "missing":
359
+ if dry_run:
360
+ action = _plan_file_if_missing(dest_path)
361
+ p = dest_path
362
+ else:
363
+ action, p = _write_file_if_missing(dest_path, rendered)
364
+ elif mode == "generated":
365
+ if dry_run:
366
+ action = _plan_file_if_generated(dest_path, rendered)
367
+ p = dest_path
368
+ else:
369
+ action, p = _write_file_if_generated(dest_path, rendered, template_path)
370
+ else:
371
+ action, p = ("skipped", dest_path)
372
+
373
+ if action == "created":
374
+ created.append(str(p.relative_to(repo_root)))
375
+ elif action == "updated":
376
+ updated.append(str(p.relative_to(repo_root)))
377
+ else:
378
+ skipped.append(str(p.relative_to(repo_root)))
379
+
380
+ if dry_run:
381
+ print("setup-claude dry-run complete (no files written)")
382
+ else:
383
+ print("setup-claude apply complete")
384
+ if notes:
385
+ print("\nNotes:")
386
+ for n in notes:
387
+ print(f" - {n}")
388
+ if created:
389
+ print("\nCreated:")
390
+ for p in created:
391
+ print(f" - {p}")
392
+ if updated:
393
+ print("\nUpdated:")
394
+ for p in updated:
395
+ print(f" - {p}")
396
+ if skipped:
397
+ print("\nSkipped:")
398
+ for p in skipped:
399
+ print(f" - {p}")
400
+
401
+ return 0
402
+
403
+
404
+ def main(argv: List[str]) -> int:
405
+ parser = argparse.ArgumentParser(prog="apply_setup_claude.py")
406
+ parser.add_argument("repo_root", nargs="?", default=str(Path.cwd()), help="Target repository root (default: cwd)")
407
+ parser.add_argument(
408
+ "--update-generated",
409
+ action="store_true",
410
+ help="Update files previously generated by /setup-claude (marker-guarded).",
411
+ )
412
+ parser.add_argument("--dry-run", action="store_true", help="Preview changes without writing any files.")
413
+ parser.add_argument(
414
+ "--print-detection",
415
+ action="store_true",
416
+ help="Print detected values as JSON and exit unless combined with --dry-run.",
417
+ )
418
+ args = parser.parse_args(argv[1:])
419
+
420
+ repo_root = Path(args.repo_root).resolve()
421
+ skill_root = Path(__file__).resolve().parents[1]
422
+
423
+ if not repo_root.exists():
424
+ print(f"Repo root not found: {repo_root}", file=sys.stderr)
425
+ return 2
426
+
427
+ detection = detect(repo_root)
428
+ if args.print_detection:
429
+ print(json.dumps(asdict(detection), indent=2, sort_keys=True))
430
+ if not args.dry_run:
431
+ return 0
432
+
433
+ return apply(
434
+ repo_root,
435
+ skill_root,
436
+ update_generated=args.update_generated,
437
+ dry_run=args.dry_run,
438
+ detection=detection,
439
+ )
440
+
441
+
442
+ if __name__ == "__main__":
443
+ raise SystemExit(main(sys.argv))