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