its-magic 0.1.2-9 → 0.1.2

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 (232) hide show
  1. package/README.md +1602 -755
  2. package/bin/its-magic.js +121 -11
  3. package/bin/postinstall.js +21 -0
  4. package/installer.ps1 +759 -0
  5. package/installer.py +981 -0
  6. package/installer.sh +649 -0
  7. package/package.json +18 -14
  8. package/scripts/check_intake_template_parity.py +284 -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 +505 -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/README.md +1602 -0
  65. package/template/decisions/DEC-0001.md +11 -0
  66. package/template/decisions/DEC-0002.md +11 -0
  67. package/template/docs/developer/README.md +44 -0
  68. package/template/docs/engineering/architecture.md +9 -0
  69. package/template/docs/engineering/artifact-ordering-policy.md +48 -0
  70. package/template/docs/engineering/artifact-ownership-policy.md +57 -0
  71. package/template/docs/engineering/auto-orchestration-reference.md +1211 -0
  72. package/{docs → template/docs}/engineering/codebase-map.md +0 -5
  73. package/template/docs/engineering/compatibility-report.md +20 -0
  74. package/template/docs/engineering/compatibility-signals.md +7 -0
  75. package/template/docs/engineering/component-scope-report.md +16 -0
  76. package/template/docs/engineering/component-scope.md +19 -0
  77. package/template/docs/engineering/context/installer-owned-paths.manifest +96 -0
  78. package/template/docs/engineering/context/readme-section-affinity.json +30 -0
  79. package/template/docs/engineering/decisions.md +16 -0
  80. package/template/docs/engineering/legacy-drift-audit.md +21 -0
  81. package/template/docs/engineering/manifests/components/api-gateway.manifest.yaml +12 -0
  82. package/template/docs/engineering/manifests/registry.manifest.yaml +20 -0
  83. package/template/docs/engineering/manifests/repo.manifest.yaml +11 -0
  84. package/template/docs/engineering/phase-context.md +16 -0
  85. package/template/docs/engineering/release-targets.json +123 -0
  86. package/template/docs/engineering/research.md +29 -0
  87. package/template/docs/engineering/runbook.md +2320 -0
  88. package/template/docs/engineering/runtime-connectivity.md +81 -0
  89. package/template/docs/engineering/security-review.md +33 -0
  90. package/template/docs/engineering/spec-pack/README.md +20 -0
  91. package/template/docs/engineering/state-archive/README.md +15 -0
  92. package/template/docs/engineering/state.md +20 -0
  93. package/template/docs/engineering/status-normalization-report.md +19 -0
  94. package/template/docs/engineering/token-cost-parity-manifest.md +16 -0
  95. package/template/docs/engineering/us-0084-remote-e2e.md +44 -0
  96. package/template/docs/product/acceptance.md +1 -0
  97. package/template/docs/product/backlog.md +5 -0
  98. package/template/docs/product/vision.md +11 -0
  99. package/template/docs/user-guides/README.md +40 -0
  100. package/{handoffs → template/handoffs}/dev_to_qa.md +0 -3
  101. package/{handoffs → template/handoffs}/po_to_tl.md +0 -3
  102. package/{handoffs → template/handoffs}/qa_to_dev.md +1 -2
  103. package/template/handoffs/release_notes.md +51 -0
  104. package/template/handoffs/release_queue.md +47 -0
  105. package/template/handoffs/release_to_dev.md +31 -0
  106. package/template/handoffs/releases/Sxxxx-release-notes.md +62 -0
  107. package/template/handoffs/resume_brief.md +14 -0
  108. package/{handoffs → template/handoffs}/tl_to_dev.md +0 -2
  109. package/template/handoffs/token_cost_runs/README.md +26 -0
  110. package/template/its_magic/.its-magic-version +1 -0
  111. package/template/its_magic/README.md +9 -0
  112. package/template/scripts/auto_outer_driver.py +521 -0
  113. package/template/scripts/caveman_compress_input.py +903 -0
  114. package/template/scripts/check_downstream_ci_guard.py +67 -0
  115. package/template/scripts/check_intake_template_parity.py +284 -0
  116. package/template/scripts/check_token_cost_parity.py +69 -0
  117. package/template/scripts/dev_environment_lib.py +463 -0
  118. package/template/scripts/doc_profile_lib.py +415 -0
  119. package/template/scripts/downstream_ci_guard_lib.py +222 -0
  120. package/template/scripts/enforce-triad-hot-surface.py +753 -0
  121. package/template/scripts/guard_installer_publish.py +88 -0
  122. package/template/scripts/intake_bug_resume_brief_refresh.py +303 -0
  123. package/template/scripts/intake_bug_routing_guard.py +67 -0
  124. package/template/scripts/intake_evidence_lib.py +802 -0
  125. package/template/scripts/intake_evidence_validate.py +73 -0
  126. package/template/scripts/materialize_codebase_map.py +184 -0
  127. package/template/scripts/pack_json_validate.py +130 -0
  128. package/template/scripts/project_readme_coverage_lib.py +417 -0
  129. package/template/scripts/readme_feature_coverage_lib.py +608 -0
  130. package/template/scripts/remote_config_summary.py +243 -0
  131. package/template/scripts/sync_push_gates.py +198 -0
  132. package/template/scripts/token_cost_compare.py +40 -0
  133. package/template/scripts/token_cost_lib.py +108 -0
  134. package/template/scripts/uat_probe_lib.py +868 -0
  135. package/template/scripts/validate-and-push.ps1 +280 -0
  136. package/template/scripts/validate-and-push.sh +243 -0
  137. package/template/scripts/validate_doc_profile.py +103 -0
  138. package/template/scripts/validate_project_readme_coverage.py +151 -0
  139. package/template/scripts/validate_readme_feature_coverage.py +140 -0
  140. package/template/sprints/S0001/progress.md +1 -0
  141. package/template/sprints/S0001/qa-findings.md +9 -0
  142. package/template/sprints/S0001/release-findings.md +24 -0
  143. package/template/sprints/S0001/sprint.md +9 -0
  144. package/template/sprints/S0001/summary.md +7 -0
  145. package/template/sprints/S0001/tasks.md +1 -0
  146. package/template/sprints/S0001/uat.json +6 -0
  147. package/template/sprints/S0001/uat.md +5 -0
  148. package/template/sprints/quick/Q0001/summary.md +1 -0
  149. package/{sprints → template/sprints}/quick/Q0001/task.json +0 -1
  150. package/.cursor/agents/curator.mdc +0 -21
  151. package/.cursor/agents/dev.mdc +0 -20
  152. package/.cursor/agents/po.mdc +0 -19
  153. package/.cursor/agents/qa.mdc +0 -19
  154. package/.cursor/agents/release.mdc +0 -19
  155. package/.cursor/agents/tech-lead.mdc +0 -21
  156. package/.cursor/commands/gsd-architecture.md +0 -29
  157. package/.cursor/commands/gsd-auto.md +0 -27
  158. package/.cursor/commands/gsd-discovery.md +0 -27
  159. package/.cursor/commands/gsd-execute.md +0 -32
  160. package/.cursor/commands/gsd-intake.md +0 -28
  161. package/.cursor/commands/gsd-map-codebase.md +0 -25
  162. package/.cursor/commands/gsd-milestone-complete.md +0 -24
  163. package/.cursor/commands/gsd-milestone-start.md +0 -26
  164. package/.cursor/commands/gsd-pause.md +0 -25
  165. package/.cursor/commands/gsd-plan-verify.md +0 -26
  166. package/.cursor/commands/gsd-qa.md +0 -28
  167. package/.cursor/commands/gsd-quick.md +0 -24
  168. package/.cursor/commands/gsd-refresh-context.md +0 -26
  169. package/.cursor/commands/gsd-release.md +0 -29
  170. package/.cursor/commands/gsd-research.md +0 -28
  171. package/.cursor/commands/gsd-resume.md +0 -26
  172. package/.cursor/commands/gsd-sprint-plan.md +0 -30
  173. package/.cursor/commands/gsd-verify-work.md +0 -25
  174. package/.cursor/hooks.json +0 -26
  175. package/.cursor/plans/cursor-gsd-team-kit_8cfee9b8.plan.md +0 -57
  176. package/.cursor/remote.json +0 -18
  177. package/.cursor/rules/gsd-core.mdc +0 -18
  178. package/.cursor/rules/gsd-handoffs.mdc +0 -10
  179. package/.cursor/rules/gsd-quality.mdc +0 -15
  180. package/.cursor/scratchpad.md +0 -34
  181. package/.github/workflows/ci.yml +0 -47
  182. package/decisions/DEC-0001.md +0 -21
  183. package/decisions/DEC-0002.md +0 -21
  184. package/docs/engineering/architecture.md +0 -354
  185. package/docs/engineering/decisions.md +0 -6
  186. package/docs/engineering/research.md +0 -11
  187. package/docs/engineering/runbook.md +0 -32
  188. package/docs/engineering/state.md +0 -33
  189. package/docs/product/acceptance.md +0 -6
  190. package/docs/product/backlog.md +0 -7
  191. package/docs/product/vision.md +0 -46
  192. package/gsd-installer.ps1 +0 -189
  193. package/gsd-installer.py +0 -195
  194. package/gsd-installer.sh +0 -201
  195. package/handoffs/release_notes.md +0 -14
  196. package/handoffs/resume_brief.md +0 -8
  197. package/milestones/M0001/milestone.json +0 -7
  198. package/milestones/M0001/phases.json +0 -9
  199. package/milestones/M0001/progress.md +0 -3
  200. package/milestones/M0001/summary.md +0 -3
  201. package/scripts/generate-release-notes.ps1 +0 -74
  202. package/scripts/generate-release-notes.sh +0 -63
  203. package/scripts/release-all.ps1 +0 -423
  204. package/scripts/release-all.sh +0 -226
  205. package/sprints/S0001/progress.md +0 -4
  206. package/sprints/S0001/qa-findings.md +0 -113
  207. package/sprints/S0001/sprint.md +0 -70
  208. package/sprints/S0001/summary.md +0 -46
  209. package/sprints/S0001/tasks.md +0 -35
  210. package/sprints/S0001/uat.json +0 -8
  211. package/sprints/S0001/uat.md +0 -8
  212. package/sprints/quick/Q0001/summary.md +0 -3
  213. /package/{.cursor/rules/gsd-escalation.mdc → template/.cursor/rules/escalation.mdc} +0 -0
  214. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.json +0 -0
  215. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.md +0 -0
  216. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.json +0 -0
  217. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.md +0 -0
  218. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.json +0 -0
  219. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.md +0 -0
  220. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.json +0 -0
  221. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.md +0 -0
  222. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/milestone.json +0 -0
  223. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/phase-context.json +0 -0
  224. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/plan-verify.json +0 -0
  225. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.json +0 -0
  226. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.md +0 -0
  227. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.json +0 -0
  228. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.md +0 -0
  229. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/uat.json +0 -0
  230. /package/{docs → template/docs}/engineering/context/phase-template.json +0 -0
  231. /package/{docs → template/docs}/engineering/dependencies.json +0 -0
  232. /package/{sprints → template/sprints}/S0001/plan-verify.json +0 -0
@@ -0,0 +1,463 @@
1
+ """
2
+ Dev environment auto-launch profile: load, detect, classify, relaunch, connect (US-0098 / DEC-0084).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import fnmatch
9
+ import json
10
+ import os
11
+ import re
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional, Tuple
16
+
17
+ SCHEMA_VERSION = 1
18
+ DEFAULT_PROFILE_PATH = ".cursor/dev-environment.json"
19
+ MAX_RETRY_COUNT = 2
20
+ RETRY_DELAYS_SECONDS = (5, 15)
21
+
22
+ VALID_DETECTED_MODES = frozenset({"local", "docker-host-local", "docker", "ssh"})
23
+ VALID_TIERS = frozenset({"A", "B", "C"})
24
+ CONNECT_STANDARD_KEYS = frozenset({"endpoint", "health_path"})
25
+ SECRET_LIKE_PATTERN = re.compile(
26
+ r"(?i)(password|secret|token|api[_-]?key|private[_-]?key)\s*[:=]\s*['\"]?[^'\"]{8,}"
27
+ )
28
+
29
+ # DEV_ENV_PROFILE_* reason codes (DEC-0084 §10)
30
+ DEV_ENV_PROFILE_DISABLED = "DEV_ENV_PROFILE_DISABLED"
31
+ DEV_ENV_PROFILE_INVALID = "DEV_ENV_PROFILE_INVALID"
32
+ DEV_ENV_PROFILE_MISSING = "DEV_ENV_PROFILE_MISSING"
33
+ DEV_ENV_DETECT_AMBIGUOUS = "DEV_ENV_DETECT_AMBIGUOUS"
34
+ DEV_ENV_COMPOSE_UNRESOLVED = "DEV_ENV_COMPOSE_UNRESOLVED"
35
+ DEV_ENV_TARGET_DISABLED = "DEV_ENV_TARGET_DISABLED"
36
+ DEV_ENV_SECRET_SURFACE_VIOLATION = "DEV_ENV_SECRET_SURFACE_VIOLATION"
37
+
38
+ # DEV_ENV_RELAUNCH_* reason codes (DEC-0084 §10)
39
+ DEV_ENV_RELAUNCH_SKIPPED_NO_SURFACE = "DEV_ENV_RELAUNCH_SKIPPED_NO_SURFACE"
40
+ DEV_ENV_RELAUNCH_SKIPPED_PROFILE_OFF = "DEV_ENV_RELAUNCH_SKIPPED_PROFILE_OFF"
41
+ DEV_ENV_RELAUNCH_FAILED = "DEV_ENV_RELAUNCH_FAILED"
42
+ DEV_ENV_RELAUNCH_RETRY_EXHAUSTED = "DEV_ENV_RELAUNCH_RETRY_EXHAUSTED"
43
+ DEV_ENV_RELAUNCH_TIMEOUT = "DEV_ENV_RELAUNCH_TIMEOUT"
44
+ DEV_ENV_CONNECT_UNAVAILABLE = "DEV_ENV_CONNECT_UNAVAILABLE"
45
+
46
+ PROFILE_REASON_CODES = (
47
+ DEV_ENV_PROFILE_DISABLED,
48
+ DEV_ENV_PROFILE_INVALID,
49
+ DEV_ENV_PROFILE_MISSING,
50
+ DEV_ENV_DETECT_AMBIGUOUS,
51
+ DEV_ENV_COMPOSE_UNRESOLVED,
52
+ DEV_ENV_TARGET_DISABLED,
53
+ DEV_ENV_SECRET_SURFACE_VIOLATION,
54
+ )
55
+
56
+ RELAUNCH_REASON_CODES = (
57
+ DEV_ENV_RELAUNCH_SKIPPED_NO_SURFACE,
58
+ DEV_ENV_RELAUNCH_SKIPPED_PROFILE_OFF,
59
+ DEV_ENV_RELAUNCH_FAILED,
60
+ DEV_ENV_RELAUNCH_RETRY_EXHAUSTED,
61
+ DEV_ENV_RELAUNCH_TIMEOUT,
62
+ DEV_ENV_CONNECT_UNAVAILABLE,
63
+ )
64
+
65
+ TIER_A_PATTERNS = (
66
+ "Dockerfile",
67
+ "Dockerfile.*",
68
+ "*Dockerfile",
69
+ "docker-compose*.yml",
70
+ "docker-compose*.yaml",
71
+ "compose.y*ml",
72
+ "package-lock.json",
73
+ "yarn.lock",
74
+ "pnpm-lock.yaml",
75
+ "Pipfile.lock",
76
+ "poetry.lock",
77
+ "go.sum",
78
+ "Cargo.lock",
79
+ "requirements.txt",
80
+ "pyproject.toml",
81
+ "package.json",
82
+ "Gemfile.lock",
83
+ )
84
+
85
+ TIER_B_PATTERNS = (
86
+ "*.env.example",
87
+ "nginx*.conf",
88
+ "traefik*.yml",
89
+ "traefik*.yaml",
90
+ "traefik*.toml",
91
+ "application.y*ml",
92
+ "application.y*aml",
93
+ "scripts/docker/**",
94
+ "docker/**/entrypoint*",
95
+ "docker/**/Dockerfile*",
96
+ )
97
+
98
+ TIER_C_SKIP_PREFIXES = (
99
+ "docs/",
100
+ "handoffs/",
101
+ "sprints/",
102
+ "decisions/",
103
+ "tests/",
104
+ ".cursor/commands/",
105
+ "template/docs/",
106
+ )
107
+
108
+
109
+ def _normalize_path(path: str) -> str:
110
+ return path.replace("\\", "/")
111
+
112
+
113
+ def _check_secret_literals(obj: Any, path: str = "") -> Optional[str]:
114
+ if isinstance(obj, dict):
115
+ for key, value in obj.items():
116
+ full = f"{path}.{key}" if path else key
117
+ if key == "connect" and isinstance(value, dict):
118
+ for ck, cv in value.items():
119
+ if ck not in CONNECT_STANDARD_KEYS and not ck.endswith("Env"):
120
+ return f"connect key must be *Env or standard: {ck}"
121
+ if isinstance(cv, str) and SECRET_LIKE_PATTERN.search(cv):
122
+ return DEV_ENV_SECRET_SURFACE_VIOLATION
123
+ child = _check_secret_literals(value, full)
124
+ if child:
125
+ return child
126
+ elif isinstance(obj, list):
127
+ for i, item in enumerate(obj):
128
+ child = _check_secret_literals(item, f"{path}[{i}]")
129
+ if child:
130
+ return child
131
+ elif isinstance(obj, str):
132
+ if SECRET_LIKE_PATTERN.search(obj):
133
+ return DEV_ENV_SECRET_SURFACE_VIOLATION
134
+ return None
135
+
136
+
137
+ def validate_profile_schema(data: Any) -> List[str]:
138
+ codes: List[str] = []
139
+ if not isinstance(data, dict):
140
+ return [DEV_ENV_PROFILE_INVALID]
141
+
142
+ if data.get("schema_version") != SCHEMA_VERSION:
143
+ codes.append(DEV_ENV_PROFILE_INVALID)
144
+
145
+ mode = data.get("detected_mode")
146
+ if mode is not None and mode not in VALID_DETECTED_MODES:
147
+ codes.append(DEV_ENV_PROFILE_INVALID)
148
+
149
+ connect = data.get("connect")
150
+ if connect is not None:
151
+ if not isinstance(connect, dict):
152
+ codes.append(DEV_ENV_PROFILE_INVALID)
153
+ else:
154
+ for ck in connect:
155
+ if ck not in CONNECT_STANDARD_KEYS and not ck.endswith("Env"):
156
+ codes.append(DEV_ENV_SECRET_SURFACE_VIOLATION)
157
+
158
+ env_refs = data.get("env_refs")
159
+ if env_refs is not None and not isinstance(env_refs, list):
160
+ codes.append(DEV_ENV_PROFILE_INVALID)
161
+
162
+ secret_violation = _check_secret_literals(data)
163
+ if secret_violation == DEV_ENV_SECRET_SURFACE_VIOLATION:
164
+ codes.append(DEV_ENV_SECRET_SURFACE_VIOLATION)
165
+
166
+ return codes
167
+
168
+
169
+ def load_profile(path: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
170
+ """Parse JSON profile; reject inline secrets; names-only schema validation."""
171
+ if not os.path.isfile(path):
172
+ return None, DEV_ENV_PROFILE_MISSING
173
+ try:
174
+ with open(path, "r", encoding="utf-8") as f:
175
+ data = json.load(f)
176
+ except (OSError, json.JSONDecodeError):
177
+ return None, DEV_ENV_PROFILE_INVALID
178
+
179
+ codes = validate_profile_schema(data)
180
+ if codes:
181
+ return None, codes[0]
182
+ return data, None
183
+
184
+
185
+ def _match_tier_patterns(rel_path: str, patterns: Tuple[str, ...]) -> bool:
186
+ norm = _normalize_path(rel_path)
187
+ base = os.path.basename(norm)
188
+ for pat in patterns:
189
+ if fnmatch.fnmatch(norm, pat) or fnmatch.fnmatch(base, pat):
190
+ return True
191
+ if "**" in pat:
192
+ prefix = pat.split("**")[0]
193
+ if norm.startswith(prefix) and fnmatch.fnmatch(norm, pat):
194
+ return True
195
+ return False
196
+
197
+
198
+ def classify_touched_files(paths: List[str]) -> Optional[str]:
199
+ """Return highest matching tier A/B/C or None when no runtime surface matched."""
200
+ highest: Optional[str] = None
201
+ tier_rank = {"A": 3, "B": 2, "C": 1}
202
+ for raw in paths:
203
+ rel = _normalize_path(raw)
204
+ if any(rel.startswith(p) for p in TIER_C_SKIP_PREFIXES):
205
+ if not (_match_tier_patterns(rel, TIER_A_PATTERNS) or _match_tier_patterns(rel, TIER_B_PATTERNS)):
206
+ continue
207
+ if _match_tier_patterns(rel, TIER_A_PATTERNS):
208
+ if highest is None or tier_rank["A"] > tier_rank.get(highest, 0):
209
+ highest = "A"
210
+ elif _match_tier_patterns(rel, TIER_B_PATTERNS):
211
+ if highest is None or tier_rank["B"] > tier_rank.get(highest, 0):
212
+ highest = "B"
213
+ elif not any(rel.startswith(p) for p in TIER_C_SKIP_PREFIXES):
214
+ if highest is None:
215
+ highest = "C"
216
+ return highest
217
+
218
+
219
+ def _read_remote_config(repo: Path) -> Optional[Dict[str, Any]]:
220
+ remote_path = repo / ".cursor" / "remote.json"
221
+ if not remote_path.is_file():
222
+ return None
223
+ try:
224
+ with open(remote_path, "r", encoding="utf-8") as f:
225
+ return json.load(f)
226
+ except (OSError, json.JSONDecodeError):
227
+ return None
228
+
229
+
230
+ def _resolve_us0086_remote_target(
231
+ repo: Path, scratchpad: Dict[str, str]
232
+ ) -> Optional[Tuple[str, Optional[str]]]:
233
+ """US-0086 precedence: remote docker/ssh wins over docker-host-local."""
234
+ if scratchpad.get("AUTO_REMOTE_AUTOMATION_PROFILE", "off") != "deterministic_v1":
235
+ return None
236
+ remote = _read_remote_config(repo)
237
+ if not remote:
238
+ return None
239
+ default_id = remote.get("defaultTarget")
240
+ targets = remote.get("targets") or []
241
+ for target in targets:
242
+ if not isinstance(target, dict):
243
+ continue
244
+ tid = target.get("id")
245
+ if tid != default_id:
246
+ continue
247
+ if not target.get("enabled", False):
248
+ return None, DEV_ENV_TARGET_DISABLED
249
+ ttype = target.get("type")
250
+ if ttype == "docker":
251
+ return "docker", None
252
+ if ttype == "ssh":
253
+ return "ssh", None
254
+ return None
255
+
256
+
257
+ def _compose_resolvable(repo: Path, profile: Optional[Dict[str, Any]]) -> bool:
258
+ candidates: List[Path] = []
259
+ if profile and profile.get("compose_file"):
260
+ candidates.append(repo / profile["compose_file"])
261
+ candidates.extend(
262
+ [
263
+ repo / "docker-compose.yml",
264
+ repo / "docker-compose.yaml",
265
+ repo / "compose.yml",
266
+ repo / "compose.yaml",
267
+ ]
268
+ )
269
+ return any(p.is_file() for p in candidates)
270
+
271
+
272
+ def _local_docker_cli_succeeds() -> bool:
273
+ try:
274
+ proc = subprocess.run(
275
+ ["docker", "info"],
276
+ capture_output=True,
277
+ text=True,
278
+ timeout=5,
279
+ check=False,
280
+ )
281
+ return proc.returncode == 0
282
+ except (OSError, subprocess.TimeoutExpired):
283
+ return False
284
+
285
+
286
+ def _dev_server_inferable(scratchpad: Dict[str, str]) -> bool:
287
+ cmd = (scratchpad.get("DEV_SERVER_COMMAND") or "").strip()
288
+ port = (scratchpad.get("DEV_SERVER_PORT") or "").strip()
289
+ return bool(cmd or port)
290
+
291
+
292
+ def detect_mode(
293
+ repo: Path,
294
+ profile: Optional[Dict[str, Any]],
295
+ scratchpad: Dict[str, str],
296
+ ) -> Tuple[Optional[str], Optional[str]]:
297
+ """
298
+ Detection precedence (DEC-0084 §3):
299
+ profile off -> skip; US-0086 remote wins; compose+local docker -> docker-host-local;
300
+ DEV_SERVER_* -> local; else DEV_ENV_DETECT_AMBIGUOUS.
301
+ """
302
+ if scratchpad.get("DEV_AUTO_LAUNCH_PROFILE", "off") == "off":
303
+ return None, DEV_ENV_PROFILE_DISABLED
304
+
305
+ remote_result = _resolve_us0086_remote_target(repo, scratchpad)
306
+ if remote_result is not None:
307
+ if isinstance(remote_result, tuple) and len(remote_result) == 2:
308
+ mode, reason = remote_result
309
+ if mode:
310
+ return mode, reason
311
+ if reason:
312
+ return None, reason
313
+
314
+ if _compose_resolvable(repo, profile) and _local_docker_cli_succeeds():
315
+ return "docker-host-local", None
316
+
317
+ if _dev_server_inferable(scratchpad):
318
+ return "local", None
319
+
320
+ if profile and profile.get("detected_mode") in VALID_DETECTED_MODES:
321
+ return profile["detected_mode"], None
322
+
323
+ return None, DEV_ENV_DETECT_AMBIGUOUS
324
+
325
+
326
+ def build_relaunch_plan(
327
+ mode: str,
328
+ tier: Optional[str],
329
+ profile: Dict[str, Any],
330
+ ) -> List[str]:
331
+ """Command list for tier recipe; no .env reads; retry_count max 2 enforced by caller."""
332
+ if tier is None:
333
+ return []
334
+ service = profile.get("service") or "app"
335
+ compose_file = profile.get("compose_file") or "docker-compose.yml"
336
+ commands: List[str] = []
337
+ if tier == "A":
338
+ if mode in ("docker-host-local", "docker"):
339
+ commands = [
340
+ f"docker compose -f {compose_file} build {service}",
341
+ f"docker compose -f {compose_file} up -d {service}",
342
+ ]
343
+ elif mode == "local":
344
+ cmd = profile.get("rebuild_recipe", {}).get("local_build_command")
345
+ if cmd:
346
+ commands = [cmd]
347
+ elif tier == "B":
348
+ if mode in ("docker-host-local", "docker"):
349
+ commands = [f"docker compose -f {compose_file} restart {service}"]
350
+ elif mode == "local":
351
+ cmd = profile.get("rebuild_recipe", {}).get("restart_command")
352
+ if cmd:
353
+ commands = [cmd]
354
+ elif tier == "C":
355
+ if mode == "local":
356
+ cmd = profile.get("rebuild_recipe", {}).get("dev_server_command")
357
+ if not cmd:
358
+ cmd = os.environ.get("DEV_SERVER_COMMAND", "").strip()
359
+ if cmd:
360
+ commands = [cmd]
361
+ elif mode == "docker-host-local":
362
+ restart_on_change = profile.get("rebuild_recipe", {}).get(
363
+ "restart_on_source_change", False
364
+ )
365
+ if restart_on_change:
366
+ commands = [f"docker compose -f {compose_file} restart {service}"]
367
+ return commands
368
+
369
+
370
+ def format_connect_block(profile: Dict[str, Any], outcome: str) -> str:
371
+ """Markdown Connect block with mandatory names-only fields."""
372
+ connect = profile.get("connect") or {}
373
+ env_refs = sorted(set(profile.get("env_refs") or []))
374
+ lines = [
375
+ "## Connect",
376
+ "",
377
+ f"- **runtime_mode**: {profile.get('detected_mode', 'unknown')}",
378
+ f"- **connect_endpoint**: {connect.get('endpoint', '(unset)')}",
379
+ f"- **health_path**: {connect.get('health_path', '/')}",
380
+ f"- **service_id**: {profile.get('service', '')}",
381
+ f"- **container_id**: (resolve via operator shell)",
382
+ f"- **target_id**: {profile.get('target_id', '')}",
383
+ f"- **env_refs**: {', '.join(env_refs) if env_refs else '(none)'}",
384
+ f"- **relaunch_outcome**: {outcome}",
385
+ "",
386
+ ]
387
+ return "\n".join(lines)
388
+
389
+
390
+ def run_self_test() -> int:
391
+ fixture_path = Path(__file__).resolve().parent.parent / "template" / ".cursor" / "dev-environment.json.example"
392
+ data, err = load_profile(str(fixture_path))
393
+ if err or not data:
394
+ print(f"DEV_ENV_SELF_TEST_FAIL: load_profile {err}", file=sys.stderr)
395
+ return 1
396
+
397
+ tier_a = classify_touched_files(["docker-compose.yml", "docs/foo.md"])
398
+ if tier_a != "A":
399
+ print(f"DEV_ENV_SELF_TEST_FAIL: tier A expected, got {tier_a}", file=sys.stderr)
400
+ return 1
401
+
402
+ tier_none = classify_touched_files(["docs/engineering/runbook.md"])
403
+ if tier_none is not None:
404
+ print(f"DEV_ENV_SELF_TEST_FAIL: docs should skip, got {tier_none}", file=sys.stderr)
405
+ return 1
406
+
407
+ scratchpad = {"DEV_AUTO_LAUNCH_PROFILE": "off"}
408
+ mode, reason = detect_mode(Path("."), data, scratchpad)
409
+ if mode is not None or reason != DEV_ENV_PROFILE_DISABLED:
410
+ print(f"DEV_ENV_SELF_TEST_FAIL: off gate {mode}/{reason}", file=sys.stderr)
411
+ return 1
412
+
413
+ block = format_connect_block(data, "success")
414
+ for field in (
415
+ "runtime_mode",
416
+ "connect_endpoint",
417
+ "health_path",
418
+ "service_id",
419
+ "container_id",
420
+ "target_id",
421
+ "env_refs",
422
+ "relaunch_outcome",
423
+ ):
424
+ if field not in block:
425
+ print(f"DEV_ENV_SELF_TEST_FAIL: missing {field}", file=sys.stderr)
426
+ return 1
427
+
428
+ plan = build_relaunch_plan("docker-host-local", "A", data)
429
+ if not plan or "docker compose" not in plan[0]:
430
+ print("DEV_ENV_SELF_TEST_FAIL: relaunch plan", file=sys.stderr)
431
+ return 1
432
+
433
+ if MAX_RETRY_COUNT != 2:
434
+ print("DEV_ENV_SELF_TEST_FAIL: retry_count", file=sys.stderr)
435
+ return 1
436
+
437
+ print("[DEV_ENVIRONMENT_SELF_TEST_OK]")
438
+ return 0
439
+
440
+
441
+ def main() -> int:
442
+ p = argparse.ArgumentParser(description="Dev environment profile helper (US-0098).")
443
+ p.add_argument("--self-test", action="store_true", help="Run built-in checks.")
444
+ p.add_argument("--load", metavar="PATH", help="Load and validate profile path.")
445
+ args = p.parse_args()
446
+
447
+ if args.self_test:
448
+ return run_self_test()
449
+
450
+ if args.load:
451
+ _, err = load_profile(args.load)
452
+ if err:
453
+ print(err, file=sys.stderr)
454
+ return 1
455
+ print("[DEV_ENVIRONMENT_LOAD_OK]")
456
+ return 0
457
+
458
+ p.print_help()
459
+ return 0
460
+
461
+
462
+ if __name__ == "__main__":
463
+ sys.exit(main())