workspace.py python
144 lines 5.5 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 9 days ago
1 """``ok workspace`` commands (§MR.7)."""
2
3 from __future__ import annotations
4
5 from argparse import Namespace
6 from pathlib import Path
7
8 from adapters.config import load_config
9 from adapters.errors import ConfigError
10 from cli.context import CliContext
11 from cli.paths import is_within_repo, resolve_config_path, resolve_repo_root
12 from cli.sanitize import format_config_error
13 from tools.workspace import (
14 EXIT_WORKSPACE_RELAY,
15 WorkspaceLoadError,
16 build_status_report,
17 check_next,
18 load_manifest_for_repo,
19 run_doctor,
20 )
21 from tools.workspace.types import EXIT_CONFIG, EXIT_OK, EXIT_USAGE
22
23
24 def run_workspace(args: Namespace, ctx: CliContext) -> int:
25 """Dispatch ``ok workspace status|check-next|doctor``."""
26 action = getattr(args, "workspace_action", None)
27 if action is None:
28 ctx.output.error("usage: ok workspace status|check-next|doctor")
29 return EXIT_USAGE
30
31 repo_root = resolve_repo_root(cwd=ctx.cwd, repo_arg=args.repo, command="workspace")
32 config_path = resolve_config_path(repo_root, args.config)
33 if not is_within_repo(repo_root, config_path):
34 ctx.output.error("refused: config path outside repo root")
35 return 4
36
37 try:
38 config = load_config(config_path)
39 except ConfigError as exc:
40 ctx.output.error(format_config_error(exc, repo_root))
41 return EXIT_CONFIG
42
43 if action == "status":
44 return _run_status(args, ctx, config, repo_root)
45 if action == "check-next":
46 return _run_check_next(args, ctx, config, repo_root)
47 if action == "doctor":
48 return _run_doctor(args, ctx, config, repo_root)
49
50 ctx.output.error(f"unknown workspace action: {action}")
51 return EXIT_USAGE
52
53
54 def _run_status(args: Namespace, ctx: CliContext, config, repo_root: Path) -> int:
55 report = build_status_report(
56 config,
57 repo_root,
58 strict_all=bool(getattr(args, "strict_all", False)),
59 )
60 if ctx.output.json_mode:
61 payload = report.to_json()
62 payload["workspace"] = payload # convenience alias for status --workspace compose
63 ctx.output.emit_json(report.to_json())
64 else:
65 if not report.configured:
66 ctx.output.emit("workspace: not_configured")
67 return EXIT_OK
68 ctx.output.emit(f"workspace.ok: {str(report.ok).lower()}")
69 ctx.output.emit(f"workspace.state: {report.state}")
70 ctx.output.emit(f"constellation_id: {report.constellation_id}")
71 ctx.output.emit(f"product_order_member: {report.product_order_member}")
72 ctx.output.emit(f"manifest_source: {report.manifest_source}")
73 if report.authoritative_handover:
74 ctx.output.emit(f"authoritative_handover: {report.authoritative_handover}")
75 for member in report.members:
76 base = member.get("handover_basename") or "?"
77 ctx.output.emit(
78 f"member {member['id']}: role={member['role']} "
79 f"status={member['member_status']} handover={base}"
80 )
81 for warning in report.warnings:
82 ctx.output.emit(f"warning: {warning}")
83 if report.check_next and report.check_next.get("messages"):
84 for msg in report.check_next["messages"]:
85 ctx.output.emit(msg)
86 # status itself exits 0 even when workspace.ok is false (§MR.12.1 / S9 separation);
87 # --exit-code is only on `ok status --workspace --exit-code`.
88 return EXIT_OK
89
90
91 def _run_check_next(args: Namespace, ctx: CliContext, config, repo_root: Path) -> int:
92 if config.workspace is None:
93 payload = {"ok": False, "state": "not_configured", "exit_code": EXIT_CONFIG}
94 if ctx.output.json_mode:
95 ctx.output.emit_json(payload)
96 else:
97 ctx.output.error("workspace not configured (check-next requires workspace:)")
98 return EXIT_CONFIG
99 try:
100 manifest = load_manifest_for_repo(config, repo_root)
101 except WorkspaceLoadError as exc:
102 if ctx.output.json_mode:
103 ctx.output.emit_json({"ok": False, "state": "error", "error": str(exc), "exit_code": EXIT_CONFIG})
104 else:
105 ctx.output.error(str(exc))
106 return EXIT_CONFIG
107
108 result = check_next(manifest, lane=getattr(args, "lane", None))
109 if ctx.output.json_mode:
110 ctx.output.emit_json(
111 {
112 "ok": result.ok,
113 "state": result.state,
114 "exit_code": result.exit_code,
115 "lane": result.lane,
116 "primary": result.primary,
117 "relays": list(result.relays),
118 "messages": list(result.messages),
119 }
120 )
121 else:
122 for msg in result.messages:
123 ctx.output.emit(msg)
124 if result.ok:
125 ctx.output.emit("workspace check-next: ok")
126 else:
127 ctx.output.error(f"workspace check-next: {result.state}")
128 return result.exit_code
129
130
131 def _run_doctor(args: Namespace, ctx: CliContext, config, repo_root: Path) -> int:
132 report = run_doctor(config, repo_root)
133 if ctx.output.json_mode:
134 ctx.output.emit_json(report.to_json())
135 else:
136 if not report.configured:
137 ctx.output.emit("workspace: not_configured")
138 return EXIT_OK
139 if not report.findings:
140 ctx.output.emit("workspace doctor: no findings")
141 for finding in report.findings:
142 loc = f" [{finding.member_id}]" if finding.member_id else ""
143 ctx.output.emit(f"{finding.code}{loc}: {finding.message}")
144 return EXIT_OK if report.ok or not report.configured else EXIT_OK
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 9 days ago