@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.
- package/Dockerfile +10 -8
- package/README.md +146 -164
- package/dist/full/csp.txt +1 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
- package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
- package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
- package/dist/full/fonts/Mona_Sans/README.txt +10 -0
- package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
- package/dist/full/fonts/Newsreader/README.txt +8 -0
- package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
- package/dist/full/index.html +36 -653
- package/dist/full/js/app.BmuponPJ.js +32 -0
- package/dist/full/js/app.BmuponPJ.js.map +1 -0
- package/dist/full/js/chunk.1fJGQGbW.js +2 -0
- package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
- package/dist/full/js/chunk.5D7JO2zp.js +2 -0
- package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
- package/dist/full/js/chunk.BrNn_D79.js +2 -0
- package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
- package/dist/full/js/chunk.CokJXIqM.js +2 -0
- package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
- package/dist/full/js/chunk.D_Haqy6R.js +2 -0
- package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
- package/dist/full/js/chunk.RO0jsH6I.js +15 -0
- package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
- package/dist/full/js/chunk.dqWOEtBM.js +2 -0
- package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
- package/dist/full/js/warm.f260bbbc.js +15 -0
- package/dist/full/orbits-favicon.png +0 -0
- package/dist/full/orbits-logo-bg-wide.png +0 -0
- package/dist/full/orbits-logo-bg.png +0 -0
- package/dist/full/orbits-logo.png +0 -0
- package/dist/full/styles.2b154ffe.css +1 -0
- package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_GUIDE.md +114 -0
- package/docs/UPDATING.md +249 -64
- package/lib/adapters/indexeddb/blobs.d.ts +3 -0
- package/lib/adapters/indexeddb/blobs.js +54 -0
- package/lib/adapters/indexeddb/database.d.ts +60 -0
- package/lib/adapters/indexeddb/database.js +126 -0
- package/lib/adapters/indexeddb/index.d.ts +5 -0
- package/lib/adapters/indexeddb/index.js +3 -0
- package/lib/adapters/indexeddb/locks.d.ts +14 -0
- package/lib/adapters/indexeddb/locks.js +40 -0
- package/lib/adapters/indexeddb/store.d.ts +40 -0
- package/lib/adapters/indexeddb/store.js +424 -0
- package/lib/adapters/indexeddb/tables.d.ts +43 -0
- package/lib/adapters/indexeddb/tables.js +127 -0
- package/lib/adapters/memory/blobs.d.ts +3 -0
- package/lib/adapters/memory/blobs.js +44 -0
- package/lib/adapters/memory/board.d.ts +41 -0
- package/lib/adapters/memory/board.js +521 -0
- package/lib/adapters/memory/index.d.ts +2 -0
- package/lib/adapters/memory/index.js +2 -0
- package/lib/adapters/memory/search.d.ts +6 -0
- package/lib/adapters/memory/search.js +30 -0
- package/lib/adapters/memory/store.d.ts +34 -0
- package/lib/adapters/memory/store.js +266 -0
- package/lib/adapters/sqlite/backups.d.ts +53 -0
- package/lib/adapters/sqlite/backups.js +232 -0
- package/lib/adapters/sqlite/blobs.d.ts +14 -0
- package/lib/adapters/sqlite/blobs.js +109 -0
- package/lib/adapters/sqlite/board.d.ts +77 -0
- package/lib/adapters/sqlite/board.js +328 -0
- package/lib/adapters/sqlite/database.d.ts +67 -0
- package/lib/adapters/sqlite/database.js +285 -0
- package/lib/adapters/sqlite/index.d.ts +3 -0
- package/lib/adapters/sqlite/index.js +2 -0
- package/lib/adapters/sqlite/lock.d.ts +11 -0
- package/lib/adapters/sqlite/lock.js +34 -0
- package/lib/adapters/sqlite/records.d.ts +54 -0
- package/lib/adapters/sqlite/records.js +546 -0
- package/lib/adapters/sqlite/schema.d.ts +54 -0
- package/lib/adapters/sqlite/schema.js +276 -0
- package/lib/adapters/sqlite/store.d.ts +39 -0
- package/lib/adapters/sqlite/store.js +385 -0
- package/lib/browser/in-page.d.ts +31 -0
- package/lib/browser/in-page.js +46 -0
- package/lib/browser/old-preview.d.ts +13 -0
- package/lib/browser/old-preview.js +80 -0
- package/lib/browser/preview-board.d.ts +8 -0
- package/lib/browser/preview-board.js +69 -0
- package/lib/cli/main.d.ts +7 -0
- package/lib/cli/main.js +343 -0
- package/lib/cli/orbit.d.ts +8 -0
- package/lib/cli/orbit.js +25 -0
- package/lib/client/http.d.ts +38 -0
- package/lib/client/http.js +334 -0
- package/lib/client/local.d.ts +26 -0
- package/lib/client/local.js +190 -0
- package/lib/client/object-url.d.ts +8 -0
- package/lib/client/object-url.js +21 -0
- package/lib/client/types.d.ts +268 -0
- package/lib/client/types.js +8 -0
- package/lib/commands/agent-tools.d.ts +31 -0
- package/lib/commands/agent-tools.js +100 -0
- package/lib/commands/index.d.ts +56 -0
- package/lib/commands/index.js +298 -0
- package/lib/commands/parse.d.ts +73 -0
- package/lib/commands/parse.js +231 -0
- package/lib/commands/present.d.ts +134 -0
- package/lib/commands/present.js +198 -0
- package/lib/commands/session.d.ts +85 -0
- package/lib/commands/session.js +138 -0
- package/lib/commands/verbs.d.ts +35 -0
- package/lib/commands/verbs.js +842 -0
- package/lib/core/actors.d.ts +18 -0
- package/lib/core/actors.js +45 -0
- package/lib/core/agent.d.ts +128 -0
- package/lib/core/agent.js +323 -0
- package/lib/core/boards.d.ts +113 -0
- package/lib/core/boards.js +288 -0
- package/lib/core/bootstrap.d.ts +47 -0
- package/lib/core/bootstrap.js +100 -0
- package/lib/core/bytes.d.ts +10 -0
- package/lib/core/bytes.js +52 -0
- package/lib/core/card-paths.d.ts +25 -0
- package/lib/core/card-paths.js +44 -0
- package/lib/core/context.d.ts +60 -0
- package/lib/core/context.js +42 -0
- package/lib/core/cross-board.d.ts +81 -0
- package/lib/core/cross-board.js +228 -0
- package/lib/core/describe.d.ts +21 -0
- package/lib/core/describe.js +81 -0
- package/lib/core/errors.d.ts +159 -0
- package/lib/core/errors.js +160 -0
- package/lib/core/files.d.ts +79 -0
- package/lib/core/files.js +262 -0
- package/lib/core/history.d.ts +20 -0
- package/lib/core/history.js +53 -0
- package/lib/core/image-types.d.ts +16 -0
- package/lib/core/image-types.js +36 -0
- package/lib/core/model.d.ts +263 -0
- package/lib/core/model.js +34 -0
- package/lib/core/nebula-link.d.ts +14 -0
- package/lib/core/nebula-link.js +39 -0
- package/lib/core/relations.d.ts +58 -0
- package/lib/core/relations.js +112 -0
- package/lib/core/search.d.ts +38 -0
- package/lib/core/search.js +92 -0
- package/lib/core/sessions.d.ts +44 -0
- package/lib/core/sessions.js +158 -0
- package/lib/core/snapshot-schema.d.ts +295 -0
- package/lib/core/snapshot-schema.js +82 -0
- package/lib/core/snapshots.d.ts +75 -0
- package/lib/core/snapshots.js +409 -0
- package/lib/core/states.d.ts +25 -0
- package/lib/core/states.js +126 -0
- package/lib/core/text.d.ts +40 -0
- package/lib/core/text.js +113 -0
- package/lib/core/tickets.d.ts +69 -0
- package/lib/core/tickets.js +311 -0
- package/lib/core/trello.d.ts +17 -0
- package/lib/core/trello.js +269 -0
- package/lib/core/verdicts.d.ts +28 -0
- package/lib/core/verdicts.js +92 -0
- package/lib/core/views.d.ts +95 -0
- package/lib/core/views.js +186 -0
- package/lib/http/app.d.ts +46 -0
- package/lib/http/app.js +491 -0
- package/lib/mcp-server.d.ts +10 -0
- package/lib/mcp-server.js +120 -0
- package/lib/node/assets.d.ts +7 -0
- package/lib/node/assets.js +68 -0
- package/lib/node/data-folder.d.ts +21 -0
- package/lib/node/data-folder.js +34 -0
- package/lib/node/env.d.ts +38 -0
- package/lib/node/env.js +49 -0
- package/lib/node/host-check.d.ts +24 -0
- package/lib/node/host-check.js +53 -0
- package/lib/node/host-services.d.ts +9 -0
- package/lib/node/host-services.js +150 -0
- package/lib/node/local.d.ts +22 -0
- package/lib/node/local.js +61 -0
- package/lib/node/migrate.d.ts +53 -0
- package/lib/node/migrate.js +635 -0
- package/lib/node/package.d.ts +7 -0
- package/lib/node/package.js +14 -0
- package/lib/node/repo-board.d.ts +49 -0
- package/lib/node/repo-board.js +245 -0
- package/lib/node/serve.d.ts +8 -0
- package/lib/node/serve.js +106 -0
- package/lib/orbit.d.ts +47 -0
- package/lib/orbit.js +44 -0
- package/lib/quiet-sqlite-warning.d.ts +1 -0
- package/lib/quiet-sqlite-warning.js +18 -0
- package/lib/store/blob-path.d.ts +6 -0
- package/lib/store/blob-path.js +12 -0
- package/lib/store/conformance.d.ts +52 -0
- package/lib/store/conformance.js +1449 -0
- package/lib/store/search-words.d.ts +10 -0
- package/lib/store/search-words.js +19 -0
- package/lib/store/types.d.ts +452 -0
- package/lib/store/types.js +34 -0
- package/llms.txt +63 -0
- package/package.json +109 -25
- package/src/cli/orbit.js +11 -658
- package/src/mcp-server.js +16 -876
- package/SKILL-ORBIT.md +0 -151
- package/build.mjs +0 -210
- package/dist/full/js/app.4e6fbe20.js +0 -952
- package/dist/full/js/app.4e6fbe20.js.map +0 -7
- package/dist/full/styles.4e60d824.css +0 -1
- package/docs/AGENT_PROTOCOL.md +0 -424
- package/docs/MCP_SETUP.md +0 -54
- package/docs/ORBIT_DISPATCH.md +0 -280
- package/docs/SS-Orbit-Dark.png +0 -0
- package/docs/SS-Orbit-Light.png +0 -0
- package/docs/mcp-client.example.json +0 -13
- package/docs/setup/orbit-init-wizard.md +0 -174
- package/public/apple-touch-icon.png +0 -0
- package/public/cursors/live_arrow.cur +0 -0
- package/public/cursors/live_link.cur +0 -0
- package/public/cursors/live_move.cur +0 -0
- package/public/cursors/live_select.cur +0 -0
- package/public/cursors/live_unavail.cur +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +0 -93
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +0 -93
- package/public/fonts/Inter/README.txt +0 -118
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/README.txt +0 -79
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +0 -100
- package/public/fonts/Orbitron/OFL.txt +0 -93
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +0 -68
- package/public/fonts/Rajdhani/OFL.txt +0 -93
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +0 -769
- package/public/js/actor-labels.js +0 -65
- package/public/js/api.js +0 -39
- package/public/js/app.js +0 -45
- package/public/js/artifact-drawer.js +0 -79
- package/public/js/board-export.js +0 -161
- package/public/js/board-menu.js +0 -374
- package/public/js/card-actions.js +0 -545
- package/public/js/config.js +0 -20
- package/public/js/create-card.js +0 -190
- package/public/js/dom.js +0 -19
- package/public/js/drawer.js +0 -273
- package/public/js/font-preference.js +0 -230
- package/public/js/format.js +0 -433
- package/public/js/help-menu.js +0 -31
- package/public/js/icons.js +0 -16
- package/public/js/kanban.js +0 -694
- package/public/js/local-backend.js +0 -1015
- package/public/js/main.js +0 -161
- package/public/js/modal.js +0 -33
- package/public/js/motion-preference.js +0 -64
- package/public/js/panel.js +0 -58
- package/public/js/router.js +0 -134
- package/public/js/search.js +0 -56
- package/public/js/settings-tabs.js +0 -7
- package/public/js/settings.js +0 -1634
- package/public/js/sse.js +0 -159
- package/public/js/starscape.js +0 -332
- package/public/js/state.js +0 -129
- package/public/js/text-editing.js +0 -128
- package/public/js/theme-preference.js +0 -95
- package/public/js/ticket-detail.js +0 -1516
- package/public/js/ticket-typeahead.js +0 -49
- package/public/js/toast.js +0 -89
- package/public/js/topbar-notes.js +0 -155
- package/public/js/transport.js +0 -22
- package/public/js/typeahead.js +0 -206
- package/public/js/unread.js +0 -121
- package/public/js/update-check.js +0 -58
- package/public/js/url-routes.js +0 -94
- package/public/orbits-favicon.png +0 -0
- package/public/orbits-logo-bg-wide.png +0 -0
- package/public/orbits-logo-bg.png +0 -0
- package/public/orbits-logo.png +0 -0
- package/public/styles.css +0 -7709
- package/src/core/agent.js +0 -387
- package/src/core/agents-md.js +0 -109
- package/src/core/artifacts.js +0 -149
- package/src/core/attachments.js +0 -238
- package/src/core/auth.js +0 -85
- package/src/core/backups.js +0 -142
- package/src/core/board.js +0 -148
- package/src/core/boards.js +0 -409
- package/src/core/bootstrap.js +0 -170
- package/src/core/css-bundle.js +0 -30
- package/src/core/db.js +0 -315
- package/src/core/delete-board.js +0 -163
- package/src/core/dispatch.js +0 -1007
- package/src/core/events.js +0 -314
- package/src/core/http.js +0 -115
- package/src/core/migrate.js +0 -198
- package/src/core/paths.js +0 -55
- package/src/core/provision-repo-board.js +0 -119
- package/src/core/queries.js +0 -246
- package/src/core/registry.js +0 -211
- package/src/core/relations.js +0 -163
- package/src/core/review-verdicts.js +0 -138
- package/src/core/router.js +0 -556
- package/src/core/search.js +0 -183
- package/src/core/seed.js +0 -234
- package/src/core/snapshots.js +0 -692
- package/src/core/states.js +0 -167
- package/src/core/system-picker.js +0 -109
- package/src/core/tickets.js +0 -400
- package/src/core/util.js +0 -106
- package/src/core/version-check.js +0 -154
- package/src/mcp/orbit-client.js +0 -287
- package/src/server.js +0 -115
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SQLite adapter: Orbit's storage port on `node:sqlite`, over a data
|
|
3
|
+
* folder.
|
|
4
|
+
*
|
|
5
|
+
* registry.db the list of boards and the settings
|
|
6
|
+
* boards/<folder>/board.db one board's records
|
|
7
|
+
* boards/<folder>/attachments/... its images (the blob store)
|
|
8
|
+
* boards/<folder>/artifacts/... its markdown
|
|
9
|
+
* backups/ copies (VACUUM INTO), with each board's files linked beside them
|
|
10
|
+
*
|
|
11
|
+
* The schema is ./schema.ts. A folder whose files lack v2's version (Orbit
|
|
12
|
+
* 1.x, or 2.0 before its rebuild) is refused untouched, with
|
|
13
|
+
* `data_needs_migration`. Several stores, in one process or several, can open
|
|
14
|
+
* one folder at once: SQLite's locks keep their writes one at a time, and
|
|
15
|
+
* each hears the others' changes.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readdirSync, rmdirSync, rmSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { OrbitError, isOrbitError } from "../../core/errors.js";
|
|
20
|
+
import { createSqliteBackups } from "./backups.js";
|
|
21
|
+
import { createSqliteBlobStore } from "./blobs.js";
|
|
22
|
+
import { BoardFiles } from "./board.js";
|
|
23
|
+
import { checkFile, Connection, createFile, untilFree } from "./database.js";
|
|
24
|
+
import { Lock } from "./lock.js";
|
|
25
|
+
import { CROSS, toRecord } from "./records.js";
|
|
26
|
+
import { BOARD_SCHEMA, REGISTRY_SCHEMA } from "./schema.js";
|
|
27
|
+
const LISTING = "id, slug, name, folder, repo_url, default_branch, last_active_at, created_at, updated_at";
|
|
28
|
+
const REGISTRY = {
|
|
29
|
+
list: `SELECT ${LISTING} FROM boards ORDER BY name, seq`,
|
|
30
|
+
get: `SELECT ${LISTING} FROM boards WHERE id = ?`,
|
|
31
|
+
bySlug: `SELECT ${LISTING} FROM boards WHERE slug = ?`,
|
|
32
|
+
latest: `SELECT ${LISTING} FROM boards ORDER BY last_active_at DESC, seq DESC LIMIT 1`,
|
|
33
|
+
slugTaken: "SELECT 1 AS taken FROM boards WHERE slug = ? AND id <> ?",
|
|
34
|
+
folderTaken: "SELECT 1 AS taken FROM boards WHERE folder = ?",
|
|
35
|
+
insert: `INSERT INTO boards (${LISTING}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
36
|
+
touch: "UPDATE boards SET last_active_at = ?, updated_at = ? WHERE id = ?",
|
|
37
|
+
follow: "UPDATE boards SET slug = ?, name = ?, repo_url = ?, default_branch = ?, updated_at = ? WHERE id = ?",
|
|
38
|
+
remove: "DELETE FROM boards WHERE id = ? RETURNING folder",
|
|
39
|
+
setting: "SELECT value FROM settings WHERE key = ?",
|
|
40
|
+
setSetting: "INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value"
|
|
41
|
+
};
|
|
42
|
+
const TABLE_OF = { state: "states", ticket: "tickets", entry: "board_entries", relation: "relations" };
|
|
43
|
+
/** A board's folder name, from the slug it was made with: letters, digits, `.`, `_` and `-`. */
|
|
44
|
+
function folderName(slug) {
|
|
45
|
+
const name = slug.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-.]+|-+$/g, "").slice(0, 80);
|
|
46
|
+
return name || "board";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Opens a store over `dataDir`, making the folder and its registry when
|
|
50
|
+
* they're missing. Rejects with `data_needs_migration` for a folder of older
|
|
51
|
+
* data, which it leaves as it was.
|
|
52
|
+
*/
|
|
53
|
+
export function openSqliteStore(options) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
resolve(open(options));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function open(options) {
|
|
59
|
+
const { dataDir } = options;
|
|
60
|
+
const busyMs = options.busyTimeoutMs ?? 5000;
|
|
61
|
+
const registryPath = join(dataDir, "registry.db");
|
|
62
|
+
const boardsDir = join(dataDir, "boards");
|
|
63
|
+
// Checked before anything is made, so a folder of older data is left exactly as it was.
|
|
64
|
+
if (!checkFile(registryPath)) {
|
|
65
|
+
mkdirSync(dataDir, { recursive: true });
|
|
66
|
+
createFile(registryPath, REGISTRY_SCHEMA);
|
|
67
|
+
checkFile(registryPath);
|
|
68
|
+
}
|
|
69
|
+
mkdirSync(boardsDir, { recursive: true });
|
|
70
|
+
const regWriter = new Connection(registryPath);
|
|
71
|
+
const regReader = new Connection(registryPath, { queryOnly: true });
|
|
72
|
+
const registryLock = new Lock();
|
|
73
|
+
/** Each board's folder, once seen; kept after a board is removed, so its blobs can still be removed. */
|
|
74
|
+
const folders = new Map();
|
|
75
|
+
const opened = new Map();
|
|
76
|
+
let closed = false;
|
|
77
|
+
const deadline = () => Date.now() + busyMs;
|
|
78
|
+
const live = () => {
|
|
79
|
+
if (closed)
|
|
80
|
+
throw new OrbitError("storage_failed", { message: "This store is closed." });
|
|
81
|
+
};
|
|
82
|
+
const listing = (row) => {
|
|
83
|
+
const folder = String(row.folder);
|
|
84
|
+
folders.set(String(row.id), folder);
|
|
85
|
+
return {
|
|
86
|
+
id: String(row.id),
|
|
87
|
+
slug: String(row.slug),
|
|
88
|
+
name: String(row.name),
|
|
89
|
+
db_path: join(boardsDir, folder, "board.db"),
|
|
90
|
+
repo_url: String(row.repo_url),
|
|
91
|
+
default_branch: String(row.default_branch),
|
|
92
|
+
last_active_at: row.last_active_at === null ? null : String(row.last_active_at),
|
|
93
|
+
created_at: String(row.created_at),
|
|
94
|
+
updated_at: String(row.updated_at)
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
const find = (sql, key) => {
|
|
98
|
+
live();
|
|
99
|
+
const row = regReader.get(sql, [key]);
|
|
100
|
+
return row ? listing(row) : null;
|
|
101
|
+
};
|
|
102
|
+
const listed = () => {
|
|
103
|
+
live();
|
|
104
|
+
return regReader.all(REGISTRY.list).map(listing);
|
|
105
|
+
};
|
|
106
|
+
/** A registry write on its own, in turn with the others. */
|
|
107
|
+
const registryWrite = (attempt) => registryLock.run(() => untilFree(attempt, deadline()));
|
|
108
|
+
const host = {
|
|
109
|
+
busyMs,
|
|
110
|
+
slugTaken: (slug, exceptId) => regReader.get(REGISTRY.slugTaken, [slug, exceptId]) !== undefined,
|
|
111
|
+
async follow(board) {
|
|
112
|
+
const release = await registryLock.acquire();
|
|
113
|
+
try {
|
|
114
|
+
await untilFree(() => {
|
|
115
|
+
regWriter.exec("BEGIN IMMEDIATE");
|
|
116
|
+
}, deadline());
|
|
117
|
+
try {
|
|
118
|
+
regWriter.run(REGISTRY.follow, [board.slug, board.name, board.repo_url, board.default_branch, board.updated_at, board.id]);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
regWriter.rollback();
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
release();
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
commit() {
|
|
131
|
+
try {
|
|
132
|
+
regWriter.exec("COMMIT");
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
release();
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
rollback() {
|
|
139
|
+
regWriter.rollback();
|
|
140
|
+
release();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
/** The board's open files: opened once per store, and again after the board was made anew. */
|
|
146
|
+
async function filesOf(entry) {
|
|
147
|
+
const pending = opened.get(entry.id);
|
|
148
|
+
if (pending) {
|
|
149
|
+
const files = await pending.catch(() => null);
|
|
150
|
+
if (files?.isLive())
|
|
151
|
+
return files;
|
|
152
|
+
if (opened.get(entry.id) === pending)
|
|
153
|
+
opened.delete(entry.id);
|
|
154
|
+
}
|
|
155
|
+
const opening = BoardFiles.open(host, entry.id, entry.db_path, entry.slug);
|
|
156
|
+
opened.set(entry.id, opening);
|
|
157
|
+
try {
|
|
158
|
+
return await opening;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (opened.get(entry.id) === opening)
|
|
162
|
+
opened.delete(entry.id);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const boardStores = new WeakMap();
|
|
167
|
+
const storeOf = (files) => {
|
|
168
|
+
let store = boardStores.get(files);
|
|
169
|
+
if (!store) {
|
|
170
|
+
store = files.store();
|
|
171
|
+
boardStores.set(files, store);
|
|
172
|
+
}
|
|
173
|
+
return store;
|
|
174
|
+
};
|
|
175
|
+
/** A folder no board uses and nothing is in. */
|
|
176
|
+
function freeFolder(slug) {
|
|
177
|
+
const base = folderName(slug);
|
|
178
|
+
for (let n = 1;; n += 1) {
|
|
179
|
+
const name = n === 1 ? base : `${base}-${String(n)}`;
|
|
180
|
+
if (regWriter.get(REGISTRY.folderTaken, [name]))
|
|
181
|
+
continue;
|
|
182
|
+
const path = join(boardsDir, name);
|
|
183
|
+
if (existsSync(path) && readdirSync(path).length > 0)
|
|
184
|
+
continue;
|
|
185
|
+
return name;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const boards = {
|
|
189
|
+
list: () => Promise.resolve(listed()),
|
|
190
|
+
get: (id) => Promise.resolve(find(REGISTRY.get, id)),
|
|
191
|
+
bySlug: (slug) => Promise.resolve(find(REGISTRY.bySlug, slug)),
|
|
192
|
+
latest() {
|
|
193
|
+
live();
|
|
194
|
+
const row = regReader.get(REGISTRY.latest);
|
|
195
|
+
return Promise.resolve(row ? listing(row) : null);
|
|
196
|
+
},
|
|
197
|
+
create(board, seed) {
|
|
198
|
+
return registryLock.run(async () => {
|
|
199
|
+
live();
|
|
200
|
+
await untilFree(() => {
|
|
201
|
+
regWriter.exec("BEGIN IMMEDIATE");
|
|
202
|
+
}, deadline());
|
|
203
|
+
let folderPath = null;
|
|
204
|
+
let files = null;
|
|
205
|
+
try {
|
|
206
|
+
if (regWriter.get(REGISTRY.get, [board.id]))
|
|
207
|
+
throw new OrbitError("duplicate_id");
|
|
208
|
+
if (regWriter.get(REGISTRY.bySlug, [board.slug]))
|
|
209
|
+
throw new OrbitError("board_slug_taken");
|
|
210
|
+
const folder = freeFolder(board.slug);
|
|
211
|
+
folderPath = join(boardsDir, folder);
|
|
212
|
+
mkdirSync(folderPath, { recursive: true });
|
|
213
|
+
const dbPath = join(folderPath, "board.db");
|
|
214
|
+
createFile(dbPath, BOARD_SCHEMA);
|
|
215
|
+
files = await BoardFiles.open(host, board.id, dbPath, board.slug);
|
|
216
|
+
const made = await files.seed(board, seed);
|
|
217
|
+
regWriter.run(REGISTRY.insert, [made.id, made.slug, made.name, folder, made.repo_url, made.default_branch, board.created_at, made.created_at, made.updated_at]);
|
|
218
|
+
regWriter.exec("COMMIT");
|
|
219
|
+
opened.set(board.id, Promise.resolve(files));
|
|
220
|
+
const entry = regReader.get(REGISTRY.get, [board.id]);
|
|
221
|
+
if (!entry)
|
|
222
|
+
throw new OrbitError("storage_failed");
|
|
223
|
+
return listing(entry);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
regWriter.rollback();
|
|
227
|
+
files?.markRemoved();
|
|
228
|
+
if (folderPath)
|
|
229
|
+
rmSync(folderPath, { recursive: true, force: true });
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
async touch(id, at) {
|
|
235
|
+
live();
|
|
236
|
+
const { changes } = await registryWrite(() => regWriter.run(REGISTRY.touch, [at, at, id]));
|
|
237
|
+
if (changes === 0)
|
|
238
|
+
throw new OrbitError("board_not_found");
|
|
239
|
+
},
|
|
240
|
+
async remove(id, options = {}) {
|
|
241
|
+
live();
|
|
242
|
+
const entry = find(REGISTRY.get, id);
|
|
243
|
+
if (!entry)
|
|
244
|
+
throw new OrbitError("board_not_found");
|
|
245
|
+
const files = await opened.get(id)?.catch(() => null);
|
|
246
|
+
const unlist = async () => {
|
|
247
|
+
const removed = [];
|
|
248
|
+
if (!options.keepData) {
|
|
249
|
+
// The files go before the board leaves the list: a delete stopped by a
|
|
250
|
+
// file another program holds leaves the board listed, to be deleted again.
|
|
251
|
+
// This store lets go of them first, since an open handle keeps a file on Windows.
|
|
252
|
+
files?.markRemoved();
|
|
253
|
+
opened.delete(id);
|
|
254
|
+
try {
|
|
255
|
+
for (const suffix of ["", "-wal", "-shm", "-journal"]) {
|
|
256
|
+
const path = `${entry.db_path}${suffix}`;
|
|
257
|
+
if (!existsSync(path))
|
|
258
|
+
continue;
|
|
259
|
+
rmSync(path, REMOVE_FILE);
|
|
260
|
+
removed.push(path);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
throw filesBusy(error, entry.slug);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const row = await registryWrite(() => regWriter.get(REGISTRY.remove, [id]));
|
|
268
|
+
if (!row)
|
|
269
|
+
throw new OrbitError("board_not_found");
|
|
270
|
+
if (options.keepData) {
|
|
271
|
+
files?.markRemoved();
|
|
272
|
+
opened.delete(id);
|
|
273
|
+
return { removed };
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
rmdirSync(join(boardsDir, String(row.folder)));
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Its images and markdown are still there: the blob store's to remove.
|
|
280
|
+
}
|
|
281
|
+
return { removed };
|
|
282
|
+
};
|
|
283
|
+
return files ? files.exclusive(unlist) : unlist();
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const settings = {
|
|
287
|
+
get(key) {
|
|
288
|
+
live();
|
|
289
|
+
const row = regReader.get(REGISTRY.setting, [key]);
|
|
290
|
+
return Promise.resolve(row ? String(row.value) : null);
|
|
291
|
+
},
|
|
292
|
+
async set(key, value) {
|
|
293
|
+
live();
|
|
294
|
+
await registryWrite(() => regWriter.run(REGISTRY.setSetting, [key, value]));
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
/** A board's folder: what this store has seen, else the registry's; null for a board it never had. */
|
|
298
|
+
const folderOf = (boardId) => {
|
|
299
|
+
if (!folders.has(boardId))
|
|
300
|
+
find(REGISTRY.get, boardId);
|
|
301
|
+
const folder = folders.get(boardId);
|
|
302
|
+
return folder === undefined ? null : join(boardsDir, folder);
|
|
303
|
+
};
|
|
304
|
+
return {
|
|
305
|
+
boards,
|
|
306
|
+
settings,
|
|
307
|
+
blobs: createSqliteBlobStore(folderOf),
|
|
308
|
+
backups: createSqliteBackups({
|
|
309
|
+
dataDir,
|
|
310
|
+
retention: options.backupRetention ?? 30,
|
|
311
|
+
busyMs,
|
|
312
|
+
now: options.now ?? (() => new Date()),
|
|
313
|
+
board(id) {
|
|
314
|
+
const entry = find(REGISTRY.get, id);
|
|
315
|
+
return entry ? { dbPath: entry.db_path, slug: entry.slug, name: entry.name } : null;
|
|
316
|
+
}
|
|
317
|
+
}),
|
|
318
|
+
async board(id) {
|
|
319
|
+
live();
|
|
320
|
+
const pending = opened.get(id);
|
|
321
|
+
const known = pending ? await pending.catch(() => null) : null;
|
|
322
|
+
if (known?.isLive())
|
|
323
|
+
return storeOf(known);
|
|
324
|
+
const entry = find(REGISTRY.get, id);
|
|
325
|
+
if (!entry)
|
|
326
|
+
throw new OrbitError("board_not_found");
|
|
327
|
+
return storeOf(await filesOf(entry));
|
|
328
|
+
},
|
|
329
|
+
async locate(kind, id, hint) {
|
|
330
|
+
live();
|
|
331
|
+
const order = listed().sort((a, b) => Number(b.id === hint) - Number(a.id === hint));
|
|
332
|
+
for (const entry of order) {
|
|
333
|
+
const files = await filesOf(entry);
|
|
334
|
+
if (files.query(`SELECT 1 AS here FROM ${TABLE_OF[kind]} WHERE id = ? LIMIT 1`, [id]).length > 0)
|
|
335
|
+
return entry;
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
},
|
|
339
|
+
async relationsInto(boardId) {
|
|
340
|
+
live();
|
|
341
|
+
const found = [];
|
|
342
|
+
for (const entry of listed()) {
|
|
343
|
+
// A board whose file is gone (or won't open) links to nothing, as cross-board reads treat it: one such board never stops every other card's drawer.
|
|
344
|
+
const files = await filesOf(entry).catch((error) => {
|
|
345
|
+
if (isOrbitError(error))
|
|
346
|
+
return null;
|
|
347
|
+
throw error;
|
|
348
|
+
});
|
|
349
|
+
if (!files)
|
|
350
|
+
continue;
|
|
351
|
+
for (const row of files.query(`${CROSS.select} WHERE target_board_id = ? ORDER BY created_at, seq`, [boardId])) {
|
|
352
|
+
found.push({ board_id: entry.id, relation: toRecord(CROSS, row, entry.id) });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return found;
|
|
356
|
+
},
|
|
357
|
+
async close() {
|
|
358
|
+
if (closed)
|
|
359
|
+
return;
|
|
360
|
+
closed = true;
|
|
361
|
+
for (const pending of opened.values()) {
|
|
362
|
+
const files = await pending.catch(() => null);
|
|
363
|
+
await files?.close();
|
|
364
|
+
}
|
|
365
|
+
opened.clear();
|
|
366
|
+
await registryLock.run(() => {
|
|
367
|
+
regWriter.close();
|
|
368
|
+
regReader.close();
|
|
369
|
+
return Promise.resolve();
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/** Windows refuses to remove a file another program has open, often only for a moment. */
|
|
375
|
+
const REMOVE_FILE = process.platform === "win32" ? { force: true, maxRetries: 10, retryDelay: 100 } : { force: true };
|
|
376
|
+
/** A file another program holds becomes `board_files_busy`, which says what to do; anything else is a storage fault. */
|
|
377
|
+
function filesBusy(error, slug) {
|
|
378
|
+
const code = error?.code;
|
|
379
|
+
if (code !== "EBUSY" && code !== "EPERM" && code !== "ENOTEMPTY")
|
|
380
|
+
return new OrbitError("storage_failed", { cause: error });
|
|
381
|
+
return new OrbitError("board_files_busy", {
|
|
382
|
+
message: `Orbit couldn't remove the files of board "${slug}": another program has them open. On Windows that is usually an Orbit server, an MCP helper, an AI app, an editor or a terminal. Close it, then delete the board again from Settings.`,
|
|
383
|
+
cause: error
|
|
384
|
+
});
|
|
385
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser edition: Orbit's own core in the page, over IndexedDB, with
|
|
3
|
+
* no server. `openBrowserOrbit()` opens the store, Orbit and its HTTP app,
|
|
4
|
+
* and gives a `fetch` that answers the JSON API in the page (app.fetch) and
|
|
5
|
+
* an OrbitClient over it. The web app sends its requests there as it would
|
|
6
|
+
* to `orbit run`; the event stream works the same way, through app.fetch.
|
|
7
|
+
*
|
|
8
|
+
* What a browser can't do stays off: agents (MCP), the folder picker,
|
|
9
|
+
* self-update. The version is the build's, with no check for a newer one.
|
|
10
|
+
*/
|
|
11
|
+
import type { OrbitClient } from "../client/types.ts";
|
|
12
|
+
import type { OrbitApp } from "../http/app.ts";
|
|
13
|
+
import type { Orbit } from "../orbit.ts";
|
|
14
|
+
export interface BrowserOrbit {
|
|
15
|
+
orbit: Orbit;
|
|
16
|
+
app: OrbitApp;
|
|
17
|
+
/** The JSON API, answered in this page. */
|
|
18
|
+
fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
19
|
+
/** The OrbitClient over that fetch: images as object URLs, changes through the event stream. */
|
|
20
|
+
client: OrbitClient;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface BrowserOrbitOptions {
|
|
24
|
+
/** The build's version, for Help and /api/version. */
|
|
25
|
+
version: string;
|
|
26
|
+
/** The IndexedDB database. Default: `orbit`. */
|
|
27
|
+
name?: string;
|
|
28
|
+
/** Make the welcome board when there are no boards. Default: true. */
|
|
29
|
+
seed?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function openBrowserOrbit(options: BrowserOrbitOptions): Promise<BrowserOrbit>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* The browser edition: Orbit's own core in the page, over IndexedDB, with
|
|
4
|
+
* no server. `openBrowserOrbit()` opens the store, Orbit and its HTTP app,
|
|
5
|
+
* and gives a `fetch` that answers the JSON API in the page (app.fetch) and
|
|
6
|
+
* an OrbitClient over it. The web app sends its requests there as it would
|
|
7
|
+
* to `orbit run`; the event stream works the same way, through app.fetch.
|
|
8
|
+
*
|
|
9
|
+
* What a browser can't do stays off: agents (MCP), the folder picker,
|
|
10
|
+
* self-update. The version is the build's, with no check for a newer one.
|
|
11
|
+
*/
|
|
12
|
+
import { openIndexedDbStore } from "../adapters/indexeddb/index.js";
|
|
13
|
+
import { createLocalLocks, webLocks } from "../adapters/indexeddb/locks.js";
|
|
14
|
+
import { createHttpClient } from "../client/http.js";
|
|
15
|
+
import { createOrbitApp } from "../http/app.js";
|
|
16
|
+
import { createOrbit } from "../orbit.js";
|
|
17
|
+
import { bringOldPreview } from "./old-preview.js";
|
|
18
|
+
import { seedPreviewBoard } from "./preview-board.js";
|
|
19
|
+
export async function openBrowserOrbit(options) {
|
|
20
|
+
const locks = webLocks() ?? createLocalLocks();
|
|
21
|
+
const store = await openIndexedDbStore({ locks, ...(options.name ? { name: options.name } : {}) });
|
|
22
|
+
const version = {
|
|
23
|
+
current: options.version,
|
|
24
|
+
deployMode: "browser",
|
|
25
|
+
selfUpdate: false,
|
|
26
|
+
repoUrl: "",
|
|
27
|
+
changesUrl: "",
|
|
28
|
+
enabled: false,
|
|
29
|
+
latest: null,
|
|
30
|
+
updateAvailable: false,
|
|
31
|
+
error: null,
|
|
32
|
+
checkedAt: null
|
|
33
|
+
};
|
|
34
|
+
const orbit = createOrbit({ store, blobs: store.blobs, host: { version: () => Promise.resolve(version) } });
|
|
35
|
+
const app = createOrbitApp(orbit, { version: options.version });
|
|
36
|
+
if (options.seed !== false) {
|
|
37
|
+
// The old preview's board first (once per origin), so a person who used it finds it instead of a welcome board.
|
|
38
|
+
await locks.request("orbit-browser-edition:seed", async () => {
|
|
39
|
+
await bringOldPreview(orbit).catch(() => null);
|
|
40
|
+
});
|
|
41
|
+
await seedPreviewBoard(orbit, locks);
|
|
42
|
+
}
|
|
43
|
+
const inPage = (input, init) => app.fetch(new Request(input, init));
|
|
44
|
+
const client = createHttpClient({ baseUrl: location.origin, fetch: inPage, images: "object" });
|
|
45
|
+
return { orbit, app, fetch: inPage, client, close: () => orbit.close() };
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board of the old browser preview (1.x and 2.0 before its rebuild kept
|
|
3
|
+
* it in this origin's `starscape-orbit-preview` database), brought into the
|
|
4
|
+
* browser edition once, so a person who tried Orbit here finds their board.
|
|
5
|
+
* Its database is opened at the version it has and read in one read-only
|
|
6
|
+
* transaction, so it stays as it was. A board nobody changed (the preview's
|
|
7
|
+
* own welcome board, as it was made) isn't brought: the new edition has its
|
|
8
|
+
* own. Whatever happens, it is decided once per origin.
|
|
9
|
+
*/
|
|
10
|
+
import type { Orbit } from "../orbit.ts";
|
|
11
|
+
export declare const OLD_PREVIEW_DB = "starscape-orbit-preview";
|
|
12
|
+
/** Brings the old preview's board over, once per origin; says what it did (or null when it was decided before). */
|
|
13
|
+
export declare function bringOldPreview(orbit: Orbit): Promise<string | null>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* The board of the old browser preview (1.x and 2.0 before its rebuild kept
|
|
4
|
+
* it in this origin's `starscape-orbit-preview` database), brought into the
|
|
5
|
+
* browser edition once, so a person who tried Orbit here finds their board.
|
|
6
|
+
* Its database is opened at the version it has and read in one read-only
|
|
7
|
+
* transaction, so it stays as it was. A board nobody changed (the preview's
|
|
8
|
+
* own welcome board, as it was made) isn't brought: the new edition has its
|
|
9
|
+
* own. Whatever happens, it is decided once per origin.
|
|
10
|
+
*/
|
|
11
|
+
import { localOwnerActor } from "../core/actors.js";
|
|
12
|
+
import { OLD_PREVIEW_FORMAT, fromOldPreview, importKeepingBoard } from "../core/snapshots.js";
|
|
13
|
+
export const OLD_PREVIEW_DB = "starscape-orbit-preview";
|
|
14
|
+
const DONE = "old_preview";
|
|
15
|
+
const STORES = ["boards", "states", "labels", "tickets", "ticket_labels", "comments", "relations", "board_entries", "events"];
|
|
16
|
+
/** The old database's stores, read whole; null when this origin has none. */
|
|
17
|
+
async function readOldPreview() {
|
|
18
|
+
if (typeof indexedDB === "undefined" || typeof indexedDB.databases !== "function")
|
|
19
|
+
return null;
|
|
20
|
+
// Opening a database that isn't there would make it: look first.
|
|
21
|
+
if (!(await indexedDB.databases()).some((db) => db.name === OLD_PREVIEW_DB))
|
|
22
|
+
return null;
|
|
23
|
+
const db = await new Promise((resolve, reject) => {
|
|
24
|
+
const request = indexedDB.open(OLD_PREVIEW_DB);
|
|
25
|
+
request.onsuccess = () => {
|
|
26
|
+
resolve(request.result);
|
|
27
|
+
};
|
|
28
|
+
request.onerror = () => {
|
|
29
|
+
reject(request.error ?? new Error("The old preview's database didn't open."));
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
const names = STORES.filter((name) => db.objectStoreNames.contains(name));
|
|
34
|
+
if (!names.length)
|
|
35
|
+
return null;
|
|
36
|
+
const tx = db.transaction(names, "readonly");
|
|
37
|
+
const read = await Promise.all(names.map((name) => new Promise((resolve, reject) => {
|
|
38
|
+
const request = tx.objectStore(name).getAll();
|
|
39
|
+
request.onsuccess = () => {
|
|
40
|
+
resolve([name, request.result]);
|
|
41
|
+
};
|
|
42
|
+
request.onerror = () => {
|
|
43
|
+
reject(request.error ?? new Error(`The old preview's ${name} didn't read.`));
|
|
44
|
+
};
|
|
45
|
+
})));
|
|
46
|
+
return Object.fromEntries(read);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
db.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** True when a person changed the board after the preview made it: any record newer than the board. */
|
|
53
|
+
function changedByAPerson(stores, board) {
|
|
54
|
+
const made = typeof board.created_at === "string" ? board.created_at : "";
|
|
55
|
+
if ((stores.events ?? []).length)
|
|
56
|
+
return true;
|
|
57
|
+
return STORES.some((name) => (stores[name] ?? []).some((row) => [row.created_at, row.updated_at, row.struck_at, row.archived_at].some((at) => typeof at === "string" && at > made)));
|
|
58
|
+
}
|
|
59
|
+
/** Brings the old preview's board over, once per origin; says what it did (or null when it was decided before). */
|
|
60
|
+
export async function bringOldPreview(orbit) {
|
|
61
|
+
const c = orbit.context(localOwnerActor());
|
|
62
|
+
if (await c.store.settings.get(DONE))
|
|
63
|
+
return null;
|
|
64
|
+
const stores = await readOldPreview().catch(() => null);
|
|
65
|
+
const board = stores?.boards?.[0];
|
|
66
|
+
let outcome = "none";
|
|
67
|
+
if (stores && board && changedByAPerson(stores, board)) {
|
|
68
|
+
let slug = typeof board.slug === "string" && board.slug ? board.slug : "preview";
|
|
69
|
+
for (let i = 2; await c.store.boards.bySlug(slug); i += 1)
|
|
70
|
+
slug = `${typeof board.slug === "string" && board.slug ? board.slug : "preview"}-${i}`;
|
|
71
|
+
const id = typeof board.id === "string" && board.id && !(await c.store.boards.get(board.id)) ? board.id : c.ids.next();
|
|
72
|
+
await importKeepingBoard(c, fromOldPreview({ schema: OLD_PREVIEW_FORMAT, board, ...stores }), { id, slug });
|
|
73
|
+
outcome = `brought:${slug}`;
|
|
74
|
+
}
|
|
75
|
+
else if (stores && board) {
|
|
76
|
+
outcome = "unchanged";
|
|
77
|
+
}
|
|
78
|
+
await c.store.settings.set(DONE, JSON.stringify({ outcome, at: c.clock.now() }));
|
|
79
|
+
return outcome;
|
|
80
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board a newcomer finds in the browser edition: lanes, a few cards
|
|
3
|
+
* that show the way around, and a note. Made once, through the core, by
|
|
4
|
+
* whichever tab gets there first.
|
|
5
|
+
*/
|
|
6
|
+
import type { Locks } from "../adapters/indexeddb/locks.ts";
|
|
7
|
+
import type { Orbit } from "../orbit.ts";
|
|
8
|
+
export declare function seedPreviewBoard(orbit: Orbit, locks: Locks): Promise<void>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board a newcomer finds in the browser edition: lanes, a few cards
|
|
3
|
+
* that show the way around, and a note. Made once, through the core, by
|
|
4
|
+
* whichever tab gets there first.
|
|
5
|
+
*/
|
|
6
|
+
import { localOwnerActor } from "../core/actors.js";
|
|
7
|
+
const LANES = [
|
|
8
|
+
["Backlog", null],
|
|
9
|
+
["Todo", null],
|
|
10
|
+
["In Progress", "in_progress"],
|
|
11
|
+
["Review", "review"],
|
|
12
|
+
["Done", "done"],
|
|
13
|
+
["Cancelled", null]
|
|
14
|
+
];
|
|
15
|
+
export async function seedPreviewBoard(orbit, locks) {
|
|
16
|
+
const client = orbit.client({ ...localOwnerActor(), id: "system", name: "system" });
|
|
17
|
+
await locks.request("orbit-browser-edition:seed", async () => {
|
|
18
|
+
if ((await client.boards.list()).boards.length)
|
|
19
|
+
return;
|
|
20
|
+
const board = await client.boards.create({ name: "Preview Board", slug: "preview", ai_enabled: false, states: LANES });
|
|
21
|
+
await client.boards.update(board.slug, {
|
|
22
|
+
project_notes: "This preview lives entirely in your browser (IndexedDB). Use it to try the flow; export the snapshot from Settings → Data when you want to move to a real Orbit install.",
|
|
23
|
+
agent_instructions: "Preview edition has no agent or MCP integration. Install the full Orbit app with npm i -g @starlove/orbit, then run orbit run and create or import a board."
|
|
24
|
+
});
|
|
25
|
+
const hint = { board_id: board.id };
|
|
26
|
+
const lanes = (await client.bootstrap(hint)).states;
|
|
27
|
+
const lane = (name) => lanes.find((state) => state.name === name)?.id;
|
|
28
|
+
const epic = await client.tickets.create({
|
|
29
|
+
...hint,
|
|
30
|
+
title: "Try the preview kanban",
|
|
31
|
+
description: "This onboarding card is for the in-browser preview. Drag it across lanes, edit fields, add comments: it all stays in this browser only.",
|
|
32
|
+
type: "epic",
|
|
33
|
+
ai_plan: "1. Drag this card to In Progress.\n2. Add a comment.\n3. Export the snapshot from Settings → Data.",
|
|
34
|
+
state_id: lane("Todo"),
|
|
35
|
+
priority: 4,
|
|
36
|
+
labels: ["onboarding", "needs-decomposition"]
|
|
37
|
+
});
|
|
38
|
+
await client.tickets.create({
|
|
39
|
+
...hint,
|
|
40
|
+
title: "Edit a card and watch it persist",
|
|
41
|
+
description: "Click into a card, change its title, refresh: your edit survives because it lives in IndexedDB.",
|
|
42
|
+
type: "feature",
|
|
43
|
+
parent_ticket_id: epic.id,
|
|
44
|
+
state_id: lane("Todo"),
|
|
45
|
+
priority: 3,
|
|
46
|
+
labels: ["onboarding"]
|
|
47
|
+
});
|
|
48
|
+
await client.tickets.create({
|
|
49
|
+
...hint,
|
|
50
|
+
title: "Move me to In Progress",
|
|
51
|
+
description: "Drag this card to the In Progress lane, then to Review.",
|
|
52
|
+
type: "task",
|
|
53
|
+
parent_ticket_id: epic.id,
|
|
54
|
+
state_id: lane("Backlog"),
|
|
55
|
+
priority: 2,
|
|
56
|
+
labels: ["onboarding"]
|
|
57
|
+
});
|
|
58
|
+
await client.tickets.create({
|
|
59
|
+
...hint,
|
|
60
|
+
title: "Export your snapshot when ready",
|
|
61
|
+
description: "Settings → Data → Export downloads a JSON snapshot you can import into a real Orbit install.",
|
|
62
|
+
type: "task",
|
|
63
|
+
state_id: lane("Backlog"),
|
|
64
|
+
priority: 1,
|
|
65
|
+
labels: ["onboarding"]
|
|
66
|
+
});
|
|
67
|
+
await client.comments.create(epic.id, { body: "Welcome to the Orbit preview. Everything lives in your browser.", kind: "note" }, hint);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orbit CLI, loaded by the bin (./orbit.ts): `orbit run` serves the app and the API, `orbit mcp` serves agents
|
|
3
|
+
* over stdio, and any other first word is a board command (`orbit view`).
|
|
4
|
+
* Each command loads only what it runs: Hono for `orbit run`, the MCP SDK for
|
|
5
|
+
* `orbit mcp`, neither for a board command.
|
|
6
|
+
*/
|
|
7
|
+
import "../quiet-sqlite-warning.ts";
|