bmad-module-ultracode-goal 0.1.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 (47) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.gitattributes +16 -0
  3. package/.nvmrc +1 -0
  4. package/LICENSE +27 -0
  5. package/README.md +200 -0
  6. package/docs/_internal/RELEASING.md +101 -0
  7. package/docs/_internal/STABILITY.md +62 -0
  8. package/docs/architecture.md +77 -0
  9. package/docs/assets/ucg-logo.svg +73 -0
  10. package/docs/gate-model.md +109 -0
  11. package/docs/getting-started.md +51 -0
  12. package/docs/health-check.md +61 -0
  13. package/docs/how-it-works.md +73 -0
  14. package/docs/index.md +25 -0
  15. package/docs/parallel-mode.md +33 -0
  16. package/docs/troubleshooting.md +56 -0
  17. package/docs/why-ultracode-goal.md +34 -0
  18. package/package.json +100 -0
  19. package/skills/.gitkeep +0 -0
  20. package/skills/module.yaml +12 -0
  21. package/skills/ultracode-goal/SKILL.md +75 -0
  22. package/skills/ultracode-goal/assets/execute-epic.workflow.js +208 -0
  23. package/skills/ultracode-goal/customize.toml +47 -0
  24. package/skills/ultracode-goal/references/define-done.md +41 -0
  25. package/skills/ultracode-goal/references/execute.md +90 -0
  26. package/skills/ultracode-goal/references/finalize.md +64 -0
  27. package/skills/ultracode-goal/references/gate.md +70 -0
  28. package/skills/ultracode-goal/references/health-check.md +332 -0
  29. package/skills/ultracode-goal/references/ingest-and-scope.md +46 -0
  30. package/skills/ultracode-goal/references/preflight.md +100 -0
  31. package/skills/ultracode-goal/scripts/gate_eval.py +280 -0
  32. package/skills/ultracode-goal/scripts/health_check_fp.py +196 -0
  33. package/skills/ultracode-goal/scripts/hooks/budget_stop.py +162 -0
  34. package/skills/ultracode-goal/scripts/hooks/guard_pretooluse.py +174 -0
  35. package/skills/ultracode-goal/scripts/preflight_check.py +399 -0
  36. package/tools/cli/commands/install.js +36 -0
  37. package/tools/cli/commands/status.js +161 -0
  38. package/tools/cli/commands/uninstall.js +175 -0
  39. package/tools/cli/commands/update.js +65 -0
  40. package/tools/cli/lib/ide-skills.js +218 -0
  41. package/tools/cli/lib/installer.js +226 -0
  42. package/tools/cli/lib/manifest.js +100 -0
  43. package/tools/cli/lib/platform-codes.yaml +223 -0
  44. package/tools/cli/lib/ui.js +353 -0
  45. package/tools/cli/lib/version-check.js +86 -0
  46. package/tools/cli/ucg-cli.js +45 -0
  47. package/tools/ucg-npx-wrapper.js +36 -0
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = []
5
+ # ///
6
+ """UltraCode-Goal PreToolUse guard (Claude Code hook).
7
+
8
+ Enforces two invariants that must NOT live in memory (context, not enforcement):
9
+ 1. No `git commit`/`git push` while on a protected branch.
10
+ 2. No `git commit` until a "tests-ran" marker exists for the current story.
11
+
12
+ Hook contract (reads one JSON object on stdin):
13
+ in : {tool_name, tool_input:{command,...}, cwd, ...}
14
+ out: exit 0 + JSON {hookSpecificOutput:{hookEventName,permissionDecision,
15
+ permissionDecisionReason}} where permissionDecision is "deny" to block.
16
+ Defensive fallback: also exit 2 with the reason on stderr (older clients
17
+ honor exit-code-2-blocks even when they ignore the JSON).
18
+
19
+ Config resolution (all optional, env wins so the conductor can inject per run):
20
+ ULTRACODE_PROTECTED_BRANCHES comma-separated; default "main,master"
21
+ ULTRACODE_IMPL_ARTIFACTS dir holding run state (story id + markers)
22
+ ULTRACODE_STORY_ID current story id; else read from
23
+ <impl_artifacts>/.current-story
24
+ Marker file checked: <impl_artifacts>/.tests-ran-<story_id>
25
+ """
26
+
27
+ import json
28
+ import os
29
+ import re
30
+ import subprocess
31
+ import sys
32
+ from pathlib import Path
33
+
34
+ DEFAULT_PROTECTED = ["main", "master"]
35
+
36
+
37
+ def _read_event() -> dict:
38
+ try:
39
+ raw = sys.stdin.read()
40
+ return json.loads(raw) if raw.strip() else {}
41
+ except (json.JSONDecodeError, ValueError):
42
+ return {}
43
+
44
+
45
+ def _allow() -> None:
46
+ """No decision needed: stay silent, let the normal permission flow run."""
47
+ sys.exit(0)
48
+
49
+
50
+ def _deny(reason: str) -> None:
51
+ print(
52
+ json.dumps(
53
+ {
54
+ "hookSpecificOutput": {
55
+ "hookEventName": "PreToolUse",
56
+ "permissionDecision": "deny",
57
+ "permissionDecisionReason": reason,
58
+ }
59
+ }
60
+ )
61
+ )
62
+ # Belt-and-suspenders: clients that ignore JSON still block on exit 2.
63
+ print(reason, file=sys.stderr)
64
+ sys.exit(2)
65
+
66
+
67
+ def _protected_branches() -> list[str]:
68
+ env = os.environ.get("ULTRACODE_PROTECTED_BRANCHES")
69
+ if env:
70
+ return [b.strip() for b in env.split(",") if b.strip()]
71
+ return DEFAULT_PROTECTED
72
+
73
+
74
+ def _current_branch(cwd: str | None) -> str | None:
75
+ try:
76
+ out = subprocess.run(
77
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
78
+ cwd=cwd or None,
79
+ capture_output=True,
80
+ text=True,
81
+ timeout=10,
82
+ )
83
+ except (OSError, subprocess.SubprocessError):
84
+ return None
85
+ branch = out.stdout.strip()
86
+ return branch or None
87
+
88
+
89
+ def _impl_artifacts(cwd: str | None) -> Path | None:
90
+ env = os.environ.get("ULTRACODE_IMPL_ARTIFACTS")
91
+ if env:
92
+ return Path(env)
93
+ if cwd:
94
+ return Path(cwd) / "_bmad-output" / "implementation-artifacts"
95
+ return None
96
+
97
+
98
+ def _current_story(impl: Path | None) -> str | None:
99
+ sid = os.environ.get("ULTRACODE_STORY_ID")
100
+ if sid:
101
+ return sid.strip()
102
+ if impl is not None:
103
+ marker = impl / ".current-story"
104
+ if marker.is_file():
105
+ try:
106
+ return marker.read_text(encoding="utf-8").strip() or None
107
+ except OSError:
108
+ return None
109
+ return None
110
+
111
+
112
+ # git verbs that write history. `git commit` and `git push` are the targets;
113
+ # a trailing word boundary keeps `git commit-tree`-style false positives out.
114
+ _GIT_WRITE = re.compile(
115
+ r"\bgit\b[^\n;&|]*?\b(?P<verb>commit|push)\b", re.IGNORECASE
116
+ )
117
+
118
+
119
+ def _git_writes(command: str) -> set[str]:
120
+ """Return {'commit','push'} subset the command would perform.
121
+
122
+ Scans each shell-segment so a chained `git add && git commit` is caught.
123
+ """
124
+ verbs: set[str] = set()
125
+ for segment in re.split(r"&&|\|\||;|\|", command):
126
+ m = _GIT_WRITE.search(segment)
127
+ if m and re.search(r"\bgit\b", segment):
128
+ verbs.add(m.group("verb").lower())
129
+ return verbs
130
+
131
+
132
+ def main() -> None:
133
+ event = _read_event()
134
+ if event.get("tool_name") != "Bash":
135
+ _allow()
136
+
137
+ command = (event.get("tool_input") or {}).get("command") or ""
138
+ if not isinstance(command, str):
139
+ _allow()
140
+
141
+ verbs = _git_writes(command)
142
+ if not verbs:
143
+ _allow()
144
+
145
+ cwd = event.get("cwd")
146
+ protected = _protected_branches()
147
+ branch = _current_branch(cwd)
148
+
149
+ if branch is not None and branch in protected:
150
+ _deny(
151
+ f"Protected-branch guard: refusing `git {'/'.join(sorted(verbs))}` "
152
+ f"on '{branch}'. UltraCode-Goal commits one green story per commit "
153
+ f"on an epic branch ('{os.environ.get('ULTRACODE_EPIC_BRANCH_PREFIX', 'ultracode/epic-')}<id>'), "
154
+ f"never on {protected}. Switch to the epic branch first."
155
+ )
156
+
157
+ if "commit" in verbs:
158
+ impl = _impl_artifacts(cwd)
159
+ story = _current_story(impl)
160
+ marker = (impl / f".tests-ran-{story}") if (impl and story) else None
161
+ if marker is None or not marker.is_file():
162
+ target = str(marker) if marker else "<impl-artifacts>/.tests-ran-<story>"
163
+ _deny(
164
+ "Tests-ran guard: refusing `git commit` — no tests-ran marker "
165
+ f"for the current story ({story or 'unknown'}). Run the story's "
166
+ f"test/lint/build to green and write {target} before committing. "
167
+ "Commit only at a verified-green story boundary."
168
+ )
169
+
170
+ _allow()
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = []
5
+ # ///
6
+ """Mechanical preflight for an UltraCode-Goal epic run.
7
+
8
+ Plumbing ONLY. This script parses tool versions, inspects git state, checks
9
+ file/directory existence, and reads TEA config flags. It reports mechanical
10
+ facts plus a `budget` count of mechanical blockers. It does NOT decide whether
11
+ to intervene, remediate, or block on semantic grounds — that judgment lives in
12
+ references/preflight.md (the LLM). The "auto-remediation then hard-gate"
13
+ posture (decision D2') is the LLM's to run; this script only tells it what is
14
+ mechanically true right now.
15
+
16
+ What counts as a mechanical blocker (each adds 1 to `budget`):
17
+ - the Claude Code primitives needed for an unattended run are below the
18
+ minimum versions the run depends on (`/goal`, dynamic workflows, auto memory),
19
+ - the test framework is not scaffolded (no playwright/cypress/jest config),
20
+ - the working tree is dirty (a per-green-story commit needs a clean base),
21
+ - the current branch is a protected branch (the epic must run on its own branch).
22
+
23
+ `green` is true iff `budget == 0`. A green preflight means there is nothing
24
+ MECHANICAL left to clear; the LLM still owns the true-RED hard gate
25
+ (undecided architecture/product, unresolvable secrets), which this script
26
+ cannot and does not evaluate.
27
+
28
+ Severity is advisory metadata for the LLM, not a gate. `remediable` flags
29
+ blockers the remediation pass can plausibly auto-clear (e.g. scaffold the
30
+ framework, branch off, commit/stash) vs. ones that need a human (none here are
31
+ inherently un-remediable; true-RED items never reach this script).
32
+
33
+ Version gates (from the grounded constraints in .decision-log.md):
34
+ /goal >= 2.1.139, dynamic workflows >= 2.1.154, auto memory >= 2.1.59.
35
+
36
+ Output: JSON to stdout. Exit 0 whenever a payload is produced (a non-green
37
+ preflight is a valid result, not an error). Exit 2 is reserved for invocation
38
+ errors where no useful payload can be produced.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import argparse
44
+ import json
45
+ import re
46
+ import subprocess
47
+ import sys
48
+ from pathlib import Path
49
+
50
+ # Minimum Claude Code versions the unattended run depends on.
51
+ MIN_GOAL = (2, 1, 139)
52
+ MIN_WORKFLOWS = (2, 1, 154)
53
+ MIN_AUTOMEMORY = (2, 1, 59)
54
+
55
+ # Filenames that signal a scaffolded test framework (playwright / cypress / jest).
56
+ FRAMEWORK_MARKERS = (
57
+ "playwright.config.ts",
58
+ "playwright.config.js",
59
+ "playwright.config.mjs",
60
+ "cypress.config.ts",
61
+ "cypress.config.js",
62
+ "cypress.config.mjs",
63
+ "cypress.json",
64
+ "jest.config.ts",
65
+ "jest.config.js",
66
+ "vitest.config.ts",
67
+ "vitest.config.js",
68
+ )
69
+
70
+ # TEA test-artifact subdirectories the gates write into (relative to test_artifacts root).
71
+ TEA_ARTIFACT_DIRS = ("test-design", "test-reviews", "traceability")
72
+
73
+ # TEA config flags worth surfacing to the LLM verbatim (mechanical read, no interpretation).
74
+ TEA_FLAG_KEYS = (
75
+ "test_execution_mode",
76
+ "tea_execution_mode",
77
+ "test_framework",
78
+ "test_stack_type",
79
+ "ci_platform",
80
+ "risk_threshold",
81
+ "tea_browser_automation",
82
+ "tea_capability_probe",
83
+ "test_artifacts",
84
+ )
85
+
86
+ # Default protected branches when the caller doesn't override (mirrors customize.toml).
87
+ DEFAULT_PROTECTED = ("main", "master")
88
+
89
+
90
+ def _run(cmd: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
91
+ """Run a command, returning (returncode, stdout, stderr). 127 if not found."""
92
+ try:
93
+ proc = subprocess.run(
94
+ cmd,
95
+ cwd=str(cwd) if cwd else None,
96
+ capture_output=True,
97
+ text=True,
98
+ timeout=30,
99
+ )
100
+ return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
101
+ except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
102
+ return 127, "", str(exc)
103
+
104
+
105
+ def _parse_semver(text: str) -> tuple[int, int, int] | None:
106
+ """Extract the first dotted three-part version from text."""
107
+ match = re.search(r"(\d+)\.(\d+)\.(\d+)", text or "")
108
+ if not match:
109
+ return None
110
+ return tuple(int(g) for g in match.groups()) # type: ignore[return-value]
111
+
112
+
113
+ def _cc_version() -> tuple[str | None, tuple[int, int, int] | None]:
114
+ """Best-effort read of the installed Claude Code version."""
115
+ code, out, _ = _run(["claude", "--version"])
116
+ if code != 0:
117
+ return None, None
118
+ return out, _parse_semver(out)
119
+
120
+
121
+ def _meets(version: tuple[int, int, int] | None, minimum: tuple[int, int, int]) -> bool:
122
+ return version is not None and version >= minimum
123
+
124
+
125
+ def _git(args: list[str], project_root: Path) -> tuple[int, str]:
126
+ code, out, _ = _run(["git", *args], cwd=project_root)
127
+ return code, out
128
+
129
+
130
+ def _git_branch(project_root: Path) -> str | None:
131
+ code, out = _git(["rev-parse", "--abbrev-ref", "HEAD"], project_root)
132
+ if code != 0 or not out:
133
+ return None
134
+ return out
135
+
136
+
137
+ def _git_clean(project_root: Path) -> bool | None:
138
+ code, out = _git(["status", "--porcelain"], project_root)
139
+ if code != 0:
140
+ return None
141
+ return out == ""
142
+
143
+
144
+ def _read_toml_or_yaml_flags(tea_config: Path) -> dict:
145
+ """Read flat key: value scalars from the TEA config.
146
+
147
+ The TEA config is YAML in practice. To stay stdlib-only we parse the flat
148
+ `key: value` scalar lines we care about ourselves (no nesting is needed for
149
+ these flags). If `tomllib` ever applies (a .toml config) we try that first.
150
+ """
151
+ if not tea_config.is_file():
152
+ return {}
153
+ text = tea_config.read_text(encoding="utf-8", errors="replace")
154
+
155
+ if tea_config.suffix.lower() == ".toml":
156
+ try:
157
+ import tomllib
158
+
159
+ data = tomllib.loads(text)
160
+ return {k: data[k] for k in TEA_FLAG_KEYS if k in data}
161
+ except Exception:
162
+ pass
163
+
164
+ flags: dict = {}
165
+ for raw in text.splitlines():
166
+ line = raw.strip()
167
+ if not line or line.startswith("#") or ":" not in line:
168
+ continue
169
+ key, _, value = line.partition(":")
170
+ key = key.strip()
171
+ if key not in TEA_FLAG_KEYS:
172
+ continue
173
+ value = value.strip().strip('"').strip("'")
174
+ if value.lower() in ("true", "false"):
175
+ flags[key] = value.lower() == "true"
176
+ else:
177
+ flags[key] = value
178
+ return flags
179
+
180
+
181
+ def _tea_artifacts_root(project_root: Path, tea_flags: dict) -> Path:
182
+ """Resolve the test_artifacts root, honoring a tea-config override if present."""
183
+ configured = tea_flags.get("test_artifacts")
184
+ if isinstance(configured, str) and configured:
185
+ resolved = configured.replace("{project-root}", str(project_root))
186
+ path = Path(resolved)
187
+ return path if path.is_absolute() else (project_root / path)
188
+ return project_root / "_bmad-output" / "test-artifacts"
189
+
190
+
191
+ def _framework_present(project_root: Path) -> bool:
192
+ """A framework config at the project root signals a scaffolded test framework."""
193
+ for marker in FRAMEWORK_MARKERS:
194
+ if (project_root / marker).is_file():
195
+ return True
196
+ return False
197
+
198
+
199
+ def _test_artifacts_dirs(artifacts_root: Path) -> dict:
200
+ """Map each TEA artifact subdir to whether it exists."""
201
+ return {name: (artifacts_root / name).is_dir() for name in TEA_ARTIFACT_DIRS}
202
+
203
+
204
+ def _sprint_status_present(project_root: Path, impl_artifacts: Path) -> bool:
205
+ """sprint-status.yaml controls test-design's System/Epic prompt (must exist to auto-run).
206
+
207
+ BMad writes it under the output tree; search the common locations rather
208
+ than hardcoding one path that may drift between installs.
209
+ """
210
+ candidates = [
211
+ impl_artifacts / "sprint-status.yaml",
212
+ project_root / "_bmad-output" / "sprint-status.yaml",
213
+ ]
214
+ for path in candidates:
215
+ if path.is_file():
216
+ return True
217
+ # Fall back to a bounded glob under the output tree.
218
+ output = project_root / "_bmad-output"
219
+ if output.is_dir():
220
+ for hit in output.rglob("sprint-status.yaml"):
221
+ if hit.is_file():
222
+ return True
223
+ return False
224
+
225
+
226
+ def _project_context_count(project_root: Path) -> int:
227
+ """Count project-context.md files (the persistent-facts source)."""
228
+ return sum(1 for _ in project_root.rglob("project-context.md"))
229
+
230
+
231
+ def build_report(
232
+ project_root: Path,
233
+ epic: str,
234
+ tea_config: Path,
235
+ impl_artifacts: Path,
236
+ protected_branches: tuple[str, ...],
237
+ ) -> dict:
238
+ cc_raw, cc_ver = _cc_version()
239
+ goal_ok = _meets(cc_ver, MIN_GOAL)
240
+ workflows_ok = _meets(cc_ver, MIN_WORKFLOWS)
241
+ automemory_ok = _meets(cc_ver, MIN_AUTOMEMORY)
242
+
243
+ git_branch = _git_branch(project_root)
244
+ git_clean = _git_clean(project_root)
245
+
246
+ tea_flags = _read_toml_or_yaml_flags(tea_config)
247
+ artifacts_root = _tea_artifacts_root(project_root, tea_flags)
248
+
249
+ framework_present = _framework_present(project_root)
250
+ test_artifacts_dirs = _test_artifacts_dirs(artifacts_root)
251
+ sprint_status_present = _sprint_status_present(project_root, impl_artifacts)
252
+ project_context_count = _project_context_count(project_root)
253
+
254
+ checks = {
255
+ "cc_version": cc_raw,
256
+ "goal_ok": goal_ok,
257
+ "workflows_ok": workflows_ok,
258
+ "automemory_ok": automemory_ok,
259
+ "git_branch": git_branch,
260
+ "git_clean": git_clean,
261
+ "framework_present": framework_present,
262
+ "test_artifacts_dirs": test_artifacts_dirs,
263
+ "sprint_status_present": sprint_status_present,
264
+ "project_context_count": project_context_count,
265
+ "tea_flags": tea_flags,
266
+ }
267
+
268
+ blockers: list[dict] = []
269
+
270
+ # --- Mechanical blockers (each counts toward the budget) ---
271
+
272
+ # Primitive versions: any one being below minimum blocks the unattended run.
273
+ if not (goal_ok and workflows_ok and automemory_ok):
274
+ below = []
275
+ if not goal_ok:
276
+ below.append("/goal>=%d.%d.%d" % MIN_GOAL)
277
+ if not workflows_ok:
278
+ below.append("workflows>=%d.%d.%d" % MIN_WORKFLOWS)
279
+ if not automemory_ok:
280
+ below.append("auto-memory>=%d.%d.%d" % MIN_AUTOMEMORY)
281
+ detail = (
282
+ "Claude Code %s is below the minimum for: %s"
283
+ % (cc_raw or "(version unreadable)", ", ".join(below))
284
+ )
285
+ blockers.append(
286
+ {
287
+ "id": "cc_version",
288
+ "kind": "version",
289
+ "severity": "high",
290
+ "detail": detail,
291
+ # The script can't upgrade the host; the LLM prompts the user.
292
+ "remediable": False,
293
+ }
294
+ )
295
+
296
+ if not framework_present:
297
+ blockers.append(
298
+ {
299
+ "id": "framework_present",
300
+ "kind": "framework",
301
+ "severity": "medium",
302
+ "detail": "No test framework config found at project root "
303
+ "(playwright/cypress/jest/vitest). ATDD halts without one.",
304
+ # Remediation pass scaffolds via bmad-testarch-framework.
305
+ "remediable": True,
306
+ }
307
+ )
308
+
309
+ if git_clean is False:
310
+ blockers.append(
311
+ {
312
+ "id": "git_clean",
313
+ "kind": "git",
314
+ "severity": "medium",
315
+ "detail": "Working tree is dirty; a per-green-story commit needs a clean base.",
316
+ "remediable": True,
317
+ }
318
+ )
319
+ elif git_clean is None:
320
+ blockers.append(
321
+ {
322
+ "id": "git_repo",
323
+ "kind": "git",
324
+ "severity": "high",
325
+ "detail": "git status failed; project-root may not be a git repository.",
326
+ "remediable": False,
327
+ }
328
+ )
329
+
330
+ if git_branch is not None and git_branch in protected_branches:
331
+ blockers.append(
332
+ {
333
+ "id": "git_branch",
334
+ "kind": "git",
335
+ "severity": "medium",
336
+ "detail": "On protected branch '%s'; the epic must run on its own branch."
337
+ % git_branch,
338
+ "remediable": True,
339
+ }
340
+ )
341
+
342
+ budget = len(blockers)
343
+ return {
344
+ "green": budget == 0,
345
+ "budget": budget,
346
+ "blockers": blockers,
347
+ "checks": checks,
348
+ }
349
+
350
+
351
+ def _resolve(path_arg: str, project_root: Path) -> Path:
352
+ """Expand a possible {project-root} token and resolve relative to project_root."""
353
+ resolved = path_arg.replace("{project-root}", str(project_root))
354
+ path = Path(resolved)
355
+ return path if path.is_absolute() else (project_root / path)
356
+
357
+
358
+ def main(argv: list[str] | None = None) -> int:
359
+ parser = argparse.ArgumentParser(
360
+ description="Mechanical preflight for an UltraCode-Goal epic run."
361
+ )
362
+ parser.add_argument("--project-root", required=True)
363
+ parser.add_argument("--epic", required=True)
364
+ parser.add_argument("--tea-config", required=True)
365
+ parser.add_argument("--impl-artifacts", required=True)
366
+ parser.add_argument(
367
+ "--protected-branch",
368
+ action="append",
369
+ default=None,
370
+ help="Protected branch name; repeatable. Defaults to main, master.",
371
+ )
372
+ args = parser.parse_args(argv)
373
+
374
+ project_root = Path(args.project_root).expanduser()
375
+ if not project_root.is_dir():
376
+ print(
377
+ json.dumps({"error": "project-root not found: %s" % project_root}),
378
+ file=sys.stderr,
379
+ )
380
+ return 2
381
+
382
+ project_root = project_root.resolve()
383
+ tea_config = _resolve(args.tea_config, project_root)
384
+ impl_artifacts = _resolve(args.impl_artifacts, project_root)
385
+ protected = tuple(args.protected_branch) if args.protected_branch else DEFAULT_PROTECTED
386
+
387
+ report = build_report(
388
+ project_root=project_root,
389
+ epic=args.epic,
390
+ tea_config=tea_config,
391
+ impl_artifacts=impl_artifacts,
392
+ protected_branches=protected,
393
+ )
394
+ print(json.dumps(report, indent=2))
395
+ return 0
396
+
397
+
398
+ if __name__ == "__main__":
399
+ sys.exit(main())
@@ -0,0 +1,36 @@
1
+ const chalk = require('chalk');
2
+ const { Installer } = require('../lib/installer');
3
+ const { UI } = require('../lib/ui');
4
+
5
+ module.exports = {
6
+ command: 'install',
7
+ description: 'Install UltraCode Goal into your project',
8
+ options: [],
9
+ action: async () => {
10
+ try {
11
+ const ui = new UI();
12
+ const config = await ui.promptInstall();
13
+
14
+ if (config.cancelled) {
15
+ console.log(chalk.yellow('\nInstallation cancelled.'));
16
+ process.exit(0);
17
+ return;
18
+ }
19
+
20
+ const installer = new Installer();
21
+ const result = await installer.install(config);
22
+
23
+ if (result && result.success) {
24
+ ui.displaySuccess(config.ucgFolder, config.ides, config._action);
25
+ process.exit(0);
26
+ } else {
27
+ console.error(chalk.red('\nInstallation failed.'));
28
+ process.exit(1);
29
+ }
30
+ } catch (error) {
31
+ console.error(chalk.red('\nInstallation failed:'), error.message);
32
+ console.error(chalk.dim(error.stack));
33
+ process.exit(1);
34
+ }
35
+ },
36
+ };