dyro 0.1.0__py3-none-any.whl
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.
- dyro/__init__.py +3 -0
- dyro/__main__.py +5 -0
- dyro/cli.py +529 -0
- dyro/config.py +179 -0
- dyro/errors.py +6 -0
- dyro/onboarding.py +106 -0
- dyro/process.py +62 -0
- dyro/tasks.py +498 -0
- dyro/workspace.py +227 -0
- dyro-0.1.0.dist-info/METADATA +116 -0
- dyro-0.1.0.dist-info/RECORD +15 -0
- dyro-0.1.0.dist-info/WHEEL +5 -0
- dyro-0.1.0.dist-info/entry_points.txt +2 -0
- dyro-0.1.0.dist-info/licenses/LICENSE +21 -0
- dyro-0.1.0.dist-info/top_level.txt +1 -0
dyro/__init__.py
ADDED
dyro/__main__.py
ADDED
dyro/cli.py
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shlex
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
from . import __version__
|
|
12
|
+
from .config import CONFIG_NAME, Config, expand_argv, load, validate_id
|
|
13
|
+
from .errors import DyroError
|
|
14
|
+
from .onboarding import ask_for_workspace, bootstrap, render_config
|
|
15
|
+
from .tasks import (
|
|
16
|
+
STATUSES,
|
|
17
|
+
answer_task,
|
|
18
|
+
board,
|
|
19
|
+
check_dispatchable,
|
|
20
|
+
decisions,
|
|
21
|
+
list_tasks,
|
|
22
|
+
load_task,
|
|
23
|
+
loop_tasks,
|
|
24
|
+
merge_task,
|
|
25
|
+
review_task,
|
|
26
|
+
run_gates,
|
|
27
|
+
run_task,
|
|
28
|
+
set_status,
|
|
29
|
+
stats,
|
|
30
|
+
status as task_status,
|
|
31
|
+
task_template,
|
|
32
|
+
)
|
|
33
|
+
from .workspace import create_line, doctor, get_line, line_root, list_lines, status_rows
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
CONFIG_TEMPLATE = '''schema_version = 1
|
|
37
|
+
|
|
38
|
+
[workspace]
|
|
39
|
+
name = "{name}"
|
|
40
|
+
|
|
41
|
+
[layout]
|
|
42
|
+
anchors = "repositories"
|
|
43
|
+
lines = "versions"
|
|
44
|
+
hotfixes = "hotfixes"
|
|
45
|
+
tasks = "worktrees"
|
|
46
|
+
|
|
47
|
+
[policy]
|
|
48
|
+
default_base = "main"
|
|
49
|
+
task_branch_prefix = "task/"
|
|
50
|
+
allow_push = false
|
|
51
|
+
require_clean_merge = true
|
|
52
|
+
|
|
53
|
+
# Commands are argv arrays, not shell strings. DyroEngineeringFlow expands only
|
|
54
|
+
# {{workspace}}, {{root}}, {{task}}, {{line}} and {{prompt}}.
|
|
55
|
+
[adapters.codex]
|
|
56
|
+
launch = ["codex", "-C", "{{workspace}}"]
|
|
57
|
+
read = ["codex", "exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "{{prompt}}"]
|
|
58
|
+
write = ["codex", "exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "{{prompt}}"]
|
|
59
|
+
|
|
60
|
+
# Add each repository anchor once. A release line or task receives linked
|
|
61
|
+
# worktrees under the configured layout paths.
|
|
62
|
+
[repositories.api]
|
|
63
|
+
path = "repositories/services/api"
|
|
64
|
+
mount = "services/api"
|
|
65
|
+
verify = [["python3", "-m", "pytest", "-q"]]
|
|
66
|
+
|
|
67
|
+
[repositories.web]
|
|
68
|
+
path = "repositories/clients/web"
|
|
69
|
+
mount = "clients/web"
|
|
70
|
+
verify = [["npm", "test", "--", "--runInBand"]]
|
|
71
|
+
'''
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _config(args: argparse.Namespace) -> Config:
|
|
75
|
+
root = Path(args.root).expanduser() if getattr(args, "root", None) else Path.cwd()
|
|
76
|
+
return load(root)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _repositories(raw: str | None) -> list[str] | None:
|
|
80
|
+
if raw is None:
|
|
81
|
+
return None
|
|
82
|
+
values = [item.strip() for item in raw.split(",") if item.strip()]
|
|
83
|
+
if not values:
|
|
84
|
+
raise DyroError("--repos 不能为空")
|
|
85
|
+
return values
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _require_yes(args: argparse.Namespace, label: str) -> None:
|
|
89
|
+
if not args.yes and not args.dry_run:
|
|
90
|
+
raise DyroError(f"{label} 会创建或修改 Git worktree;请先使用 --dry-run 检查,再加 --yes 执行")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _print_command(argv: tuple[str, ...]) -> None:
|
|
94
|
+
print("$ " + shlex.join(argv))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def cmd_init(args: argparse.Namespace) -> None:
|
|
98
|
+
root = Path(args.path).expanduser().resolve()
|
|
99
|
+
config_file = root / CONFIG_NAME
|
|
100
|
+
if config_file.exists():
|
|
101
|
+
raise DyroError(f"配置已存在:{config_file}")
|
|
102
|
+
if args.dry_run:
|
|
103
|
+
print(f"DRY RUN: 将创建 {config_file}")
|
|
104
|
+
return
|
|
105
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
if args.wizard:
|
|
107
|
+
name, repositories, base = ask_for_workspace(args.name)
|
|
108
|
+
content = render_config(name, repositories, base)
|
|
109
|
+
else:
|
|
110
|
+
content = CONFIG_TEMPLATE.format(name=args.name)
|
|
111
|
+
config_file.write_text(content, encoding="utf-8")
|
|
112
|
+
for relative in (".dyro/tasks", ".dyro/lines", ".dyro/hotfixes"):
|
|
113
|
+
(root / relative).mkdir(parents=True, exist_ok=True)
|
|
114
|
+
print(f"已初始化 {root}")
|
|
115
|
+
print("下一步:登记 repositories,随后运行 dyro doctor")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmd_bootstrap(args: argparse.Namespace) -> None:
|
|
119
|
+
_require_yes(args, "bootstrap")
|
|
120
|
+
config = _config(args)
|
|
121
|
+
for message in bootstrap(config, dry_run=args.dry_run):
|
|
122
|
+
print(message)
|
|
123
|
+
if not args.dry_run:
|
|
124
|
+
for finding in doctor(config):
|
|
125
|
+
print(finding)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def cmd_doctor(args: argparse.Namespace) -> None:
|
|
129
|
+
findings = doctor(_config(args))
|
|
130
|
+
for finding in findings:
|
|
131
|
+
print(finding)
|
|
132
|
+
if any(item.startswith("FAIL") for item in findings):
|
|
133
|
+
raise DyroError("doctor 发现结构错误")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def cmd_status(args: argparse.Namespace) -> None:
|
|
137
|
+
rows = status_rows(_config(args))
|
|
138
|
+
print(f"{'SCOPE':24} {'REPOSITORY':14} {'BRANCH':24} {'HEAD':12} {'DIRTY':>5} UPSTREAM")
|
|
139
|
+
for scope, repo_id, branch, head, upstream, dirty in rows:
|
|
140
|
+
dirty_text = "-" if dirty < 0 else str(dirty)
|
|
141
|
+
print(f"{scope:24} {repo_id:14} {branch:24} {head:12} {dirty_text:>5} {upstream}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def cmd_agent_list(args: argparse.Namespace) -> None:
|
|
145
|
+
config = _config(args)
|
|
146
|
+
for adapter_id, adapter in sorted(config.adapters.items()):
|
|
147
|
+
print(f"{adapter_id:16} launch={shlex.join(adapter.launch)}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def cmd_open(args: argparse.Namespace) -> None:
|
|
151
|
+
config = _config(args)
|
|
152
|
+
line = get_line(config, args.line, args.kind)
|
|
153
|
+
try:
|
|
154
|
+
adapter = config.adapters[args.agent]
|
|
155
|
+
except KeyError as exc:
|
|
156
|
+
raise DyroError(f"未配置 Agent adapter:{args.agent}") from exc
|
|
157
|
+
workspace = line_root(config, line)
|
|
158
|
+
if not workspace.is_dir():
|
|
159
|
+
raise DyroError(f"开发线工作区不存在:{workspace}")
|
|
160
|
+
argv = expand_argv(adapter.launch, workspace=workspace, root=config.root, task="", line=line.id, prompt=args.prompt or "")
|
|
161
|
+
_print_command(argv)
|
|
162
|
+
if args.dry_run:
|
|
163
|
+
return
|
|
164
|
+
os.chdir(workspace)
|
|
165
|
+
os.execvp(argv[0], list(argv))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _choose(label: str, values: list[str]) -> str:
|
|
169
|
+
if not values:
|
|
170
|
+
raise DyroError(f"没有可选的 {label}")
|
|
171
|
+
if len(values) == 1:
|
|
172
|
+
return values[0]
|
|
173
|
+
print(f"请选择{label}:")
|
|
174
|
+
for index, value in enumerate(values, start=1):
|
|
175
|
+
print(f" {index}) {value}")
|
|
176
|
+
raw = input("编号:").strip()
|
|
177
|
+
if not raw.isdigit() or not (1 <= int(raw) <= len(values)):
|
|
178
|
+
raise DyroError(f"无效的{label}选择")
|
|
179
|
+
return values[int(raw) - 1]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def cmd_start(args: argparse.Namespace) -> None:
|
|
183
|
+
"""Newcomer-friendly path: validate, select a line, then open an Agent."""
|
|
184
|
+
config = _config(args)
|
|
185
|
+
findings = doctor(config)
|
|
186
|
+
failures = [finding for finding in findings if finding.startswith("FAIL")]
|
|
187
|
+
if failures:
|
|
188
|
+
print("\n".join(failures))
|
|
189
|
+
raise DyroError("工作区尚未就绪;先修复 doctor 失败项,或运行 dyro bootstrap --yes")
|
|
190
|
+
line_id = args.line or _choose("开发线", [line.id for line in list_lines(config)])
|
|
191
|
+
line = get_line(config, line_id, args.kind)
|
|
192
|
+
agent = args.agent or _choose("Agent", sorted(config.adapters))
|
|
193
|
+
open_args = argparse.Namespace(root=str(config.root), line=line.id, kind=line.kind, agent=agent, prompt=args.prompt or "", dry_run=args.dry_run)
|
|
194
|
+
cmd_open(open_args)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def cmd_line_list(args: argparse.Namespace) -> None:
|
|
198
|
+
config = _config(args)
|
|
199
|
+
lines = list_lines(config, args.kind)
|
|
200
|
+
if not lines:
|
|
201
|
+
print("暂无已登记开发线")
|
|
202
|
+
return
|
|
203
|
+
print(f"{'KIND':8} {'ID':28} {'BRANCH':30} {'BASE':24} REPOSITORIES")
|
|
204
|
+
for line in lines:
|
|
205
|
+
print(f"{line.kind:8} {line.id:28} {line.branch:30} {line.base:24} {', '.join(line.repositories)}")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _create_line(args: argparse.Namespace, kind: str) -> None:
|
|
209
|
+
config = _config(args)
|
|
210
|
+
_require_yes(args, "创建开发线")
|
|
211
|
+
branch = args.branch or (f"hotfix/{args.id}" if kind == "hotfix" else f"feat/{args.id}")
|
|
212
|
+
if kind == "hotfix" and not args.base:
|
|
213
|
+
raise DyroError("Hotfix 必须显式提供 --base(已核实的 release/tag/deployed SHA)")
|
|
214
|
+
base = args.base or config.policy.default_base
|
|
215
|
+
line = create_line(
|
|
216
|
+
config,
|
|
217
|
+
line_id=args.id,
|
|
218
|
+
branch=branch,
|
|
219
|
+
base=base,
|
|
220
|
+
repositories=_repositories(args.repos),
|
|
221
|
+
kind=kind,
|
|
222
|
+
dry_run=args.dry_run,
|
|
223
|
+
)
|
|
224
|
+
print(f"{'DRY RUN: ' if args.dry_run else ''}已创建 {line.kind} {line.id},分支 {line.branch},基线 {line.base}")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def cmd_line_create(args: argparse.Namespace) -> None:
|
|
228
|
+
_create_line(args, "line")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def cmd_hotfix_create(args: argparse.Namespace) -> None:
|
|
232
|
+
_create_line(args, "hotfix")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def cmd_task_create(args: argparse.Namespace) -> None:
|
|
236
|
+
config = _config(args)
|
|
237
|
+
validate_id(args.id, "任务 ID")
|
|
238
|
+
get_line(config, args.line)
|
|
239
|
+
if args.repository not in config.repositories:
|
|
240
|
+
raise DyroError(f"未配置仓库:{args.repository}")
|
|
241
|
+
path = config.task_specs_dir / args.id
|
|
242
|
+
if path.exists():
|
|
243
|
+
raise DyroError(f"任务目录已存在:{path}")
|
|
244
|
+
if args.dry_run:
|
|
245
|
+
print(f"DRY RUN: 将创建 {path}")
|
|
246
|
+
return
|
|
247
|
+
path.mkdir(parents=True)
|
|
248
|
+
mount = config.repositories[args.repository].mount
|
|
249
|
+
(path / "task.toml").write_text(task_template(args.id, args.title, args.line, args.repository, mount), encoding="utf-8")
|
|
250
|
+
(path / "handoff.md").write_text(f"# {args.title}\n\n- 目标:\n- 范围:\n- 验收:\n", encoding="utf-8")
|
|
251
|
+
print(f"已创建任务:{path}")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def cmd_task_list(args: argparse.Namespace) -> None:
|
|
255
|
+
config = _config(args)
|
|
256
|
+
for task in list_tasks(config):
|
|
257
|
+
print(f"{task.id:30} {task_status(config, task):16} {task.line:20} {task.title}")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def cmd_task_board(args: argparse.Namespace) -> None:
|
|
261
|
+
print(board(_config(args)), end="")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def cmd_task_status(args: argparse.Namespace) -> None:
|
|
265
|
+
config = _config(args)
|
|
266
|
+
task = load_task(config, args.id)
|
|
267
|
+
if args.value is None:
|
|
268
|
+
print(task_status(config, task))
|
|
269
|
+
return
|
|
270
|
+
set_status(config, task, args.value, force=args.force, dry_run=args.dry_run)
|
|
271
|
+
print(f"{'DRY RUN: ' if args.dry_run else ''}{task.id} -> {args.value}")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def cmd_task_run(args: argparse.Namespace) -> None:
|
|
275
|
+
config = _config(args)
|
|
276
|
+
task = load_task(config, args.id)
|
|
277
|
+
result = run_task(config, task, dry_run=args.dry_run)
|
|
278
|
+
print(f"{task.id} -> {result}")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def cmd_task_next(args: argparse.Namespace) -> None:
|
|
282
|
+
config = _config(args)
|
|
283
|
+
candidates = []
|
|
284
|
+
for task in list_tasks(config):
|
|
285
|
+
if task_status(config, task) not in ("backlog", "assigned"):
|
|
286
|
+
continue
|
|
287
|
+
try:
|
|
288
|
+
check_dispatchable(config, task)
|
|
289
|
+
candidates.append(task)
|
|
290
|
+
except DyroError:
|
|
291
|
+
continue
|
|
292
|
+
if args.id:
|
|
293
|
+
candidates = [task for task in candidates if task.id == args.id]
|
|
294
|
+
if not candidates:
|
|
295
|
+
raise DyroError("没有可执行任务;可检查 task board 与 decisions")
|
|
296
|
+
if not args.run:
|
|
297
|
+
for task in candidates:
|
|
298
|
+
print(f"{task.id:30} {task.line:20} {task.title}")
|
|
299
|
+
print("运行:dyro task next --run --yes" + (" --id <任务ID>" if len(candidates) > 1 else ""))
|
|
300
|
+
return
|
|
301
|
+
_require_yes(args, "启动下一个任务")
|
|
302
|
+
if len(candidates) > 1:
|
|
303
|
+
if not args.id:
|
|
304
|
+
selected_id = _choose("任务", [task.id for task in candidates])
|
|
305
|
+
candidates = [task for task in candidates if task.id == selected_id]
|
|
306
|
+
task = candidates[0]
|
|
307
|
+
print(f"{task.id} -> {run_task(config, task, dry_run=args.dry_run)}")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def cmd_task_answer(args: argparse.Namespace) -> None:
|
|
311
|
+
config = _config(args)
|
|
312
|
+
task = load_task(config, args.id)
|
|
313
|
+
if args.file:
|
|
314
|
+
answer = Path(args.file).read_text(encoding="utf-8")
|
|
315
|
+
else:
|
|
316
|
+
answer = args.text
|
|
317
|
+
if not answer.strip():
|
|
318
|
+
raise DyroError("回答不能为空")
|
|
319
|
+
print(f"{task.id} -> {answer_task(config, task, answer, dry_run=args.dry_run)}")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def cmd_task_gates(args: argparse.Namespace) -> None:
|
|
323
|
+
config = _config(args)
|
|
324
|
+
task = load_task(config, args.id)
|
|
325
|
+
passed = run_gates(config, task, dry_run=args.dry_run)
|
|
326
|
+
print("PASS" if passed else "FAIL")
|
|
327
|
+
if not passed:
|
|
328
|
+
raise DyroError(f"任务 {task.id} 门禁未通过")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def cmd_task_review(args: argparse.Namespace) -> None:
|
|
332
|
+
config = _config(args)
|
|
333
|
+
task = load_task(config, args.id)
|
|
334
|
+
print(f"{task.id} -> {review_task(config, task, dry_run=args.dry_run)}")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def cmd_task_merge(args: argparse.Namespace) -> None:
|
|
338
|
+
_require_yes(args, "合并任务")
|
|
339
|
+
config = _config(args)
|
|
340
|
+
task = load_task(config, args.id)
|
|
341
|
+
merge_task(config, task, push=args.push, dry_run=args.dry_run)
|
|
342
|
+
print(f"{'DRY RUN: ' if args.dry_run else ''}已合并 {task.id}" + (" 并推送" if args.push else ""))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def cmd_task_decisions(args: argparse.Namespace) -> None:
|
|
346
|
+
items = decisions(_config(args))
|
|
347
|
+
if not items:
|
|
348
|
+
print("暂无决策点")
|
|
349
|
+
return
|
|
350
|
+
for key, value in sorted(items.items()):
|
|
351
|
+
print(f"{key:32} {value}")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def cmd_task_stats(args: argparse.Namespace) -> None:
|
|
355
|
+
report = stats(_config(args))
|
|
356
|
+
if not report:
|
|
357
|
+
print("台账为空")
|
|
358
|
+
return
|
|
359
|
+
print(f"{'AGENT':18} {'EXEC':>5} {'EXEC OK':>8} {'REVIEW':>7} {'REVIEW OK':>10}")
|
|
360
|
+
for agent, counters in sorted(report.items()):
|
|
361
|
+
print(f"{agent:18} {counters['executor']:>5} {counters['executor_ok']:>8} {counters['review']:>7} {counters['review_ok']:>10}")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def cmd_task_loop(args: argparse.Namespace) -> None:
|
|
365
|
+
for task_id, result in loop_tasks(_config(args), dry_run=args.dry_run):
|
|
366
|
+
print(f"{task_id} -> {result}")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def cmd_task_daemon(args: argparse.Namespace) -> None:
|
|
370
|
+
config = _config(args)
|
|
371
|
+
while True:
|
|
372
|
+
tasks = list_tasks(config)
|
|
373
|
+
active_groups = {task.conflict_group for task in tasks if task.conflict_group and task_status(config, task) == "in_progress"}
|
|
374
|
+
queued = []
|
|
375
|
+
for task in tasks:
|
|
376
|
+
if task_status(config, task) != "assigned" or len(queued) >= max(1, args.parallel):
|
|
377
|
+
continue
|
|
378
|
+
if task.conflict_group and task.conflict_group in active_groups:
|
|
379
|
+
continue
|
|
380
|
+
queued.append(task)
|
|
381
|
+
if task.conflict_group:
|
|
382
|
+
active_groups.add(task.conflict_group)
|
|
383
|
+
if queued:
|
|
384
|
+
with ThreadPoolExecutor(max_workers=max(1, args.parallel), thread_name_prefix="dyro-dispatch") as pool:
|
|
385
|
+
futures = {pool.submit(run_task, config, task, dry_run=args.dry_run): task for task in queued}
|
|
386
|
+
for future in as_completed(futures):
|
|
387
|
+
task = futures[future]
|
|
388
|
+
try:
|
|
389
|
+
print(f"dispatch {task.id} -> {future.result()}")
|
|
390
|
+
except DyroError as exc:
|
|
391
|
+
print(f"skip {task.id}: {exc}")
|
|
392
|
+
review_queue = [task for task in list_tasks(config) if task_status(config, task) == "review"]
|
|
393
|
+
if review_queue:
|
|
394
|
+
with ThreadPoolExecutor(max_workers=max(1, args.parallel), thread_name_prefix="dyro-review") as pool:
|
|
395
|
+
futures = {pool.submit(review_task, config, task, dry_run=args.dry_run): task for task in review_queue}
|
|
396
|
+
for future in as_completed(futures):
|
|
397
|
+
task = futures[future]
|
|
398
|
+
try:
|
|
399
|
+
print(f"review {task.id} -> {future.result()}")
|
|
400
|
+
except DyroError as exc:
|
|
401
|
+
print(f"keep review {task.id}: {exc}")
|
|
402
|
+
if args.once or args.dry_run:
|
|
403
|
+
return
|
|
404
|
+
time.sleep(max(10, args.interval))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _add_common(parser: argparse.ArgumentParser) -> None:
|
|
408
|
+
parser.add_argument("--root", help="工作区根目录;默认从当前目录向上查找 dyro.toml")
|
|
409
|
+
parser.add_argument("--dry-run", action="store_true", help="仅输出计划,不写文件、不调用 Agent 或 Git 写操作")
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
413
|
+
parser = argparse.ArgumentParser(prog="dyro", description="DyroEngineeringFlow:本地优先的多仓工程自动化与交付控制平台")
|
|
414
|
+
parser.add_argument("--version", action="version", version=f"dyro {__version__}")
|
|
415
|
+
_add_common(parser)
|
|
416
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
417
|
+
|
|
418
|
+
init = sub.add_parser("init", help="初始化工作区配置")
|
|
419
|
+
init.add_argument("path", nargs="?", default=".")
|
|
420
|
+
init.add_argument("--name", default="my-workspace")
|
|
421
|
+
init.add_argument("--wizard", action="store_true", help="交互式登记真实仓库与可选 remote")
|
|
422
|
+
init.set_defaults(func=cmd_init)
|
|
423
|
+
|
|
424
|
+
sub.add_parser("doctor", help="验证动态工作区结构").set_defaults(func=cmd_doctor)
|
|
425
|
+
sub.add_parser("status", help="显示 anchors 与开发线 Git 状态").set_defaults(func=cmd_status)
|
|
426
|
+
bootstrap_parser = sub.add_parser("bootstrap", help="clone 配置了 remote 的缺失仓库 anchor")
|
|
427
|
+
bootstrap_parser.add_argument("--yes", action="store_true")
|
|
428
|
+
bootstrap_parser.set_defaults(func=cmd_bootstrap)
|
|
429
|
+
agent = sub.add_parser("agent", help="Agent adapters")
|
|
430
|
+
agent_sub = agent.add_subparsers(dest="agent_command", required=True)
|
|
431
|
+
agent_sub.add_parser("list").set_defaults(func=cmd_agent_list)
|
|
432
|
+
open_cmd = sub.add_parser("open", help="在指定开发线启动 Agent")
|
|
433
|
+
open_cmd.add_argument("line")
|
|
434
|
+
open_cmd.add_argument("--kind", choices=("line", "hotfix"))
|
|
435
|
+
open_cmd.add_argument("--agent", default="codex")
|
|
436
|
+
open_cmd.add_argument("--prompt", default="")
|
|
437
|
+
open_cmd.set_defaults(func=cmd_open)
|
|
438
|
+
start = sub.add_parser("start", help="新人入口:检查工作区、选择开发线和 Agent")
|
|
439
|
+
start.add_argument("--line")
|
|
440
|
+
start.add_argument("--kind", choices=("line", "hotfix"))
|
|
441
|
+
start.add_argument("--agent")
|
|
442
|
+
start.add_argument("--prompt", default="")
|
|
443
|
+
start.set_defaults(func=cmd_start)
|
|
444
|
+
|
|
445
|
+
line = sub.add_parser("line", help="功能开发线")
|
|
446
|
+
line_sub = line.add_subparsers(dest="line_command", required=True)
|
|
447
|
+
line_list = line_sub.add_parser("list")
|
|
448
|
+
line_list.add_argument("--kind", choices=("line", "hotfix"))
|
|
449
|
+
line_list.set_defaults(func=cmd_line_list)
|
|
450
|
+
line_create = line_sub.add_parser("create")
|
|
451
|
+
line_create.add_argument("id")
|
|
452
|
+
line_create.add_argument("--branch")
|
|
453
|
+
line_create.add_argument("--base")
|
|
454
|
+
line_create.add_argument("--repos", help="逗号分隔;默认全部 configured repositories")
|
|
455
|
+
line_create.add_argument("--yes", action="store_true")
|
|
456
|
+
line_create.set_defaults(func=cmd_line_create)
|
|
457
|
+
|
|
458
|
+
hotfix = sub.add_parser("hotfix", help="生产 Hotfix 开发线")
|
|
459
|
+
hotfix_sub = hotfix.add_subparsers(dest="hotfix_command", required=True)
|
|
460
|
+
hotfix_create = hotfix_sub.add_parser("create")
|
|
461
|
+
hotfix_create.add_argument("id")
|
|
462
|
+
hotfix_create.add_argument("--branch")
|
|
463
|
+
hotfix_create.add_argument("--base", required=True)
|
|
464
|
+
hotfix_create.add_argument("--repos")
|
|
465
|
+
hotfix_create.add_argument("--yes", action="store_true")
|
|
466
|
+
hotfix_create.set_defaults(func=cmd_hotfix_create)
|
|
467
|
+
|
|
468
|
+
task = sub.add_parser("task", help="任务编排")
|
|
469
|
+
task_sub = task.add_subparsers(dest="task_command", required=True)
|
|
470
|
+
task_create = task_sub.add_parser("create")
|
|
471
|
+
task_create.add_argument("id")
|
|
472
|
+
task_create.add_argument("--title", required=True)
|
|
473
|
+
task_create.add_argument("--line", required=True)
|
|
474
|
+
task_create.add_argument("--repository", required=True)
|
|
475
|
+
task_create.set_defaults(func=cmd_task_create)
|
|
476
|
+
task_sub.add_parser("list").set_defaults(func=cmd_task_list)
|
|
477
|
+
task_sub.add_parser("board").set_defaults(func=cmd_task_board)
|
|
478
|
+
task_status_parser = task_sub.add_parser("status")
|
|
479
|
+
task_status_parser.add_argument("id")
|
|
480
|
+
task_status_parser.add_argument("value", nargs="?", choices=STATUSES)
|
|
481
|
+
task_status_parser.add_argument("--force", action="store_true")
|
|
482
|
+
task_status_parser.set_defaults(func=cmd_task_status)
|
|
483
|
+
task_run = task_sub.add_parser("run")
|
|
484
|
+
task_run.add_argument("id")
|
|
485
|
+
task_run.set_defaults(func=cmd_task_run)
|
|
486
|
+
task_next = task_sub.add_parser("next", help="显示或启动下一个满足依赖的任务")
|
|
487
|
+
task_next.add_argument("--id")
|
|
488
|
+
task_next.add_argument("--run", action="store_true")
|
|
489
|
+
task_next.add_argument("--yes", action="store_true")
|
|
490
|
+
task_next.set_defaults(func=cmd_task_next)
|
|
491
|
+
task_answer = task_sub.add_parser("answer")
|
|
492
|
+
task_answer.add_argument("id")
|
|
493
|
+
group = task_answer.add_mutually_exclusive_group(required=True)
|
|
494
|
+
group.add_argument("--text")
|
|
495
|
+
group.add_argument("--file")
|
|
496
|
+
task_answer.set_defaults(func=cmd_task_answer)
|
|
497
|
+
task_gates = task_sub.add_parser("gates")
|
|
498
|
+
task_gates.add_argument("id")
|
|
499
|
+
task_gates.set_defaults(func=cmd_task_gates)
|
|
500
|
+
task_review = task_sub.add_parser("review")
|
|
501
|
+
task_review.add_argument("id")
|
|
502
|
+
task_review.set_defaults(func=cmd_task_review)
|
|
503
|
+
task_merge = task_sub.add_parser("merge")
|
|
504
|
+
task_merge.add_argument("id")
|
|
505
|
+
task_merge.add_argument("--yes", action="store_true")
|
|
506
|
+
task_merge.add_argument("--push", action="store_true")
|
|
507
|
+
task_merge.set_defaults(func=cmd_task_merge)
|
|
508
|
+
task_sub.add_parser("decisions").set_defaults(func=cmd_task_decisions)
|
|
509
|
+
task_sub.add_parser("stats").set_defaults(func=cmd_task_stats)
|
|
510
|
+
task_sub.add_parser("loop").set_defaults(func=cmd_task_loop)
|
|
511
|
+
daemon = task_sub.add_parser("daemon")
|
|
512
|
+
daemon.add_argument("--parallel", type=int, default=2)
|
|
513
|
+
daemon.add_argument("--interval", type=int, default=30)
|
|
514
|
+
daemon.add_argument("--once", action="store_true")
|
|
515
|
+
daemon.set_defaults(func=cmd_task_daemon)
|
|
516
|
+
return parser
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def main(argv: list[str] | None = None) -> None:
|
|
520
|
+
parser = build_parser()
|
|
521
|
+
args = parser.parse_args(argv)
|
|
522
|
+
try:
|
|
523
|
+
args.func(args)
|
|
524
|
+
except DyroError as exc:
|
|
525
|
+
parser.exit(2, f"错误:{exc}\n")
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
if __name__ == "__main__":
|
|
529
|
+
main()
|