@seanyao/roll 2026.520.1 → 2026.521.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.
- package/CHANGELOG.md +24 -0
- package/bin/roll +232 -79
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
- package/lib/loop-fmt.py +26 -18
- package/lib/roll-backlog.py +2 -39
- package/lib/roll-help.py +1 -2
- package/lib/roll-home.py +5 -6
- package/lib/roll-init.py +103 -48
- package/lib/roll-loop-status.py +33 -25
- package/lib/roll-peer.py +25 -15
- package/lib/roll-setup.py +70 -30
- package/lib/roll-status.py +4 -5
- package/package.json +1 -1
- package/skills/roll-loop/SKILL.md +34 -29
- package/skills/roll-peer/SKILL.md +58 -0
package/lib/roll-init.py
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""roll-init — v2 terminal view for `roll init
|
|
2
|
+
"""roll-init — v2 terminal view for `roll init`.
|
|
3
|
+
|
|
4
|
+
Reads a single JSON document from stdin describing real step outcomes
|
|
5
|
+
captured by the bash flow in `bin/roll cmd_init`. Renders the v2 UI
|
|
6
|
+
(banner + horizontal rule + numbered steps + NEXT block) preserving the
|
|
7
|
+
visual style of US-VIEW-008 but reflecting what actually happened.
|
|
8
|
+
|
|
9
|
+
Input schema (single JSON object on stdin):
|
|
10
|
+
{
|
|
11
|
+
"kind": "init", # informational; UI label uses header_label
|
|
12
|
+
"header_label": "INIT", # banner label (e.g. "INIT" / "REINIT")
|
|
13
|
+
"subtitle": "项目初始化", # banner subtitle
|
|
14
|
+
"project_path": "/path/to/project", # right-aligned banner text
|
|
15
|
+
"steps": [
|
|
16
|
+
{"num": 1, "label": "Detect project type", "status": "ok"},
|
|
17
|
+
{"num": 2, "label": "Create AGENTS.md", "status": "ok",
|
|
18
|
+
"files": [["+", "AGENTS.md"]]},
|
|
19
|
+
{"num": 3, "label": "...", "status": "skip", "note": "already exists"},
|
|
20
|
+
{"num": 4, "label": "...", "status": "fail", "error": "permission denied"}
|
|
21
|
+
],
|
|
22
|
+
"footer": {"status": "ok", "label": "Project ready"},
|
|
23
|
+
"next": [["Edit .roll/backlog.md", "open the backlog and add your first US"]]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
`status` values: ok | skip | fail.
|
|
27
|
+
`files` ops: "+" created "~" merged "·" unchanged "✗" failed.
|
|
28
|
+
|
|
29
|
+
If stdin is empty or invalid JSON, exit 1 with a short message — the
|
|
30
|
+
renderer is no longer runnable standalone, it always renders real data.
|
|
31
|
+
"""
|
|
3
32
|
from __future__ import annotations
|
|
4
33
|
|
|
5
34
|
import argparse
|
|
35
|
+
import json
|
|
6
36
|
import os
|
|
7
37
|
import sys
|
|
8
38
|
|
|
@@ -12,32 +42,6 @@ if _LIB_DIR not in sys.path:
|
|
|
12
42
|
import roll_render
|
|
13
43
|
from roll_render import c, row, COLS
|
|
14
44
|
|
|
15
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
16
|
-
# Demo data — 6 steps mirror cmd_init's actual phases.
|
|
17
|
-
# AC text: detect → AGENTS.md → BACKLOG.md → docs/features/ → merge CLAUDE.md → link skills
|
|
18
|
-
# Each entry: (label, [(op, filename)]) where op ∈ {"+", "~"}.
|
|
19
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
20
|
-
|
|
21
|
-
_DEMO_STEPS = [
|
|
22
|
-
("Detect project type", []),
|
|
23
|
-
("Create AGENTS.md", [("+", "AGENTS.md")]),
|
|
24
|
-
("Create .roll/backlog.md", [("+", ".roll/backlog.md")]),
|
|
25
|
-
("Create .roll/features/", [("+", ".roll/features/")]),
|
|
26
|
-
("Merge existing CLAUDE.md", [("~", "CLAUDE.md")]),
|
|
27
|
-
("Link skills to AI clients", [("+", "~/.claude/skills/roll-build"),
|
|
28
|
-
("+", "~/.claude/skills/roll-fix")]),
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
_NEXT_STEPS = [
|
|
32
|
-
("Edit .roll/backlog.md", "open the backlog and add your first US"),
|
|
33
|
-
("Run roll loop now", "execute one cycle manually to test the flow"),
|
|
34
|
-
("Enable loop scheduling", "roll loop on — let it run hourly"),
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
39
|
-
# Render
|
|
40
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
41
45
|
|
|
42
46
|
def _divider(char: str = "─") -> None:
|
|
43
47
|
print(c("dim", char * min(COLS, 80)))
|
|
@@ -48,53 +52,104 @@ def _op_marker(op: str) -> str:
|
|
|
48
52
|
return c("green", "+", bold=True)
|
|
49
53
|
if op == "~":
|
|
50
54
|
return c("amber", "~", bold=True)
|
|
55
|
+
if op == "·":
|
|
56
|
+
return c("dim", "·")
|
|
57
|
+
if op == "✗":
|
|
58
|
+
return c("red", "✗", bold=True)
|
|
51
59
|
return c("dim", op)
|
|
52
60
|
|
|
53
61
|
|
|
54
|
-
def
|
|
55
|
-
|
|
62
|
+
def _step_icon(status: str) -> str:
|
|
63
|
+
if status == "ok":
|
|
64
|
+
return c("green", "✓")
|
|
65
|
+
if status == "skip":
|
|
66
|
+
return c("amber", "↷")
|
|
67
|
+
if status == "fail":
|
|
68
|
+
return c("red", "✗", bold=True)
|
|
69
|
+
return c("dim", "·")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _file_color(op: str) -> str:
|
|
73
|
+
if op == "+":
|
|
74
|
+
return "green"
|
|
75
|
+
if op == "~":
|
|
76
|
+
return "amber"
|
|
77
|
+
if op == "✗":
|
|
78
|
+
return "red"
|
|
79
|
+
return "dim"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def render(payload: dict) -> None:
|
|
83
|
+
header_label = payload.get("header_label", "INIT")
|
|
84
|
+
subtitle = payload.get("subtitle", "项目初始化")
|
|
85
|
+
project_path = payload.get("project_path", "")
|
|
86
|
+
|
|
87
|
+
left = " " + c("blue", header_label, bold=True) + c("dim", " · ") + c("dim", subtitle)
|
|
56
88
|
right = c("dim", project_path) + " "
|
|
57
89
|
print(row(left, right))
|
|
58
90
|
_divider()
|
|
59
91
|
print()
|
|
60
92
|
|
|
61
|
-
for
|
|
62
|
-
num
|
|
63
|
-
icon =
|
|
93
|
+
for step in payload.get("steps", []):
|
|
94
|
+
num = c("dim", f" {step.get('num', 0)}.")
|
|
95
|
+
icon = _step_icon(step.get("status", "ok"))
|
|
96
|
+
label = step.get("label", "")
|
|
64
97
|
print(f"{num} {icon} {label}")
|
|
65
|
-
for
|
|
98
|
+
for entry in step.get("files", []) or []:
|
|
99
|
+
op, fname = entry[0], entry[1]
|
|
66
100
|
mark = _op_marker(op)
|
|
67
|
-
color =
|
|
101
|
+
color = _file_color(op)
|
|
68
102
|
print(" " + mark + " " + c(color, fname))
|
|
103
|
+
note = step.get("note") or step.get("error")
|
|
104
|
+
if note:
|
|
105
|
+
tone = "red" if step.get("status") == "fail" else "dim"
|
|
106
|
+
print(" " + c(tone, str(note)))
|
|
69
107
|
|
|
70
108
|
print()
|
|
71
109
|
_divider()
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
110
|
+
|
|
111
|
+
footer = payload.get("footer") or {}
|
|
112
|
+
f_status = footer.get("status", "ok")
|
|
113
|
+
f_label = footer.get("label", "Done")
|
|
114
|
+
icon_color = "green" if f_status == "ok" else "red"
|
|
115
|
+
icon = "✓" if f_status == "ok" else "✗"
|
|
116
|
+
print(" " + c(icon_color, icon) + " " + c(icon_color, f_label, bold=True))
|
|
117
|
+
|
|
118
|
+
next_items = payload.get("next") or []
|
|
119
|
+
if next_items:
|
|
120
|
+
print()
|
|
121
|
+
print(" " + c("pink", "NEXT", bold=True) + c("dim", " · 下一步"))
|
|
122
|
+
for i, entry in enumerate(next_items, start=1):
|
|
123
|
+
label = entry[0]
|
|
124
|
+
hint = entry[1] if len(entry) > 1 else ""
|
|
125
|
+
num = c("dim", f" {i}.")
|
|
126
|
+
print(f"{num} {c('fg', label, bold=True)}")
|
|
127
|
+
if hint:
|
|
128
|
+
print(" " + c("dim", hint))
|
|
79
129
|
_divider("═")
|
|
80
130
|
|
|
81
131
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
132
|
+
def _read_payload() -> dict:
|
|
133
|
+
raw = sys.stdin.read()
|
|
134
|
+
if not raw.strip():
|
|
135
|
+
sys.stderr.write("roll-init.py: expected JSON on stdin\n")
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
try:
|
|
138
|
+
return json.loads(raw)
|
|
139
|
+
except json.JSONDecodeError as exc:
|
|
140
|
+
sys.stderr.write(f"roll-init.py: invalid JSON on stdin: {exc}\n")
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
85
143
|
|
|
86
144
|
def main() -> None:
|
|
87
145
|
ap = argparse.ArgumentParser(add_help=False)
|
|
88
|
-
ap.add_argument("--demo", action="store_true")
|
|
89
146
|
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
90
|
-
ap.add_argument("--en", action="store_true")
|
|
91
|
-
ap.add_argument("--zh", action="store_true")
|
|
92
147
|
args, _ = ap.parse_known_args()
|
|
93
148
|
|
|
94
149
|
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
95
150
|
roll_render.USE_COLOR = False
|
|
96
151
|
|
|
97
|
-
|
|
152
|
+
render(_read_payload())
|
|
98
153
|
|
|
99
154
|
|
|
100
155
|
if __name__ == "__main__":
|
package/lib/roll-loop-status.py
CHANGED
|
@@ -17,7 +17,7 @@ Usage:
|
|
|
17
17
|
python3 lib/roll-loop-status.py --days 7
|
|
18
18
|
python3 lib/roll-loop-status.py --no-color
|
|
19
19
|
python3 lib/roll-loop-status.py --en | --zh # collapse bilingual rows
|
|
20
|
-
python3 lib/roll-loop-status.py
|
|
20
|
+
ROLL_RENDER_FIXTURE=1 python3 lib/roll-loop-status.py # render with fixture data (test only)
|
|
21
21
|
|
|
22
22
|
Wire it in bin/roll under `loop status` (replace _loop_status with a call to
|
|
23
23
|
this script).
|
|
@@ -206,9 +206,19 @@ def aggregate(events: List[Dict[str, Any]], cron: List[Dict[str, Any]]) -> List[
|
|
|
206
206
|
elif stage == "usage":
|
|
207
207
|
# US-LOOP-004: loop-fmt emits this with full token / cost data.
|
|
208
208
|
# Detail is a dict (not the legacy string form).
|
|
209
|
+
# US-VIEW-010: token counts are per-turn deltas — sum across events
|
|
210
|
+
# so list-price cost computed from totals matches actual API usage.
|
|
211
|
+
# Non-additive fields (model, cost_reported_usd, duration_ms) take
|
|
212
|
+
# the last value seen.
|
|
209
213
|
d = e.get("detail") or {}
|
|
210
214
|
if isinstance(d, dict):
|
|
211
|
-
cy
|
|
215
|
+
prev = cy.get("usage_event") or {}
|
|
216
|
+
merged = dict(prev)
|
|
217
|
+
merged.update(d)
|
|
218
|
+
for k in ("input_tokens", "output_tokens",
|
|
219
|
+
"cache_creation_tokens", "cache_read_tokens"):
|
|
220
|
+
merged[k] = int(prev.get(k) or 0) + int(d.get(k) or 0)
|
|
221
|
+
cy["usage_event"] = merged
|
|
212
222
|
elif stage in ("test", "build") and e.get("outcome") == "fail":
|
|
213
223
|
cy["fail_detail"] = detail or stage
|
|
214
224
|
|
|
@@ -315,8 +325,7 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
315
325
|
for cy in cycles:
|
|
316
326
|
# Path 1: usage event written by loop-fmt at result time.
|
|
317
327
|
ue = cy.get("usage_event")
|
|
318
|
-
if isinstance(ue, dict) and (ue.get("input_tokens") or ue.get("output_tokens")
|
|
319
|
-
or ue.get("cost_reported_usd")):
|
|
328
|
+
if isinstance(ue, dict) and (ue.get("input_tokens") or ue.get("output_tokens")):
|
|
320
329
|
cy["tokens"] = mp.total_tokens(
|
|
321
330
|
input_tokens=ue.get("input_tokens", 0),
|
|
322
331
|
output_tokens=ue.get("output_tokens", 0),
|
|
@@ -324,20 +333,18 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
324
333
|
cache_read_tokens=ue.get("cache_read_tokens", 0),
|
|
325
334
|
)
|
|
326
335
|
cy["model"] = ue.get("model")
|
|
327
|
-
#
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
cache_read_tokens=ue.get("cache_read_tokens", 0),
|
|
340
|
-
)
|
|
336
|
+
# US-VIEW-010: aggregate now sums per-turn usage tokens, so the
|
|
337
|
+
# totals in `ue` reflect the whole cycle. Always compute cost at
|
|
338
|
+
# list price for cross-account comparability — supersedes FIX-060
|
|
339
|
+
# which preferred cost_reported_usd as a workaround for
|
|
340
|
+
# last-event-only token counts (that root cause is now gone).
|
|
341
|
+
cy["cost_list"] = mp.compute_list_cost(
|
|
342
|
+
ue.get("model"),
|
|
343
|
+
input_tokens=ue.get("input_tokens", 0),
|
|
344
|
+
output_tokens=ue.get("output_tokens", 0),
|
|
345
|
+
cache_creation_tokens=ue.get("cache_creation_tokens", 0),
|
|
346
|
+
cache_read_tokens=ue.get("cache_read_tokens", 0),
|
|
347
|
+
)
|
|
341
348
|
if ue.get("duration_ms") and not cy.get("duration_s"):
|
|
342
349
|
cy["duration_s"] = int(ue["duration_ms"] / 1000)
|
|
343
350
|
continue
|
|
@@ -699,7 +706,7 @@ def render(events, cron, state, backlog, *, days=3, lang="both", now=None,
|
|
|
699
706
|
c("muted", " ") +
|
|
700
707
|
c("dim", "watch ") + c("blue", "roll loop --watch") +
|
|
701
708
|
c("muted", " ") +
|
|
702
|
-
c("dim", "more ") + c("blue", "roll loop --days 7"))
|
|
709
|
+
c("dim", "more ") + c("blue", "roll loop status --days 7"))
|
|
703
710
|
|
|
704
711
|
def _read_plist_loop_minute() -> int:
|
|
705
712
|
"""FIX-063: read actual loop Minute from launchd plist (truth source).
|
|
@@ -733,9 +740,9 @@ def _next_cron_hint(state: Dict[str, str], zh: bool = False) -> str:
|
|
|
733
740
|
return nxt.strftime("%H:%M") + f" · in {mins}m {secs:02d}s"
|
|
734
741
|
|
|
735
742
|
# ════════════════════════════════════════════════════════════════════════════
|
|
736
|
-
#
|
|
743
|
+
# Fixture data (test-only; opt in via ROLL_RENDER_FIXTURE=1)
|
|
737
744
|
# ════════════════════════════════════════════════════════════════════════════
|
|
738
|
-
def
|
|
745
|
+
def _fixture_data():
|
|
739
746
|
now = datetime.now(timezone.utc)
|
|
740
747
|
events, cron = [], []
|
|
741
748
|
cycle_id = 0
|
|
@@ -788,7 +795,6 @@ def main(argv=None):
|
|
|
788
795
|
p.add_argument("--no-color", action="store_true", help="strip ANSI (also honors NO_COLOR=1)")
|
|
789
796
|
p.add_argument("--en", action="store_true", help="EN rows only")
|
|
790
797
|
p.add_argument("--zh", action="store_true", help="ZH rows only")
|
|
791
|
-
p.add_argument("--demo", action="store_true", help="render with fixture data")
|
|
792
798
|
args = p.parse_args(argv)
|
|
793
799
|
|
|
794
800
|
roll_render.USE_COLOR = (not args.no_color
|
|
@@ -797,10 +803,12 @@ def main(argv=None):
|
|
|
797
803
|
|
|
798
804
|
lang = "en" if args.en else ("zh" if args.zh else "both")
|
|
799
805
|
|
|
800
|
-
|
|
801
|
-
|
|
806
|
+
use_fixture = bool(os.environ.get("ROLL_RENDER_FIXTURE"))
|
|
807
|
+
if use_fixture:
|
|
808
|
+
events, cron, state, backlog = _fixture_data()
|
|
802
809
|
runs = {}
|
|
803
810
|
git_merges = {}
|
|
811
|
+
slug = None
|
|
804
812
|
else:
|
|
805
813
|
slug = project_slug()
|
|
806
814
|
events = load_events(slug, args.days)
|
|
@@ -812,7 +820,7 @@ def main(argv=None):
|
|
|
812
820
|
|
|
813
821
|
render(events, cron, state, backlog, days=args.days, lang=lang,
|
|
814
822
|
runs=runs, git_merges=git_merges,
|
|
815
|
-
claude_slug=
|
|
823
|
+
claude_slug=slug)
|
|
816
824
|
|
|
817
825
|
if __name__ == "__main__":
|
|
818
826
|
try:
|
package/lib/roll-peer.py
CHANGED
|
@@ -50,10 +50,11 @@ def _agent_c(name: str) -> str:
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
# ════════════════════════════════════════════════════════════════════════════
|
|
53
|
-
#
|
|
53
|
+
# Fixture data (test-only; opt in via ROLL_RENDER_FIXTURE=1)
|
|
54
|
+
# Illustrative cross-agent review: claude proposes, codex reviews
|
|
54
55
|
# ════════════════════════════════════════════════════════════════════════════
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
_FIXTURE_SUBJECT = {
|
|
57
58
|
"story": "US-AUTH-014",
|
|
58
59
|
"title": "Session refresh fallback when refresh-token API 5xx",
|
|
59
60
|
"pr": "#412",
|
|
@@ -63,7 +64,7 @@ _DEMO_SUBJECT = {
|
|
|
63
64
|
"reviewer": "codex",
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
_FIXTURE_ROUNDS = [
|
|
67
68
|
{
|
|
68
69
|
"n": 1,
|
|
69
70
|
"hint": "first pass — proposer ships, reviewer probes",
|
|
@@ -93,13 +94,13 @@ _DEMO_ROUNDS = [
|
|
|
93
94
|
},
|
|
94
95
|
]
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
_FIXTURE_VERDICT = {
|
|
97
98
|
"outcome": "approved",
|
|
98
99
|
"reason": "2 rounds · 5 turns · all blocks resolved",
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
_FIXTURE_ARTIFACT = "~/.roll/.peer-state/logs/20260519_213700_claude_codex.md"
|
|
103
|
+
_FIXTURE_NEXT = [
|
|
103
104
|
("Continue execution", "claude resumes work on US-AUTH-014"),
|
|
104
105
|
("Inspect log", "open the artifact above to replay the transcript"),
|
|
105
106
|
]
|
|
@@ -205,19 +206,19 @@ def _footer(artifact: str, next_steps: list) -> None:
|
|
|
205
206
|
# Top-level render
|
|
206
207
|
# ════════════════════════════════════════════════════════════════════════════
|
|
207
208
|
|
|
208
|
-
def
|
|
209
|
-
_eyebrow(
|
|
209
|
+
def render_fixture() -> None:
|
|
210
|
+
_eyebrow(_FIXTURE_SUBJECT["trigger"])
|
|
210
211
|
_divider()
|
|
211
212
|
print()
|
|
212
|
-
_subject(
|
|
213
|
+
_subject(_FIXTURE_SUBJECT)
|
|
213
214
|
print()
|
|
214
|
-
_pair_overview(
|
|
215
|
-
for rd in
|
|
215
|
+
_pair_overview(_FIXTURE_SUBJECT)
|
|
216
|
+
for rd in _FIXTURE_ROUNDS:
|
|
216
217
|
_round_header(rd["n"], rd["hint"])
|
|
217
218
|
for agent, weight, body in rd["turns"]:
|
|
218
219
|
_turn(agent, weight, body)
|
|
219
|
-
_verdict(
|
|
220
|
-
_footer(
|
|
220
|
+
_verdict(_FIXTURE_VERDICT)
|
|
221
|
+
_footer(_FIXTURE_ARTIFACT, _FIXTURE_NEXT)
|
|
221
222
|
|
|
222
223
|
|
|
223
224
|
# ════════════════════════════════════════════════════════════════════════════
|
|
@@ -226,7 +227,6 @@ def render_demo() -> None:
|
|
|
226
227
|
|
|
227
228
|
def main() -> None:
|
|
228
229
|
ap = argparse.ArgumentParser(add_help=False)
|
|
229
|
-
ap.add_argument("--demo", action="store_true")
|
|
230
230
|
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
231
231
|
ap.add_argument("--en", action="store_true")
|
|
232
232
|
ap.add_argument("--zh", action="store_true")
|
|
@@ -235,7 +235,17 @@ def main() -> None:
|
|
|
235
235
|
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
236
236
|
roll_render.USE_COLOR = False
|
|
237
237
|
|
|
238
|
-
|
|
238
|
+
# FIX-076: this standalone entrypoint only knows how to render the fixture
|
|
239
|
+
# transcript (for UI tests). Real peer review is orchestrated by bin/roll
|
|
240
|
+
# and never invokes this main(). Require an explicit opt-in so a stray
|
|
241
|
+
# `python3 lib/roll-peer.py` invocation can't masquerade as live output.
|
|
242
|
+
if not os.environ.get("ROLL_RENDER_FIXTURE"):
|
|
243
|
+
print("Error: lib/roll-peer.py only renders fixture data; "
|
|
244
|
+
"set ROLL_RENDER_FIXTURE=1 to use it (test-only).",
|
|
245
|
+
file=sys.stderr)
|
|
246
|
+
sys.exit(2)
|
|
247
|
+
|
|
248
|
+
render_fixture()
|
|
239
249
|
|
|
240
250
|
|
|
241
251
|
if __name__ == "__main__":
|
package/lib/roll-setup.py
CHANGED
|
@@ -1,61 +1,101 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""roll-setup — v2 terminal view for `roll setup`.
|
|
2
|
+
"""roll-setup — v2 terminal view for `roll setup`.
|
|
3
|
+
|
|
4
|
+
Reads a single JSON document from stdin describing the real outcomes of
|
|
5
|
+
`bin/roll cmd_setup`'s step sequence (detect platform / install skills /
|
|
6
|
+
sync conventions / etc). Renders the v2 UI preserving the visual style
|
|
7
|
+
of US-VIEW-007 while reflecting actual results.
|
|
8
|
+
|
|
9
|
+
Input schema matches `lib/roll-init.py` (see that file for details).
|
|
10
|
+
"""
|
|
3
11
|
from __future__ import annotations
|
|
4
12
|
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
5
15
|
import os
|
|
6
16
|
import sys
|
|
7
17
|
|
|
8
18
|
_LIB_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
9
19
|
if _LIB_DIR not in sys.path:
|
|
10
20
|
sys.path.insert(0, _LIB_DIR)
|
|
21
|
+
import roll_render
|
|
11
22
|
from roll_render import c, row, COLS
|
|
12
23
|
|
|
13
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
14
|
-
# Demo data
|
|
15
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
16
|
-
|
|
17
|
-
_DEMO_STEPS = [
|
|
18
|
-
"Detect platform & shell",
|
|
19
|
-
"Fetch latest roll version",
|
|
20
|
-
"Install skills to ~/.claude",
|
|
21
|
-
"Symlink bin/roll to PATH",
|
|
22
|
-
"Check for config drift",
|
|
23
|
-
"Apply convention templates",
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
27
|
-
# Rendering
|
|
28
|
-
# ════════════════════════════════════════════════════════════════════════════
|
|
29
24
|
|
|
30
25
|
def _divider(char: str = "─") -> None:
|
|
31
26
|
print(c("dim", char * min(COLS, 80)))
|
|
32
27
|
|
|
33
28
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
def _step_icon(status: str) -> str:
|
|
30
|
+
if status == "ok":
|
|
31
|
+
return c("green", "✓")
|
|
32
|
+
if status == "skip":
|
|
33
|
+
return c("amber", "↷")
|
|
34
|
+
if status == "forced":
|
|
35
|
+
return c("blue", "~")
|
|
36
|
+
if status == "fail":
|
|
37
|
+
return c("red", "✗", bold=True)
|
|
38
|
+
return c("dim", "·")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def render(payload: dict) -> None:
|
|
42
|
+
header_label = payload.get("header_label", "SETUP")
|
|
43
|
+
subtitle = payload.get("subtitle", "初始化")
|
|
44
|
+
right_text = payload.get("project_path") or payload.get("right", "")
|
|
45
|
+
|
|
46
|
+
left = " " + c("blue", header_label, bold=True) + c("dim", " · ") + c("dim", subtitle)
|
|
47
|
+
right = c("dim", right_text) + " " if right_text else ""
|
|
48
|
+
print(row(left, right))
|
|
38
49
|
_divider()
|
|
39
50
|
print()
|
|
40
51
|
|
|
41
|
-
for
|
|
42
|
-
num
|
|
43
|
-
icon =
|
|
52
|
+
for step in payload.get("steps", []):
|
|
53
|
+
num = c("dim", f" {step.get('num', 0)}.")
|
|
54
|
+
icon = _step_icon(step.get("status", "ok"))
|
|
55
|
+
label = step.get("label", "")
|
|
44
56
|
print(f"{num} {icon} {label}")
|
|
57
|
+
note = step.get("note") or step.get("error")
|
|
58
|
+
if note:
|
|
59
|
+
tone = "red" if step.get("status") == "fail" else "dim"
|
|
60
|
+
print(" " + c(tone, str(note)))
|
|
45
61
|
|
|
46
62
|
print()
|
|
47
63
|
_divider()
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
|
|
65
|
+
footer = payload.get("footer") or {}
|
|
66
|
+
f_status = footer.get("status", "ok")
|
|
67
|
+
f_label = footer.get("label", "Setup complete")
|
|
68
|
+
icon_color = "green" if f_status == "ok" else "red"
|
|
69
|
+
msg = c(icon_color, f_label)
|
|
70
|
+
next_hint = footer.get("hint")
|
|
71
|
+
if next_hint:
|
|
72
|
+
print(f" {msg} — {next_hint}")
|
|
73
|
+
else:
|
|
74
|
+
print(f" {msg}")
|
|
50
75
|
_divider("═")
|
|
51
76
|
|
|
52
77
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
78
|
+
def _read_payload() -> dict:
|
|
79
|
+
raw = sys.stdin.read()
|
|
80
|
+
if not raw.strip():
|
|
81
|
+
sys.stderr.write("roll-setup.py: expected JSON on stdin\n")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
try:
|
|
84
|
+
return json.loads(raw)
|
|
85
|
+
except json.JSONDecodeError as exc:
|
|
86
|
+
sys.stderr.write(f"roll-setup.py: invalid JSON on stdin: {exc}\n")
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
56
89
|
|
|
57
90
|
def main() -> None:
|
|
58
|
-
|
|
91
|
+
ap = argparse.ArgumentParser(add_help=False)
|
|
92
|
+
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
93
|
+
args, _ = ap.parse_known_args()
|
|
94
|
+
|
|
95
|
+
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
96
|
+
roll_render.USE_COLOR = False
|
|
97
|
+
|
|
98
|
+
render(_read_payload())
|
|
59
99
|
|
|
60
100
|
|
|
61
101
|
if __name__ == "__main__":
|
package/lib/roll-status.py
CHANGED
|
@@ -8,7 +8,7 @@ project templates, and this-project metrics.
|
|
|
8
8
|
Usage:
|
|
9
9
|
python3 lib/roll-status.py # live data
|
|
10
10
|
python3 lib/roll-status.py --no-color
|
|
11
|
-
python3 lib/roll-status.py
|
|
11
|
+
ROLL_RENDER_FIXTURE=1 python3 lib/roll-status.py # render with fixture data (test only)
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
@@ -149,9 +149,9 @@ def _launchd_state(service: str, slug: str) -> str:
|
|
|
149
149
|
return "installed-off"
|
|
150
150
|
|
|
151
151
|
# ════════════════════════════════════════════════════════════════════════════
|
|
152
|
-
#
|
|
152
|
+
# Fixture data (test-only; opt in via ROLL_RENDER_FIXTURE=1)
|
|
153
153
|
# ════════════════════════════════════════════════════════════════════════════
|
|
154
|
-
def
|
|
154
|
+
def _fixture_data() -> Dict[str, Any]:
|
|
155
155
|
return dict(
|
|
156
156
|
conventions=[
|
|
157
157
|
("AGENTS.md", True), ("CLAUDE.md", True), ("GEMINI.md", False),
|
|
@@ -347,7 +347,6 @@ def _live_data() -> Dict[str, Any]:
|
|
|
347
347
|
# ════════════════════════════════════════════════════════════════════════════
|
|
348
348
|
def main() -> None:
|
|
349
349
|
ap = argparse.ArgumentParser(add_help=False)
|
|
350
|
-
ap.add_argument("--demo", action="store_true")
|
|
351
350
|
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
352
351
|
ap.add_argument("--en", action="store_true")
|
|
353
352
|
ap.add_argument("--zh", action="store_true")
|
|
@@ -356,7 +355,7 @@ def main() -> None:
|
|
|
356
355
|
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
357
356
|
roll_render.USE_COLOR = False
|
|
358
357
|
|
|
359
|
-
d =
|
|
358
|
+
d = _fixture_data() if os.environ.get("ROLL_RENDER_FIXTURE") else _live_data()
|
|
360
359
|
|
|
361
360
|
_render_health(d)
|
|
362
361
|
_render_global_conventions(d["conventions"])
|