its-magic 0.1.2-9 → 0.1.3-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 (237) hide show
  1. package/README.md +1602 -755
  2. package/bin/its-magic.js +121 -11
  3. package/bin/postinstall.js +93 -0
  4. package/installer.ps1 +759 -0
  5. package/installer.py +1036 -0
  6. package/installer.sh +649 -0
  7. package/package.json +18 -14
  8. package/scripts/check_intake_template_parity.py +312 -0
  9. package/scripts/doc_profile_lib.py +415 -0
  10. package/scripts/guard_installer_publish.py +88 -0
  11. package/scripts/intake_bug_routing_guard.py +67 -0
  12. package/scripts/intake_evidence_lib.py +802 -0
  13. package/scripts/intake_evidence_validate.py +73 -0
  14. package/scripts/materialize_codebase_map.py +184 -0
  15. package/scripts/remote_config_summary.py +243 -0
  16. package/template/.cursor/agents/curator.mdc +35 -0
  17. package/template/.cursor/agents/dev.mdc +29 -0
  18. package/template/.cursor/agents/po.mdc +141 -0
  19. package/template/.cursor/agents/qa.mdc +28 -0
  20. package/template/.cursor/agents/release.mdc +28 -0
  21. package/template/.cursor/agents/security.mdc +98 -0
  22. package/template/.cursor/agents/tech-lead.mdc +57 -0
  23. package/template/.cursor/commands/architecture.md +127 -0
  24. package/template/.cursor/commands/ask.md +55 -0
  25. package/template/.cursor/commands/auto.md +533 -0
  26. package/template/.cursor/commands/discovery.md +47 -0
  27. package/template/.cursor/commands/execute.md +343 -0
  28. package/template/.cursor/commands/intake.md +323 -0
  29. package/template/.cursor/commands/map-codebase.md +46 -0
  30. package/template/.cursor/commands/memory-audit.md +175 -0
  31. package/template/.cursor/commands/milestone-complete.md +51 -0
  32. package/template/.cursor/commands/milestone-start.md +59 -0
  33. package/template/.cursor/commands/pause.md +64 -0
  34. package/{.cursor/commands/gsd-phase-context.md → template/.cursor/commands/phase-context.md} +6 -2
  35. package/template/.cursor/commands/plan-verify.md +34 -0
  36. package/template/.cursor/commands/qa.md +226 -0
  37. package/template/.cursor/commands/quick.md +41 -0
  38. package/template/.cursor/commands/refresh-context.md +82 -0
  39. package/template/.cursor/commands/release.md +523 -0
  40. package/template/.cursor/commands/research.md +49 -0
  41. package/template/.cursor/commands/resume.md +67 -0
  42. package/template/.cursor/commands/security-review.md +81 -0
  43. package/template/.cursor/commands/sprint-plan.md +103 -0
  44. package/template/.cursor/commands/status-reconcile.md +95 -0
  45. package/template/.cursor/commands/verify-work.md +152 -0
  46. package/template/.cursor/dev-environment.json.example +22 -0
  47. package/{.cursor → template/.cursor}/hooks/README.md +3 -2
  48. package/{.cursor/hooks/gsd-hook.py → template/.cursor/hooks/hook.py} +15 -6
  49. package/template/.cursor/hooks.json +26 -0
  50. package/template/.cursor/remote.json +31 -0
  51. package/template/.cursor/rules/caveman.mdc +184 -0
  52. package/template/.cursor/rules/coding-standards.mdc +39 -0
  53. package/template/.cursor/rules/core.mdc +111 -0
  54. package/template/.cursor/rules/handoffs.mdc +27 -0
  55. package/template/.cursor/rules/quality.mdc +49 -0
  56. package/template/.cursor/scratchpad.local.example.md +323 -0
  57. package/template/.cursor/scratchpad.md +324 -0
  58. package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/SKILL.md +2 -2
  59. package/template/.cursorignore +6 -0
  60. package/template/.env.example +28 -0
  61. package/template/.github/workflows/ci.yml +194 -0
  62. package/{.github → template/.github}/workflows/deploy.yml +5 -2
  63. package/template/.its-magic-version +1 -0
  64. package/template/CHANGELOG.md +11 -0
  65. package/template/README.md +1602 -0
  66. package/template/decisions/DEC-0001.md +11 -0
  67. package/template/decisions/DEC-0002.md +11 -0
  68. package/template/docs/developer/README.md +44 -0
  69. package/template/docs/engineering/architecture.md +9 -0
  70. package/template/docs/engineering/artifact-ordering-policy.md +48 -0
  71. package/template/docs/engineering/artifact-ownership-policy.md +57 -0
  72. package/template/docs/engineering/auto-orchestration-reference.md +1211 -0
  73. package/{docs → template/docs}/engineering/codebase-map.md +0 -5
  74. package/template/docs/engineering/compatibility-report.md +20 -0
  75. package/template/docs/engineering/compatibility-signals.md +7 -0
  76. package/template/docs/engineering/component-scope-report.md +16 -0
  77. package/template/docs/engineering/component-scope.md +19 -0
  78. package/template/docs/engineering/context/installer-owned-paths.manifest +96 -0
  79. package/template/docs/engineering/context/readme-section-affinity.json +30 -0
  80. package/template/docs/engineering/decisions.md +16 -0
  81. package/template/docs/engineering/legacy-drift-audit.md +21 -0
  82. package/template/docs/engineering/manifests/components/api-gateway.manifest.yaml +12 -0
  83. package/template/docs/engineering/manifests/registry.manifest.yaml +20 -0
  84. package/template/docs/engineering/manifests/repo.manifest.yaml +11 -0
  85. package/template/docs/engineering/phase-context.md +16 -0
  86. package/template/docs/engineering/release-targets.json +123 -0
  87. package/template/docs/engineering/research.md +29 -0
  88. package/template/docs/engineering/runbook.md +2391 -0
  89. package/template/docs/engineering/runtime-connectivity.md +81 -0
  90. package/template/docs/engineering/security-review.md +33 -0
  91. package/template/docs/engineering/spec-pack/README.md +20 -0
  92. package/template/docs/engineering/state-archive/README.md +15 -0
  93. package/template/docs/engineering/state.md +20 -0
  94. package/template/docs/engineering/status-normalization-report.md +19 -0
  95. package/template/docs/engineering/token-cost-parity-manifest.md +16 -0
  96. package/template/docs/engineering/us-0084-remote-e2e.md +44 -0
  97. package/template/docs/product/acceptance.md +1 -0
  98. package/template/docs/product/backlog.md +5 -0
  99. package/template/docs/product/vision.md +11 -0
  100. package/template/docs/user-guides/README.md +40 -0
  101. package/{handoffs → template/handoffs}/dev_to_qa.md +0 -3
  102. package/{handoffs → template/handoffs}/po_to_tl.md +0 -3
  103. package/{handoffs → template/handoffs}/qa_to_dev.md +1 -2
  104. package/template/handoffs/release_notes.md +51 -0
  105. package/template/handoffs/release_queue.md +47 -0
  106. package/template/handoffs/release_to_dev.md +31 -0
  107. package/template/handoffs/releases/Sxxxx-release-notes.md +62 -0
  108. package/template/handoffs/releases/vX.Y.Z-release-notes.md.example +21 -0
  109. package/template/handoffs/resume_brief.md +14 -0
  110. package/{handoffs → template/handoffs}/tl_to_dev.md +0 -2
  111. package/template/handoffs/token_cost_runs/README.md +26 -0
  112. package/template/its_magic/.its-magic-version +1 -0
  113. package/template/its_magic/README.md +9 -0
  114. package/template/scripts/auto_outer_driver.py +521 -0
  115. package/template/scripts/caveman_compress_input.py +903 -0
  116. package/template/scripts/check_downstream_ci_guard.py +67 -0
  117. package/template/scripts/check_intake_template_parity.py +312 -0
  118. package/template/scripts/check_token_cost_parity.py +69 -0
  119. package/template/scripts/dev_environment_lib.py +601 -0
  120. package/template/scripts/doc_profile_lib.py +415 -0
  121. package/template/scripts/downstream_ci_guard_lib.py +222 -0
  122. package/template/scripts/enforce-triad-hot-surface.py +753 -0
  123. package/template/scripts/guard_installer_publish.py +88 -0
  124. package/template/scripts/intake_bug_resume_brief_refresh.py +303 -0
  125. package/template/scripts/intake_bug_routing_guard.py +67 -0
  126. package/template/scripts/intake_evidence_lib.py +802 -0
  127. package/template/scripts/intake_evidence_validate.py +73 -0
  128. package/template/scripts/materialize_codebase_map.py +184 -0
  129. package/template/scripts/pack_json_validate.py +130 -0
  130. package/template/scripts/project_readme_coverage_lib.py +417 -0
  131. package/template/scripts/readme_feature_coverage_lib.py +608 -0
  132. package/{scripts → template/scripts}/release-all.sh +27 -4
  133. package/template/scripts/release_changelog_backfill.py +153 -0
  134. package/template/scripts/release_changelog_lib.py +544 -0
  135. package/template/scripts/release_changelog_validate.py +134 -0
  136. package/template/scripts/remote_config_summary.py +243 -0
  137. package/template/scripts/sync_push_gates.py +198 -0
  138. package/template/scripts/token_cost_compare.py +40 -0
  139. package/template/scripts/token_cost_lib.py +108 -0
  140. package/template/scripts/uat_probe_lib.py +868 -0
  141. package/template/scripts/validate-and-push.ps1 +280 -0
  142. package/template/scripts/validate-and-push.sh +243 -0
  143. package/template/scripts/validate_doc_profile.py +103 -0
  144. package/template/scripts/validate_project_readme_coverage.py +151 -0
  145. package/template/scripts/validate_readme_feature_coverage.py +140 -0
  146. package/template/sprints/S0001/progress.md +1 -0
  147. package/template/sprints/S0001/qa-findings.md +9 -0
  148. package/template/sprints/S0001/release-findings.md +24 -0
  149. package/template/sprints/S0001/sprint.md +9 -0
  150. package/template/sprints/S0001/summary.md +7 -0
  151. package/template/sprints/S0001/tasks.md +1 -0
  152. package/template/sprints/S0001/uat.json +6 -0
  153. package/template/sprints/S0001/uat.md +5 -0
  154. package/template/sprints/quick/Q0001/summary.md +1 -0
  155. package/{sprints → template/sprints}/quick/Q0001/task.json +0 -1
  156. package/.cursor/agents/curator.mdc +0 -21
  157. package/.cursor/agents/dev.mdc +0 -20
  158. package/.cursor/agents/po.mdc +0 -19
  159. package/.cursor/agents/qa.mdc +0 -19
  160. package/.cursor/agents/release.mdc +0 -19
  161. package/.cursor/agents/tech-lead.mdc +0 -21
  162. package/.cursor/commands/gsd-architecture.md +0 -29
  163. package/.cursor/commands/gsd-auto.md +0 -27
  164. package/.cursor/commands/gsd-discovery.md +0 -27
  165. package/.cursor/commands/gsd-execute.md +0 -32
  166. package/.cursor/commands/gsd-intake.md +0 -28
  167. package/.cursor/commands/gsd-map-codebase.md +0 -25
  168. package/.cursor/commands/gsd-milestone-complete.md +0 -24
  169. package/.cursor/commands/gsd-milestone-start.md +0 -26
  170. package/.cursor/commands/gsd-pause.md +0 -25
  171. package/.cursor/commands/gsd-plan-verify.md +0 -26
  172. package/.cursor/commands/gsd-qa.md +0 -28
  173. package/.cursor/commands/gsd-quick.md +0 -24
  174. package/.cursor/commands/gsd-refresh-context.md +0 -26
  175. package/.cursor/commands/gsd-release.md +0 -29
  176. package/.cursor/commands/gsd-research.md +0 -28
  177. package/.cursor/commands/gsd-resume.md +0 -26
  178. package/.cursor/commands/gsd-sprint-plan.md +0 -30
  179. package/.cursor/commands/gsd-verify-work.md +0 -25
  180. package/.cursor/hooks.json +0 -26
  181. package/.cursor/plans/cursor-gsd-team-kit_8cfee9b8.plan.md +0 -57
  182. package/.cursor/remote.json +0 -18
  183. package/.cursor/rules/gsd-core.mdc +0 -18
  184. package/.cursor/rules/gsd-handoffs.mdc +0 -10
  185. package/.cursor/rules/gsd-quality.mdc +0 -15
  186. package/.cursor/scratchpad.md +0 -34
  187. package/.github/workflows/ci.yml +0 -47
  188. package/decisions/DEC-0001.md +0 -21
  189. package/decisions/DEC-0002.md +0 -21
  190. package/docs/engineering/architecture.md +0 -354
  191. package/docs/engineering/decisions.md +0 -6
  192. package/docs/engineering/research.md +0 -11
  193. package/docs/engineering/runbook.md +0 -32
  194. package/docs/engineering/state.md +0 -33
  195. package/docs/product/acceptance.md +0 -6
  196. package/docs/product/backlog.md +0 -7
  197. package/docs/product/vision.md +0 -46
  198. package/gsd-installer.ps1 +0 -189
  199. package/gsd-installer.py +0 -195
  200. package/gsd-installer.sh +0 -201
  201. package/handoffs/release_notes.md +0 -14
  202. package/handoffs/resume_brief.md +0 -8
  203. package/milestones/M0001/milestone.json +0 -7
  204. package/milestones/M0001/phases.json +0 -9
  205. package/milestones/M0001/progress.md +0 -3
  206. package/milestones/M0001/summary.md +0 -3
  207. package/scripts/generate-release-notes.ps1 +0 -74
  208. package/scripts/generate-release-notes.sh +0 -63
  209. package/scripts/release-all.ps1 +0 -423
  210. package/sprints/S0001/progress.md +0 -4
  211. package/sprints/S0001/qa-findings.md +0 -113
  212. package/sprints/S0001/sprint.md +0 -70
  213. package/sprints/S0001/summary.md +0 -46
  214. package/sprints/S0001/tasks.md +0 -35
  215. package/sprints/S0001/uat.json +0 -8
  216. package/sprints/S0001/uat.md +0 -8
  217. package/sprints/quick/Q0001/summary.md +0 -3
  218. /package/{.cursor/rules/gsd-escalation.mdc → template/.cursor/rules/escalation.mdc} +0 -0
  219. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.json +0 -0
  220. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.md +0 -0
  221. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.json +0 -0
  222. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.md +0 -0
  223. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.json +0 -0
  224. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.md +0 -0
  225. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.json +0 -0
  226. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.md +0 -0
  227. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/milestone.json +0 -0
  228. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/phase-context.json +0 -0
  229. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/plan-verify.json +0 -0
  230. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.json +0 -0
  231. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.md +0 -0
  232. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.json +0 -0
  233. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.md +0 -0
  234. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/uat.json +0 -0
  235. /package/{docs → template/docs}/engineering/context/phase-template.json +0 -0
  236. /package/{docs → template/docs}/engineering/dependencies.json +0 -0
  237. /package/{sprints → template/sprints}/S0001/plan-verify.json +0 -0
package/installer.py ADDED
@@ -0,0 +1,1036 @@
1
+ import argparse
2
+ import filecmp
3
+ import importlib.util
4
+ import json
5
+ import os
6
+ import re
7
+ import shlex
8
+ import shutil
9
+ import sys
10
+ from datetime import datetime
11
+
12
+ REPO_URL = "https://github.com/fl0wm0ti0n/its-magic"
13
+ MANIFEST_RELATIVE_PATH = os.path.join("docs", "engineering", "context", "installer-owned-paths.manifest")
14
+ MANIFEST_REQUIRED_SCRIPTS_SECTION = "required_install_script_paths"
15
+
16
+
17
+ def normalize(path):
18
+ return os.path.normpath(os.path.abspath(path))
19
+
20
+
21
+ def read_version(source_root):
22
+ package_path = os.path.join(source_root, "package.json")
23
+ try:
24
+ with open(package_path, "r", encoding="utf-8") as f:
25
+ return json.load(f).get("version", "unknown")
26
+ except Exception:
27
+ return "unknown"
28
+
29
+
30
+ def list_source_files(source_root, include_paths):
31
+ files = []
32
+ for rel in include_paths:
33
+ src = os.path.join(source_root, rel)
34
+ if os.path.isfile(src):
35
+ files.append(rel)
36
+ elif os.path.isdir(src):
37
+ for root, _, filenames in os.walk(src):
38
+ for name in filenames:
39
+ full = os.path.join(root, name)
40
+ rel_path = os.path.relpath(full, source_root)
41
+ files.append(rel_path)
42
+ return sorted(set(files))
43
+
44
+
45
+ def read_manifest_paths(manifest_path, section_name):
46
+ items = []
47
+ in_section = False
48
+ with open(manifest_path, "r", encoding="utf-8") as f:
49
+ for raw in f:
50
+ line = raw.strip()
51
+ if not line or line.startswith("#"):
52
+ continue
53
+ if line.startswith("[") and line.endswith("]"):
54
+ in_section = line == f"[{section_name}]"
55
+ continue
56
+ if in_section:
57
+ items.append(line)
58
+ return items
59
+
60
+
61
+ def load_ownership_manifest(source_root, script_dir):
62
+ candidates = [
63
+ os.path.join(source_root, MANIFEST_RELATIVE_PATH),
64
+ os.path.join(script_dir, MANIFEST_RELATIVE_PATH),
65
+ ]
66
+ for path in candidates:
67
+ if not os.path.isfile(path):
68
+ continue
69
+ install_paths = read_manifest_paths(path, "install_include_paths")
70
+ clean_paths = read_manifest_paths(path, "clean_paths")
71
+ required_script_paths = read_manifest_paths(path, MANIFEST_REQUIRED_SCRIPTS_SECTION)
72
+ if not install_paths or not clean_paths:
73
+ raise RuntimeError(f"[INSTALL_MANIFEST_ERROR] {path} is missing required sections or entries.")
74
+ if not required_script_paths:
75
+ raise RuntimeError(
76
+ f"[INSTALL_MANIFEST_ERROR] {path} is missing [{MANIFEST_REQUIRED_SCRIPTS_SECTION}] entries."
77
+ )
78
+ return install_paths, clean_paths, required_script_paths, path
79
+ raise RuntimeError("[INSTALL_SOURCE_ERROR] installer-owned-paths.manifest not found. Reinstall its-magic package.")
80
+
81
+
82
+ def validate_install_completeness(target_root, source_root, required_script_paths, manifest_path):
83
+ missing_paths = []
84
+ for rel in sorted(set(required_script_paths)):
85
+ src = os.path.join(source_root, rel)
86
+ dst = os.path.join(target_root, rel)
87
+ if not os.path.isfile(src) or not os.path.isfile(dst):
88
+ missing_paths.append(rel.replace("\\", "/"))
89
+ if not missing_paths:
90
+ return True
91
+ print(
92
+ "[INSTALL_COMPLETENESS_FAILED] Required installer scripts are missing after "
93
+ "copy/classification invariant check."
94
+ )
95
+ for rel in missing_paths:
96
+ print(f"[INSTALL_REQUIRED_SCRIPT_MISSING:{rel}]")
97
+ print(
98
+ "Fix: update manifest parity and required-script inventory at "
99
+ f"{MANIFEST_RELATIVE_PATH} (section [{MANIFEST_REQUIRED_SCRIPTS_SECTION}]), "
100
+ "ensure each listed script exists in template/scripts and clean-path ownership, "
101
+ "then rerun installer missing/upgrade."
102
+ )
103
+ print(f"Manifest source: {manifest_path}")
104
+ return False
105
+
106
+
107
+ def ensure_parent(path):
108
+ parent = os.path.dirname(path)
109
+ if parent and not os.path.isdir(parent):
110
+ os.makedirs(parent, exist_ok=True)
111
+
112
+
113
+ def backup_files(target_root, rel_paths):
114
+ timestamp = datetime.utcnow().strftime("%Y%m%d-%H%M%SZ")
115
+ backup_root = os.path.join(target_root, "backups", timestamp)
116
+ for rel in rel_paths:
117
+ src = os.path.join(target_root, rel)
118
+ if os.path.isfile(src):
119
+ dst = os.path.join(backup_root, rel)
120
+ ensure_parent(dst)
121
+ shutil.copy2(src, dst)
122
+ return backup_root
123
+
124
+
125
+ def choose_mode():
126
+ print("Select install mode:")
127
+ print("1) missing-only (copy only files that do not exist)")
128
+ print("2) overwrite-all (replace existing files)")
129
+ print("3) interactive (prompt per file)")
130
+ print("4) upgrade (update framework files, preserve user data)")
131
+ choice = input("Enter 1, 2, 3, or 4: ").strip()
132
+ if choice == "1":
133
+ return "missing"
134
+ if choice == "2":
135
+ return "overwrite"
136
+ if choice == "4":
137
+ return "upgrade"
138
+ return "interactive"
139
+
140
+
141
+ FRAMEWORK_PREFIXES = (
142
+ ".cursor/commands/", ".cursor/rules/", ".cursor/agents/",
143
+ ".cursor/skills/", ".cursor/hooks/", ".github/workflows/",
144
+ "scripts/validate-and-push", "docs/engineering/context/", "its_magic/",
145
+ )
146
+ FRAMEWORK_EXACT = {
147
+ ".cursor/hooks.json", ".cursor/scratchpad.local.example.md",
148
+ ".its-magic-version", "its_magic/.its-magic-version", "its_magic/README.md",
149
+ }
150
+ USER_DATA_PREFIXES = (
151
+ "docs/product/", "docs/engineering/", "docs/user-guides/",
152
+ "sprints/", "handoffs/", "decisions/",
153
+ )
154
+ MIXED_FILES = {"README.md"}
155
+
156
+ # Model B (DEC-0055 / US-0073): baseline bytes live in template only; installs materialize `.cursor/scratchpad.md`.
157
+ SCRATCHPAD_BASELINE_REL = os.path.join(".cursor", "scratchpad.md")
158
+ SCRATCHPAD_EXAMPLE_REL = os.path.join(".cursor", "scratchpad.local.example.md")
159
+ SCRATCHPAD_LOCAL_REL = os.path.join(".cursor", "scratchpad.local.md")
160
+
161
+ # After merge (local > baseline > example), these must be non-empty (fail closed).
162
+ REQUIRED_SCRATCHPAD_KEYS = (
163
+ "MAGIC_CONTEXT_STRICT",
164
+ "AUTO_FLOW_MODE",
165
+ "PHASE_MODE",
166
+ "PERMISSION_MODE",
167
+ "AUTO_LOOP_MAX_CYCLES",
168
+ "SYNC_POLICY_MODE",
169
+ "DONE",
170
+ "TEAM_MODE",
171
+ )
172
+
173
+
174
+ def parse_scratchpad_file(path):
175
+ """Parse KEY=value lines; empty values are retained (explicit override to empty)."""
176
+ if not os.path.isfile(path):
177
+ return {}
178
+ out = {}
179
+ with open(path, "r", encoding="utf-8") as f:
180
+ for raw in f:
181
+ line = raw.strip()
182
+ if not line or line.startswith("#") or line.startswith("- "):
183
+ continue
184
+ if "=" not in line:
185
+ continue
186
+ key, _, val = line.partition("=")
187
+ key = key.strip()
188
+ if not key:
189
+ continue
190
+ out[key] = val.strip()
191
+ return out
192
+
193
+
194
+ def merge_scratchpad_layers(target_root):
195
+ """
196
+ Model B merge precedence: local > materialized baseline > example (later wins only when key absent).
197
+ """
198
+ ex_path = os.path.join(target_root, SCRATCHPAD_EXAMPLE_REL)
199
+ base_path = os.path.join(target_root, SCRATCHPAD_BASELINE_REL)
200
+ loc_path = os.path.join(target_root, SCRATCHPAD_LOCAL_REL)
201
+ example = parse_scratchpad_file(ex_path)
202
+ baseline = parse_scratchpad_file(base_path)
203
+ local = parse_scratchpad_file(loc_path)
204
+ merged = {}
205
+ all_keys = set(example) | set(baseline) | set(local)
206
+ for key in all_keys:
207
+ if key in local:
208
+ merged[key] = local[key]
209
+ elif key in baseline:
210
+ merged[key] = baseline[key]
211
+ elif key in example:
212
+ merged[key] = example[key]
213
+ paths = {"example": ex_path, "baseline": base_path, "local": loc_path}
214
+ return merged, paths
215
+
216
+
217
+ def validate_merged_scratchpad(target_root):
218
+ """Return (ok, list of diagnostic lines)."""
219
+ merged, paths = merge_scratchpad_layers(target_root)
220
+ diagnostics = []
221
+ if not os.path.isfile(paths["example"]):
222
+ diagnostics.append(
223
+ "[SCRATCHPAD_MERGE_ERROR] EXAMPLE_LAYER_MISSING: "
224
+ f".cursor/scratchpad.local.example.md not found under {target_root}. "
225
+ "Fix: re-run its-magic install/upgrade."
226
+ )
227
+ if not os.path.isfile(paths["baseline"]):
228
+ diagnostics.append(
229
+ "[SCRATCHPAD_MERGE_ERROR] MATERIALIZED_BASELINE_MISSING: "
230
+ f".cursor/scratchpad.md not found under {target_root}. "
231
+ "Fix: run `python installer.py --scratchpad-postinstall --target <repo> --mode missing` "
232
+ "or re-run its-magic install (Model B materialization; see docs)."
233
+ )
234
+ missing = []
235
+ for key in REQUIRED_SCRATCHPAD_KEYS:
236
+ val = merged.get(key)
237
+ if val is None or str(val).strip() == "":
238
+ missing.append(key)
239
+ if missing:
240
+ diagnostics.append(
241
+ "[SCRATCHPAD_MERGE_ERROR] REQUIRED_KEY_MISSING_AFTER_MERGE: "
242
+ f"keys={','.join(missing)}. Layers consulted: local, baseline|materialized, example "
243
+ f"({paths['local']}, {paths['baseline']}, {paths['example']}). "
244
+ "Fix: set non-empty values in .cursor/scratchpad.local.md or restore materialized baseline from template."
245
+ )
246
+ ok = not diagnostics
247
+ return ok, diagnostics
248
+
249
+
250
+ def materialize_scratchpad_example(target_root, source_root, print_ok=True):
251
+ """
252
+ Always refresh framework-owned scratchpad.local.example from template first
253
+ (example-first ordering before baseline; never touches scratchpad.local.md).
254
+ """
255
+ src = os.path.join(source_root, SCRATCHPAD_EXAMPLE_REL)
256
+ dst = os.path.join(target_root, SCRATCHPAD_EXAMPLE_REL)
257
+ if not os.path.isfile(src):
258
+ print(
259
+ "[SCRATCHPAD_EXAMPLE_ERROR] TEMPLATE_EXAMPLE_MISSING: "
260
+ f"expected template file at {src}. Reinstall its-magic package."
261
+ )
262
+ return False
263
+ ensure_parent(dst)
264
+ shutil.copy2(src, dst)
265
+ if print_ok:
266
+ print(
267
+ "[SCRATCHPAD_LAYER] example_refresh: copied template "
268
+ f"{SCRATCHPAD_EXAMPLE_REL} -> target (ordering: example before baseline)."
269
+ )
270
+ return True
271
+
272
+
273
+ def materialize_scratchpad_baseline(target_root, source_root, mode, print_ok=True):
274
+ """
275
+ Write stable baseline bytes from template when Model B requires it.
276
+ Never touches .cursor/scratchpad.local.md.
277
+ """
278
+ src = os.path.join(source_root, SCRATCHPAD_BASELINE_REL)
279
+ dst = os.path.join(target_root, SCRATCHPAD_BASELINE_REL)
280
+ if not os.path.isfile(src):
281
+ print(
282
+ "[SCRATCHPAD_MATERIALIZE_ERROR] TEMPLATE_BASELINE_MISSING: "
283
+ f"expected template file at {src}. Reinstall its-magic package."
284
+ )
285
+ return False
286
+ wrote = False
287
+ if mode == "overwrite":
288
+ ensure_parent(dst)
289
+ shutil.copy2(src, dst)
290
+ wrote = True
291
+ elif mode == "upgrade":
292
+ if not os.path.isfile(dst):
293
+ ensure_parent(dst)
294
+ shutil.copy2(src, dst)
295
+ wrote = True
296
+ else:
297
+ # missing, interactive
298
+ if not os.path.isfile(dst):
299
+ ensure_parent(dst)
300
+ shutil.copy2(src, dst)
301
+ wrote = True
302
+ if wrote and print_ok:
303
+ print(
304
+ "[SCRATCHPAD_LAYER] baseline_materialize: wrote materialized "
305
+ f"{SCRATCHPAD_BASELINE_REL} from template (Model B)."
306
+ )
307
+ elif print_ok and os.path.isfile(dst):
308
+ print(
309
+ "[SCRATCHPAD_LAYER] baseline_skip: materialized baseline already present "
310
+ f"({SCRATCHPAD_BASELINE_REL}); not overwritten in this mode."
311
+ )
312
+ return True
313
+
314
+
315
+ def _load_doc_profile_lib():
316
+ """
317
+ Load doc_profile_lib from scripts/ adjacent to this installer (repo checkout or npm package root).
318
+ Path is derived from __file__ only (no cwd / PYTHONPATH dependency).
319
+ """
320
+ here = os.path.dirname(os.path.abspath(__file__))
321
+ path = os.path.join(here, "scripts", "doc_profile_lib.py")
322
+ if not os.path.isfile(path):
323
+ raise RuntimeError(
324
+ "[DOC_PROFILE_LIB_MISSING] Expected documentation profile library at "
325
+ f"{path} (same directory as installer.py). "
326
+ "Global installs require this file in the published its-magic package; "
327
+ f"reinstall or upgrade its-magic ({REPO_URL})."
328
+ )
329
+ spec = importlib.util.spec_from_file_location("doc_profile_lib", path)
330
+ if spec is None or spec.loader is None:
331
+ raise RuntimeError(
332
+ "[DOC_PROFILE_LIB_LOAD_ERROR] Could not create import spec for "
333
+ f"{path}. Reinstall its-magic ({REPO_URL})."
334
+ )
335
+ mod = importlib.util.module_from_spec(spec)
336
+ sys.modules["doc_profile_lib"] = mod
337
+ try:
338
+ spec.loader.exec_module(mod)
339
+ except Exception as e:
340
+ sys.modules.pop("doc_profile_lib", None)
341
+ raise RuntimeError(
342
+ "[DOC_PROFILE_LIB_LOAD_ERROR] doc_profile_lib failed to load "
343
+ f"({e!r}). Reinstall its-magic ({REPO_URL})."
344
+ ) from e
345
+ return mod
346
+
347
+
348
+ def _load_dev_environment_lib():
349
+ """Load dev_environment_lib from scripts/ adjacent to this installer."""
350
+ here = os.path.dirname(os.path.abspath(__file__))
351
+ path = os.path.join(here, "scripts", "dev_environment_lib.py")
352
+ if not os.path.isfile(path):
353
+ raise RuntimeError(
354
+ "[DEV_ENVIRONMENT_LIB_MISSING] Expected dev environment library at "
355
+ f"{path} (same directory as installer.py). "
356
+ f"Reinstall or upgrade its-magic ({REPO_URL})."
357
+ )
358
+ spec = importlib.util.spec_from_file_location("dev_environment_lib", path)
359
+ if spec is None or spec.loader is None:
360
+ raise RuntimeError(
361
+ "[DEV_ENVIRONMENT_LIB_LOAD_ERROR] Could not create import spec for "
362
+ f"{path}. Reinstall its-magic ({REPO_URL})."
363
+ )
364
+ mod = importlib.util.module_from_spec(spec)
365
+ sys.modules["dev_environment_lib"] = mod
366
+ try:
367
+ spec.loader.exec_module(mod)
368
+ except Exception as e:
369
+ sys.modules.pop("dev_environment_lib", None)
370
+ raise RuntimeError(
371
+ "[DEV_ENVIRONMENT_LIB_LOAD_ERROR] dev_environment_lib failed to load "
372
+ f"({e!r}). Reinstall its-magic ({REPO_URL})."
373
+ ) from e
374
+ return mod
375
+
376
+
377
+ def bootstrap_dev_environment_profile_installer_hook(target_root, source_root):
378
+ """
379
+ Non-destructive dev-environment profile bootstrap.
380
+ Fail-closed only on PATH_INVALID / SOURCE_MISSING.
381
+ """
382
+ try:
383
+ lib = _load_dev_environment_lib()
384
+ except RuntimeError as exc:
385
+ print(str(exc))
386
+ return False
387
+ merged, _paths = merge_scratchpad_layers(target_root)
388
+ reason, _channel = lib.bootstrap_dev_environment_profile(
389
+ target_root, source_root, merged
390
+ )
391
+ if reason in (
392
+ lib.DEV_ENV_BOOTSTRAP_PATH_INVALID,
393
+ lib.DEV_ENV_BOOTSTRAP_SOURCE_MISSING,
394
+ ):
395
+ return False
396
+ return True
397
+
398
+
399
+ def _doc_profile_sync(target_root, merged, print_ok=True):
400
+ """Append missing normative README/developer doc sections from merged profile (non-destructive)."""
401
+ doc_profile_lib = _load_doc_profile_lib()
402
+ notes = doc_profile_lib.ensure_doc_surfaces_merged(merged, target_root, print_ok=print_ok)
403
+ bad = [ln for ln in notes if ln.startswith("[DOC_PROFILE_INVALID]")]
404
+ return (not bad), notes
405
+
406
+
407
+ def run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True):
408
+ if not materialize_scratchpad_example(target_root, source_root, print_ok=print_ok):
409
+ return False
410
+ if not materialize_scratchpad_baseline(target_root, source_root, mode, print_ok=print_ok):
411
+ return False
412
+ ok, diagnostics = validate_merged_scratchpad(target_root)
413
+ for line in diagnostics:
414
+ print(line)
415
+ if ok:
416
+ merged, _paths = merge_scratchpad_layers(target_root)
417
+ try:
418
+ dp_ok, dp_notes = _doc_profile_sync(target_root, merged, print_ok=print_ok)
419
+ except RuntimeError as e:
420
+ print(str(e))
421
+ ok = False
422
+ else:
423
+ for line in dp_notes:
424
+ print(line)
425
+ if not dp_ok:
426
+ ok = False
427
+ if ok and print_ok:
428
+ loc = os.path.join(target_root, SCRATCHPAD_LOCAL_REL)
429
+ if os.path.isfile(loc):
430
+ print(
431
+ "[SCRATCHPAD_LAYER] user_local: preserved "
432
+ f"{SCRATCHPAD_LOCAL_REL} (merge precedence unchanged)."
433
+ )
434
+ print(
435
+ "[SCRATCHPAD_POSTINSTALL_OK] Model B: example refreshed, baseline handled, "
436
+ "merged scratchpad validation passed."
437
+ )
438
+ return ok
439
+
440
+
441
+ def classify_file(rel_path):
442
+ normalized = rel_path.replace(os.sep, "/")
443
+ if normalized in MIXED_FILES:
444
+ return "mixed"
445
+ for p in FRAMEWORK_PREFIXES:
446
+ if normalized.startswith(p):
447
+ return "framework"
448
+ if normalized in FRAMEWORK_EXACT:
449
+ return "framework"
450
+ for p in USER_DATA_PREFIXES:
451
+ if normalized.startswith(p):
452
+ return "user-data"
453
+ return "framework"
454
+
455
+
456
+ def read_installed_version(target_root):
457
+ primary = os.path.join(target_root, "its_magic", ".its-magic-version")
458
+ if os.path.isfile(primary):
459
+ with open(primary, "r", encoding="utf-8") as f:
460
+ return f.read().strip()
461
+ legacy = os.path.join(target_root, ".its-magic-version")
462
+ if os.path.isfile(legacy):
463
+ with open(legacy, "r", encoding="utf-8") as f:
464
+ return f.read().strip()
465
+ return "unknown"
466
+
467
+
468
+ def write_installed_version(target_root, ver):
469
+ vf = os.path.join(target_root, "its_magic", ".its-magic-version")
470
+ ensure_parent(vf)
471
+ with open(vf, "w", encoding="utf-8") as f:
472
+ f.write(ver)
473
+ legacy = os.path.join(target_root, ".its-magic-version")
474
+ if os.path.isfile(legacy):
475
+ os.remove(legacy)
476
+
477
+
478
+ def sync_root_readme_to_its_magic(target_root):
479
+ root_readme = os.path.join(target_root, "README.md")
480
+ if not os.path.isfile(root_readme):
481
+ return False
482
+ its_magic_readme = os.path.join(target_root, "its_magic", "README.md")
483
+ ensure_parent(its_magic_readme)
484
+ shutil.copy2(root_readme, its_magic_readme)
485
+ return True
486
+
487
+
488
+ def read_runbook_key(runbook_path, key):
489
+ if not os.path.isfile(runbook_path):
490
+ return ""
491
+ needle = f"{key}:"
492
+ with open(runbook_path, "r", encoding="utf-8") as f:
493
+ for raw in f:
494
+ line = raw.rstrip("\n")
495
+ if line.startswith(needle):
496
+ return line[len(needle):].strip()
497
+ return ""
498
+
499
+
500
+ def write_runbook_key(runbook_path, key, value):
501
+ with open(runbook_path, "r", encoding="utf-8") as f:
502
+ lines = f.readlines()
503
+ needle = f"{key}:"
504
+ changed = False
505
+ for idx, raw in enumerate(lines):
506
+ line = raw.rstrip("\n")
507
+ if line.startswith(needle):
508
+ lines[idx] = f"{needle} {value}\n"
509
+ changed = True
510
+ break
511
+ if changed:
512
+ with open(runbook_path, "w", encoding="utf-8") as f:
513
+ f.writelines(lines)
514
+ return changed
515
+
516
+
517
+ def package_has_script(target_root, script_name):
518
+ pkg_path = os.path.join(target_root, "package.json")
519
+ if not os.path.isfile(pkg_path):
520
+ return False
521
+ try:
522
+ with open(pkg_path, "r", encoding="utf-8") as f:
523
+ pkg = json.load(f)
524
+ scripts = pkg.get("scripts", {})
525
+ val = scripts.get(script_name)
526
+ return isinstance(val, str) and val.strip() != ""
527
+ except Exception:
528
+ return False
529
+
530
+
531
+ def detect_runbook_defaults(target_root):
532
+ tests_sh = os.path.join(target_root, "tests", "run-tests.sh")
533
+ has_pkg = os.path.isfile(os.path.join(target_root, "package.json"))
534
+ has_py = any(
535
+ os.path.isfile(os.path.join(target_root, p))
536
+ for p in ("pyproject.toml", "requirements.txt", "setup.py")
537
+ )
538
+ has_go = os.path.isfile(os.path.join(target_root, "go.mod"))
539
+
540
+ result = {"TEST_COMMAND": "", "LINT_COMMAND": "", "TYPECHECK_COMMAND": ""}
541
+
542
+ if has_pkg and package_has_script(target_root, "test"):
543
+ result["TEST_COMMAND"] = "npm run test"
544
+ if package_has_script(target_root, "lint"):
545
+ result["LINT_COMMAND"] = "npm run lint"
546
+ if package_has_script(target_root, "typecheck"):
547
+ result["TYPECHECK_COMMAND"] = "npm run typecheck"
548
+ elif has_go:
549
+ result["TEST_COMMAND"] = "go test ./..."
550
+ elif has_py:
551
+ result["TEST_COMMAND"] = "python -m pytest"
552
+ elif os.path.isfile(tests_sh):
553
+ result["TEST_COMMAND"] = "sh tests/run-tests.sh"
554
+
555
+ return result
556
+
557
+
558
+ def validate_bootstrap_command(target_root, key, command):
559
+ if not command:
560
+ return False, f"{key}_UNDETECTED"
561
+ if command.startswith("npm run "):
562
+ if not shutil.which("npm"):
563
+ return False, "NPM_NOT_FOUND"
564
+ script_name = command[len("npm run "):].strip()
565
+ if not package_has_script(target_root, script_name):
566
+ return False, f"NPM_SCRIPT_MISSING:{script_name}"
567
+ return True, "OK"
568
+ if command.startswith("python -m "):
569
+ if not shutil.which("python"):
570
+ return False, "PYTHON_NOT_FOUND"
571
+ if command == "python -m pytest":
572
+ has_py = any(
573
+ os.path.isfile(os.path.join(target_root, p))
574
+ for p in ("pyproject.toml", "requirements.txt", "setup.py")
575
+ )
576
+ if not has_py:
577
+ return False, "PYTHON_STACK_MARKERS_MISSING"
578
+ return True, "OK"
579
+ if command.startswith("go test "):
580
+ if not shutil.which("go"):
581
+ return False, "GO_NOT_FOUND"
582
+ if not os.path.isfile(os.path.join(target_root, "go.mod")):
583
+ return False, "GO_MOD_MISSING"
584
+ return True, "OK"
585
+ if command.startswith("powershell "):
586
+ if not os.path.isfile(os.path.join(target_root, "tests", "run-tests.ps1")):
587
+ return False, "RUN_TESTS_PS1_MISSING"
588
+ return True, "OK"
589
+ if command.startswith("sh "):
590
+ if not shutil.which("sh"):
591
+ return False, "SH_NOT_FOUND"
592
+ if not os.path.isfile(os.path.join(target_root, "tests", "run-tests.sh")):
593
+ return False, "RUN_TESTS_SH_MISSING"
594
+ return True, "OK"
595
+ try:
596
+ parts = shlex.split(command)
597
+ except ValueError:
598
+ return False, "COMMAND_PARSE_FAILED"
599
+ if not parts:
600
+ return False, "COMMAND_EMPTY"
601
+ if not shutil.which(parts[0]):
602
+ return False, f"EXECUTABLE_NOT_FOUND:{parts[0]}"
603
+ return True, "OK"
604
+
605
+
606
+ def bootstrap_runbook_commands(target_root):
607
+ runbook_path = os.path.join(target_root, "docs", "engineering", "runbook.md")
608
+ if not os.path.isfile(runbook_path):
609
+ return True, []
610
+
611
+ defaults = detect_runbook_defaults(target_root)
612
+ diagnostics = []
613
+ changed = []
614
+
615
+ for key in ("TEST_COMMAND", "LINT_COMMAND", "TYPECHECK_COMMAND"):
616
+ current = read_runbook_key(runbook_path, key)
617
+ if current:
618
+ continue
619
+ candidate = defaults.get(key, "")
620
+ if not candidate:
621
+ if key == "TEST_COMMAND":
622
+ diagnostics.append(
623
+ "[RUNBOOK_BOOTSTRAP_ERROR] TEST_COMMAND_UNRESOLVED: could not detect a valid baseline test command. "
624
+ "Fix: define TEST_COMMAND in docs/engineering/runbook.md or add detectable stack markers (package.json scripts.test, pyproject.toml, go.mod)."
625
+ )
626
+ continue
627
+ valid, reason = validate_bootstrap_command(target_root, key, candidate)
628
+ if valid:
629
+ if write_runbook_key(runbook_path, key, candidate):
630
+ changed.append(f"{key}={candidate}")
631
+ elif key == "TEST_COMMAND":
632
+ diagnostics.append(
633
+ f"[RUNBOOK_BOOTSTRAP_ERROR] TEST_COMMAND_INVALID:{reason}. "
634
+ "Fix: set a valid TEST_COMMAND in docs/engineering/runbook.md."
635
+ )
636
+
637
+ if changed:
638
+ diagnostics.append("[RUNBOOK_BOOTSTRAP] Applied defaults: " + ", ".join(changed))
639
+
640
+ has_test = read_runbook_key(runbook_path, "TEST_COMMAND") != ""
641
+ if not has_test:
642
+ return False, diagnostics
643
+ return True, diagnostics
644
+
645
+
646
+ def prompt_yes_no(label, default=False):
647
+ suffix = "Y/n" if default else "y/N"
648
+ value = input(f"{label} [{suffix}]: ").strip().lower()
649
+ if not value:
650
+ return default
651
+ return value in ("y", "yes")
652
+
653
+
654
+ def show_banner(include_install_message=False):
655
+ try:
656
+ sys.stdout.reconfigure(encoding="utf-8")
657
+ except Exception:
658
+ pass
659
+ m = "\033[1;35m"
660
+ c = "\033[1;36m"
661
+ y = "\033[1;33m"
662
+ g = "\033[1;32m"
663
+ r = "\033[0m"
664
+ print()
665
+ print(f"{m} ██╗████████╗███████╗ ███╗ ███╗ █████╗ ██████╗ ██╗ ██████╗{r}")
666
+ print(f"{m} ██║╚══██╔══╝██╔════╝ ████╗ ████║██╔══██╗██╔════╝ ██║██╔════╝{r}")
667
+ print(f"{m} ██║ ██║ ███████╗█████╗██╔████╔██║███████║██║ ███╗██║██║ {r}")
668
+ print(f"{c} ██║ ██║ ╚════██║╚════╝██║╚██╔╝██║██╔══██║██║ ██║██║██║ {r}")
669
+ print(f"{c} ██║ ██║ ███████║ ██║ ╚═╝ ██║██║ ██║╚██████╔╝██║╚██████╗{r}")
670
+ print(f"{c} ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝{r}")
671
+ print()
672
+ print(f"{y} AI dev team{r}")
673
+ if include_install_message:
674
+ print(f"{g} Installation complete!{r}")
675
+ print()
676
+
677
+
678
+ def show_help(version):
679
+ show_banner(include_install_message=False)
680
+ print(f"its-magic v{version}")
681
+ print(f"Repository: {REPO_URL}")
682
+ print()
683
+ print("Install AI dev team workflow files into any Cursor repository.")
684
+ print()
685
+ print("Usage:")
686
+ print(" its-magic --target <path> [--mode <mode>] [--backup] [--create]")
687
+ print(" its-magic --clean-repo [--target <path>] [--yes]")
688
+ print(" its-magic --help | --version")
689
+ print()
690
+ print("Install options:")
691
+ print(" --target <path> Path to the repository where workflow files are installed.")
692
+ print(" If omitted you will be prompted interactively.")
693
+ print(" --mode <mode> How to handle files that already exist in the target:")
694
+ print(" missing Only copy files that do not exist yet (default).")
695
+ print(" Safe for repos that already have some workflow files.")
696
+ print(" overwrite Replace every file, even if it already exists.")
697
+ print(" Combine with --backup to keep a snapshot first.")
698
+ print(" interactive Ask per file whether to overwrite or skip.")
699
+ print(" upgrade Update framework files while preserving user data.")
700
+ print(" Use after updating its-magic to a newer version.")
701
+ print(" --backup Before overwriting, save existing files to backups/<timestamp>/.")
702
+ print(" Ignored when mode is 'missing' (nothing gets replaced).")
703
+ print(" --create Create the target directory if it does not exist.")
704
+ print(" Note: installer bootstraps runbook TEST/LINT/TYPECHECK commands")
705
+ print(" from OS+stack detection; unresolved TEST_COMMAND fails fast with")
706
+ print(" [RUNBOOK_BOOTSTRAP_ERROR] diagnostics.")
707
+ print(" Note: scratchpad Model B: `.cursor/scratchpad.md` is")
708
+ print(" materialized from the packaged template when missing; merged validation")
709
+ print(" requires Python 3 on PATH for installer.ps1 / installer.sh. Recovery:")
710
+ print(" python installer.py --scratchpad-postinstall --target <repo> --mode missing")
711
+ print()
712
+ print("Clean options:")
713
+ print(" --clean-repo Remove all its-magic workflow artifacts from the target repo")
714
+ print(" (owned paths from installer manifest, including .cursor,")
715
+ print(" docs/product, docs/engineering, docs/user-guides, sprints,")
716
+ print(" handoffs, decisions, workflow scripts, CI files, and")
717
+ print(" installer metadata under its_magic/ (legacy .its-magic-version")
718
+ print(" is also removed when present). Your own source code is never touched.")
719
+ print(" --target <path> Repo to clean (default: current directory).")
720
+ print(" --yes Skip the confirmation prompt.")
721
+ print()
722
+ print("Info:")
723
+ print(" --help, -h Show this help and exit.")
724
+ print(" --version, -v Print the installed version and exit.")
725
+ print()
726
+ print("Examples:")
727
+ print(" its-magic --target . --mode missing Safe first-time setup")
728
+ print(" its-magic --target . --mode upgrade Update framework, keep user data")
729
+ print(" its-magic --target . --mode overwrite --backup Replace all files, keep backup")
730
+ print(" its-magic --clean-repo --target . --yes Remove workflow artifacts silently")
731
+ print()
732
+
733
+
734
+ def clean_repo(target_root, clean_paths):
735
+ for rel in clean_paths:
736
+ full = os.path.join(target_root, rel)
737
+ if os.path.exists(full):
738
+ if os.path.isdir(full):
739
+ shutil.rmtree(full)
740
+ else:
741
+ os.remove(full)
742
+ print(f"Removed: {rel}")
743
+ print("Clean completed.")
744
+
745
+
746
+ def main():
747
+ script_dir = normalize(os.path.dirname(__file__))
748
+ template_dir = os.path.join(script_dir, "template")
749
+ source_root = template_dir
750
+ version = read_version(script_dir)
751
+
752
+ parser = argparse.ArgumentParser(
753
+ description="Install its-magic into a repo",
754
+ add_help=False,
755
+ )
756
+ parser.add_argument("--target", help="Target repository path")
757
+ parser.add_argument("--mode", choices=["missing", "overwrite", "interactive", "upgrade"], help="Install mode")
758
+ parser.add_argument("--backup", action="store_true", help="Backup files before overwriting")
759
+ parser.add_argument("--create", action="store_true", help="Create target directory if missing")
760
+ parser.add_argument("--clean-repo", action="store_true", help="Remove installed workflow artifacts")
761
+ parser.add_argument("--yes", action="store_true", help="Skip clean confirmation prompt")
762
+ parser.add_argument("--help", "-h", action="store_true", help="Show help")
763
+ parser.add_argument("--version", "-v", action="store_true", help="Show version")
764
+ parser.add_argument(
765
+ "--scratchpad-postinstall",
766
+ action="store_true",
767
+ help=argparse.SUPPRESS,
768
+ )
769
+ parser.add_argument("--source-root", help=argparse.SUPPRESS)
770
+ parser.add_argument(
771
+ "--validate-install-completeness",
772
+ action="store_true",
773
+ help=argparse.SUPPRESS,
774
+ )
775
+ args = parser.parse_args()
776
+
777
+ if len(sys.argv) == 1 or args.help:
778
+ show_help(version)
779
+ return 0
780
+
781
+ if args.version:
782
+ print(f"its-magic v{version}")
783
+ return 0
784
+
785
+ if args.source_root:
786
+ source_root = normalize(args.source_root)
787
+
788
+ if args.scratchpad_postinstall:
789
+ target_root = normalize(args.target) if args.target else normalize(".")
790
+ mode = args.mode or "missing"
791
+ if mode not in ("missing", "overwrite", "interactive", "upgrade"):
792
+ print(
793
+ "[SCRATCHPAD_POSTINSTALL_ERROR] INVALID_MODE: use --mode "
794
+ "missing|overwrite|interactive|upgrade with --scratchpad-postinstall."
795
+ )
796
+ return 1
797
+ if not os.path.isdir(source_root):
798
+ print("[INSTALL_SOURCE_ERROR] template directory is missing. Reinstall its-magic package.")
799
+ return 1
800
+ if not os.path.isdir(target_root):
801
+ print(f"[SCRATCHPAD_POSTINSTALL_ERROR] TARGET_MISSING: {target_root}")
802
+ return 1
803
+ ok = run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True)
804
+ return 0 if ok else 1
805
+
806
+ if args.validate_install_completeness:
807
+ target_root = normalize(args.target) if args.target else normalize(".")
808
+ if not os.path.isdir(target_root):
809
+ print(f"[INSTALL_COMPLETENESS_FAILED] target directory missing: {target_root}")
810
+ return 1
811
+ if not os.path.isdir(source_root):
812
+ print("[INSTALL_SOURCE_ERROR] template directory is missing. Reinstall its-magic package.")
813
+ return 1
814
+ try:
815
+ _install_paths, _clean_paths, required_script_paths, manifest_path = load_ownership_manifest(
816
+ source_root, script_dir
817
+ )
818
+ except RuntimeError as exc:
819
+ print(str(exc))
820
+ return 1
821
+ ok = validate_install_completeness(target_root, source_root, required_script_paths, manifest_path)
822
+ return 0 if ok else 1
823
+
824
+ if not os.path.isdir(source_root):
825
+ print("[INSTALL_SOURCE_ERROR] template directory is missing. Reinstall its-magic package.")
826
+ return 1
827
+ try:
828
+ include_paths, clean_paths, required_script_paths, manifest_path = load_ownership_manifest(
829
+ source_root, script_dir
830
+ )
831
+ except RuntimeError as exc:
832
+ print(str(exc))
833
+ return 1
834
+
835
+ target_root = normalize(args.target) if args.target else None
836
+
837
+ if args.clean_repo:
838
+ if not target_root:
839
+ target_root = normalize(".")
840
+ if not os.path.isdir(target_root):
841
+ print("Target directory does not exist.")
842
+ return 1
843
+ if not args.yes and not prompt_yes_no(f"Clean its-magic workflow artifacts in {target_root}?", default=False):
844
+ print("Aborted.")
845
+ return 1
846
+ clean_repo(target_root, clean_paths)
847
+ return 0
848
+
849
+ if not target_root:
850
+ target_root = normalize(input("Target repository path: ").strip())
851
+
852
+ if not os.path.isdir(target_root):
853
+ if args.create or prompt_yes_no("Target missing. Create?", default=False):
854
+ os.makedirs(target_root, exist_ok=True)
855
+ else:
856
+ print("Target directory does not exist.")
857
+ return 1
858
+
859
+ mode = args.mode or choose_mode()
860
+ backup_enabled = args.backup
861
+ if mode in ("overwrite", "interactive") and not args.backup:
862
+ backup_enabled = prompt_yes_no("Backup existing files before overwrite?", False)
863
+
864
+ files = list_source_files(source_root, include_paths)
865
+ if not files:
866
+ print("No source files found to install.")
867
+ return 1
868
+
869
+ overwrite_candidates = []
870
+ if backup_enabled and mode == "overwrite":
871
+ for rel in files:
872
+ if os.path.isfile(os.path.join(target_root, rel)):
873
+ overwrite_candidates.append(rel)
874
+ if overwrite_candidates:
875
+ broot = backup_files(target_root, overwrite_candidates)
876
+ print(f"Backup created at: {broot}")
877
+
878
+ if mode == "upgrade":
879
+ old_ver = read_installed_version(target_root)
880
+ print(f"\n\033[1;36mUpgrading from v{old_ver} to v{version}\033[0m\n")
881
+
882
+ if backup_enabled:
883
+ bc = [r for r in files if classify_file(r) == "framework" and os.path.isfile(os.path.join(target_root, r))]
884
+ if bc:
885
+ broot = backup_files(target_root, bc)
886
+ print(f"Backup created at: {broot}")
887
+
888
+ added, updated, review = [], [], []
889
+ unchanged = preserved = 0
890
+ scratchpad_example_rel = ".cursor/scratchpad.local.example.md"
891
+ scratchpad_example_status = "not-seen"
892
+
893
+ for rel in files:
894
+ src = os.path.join(source_root, rel)
895
+ dst = os.path.join(target_root, rel)
896
+ exists = os.path.isfile(dst)
897
+ cat = classify_file(rel)
898
+
899
+ if not exists:
900
+ ensure_parent(dst)
901
+ shutil.copy2(src, dst)
902
+ added.append(rel)
903
+ if rel == scratchpad_example_rel:
904
+ scratchpad_example_status = "added"
905
+ continue
906
+
907
+ if cat == "framework":
908
+ if filecmp.cmp(src, dst, shallow=False):
909
+ unchanged += 1
910
+ if rel == scratchpad_example_rel:
911
+ scratchpad_example_status = "unchanged"
912
+ else:
913
+ ensure_parent(dst)
914
+ shutil.copy2(src, dst)
915
+ updated.append(rel)
916
+ if rel == scratchpad_example_rel:
917
+ scratchpad_example_status = "updated"
918
+ continue
919
+
920
+ if cat == "user-data":
921
+ preserved += 1
922
+ continue
923
+
924
+ if cat == "mixed":
925
+ preserved += 1
926
+ if not filecmp.cmp(src, dst, shallow=False):
927
+ review.append(rel)
928
+ continue
929
+
930
+ if not run_scratchpad_postinstall(target_root, source_root, "upgrade", print_ok=True):
931
+ return 1
932
+ if not bootstrap_dev_environment_profile_installer_hook(target_root, source_root):
933
+ return 1
934
+ if not validate_install_completeness(target_root, source_root, required_script_paths, manifest_path):
935
+ return 1
936
+
937
+ write_installed_version(target_root, version)
938
+ sync_root_readme_to_its_magic(target_root)
939
+ runbook_ok, runbook_notes = bootstrap_runbook_commands(target_root)
940
+ for note in runbook_notes:
941
+ print(note)
942
+ if not runbook_ok:
943
+ return 1
944
+
945
+ show_banner()
946
+ g = "\033[1;32m"
947
+ y = "\033[1;33m"
948
+ p = "\033[1;35m"
949
+ d = "\033[0;90m"
950
+ r = "\033[0m"
951
+ print(f"{g}Upgrade complete: v{old_ver} -> v{version}{r}\n")
952
+ if added:
953
+ print(f" {g}Added (new): {len(added)} files{r}")
954
+ for f in added:
955
+ print(f" {f}")
956
+ if updated:
957
+ print(f" {y}Updated (framework): {len(updated)} files{r}")
958
+ for f in updated:
959
+ print(f" {f}")
960
+ print(f" Unchanged: {unchanged} files")
961
+ print(f" Preserved (user): {preserved} files")
962
+ if scratchpad_example_status == "not-seen":
963
+ scratchpad_example_status = "not-in-manifest"
964
+ print(f" Scratchpad example: {scratchpad_example_status} (.cursor/scratchpad.local.example.md)")
965
+ print(
966
+ " Scratchpad layers: post-install refreshed example-first, then baseline "
967
+ "(see [SCRATCHPAD_LAYER] lines)."
968
+ )
969
+ if os.path.isfile(os.path.join(target_root, ".cursor", "scratchpad.local.md")):
970
+ print(" User local file: preserved (.cursor/scratchpad.local.md)")
971
+ if review:
972
+ print(f"\n {p}Review recommended: {len(review)} files{r}")
973
+ for f in review:
974
+ print(f" {f}")
975
+ print(f" {d}Check .cursor/scratchpad.local.example.md for new flags.{r}")
976
+ print(f"\nRepository: {REPO_URL}\n")
977
+ return 0
978
+
979
+ for rel in files:
980
+ src = os.path.join(source_root, rel)
981
+ dst = os.path.join(target_root, rel)
982
+ exists = os.path.isfile(dst)
983
+
984
+ if mode == "missing":
985
+ if exists:
986
+ continue
987
+ ensure_parent(dst)
988
+ shutil.copy2(src, dst)
989
+ continue
990
+
991
+ if mode == "overwrite":
992
+ ensure_parent(dst)
993
+ shutil.copy2(src, dst)
994
+ continue
995
+
996
+ if mode == "interactive":
997
+ if not exists:
998
+ ensure_parent(dst)
999
+ shutil.copy2(src, dst)
1000
+ continue
1001
+ answer = input(f"File exists: {rel} | [o]verwrite [s]kip [q]uit: ").strip().lower()
1002
+ if answer == "q":
1003
+ print("Aborted.")
1004
+ return 1
1005
+ if answer == "o":
1006
+ if backup_enabled:
1007
+ broot = backup_files(target_root, [rel])
1008
+ print(f"Backed up: {rel} -> {broot}")
1009
+ ensure_parent(dst)
1010
+ shutil.copy2(src, dst)
1011
+
1012
+ if not run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True):
1013
+ return 1
1014
+ if not bootstrap_dev_environment_profile_installer_hook(target_root, source_root):
1015
+ return 1
1016
+ if not validate_install_completeness(target_root, source_root, required_script_paths, manifest_path):
1017
+ return 1
1018
+
1019
+ write_installed_version(target_root, version)
1020
+ sync_root_readme_to_its_magic(target_root)
1021
+ runbook_ok, runbook_notes = bootstrap_runbook_commands(target_root)
1022
+ for note in runbook_notes:
1023
+ print(note)
1024
+ if not runbook_ok:
1025
+ return 1
1026
+
1027
+ show_banner(include_install_message=True)
1028
+ print(f"its-magic v{version}")
1029
+ print(f"Repository: {REPO_URL}")
1030
+ print()
1031
+ return 0
1032
+
1033
+
1034
+ if __name__ == "__main__":
1035
+ raise SystemExit(main())
1036
+