dsh-task-ledger 0.0.101 → 0.0.102

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 (67) hide show
  1. package/dist/client.js +2 -2
  2. package/dist/host/repositories/runner.d.ts +1 -1
  3. package/dist/host/repositories/runner.js +10 -7
  4. package/dist/host/skill/registration.d.ts +2 -1
  5. package/dist/host/skill/registration.js +4 -3
  6. package/package.json +1 -1
  7. package/skill/README.md +4 -2
  8. package/skill/SKILL.md +5 -3
  9. package/skill/assets/worklog.md +1 -1
  10. package/skill/package.json +1 -1
  11. package/skill/references/init-identity.md +3 -3
  12. package/skill/references/log-note.md +1 -1
  13. package/skill/references/troubleshooting.md +1 -1
  14. package/skill/scripts/bin/ledger-darwin-arm64 +0 -0
  15. package/skill/scripts/bin/ledger-linux-amd64 +0 -0
  16. package/skill/scripts/bin/ledger-windows-amd64.exe +0 -0
  17. package/skill/scripts/progress +44 -0
  18. package/skill/scripts/ledgerkit/__init__.py +0 -0
  19. package/skill/scripts/ledgerkit/actions/__init__.py +0 -0
  20. package/skill/scripts/ledgerkit/actions/atom.py +0 -154
  21. package/skill/scripts/ledgerkit/actions/bootstrap.py +0 -37
  22. package/skill/scripts/ledgerkit/actions/claim.py +0 -88
  23. package/skill/scripts/ledgerkit/actions/close_task.py +0 -191
  24. package/skill/scripts/ledgerkit/actions/guard.py +0 -158
  25. package/skill/scripts/ledgerkit/actions/handoff.py +0 -93
  26. package/skill/scripts/ledgerkit/actions/init_identity.py +0 -90
  27. package/skill/scripts/ledgerkit/actions/list_tasks.py +0 -74
  28. package/skill/scripts/ledgerkit/actions/log_progress.py +0 -67
  29. package/skill/scripts/ledgerkit/actions/new_task.py +0 -129
  30. package/skill/scripts/ledgerkit/actions/query.py +0 -35
  31. package/skill/scripts/ledgerkit/actions/query_history.py +0 -220
  32. package/skill/scripts/ledgerkit/actions/registry.py +0 -82
  33. package/skill/scripts/ledgerkit/actions/relabel.py +0 -89
  34. package/skill/scripts/ledgerkit/actions/release.py +0 -76
  35. package/skill/scripts/ledgerkit/actions/result.py +0 -90
  36. package/skill/scripts/ledgerkit/actions/set_task.py +0 -71
  37. package/skill/scripts/ledgerkit/actions/show_task.py +0 -55
  38. package/skill/scripts/ledgerkit/actions/validate.py +0 -189
  39. package/skill/scripts/ledgerkit/core/__init__.py +0 -0
  40. package/skill/scripts/ledgerkit/core/history_md.py +0 -152
  41. package/skill/scripts/ledgerkit/core/identity.py +0 -54
  42. package/skill/scripts/ledgerkit/core/ids.py +0 -58
  43. package/skill/scripts/ledgerkit/core/labels.py +0 -89
  44. package/skill/scripts/ledgerkit/core/paths.py +0 -92
  45. package/skill/scripts/ledgerkit/core/roots.py +0 -74
  46. package/skill/scripts/ledgerkit/core/time.py +0 -103
  47. package/skill/scripts/ledgerkit/core/worklog_md.py +0 -435
  48. package/skill/scripts/ledgerkit/env.py +0 -94
  49. package/skill/scripts/ledgerkit/errors.py +0 -15
  50. package/skill/scripts/ledgerkit/notices.py +0 -28
  51. package/skill/scripts/ledgerkit/shell/__init__.py +0 -0
  52. package/skill/scripts/ledgerkit/shell/cli.py +0 -532
  53. package/skill/scripts/ledgerkit/shell/gitops.py +0 -668
  54. package/skill/scripts/ledgerkit/shell/merge.py +0 -266
  55. package/skill/scripts/ledgerkit/shell/presync.py +0 -23
  56. package/skill/scripts/ledgerkit/shell/render.py +0 -216
  57. package/skill/scripts/ledgerkit/shell/runner.py +0 -232
  58. package/skill/scripts/ledgerkit/store/__init__.py +0 -0
  59. package/skill/scripts/ledgerkit/store/atomic.py +0 -130
  60. package/skill/scripts/ledgerkit/store/dirty.py +0 -108
  61. package/skill/scripts/ledgerkit/store/history_store.py +0 -134
  62. package/skill/scripts/ledgerkit/store/json_store.py +0 -166
  63. package/skill/scripts/ledgerkit/store/locks.py +0 -64
  64. package/skill/scripts/ledgerkit/store/owners_index.py +0 -127
  65. package/skill/scripts/ledgerkit/store/task_store.py +0 -28
  66. package/skill/scripts/ledgerkit/store/worklog_store.py +0 -178
  67. package/skill/scripts/progress.py +0 -20
@@ -1,532 +0,0 @@
1
- """命令行:只做解析与装配,不碰文件。
2
-
3
- - `--json` 与 `--count-only` 互斥(退出码 2)。
4
- - `TASK_LEDGER_NO_SYNC` **只在这里读一次**,折成 `args.no_sync` 往下传。
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import argparse
10
- import os
11
- import sys
12
- from pathlib import Path
13
-
14
- from ..actions.query import HistoryQuery, ListQuery
15
- from ..core.roots import as_root, root_of
16
- from ..core.time import to_utc
17
- from ..errors import BUSINESS, OK, USAGE, Fail
18
- from . import render, runner
19
- from .gitops import repo_sync_lock
20
-
21
- NO_SYNC_ENV = "TASK_LEDGER_NO_SYNC"
22
-
23
-
24
- def build_parser() -> argparse.ArgumentParser:
25
- """动作名与只读名单从 `actions.registry` 取,不在 cli 里另写一份。"""
26
- parser = argparse.ArgumentParser(
27
- prog="progress.py", description="任务台账(本地文件 + git 同步)"
28
- )
29
- parser.add_argument(
30
- "--repo", help="工作区根(直接含有 ledger/ 的那个目录;默认按当前目录向上找)"
31
- )
32
- parser.add_argument(
33
- "--no-sync", action="store_true", help="只写本地,不取最新 / 不提交 / 不推送"
34
- )
35
- sub = parser.add_subparsers(dest="command", required=True)
36
-
37
- new = sub.add_parser("new", help="建任务")
38
- new.add_argument("--title", required=True)
39
- new.add_argument("--id", dest="tid")
40
- new.add_argument("--created")
41
- new.add_argument("--category")
42
- new.add_argument("--tags")
43
- new.add_argument("--self", action="store_true", dest="mine")
44
-
45
- claim = sub.add_parser("claim", help="认领公共池里的任务")
46
- claim.add_argument("--id", required=True, dest="tid")
47
-
48
- setp = sub.add_parser("set", help="释放 / 交接 / 改标签")
49
- setp.add_argument("--id", required=True, dest="tid")
50
- setp.add_argument("--owner", help="交给别人的 uuid(交接)")
51
- setp.add_argument("--release", action="store_true", help="释放回公共池")
52
- setp.add_argument("--tags", help="改标签,顿号 / 逗号分隔")
53
-
54
- logp = sub.add_parser("log", help="记一条进展")
55
- logp.add_argument("--id", required=True, dest="tid")
56
- logp.add_argument("--note", required=True)
57
-
58
- close = sub.add_parser("close", help="结案")
59
- close.add_argument("--id", required=True, dest="tid")
60
- close.add_argument("--summary", required=True)
61
- close.add_argument("--finished")
62
- close.add_argument("--tags")
63
-
64
- listp = sub.add_parser("list", help="查在办任务")
65
- listp.add_argument("--pool", action="store_true")
66
- listp.add_argument("--mine", action="store_true")
67
- listp.add_argument(
68
- "--owner", help="某个人的 uuid(精确匹配;名字请用 --owner-name)"
69
- )
70
- listp.add_argument(
71
- "--owner-name", dest="owner_name", help="按显示名模糊筛(子串、不分大小写)"
72
- )
73
- listp.add_argument("--status")
74
- listp.add_argument("--category")
75
- listp.add_argument("--tag")
76
- listp.add_argument("--since")
77
- listp.add_argument("--until")
78
- listp.add_argument("--json", action="store_true")
79
-
80
- showp = sub.add_parser("show", help="看一条任务")
81
- showp.add_argument("--id", required=True, dest="tid")
82
- showp.add_argument("--json", action="store_true")
83
-
84
- hist = sub.add_parser("history", help="查已结案")
85
- hist.add_argument(
86
- "--owner", help="某个人的 uuid(精确匹配;名字请用 --owner-name)"
87
- )
88
- hist.add_argument(
89
- "--owner-name", dest="owner_name", help="按显示名模糊筛(子串、不分大小写)"
90
- )
91
- hist.add_argument("--mine", action="store_true")
92
- hist.add_argument("--category")
93
- hist.add_argument("--tag")
94
- hist.add_argument("--since")
95
- hist.add_argument("--until")
96
- hist.add_argument("--limit", type=int)
97
- hist.add_argument("--months", type=int)
98
- hist.add_argument("--count-only", action="store_true")
99
- hist.add_argument("--json", action="store_true")
100
-
101
- valp = sub.add_parser(
102
- "validate",
103
- help="台账体检(默认浅度;--deep 历史读全部月份;两档都不读过程文档)",
104
- )
105
- valp.add_argument("--deep", action="store_true")
106
- valp.add_argument("--json", action="store_true")
107
-
108
- who = sub.add_parser("whoami", help="我是谁(本机身份;未初始化也照常回答)")
109
- who.add_argument("--json", action="store_true")
110
-
111
- initp = sub.add_parser("init", help="初始化本机身份(一台机器只一次)")
112
- initp.add_argument("--claim", help="认领名册里已有的 uuid")
113
- initp.add_argument("--new", action="store_true", dest="new_id", help="新建身份")
114
- initp.add_argument("--name", help="新建身份时的显示名字(必填)")
115
- initp.add_argument(
116
- "--yes",
117
- action="store_true",
118
- help="已禁用:身份写入只能在真终端交互完成",
119
- )
120
-
121
- # 宿主(插件 / SKILL.md)的调用写法是 `progress.py list --repo <仓库>`:
122
- # `--repo` 挂在**子命令后面**,所以每个子命令都要认;子命令给的值覆盖顶层那个。
123
- for parser_ in sub.choices.values():
124
- # default=SUPPRESS:子命令没给就不写回 namespace,否则会把顶层的 --repo 覆盖成 None
125
- parser_.add_argument(
126
- "--repo", default=argparse.SUPPRESS, help=argparse.SUPPRESS
127
- )
128
-
129
- return parser
130
-
131
-
132
- def to_query(args: argparse.Namespace) -> None:
133
- """装配查询条件并挂到 `args.q`(参数 → 查询对象的装配只此一处)。"""
134
- command = getattr(args, "command", "")
135
- if command == "list":
136
- if getattr(args, "mine", False):
137
- scope = "mine"
138
- elif getattr(args, "pool", False):
139
- scope = "pool"
140
- elif getattr(args, "owner", None):
141
- scope = "owner"
142
- else:
143
- scope = "all"
144
- args.q = ListQuery(
145
- scope=scope,
146
- owner=getattr(args, "owner", None),
147
- owner_name=getattr(args, "owner_name", None),
148
- statuses=tuple(
149
- s.strip()
150
- for s in (getattr(args, "status", "") or "").split(",")
151
- if s.strip()
152
- ),
153
- category=getattr(args, "category", None),
154
- tag=getattr(args, "tag", None),
155
- since=_utc_of(getattr(args, "since", None), "--since"),
156
- until=_utc_of(
157
- getattr(args, "until", None), "--until", date_only_ref="23:59:59"
158
- ),
159
- )
160
- elif command == "history":
161
- args.q = HistoryQuery(
162
- owner=getattr(args, "owner", None),
163
- owner_name=getattr(args, "owner_name", None),
164
- mine=bool(getattr(args, "mine", False)),
165
- category=getattr(args, "category", None),
166
- tag=getattr(args, "tag", None),
167
- since=_utc_of(getattr(args, "since", None), "--since"),
168
- until=_utc_of(
169
- getattr(args, "until", None), "--until", date_only_ref="23:59:59"
170
- ),
171
- limit=getattr(args, "limit", None),
172
- months=getattr(args, "months", None),
173
- count_only=bool(getattr(args, "count_only", False)),
174
- )
175
-
176
-
177
- def _utc_of(
178
- value: str | None, field: str, *, date_only_ref: str | None = None
179
- ) -> str | None:
180
- return to_utc(value, field=field, date_only_ref=date_only_ref) if value else None
181
-
182
-
183
- def command_of(args: argparse.Namespace) -> str:
184
- """命令行 → 名册的键(命令写法)。"""
185
- return str(getattr(args, "command", ""))
186
-
187
-
188
- def _split_tags(value: str | None) -> tuple[str, ...]:
189
- if not value:
190
- return ()
191
- # 分隔符与 core/labels._SPLIT_RE 保持一致:少一个「、」就会把
192
- # 「甲、乙」静默合成一个标签(2026-10-24 修)
193
- for sep in (",", "、"):
194
- value = value.replace(sep, ",")
195
- return tuple(t.strip() for t in value.split(",") if t.strip())
196
-
197
-
198
- def resolve_repo(arg: str | None, start: Path | None = None) -> Path:
199
- """`--repo` 给了就校验后用;没给就从 `start`(默认 cwd)往上找。
200
-
201
- 找不到 ⇒ Fail:绝不在当前目录自作主张建台账(2026-10-22 设计定稿)。
202
- 建台账是第一条写命令的事,不需要、也没有 `init` 子命令。
203
- """
204
- if arg:
205
- return as_root(arg, source="--repo")
206
- here = (start or Path.cwd()).resolve()
207
- root = root_of(here)
208
- if root is None:
209
- raise Fail(
210
- "找不到台账:从当前目录往上都没有 ledger/ 目录。\n"
211
- " 处理:用 --repo <工作区根> 指定(根是直接含 ledger/ 的那个目录,不是 ledger/ 本身);\n"
212
- " 或先 cd 到项目根再跑。第一条写命令会自动把 ledger/ 建出来,不用先初始化。"
213
- )
214
- return root
215
-
216
-
217
- def _repo_of(args: argparse.Namespace) -> Path:
218
- """本次命令的工作区根(runner 与锁都用它;`main` 里已解析过一次)。"""
219
- return resolve_repo(getattr(args, "repo", None))
220
-
221
-
222
- def main(argv: list[str] | None = None) -> int:
223
- parser = build_parser()
224
- args = parser.parse_args(argv)
225
- if os.environ.get(NO_SYNC_ENV):
226
- args.no_sync = True
227
- if getattr(args, "json", False) and getattr(args, "count_only", False):
228
- parser.error("--json 与 --count-only 不能同时用")
229
- return USAGE
230
- for field in ("tags",):
231
- if hasattr(args, field):
232
- setattr(args, field, _split_tags(getattr(args, field)))
233
- to_query(args)
234
-
235
- command = command_of(args)
236
- if command == "whoami":
237
- return _run_whoami(args)
238
- if command == "init":
239
- return _run_init(args)
240
- try:
241
- # 定位只做一次:解析好的工作区根挂回 args,runner 与整命令锁都拿它
242
- args.repo = str(resolve_repo(getattr(args, "repo", None)))
243
- if _readonly(command):
244
- return runner.run(command, args) # 只读不碰 git ⇒ 不拿同步锁
245
- with repo_sync_lock(_lock_dir(Path(args.repo))):
246
- return runner.run(command, args)
247
- except Fail as exc:
248
- render.fail_message(exc)
249
- return BUSINESS
250
- finally:
251
- render.flush_notices()
252
-
253
-
254
- def _lock_dir(repo: Path) -> Path:
255
- """整命令锁放 **git 公共目录**里(不进仓库、不被 git status 看见;worktree 之间共享)。
256
-
257
- `repo/.git` 是目录 ⇒ 就是它;是文件 ⇒ 链接 worktree,按 `gitdir:` 指针反推公共目录
258
- (`<common>/worktrees/<name>` 的上级的上级);两者都不成 ⇒ 不是 git 仓库,
259
- 锁文件放系统临时目录(按仓库绝对路径哈希)—— 落用户工作区会被日后
260
- `git init && git add -A` 误提交(2026-10-24 修)。
261
- """
262
- dot = Path(repo) / ".git"
263
- if dot.is_dir():
264
- return dot
265
- if dot.is_file():
266
- try:
267
- text = dot.read_text(encoding="utf-8").strip()
268
- except OSError:
269
- text = ""
270
- if text.startswith("gitdir:"):
271
- common = Path(text.split(":", 1)[1].strip()).parent.parent
272
- if common.is_dir():
273
- return common
274
- import hashlib
275
- import tempfile
276
-
277
- key = hashlib.sha256(str(Path(repo).resolve()).encode("utf-8")).hexdigest()[:16]
278
- lock_dir = Path(tempfile.gettempdir()) / f"dsh-task-ledger-{key}"
279
- lock_dir.mkdir(parents=True, exist_ok=True)
280
- return lock_dir
281
-
282
-
283
- def _print_candidates(opts: list[dict]) -> None:
284
- print("这台机器还没初始化。当前仓库里已有的用户:")
285
- for i, opt in enumerate(opts, 1):
286
- print(f" {i}. @{opt['name']} {opt['uuid']}")
287
-
288
-
289
- def _print_init_result(res: dict) -> None:
290
- name_disp = res.get("name") or res["uuid"][:8]
291
- if res.get("already"):
292
- print(f"已经初始化过,未做改动:@{name_disp} {res['uuid']}")
293
- print(f"文件:{res['path']}")
294
- return
295
- print(f"\x1b[32m✓ 已初始化:\x1b[0m@{name_disp} {res['uuid']}")
296
- print(f"文件:{res['path']}")
297
-
298
-
299
- def _select_identity(opts: list[dict], *, title: str) -> tuple[str, int | None]:
300
- """交互式选身份:↑/↓ 移动、Enter 确认、Ctrl-C 取消,末尾「+ 新建身份」。
301
-
302
- 返回 (kind, index):kind∈{claim,new,cancel};claim/new 时 index 为候选项下标。
303
- TTY 下用 raw 键盘 + 原地重绘(不整屏清屏、不动滚动条);不支持时退回编号输入。
304
- 仅在 TTY 调用。
305
- """
306
- entries = list(opts) + [{"new": True}]
307
- n = len(entries)
308
- height = n + 4 # 标题 + 空行 + n 个选项 + 空行 + 提示
309
-
310
- def _frame(sel: int) -> list[str]:
311
- rows = ["\x1b[1;36m" + title + "\x1b[0m", ""]
312
- for i, e in enumerate(entries):
313
- label = (
314
- "+ 新建身份(自己输入名字)"
315
- if e.get("new")
316
- else f"@{e['name']} {e['uuid']}"
317
- )
318
- rows.append(
319
- "\x1b[7m ❯ " + label + " \x1b[0m" if i == sel else " " + label
320
- )
321
- rows += ["", "\x1b[2m ↑/↓ 移动 Enter 确认 ESC / Ctrl-C 退出\x1b[0m"]
322
- return rows
323
-
324
- try:
325
- fd = sys.stdin.fileno()
326
- if not sys.stdin.isatty():
327
- raise OSError("not a tty")
328
- import os
329
- import select
330
- import termios
331
- import tty
332
-
333
- old = termios.tcgetattr(fd)
334
- sel = 0
335
- drew = False
336
- result: tuple[str, int | None] = ("cancel", None)
337
- try:
338
- tty.setraw(fd)
339
- sys.stdout.write("\x1b[?25l") # 隐藏光标
340
- while True:
341
- if drew:
342
- sys.stdout.write("\x1b[1A" * height) # 相对上移回块顶
343
- else:
344
- sys.stdout.write("\n") # 另起一行,不覆盖 shell 提示
345
- drew = True
346
- # promptui 同款:每行「清整行 + 回车 + 内容 + LF」
347
- for row in _frame(sel):
348
- sys.stdout.write("\x1b[2K\r" + row + "\n")
349
- sys.stdout.flush()
350
-
351
- ch = os.read(fd, 1)
352
- if ch == b"\x1b":
353
- # 区分「单独 ESC(退出)」与「方向键 ESC [ A/B」:短暂等待后续字节
354
- if not select.select([fd], [], [], 0.05)[0]:
355
- result = ("cancel", None)
356
- break
357
- nxt = os.read(fd, 1)
358
- if nxt in (b"[", b"O"):
359
- k = os.read(fd, 1)
360
- if k == b"A":
361
- sel = (sel - 1) % n
362
- elif k == b"B":
363
- sel = (sel + 1) % n
364
- elif ch in (b"\r", b"\n"):
365
- result = (
366
- ("new", None) if entries[sel].get("new") else ("claim", sel)
367
- )
368
- break
369
- elif ch == b"\x03": # Ctrl-C
370
- result = ("cancel", None)
371
- break
372
- finally:
373
- sys.stdout.write("\x1b[?25h") # 显示光标
374
- if drew:
375
- # promptui 同款擦除:逐行「上移 + 清整行 + 回车」,不动主屏滚动
376
- sys.stdout.write("\x1b[1A\x1b[2K\r" * height)
377
- termios.tcsetattr(fd, termios.TCSADRAIN, old)
378
- sys.stdout.flush()
379
- return result
380
- except (OSError, ImportError):
381
- print(title)
382
- for i, o in enumerate(opts, 1):
383
- print(f" {i}. @{o['name']} {o['uuid']}")
384
- print(f" {n}. + 新建身份")
385
- while True:
386
- try:
387
- c = input(f"选编号(1-{n}):").strip()
388
- except (EOFError, KeyboardInterrupt):
389
- return ("cancel", None)
390
- if c.lower() in ("n", "new"):
391
- return ("new", None)
392
- try:
393
- idx = int(c) - 1
394
- except ValueError:
395
- continue
396
- if idx == len(opts):
397
- return ("new", None)
398
- if 0 <= idx < len(opts):
399
- return ("claim", idx)
400
- return ("cancel", None)
401
-
402
-
403
- def _confirm(question: str) -> bool:
404
- """Y/N 确认:Y 返回 True,N 返回 False;Ctrl-C/Ctrl-D 向上抛,当作取消退出。"""
405
- while True:
406
- ans = input(question + " [Y/N]:").strip().lower()
407
- if ans in ("y", "yes"):
408
- return True
409
- if ans in ("n", "no"):
410
- return False
411
- print("请输入 Y 或 N。")
412
-
413
-
414
- def _run_whoami(args: argparse.Namespace) -> int:
415
- """只读机器身份:不解析台账、不碰 git、不看 `--repo`。
416
-
417
- 未初始化**不是错误**:照常回答(`uuid` / `name` 双 null),由调用方据此引导 init。
418
- """
419
- from .. import env
420
-
421
- try:
422
- identity = env.read_identity()
423
- except Fail as exc:
424
- render.fail_message(exc)
425
- return BUSINESS
426
- render.whoami_view(identity, as_json=bool(getattr(args, "json", False)))
427
- return OK
428
-
429
-
430
- def _run_init(args: argparse.Namespace) -> int:
431
- """init 不进 runner、不解析台账、不碰 git:只写机器身份文件。
432
-
433
- 交互式:裸 init 在真终端读名册、让用户箭头选 / 新建;无终端(agent)只报候选、不写。
434
- """
435
- from .. import env
436
- from ..actions.init_identity import init_identity
437
-
438
- repo: Path | None = None
439
- raw = getattr(args, "repo", None)
440
- try:
441
- repo = as_root(raw, source="--repo") if raw else root_of(Path.cwd().resolve())
442
- except Fail:
443
- repo = None
444
- claim = getattr(args, "claim", None)
445
- new = bool(getattr(args, "new_id", False))
446
- name = (getattr(args, "name", "") or "").strip()
447
- yes = bool(getattr(args, "yes", False))
448
- tty = sys.stdin.isatty()
449
-
450
- try:
451
- if yes:
452
- raise Fail(
453
- "--yes 已禁用:身份写入只能在真终端交互完成,Agent 不得代写或重建身份。\n"
454
- " 处理:请用户在自己的终端运行 `python3 progress.py init --repo <工作区根>`。"
455
- )
456
- existing = env.read_identity()
457
- if existing is not None:
458
- # 已初始化:幂等空转(带不带 flag 都一样,零写)
459
- res = init_identity(repo, claim=claim, new=new, name=name)
460
- elif claim or new or name:
461
- # 身份写入只能由用户在真终端交互完成;非终端(agent)一律拒绝(2026-10-26 事故)
462
- if not tty:
463
- raise Fail(
464
- "身份写入只能在真终端交互完成;Agent 不得代写或代为重建身份。\n"
465
- " 处理:请用户在自己的终端运行 `python3 progress.py init --repo <工作区根>`,\n"
466
- " 由 ta 亲自选择/新建并确认。"
467
- )
468
- res = init_identity(repo, claim=claim, new=new, name=name)
469
- else:
470
- # 裸 init:先看名册
471
- res = init_identity(repo, claim=None, new=False, name="")
472
- if res.get("need_choose"):
473
- opts = res.get("options", [])
474
- if not tty:
475
- _print_candidates(opts)
476
- print(
477
- "请重跑:init --claim=<uuid> 认领,或 init --new --name=<名字> 新建"
478
- )
479
- return 0
480
- while True:
481
- kind, idx = _select_identity(
482
- opts, title="这台机器还没初始化。选择你的身份,或新建一个:"
483
- )
484
- if kind == "cancel":
485
- print(
486
- "\x1b[2m✕ 已取消初始化。随时可重跑 init 重新开始。\x1b[0m"
487
- )
488
- return 0
489
- if kind == "new":
490
- prompt_name = input("输入用户名:").strip()
491
- if not prompt_name:
492
- print("\x1b[2m✕ 未输入用户名,已取消。\x1b[0m")
493
- return 0
494
- if not _confirm(
495
- f"您确认要用「{prompt_name}」这个名称新建账户吗?"
496
- ):
497
- continue # N:回到选择菜单重来
498
- res = init_identity(
499
- repo, claim=None, new=True, name=prompt_name
500
- )
501
- break
502
- chosen = opts[idx]
503
- if not _confirm(f"您确认是要使用「@{chosen['name']}」这个身份吗?"):
504
- continue # N:回到选择菜单重来
505
- res = init_identity(repo, claim=chosen["uuid"], new=False, name="")
506
- break
507
- else:
508
- # 名册空:交互输入名字;非终端交给用户去终端跑(init_identity 会报需要名字)
509
- prompt_name = input("输入用户名:").strip() if tty else ""
510
- res = init_identity(repo, claim=None, new=tty, name=prompt_name)
511
- except Fail as exc:
512
- render.fail_message(exc)
513
- return BUSINESS
514
- except (ValueError, IndexError):
515
- render.fail_message(Fail("输入无效,初始化已取消。"))
516
- return BUSINESS
517
- except (KeyboardInterrupt, EOFError):
518
- # 中途 Ctrl-C / Ctrl-D:平静取消,不抛栈、干净退出
519
- print("\x1b[2m✕ 已取消初始化。随时可重跑 init 重新开始。\x1b[0m")
520
- return 0
521
- _print_init_result(res)
522
- return 0
523
-
524
-
525
- def _readonly(command: str) -> bool:
526
- from ..actions.registry import spec_of
527
-
528
- return spec_of(command).readonly
529
-
530
-
531
- if __name__ == "__main__": # pragma: no cover
532
- sys.exit(main())