@starlove/orbit 1.4.1 → 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.4e6fbe20.js +0 -952
  204. package/dist/full/js/app.4e6fbe20.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 -230
  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,49 @@
1
+ /**
2
+ * A repo names its board in a committed orbit.json: { "board": "<slug>" }.
3
+ * Orbit reads it only when a command names no board, and records no folder:
4
+ * the `orbit` CLI and `orbit mcp` read the file where they run (or where
5
+ * `--cwd` says) and hand the board on, to their own session or, in remote
6
+ * mode, to POST /api/cmd as `repo_board`.
7
+ *
8
+ * Which file counts: the nearest orbit.json up from where the agent runs,
9
+ * stopping at the repo's top (the folder holding .git, a folder or a
10
+ * worktree's file). Outside a repo, only the current folder's. Nothing above
11
+ * the repo's top or above $HOME ever applies.
12
+ *
13
+ * A cloned repo's file is untrusted. It only names a board and grants nothing:
14
+ * only a regular file of at most 16 KB is read, a board must be a slug, and
15
+ * writing never follows a symlink (a temporary file is renamed into place).
16
+ */
17
+ import type { RepoChoice, RepoSource } from "../commands/index.ts";
18
+ export declare const REPO_FILE = "orbit.json";
19
+ export declare const REPO_FILE_MAX_BYTES: number;
20
+ /** The repo's top: the nearest folder at or above `start` holding .git, never above $HOME. Null outside a repo. */
21
+ export declare function repoTop(start: string): string | null;
22
+ /** The orbit.json that applies at `start`: where it is (or where link would make it), whether it exists, and whether it's in a repo. */
23
+ export declare function findRepoFile(start: string): {
24
+ path: string;
25
+ exists: boolean;
26
+ inRepo: boolean;
27
+ };
28
+ /** The board the repo at `start` names, or why its file can't be used. */
29
+ export declare function readRepoBoard(start: string): RepoChoice;
30
+ /** What a remote client sends for a repo's board: the fields of POST /api/cmd. */
31
+ export declare function repoBoardFields(repo: RepoChoice): {
32
+ repo_board?: string;
33
+ repo_board_problem?: string;
34
+ };
35
+ export interface LinkResult {
36
+ ok: boolean;
37
+ text: string;
38
+ data: unknown;
39
+ }
40
+ /** Makes the repo at `start` name board `slug`, keeping every other field. */
41
+ export declare function linkRepoBoard(start: string, slug: string): LinkResult;
42
+ /** Takes `board` out of the repo's orbit.json, and the file with it when nothing else is left. */
43
+ export declare function unlinkRepoBoard(start: string): LinkResult;
44
+ /** `boards link <board>` or `boards unlink`, from a shell's words: which, or null. */
45
+ export declare function repoLinkWords(argv: readonly string[]): "link" | "unlink" | null;
46
+ /** The repo source for a command run where `dir` is: reads the file there, and (for a shell) writes it. */
47
+ export declare function repoSourceAt(dir: string, options?: {
48
+ writes?: boolean;
49
+ }): RepoSource;
@@ -0,0 +1,245 @@
1
+ /**
2
+ * A repo names its board in a committed orbit.json: { "board": "<slug>" }.
3
+ * Orbit reads it only when a command names no board, and records no folder:
4
+ * the `orbit` CLI and `orbit mcp` read the file where they run (or where
5
+ * `--cwd` says) and hand the board on, to their own session or, in remote
6
+ * mode, to POST /api/cmd as `repo_board`.
7
+ *
8
+ * Which file counts: the nearest orbit.json up from where the agent runs,
9
+ * stopping at the repo's top (the folder holding .git, a folder or a
10
+ * worktree's file). Outside a repo, only the current folder's. Nothing above
11
+ * the repo's top or above $HOME ever applies.
12
+ *
13
+ * A cloned repo's file is untrusted. It only names a board and grants nothing:
14
+ * only a regular file of at most 16 KB is read, a board must be a slug, and
15
+ * writing never follows a symlink (a temporary file is renamed into place).
16
+ */
17
+ import { randomBytes } from "node:crypto";
18
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { dirname, join, resolve } from "node:path";
21
+ import { oneLine } from "../commands/present.js";
22
+ import { isSlug } from "../core/text.js";
23
+ export const REPO_FILE = "orbit.json";
24
+ export const REPO_FILE_MAX_BYTES = 16 * 1024;
25
+ class RepoFileProblem extends Error {
26
+ problem;
27
+ constructor(problem) {
28
+ super(problem);
29
+ this.problem = problem;
30
+ }
31
+ }
32
+ function lstatOrNull(path) {
33
+ try {
34
+ return lstatSync(path);
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ const homeDir = () => resolve(process.env.HOME ?? homedir());
41
+ /** The repo's top: the nearest folder at or above `start` holding .git, never above $HOME. Null outside a repo. */
42
+ export function repoTop(start) {
43
+ const home = homeDir();
44
+ let dir = resolve(start);
45
+ for (;;) {
46
+ const git = lstatOrNull(join(dir, ".git"));
47
+ if (git && (git.isDirectory() || git.isFile()))
48
+ return dir;
49
+ const parent = dirname(dir);
50
+ if (dir === home || parent === dir)
51
+ return null;
52
+ dir = parent;
53
+ }
54
+ }
55
+ /** The orbit.json that applies at `start`: where it is (or where link would make it), whether it exists, and whether it's in a repo. */
56
+ export function findRepoFile(start) {
57
+ const from = resolve(start);
58
+ const top = repoTop(from);
59
+ if (!top) {
60
+ const path = join(from, REPO_FILE);
61
+ return { path, exists: Boolean(lstatOrNull(path)), inRepo: false };
62
+ }
63
+ for (let dir = from;; dir = dirname(dir)) {
64
+ const path = join(dir, REPO_FILE);
65
+ if (lstatOrNull(path))
66
+ return { path, exists: true, inRepo: true };
67
+ if (dir === top || dirname(dir) === dir)
68
+ break;
69
+ }
70
+ return { path: join(top, REPO_FILE), exists: false, inRepo: true };
71
+ }
72
+ /** The file's JSON object, read safely: a regular file, at most 16 KB, never through a symlink. */
73
+ function readObject(path) {
74
+ const stat = lstatOrNull(path);
75
+ if (!stat)
76
+ return null;
77
+ if (stat.isSymbolicLink())
78
+ throw new RepoFileProblem("symlink");
79
+ if (!stat.isFile())
80
+ throw new RepoFileProblem("not_file");
81
+ if (stat.size > REPO_FILE_MAX_BYTES)
82
+ throw new RepoFileProblem("too_big");
83
+ // O_NOFOLLOW: a file swapped for a link after the lstat isn't followed.
84
+ // O_NONBLOCK: a file swapped for a pipe doesn't hang the command.
85
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
86
+ let text;
87
+ let mode;
88
+ try {
89
+ const opened = fstatSync(fd);
90
+ if (!opened.isFile())
91
+ throw new RepoFileProblem("not_file");
92
+ if (opened.size > REPO_FILE_MAX_BYTES)
93
+ throw new RepoFileProblem("too_big");
94
+ mode = opened.mode & 0o777;
95
+ const buffer = Buffer.alloc(REPO_FILE_MAX_BYTES + 1);
96
+ let length = 0;
97
+ for (let n = 1; length < buffer.length && n > 0; length += n)
98
+ n = readSync(fd, buffer, length, buffer.length - length, null);
99
+ if (length > REPO_FILE_MAX_BYTES)
100
+ throw new RepoFileProblem("too_big");
101
+ text = buffer.subarray(0, length).toString("utf8");
102
+ }
103
+ finally {
104
+ closeSync(fd);
105
+ }
106
+ let data;
107
+ try {
108
+ data = JSON.parse(text);
109
+ }
110
+ catch {
111
+ throw new RepoFileProblem("not_json");
112
+ }
113
+ if (!data || typeof data !== "object" || Array.isArray(data))
114
+ throw new RepoFileProblem("not_json");
115
+ return { data: data, mode };
116
+ }
117
+ /** The board the repo at `start` names, or why its file can't be used. */
118
+ export function readRepoBoard(start) {
119
+ try {
120
+ const found = findRepoFile(start);
121
+ const read = found.exists ? readObject(found.path) : null;
122
+ const value = read?.data.board;
123
+ if (value === undefined || value === null)
124
+ return { board: null };
125
+ if (!isSlug(value))
126
+ return { board: null, problem: "not_slug", value };
127
+ return { board: value };
128
+ }
129
+ catch (error) {
130
+ if (error instanceof RepoFileProblem)
131
+ return { board: null, problem: error.problem === "symlink" ? "not_file" : error.problem };
132
+ return { board: null, problem: "not_file" };
133
+ }
134
+ }
135
+ /** What a remote client sends for a repo's board: the fields of POST /api/cmd. */
136
+ export function repoBoardFields(repo) {
137
+ const fields = {};
138
+ if (repo.board)
139
+ fields.repo_board = repo.board;
140
+ if (repo.problem)
141
+ fields.repo_board_problem = repo.problem;
142
+ if (repo.problem === "not_slug" && typeof repo.value === "string")
143
+ fields.repo_board = repo.value;
144
+ return fields;
145
+ }
146
+ /** A refusal for a file Orbit won't write. */
147
+ function writeProblemText(problem) {
148
+ switch (problem) {
149
+ case "symlink":
150
+ return "This repo's orbit.json is a symlink, so Orbit won't write through it. Replace it with a plain file, then link again.";
151
+ case "too_big":
152
+ return "This repo's orbit.json is over 16 KB, so Orbit won't overwrite it. Fix or remove it, then link again.";
153
+ case "not_json":
154
+ return "This repo's orbit.json isn't a JSON object, so Orbit won't overwrite it. Fix or remove it, then link again.";
155
+ case "not_file":
156
+ return "This repo's orbit.json isn't a plain file, so Orbit won't write it. Remove it, then link again.";
157
+ }
158
+ }
159
+ /** The file's text: one line when it holds only the board, else two-space JSON. */
160
+ function fileText(data) {
161
+ const keys = Object.keys(data);
162
+ if (keys.length === 1 && keys[0] === "board")
163
+ return `{ "board": ${JSON.stringify(data.board)} }\n`;
164
+ return `${JSON.stringify(data, null, 2)}\n`;
165
+ }
166
+ /** Writes through a new file renamed into place, so no link or shared inode is ever written through. */
167
+ function writeReplacing(path, text, mode = 0o644) {
168
+ const temporary = join(dirname(path), `.${REPO_FILE}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
169
+ try {
170
+ writeFileSync(temporary, text, { flag: "wx", mode });
171
+ renameSync(temporary, path);
172
+ }
173
+ catch (error) {
174
+ try {
175
+ unlinkSync(temporary);
176
+ }
177
+ catch {
178
+ // Never made.
179
+ }
180
+ throw error;
181
+ }
182
+ }
183
+ function readForWrite(start) {
184
+ const found = findRepoFile(start);
185
+ try {
186
+ const read = found.exists ? readObject(found.path) : null;
187
+ return { found, data: read?.data ?? {}, mode: read?.mode };
188
+ }
189
+ catch (error) {
190
+ if (error instanceof RepoFileProblem)
191
+ return { found, refused: writeProblemText(error.problem) };
192
+ throw error;
193
+ }
194
+ }
195
+ /** Makes the repo at `start` name board `slug`, keeping every other field. */
196
+ export function linkRepoBoard(start, slug) {
197
+ const read = readForWrite(start);
198
+ if (read.refused !== undefined)
199
+ return { ok: false, text: read.refused, data: null };
200
+ const { found, data, mode } = read;
201
+ const previous = typeof data.board === "string" ? data.board : null;
202
+ const whose = found.inRepo ? "This repo's" : "This folder's";
203
+ // What happens, not an order: take's rule (a branch, local commits, never push or merge) still holds.
204
+ const commit = found.inRepo ? "Other branches and clones see it once it is committed and reaches the main branch." : "It isn't in a git repo, so it counts in this folder only.";
205
+ const result = { board: slug, linked: true, previous };
206
+ if (previous === slug)
207
+ return { ok: true, text: `${whose} orbit.json already names board ${slug}. ${commit}`, data: result };
208
+ writeReplacing(found.path, fileText({ ...data, board: slug }), mode);
209
+ const was = previous ? ` (it named ${oneLine(previous, 60)})` : "";
210
+ return { ok: true, text: `${whose} orbit.json names board ${slug} now${was}, so commands here use it when they name none. ${commit}`, data: result };
211
+ }
212
+ /** Takes `board` out of the repo's orbit.json, and the file with it when nothing else is left. */
213
+ export function unlinkRepoBoard(start) {
214
+ const read = readForWrite(start);
215
+ if (read.refused !== undefined)
216
+ return { ok: false, text: read.refused, data: null };
217
+ const { found, data, mode } = read;
218
+ const whose = found.inRepo ? "This repo's" : "This folder's";
219
+ const commit = found.inRepo ? " Commit the change." : "";
220
+ if (!found.exists || data.board === undefined)
221
+ return { ok: true, text: `${whose} orbit.json names no board, so there's nothing to unlink.`, data: { board: null, linked: false } };
222
+ const { board, ...rest } = data;
223
+ const named = typeof board === "string" ? oneLine(board, 60) : "";
224
+ if (!Object.keys(rest).length) {
225
+ unlinkSync(found.path);
226
+ return { ok: true, text: `${whose} orbit.json named board ${named}; Orbit removed the file, since nothing else was in it.${commit}`, data: { board: null, linked: false, previous: board, removed: true } };
227
+ }
228
+ writeReplacing(found.path, fileText(rest), mode);
229
+ return { ok: true, text: `${whose} orbit.json names no board now (it named ${named}); its other fields stay.${commit}`, data: { board: null, linked: false, previous: board, removed: false } };
230
+ }
231
+ /** `boards link <board>` or `boards unlink`, from a shell's words: which, or null. */
232
+ export function repoLinkWords(argv) {
233
+ const words = argv.filter((word) => !word.startsWith("-"));
234
+ if (words[0] !== "boards")
235
+ return null;
236
+ return words[1] === "link" || words[1] === "unlink" ? words[1] : null;
237
+ }
238
+ /** The repo source for a command run where `dir` is: reads the file there, and (for a shell) writes it. */
239
+ export function repoSourceAt(dir, options = {}) {
240
+ // "this repo" in a git repo, else "this folder", for "let ... name it".
241
+ const source = { read: () => readRepoBoard(dir), place: repoTop(dir) ? "this repo" : "this folder" };
242
+ if (options.writes)
243
+ source.link = (sub, slug) => (sub === "link" ? linkRepoBoard(dir, slug ?? "") : unlinkRepoBoard(dir));
244
+ return source;
245
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `orbit run`: the app and the API on this computer, over @hono/node-server.
3
+ * It listens on 127.0.0.1 unless HOST says otherwise (the API has no
4
+ * accounts, so it is never exposed by accident), starts at 13701 and moves
5
+ * up past busy ports unless PORT names one, then says what to do next.
6
+ */
7
+ import type { OrbitEnv } from "./env.ts";
8
+ export declare function runServer(env: OrbitEnv): Promise<void>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * `orbit run`: the app and the API on this computer, over @hono/node-server.
3
+ * It listens on 127.0.0.1 unless HOST says otherwise (the API has no
4
+ * accounts, so it is never exposed by accident), starts at 13701 and moves
5
+ * up past busy ports unless PORT names one, then says what to do next.
6
+ */
7
+ import { networkInterfaces } from "node:os";
8
+ import { createAdaptorServer } from "@hono/node-server";
9
+ import { createOrbitApp } from "../http/app.js";
10
+ import { folderAssets } from "./assets.js";
11
+ import { nextSteps } from "./data-folder.js";
12
+ import { hostCheck, misdirected } from "./host-check.js";
13
+ import { hostServices } from "./host-services.js";
14
+ import { openLocalOrbit, packageVersion, publicDir } from "./local.js";
15
+ const DEFAULT_PORT = 13701;
16
+ function displayHost(host) {
17
+ return !host || host === "::" || host === "0.0.0.0" ? "localhost" : host;
18
+ }
19
+ function networkUrls(port) {
20
+ return Object.values(networkInterfaces())
21
+ .flat()
22
+ .filter((item) => item && item.family === "IPv4" && !item.internal)
23
+ .map((item) => `http://${item?.address ?? ""}:${port}`);
24
+ }
25
+ /**
26
+ * What `orbit run` adds around the app: a request naming a host Orbit doesn't
27
+ * answer to is refused (DNS rebinding; host-check.ts), and every response
28
+ * says not to sniff its type and to send the page's address only to Orbit
29
+ * itself.
30
+ */
31
+ function guarded(fetch, check) {
32
+ return async (request) => {
33
+ const refused = check.refused(request.headers);
34
+ if (refused !== null) {
35
+ console.error(`Orbit: 421 for the host name ${refused.replace(/[^A-Za-z0-9.:[\]_-]/g, "").slice(0, 120)} on ${request.method} ${new URL(request.url).pathname} (see ORBIT_ALLOWED_HOSTS)`);
36
+ return misdirected(refused);
37
+ }
38
+ const response = await fetch(request);
39
+ const headers = new Headers(response.headers);
40
+ headers.set("X-Content-Type-Options", "nosniff");
41
+ if (!headers.has("Referrer-Policy"))
42
+ headers.set("Referrer-Policy", "same-origin");
43
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
44
+ };
45
+ }
46
+ export async function runServer(env) {
47
+ const local = await openLocalOrbit(env, { host: hostServices(env, env.dataDir) });
48
+ const { orbit } = local;
49
+ const app = createOrbitApp(orbit, {
50
+ assets: folderAssets(publicDir()),
51
+ version: packageVersion(),
52
+ log: (line) => { console.error(line); }
53
+ });
54
+ const server = createAdaptorServer({ fetch: guarded(app.fetch, hostCheck({ bind: env.host, allowed: env.allowedHosts })) });
55
+ const first = env.port ?? DEFAULT_PORT;
56
+ let port = first;
57
+ const listen = () => {
58
+ server.listen(port, env.host);
59
+ };
60
+ server.on("error", (error) => {
61
+ if (env.port === null && error.code === "EADDRINUSE" && port < first + 99) {
62
+ port += 1;
63
+ console.warn(`[orbit] Port ${port - 1} is in use; trying ${port}.`);
64
+ listen();
65
+ return;
66
+ }
67
+ console.error(error);
68
+ void local.close().finally(() => process.exit(1));
69
+ });
70
+ server.on("listening", () => {
71
+ void (async () => {
72
+ const address = server.address();
73
+ const boundPort = address?.port ?? port;
74
+ const url = `http://${displayHost(address?.address ?? env.host)}:${boundPort}`;
75
+ const boards = (await orbit.context({ id: "local", name: "local", type: "human", role: "owner", permissions: [], boards: ["*"] }).store.boards.list()).length;
76
+ console.log(`Starscape Orbit listening on ${url}`);
77
+ // An upgrader's first sight of v2: an empty folder, and their old boards one command away, first.
78
+ const older = !boards && orbit.deployment.older_data;
79
+ const others = ["0.0.0.0", "::"].includes(env.host) ? networkUrls(boundPort).filter((other) => other !== url) : [];
80
+ if (others.length)
81
+ console.log(`Network: ${others.join(", ")}`);
82
+ // What a newcomer does next: open the app, then tell their agents.
83
+ console.log("");
84
+ console.log("Next:");
85
+ nextSteps(url, boards ? "to see your boards" : older ? "to see them" : "and make a board", older).forEach((line) => { console.log(line); });
86
+ console.log("");
87
+ console.log(`Boards: ${boards || "none yet"}, in ${env.dataDir}`);
88
+ console.log("Stop it with Ctrl+C.");
89
+ })();
90
+ });
91
+ let stopping = false;
92
+ const shutdown = () => {
93
+ if (stopping)
94
+ return;
95
+ stopping = true;
96
+ server.close(() => {
97
+ void local.close().finally(() => process.exit(0));
98
+ });
99
+ setTimeout(() => {
100
+ void local.close().finally(() => process.exit(0));
101
+ }, 1500).unref();
102
+ };
103
+ process.once("SIGINT", shutdown);
104
+ process.once("SIGTERM", shutdown);
105
+ listen();
106
+ }
package/lib/orbit.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Orbit as a library: `createOrbit({ store, blobs })` gives every use case,
3
+ * acting as an actor, through one client (./client/types.ts), and every
4
+ * board's committed changes. It touches nothing but the ports it is given,
5
+ * so the same code runs on a server, in a CLI and in a browser tab.
6
+ */
7
+ import type { Clock, Ctx, Deployment, Ids } from "./core/context.ts";
8
+ import type { Actor, BoardChange, Id } from "./core/model.ts";
9
+ import type { RepoSource, Transport } from "./commands/index.ts";
10
+ import type { HostServices, OrbitClient } from "./client/types.ts";
11
+ import type { BlobStore, OrbitStore } from "./store/types.ts";
12
+ export interface OrbitOptions {
13
+ store: OrbitStore;
14
+ blobs: BlobStore;
15
+ /** Default: the system clock. */
16
+ clock?: Clock;
17
+ /** Default: crypto.randomUUID(). */
18
+ ids?: Ids;
19
+ /** Where this Orbit runs, for the person's own app; empty for a host without files. */
20
+ deployment?: Partial<Deployment>;
21
+ /** Called after each change that went through, with its board (a host's automatic backups). */
22
+ changed?: (boardId: Id) => void;
23
+ /** What the computer Orbit runs on can do: the version check, self-update, a folder picker. None by default. */
24
+ host?: HostServices;
25
+ }
26
+ export interface ClientSession {
27
+ transport?: Transport;
28
+ shared?: boolean;
29
+ board?: string | null;
30
+ repo?: RepoSource | null;
31
+ }
32
+ export interface Orbit {
33
+ /** Every use case, acting as `actor`, in this process: the in-process OrbitClient. */
34
+ client(actor: Actor, session?: ClientSession): OrbitClient;
35
+ /** The use cases' context for `actor`, for hosts that call the core directly. */
36
+ context(actor: Actor): Ctx;
37
+ /** One board's committed changes, from any process. Returns the function that stops. */
38
+ watch(boardId: Id, listener: (change: BoardChange) => void): () => void;
39
+ readonly deployment: Deployment;
40
+ readonly host: HostServices;
41
+ close(): Promise<void>;
42
+ }
43
+ export declare function createOrbit(options: OrbitOptions): Orbit;
44
+ export type { ChangesOptions, HostServices, ImageUrl, OrbitClient, StreamState, VersionInfo } from "./client/types.ts";
45
+ export type * from "./core/model.ts";
46
+ export { OrbitError, isOrbitError } from "./core/errors.ts";
47
+ export { localAgentActor, localOwnerActor, namedAgentActor } from "./core/actors.ts";
package/lib/orbit.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Orbit as a library: `createOrbit({ store, blobs })` gives every use case,
3
+ * acting as an actor, through one client (./client/types.ts), and every
4
+ * board's committed changes. It touches nothing but the ports it is given,
5
+ * so the same code runs on a server, in a CLI and in a browser tab.
6
+ */
7
+ import { createLocalClient } from "./client/local.js";
8
+ const systemClock = { now: () => new Date().toISOString() };
9
+ const randomIds = { next: () => crypto.randomUUID() };
10
+ export function createOrbit(options) {
11
+ const deployment = { app_root: "", data_dir: "", data_dir_default: false, mcp_server_path: "", export_dir: "", older_data: false, ...options.deployment };
12
+ const runtime = {
13
+ store: options.store,
14
+ blobs: options.blobs,
15
+ clock: options.clock ?? systemClock,
16
+ ids: options.ids ?? randomIds,
17
+ deployment,
18
+ changed: options.changed ?? (() => undefined)
19
+ };
20
+ const context = (actor) => ({ ...runtime, actor });
21
+ const host = options.host ?? {};
22
+ function watch(boardId, listener) {
23
+ let stop = null;
24
+ let stopped = false;
25
+ void options.store.board(boardId).then((data) => {
26
+ if (!stopped)
27
+ stop = data.watch(listener);
28
+ }, () => undefined);
29
+ return () => {
30
+ stopped = true;
31
+ stop?.();
32
+ };
33
+ }
34
+ return {
35
+ client: (actor, session = {}) => createLocalClient(context(actor), { ...session, watch, host }),
36
+ context,
37
+ watch,
38
+ deployment,
39
+ host,
40
+ close: () => options.store.close()
41
+ };
42
+ }
43
+ export { OrbitError, isOrbitError } from "./core/errors.js";
44
+ export { localAgentActor, localOwnerActor, namedAgentActor } from "./core/actors.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ // Node 22 warns "SQLite is an experimental feature and might change
2
+ // at any time" when node:sqlite loads, so every orbit command printed it (and
3
+ // Node's --trace-warnings hint) to stderr. Orbit's entry points import this
4
+ // file first, before anything loads node:sqlite. It drops exactly that one
5
+ // warning; every other warning, ExperimentalWarning or not, still prints.
6
+ //
7
+ // Node emits a warning's event on the next tick, after the module graph that
8
+ // loaded node:sqlite has run, so the filter sits on the event: it works
9
+ // whether node:sqlite is imported statically or later.
10
+ const SQLITE_WARNING = /^SQLite is an experimental feature/;
11
+ const emit = process.emit.bind(process);
12
+ process.emit = function orbitEmit(name, ...args) {
13
+ const warning = args[0];
14
+ if (name === "warning" && warning instanceof Error && warning.name === "ExperimentalWarning" && SQLITE_WARNING.test(warning.message))
15
+ return false;
16
+ return emit(name, ...args);
17
+ };
18
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The rule every BlobStore applies to paths: relative, POSIX-style, and
3
+ * inside the board. Shared so every adapter refuses the same paths.
4
+ */
5
+ /** Refuses an empty or absolute path, a backslash or NUL, and any empty, `.` or `..` segment, with `invalid_blob_path`. */
6
+ export declare function checkBlobPath(path: string): void;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The rule every BlobStore applies to paths: relative, POSIX-style, and
3
+ * inside the board. Shared so every adapter refuses the same paths.
4
+ */
5
+ import { OrbitError } from "../core/errors.js";
6
+ /** Refuses an empty or absolute path, a backslash or NUL, and any empty, `.` or `..` segment, with `invalid_blob_path`. */
7
+ export function checkBlobPath(path) {
8
+ const segments = path.split("/");
9
+ const bad = path === "" || path.includes("\\") || path.includes("\0") || segments.some((segment) => segment === "" || segment === "." || segment === "..");
10
+ if (bad)
11
+ throw new OrbitError("invalid_blob_path");
12
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The behaviour every OrbitStore and BlobStore must show, written once and
3
+ * run against each adapter: memory and SQLite in vitest, IndexedDB in real
4
+ * browsers too. It imports no test framework, so a stranger's adapter proves
5
+ * itself the same way:
6
+ *
7
+ * registerStoreConformance({ describe, it }, "my adapter", () => ({ open: () => openMyStore(dir) }));
8
+ *
9
+ * Each case gets a fresh harness. `open()` gives a store over the harness's
10
+ * data; calling it again gives a second store over the same data, as a second
11
+ * process (or tab) would have, which is how the cases prove that writes are
12
+ * serialised and heard everywhere.
13
+ */
14
+ import type { BlobStore, OrbitStore } from "./types.ts";
15
+ export interface StoreHarness {
16
+ /** A store over this harness's data; each call opens another over the same data. */
17
+ open(): Promise<OrbitStore>;
18
+ /** Closes what `open()` opened and deletes the data. */
19
+ dispose?(): Promise<void>;
20
+ }
21
+ export type StoreHarnessFactory = () => StoreHarness | Promise<StoreHarness>;
22
+ export interface ConformanceCase {
23
+ name: string;
24
+ run(harness: StoreHarness): Promise<void>;
25
+ }
26
+ export interface BlobConformanceCase {
27
+ name: string;
28
+ run(blobs: BlobStore): Promise<void>;
29
+ }
30
+ export interface ConformanceResult {
31
+ name: string;
32
+ ok: boolean;
33
+ error?: string;
34
+ }
35
+ /** The part of vitest, jest or mocha the suite registers itself with. */
36
+ export interface TestApi {
37
+ describe(name: string, body: () => void): void;
38
+ it(name: string, body: () => Promise<void>): void;
39
+ }
40
+ export declare const storeConformanceCases: ConformanceCase[];
41
+ export declare const blobConformanceCases: BlobConformanceCase[];
42
+ /** Registers every store case, under one describe block, with a test framework. */
43
+ export declare function registerStoreConformance(api: TestApi, label: string, factory: StoreHarnessFactory, options?: {
44
+ skip?: string[];
45
+ }): void;
46
+ /** Registers every blob case, each on a fresh BlobStore. */
47
+ export declare function registerBlobConformance(api: TestApi, label: string, factory: () => BlobStore | Promise<BlobStore>): void;
48
+ /** Runs every store case in order, each on a fresh harness, and reports instead of throwing. */
49
+ export declare function runStoreConformance(factory: StoreHarnessFactory, options?: {
50
+ skip?: string[];
51
+ only?: string[];
52
+ }): Promise<ConformanceResult[]>;