catherd-cli 1.5.0 → 2.0.0-beta.20261022113309

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 (275) hide show
  1. package/README.md +164 -208
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -247
  5. package/package.json +6 -8
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/bin/catherd-mcp +1 -1
  9. package/plugin/commands/catherd.md +2 -2
  10. package/plugin/skills/catherd/SKILL.md +97 -421
  11. package/plugin/skills/catherd-setup/SKILL.md +20 -19
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +1 -2
  14. package/src/adapters/cursor/home.ts +1 -1
  15. package/src/adapters/cursor/index.ts +4 -4
  16. package/src/cli.ts +22 -27
  17. package/src/domain/bars.ts +33 -107
  18. package/src/domain/blocked.ts +72 -0
  19. package/src/domain/branch.ts +51 -0
  20. package/src/domain/brief.ts +21 -6
  21. package/src/domain/catalog.ts +69 -324
  22. package/src/domain/cost.ts +15 -30
  23. package/src/domain/errors.ts +2 -2
  24. package/src/domain/fail-lines.ts +26 -0
  25. package/src/domain/failover.ts +37 -87
  26. package/src/domain/findings.ts +47 -0
  27. package/src/domain/gate-brief.ts +1 -0
  28. package/src/domain/ids.ts +13 -2
  29. package/src/domain/jev.ts +47 -2
  30. package/src/domain/judge.ts +124 -0
  31. package/src/domain/lane.ts +22 -3
  32. package/src/domain/milestone-memory.ts +53 -0
  33. package/src/domain/notice.ts +25 -8
  34. package/src/domain/notify.ts +32 -0
  35. package/src/domain/plan-doc.ts +198 -0
  36. package/src/domain/plan-lint.ts +141 -0
  37. package/src/domain/profile-rules.ts +101 -160
  38. package/src/domain/profile.ts +116 -63
  39. package/src/domain/record.ts +53 -2
  40. package/src/domain/role-prompts.ts +131 -45
  41. package/src/domain/role-scope.ts +4 -34
  42. package/src/domain/role-tools.ts +2 -2
  43. package/src/domain/roles.ts +2 -0
  44. package/src/domain/route.ts +17 -0
  45. package/src/domain/rules.ts +33 -0
  46. package/src/domain/run-events.ts +298 -0
  47. package/src/domain/run-machine.ts +668 -0
  48. package/src/domain/run-state.ts +1310 -0
  49. package/src/domain/scorecard.ts +197 -0
  50. package/src/domain/select.ts +151 -146
  51. package/src/domain/slopalytics.ts +72 -0
  52. package/src/entry/answer-command.ts +39 -0
  53. package/src/entry/catalog-command.ts +25 -174
  54. package/src/entry/check-bin.ts +17 -0
  55. package/src/entry/cli-kit.ts +1 -1
  56. package/src/entry/daemon-bin.ts +22 -0
  57. package/src/entry/glyphs.ts +15 -2
  58. package/src/entry/init-command.ts +18 -68
  59. package/src/entry/knowledge-command.ts +45 -4
  60. package/src/entry/lock-command.ts +6 -7
  61. package/src/entry/mcp/role-server.ts +18 -0
  62. package/src/entry/mcp/server.ts +13 -40
  63. package/src/entry/mcp/session-tools.ts +149 -0
  64. package/src/entry/panel/openapi.ts +189 -0
  65. package/src/entry/panel/server.ts +410 -0
  66. package/src/entry/panel-command.ts +68 -0
  67. package/src/entry/profile-command.ts +10 -14
  68. package/src/entry/role-cli-command.ts +47 -3
  69. package/src/entry/runs-command.ts +3 -19
  70. package/src/entry/scorecard-command.ts +187 -0
  71. package/src/entry/supervise-bin.ts +1 -1
  72. package/src/infra/env.ts +0 -2
  73. package/src/infra/git.ts +64 -0
  74. package/src/infra/heavy-lock.ts +45 -7
  75. package/src/infra/launch.ts +63 -7
  76. package/src/infra/memory.ts +45 -0
  77. package/src/infra/ntfy.ts +40 -0
  78. package/src/infra/open-url.ts +42 -0
  79. package/src/infra/paths.ts +2 -2
  80. package/src/infra/slopalytics.ts +30 -0
  81. package/src/infra/store.ts +6 -1
  82. package/src/services/admission.ts +24 -9
  83. package/src/services/backends.ts +4 -6
  84. package/src/services/capture.ts +1 -22
  85. package/src/services/catalog-service.ts +155 -262
  86. package/src/services/credentials.ts +4 -20
  87. package/src/services/daemon.ts +181 -0
  88. package/src/services/dispatch-service.ts +5 -1
  89. package/src/services/doctor-backends.ts +1 -1
  90. package/src/services/doctor-checks.ts +47 -0
  91. package/src/services/doctor.ts +4 -9
  92. package/src/services/failover-rungs.ts +109 -0
  93. package/src/services/finalize.ts +12 -0
  94. package/src/services/gate-env.ts +1 -2
  95. package/src/services/gate-service.ts +21 -6
  96. package/src/services/jev-service.ts +2 -4
  97. package/src/services/lane-service.ts +34 -3
  98. package/src/services/milestone-check.ts +433 -0
  99. package/src/services/milestone-tree.ts +170 -0
  100. package/src/services/milestones.ts +35 -8
  101. package/src/services/notifier.ts +69 -2
  102. package/src/services/notify-service.ts +26 -0
  103. package/src/services/panel-setup.ts +116 -0
  104. package/src/services/plan-input.ts +151 -0
  105. package/src/services/ports.ts +30 -4
  106. package/src/services/preflight.ts +82 -8
  107. package/src/services/profile-service.ts +30 -3
  108. package/src/services/profile-store.ts +8 -3
  109. package/src/services/protocol.ts +4 -0
  110. package/src/services/provenance.ts +31 -90
  111. package/src/services/questions.ts +9 -0
  112. package/src/services/reconcile.ts +11 -0
  113. package/src/services/routing-service.ts +26 -15
  114. package/src/services/run-briefs.ts +477 -0
  115. package/src/services/run-client.ts +217 -0
  116. package/src/services/run-effects.ts +837 -0
  117. package/src/services/run-events-store.ts +174 -0
  118. package/src/services/run-evidence.ts +1 -1
  119. package/src/services/run-read.ts +221 -0
  120. package/src/services/run-report.ts +176 -0
  121. package/src/services/run-service.ts +55 -6
  122. package/src/services/run-store.ts +54 -7
  123. package/src/services/scorecard-metrics.ts +86 -0
  124. package/src/services/scorecard.ts +382 -0
  125. package/src/services/summary.ts +27 -3
  126. package/src/services/tree-path.ts +27 -0
  127. package/src/services/workspace-admission.ts +5 -13
  128. package/src/services/workspace-service.ts +5 -0
  129. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  130. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  131. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  132. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  133. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  134. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  135. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  136. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  137. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  138. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  139. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  140. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  141. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  142. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  143. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  144. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  145. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  146. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  147. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  148. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  149. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  150. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  151. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  152. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  153. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  154. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  155. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  156. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  157. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  158. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  159. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  160. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  161. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  162. package/web/dist/assets/index-C5IbylvM.css +2 -0
  163. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  164. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  165. package/web/dist/assets/label-DFSYBc62.js +1 -0
  166. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  167. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  168. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  169. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  170. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  171. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  172. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  173. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  174. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  175. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  176. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  177. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  178. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  179. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  180. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  181. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  182. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  183. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  184. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  185. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  186. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  187. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  188. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  189. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  190. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  191. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  192. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  193. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  194. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  195. package/web/dist/assets/time--9kSQbbp.js +1 -0
  196. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  197. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  198. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  199. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  200. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  201. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  202. package/web/dist/index.html +15 -0
  203. package/catalog/ATTRIBUTION.md +0 -44
  204. package/catalog/scores.json +0 -3202
  205. package/catalog/sources.json +0 -97
  206. package/src/adapters/antigravity/events.ts +0 -131
  207. package/src/adapters/antigravity/home.ts +0 -46
  208. package/src/adapters/antigravity/index.ts +0 -335
  209. package/src/adapters/antigravity/models.ts +0 -37
  210. package/src/adapters/grok/events.ts +0 -112
  211. package/src/adapters/grok/home.ts +0 -187
  212. package/src/adapters/grok/index.ts +0 -358
  213. package/src/adapters/grok/models.ts +0 -48
  214. package/src/domain/calibration.ts +0 -132
  215. package/src/domain/sources.ts +0 -144
  216. package/src/entry/mcp/dispatch-tools.ts +0 -52
  217. package/src/entry/mcp/lane-tools.ts +0 -131
  218. package/src/entry/mcp/protocol-tools.ts +0 -63
  219. package/src/entry/mcp/push-tools.ts +0 -16
  220. package/src/entry/mcp/run-tools.ts +0 -164
  221. package/src/entry/mcp/setup-tools.ts +0 -109
  222. package/src/entry/mcp/workspace-tools.ts +0 -142
  223. package/src/entry/tui/commands.ts +0 -563
  224. package/src/entry/tui/effects.ts +0 -385
  225. package/src/entry/tui/fixtures.ts +0 -637
  226. package/src/entry/tui/profile-edits.ts +0 -146
  227. package/src/entry/tui/profile-tree.ts +0 -429
  228. package/src/entry/tui/providers/app.tsx +0 -162
  229. package/src/entry/tui/providers/boundary.tsx +0 -57
  230. package/src/entry/tui/providers/data.tsx +0 -160
  231. package/src/entry/tui/providers/keymap.tsx +0 -166
  232. package/src/entry/tui/providers/root.tsx +0 -45
  233. package/src/entry/tui/providers/theme.tsx +0 -17
  234. package/src/entry/tui/providers/toast.tsx +0 -77
  235. package/src/entry/tui/run.tsx +0 -114
  236. package/src/entry/tui/state.ts +0 -400
  237. package/src/entry/tui/stories.ts +0 -114
  238. package/src/entry/tui/text.ts +0 -130
  239. package/src/entry/tui/theme.ts +0 -140
  240. package/src/entry/tui/views/app.tsx +0 -208
  241. package/src/entry/tui/views/dialogs.tsx +0 -41
  242. package/src/entry/tui/views/profile-actions.ts +0 -331
  243. package/src/entry/tui/views/profiles.tsx +0 -520
  244. package/src/entry/tui/views/runs.tsx +0 -619
  245. package/src/entry/tui/views/save-dialog.tsx +0 -344
  246. package/src/entry/tui/views/status.tsx +0 -213
  247. package/src/entry/tui/widgets/chrome.tsx +0 -114
  248. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  249. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  250. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  251. package/src/entry/tui/widgets/dialog.tsx +0 -68
  252. package/src/entry/tui/widgets/line.tsx +0 -92
  253. package/src/entry/tui/widgets/list.tsx +0 -197
  254. package/src/entry/tui/widgets/spinner.tsx +0 -49
  255. package/src/entry/tui/widgets/toast.tsx +0 -62
  256. package/src/infra/sources/arena.ts +0 -95
  257. package/src/infra/sources/artificial-analysis.ts +0 -119
  258. package/src/infra/sources/cache.ts +0 -126
  259. package/src/infra/sources/csv.ts +0 -38
  260. package/src/infra/sources/epoch.ts +0 -82
  261. package/src/infra/sources/http.ts +0 -75
  262. package/src/infra/sources/litellm.ts +0 -58
  263. package/src/infra/sources/models-dev.ts +0 -59
  264. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  265. package/src/infra/sources/openrouter-models.ts +0 -60
  266. package/src/infra/sources/rows.ts +0 -38
  267. package/src/infra/sources/vectara.ts +0 -69
  268. package/src/infra/sources/zip.ts +0 -48
  269. package/src/services/catalog-refresh.ts +0 -201
  270. package/src/services/doctor-sources.ts +0 -76
  271. package/src/services/runs-page.ts +0 -290
  272. package/src/services/source-derive.ts +0 -397
  273. package/src/services/source-sync.ts +0 -312
  274. package/src/services/standins.ts +0 -238
  275. package/src/services/treat-likes.ts +0 -113
@@ -1,563 +0,0 @@
1
- // Command ids and default keys follow anomalyco/opencode packages/tui/src/config/keybind.ts
2
- // (MIT, © 2025 opencode); see THIRD_PARTY_NOTICES.md.
3
- import { z } from "zod";
4
- import { CatherdError } from "../../domain/errors.ts";
5
-
6
- /**
7
- * Where a command's keys are live (spec §9.2's context stack global → tab → pane → dialog → input).
8
- * - `global`: in every mode, dialogs included.
9
- * - `app`, `tab.*`, `list`: the base mode (no dialog open); their printable keys only while no text input
10
- * has focus.
11
- * - `row.*`: the base mode, and only while no text input has focus (their keys act on the selected row).
12
- * - `filter`: only while a list's filter input has focus.
13
- * - `dialog`: only while a dialog is open (the keymap's modal mode).
14
- */
15
- export const SCOPES = [
16
- "global",
17
- "app",
18
- "tab.status",
19
- "tab.profiles",
20
- "tab.runs",
21
- "list",
22
- "row.status",
23
- "row.profiles",
24
- "row.runs",
25
- "filter",
26
- "dialog",
27
- ] as const;
28
- export type Scope = (typeof SCOPES)[number];
29
-
30
- type Group = "App" | "Tabs" | "Profiles" | "Edit" | "Status" | "Runs" | "Lists" | "Dialogs";
31
-
32
- export interface CommandDef {
33
- id: string;
34
- title: string;
35
- group: Group;
36
- scope: Scope;
37
- /** default keys, in the keymap's syntax: `ctrl+s`, `shift+g`, `<leader>n`, `space`, `return` */
38
- keys: readonly string[];
39
- /** listed in the command palette */
40
- palette?: true;
41
- /** listed under "Suggested" while the palette's filter is empty */
42
- suggested?: true;
43
- /** footer order; commands without one are never hinted */
44
- hint?: number;
45
- /** the footer's label, when the title is too long for it */
46
- short?: string;
47
- /** the CLI command that does the same, shown in the palette (research C3: the TUI teaches the CLI) */
48
- cli?: string;
49
- }
50
-
51
- /** The leader key and its timeout (spec §9.2: `ctrl+x`, 2 s, as in opencode). */
52
- export const LEADER = "ctrl+x";
53
- export const LEADER_TIMEOUT_MS = 2_000;
54
-
55
- /** Every action in the TUI. The palette, footer, help and keys all come from this one table. */
56
- export const COMMANDS = [
57
- // global: live in every mode
58
- { id: "app.interrupt", title: "Cancel, or quit", group: "App", scope: "global", keys: ["ctrl+c"] },
59
- // hinted only while a dialog is open (the Footer), where esc cancels it
60
- {
61
- id: "app.back",
62
- title: "Back",
63
- group: "App",
64
- scope: "global",
65
- keys: ["escape"],
66
- hint: 80,
67
- short: "cancel",
68
- },
69
-
70
- // app: any tab, no dialog open
71
- {
72
- id: "app.palette",
73
- title: "Command palette",
74
- group: "App",
75
- scope: "app",
76
- keys: ["ctrl+p", ":"],
77
- hint: 90,
78
- short: "commands",
79
- },
80
- {
81
- id: "app.help",
82
- title: "Keyboard shortcuts",
83
- group: "App",
84
- scope: "app",
85
- keys: ["?"],
86
- palette: true,
87
- hint: 91,
88
- short: "help",
89
- },
90
- { id: "app.quit", title: "Quit", group: "App", scope: "app", keys: ["q", "<leader>q"], palette: true },
91
- {
92
- id: "app.stories",
93
- title: "Stories…",
94
- group: "App",
95
- scope: "app",
96
- keys: [],
97
- palette: true,
98
- suggested: true,
99
- },
100
- {
101
- id: "tab.status",
102
- title: "Go to Status",
103
- group: "Tabs",
104
- scope: "app",
105
- keys: ["1", "<leader>1"],
106
- palette: true,
107
- },
108
- {
109
- id: "tab.profiles",
110
- title: "Go to Profiles",
111
- group: "Tabs",
112
- scope: "app",
113
- keys: ["2", "<leader>2"],
114
- palette: true,
115
- },
116
- {
117
- id: "tab.runs",
118
- title: "Go to Runs",
119
- group: "Tabs",
120
- scope: "app",
121
- keys: ["3", "<leader>3"],
122
- palette: true,
123
- },
124
- { id: "tab.next", title: "Next tab", group: "Tabs", scope: "app", keys: ["]"] },
125
- { id: "tab.prev", title: "Previous tab", group: "Tabs", scope: "app", keys: ["["] },
126
- {
127
- id: "profile.new",
128
- title: "New profile…",
129
- group: "Profiles",
130
- scope: "app",
131
- keys: ["<leader>n"],
132
- palette: true,
133
- suggested: true,
134
- cli: "catherd profile new <name>",
135
- },
136
- {
137
- id: "profile.list",
138
- title: "Switch profile…",
139
- group: "Profiles",
140
- scope: "app",
141
- keys: ["<leader>l"],
142
- palette: true,
143
- suggested: true,
144
- cli: "catherd profile list",
145
- },
146
-
147
- // tab.status
148
- {
149
- id: "status.recheck",
150
- title: "Re-check setup",
151
- group: "Status",
152
- scope: "tab.status",
153
- keys: ["r"],
154
- palette: true,
155
- suggested: true,
156
- hint: 3,
157
- short: "re-check",
158
- cli: "catherd doctor",
159
- },
160
- {
161
- id: "status.copy",
162
- title: "Copy the fix command",
163
- group: "Status",
164
- scope: "tab.status",
165
- keys: ["y"],
166
- palette: true,
167
- hint: 2,
168
- short: "copy fix",
169
- },
170
-
171
- // tab.profiles
172
- {
173
- id: "profile.save",
174
- title: "Save profile…",
175
- group: "Profiles",
176
- scope: "tab.profiles",
177
- keys: ["ctrl+s"],
178
- palette: true,
179
- suggested: true,
180
- hint: 3,
181
- short: "save",
182
- cli: "catherd profile set <path> <value>",
183
- },
184
- {
185
- id: "profile.activate",
186
- title: "Make this profile active…",
187
- group: "Profiles",
188
- scope: "tab.profiles",
189
- keys: ["a"],
190
- palette: true,
191
- hint: 6,
192
- short: "activate",
193
- cli: "catherd profile use <name> [--repo]",
194
- },
195
- {
196
- id: "profile.copy",
197
- title: "Copy this profile…",
198
- group: "Profiles",
199
- scope: "tab.profiles",
200
- keys: [],
201
- palette: true,
202
- cli: "catherd profile copy <from> <name>",
203
- },
204
- {
205
- id: "profile.revert",
206
- title: "Discard unsaved changes…",
207
- group: "Profiles",
208
- scope: "tab.profiles",
209
- keys: [],
210
- palette: true,
211
- },
212
- {
213
- id: "catalog.refresh",
214
- title: "Sync the public sources and refresh the model catalog",
215
- group: "Profiles",
216
- scope: "tab.profiles",
217
- keys: ["r"],
218
- palette: true,
219
- cli: "catherd catalog sync",
220
- },
221
- {
222
- id: "edit.undo",
223
- title: "Undo",
224
- group: "Edit",
225
- scope: "tab.profiles",
226
- keys: ["<leader>u"],
227
- palette: true,
228
- hint: 7,
229
- short: "undo",
230
- },
231
- {
232
- id: "edit.redo",
233
- title: "Redo",
234
- group: "Edit",
235
- scope: "tab.profiles",
236
- keys: ["<leader>r"],
237
- palette: true,
238
- },
239
-
240
- // tab.runs
241
- {
242
- id: "runs.refresh",
243
- title: "Refresh now",
244
- group: "Runs",
245
- scope: "tab.runs",
246
- keys: ["r"],
247
- palette: true,
248
- hint: 3,
249
- short: "refresh",
250
- },
251
- {
252
- id: "runs.pause",
253
- title: "Pause or resume updates",
254
- group: "Runs",
255
- scope: "tab.runs",
256
- keys: ["p"],
257
- palette: true,
258
- hint: 4,
259
- short: "pause",
260
- },
261
-
262
- // list: every list and tree
263
- { id: "list.up", title: "Move up", group: "Lists", scope: "list", keys: ["up", "k"] },
264
- { id: "list.down", title: "Move down", group: "Lists", scope: "list", keys: ["down", "j"] },
265
- { id: "list.pageUp", title: "Page up", group: "Lists", scope: "list", keys: ["pageup"] },
266
- { id: "list.pageDown", title: "Page down", group: "Lists", scope: "list", keys: ["pagedown"] },
267
- { id: "list.first", title: "First row", group: "Lists", scope: "list", keys: ["home", "g"] },
268
- { id: "list.last", title: "Last row", group: "Lists", scope: "list", keys: ["end", "shift+g"] },
269
- {
270
- id: "list.filter",
271
- title: "Filter",
272
- group: "Lists",
273
- scope: "list",
274
- keys: ["/"],
275
- hint: 8,
276
- short: "filter",
277
- },
278
-
279
- // row.*: act on the selected row
280
- {
281
- id: "status.open",
282
- title: "Open",
283
- group: "Status",
284
- scope: "row.status",
285
- keys: ["return"],
286
- hint: 1,
287
- short: "open",
288
- },
289
- {
290
- id: "tree.toggle",
291
- title: "Toggle",
292
- group: "Profiles",
293
- scope: "row.profiles",
294
- keys: ["space"],
295
- hint: 1,
296
- short: "toggle",
297
- },
298
- {
299
- id: "tree.open",
300
- title: "Change or expand",
301
- group: "Profiles",
302
- scope: "row.profiles",
303
- keys: ["return"],
304
- hint: 2,
305
- short: "change",
306
- },
307
- { id: "tree.expand", title: "Expand", group: "Profiles", scope: "row.profiles", keys: ["right", "l"] },
308
- {
309
- id: "tree.detail",
310
- title: "Show the rung's values, sources and runs",
311
- group: "Profiles",
312
- scope: "row.profiles",
313
- keys: ["i"],
314
- palette: true,
315
- cli: "catherd catalog list --json",
316
- },
317
- {
318
- id: "tree.treatLike",
319
- title: "Treat the rung like another…",
320
- group: "Profiles",
321
- scope: "row.profiles",
322
- keys: ["t"],
323
- palette: true,
324
- cli: "catherd catalog treat-like --suggest <rung>",
325
- },
326
- {
327
- id: "tree.collapse",
328
- title: "Collapse, or go to the parent",
329
- group: "Profiles",
330
- scope: "row.profiles",
331
- keys: ["left", "h"],
332
- },
333
- {
334
- id: "runs.open",
335
- title: "Open the session, milestone or role",
336
- group: "Runs",
337
- scope: "row.runs",
338
- keys: ["return"],
339
- hint: 1,
340
- short: "open",
341
- },
342
- {
343
- id: "runs.cancel",
344
- title: "Cancel the selected role (press twice)",
345
- group: "Runs",
346
- scope: "row.runs",
347
- keys: ["ctrl+d"],
348
- hint: 2,
349
- short: "cancel",
350
- cli: "catherd runs cancel <run> <name>",
351
- },
352
-
353
- // filter: a list's filter input has focus
354
- {
355
- id: "filter.accept",
356
- title: "Keep the filter",
357
- group: "Lists",
358
- scope: "filter",
359
- keys: ["return"],
360
- hint: 1,
361
- short: "done",
362
- },
363
-
364
- // dialog: a dialog is open
365
- // hints with the same short label and order show as one: `↑↓ move`
366
- {
367
- id: "dialog.up",
368
- title: "Previous",
369
- group: "Dialogs",
370
- scope: "dialog",
371
- keys: ["up", "ctrl+p"],
372
- hint: 0,
373
- short: "move",
374
- },
375
- {
376
- id: "dialog.down",
377
- title: "Next",
378
- group: "Dialogs",
379
- scope: "dialog",
380
- keys: ["down", "ctrl+n"],
381
- hint: 0,
382
- short: "move",
383
- },
384
- { id: "dialog.pageUp", title: "Page up", group: "Dialogs", scope: "dialog", keys: ["pageup"] },
385
- { id: "dialog.pageDown", title: "Page down", group: "Dialogs", scope: "dialog", keys: ["pagedown"] },
386
- {
387
- id: "dialog.left",
388
- title: "Previous button",
389
- group: "Dialogs",
390
- scope: "dialog",
391
- keys: ["left", "shift+tab"],
392
- hint: 0,
393
- short: "button",
394
- },
395
- {
396
- id: "dialog.right",
397
- title: "Next button",
398
- group: "Dialogs",
399
- scope: "dialog",
400
- keys: ["right", "tab"],
401
- hint: 0,
402
- short: "button",
403
- },
404
- {
405
- id: "dialog.submit",
406
- title: "Choose",
407
- group: "Dialogs",
408
- scope: "dialog",
409
- keys: ["return"],
410
- hint: 1,
411
- short: "choose",
412
- },
413
- {
414
- id: "dialog.delete",
415
- title: "Delete (press twice)",
416
- group: "Dialogs",
417
- scope: "dialog",
418
- keys: ["ctrl+d"],
419
- hint: 2,
420
- short: "delete",
421
- },
422
- ] as const satisfies readonly CommandDef[];
423
-
424
- export type CommandId = (typeof COMMANDS)[number]["id"];
425
- export type Keybinds = Record<CommandId, readonly string[]>;
426
-
427
- const BY_ID = new Map<string, CommandDef>(COMMANDS.map((c) => [c.id, c]));
428
- export const commandsIn = (scope: Scope): CommandDef[] => COMMANDS.filter((c) => c.scope === scope);
429
-
430
- /** Keys a text input would take as typing: one character, `space`, or a shifted letter. */
431
- export const isPrintable = (key: string): boolean =>
432
- [...key].length === 1 || key === "space" || /^shift\+.$/.test(key);
433
-
434
- /** Scopes whose printable keys go quiet while a text input has focus. */
435
- const GUARDED: readonly Scope[] = ["app", "tab.status", "tab.profiles", "tab.runs", "list"];
436
- export const isGuarded = (scope: Scope): boolean => GUARDED.includes(scope);
437
-
438
- /** Commands only the ctrl+c layering may run; rebinding them could leave no way out. */
439
- const RESERVED: readonly CommandId[] = ["app.interrupt", "app.back"];
440
-
441
- export const DEFAULT_KEYS: Keybinds = Object.fromEntries(
442
- COMMANDS.map((c): [string, readonly string[]] => [c.id, c.keys]),
443
- ) as Keybinds;
444
-
445
- /** Spec §9.2 `config.json` `keybinds`: `{ <command id>: string | string[] | "none" }`. */
446
- const KeybindsSchema = z.record(z.string(), z.union([z.string().min(1), z.array(z.string().min(1))]));
447
-
448
- /** The stacks of scopes that can be live at once; a key must mean one thing in each. */
449
- export const CONTEXTS: readonly { tab: "status" | "profiles" | "runs" | null; typing: boolean }[] = [
450
- ...(["status", "profiles", "runs"] as const).flatMap((tab) => [
451
- { tab, typing: false },
452
- { tab, typing: true },
453
- ]),
454
- { tab: null, typing: true },
455
- ];
456
-
457
- /** The keys live in one context, with the command each would run. */
458
- export function keysIn(ctx: (typeof CONTEXTS)[number], keys: Keybinds): [string, CommandId][] {
459
- const scopes: Scope[] =
460
- ctx.tab === null
461
- ? ["global", "dialog"]
462
- : ["global", "app", `tab.${ctx.tab}`, "list", ctx.typing ? "filter" : `row.${ctx.tab}`];
463
- const out: [string, CommandId][] = [];
464
- for (const c of COMMANDS) {
465
- if (!scopes.includes(c.scope)) continue;
466
- for (const k of keys[c.id])
467
- if (!(ctx.typing && isGuarded(c.scope) && isPrintable(k))) out.push([k, c.id]);
468
- }
469
- return out;
470
- }
471
-
472
- /** Every key that means two things somewhere: `"<key>: <id>, <id> (<context>)"`. */
473
- export function duplicateKeys(keys: Keybinds): string[] {
474
- const out: string[] = [];
475
- for (const ctx of CONTEXTS) {
476
- const seen = new Map<string, CommandId>();
477
- for (const [k, id] of keysIn(ctx, keys)) {
478
- const first = seen.get(k);
479
- if (first && first !== id)
480
- out.push(`${k}: ${first}, ${id} (${ctx.tab ?? "dialog"}${ctx.typing && ctx.tab ? ", typing" : ""})`);
481
- else seen.set(k, id);
482
- }
483
- }
484
- return [...new Set(out)];
485
- }
486
-
487
- /**
488
- * The keys in force: the defaults with `config.json`'s `keybinds` over them. Unknown ids, reserved
489
- * commands, the leader as a command's own key, a typed key on a command live while the user types, and
490
- * overrides that make one key mean two things are refused with `E_CONFIG_KEYBIND`.
491
- */
492
- export function resolveKeybinds(raw: unknown): Keybinds {
493
- const keys: Keybinds = { ...DEFAULT_KEYS };
494
- if (raw === undefined) return keys;
495
- const parsed = KeybindsSchema.safeParse(raw);
496
- if (!parsed.success)
497
- throw new CatherdError("E_CONFIG_KEYBIND", "config.json keybinds must map command ids to keys", {
498
- fix: 'e.g. "keybinds": { "profile.save": "ctrl+w", "app.help": "none" }',
499
- });
500
- for (const [id, value] of Object.entries(parsed.data)) {
501
- if (!BY_ID.has(id))
502
- throw new CatherdError("E_CONFIG_KEYBIND", `config.json keybinds: no command "${id}"`, {
503
- fix: "the command palette (ctrl+p) and catherd's README list the command ids",
504
- });
505
- if ((RESERVED as readonly string[]).includes(id))
506
- throw new CatherdError("E_CONFIG_KEYBIND", `config.json keybinds: "${id}" cannot be rebound`, {
507
- fix: `remove "${id}" from keybinds`,
508
- });
509
- // a comma list is several keys, as in opencode's config
510
- const list = (Array.isArray(value) ? value : [value]).flatMap((v) => v.split(",").map((k) => k.trim()));
511
- if (list.some((k) => k.split(" ")[0] === LEADER))
512
- throw new CatherdError(
513
- "E_CONFIG_KEYBIND",
514
- `config.json keybinds: ${id} cannot take ${LEADER}, the leader`,
515
- {
516
- fix: `bind ${id} to another key, or to the leader and a key: "<leader>w"`,
517
- },
518
- );
519
- // dialog and filter commands are live while the user types: a typed key would take letters from the text
520
- const scope = (BY_ID.get(id) as CommandDef).scope;
521
- const typed = list.find(isPrintable);
522
- if ((scope === "dialog" || scope === "filter") && typed)
523
- throw new CatherdError(
524
- "E_CONFIG_KEYBIND",
525
- `config.json keybinds: ${id} works while you type, so it cannot take "${typed}"`,
526
- { fix: `bind ${id} to a key with ctrl or alt, or a named key such as "return"` },
527
- );
528
- keys[id as CommandId] = list.includes("none") ? [] : list.filter(Boolean);
529
- }
530
- const dup = duplicateKeys(keys);
531
- if (dup.length)
532
- throw new CatherdError(
533
- "E_CONFIG_KEYBIND",
534
- `config.json keybinds give one key two meanings: ${dup.join("; ")}`,
535
- {
536
- fix: 'bind one of them to another key, or to "none"',
537
- },
538
- );
539
- return keys;
540
- }
541
-
542
- const WORDS: Record<string, string> = {
543
- return: "enter",
544
- escape: "esc",
545
- pageup: "pgup",
546
- pagedown: "pgdn",
547
- up: "↑",
548
- down: "↓",
549
- left: "←",
550
- right: "→",
551
- };
552
- const PLAIN_WORDS: Record<string, string> = { up: "up", down: "down", left: "left", right: "right" };
553
-
554
- /** How a key reads in the footer, palette and help: `ctrl+x n`, `enter`, `G`, `↑`. */
555
- export function formatKey(key: string, plain = false): string {
556
- if (key.startsWith("<leader>")) return `${LEADER} ${formatKey(key.slice("<leader>".length), plain)}`;
557
- if (/^shift\+[a-z]$/.test(key)) return key.slice(-1).toUpperCase();
558
- return (plain ? PLAIN_WORDS[key] : undefined) ?? WORDS[key] ?? key;
559
- }
560
-
561
- /** All of a command's keys, `/`-joined, or "" when it has none. */
562
- export const formatKeys = (keys: readonly string[], plain = false): string =>
563
- keys.map((k) => formatKey(k, plain)).join("/");