@starlove/orbit 1.4.0 → 2.0.0

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 (329) hide show
  1. package/Dockerfile +10 -8
  2. package/README.md +146 -164
  3. package/dist/full/csp.txt +1 -0
  4. package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
  5. package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
  6. package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
  7. package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
  8. package/dist/full/fonts/Mona_Sans/README.txt +10 -0
  9. package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
  10. package/dist/full/fonts/Newsreader/README.txt +8 -0
  11. package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
  12. package/dist/full/index.html +36 -653
  13. package/dist/full/js/app.BmuponPJ.js +32 -0
  14. package/dist/full/js/app.BmuponPJ.js.map +1 -0
  15. package/dist/full/js/chunk.1fJGQGbW.js +2 -0
  16. package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
  17. package/dist/full/js/chunk.5D7JO2zp.js +2 -0
  18. package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
  19. package/dist/full/js/chunk.BrNn_D79.js +2 -0
  20. package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
  21. package/dist/full/js/chunk.CokJXIqM.js +2 -0
  22. package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
  23. package/dist/full/js/chunk.D_Haqy6R.js +2 -0
  24. package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
  25. package/dist/full/js/chunk.RO0jsH6I.js +15 -0
  26. package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
  27. package/dist/full/js/chunk.dqWOEtBM.js +2 -0
  28. package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
  29. package/dist/full/js/warm.f260bbbc.js +15 -0
  30. package/dist/full/orbits-favicon.png +0 -0
  31. package/dist/full/orbits-logo-bg-wide.png +0 -0
  32. package/dist/full/orbits-logo-bg.png +0 -0
  33. package/dist/full/orbits-logo.png +0 -0
  34. package/dist/full/styles.2b154ffe.css +1 -0
  35. package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
  36. package/dist/full/version.json +2 -2
  37. package/docs/AGENT_GUIDE.md +114 -0
  38. package/docs/UPDATING.md +249 -64
  39. package/lib/adapters/indexeddb/blobs.d.ts +3 -0
  40. package/lib/adapters/indexeddb/blobs.js +54 -0
  41. package/lib/adapters/indexeddb/database.d.ts +60 -0
  42. package/lib/adapters/indexeddb/database.js +126 -0
  43. package/lib/adapters/indexeddb/index.d.ts +5 -0
  44. package/lib/adapters/indexeddb/index.js +3 -0
  45. package/lib/adapters/indexeddb/locks.d.ts +14 -0
  46. package/lib/adapters/indexeddb/locks.js +40 -0
  47. package/lib/adapters/indexeddb/store.d.ts +40 -0
  48. package/lib/adapters/indexeddb/store.js +424 -0
  49. package/lib/adapters/indexeddb/tables.d.ts +43 -0
  50. package/lib/adapters/indexeddb/tables.js +127 -0
  51. package/lib/adapters/memory/blobs.d.ts +3 -0
  52. package/lib/adapters/memory/blobs.js +44 -0
  53. package/lib/adapters/memory/board.d.ts +41 -0
  54. package/lib/adapters/memory/board.js +521 -0
  55. package/lib/adapters/memory/index.d.ts +2 -0
  56. package/lib/adapters/memory/index.js +2 -0
  57. package/lib/adapters/memory/search.d.ts +6 -0
  58. package/lib/adapters/memory/search.js +30 -0
  59. package/lib/adapters/memory/store.d.ts +34 -0
  60. package/lib/adapters/memory/store.js +266 -0
  61. package/lib/adapters/sqlite/backups.d.ts +53 -0
  62. package/lib/adapters/sqlite/backups.js +232 -0
  63. package/lib/adapters/sqlite/blobs.d.ts +14 -0
  64. package/lib/adapters/sqlite/blobs.js +109 -0
  65. package/lib/adapters/sqlite/board.d.ts +77 -0
  66. package/lib/adapters/sqlite/board.js +328 -0
  67. package/lib/adapters/sqlite/database.d.ts +67 -0
  68. package/lib/adapters/sqlite/database.js +285 -0
  69. package/lib/adapters/sqlite/index.d.ts +3 -0
  70. package/lib/adapters/sqlite/index.js +2 -0
  71. package/lib/adapters/sqlite/lock.d.ts +11 -0
  72. package/lib/adapters/sqlite/lock.js +34 -0
  73. package/lib/adapters/sqlite/records.d.ts +54 -0
  74. package/lib/adapters/sqlite/records.js +546 -0
  75. package/lib/adapters/sqlite/schema.d.ts +54 -0
  76. package/lib/adapters/sqlite/schema.js +276 -0
  77. package/lib/adapters/sqlite/store.d.ts +39 -0
  78. package/lib/adapters/sqlite/store.js +385 -0
  79. package/lib/browser/in-page.d.ts +31 -0
  80. package/lib/browser/in-page.js +46 -0
  81. package/lib/browser/old-preview.d.ts +13 -0
  82. package/lib/browser/old-preview.js +80 -0
  83. package/lib/browser/preview-board.d.ts +8 -0
  84. package/lib/browser/preview-board.js +69 -0
  85. package/lib/cli/main.d.ts +7 -0
  86. package/lib/cli/main.js +343 -0
  87. package/lib/cli/orbit.d.ts +8 -0
  88. package/lib/cli/orbit.js +25 -0
  89. package/lib/client/http.d.ts +38 -0
  90. package/lib/client/http.js +334 -0
  91. package/lib/client/local.d.ts +26 -0
  92. package/lib/client/local.js +190 -0
  93. package/lib/client/object-url.d.ts +8 -0
  94. package/lib/client/object-url.js +21 -0
  95. package/lib/client/types.d.ts +268 -0
  96. package/lib/client/types.js +8 -0
  97. package/lib/commands/agent-tools.d.ts +31 -0
  98. package/lib/commands/agent-tools.js +100 -0
  99. package/lib/commands/index.d.ts +56 -0
  100. package/lib/commands/index.js +298 -0
  101. package/lib/commands/parse.d.ts +73 -0
  102. package/lib/commands/parse.js +231 -0
  103. package/lib/commands/present.d.ts +134 -0
  104. package/lib/commands/present.js +198 -0
  105. package/lib/commands/session.d.ts +85 -0
  106. package/lib/commands/session.js +138 -0
  107. package/lib/commands/verbs.d.ts +35 -0
  108. package/lib/commands/verbs.js +842 -0
  109. package/lib/core/actors.d.ts +18 -0
  110. package/lib/core/actors.js +45 -0
  111. package/lib/core/agent.d.ts +128 -0
  112. package/lib/core/agent.js +323 -0
  113. package/lib/core/boards.d.ts +113 -0
  114. package/lib/core/boards.js +288 -0
  115. package/lib/core/bootstrap.d.ts +47 -0
  116. package/lib/core/bootstrap.js +100 -0
  117. package/lib/core/bytes.d.ts +10 -0
  118. package/lib/core/bytes.js +52 -0
  119. package/lib/core/card-paths.d.ts +25 -0
  120. package/lib/core/card-paths.js +44 -0
  121. package/lib/core/context.d.ts +60 -0
  122. package/lib/core/context.js +42 -0
  123. package/lib/core/cross-board.d.ts +81 -0
  124. package/lib/core/cross-board.js +228 -0
  125. package/lib/core/describe.d.ts +21 -0
  126. package/lib/core/describe.js +81 -0
  127. package/lib/core/errors.d.ts +159 -0
  128. package/lib/core/errors.js +160 -0
  129. package/lib/core/files.d.ts +79 -0
  130. package/lib/core/files.js +262 -0
  131. package/lib/core/history.d.ts +20 -0
  132. package/lib/core/history.js +53 -0
  133. package/lib/core/image-types.d.ts +16 -0
  134. package/lib/core/image-types.js +36 -0
  135. package/lib/core/model.d.ts +263 -0
  136. package/lib/core/model.js +34 -0
  137. package/lib/core/nebula-link.d.ts +14 -0
  138. package/lib/core/nebula-link.js +39 -0
  139. package/lib/core/relations.d.ts +58 -0
  140. package/lib/core/relations.js +112 -0
  141. package/lib/core/search.d.ts +38 -0
  142. package/lib/core/search.js +92 -0
  143. package/lib/core/sessions.d.ts +44 -0
  144. package/lib/core/sessions.js +158 -0
  145. package/lib/core/snapshot-schema.d.ts +295 -0
  146. package/lib/core/snapshot-schema.js +82 -0
  147. package/lib/core/snapshots.d.ts +75 -0
  148. package/lib/core/snapshots.js +409 -0
  149. package/lib/core/states.d.ts +25 -0
  150. package/lib/core/states.js +126 -0
  151. package/lib/core/text.d.ts +40 -0
  152. package/lib/core/text.js +113 -0
  153. package/lib/core/tickets.d.ts +69 -0
  154. package/lib/core/tickets.js +311 -0
  155. package/lib/core/trello.d.ts +17 -0
  156. package/lib/core/trello.js +269 -0
  157. package/lib/core/verdicts.d.ts +28 -0
  158. package/lib/core/verdicts.js +92 -0
  159. package/lib/core/views.d.ts +95 -0
  160. package/lib/core/views.js +186 -0
  161. package/lib/http/app.d.ts +46 -0
  162. package/lib/http/app.js +491 -0
  163. package/lib/mcp-server.d.ts +10 -0
  164. package/lib/mcp-server.js +120 -0
  165. package/lib/node/assets.d.ts +7 -0
  166. package/lib/node/assets.js +68 -0
  167. package/lib/node/data-folder.d.ts +21 -0
  168. package/lib/node/data-folder.js +34 -0
  169. package/lib/node/env.d.ts +38 -0
  170. package/lib/node/env.js +49 -0
  171. package/lib/node/host-check.d.ts +24 -0
  172. package/lib/node/host-check.js +53 -0
  173. package/lib/node/host-services.d.ts +9 -0
  174. package/lib/node/host-services.js +150 -0
  175. package/lib/node/local.d.ts +22 -0
  176. package/lib/node/local.js +61 -0
  177. package/lib/node/migrate.d.ts +53 -0
  178. package/lib/node/migrate.js +635 -0
  179. package/lib/node/package.d.ts +7 -0
  180. package/lib/node/package.js +14 -0
  181. package/lib/node/repo-board.d.ts +49 -0
  182. package/lib/node/repo-board.js +245 -0
  183. package/lib/node/serve.d.ts +8 -0
  184. package/lib/node/serve.js +106 -0
  185. package/lib/orbit.d.ts +47 -0
  186. package/lib/orbit.js +44 -0
  187. package/lib/quiet-sqlite-warning.d.ts +1 -0
  188. package/lib/quiet-sqlite-warning.js +18 -0
  189. package/lib/store/blob-path.d.ts +6 -0
  190. package/lib/store/blob-path.js +12 -0
  191. package/lib/store/conformance.d.ts +52 -0
  192. package/lib/store/conformance.js +1449 -0
  193. package/lib/store/search-words.d.ts +10 -0
  194. package/lib/store/search-words.js +19 -0
  195. package/lib/store/types.d.ts +452 -0
  196. package/lib/store/types.js +34 -0
  197. package/llms.txt +63 -0
  198. package/package.json +109 -25
  199. package/src/cli/orbit.js +11 -658
  200. package/src/mcp-server.js +16 -876
  201. package/SKILL-ORBIT.md +0 -151
  202. package/build.mjs +0 -210
  203. package/dist/full/js/app.62905fcb.js +0 -952
  204. package/dist/full/js/app.62905fcb.js.map +0 -7
  205. package/dist/full/styles.4e60d824.css +0 -1
  206. package/docs/AGENT_PROTOCOL.md +0 -424
  207. package/docs/MCP_SETUP.md +0 -54
  208. package/docs/ORBIT_DISPATCH.md +0 -280
  209. package/docs/SS-Orbit-Dark.png +0 -0
  210. package/docs/SS-Orbit-Light.png +0 -0
  211. package/docs/mcp-client.example.json +0 -13
  212. package/docs/setup/orbit-init-wizard.md +0 -174
  213. package/public/apple-touch-icon.png +0 -0
  214. package/public/cursors/live_arrow.cur +0 -0
  215. package/public/cursors/live_link.cur +0 -0
  216. package/public/cursors/live_move.cur +0 -0
  217. package/public/cursors/live_select.cur +0 -0
  218. package/public/cursors/live_unavail.cur +0 -0
  219. package/public/favicon-32x32.png +0 -0
  220. package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
  221. package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
  222. package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
  223. package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
  224. package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
  225. package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
  226. package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
  227. package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
  228. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
  229. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
  230. package/public/fonts/Chakra_Petch/OFL.txt +0 -93
  231. package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
  232. package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
  233. package/public/fonts/Inter/OFL.txt +0 -93
  234. package/public/fonts/Inter/README.txt +0 -118
  235. package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
  236. package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
  237. package/public/fonts/JetBrains_Mono/README.txt +0 -79
  238. package/public/fonts/OpenRing-Regular.ttf +0 -0
  239. package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  240. package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  241. package/public/fonts/Open_Sans/README.txt +0 -100
  242. package/public/fonts/Orbitron/OFL.txt +0 -93
  243. package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
  244. package/public/fonts/Orbitron/README.txt +0 -68
  245. package/public/fonts/Rajdhani/OFL.txt +0 -93
  246. package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
  247. package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
  248. package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
  249. package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
  250. package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
  251. package/public/fonts/runescape-uf.ttf +0 -0
  252. package/public/index.html +0 -769
  253. package/public/js/actor-labels.js +0 -65
  254. package/public/js/api.js +0 -39
  255. package/public/js/app.js +0 -45
  256. package/public/js/artifact-drawer.js +0 -79
  257. package/public/js/board-export.js +0 -161
  258. package/public/js/board-menu.js +0 -374
  259. package/public/js/card-actions.js +0 -545
  260. package/public/js/config.js +0 -20
  261. package/public/js/create-card.js +0 -190
  262. package/public/js/dom.js +0 -19
  263. package/public/js/drawer.js +0 -273
  264. package/public/js/font-preference.js +0 -229
  265. package/public/js/format.js +0 -433
  266. package/public/js/help-menu.js +0 -31
  267. package/public/js/icons.js +0 -16
  268. package/public/js/kanban.js +0 -694
  269. package/public/js/local-backend.js +0 -1015
  270. package/public/js/main.js +0 -161
  271. package/public/js/modal.js +0 -33
  272. package/public/js/motion-preference.js +0 -64
  273. package/public/js/panel.js +0 -58
  274. package/public/js/router.js +0 -134
  275. package/public/js/search.js +0 -56
  276. package/public/js/settings-tabs.js +0 -7
  277. package/public/js/settings.js +0 -1634
  278. package/public/js/sse.js +0 -159
  279. package/public/js/starscape.js +0 -332
  280. package/public/js/state.js +0 -129
  281. package/public/js/text-editing.js +0 -128
  282. package/public/js/theme-preference.js +0 -95
  283. package/public/js/ticket-detail.js +0 -1516
  284. package/public/js/ticket-typeahead.js +0 -49
  285. package/public/js/toast.js +0 -89
  286. package/public/js/topbar-notes.js +0 -155
  287. package/public/js/transport.js +0 -22
  288. package/public/js/typeahead.js +0 -206
  289. package/public/js/unread.js +0 -121
  290. package/public/js/update-check.js +0 -58
  291. package/public/js/url-routes.js +0 -94
  292. package/public/orbits-favicon.png +0 -0
  293. package/public/orbits-logo-bg-wide.png +0 -0
  294. package/public/orbits-logo-bg.png +0 -0
  295. package/public/orbits-logo.png +0 -0
  296. package/public/styles.css +0 -7709
  297. package/src/core/agent.js +0 -387
  298. package/src/core/agents-md.js +0 -109
  299. package/src/core/artifacts.js +0 -149
  300. package/src/core/attachments.js +0 -238
  301. package/src/core/auth.js +0 -85
  302. package/src/core/backups.js +0 -142
  303. package/src/core/board.js +0 -148
  304. package/src/core/boards.js +0 -409
  305. package/src/core/bootstrap.js +0 -170
  306. package/src/core/css-bundle.js +0 -30
  307. package/src/core/db.js +0 -315
  308. package/src/core/delete-board.js +0 -163
  309. package/src/core/dispatch.js +0 -1007
  310. package/src/core/events.js +0 -314
  311. package/src/core/http.js +0 -115
  312. package/src/core/migrate.js +0 -198
  313. package/src/core/paths.js +0 -55
  314. package/src/core/provision-repo-board.js +0 -119
  315. package/src/core/queries.js +0 -246
  316. package/src/core/registry.js +0 -211
  317. package/src/core/relations.js +0 -163
  318. package/src/core/review-verdicts.js +0 -138
  319. package/src/core/router.js +0 -556
  320. package/src/core/search.js +0 -183
  321. package/src/core/seed.js +0 -234
  322. package/src/core/snapshots.js +0 -692
  323. package/src/core/states.js +0 -167
  324. package/src/core/system-picker.js +0 -109
  325. package/src/core/tickets.js +0 -400
  326. package/src/core/util.js +0 -106
  327. package/src/core/version-check.js +0 -154
  328. package/src/mcp/orbit-client.js +0 -287
  329. package/src/server.js +0 -115
@@ -0,0 +1,298 @@
1
+ /**
2
+ * The command language: one grammar table (COMMANDS) that the CLI, both MCP
3
+ * tools (`orbit_read`, `orbit_write`) and `POST /api/cmd` all run, and from
4
+ * which help, the MCP tool descriptions and `/llms.txt` are written.
5
+ *
6
+ * Whether a line reads or writes is decided from its parsed words, before it
7
+ * runs, so no option can move a write past `orbit_read`'s check.
8
+ */
9
+ import { OrbitError } from "../core/errors.js";
10
+ import { asText } from "../core/text.js";
11
+ import { CommandError, fail, noMoreWords, parseArgs, parseCardRef, tokenizeMarked } from "./parse.js";
12
+ import { oneLine } from "./present.js";
13
+ import { hint, setsComputerDefault } from "./session.js";
14
+ import { BOARDS_USAGE, TEXT_TWICE_MESSAGE, agentLines, cmdArchive, cmdAsk, cmdBoards, cmdCard, cmdChanges, cmdEdit, cmdFind, cmdFinish, cmdJournal, cmdLink, cmdMove, cmdNew, cmdNote, cmdRestore, cmdTake, cmdUnlink, cmdView } from "./verbs.js";
15
+ export { createSession, repoBoardFromRequest } from "./session.js";
16
+ export { cardLink, parseCardRef, shellSafe, tokenize } from "./parse.js";
17
+ export { agentLines } from "./verbs.js";
18
+ /** Where `new` puts a card when no lane is given (createTicket's default lane). */
19
+ const NEW_LANE = "With no --lane the card goes to Todo (or the lane a person chose for new cards).";
20
+ /** What `help boards` adds: which board a command uses, and how a repo names its own. */
21
+ const BOARDS_HELP = {
22
+ cli: [
23
+ "A command uses the board it names, else ORBIT_BOARD, else this repo's orbit.json, else this computer's default (orbit boards use), else the only board.",
24
+ 'orbit boards link <board> writes { "board": "<board>" } to the nearest orbit.json in this repo, or a new one at its top; other branches and clones see it once it is committed and reaches the main branch. orbit boards unlink takes it out.'
25
+ ],
26
+ mcp: [
27
+ "A command uses the board it names, else this session's (view <board>), else the repo's orbit.json, else this computer's default, else the only board.",
28
+ "boards link <board> says how a repo names its board in orbit.json; Orbit writes no files from here."
29
+ ]
30
+ };
31
+ function cmdHelp(session, args) {
32
+ const { positional } = args;
33
+ noMoreWords(args, 1, "help [command]");
34
+ const shell = session.transport === "cli";
35
+ const say = (line) => hint(session, line);
36
+ const named = positional[0];
37
+ if (named) {
38
+ const command = BY_NAME.get(named.toLowerCase());
39
+ if (!command)
40
+ fail(unknownCommand(named));
41
+ const usage = shell ? say(command.usage).replaceAll(` | ${command.name}`, ` | orbit ${command.name}`) : command.usage;
42
+ const lines = [usage, command.summary];
43
+ if (command.aliases.length)
44
+ lines.push(`Also: ${command.aliases.join(", ")}.`);
45
+ const field = shell ? '-m "..."' : "the text field";
46
+ if (command.name === "new") {
47
+ lines.push(`Every word before the options is the title; the description goes in ${field}.`);
48
+ lines.push(NEW_LANE);
49
+ }
50
+ else if (command.name === "edit")
51
+ lines.push(`Text: ${field} replaces the description (or the field flagged).`);
52
+ else if (command.text)
53
+ lines.push(`Text: ${field} (or the words after the card).`);
54
+ else {
55
+ if (command.name === "boards")
56
+ lines.push(...BOARDS_HELP[shell ? "cli" : "mcp"]);
57
+ if (command.name === "take")
58
+ lines.push(`Name the card (${say("take #12")}) to take that one. With no card named it takes the top card in AI Ready at once (highest priority, then oldest; blocked cards and epics wait).`);
59
+ lines.push("Takes no text.");
60
+ }
61
+ return Promise.resolve({ text: lines.join("\n"), data: { command: command.name, usage: command.usage, summary: command.summary, aliases: command.aliases, kind: command.kind } });
62
+ }
63
+ const listed = COMMANDS.filter((c) => c.name !== "boards");
64
+ const lines = [
65
+ // A shell's overview (src/cli/main.ts) puts these above its Start section.
66
+ ...(shell ? [] : [...agentLines(session), ""]),
67
+ "Boards:",
68
+ ` ${say("boards")}: list boards; * marks the one commands use`,
69
+ ` ${say("boards new <name> [--slug s]")}: make a board`,
70
+ shell ? ` ${say("boards link <board>")}: this repo's board, named in its orbit.json (commit it)` : " boards link <board>: how a repo names its board (orbit.json)",
71
+ shell ? ` ${say("boards use <board>")}: the default board for every folder on this computer` : " boards use <board>: choose one for this session",
72
+ ` With several boards and no default, name one: ${shell ? "orbit view garden, orbit card garden#12" : "view garden (kept for this session), garden#12"}, or --board garden.`,
73
+ "",
74
+ shell ? "Cards are numbers: orbit card 12 (in a shell #12 starts a comment; garden#12 works). Lanes by name." : "Cards are #n (or board#n); lanes by name.",
75
+ "Lanes: Backlog someday, Todo next, AI Ready for an agent, In Progress being worked, Review for a person to check, Done and Cancelled closed.",
76
+ `${say("new")} puts a card in Todo when no --lane is given (or in the lane a person chose for new cards).`,
77
+ "AI Ready holds cards an agent may take: each says its goal and when it's done.",
78
+ "Priorities are words: urgent, high, med, low, maybe (med is the default; view leaves it unsaid).",
79
+ shell ? 'Long text goes in -m "...". Add --json for structured output.' : "Long text goes in the text field. Add --json for structured output.",
80
+ "Read:"
81
+ ];
82
+ for (const command of listed.filter((c) => c.kind === "read"))
83
+ lines.push(` ${say(command.usage)}: ${command.summary}`);
84
+ lines.push("Write:");
85
+ for (const command of listed.filter((c) => c.kind !== "read"))
86
+ lines.push(` ${say(command.usage)}: ${command.summary}`);
87
+ lines.push(shell ? 'Work loop: orbit take -> work -> orbit finish 12 -m "...". Unclear? orbit ask 12 -m "...", then stop.' : "Work loop: take -> work -> finish #n. Unclear? ask #n, then stop.");
88
+ return Promise.resolve({
89
+ text: lines.join("\n"),
90
+ data: { commands: COMMANDS.map((c) => ({ name: c.name, usage: c.usage, summary: c.summary, kind: typeof c.kind === "string" ? c.kind : "read" })) }
91
+ });
92
+ }
93
+ /** Edit distance, for "did you mean". */
94
+ function distance(a, b) {
95
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
96
+ for (let i = 1; i <= a.length; i += 1) {
97
+ let prev = row[0] ?? 0;
98
+ row[0] = i;
99
+ for (let j = 1; j <= b.length; j += 1) {
100
+ const next = Math.min((row[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
101
+ prev = row[j] ?? 0;
102
+ row[j] = next;
103
+ }
104
+ }
105
+ return row[b.length] ?? 0;
106
+ }
107
+ function unknownCommand(verb) {
108
+ const word = verb.toLowerCase();
109
+ const best = [...BY_NAME.keys()].map((name) => [name, distance(word, name)]).sort((x, y) => x[1] - y[1])[0];
110
+ const guess = best && best[1] <= 2 ? ` Did you mean ${BY_NAME.get(best[0])?.name ?? best[0]}?` : "";
111
+ return `Unknown command "${oneLine(verb, 40)}".${guess} Commands: ${COMMANDS.map((c) => c.name).join(", ")}.`;
112
+ }
113
+ const LINKS = ["use", "link", "unlink"];
114
+ export const COMMANDS = [
115
+ { name: "view", brief: "view [board] [--all] [--lane L]", aliases: ["board", "ls", "list"], kind: "read", usage: "view [board] [--all] [--lane L]", summary: "the board: lanes in order, one line per open card", flags: { all: "bool", lane: "value" }, run: cmdView },
116
+ { name: "card", brief: "card #n [--comments]", aliases: ["show", "read", "open"], kind: "read", usage: "card #n [--comments]", summary: "one card: its facts, links and text; --comments for the discussion", flags: { comments: "bool" }, run: cmdCard },
117
+ { name: "journal", brief: "journal (rules, decisions)", aliases: ["rules", "decisions"], kind: "read", usage: "journal [board]", summary: "the board's rules for agents, and its journal of decisions and lessons", run: cmdJournal },
118
+ { name: "find", brief: "find <words>", aliases: ["search"], kind: "read", usage: "find <words>", summary: "cards matching words, one line each", run: cmdFind },
119
+ { name: "changes", brief: "changes [--since 2h]", aliases: ["log", "history"], kind: "read", usage: "changes [--since 2h]", summary: "what happened on the board lately (default: the last 24h)", flags: { since: "value" }, run: cmdChanges },
120
+ {
121
+ name: "boards",
122
+ briefs: { read: "boards; boards use <board>", write: "boards new <name>" },
123
+ aliases: [],
124
+ // A listing reads; so do `use`, `link` and `unlink`, except in a shell on
125
+ // this computer, where `use` saves the default board and `link` writes the
126
+ // repo's orbit.json.
127
+ kind: (positional, session) => {
128
+ const sub = positional[0];
129
+ if (sub === undefined)
130
+ return "read";
131
+ if (LINKS.includes(sub))
132
+ return setsComputerDefault(session) && (sub === "use" || session.repo?.link) ? "write" : "read";
133
+ return "write";
134
+ },
135
+ label: (positional) => (LINKS.includes(positional[0] ?? "") ? `boards ${positional[0] ?? ""}` : "boards new"),
136
+ // Anything but `boards new`, `use`, `link` or `unlink` that isn't a listing is a mistake; say how boards works.
137
+ readOnlyText: (positional) => (["new", ...LINKS].includes(positional[0] ?? "") ? null : `boards takes: ${BOARDS_USAGE}.`),
138
+ usage: BOARDS_USAGE,
139
+ summary: "list boards; create one; choose one (in a terminal, the default for every folder on this computer); link: the repo names its board in orbit.json",
140
+ flags: { slug: "value", "no-ai": "bool" },
141
+ run: cmdBoards
142
+ },
143
+ { name: "help", brief: "help [command]", aliases: ["?"], kind: "read", usage: "help [command]", summary: "this list, or one command", run: cmdHelp },
144
+ {
145
+ name: "take",
146
+ brief: "take [#n]",
147
+ aliases: ["claim", "start"],
148
+ kind: "write",
149
+ usage: "take [#n] [--force]",
150
+ summary: "claim a card (with no card named: the next AI Ready card); returns it with the board's rules and next steps",
151
+ flags: { force: "bool", type: "value" },
152
+ run: cmdTake
153
+ },
154
+ { name: "move", brief: "move #n <lane>", aliases: ["mv"], kind: "write", usage: "move #n <lane>", summary: "move a card to a lane, named any way you like", run: cmdMove },
155
+ {
156
+ name: "new",
157
+ text: true,
158
+ brief: "new <title> [--lane L] [--type bug] [--priority high] [--label a,b] [--parent #n] (text: description)",
159
+ aliases: ["create", "add"],
160
+ kind: "write",
161
+ usage: "new <title> [--lane L] [--type bug] [--priority high] [--label a,b] [--parent #n]",
162
+ summary: "create a card; its description as text",
163
+ flags: { lane: "value", type: "value", priority: "value", label: "list", parent: "value" },
164
+ run: cmdNew
165
+ },
166
+ {
167
+ name: "edit",
168
+ text: true,
169
+ brief: "edit #n [--title T] [--priority P] [--label a,b] [--parent #n] (text: description, or --plan)",
170
+ aliases: ["update", "set"],
171
+ kind: "write",
172
+ usage: "edit #n [--title T] [--priority P] [--label a,b] [--add-label x] [--drop-label x] [--type T] [--parent #n|none] [--nebula URL|none] [--plan|--summary|--revisions]",
173
+ summary: "change a card's fields; text replaces its description (or the field flagged)",
174
+ flags: { title: "value", type: "value", priority: "value", label: "list", "add-label": "list", "drop-label": "list", parent: "value", nebula: "value", plan: "bool", summary: "bool", revisions: "bool" },
175
+ run: cmdEdit
176
+ },
177
+ {
178
+ name: "note",
179
+ text: true,
180
+ brief: "note #n (text); note --decision|--lesson <title> (journal)",
181
+ aliases: ["comment"],
182
+ kind: "write",
183
+ usage: "note #n | note [#n] --decision|--lesson <title>",
184
+ summary: "a comment on a card (text), or a decision or lesson for the board's journal",
185
+ flags: { decision: "bool", lesson: "bool" },
186
+ run: cmdNote
187
+ },
188
+ { name: "ask", text: true, brief: "ask #n", aliases: ["checkpoint"], kind: "write", usage: "ask #n", summary: 'the goal or "done" is unclear: post what\'s missing (text) and hand the card to a person, then stop', run: cmdAsk },
189
+ {
190
+ name: "finish",
191
+ text: true,
192
+ brief: "finish #n [--pr URL]",
193
+ aliases: ["complete"],
194
+ kind: "write",
195
+ usage: "finish #n [--pr URL] [--lane L]",
196
+ summary: "done working: the summary (text) of what changed and how you checked; moves the card to Review",
197
+ flags: { pr: "value", lane: "value" },
198
+ run: cmdFinish
199
+ },
200
+ { name: "link", brief: "link #a blocks|blocked-by|relates #b; unlink #a #b", aliases: [], kind: "write", usage: "link #a blocks|blocked-by|relates #b", summary: "say one card blocks (or relates to) another", run: cmdLink },
201
+ { name: "unlink", brief: null, aliases: [], kind: "write", usage: "unlink #a #b", summary: "remove the links between two cards", run: cmdUnlink },
202
+ { name: "archive", brief: "archive #n; restore #n", aliases: [], kind: "write", usage: "archive #n", summary: "put a card away (restore brings it back)", run: cmdArchive },
203
+ { name: "restore", brief: null, aliases: [], kind: "write", usage: "restore #n", summary: "bring an archived card back", run: cmdRestore }
204
+ ];
205
+ const BY_NAME = new Map();
206
+ for (const command of COMMANDS) {
207
+ BY_NAME.set(command.name, command);
208
+ for (const alias of command.aliases)
209
+ BY_NAME.set(alias, command);
210
+ }
211
+ /** `help` (or `help <command>`) for an actor and transport, without a data folder: it reads no board. */
212
+ export async function helpText(actor, transport, words = []) {
213
+ try {
214
+ const args = parseArgs(words, {}, "help [command]", [], transport === "cli");
215
+ return { ok: true, text: (await cmdHelp({ actor, transport }, args)).text };
216
+ }
217
+ catch (error) {
218
+ return { ok: false, text: explain(error) };
219
+ }
220
+ }
221
+ /** A command by name or alias. */
222
+ export function commandNamed(word) {
223
+ return BY_NAME.get(word.toLowerCase());
224
+ }
225
+ /** Refusals whose message says the way out (and carries no path), shown as they are. */
226
+ const SAYS_WHAT_TO_DO = new Set(["board_file_missing", "board_files_busy"]);
227
+ /** Core errors are codes (invalid_state); say what they mean. A database's own message is never shown. */
228
+ function explain(error) {
229
+ if (error instanceof CommandError)
230
+ return error.message;
231
+ if (error instanceof OrbitError && SAYS_WHAT_TO_DO.has(error.code) && error.message !== error.code)
232
+ return error.message;
233
+ if (error instanceof OrbitError)
234
+ return `Orbit refused: ${error.code.replace(/_/g, " ")}.`;
235
+ return "Orbit refused: internal error.";
236
+ }
237
+ /**
238
+ * Runs one command line. Never throws for a mistake in the line: that comes
239
+ * back as ok false with a text that says how to put it right.
240
+ */
241
+ export async function runCommand(input, session) {
242
+ let words;
243
+ let marks;
244
+ try {
245
+ if (Array.isArray(input.argv)) {
246
+ // An argv array (a shell, or HTTP) marks its own word boundaries.
247
+ words = input.argv.map((word) => asText(word));
248
+ marks = words.map(() => ({ quoted: false, hadQuote: false }));
249
+ }
250
+ else {
251
+ const tokens = tokenizeMarked(input.command ?? input.line ?? "");
252
+ words = tokens.map((token) => token.text);
253
+ marks = tokens.map(({ quoted, hadQuote, lost }) => ({ quoted, hadQuote, lost }));
254
+ }
255
+ }
256
+ catch (error) {
257
+ return { ok: false, text: explain(error), data: null, board: session.board, kind: "read" };
258
+ }
259
+ const [verb = "help", ...rest] = words;
260
+ const command = BY_NAME.get(verb.toLowerCase());
261
+ if (!command)
262
+ return { ok: false, text: unknownCommand(verb), data: null, board: session.board, kind: "read" };
263
+ const staticKind = typeof command.kind === "function" ? "write" : command.kind;
264
+ let args;
265
+ try {
266
+ args = parseArgs(rest, command.flags ?? {}, command.usage, marks.slice(1), session.transport === "cli");
267
+ }
268
+ catch (error) {
269
+ return { ok: false, text: explain(error), data: null, board: session.board, kind: staticKind };
270
+ }
271
+ const kind = typeof command.kind === "function" ? command.kind(args.positional, session) : command.kind;
272
+ if (input.readOnly && kind !== "read") {
273
+ const refusal = command.readOnlyText?.(args.positional);
274
+ return { ok: false, text: refusal ?? `${command.label ? command.label(args.positional) : command.name} changes the board; send it with orbit_write.`, data: null, board: session.board, kind };
275
+ }
276
+ try {
277
+ const text = asText(input.text);
278
+ if (text !== "") {
279
+ if (args.flags.text !== undefined)
280
+ fail(TEXT_TWICE_MESSAGE);
281
+ args.flags.text = text;
282
+ }
283
+ if (args.flags.text !== undefined && !command.text) {
284
+ const card = parseCardRef(args.positional[0]);
285
+ const keep = card ? ` To keep it, add it as a note: \`${hint(session, `note #${card.n}`)}\` with the text.` : "";
286
+ fail(`${command.name} takes no text.${keep}`);
287
+ }
288
+ const json = Boolean(input.json) || args.flags.json === true;
289
+ const result = await command.run(session, args);
290
+ // Only a write that went through marks its board active; reads change nothing at all.
291
+ if (kind === "write" && result.board)
292
+ await session.c.store.boards.touch(result.board.board.id, session.c.clock.now());
293
+ return { ok: true, text: json ? JSON.stringify(result.data ?? null) : result.text, data: result.data ?? null, board: session.board, kind };
294
+ }
295
+ catch (error) {
296
+ return { ok: false, text: explain(error), data: null, board: session.board, kind };
297
+ }
298
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Reading a command line: shell-like words, then options and the other words,
3
+ * with the guards that keep every word where its writer meant it. A command
4
+ * has a shape (a few fixed words, then at most one free run); extra words, a
5
+ * run split by an option, or an unquoted value that may have spaces followed
6
+ * by loose words are refused, never dropped or moved.
7
+ */
8
+ /** A command's problem, said so the next call can be right. */
9
+ export declare class CommandError extends Error {
10
+ constructor(message: string);
11
+ }
12
+ export declare function fail(message: string): never;
13
+ /** A command line as a shell takes it: `card #n` -> `card 12`, `link #a ... #b` -> `link 12 ... 13`. */
14
+ export declare function shellSafe(line: string): string;
15
+ /** How a usage is written where the command came from: `orbit card 12` in a shell. */
16
+ export declare function usageText(shell: boolean, usage: string): string;
17
+ /** A word and how it was written. */
18
+ export interface Mark {
19
+ /** It starts with a quote, so it is a word, never an option. */
20
+ quoted: boolean;
21
+ /** Some part of it was quoted, so its end was marked by hand. */
22
+ hadQuote: boolean;
23
+ /** The tokenizer ate a character the writer likely meant (a quote inside a word, `C:\new`). */
24
+ lost?: boolean;
25
+ }
26
+ /** Shell-like words: whitespace splits, quotes group, backslash escapes. */
27
+ export declare function tokenizeMarked(line: string): {
28
+ text: string;
29
+ quoted: boolean;
30
+ hadQuote: boolean;
31
+ lost: boolean;
32
+ }[];
33
+ export declare function tokenize(line: string): string[];
34
+ export type FlagKind = "value" | "bool" | "text" | "list";
35
+ export declare const TEXT_TWICE = "Text given twice: as -m/--text (or the text field) and as words on the line. Send it once: quote it, or put it all in text.";
36
+ export type FlagValue = string | boolean | string[];
37
+ /** A word that wasn't an option, and where it sat: its run (an option ends a run), its place, and the option just before it. */
38
+ export interface Place {
39
+ run: number;
40
+ index: number;
41
+ after: {
42
+ word: string;
43
+ value?: string;
44
+ end: number;
45
+ } | null;
46
+ lost: boolean;
47
+ quoted: boolean;
48
+ }
49
+ export interface Args {
50
+ flags: Record<string, FlagValue | undefined>;
51
+ positional: string[];
52
+ at: Place[];
53
+ words: string[];
54
+ /** Whether usages are written for a shell. */
55
+ shell: boolean;
56
+ }
57
+ /** The value of an option as a string, when it is one. */
58
+ export declare function flagString(args: Args, name: string): string | undefined;
59
+ export declare function flagList(args: Args, name: string): string[] | undefined;
60
+ export declare function flagOn(args: Args, name: string): boolean;
61
+ /** Options and the other words. */
62
+ export declare function parseArgs(words: string[], spec: Record<string, FlagKind>, usage: string, marks: Mark[], shell: boolean): Args;
63
+ /** Words past the first `max` are refused, never dropped. */
64
+ export declare function noMoreWords(args: Args, max: number, usage: string, help?: string): void;
65
+ /** The words from `from` on, as one run: an option in the middle means they weren't meant as one. */
66
+ export declare function freeWords(args: Args, from: number, what: string): string[];
67
+ /** A card reference: `#12`, `12`, `board#12`, or the link Nebula and the Suite use, `orbit:card/<board>/<n>` (also inside `[[...|label]]`). */
68
+ export declare function parseCardRef(word: string | undefined): {
69
+ board: string | null;
70
+ n: number;
71
+ } | null;
72
+ /** The link that names a card everywhere: `orbit:card/<board>/<n>`. */
73
+ export declare function cardLink(slug: string, n: number): string;
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Reading a command line: shell-like words, then options and the other words,
3
+ * with the guards that keep every word where its writer meant it. A command
4
+ * has a shape (a few fixed words, then at most one free run); extra words, a
5
+ * run split by an option, or an unquoted value that may have spaces followed
6
+ * by loose words are refused, never dropped or moved.
7
+ */
8
+ import { oneLine } from "./present.js";
9
+ /** A command's problem, said so the next call can be right. */
10
+ export class CommandError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "CommandError";
14
+ }
15
+ }
16
+ export function fail(message) {
17
+ throw new CommandError(message);
18
+ }
19
+ /** A command line as a shell takes it: `card #n` -> `card 12`, `link #a ... #b` -> `link 12 ... 13`. */
20
+ export function shellSafe(line) {
21
+ const refs = { n: "12", a: "12", b: "13" };
22
+ return line.replace(/(^|[\s([|`])#(n|a|b|\d+)\b/g, (_, before, ref) => `${before}${refs[ref] ?? ref}`);
23
+ }
24
+ /** How a usage is written where the command came from: `orbit card 12` in a shell. */
25
+ export function usageText(shell, usage) {
26
+ return shell ? `orbit ${shellSafe(usage)}` : usage;
27
+ }
28
+ /** Shell-like words: whitespace splits, quotes group, backslash escapes. */
29
+ export function tokenizeMarked(line) {
30
+ const tokens = [];
31
+ let word = "";
32
+ let quote = null;
33
+ let started = false;
34
+ let quoted = false;
35
+ let hadQuote = false;
36
+ let lost = false;
37
+ const push = () => {
38
+ tokens.push({ text: word, quoted, hadQuote, lost: lost || (hadQuote && !quoted) });
39
+ };
40
+ for (let i = 0; i < line.length; i += 1) {
41
+ const ch = line.charAt(i);
42
+ if (quote) {
43
+ if (ch === quote)
44
+ quote = null;
45
+ else if (ch === "\\" && quote === '"' && i + 1 < line.length)
46
+ word += line.charAt(++i);
47
+ else
48
+ word += ch;
49
+ continue;
50
+ }
51
+ if (ch === '"' || ch === "'") {
52
+ if (!started)
53
+ quoted = true;
54
+ quote = ch;
55
+ hadQuote = true;
56
+ started = true;
57
+ }
58
+ else if (ch === "\\" && i + 1 < line.length) {
59
+ const next = line.charAt(++i);
60
+ if (!/["'\\\s]/.test(next))
61
+ lost = true;
62
+ word += next;
63
+ started = true;
64
+ }
65
+ else if (/\s/.test(ch)) {
66
+ if (started)
67
+ push();
68
+ word = "";
69
+ started = false;
70
+ quoted = false;
71
+ hadQuote = false;
72
+ lost = false;
73
+ }
74
+ else {
75
+ word += ch;
76
+ started = true;
77
+ }
78
+ }
79
+ if (quote)
80
+ fail(`Unclosed ${quote} quote in: ${oneLine(line, 120)}`);
81
+ if (started)
82
+ push();
83
+ return tokens;
84
+ }
85
+ export function tokenize(line) {
86
+ return tokenizeMarked(line).map((token) => token.text);
87
+ }
88
+ /** Options every command takes. */
89
+ const GLOBAL_FLAGS = { board: "value", json: "bool", m: "text", message: "text", text: "text" };
90
+ /** Options whose values often have spaces. Unquoted and followed by more words, Orbit can't tell where the value ends, so it asks. */
91
+ const SPACED_VALUES = {
92
+ lane: '--lane "AI Ready"',
93
+ label: '--label "ui, backend"',
94
+ "add-label": '--add-label "needs review"',
95
+ "drop-label": '--drop-label "needs review"',
96
+ title: '--title "Fix login bug"',
97
+ m: '-m "..."',
98
+ message: '--message "..."',
99
+ text: '--text "..."',
100
+ since: "--since 2h"
101
+ };
102
+ export const TEXT_TWICE = "Text given twice: as -m/--text (or the text field) and as words on the line. Send it once: quote it, or put it all in text.";
103
+ /** The value of an option as a string, when it is one. */
104
+ export function flagString(args, name) {
105
+ const value = args.flags[name];
106
+ return typeof value === "string" ? value : undefined;
107
+ }
108
+ export function flagList(args, name) {
109
+ const value = args.flags[name];
110
+ return Array.isArray(value) ? value : undefined;
111
+ }
112
+ export function flagOn(args, name) {
113
+ return args.flags[name] === true;
114
+ }
115
+ /** Options and the other words. */
116
+ export function parseArgs(words, spec, usage, marks, shell) {
117
+ const flags = {};
118
+ const positional = [];
119
+ const at = [];
120
+ let run = 0;
121
+ let lastFlag = null;
122
+ const allowed = { ...GLOBAL_FLAGS, ...spec };
123
+ const optionAt = (j) => {
124
+ const word = words[j];
125
+ return word !== undefined && !marks[j]?.quoted && !/^-\d/.test(word) && /^--?[a-z][a-z-]*(=.*)?$/i.test(word);
126
+ };
127
+ for (let i = 0; i < words.length; i += 1) {
128
+ const word = words[i] ?? "";
129
+ const match = /^--?([a-z][a-z-]*)(?:=(.*))?$/i.exec(word);
130
+ if (!match || /^-\d/.test(word) || marks[i]?.quoted) {
131
+ positional.push(word);
132
+ at.push({ run, index: i, after: lastFlag, lost: Boolean(marks[i]?.lost), quoted: Boolean(marks[i]?.quoted) });
133
+ continue;
134
+ }
135
+ run += 1;
136
+ const name = (match[1] ?? "").toLowerCase();
137
+ const kind = allowed[name];
138
+ if (!kind) {
139
+ const names = Object.keys(spec).map((flag) => `--${flag}`);
140
+ fail(`Unknown option ${word}. Usage: ${usageText(shell, usage)}${names.length ? ` (options: ${names.join(", ")})` : ""}`);
141
+ }
142
+ const inlineValue = match[2];
143
+ const flagWord = inlineValue !== undefined ? word.slice(0, word.indexOf("=")) : word;
144
+ if (kind === "bool") {
145
+ flags[name] = inlineValue === undefined ? true : !/^(0|false|no)$/i.test(inlineValue);
146
+ lastFlag = { word: flagWord, end: i };
147
+ continue;
148
+ }
149
+ if (inlineValue === undefined && optionAt(i + 1))
150
+ fail(`${flagWord} needs a value. Usage: ${usageText(shell, usage)}`);
151
+ const value = inlineValue ?? words[++i];
152
+ if (value === undefined)
153
+ fail(`${flagWord} needs a value. Usage: ${usageText(shell, usage)}`);
154
+ if (kind === "value" && !value.trim())
155
+ fail(name === "title" ? "A card needs a title." : `${flagWord} needs a value. Usage: ${usageText(shell, usage)}`);
156
+ lastFlag = { word: flagWord, value, end: i };
157
+ // The value's end is clear when it was quoted, has a space (an argv word), is a whole time, or the next word was quoted.
158
+ const marked = Boolean(marks[i]?.hadQuote) || /\s/.test(value) || (name === "since" && /^\d+\s*(m|min|h|d|w)$/i.test(value)) || Boolean(marks[i + 1]?.quoted) || /\s/.test(words[i + 1] ?? "");
159
+ const example = SPACED_VALUES[name];
160
+ if (example && !marked && i + 1 < words.length && !optionAt(i + 1)) {
161
+ let j = i + 1;
162
+ const loose = [];
163
+ while (j < words.length && !optionAt(j))
164
+ loose.push(words[j++] ?? "");
165
+ const shown = example.replace(/^--?[a-z-]+/, flagWord);
166
+ const how = name === "since" ? `Write the time as one word (${shown})` : `Quote a value with spaces (${shown})`;
167
+ fail(`"${oneLine(loose.join(" "), 80)}" follows ${flagWord} ${oneLine(value, 40)}, so Orbit can't tell where its value ends. ${how}, or put options after the other words.`);
168
+ }
169
+ if (kind === "list") {
170
+ const before = flags[name];
171
+ flags[name] = [...(Array.isArray(before) ? before : []), ...value.split(",").map((v) => v.trim()).filter(Boolean)];
172
+ }
173
+ else if (kind === "text") {
174
+ if (flags.text !== undefined)
175
+ fail(TEXT_TWICE);
176
+ flags.text = value;
177
+ }
178
+ else {
179
+ flags[name] = value;
180
+ }
181
+ }
182
+ return { flags, positional, at, words, shell };
183
+ }
184
+ /** Words past the first `max` are refused, never dropped. */
185
+ export function noMoreWords(args, max, usage, help = "") {
186
+ if (args.positional.length <= max)
187
+ return;
188
+ const extra = args.positional.slice(max).join(" ");
189
+ const first = args.at[max];
190
+ const flag = first?.after;
191
+ const tail = help ? ` ${help}` : "";
192
+ if (first && flag?.value !== undefined && first.index === flag.end + 1) {
193
+ const run = args.positional.slice(max).filter((_, i) => args.at[max + i]?.run === first.run).join(" ");
194
+ fail(`"${oneLine(extra, 80)}" follows ${flag.word} ${oneLine(flag.value, 40)}. If they belong to it, quote it: ${flag.word} ${JSON.stringify(oneLine(`${flag.value} ${run}`, 120))}.${tail}`);
195
+ }
196
+ fail(`Unexpected words "${oneLine(extra, 80)}". Usage: ${usageText(args.shell, usage)}${tail}`);
197
+ }
198
+ /** The words from `from` on, as one run: an option in the middle means they weren't meant as one. */
199
+ export function freeWords(args, from, what) {
200
+ const at = args.at.slice(from);
201
+ // A quoted word groups only when it is the whole run; inside longer words its quotes would vanish.
202
+ if (at.some((place) => place.lost || (place.quoted && at.length > 1))) {
203
+ const how = what === "text" ? 'Send the text as text (-m "..." in a shell)' : `Quote the whole ${what}`;
204
+ fail(`Quotes or backslashes in the ${what} would be lost. ${how}, so every character is kept.`);
205
+ }
206
+ const first = at[0];
207
+ const split = at.find((place) => place.run !== first?.run);
208
+ if (first && split) {
209
+ const last = at[at.length - 1] ?? first;
210
+ const span = args.words.slice(first.index, last.index + 1).join(" ");
211
+ const flag = split.after;
212
+ const shown = flag ? `${flag.word}${flag.value !== undefined ? ` ${oneLine(flag.value, 40)}` : ""}` : "an option";
213
+ fail(`Words on both sides of ${shown}. If they're all the ${what}, quote it: ${JSON.stringify(oneLine(span, 160))}. Otherwise put options after the ${what}.`);
214
+ }
215
+ return args.positional.slice(from);
216
+ }
217
+ /** A card reference: `#12`, `12`, `board#12`, or the link Nebula and the Suite use, `orbit:card/<board>/<n>` (also inside `[[...|label]]`). */
218
+ export function parseCardRef(word) {
219
+ const text = (word ?? "").trim().replace(/^\[\[/, "").replace(/(\|[^\]]*)?\]\]$/, "");
220
+ let match = /^orbit:card\/([^/\s]+)\/#?(\d+)$/i.exec(text);
221
+ if (match)
222
+ return { board: match[1] ?? null, n: Number(match[2]) };
223
+ match = /^(?:([a-z0-9][a-z0-9._-]*)#|#)?(\d+)$/i.exec(text);
224
+ if (match)
225
+ return { board: match[1] ?? null, n: Number(match[2]) };
226
+ return null;
227
+ }
228
+ /** The link that names a card everywhere: `orbit:card/<board>/<n>`. */
229
+ export function cardLink(slug, n) {
230
+ return `orbit:card/${slug}/${n}`;
231
+ }