@korajs/cli 0.6.0 → 1.0.0-beta.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin.cjs +2582 -202
  3. package/dist/bin.cjs.map +1 -1
  4. package/dist/bin.js +152 -96
  5. package/dist/bin.js.map +1 -1
  6. package/dist/chunk-5NI2FEQL.js +92 -0
  7. package/dist/chunk-5NI2FEQL.js.map +1 -0
  8. package/dist/chunk-6C4BHSRA.js +82 -0
  9. package/dist/chunk-6C4BHSRA.js.map +1 -0
  10. package/dist/{chunk-VLTPEATY.js → chunk-B5YS4STN.js} +9 -96
  11. package/dist/chunk-B5YS4STN.js.map +1 -0
  12. package/dist/{chunk-EEZNRI5W.js → chunk-BWTKRKNJ.js} +13 -5
  13. package/dist/{chunk-EEZNRI5W.js.map → chunk-BWTKRKNJ.js.map} +1 -1
  14. package/dist/{chunk-Q2FBCOQD.js → chunk-EOWLAAIV.js} +5 -3
  15. package/dist/{chunk-Q2FBCOQD.js.map → chunk-EOWLAAIV.js.map} +1 -1
  16. package/dist/create.cjs +8 -2
  17. package/dist/create.cjs.map +1 -1
  18. package/dist/create.js +3 -2
  19. package/dist/create.js.map +1 -1
  20. package/dist/index.js +3 -2
  21. package/dist/lab-manager-KUDII6BT.js +280 -0
  22. package/dist/lab-manager-KUDII6BT.js.map +1 -0
  23. package/dist/schema-loader-GGHECMTM.js +9 -0
  24. package/dist/schema-loader-GGHECMTM.js.map +1 -0
  25. package/dist/spectator-manager-E2LH2P54.js +142 -0
  26. package/dist/spectator-manager-E2LH2P54.js.map +1 -0
  27. package/dist/studio-server-NIFKZI4F.js +1738 -0
  28. package/dist/studio-server-NIFKZI4F.js.map +1 -0
  29. package/package.json +8 -6
  30. package/templates/react-basic/AGENTS.md +87 -0
  31. package/templates/react-sync/AGENTS.md +87 -0
  32. package/templates/react-sync/src/App.tsx +8 -0
  33. package/templates/react-sync/src/index.css +11 -0
  34. package/templates/react-tailwind/AGENTS.md +87 -0
  35. package/templates/react-tailwind-sync/AGENTS.md +87 -0
  36. package/templates/react-tailwind-sync/src/App.tsx +28 -0
  37. package/templates/svelte-basic/AGENTS.md +76 -0
  38. package/templates/svelte-basic/src/App.svelte +27 -31
  39. package/templates/svelte-basic/src/Root.svelte +4 -4
  40. package/templates/svelte-basic/src/modules/todos/useTodos.ts +1 -1
  41. package/templates/svelte-basic/vite.config.ts +1 -1
  42. package/templates/svelte-sync/AGENTS.md +76 -0
  43. package/templates/svelte-sync/src/App.svelte +31 -35
  44. package/templates/svelte-sync/src/Root.svelte +6 -6
  45. package/templates/svelte-sync/src/modules/todos/useTodos.ts +1 -1
  46. package/templates/svelte-tailwind/AGENTS.md +76 -0
  47. package/templates/svelte-tailwind/src/App.svelte +17 -21
  48. package/templates/svelte-tailwind/src/Root.svelte +4 -4
  49. package/templates/svelte-tailwind/src/modules/todos/useTodos.ts +1 -1
  50. package/templates/svelte-tailwind/vite.config.ts +2 -2
  51. package/templates/svelte-tailwind-sync/AGENTS.md +76 -0
  52. package/templates/svelte-tailwind-sync/src/App.svelte +52 -56
  53. package/templates/svelte-tailwind-sync/src/Root.svelte +6 -6
  54. package/templates/svelte-tailwind-sync/src/modules/todos/useTodos.ts +1 -1
  55. package/templates/svelte-tailwind-sync/vite.config.ts +1 -1
  56. package/templates/tauri-react/AGENTS.md +91 -0
  57. package/templates/vue-basic/AGENTS.md +84 -0
  58. package/templates/vue-basic/src/modules/todos/useTodos.ts +1 -1
  59. package/templates/vue-sync/AGENTS.md +84 -0
  60. package/templates/vue-sync/src/main.ts +5 -1
  61. package/templates/vue-sync/src/modules/todos/useTodos.ts +1 -1
  62. package/templates/vue-tailwind/AGENTS.md +84 -0
  63. package/templates/vue-tailwind/src/App.vue +1 -8
  64. package/templates/vue-tailwind/src/modules/todos/useTodos.ts +1 -1
  65. package/templates/vue-tailwind-sync/AGENTS.md +84 -0
  66. package/templates/vue-tailwind-sync/src/modules/todos/useTodos.ts +1 -1
  67. package/dist/chunk-VLTPEATY.js.map +0 -1
package/dist/bin.cjs CHANGED
@@ -5,25 +5,2411 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
8
15
  var __copyProps = (to, from, except, desc) => {
9
16
  if (from && typeof from === "object" || typeof from === "function") {
10
17
  for (let key of __getOwnPropNames(from))
11
18
  if (!__hasOwnProp.call(to, key) && key !== except)
12
19
  __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
20
  }
14
- return to;
15
- };
16
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
- // If the importer is in node compatibility mode or this is not an ESM
18
- // file that has been converted to a CommonJS file using a Babel-
19
- // compatible transform (i.e. "__esModule" has not been set), then set
20
- // "default" to the CommonJS "module.exports" for node compatibility.
21
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
- mod
23
- ));
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+
32
+ // src/utils/fs-helpers.ts
33
+ async function directoryExists(path) {
34
+ try {
35
+ await (0, import_promises2.access)(path);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ async function findProjectRoot(startDir) {
42
+ let current = (0, import_node_path2.resolve)(startDir ?? process.cwd());
43
+ for (; ; ) {
44
+ const pkgPath = (0, import_node_path2.join)(current, "package.json");
45
+ try {
46
+ const content = await (0, import_promises2.readFile)(pkgPath, "utf-8");
47
+ const pkg = JSON.parse(content);
48
+ if (isKoraProject(pkg)) {
49
+ return current;
50
+ }
51
+ } catch {
52
+ }
53
+ const parent = (0, import_node_path2.dirname)(current);
54
+ if (parent === current) break;
55
+ current = parent;
56
+ }
57
+ return null;
58
+ }
59
+ async function findSchemaFile(projectRoot) {
60
+ const candidates = [
61
+ (0, import_node_path2.join)(projectRoot, "src", "schema.ts"),
62
+ (0, import_node_path2.join)(projectRoot, "schema.ts"),
63
+ (0, import_node_path2.join)(projectRoot, "src", "schema.js"),
64
+ (0, import_node_path2.join)(projectRoot, "schema.js")
65
+ ];
66
+ for (const candidate of candidates) {
67
+ try {
68
+ await (0, import_promises2.access)(candidate);
69
+ return candidate;
70
+ } catch {
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ async function hasTsxInstalled(projectRoot) {
76
+ try {
77
+ await (0, import_promises2.access)((0, import_node_path2.join)(projectRoot, "node_modules", "tsx", "package.json"));
78
+ return true;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+ async function resolveProjectBinaryEntryPoint(projectRoot, packageName, binaryName) {
84
+ const pkgJsonPath = (0, import_node_path2.join)(projectRoot, "node_modules", packageName, "package.json");
85
+ try {
86
+ const content = await (0, import_promises2.readFile)(pkgJsonPath, "utf-8");
87
+ const pkg = JSON.parse(content);
88
+ let binPath;
89
+ if (typeof pkg.bin === "string") {
90
+ binPath = pkg.bin;
91
+ } else if (typeof pkg.bin === "object" && pkg.bin !== null) {
92
+ binPath = pkg.bin[binaryName];
93
+ }
94
+ if (!binPath) return null;
95
+ const fullPath = (0, import_node_path2.join)(projectRoot, "node_modules", packageName, binPath);
96
+ await (0, import_promises2.access)(fullPath);
97
+ return fullPath;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ function isKoraProject(pkg) {
103
+ if (typeof pkg !== "object" || pkg === null) return false;
104
+ const record = pkg;
105
+ return hasKoraDep(record.dependencies) || hasKoraDep(record.devDependencies);
106
+ }
107
+ function hasKoraDep(deps) {
108
+ if (typeof deps !== "object" || deps === null) return false;
109
+ return Object.keys(deps).some((key) => key === "kora" || key.startsWith("@korajs/"));
110
+ }
111
+ var import_promises2, import_node_path2;
112
+ var init_fs_helpers = __esm({
113
+ "src/utils/fs-helpers.ts"() {
114
+ "use strict";
115
+ import_promises2 = require("fs/promises");
116
+ import_node_path2 = require("path");
117
+ }
118
+ });
119
+
120
+ // src/commands/migrate/schema-loader.ts
121
+ var schema_loader_exports = {};
122
+ __export(schema_loader_exports, {
123
+ loadSchemaDefinition: () => loadSchemaDefinition
124
+ });
125
+ async function loadSchemaDefinition(schemaPath, projectRoot) {
126
+ const ext = (0, import_node_path4.extname)(schemaPath);
127
+ const moduleValue = ext === ".ts" || ext === ".mts" || ext === ".cts" ? await loadTypeScriptModule(schemaPath, projectRoot) : await import(`${(0, import_node_url2.pathToFileURL)(schemaPath).href}?t=${Date.now()}-${Math.random()}`);
128
+ return extractSchema(moduleValue);
129
+ }
130
+ function extractSchema(value) {
131
+ if (typeof value !== "object" || value === null) {
132
+ throw new Error("Schema module must export an object.");
133
+ }
134
+ const moduleRecord = value;
135
+ const candidate = moduleRecord.default ?? moduleRecord;
136
+ if (!isSchemaDefinition(candidate)) {
137
+ throw new Error("Schema module must export a valid SchemaDefinition as default export.");
138
+ }
139
+ return candidate;
140
+ }
141
+ function isSchemaDefinition(value) {
142
+ if (typeof value !== "object" || value === null) return false;
143
+ const object = value;
144
+ return typeof object.version === "number" && typeof object.collections === "object" && object.collections !== null && typeof object.relations === "object" && object.relations !== null;
145
+ }
146
+ async function loadTypeScriptModule(schemaPath, projectRoot) {
147
+ if (!await hasTsxInstalled(projectRoot)) {
148
+ throw new Error(
149
+ `Schema file is TypeScript (${schemaPath}) but local "tsx" was not found. Install tsx in the project.`
150
+ );
151
+ }
152
+ const script = "const modulePath = process.argv[process.argv.length - 1];import('node:url').then(u => import(u.pathToFileURL(modulePath).href)).then(mod => { const v = mod.default ?? mod; process.stdout.write(JSON.stringify(v)) }).catch(e => { process.stderr.write(String(e)); process.exit(1) })";
153
+ const output = await runCommand2(
154
+ process.execPath,
155
+ ["--import", "tsx", "--eval", script, schemaPath],
156
+ projectRoot
157
+ );
158
+ try {
159
+ return JSON.parse(output);
160
+ } catch {
161
+ throw new Error(`Failed to parse schema module output for ${schemaPath}`);
162
+ }
163
+ }
164
+ async function runCommand2(command, args, cwd) {
165
+ return await new Promise((resolve12, reject) => {
166
+ const child = (0, import_node_child_process2.spawn)(command, args, {
167
+ cwd,
168
+ stdio: ["ignore", "pipe", "pipe"],
169
+ env: process.env
170
+ });
171
+ let stdout = "";
172
+ let stderr = "";
173
+ child.stdout?.on("data", (chunk) => {
174
+ stdout += chunk.toString("utf-8");
175
+ });
176
+ child.stderr?.on("data", (chunk) => {
177
+ stderr += chunk.toString("utf-8");
178
+ });
179
+ child.on("error", (error) => {
180
+ reject(error);
181
+ });
182
+ child.on("exit", (code) => {
183
+ if (code === 0) {
184
+ resolve12(stdout.trim());
185
+ return;
186
+ }
187
+ reject(
188
+ new Error(`Failed to load TypeScript schema (exit ${code ?? "unknown"}): ${stderr.trim()}`)
189
+ );
190
+ });
191
+ });
192
+ }
193
+ var import_node_child_process2, import_node_path4, import_node_url2;
194
+ var init_schema_loader = __esm({
195
+ "src/commands/migrate/schema-loader.ts"() {
196
+ "use strict";
197
+ import_node_child_process2 = require("child_process");
198
+ import_node_path4 = require("path");
199
+ import_node_url2 = require("url");
200
+ init_fs_helpers();
201
+ }
202
+ });
203
+
204
+ // src/commands/studio/db-reader.ts
205
+ function parseVersionStamp(raw) {
206
+ if (typeof raw !== "string" || raw.length === 0) {
207
+ return null;
208
+ }
209
+ const parts = raw.split(":");
210
+ if (parts.length < 3) {
211
+ return null;
212
+ }
213
+ const wallTime = Number.parseInt(parts[0] ?? "", 10);
214
+ const logical = Number.parseInt(parts[1] ?? "", 10);
215
+ if (Number.isNaN(wallTime) || Number.isNaN(logical)) {
216
+ return null;
217
+ }
218
+ return { wallTime, logical, nodeId: parts.slice(2).join(":") };
219
+ }
220
+ function parseFieldVersionsColumn(raw) {
221
+ if (typeof raw !== "string" || raw.length === 0) {
222
+ return {};
223
+ }
224
+ try {
225
+ const parsed = JSON.parse(raw);
226
+ const result = {};
227
+ for (const [field, value] of Object.entries(parsed)) {
228
+ const version = parseVersionStamp(value);
229
+ if (version) {
230
+ result[field] = version;
231
+ }
232
+ }
233
+ return result;
234
+ } catch {
235
+ return {};
236
+ }
237
+ }
238
+ function safeIdentifier(name) {
239
+ if (!/^[a-zA-Z0-9_]+$/.test(name)) {
240
+ throw new Error(`Unsafe SQL identifier: "${name}"`);
241
+ }
242
+ return name;
243
+ }
244
+ function parseJsonColumn(raw) {
245
+ if (typeof raw !== "string" || raw.length === 0) {
246
+ return null;
247
+ }
248
+ try {
249
+ return JSON.parse(raw);
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+ function parseJsonArrayColumn(raw) {
255
+ if (typeof raw !== "string" || raw.length === 0) {
256
+ return [];
257
+ }
258
+ try {
259
+ const parsed = JSON.parse(raw);
260
+ return Array.isArray(parsed) ? parsed.map(String) : [];
261
+ } catch {
262
+ return [];
263
+ }
264
+ }
265
+ var KORA_TABLE_PREFIX, OPS_TABLE_PREFIX, META_COLUMNS, StudioDbReader;
266
+ var init_db_reader = __esm({
267
+ "src/commands/studio/db-reader.ts"() {
268
+ "use strict";
269
+ KORA_TABLE_PREFIX = "_kora_";
270
+ OPS_TABLE_PREFIX = "_kora_ops_";
271
+ META_COLUMNS = /* @__PURE__ */ new Set([
272
+ "id",
273
+ "_created_at",
274
+ "_updated_at",
275
+ "_version",
276
+ "_field_versions",
277
+ "_deleted"
278
+ ]);
279
+ StudioDbReader = class _StudioDbReader {
280
+ constructor(db, dbPath) {
281
+ this.db = db;
282
+ this.dbPath = dbPath;
283
+ }
284
+ db;
285
+ dbPath;
286
+ /**
287
+ * Open a Kora database strictly read-only.
288
+ * @throws when the file does not exist or is not a Kora database
289
+ */
290
+ static async open(dbPath) {
291
+ let Database;
292
+ try {
293
+ Database = (await import("better-sqlite3")).default;
294
+ } catch {
295
+ throw new Error(
296
+ 'Kora Studio needs the "better-sqlite3" package to open database files. Install it in your project: pnpm add -D better-sqlite3'
297
+ );
298
+ }
299
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
300
+ const reader = new _StudioDbReader(db, dbPath);
301
+ if (reader.listCollections().length === 0 && !reader.hasKoraTables()) {
302
+ db.close();
303
+ throw new Error(`"${dbPath}" does not look like a Kora database (no _kora_* tables found).`);
304
+ }
305
+ return reader;
306
+ }
307
+ close() {
308
+ this.db.close();
309
+ }
310
+ hasKoraTables() {
311
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name LIKE ?").get(`${KORA_TABLE_PREFIX}%`);
312
+ return row.count > 0;
313
+ }
314
+ /** Collections = tables that have a companion _kora_ops_<name> table. */
315
+ listCollections() {
316
+ const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all();
317
+ const names = new Set(tables.map((t2) => t2.name));
318
+ const collections = [];
319
+ for (const { name } of tables) {
320
+ if (name.startsWith(OPS_TABLE_PREFIX)) {
321
+ const collection = name.slice(OPS_TABLE_PREFIX.length);
322
+ if (names.has(collection)) {
323
+ collections.push(collection);
324
+ }
325
+ }
326
+ }
327
+ return collections.sort();
328
+ }
329
+ overview() {
330
+ const collections = this.listCollections().map((name) => {
331
+ const table = safeIdentifier(name);
332
+ const live = this.db.prepare(`SELECT COUNT(*) as count FROM ${table} WHERE _deleted = 0`).get();
333
+ const tombstones = this.db.prepare(`SELECT COUNT(*) as count FROM ${table} WHERE _deleted = 1`).get();
334
+ const operations = this.db.prepare(`SELECT COUNT(*) as count FROM ${OPS_TABLE_PREFIX}${table}`).get();
335
+ const columnRows = this.db.prepare(`PRAGMA table_info(${table})`).all();
336
+ return {
337
+ name,
338
+ liveRecords: live.count,
339
+ tombstones: tombstones.count,
340
+ operations: operations.count,
341
+ columns: columnRows.map((c) => c.name).filter((c) => !META_COLUMNS.has(c))
342
+ };
343
+ });
344
+ const versionVector = this.tableExists("_kora_version_vector") ? this.db.prepare(
345
+ "SELECT node_id as nodeId, sequence_number as sequenceNumber FROM _kora_version_vector ORDER BY node_id"
346
+ ).all() : [];
347
+ const meta = this.tableExists("_kora_meta") ? this.db.prepare("SELECT key, value FROM _kora_meta ORDER BY key").all() : [];
348
+ const pendingSyncOps = this.tableExists("_kora_sync_queue") ? this.db.prepare("SELECT COUNT(*) as count FROM _kora_sync_queue").get().count : 0;
349
+ const auditTraces = this.tableExists("_kora_audit_traces") ? this.db.prepare("SELECT COUNT(*) as count FROM _kora_audit_traces").get().count : 0;
350
+ return { dbPath: this.dbPath, collections, versionVector, meta, pendingSyncOps, auditTraces };
351
+ }
352
+ records(collection, options = {}) {
353
+ const table = safeIdentifier(collection);
354
+ const limit = Math.min(Math.max(options.limit ?? 50, 1), 500);
355
+ const offset = Math.max(options.offset ?? 0, 0);
356
+ const clauses = [];
357
+ const params = [];
358
+ if (!options.includeDeleted) {
359
+ clauses.push("_deleted = 0");
360
+ }
361
+ if (options.search && options.search.trim().length > 0) {
362
+ const textColumns = this.db.prepare(`PRAGMA table_info(${table})`).all().filter((c) => c.type.toUpperCase().includes("TEXT") || c.name === "id");
363
+ const term = `%${options.search.trim()}%`;
364
+ const likes = textColumns.map((c) => {
365
+ params.push(term);
366
+ return `${safeIdentifier(c.name)} LIKE ?`;
367
+ });
368
+ if (likes.length > 0) {
369
+ clauses.push(`(${likes.join(" OR ")})`);
370
+ }
371
+ }
372
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
373
+ const total = this.db.prepare(`SELECT COUNT(*) as count FROM ${table} ${where}`).get(...params).count;
374
+ const rows = this.db.prepare(`SELECT * FROM ${table} ${where} ORDER BY _updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
375
+ return { records: rows.map((row) => this.toStudioRecord(row)), total };
376
+ }
377
+ /** Full operation log for one collection (capped), for replay and the DAG. */
378
+ allOperations(collection, cap = 5e3) {
379
+ const table = safeIdentifier(collection);
380
+ const rows = this.db.prepare(`SELECT * FROM ${OPS_TABLE_PREFIX}${table} ORDER BY timestamp ASC LIMIT ?`).all(Math.min(Math.max(cap, 1), 2e4));
381
+ return rows.map((row) => this.toStudioOperation(row));
382
+ }
383
+ /**
384
+ * Cheap change fingerprint for live updates. SQLite's `data_version` PRAGMA
385
+ * increments whenever ANOTHER connection commits — exactly what a read-only
386
+ * watcher needs to know "something changed, refetch".
387
+ */
388
+ fingerprint() {
389
+ const row = this.db.prepare("PRAGMA data_version").get();
390
+ return String(row.data_version);
391
+ }
392
+ /** Raw value of a single column on a record (for richtext preview decode). */
393
+ rawFieldValue(collection, recordId, field) {
394
+ const table = safeIdentifier(collection);
395
+ const column = safeIdentifier(field);
396
+ const row = this.db.prepare(`SELECT ${column} as value FROM ${table} WHERE id = ?`).get(recordId);
397
+ return row?.value;
398
+ }
399
+ record(collection, recordId) {
400
+ const table = safeIdentifier(collection);
401
+ const row = this.db.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(recordId);
402
+ return row ? this.toStudioRecord(row) : null;
403
+ }
404
+ /** Full operation history for one record, newest first. */
405
+ recordOperations(collection, recordId) {
406
+ const table = safeIdentifier(collection);
407
+ const rows = this.db.prepare(
408
+ `SELECT * FROM ${OPS_TABLE_PREFIX}${table} WHERE record_id = ? ORDER BY timestamp DESC`
409
+ ).all(recordId);
410
+ return rows.map((row) => this.toStudioOperation(row));
411
+ }
412
+ operations(collection, options = {}) {
413
+ const table = safeIdentifier(collection);
414
+ const limit = Math.min(Math.max(options.limit ?? 50, 1), 500);
415
+ const offset = Math.max(options.offset ?? 0, 0);
416
+ const total = this.db.prepare(`SELECT COUNT(*) as count FROM ${OPS_TABLE_PREFIX}${table}`).get().count;
417
+ const rows = this.db.prepare(`SELECT * FROM ${OPS_TABLE_PREFIX}${table} ORDER BY timestamp DESC LIMIT ? OFFSET ?`).all(limit, offset);
418
+ return { operations: rows.map((row) => this.toStudioOperation(row)), total };
419
+ }
420
+ auditTraces(limit = 100) {
421
+ if (!this.tableExists("_kora_audit_traces")) {
422
+ return [];
423
+ }
424
+ const rows = this.db.prepare(
425
+ `SELECT id, recorded_at, event_type, collection, record_id, field, strategy, tier, constraint_name
426
+ FROM _kora_audit_traces ORDER BY recorded_at DESC LIMIT ?`
427
+ ).all(Math.min(Math.max(limit, 1), 500));
428
+ return rows.map((row) => ({
429
+ id: String(row.id),
430
+ recordedAt: Number(row.recorded_at),
431
+ eventType: String(row.event_type),
432
+ collection: String(row.collection),
433
+ recordId: String(row.record_id),
434
+ field: String(row.field),
435
+ strategy: String(row.strategy),
436
+ tier: Number(row.tier),
437
+ constraintName: row.constraint_name === null ? null : String(row.constraint_name)
438
+ }));
439
+ }
440
+ tableExists(name) {
441
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name = ?").get(name);
442
+ return row.count > 0;
443
+ }
444
+ toStudioRecord(row) {
445
+ const fields = {};
446
+ for (const [key, value] of Object.entries(row)) {
447
+ if (!META_COLUMNS.has(key)) {
448
+ fields[key] = value instanceof Uint8Array || Buffer.isBuffer(value) ? `<binary ${value.byteLength} bytes>` : value;
449
+ }
450
+ }
451
+ return {
452
+ id: String(row.id),
453
+ fields,
454
+ createdAt: Number(row._created_at),
455
+ updatedAt: Number(row._updated_at),
456
+ deleted: row._deleted === 1,
457
+ version: parseVersionStamp(row._version),
458
+ fieldVersions: parseFieldVersionsColumn(row._field_versions)
459
+ };
460
+ }
461
+ toStudioOperation(row) {
462
+ return {
463
+ id: String(row.id),
464
+ nodeId: String(row.node_id),
465
+ type: String(row.type),
466
+ recordId: String(row.record_id),
467
+ data: parseJsonColumn(row.data),
468
+ previousData: parseJsonColumn(row.previous_data),
469
+ timestamp: parseVersionStamp(row.timestamp),
470
+ sequenceNumber: Number(row.sequence_number),
471
+ causalDeps: parseJsonArrayColumn(row.causal_deps),
472
+ schemaVersion: Number(row.schema_version)
473
+ };
474
+ }
475
+ };
476
+ }
477
+ });
478
+
479
+ // src/commands/studio/studio-replay.ts
480
+ function compareVersion(a, b) {
481
+ if (!a || !b) {
482
+ return a === b ? 0 : a ? 1 : -1;
483
+ }
484
+ if (a.wallTime !== b.wallTime) {
485
+ return a.wallTime - b.wallTime;
486
+ }
487
+ if (a.logical !== b.logical) {
488
+ return a.logical - b.logical;
489
+ }
490
+ return a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : 0;
491
+ }
492
+ function sortByHlc(ops) {
493
+ return [...ops].sort((a, b) => compareVersion(a.timestamp, b.timestamp));
494
+ }
495
+ function replayToOperation(ops, upToOpId) {
496
+ const ordered = sortByHlc(ops);
497
+ let cutIndex = ordered.length - 1;
498
+ if (upToOpId !== null) {
499
+ const found = ordered.findIndex((o) => o.id === upToOpId);
500
+ if (found === -1) {
501
+ throw new Error(`Operation "${upToOpId}" not found in the log`);
502
+ }
503
+ cutIndex = found;
504
+ }
505
+ const records = /* @__PURE__ */ new Map();
506
+ for (let i = 0; i <= cutIndex; i++) {
507
+ const operation = ordered[i];
508
+ if (!operation) {
509
+ continue;
510
+ }
511
+ const writer = operation.timestamp ? shortNode(operation.timestamp.nodeId) : "?";
512
+ if (operation.type === "insert" && operation.data) {
513
+ const writers = {};
514
+ for (const field of Object.keys(operation.data)) {
515
+ writers[field] = writer;
516
+ }
517
+ records.set(operation.recordId, {
518
+ fields: { ...operation.data },
519
+ deleted: false,
520
+ writers
521
+ });
522
+ } else if (operation.type === "update" && operation.data) {
523
+ const existing = records.get(operation.recordId);
524
+ if (existing) {
525
+ for (const [field, value] of Object.entries(operation.data)) {
526
+ existing.fields[field] = value;
527
+ existing.writers[field] = writer;
528
+ }
529
+ } else {
530
+ const writers = {};
531
+ for (const field of Object.keys(operation.data)) {
532
+ writers[field] = writer;
533
+ }
534
+ records.set(operation.recordId, {
535
+ fields: { ...operation.data },
536
+ deleted: false,
537
+ writers
538
+ });
539
+ }
540
+ } else if (operation.type === "delete") {
541
+ const existing = records.get(operation.recordId);
542
+ if (existing) {
543
+ existing.deleted = true;
544
+ } else {
545
+ records.set(operation.recordId, { fields: {}, deleted: true, writers: {} });
546
+ }
547
+ }
548
+ }
549
+ return {
550
+ records: [...records.entries()].map(([id, r]) => ({
551
+ id,
552
+ fields: r.fields,
553
+ deleted: r.deleted,
554
+ lastWriterByField: r.writers
555
+ })),
556
+ appliedCount: cutIndex + 1,
557
+ totalCount: ordered.length,
558
+ cutOperation: ordered[cutIndex] ?? null
559
+ };
560
+ }
561
+ function buildCausalDag(ops) {
562
+ const ordered = sortByHlc(ops);
563
+ const laneByNode = /* @__PURE__ */ new Map();
564
+ const lanes = [];
565
+ for (const operation of ordered) {
566
+ if (!laneByNode.has(operation.nodeId)) {
567
+ laneByNode.set(operation.nodeId, lanes.length);
568
+ lanes.push({ nodeId: operation.nodeId, shortNodeId: shortNode(operation.nodeId) });
569
+ }
570
+ }
571
+ const idSet = new Set(ordered.map((o) => o.id));
572
+ const nodes = ordered.map((operation, index) => ({
573
+ id: operation.id,
574
+ type: operation.type,
575
+ recordId: operation.recordId,
576
+ nodeId: operation.nodeId,
577
+ shortNodeId: shortNode(operation.nodeId),
578
+ sequenceNumber: operation.sequenceNumber,
579
+ wallTime: operation.timestamp?.wallTime ?? 0,
580
+ x: index,
581
+ lane: laneByNode.get(operation.nodeId) ?? 0,
582
+ dataPreview: operation.data ? JSON.stringify(operation.data).slice(0, 80) : ""
583
+ }));
584
+ const edges = [];
585
+ for (const operation of ordered) {
586
+ for (const dep of operation.causalDeps) {
587
+ if (idSet.has(dep)) {
588
+ edges.push({ from: dep, to: operation.id });
589
+ }
590
+ }
591
+ }
592
+ return { nodes, edges, lanes };
593
+ }
594
+ function shortNode(nodeId) {
595
+ return nodeId.length > 8 ? nodeId.slice(-8) : nodeId;
596
+ }
597
+ var init_studio_replay = __esm({
598
+ "src/commands/studio/studio-replay.ts"() {
599
+ "use strict";
600
+ }
601
+ });
602
+
603
+ // src/commands/studio/studio-ui.ts
604
+ var STUDIO_HTML;
605
+ var init_studio_ui = __esm({
606
+ "src/commands/studio/studio-ui.ts"() {
607
+ "use strict";
608
+ STUDIO_HTML = `<!DOCTYPE html>
609
+ <html lang="en">
610
+ <head>
611
+ <meta charset="utf-8" />
612
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
613
+ <title>Kora Studio</title>
614
+ <link rel="stylesheet" href="/style.css" />
615
+ </head>
616
+ <body>
617
+ <header>
618
+ <div class="logo">kora <span>studio</span></div>
619
+ <div class="modebadge" id="modebadge"></div>
620
+ <div class="dbpath mono" id="dbpath"></div>
621
+ <div class="live" id="live"><div class="dot"></div><span>live</span></div>
622
+ <button id="refresh">Refresh</button>
623
+ </header>
624
+ <div class="tabs" id="tabs"></div>
625
+ <div class="layout">
626
+ <nav class="sidebar" id="sidebar"></nav>
627
+ <main id="main"><div class="empty">Loading\u2026</div></main>
628
+ </div>
629
+ <div class="drawer" id="drawer"></div>
630
+ <div class="dag-tip" id="dagtip"></div>
631
+ <script src="/app.js"></script>
632
+ </body>
633
+ </html>
634
+ `;
635
+ }
636
+ });
637
+
638
+ // src/commands/studio/studio-ui-app.ts
639
+ var STUDIO_APP_JS;
640
+ var init_studio_ui_app = __esm({
641
+ "src/commands/studio/studio-ui-app.ts"() {
642
+ "use strict";
643
+ STUDIO_APP_JS = `
644
+ (function () {
645
+ 'use strict';
646
+
647
+ // \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
648
+ var state = {
649
+ mode: 'file', // 'file' | 'lab'
650
+ tab: 'data', // data | ops | timetravel | merges | sync | lab
651
+ device: null, // selected lab device for data-ish tabs
652
+ devices: [],
653
+ collection: null,
654
+ page: 0,
655
+ includeDeleted: false,
656
+ search: '',
657
+ overview: null,
658
+ ttPosition: null, // time-travel slider position
659
+ ttPlaying: null,
660
+ labState: null,
661
+ feed: [],
662
+ deviceColors: {},
663
+ live: false
664
+ };
665
+ var PAGE = 50;
666
+ var refreshTimer = null;
667
+
668
+ // \u2500\u2500 Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
669
+ function qs(sel) { return document.querySelector(sel); }
670
+ function api(path) {
671
+ var sep = path.indexOf('?') === -1 ? '?' : '&';
672
+ var url = state.device ? path + sep + 'device=' + encodeURIComponent(state.device) : path;
673
+ return fetch(url).then(function (r) {
674
+ if (!r.ok) { return r.json().then(function (b) { throw new Error(b.error || r.statusText); }); }
675
+ return r.json();
676
+ });
677
+ }
678
+ function post(path, body) {
679
+ return fetch(path, {
680
+ method: 'POST',
681
+ headers: { 'content-type': 'application/json' },
682
+ body: JSON.stringify(body || {})
683
+ }).then(function (r) {
684
+ if (!r.ok) { return r.json().then(function (b) { throw new Error(b.error || r.statusText); }); }
685
+ return r.json();
686
+ });
687
+ }
688
+ function el(tag, attrs, children) {
689
+ var node = document.createElement(tag);
690
+ if (attrs) { Object.keys(attrs).forEach(function (k) {
691
+ if (k === 'class') node.className = attrs[k];
692
+ else if (k === 'text') node.textContent = attrs[k];
693
+ else if (k === 'html') node.innerHTML = attrs[k];
694
+ else if (k.slice(0, 2) === 'on') node.addEventListener(k.slice(2), attrs[k]);
695
+ else node.setAttribute(k, attrs[k]);
696
+ }); }
697
+ (children || []).forEach(function (c) { if (c) node.appendChild(c); });
698
+ return node;
699
+ }
700
+ function fmtTime(ms) {
701
+ if (!ms) return '\xB7';
702
+ var d = new Date(ms);
703
+ return d.toLocaleTimeString() + '.' + ('00' + d.getMilliseconds()).slice(-3);
704
+ }
705
+ function fmtDateTime(ms) {
706
+ if (!ms) return '\xB7';
707
+ var d = new Date(ms);
708
+ return d.toLocaleDateString() + ' ' + d.toLocaleTimeString();
709
+ }
710
+ function shortNode(id) { return id && id.length > 8 ? id.slice(-8) : (id || '\xB7'); }
711
+ function shortId(id) { return id && id.length > 13 ? id.slice(0, 13) + '\u2026' : (id || ''); }
712
+ function fmtVal(v) {
713
+ if (v === null || v === undefined) return 'null';
714
+ if (typeof v === 'object') return JSON.stringify(v);
715
+ return String(v);
716
+ }
717
+ function deviceColor(name) {
718
+ if (!(name in state.deviceColors)) {
719
+ state.deviceColors[name] = 'dev-c' + (Object.keys(state.deviceColors).length % 6);
720
+ }
721
+ return state.deviceColors[name];
722
+ }
723
+ function debounceRefresh() {
724
+ if (refreshTimer) clearTimeout(refreshTimer);
725
+ refreshTimer = setTimeout(function () { render(false); }, 250);
726
+ }
727
+
728
+ // \u2500\u2500 Boot + live events \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
729
+ fetch('/api/mode').then(function (r) { return r.json(); }).then(function (info) {
730
+ state.mode = info.mode;
731
+ state.devices = info.devices || [];
732
+ if (state.mode === 'lab') { state.tab = 'lab'; state.device = state.devices[0] || null; }
733
+ qs('#modebadge').textContent = state.mode;
734
+ if (info.spectator) {
735
+ qs('#dbpath').textContent = '\u21C4 ' + info.spectator.url +
736
+ (info.spectator.connected ? ' \xB7 connected' : ' \xB7 connecting\u2026') +
737
+ ' \xB7 ' + info.spectator.operationsReceived + ' ops received';
738
+ }
739
+ connectEvents();
740
+ render(true);
741
+ });
742
+
743
+ function connectEvents() {
744
+ var source = new EventSource('/api/events');
745
+ source.addEventListener('hello', function () {
746
+ state.live = true; qs('#live').className = 'live on';
747
+ });
748
+ source.addEventListener('change', function () {
749
+ if (state.tab !== 'timetravel') debounceRefresh();
750
+ });
751
+ source.addEventListener('lab', function (event) {
752
+ var data = JSON.parse(event.data);
753
+ state.feed.push(data);
754
+ if (state.feed.length > 400) state.feed.shift();
755
+ appendFeedLine(data);
756
+ });
757
+ source.addEventListener('spectator', function (event) {
758
+ var data = JSON.parse(event.data);
759
+ data.device = 'server';
760
+ state.feed.push(data);
761
+ if (state.feed.length > 400) state.feed.shift();
762
+ appendFeedLine(data);
763
+ // Keep the header status fresh as ops stream in.
764
+ fetch('/api/spectator/status').then(function (r) { return r.json(); }).then(function (s) {
765
+ qs('#dbpath').textContent = '\u21C4 ' + s.url +
766
+ (s.connected ? ' \xB7 connected' : ' \xB7 reconnecting\u2026') +
767
+ ' \xB7 ' + s.operationsReceived + ' ops received';
768
+ }).catch(function () {});
769
+ });
770
+ source.onerror = function () {
771
+ state.live = false; qs('#live').className = 'live';
772
+ };
773
+ }
774
+
775
+ // \u2500\u2500 Tabs + chrome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
776
+ function renderTabs() {
777
+ var tabs = qs('#tabs');
778
+ tabs.textContent = '';
779
+ var defs = [];
780
+ if (state.mode === 'lab') defs.push(['lab', 'Lab']);
781
+ defs.push(['data', 'Data'], ['ops', 'Operations'], ['timetravel', 'Time Travel'], ['merges', 'Merges'], ['sync', 'Sync']);
782
+ defs.forEach(function (d) {
783
+ tabs.appendChild(el('div', {
784
+ class: 'tab' + (state.tab === d[0] ? ' active' : ''),
785
+ text: d[1],
786
+ onclick: function () { state.tab = d[0]; state.page = 0; closeDrawer(); render(true); }
787
+ }));
788
+ });
789
+ tabs.appendChild(el('div', { class: 'spacer' }));
790
+ if (state.mode === 'lab' && state.tab !== 'lab') {
791
+ var pick = el('select', { onchange: function () { state.device = pick.value; state.page = 0; render(true); } },
792
+ state.devices.map(function (d) {
793
+ var o = el('option', { value: d, text: d });
794
+ if (d === state.device) o.selected = true;
795
+ return o;
796
+ }));
797
+ tabs.appendChild(el('div', { class: 'devicepick' }, [el('span', { text: 'device:' }), pick]));
798
+ }
799
+ }
800
+
801
+ function renderSidebar() {
802
+ var nav = qs('#sidebar');
803
+ nav.textContent = '';
804
+ if (state.tab === 'lab' || state.tab === 'sync' || state.tab === 'merges') {
805
+ nav.style.display = 'none';
806
+ return;
807
+ }
808
+ nav.style.display = '';
809
+ nav.appendChild(el('h3', { text: 'Collections' }));
810
+ (state.overview ? state.overview.collections : []).forEach(function (c) {
811
+ nav.appendChild(el('div', {
812
+ class: 'item' + (state.collection === c.name ? ' active' : ''),
813
+ onclick: function () { state.collection = c.name; state.page = 0; closeDrawer(); render(false); }
814
+ }, [
815
+ el('span', { text: c.name }),
816
+ el('span', { class: 'count', text: String(c.liveRecords) + (c.tombstones ? ' +' + c.tombstones + '\u2020' : '') })
817
+ ]));
818
+ });
819
+ }
820
+
821
+ // \u2500\u2500 Main render \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
822
+ function render(full) {
823
+ renderTabs();
824
+ if (state.tab === 'lab') { renderSidebar(); renderLab(full); return; }
825
+ api('/api/overview').then(function (overview) {
826
+ state.overview = overview;
827
+ qs('#dbpath').textContent = overview.dbPath || '';
828
+ if (!state.collection || !overview.collections.some(function (c) { return c.name === state.collection; })) {
829
+ state.collection = overview.collections.length ? overview.collections[0].name : null;
830
+ }
831
+ renderSidebar();
832
+ var main = qs('#main');
833
+ main.textContent = '';
834
+ if (state.tab === 'data') return renderData(main);
835
+ if (state.tab === 'ops') return renderOps(main);
836
+ if (state.tab === 'timetravel') return renderTimeTravel(main);
837
+ if (state.tab === 'merges') return renderMerges(main);
838
+ if (state.tab === 'sync') return renderSync(main);
839
+ }).catch(function (e) {
840
+ qs('#main').innerHTML = '<div class="error">' + e.message + '</div>';
841
+ });
842
+ }
843
+
844
+ function toolbar(main, title, extra) {
845
+ main.appendChild(el('div', { class: 'toolbar' }, [el('h2', { text: title })].concat(extra || [])));
846
+ }
847
+ function pager(main, total) {
848
+ var pages = Math.max(1, Math.ceil(total / PAGE));
849
+ main.appendChild(el('div', { class: 'pager' }, [
850
+ el('button', { text: '\u2039 Prev', onclick: function () { if (state.page > 0) { state.page--; render(false); } } }),
851
+ el('span', { text: 'page ' + (state.page + 1) + ' / ' + pages + ' \xB7 ' + total + ' total' }),
852
+ el('button', { text: 'Next \u203A', onclick: function () { if (state.page < pages - 1) { state.page++; render(false); } } })
853
+ ]));
854
+ }
855
+
856
+ // \u2500\u2500 Data tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
857
+ function renderData(main) {
858
+ if (!state.collection) { main.appendChild(el('div', { class: 'empty', text: 'No collections.' })); return; }
859
+ var searchBox = el('input', { type: 'text', placeholder: 'search\u2026', value: state.search });
860
+ searchBox.oninput = function () { state.search = searchBox.value; state.page = 0; debounceRefresh(); };
861
+ var deletedToggle = el('label', {}, [
862
+ (function () { var cb = el('input', { type: 'checkbox' }); cb.checked = state.includeDeleted;
863
+ cb.onchange = function () { state.includeDeleted = cb.checked; state.page = 0; render(false); }; return cb; })(),
864
+ el('span', { text: 'tombstones' })
865
+ ]);
866
+ toolbar(main, state.collection, [searchBox, deletedToggle]);
867
+
868
+ var q = '/api/collections/' + state.collection + '/records?limit=' + PAGE +
869
+ '&offset=' + (state.page * PAGE) + '&includeDeleted=' + state.includeDeleted +
870
+ (state.search ? '&search=' + encodeURIComponent(state.search) : '');
871
+ api(q).then(function (data) {
872
+ var info = state.overview.collections.filter(function (c) { return c.name === state.collection; })[0];
873
+ var columns = info ? info.columns : [];
874
+ if (!data.records.length) { main.appendChild(el('div', { class: 'empty', text: 'No records.' })); return; }
875
+ var thead = el('tr', {}, [el('th', { text: 'id' })].concat(
876
+ columns.map(function (c) { return el('th', { text: c }); }),
877
+ [el('th', { text: 'updated' })]
878
+ ));
879
+ var tbody = el('tbody', {}, data.records.map(function (r) {
880
+ return el('tr', { class: 'clickable' + (r.deleted ? ' tombstone' : ''), onclick: function () { openRecord(r.id); } },
881
+ [el('td', { class: 'mono', text: shortId(r.id) })].concat(
882
+ columns.map(function (c) { return el('td', { text: fmtVal(r.fields[c]).slice(0, 60) }); }),
883
+ [el('td', { class: 'mono', text: fmtDateTime(r.updatedAt) })]
884
+ ));
885
+ }));
886
+ main.appendChild(el('table', { class: 'grid' }, [el('thead', {}, [thead]), tbody]));
887
+ pager(main, data.total);
888
+ }).catch(showError(main));
889
+ }
890
+
891
+ // \u2500\u2500 Record drawer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
892
+ function openRecord(id) {
893
+ api('/api/collections/' + state.collection + '/records/' + encodeURIComponent(id)).then(function (data) {
894
+ var drawer = qs('#drawer');
895
+ drawer.textContent = '';
896
+ var r = data.record;
897
+ drawer.appendChild(el('h3', {}, [
898
+ el('span', { class: 'mono', text: r.id }),
899
+ el('span', { class: 'close', text: '\u2715', onclick: closeDrawer })
900
+ ]));
901
+ if (r.deleted) drawer.appendChild(el('span', { class: 'badge delete', text: 'tombstone' }));
902
+
903
+ drawer.appendChild(el('h4', { text: 'Fields \u2014 with last writer per field' }));
904
+ Object.keys(r.fields).forEach(function (f) {
905
+ var fv = r.fieldVersions[f];
906
+ var val = fmtVal(r.fields[f]);
907
+ var preview = data.richtextPreviews && data.richtextPreviews[f];
908
+ drawer.appendChild(el('div', { class: 'fieldrow' }, [
909
+ el('div', { class: 'fname mono', text: f }),
910
+ el('div', { class: 'fval', text: val }),
911
+ preview ? el('div', { class: 'preview', text: '\u201C' + preview + '\u201D' }) : null,
912
+ fv ? el('span', { class: 'chip' }, [
913
+ el('span', { text: 'last writer' }),
914
+ el('b', { class: 'mono', text: shortNode(fv.nodeId) }),
915
+ el('span', { class: 'mono', text: fmtDateTime(fv.wallTime) })
916
+ ]) : el('span', { class: 'chip', text: 'no field version' })
917
+ ]));
918
+ });
919
+
920
+ drawer.appendChild(el('h4', { text: 'Causal graph for this record' }));
921
+ var dagBox = el('div', { class: 'dagwrap' });
922
+ drawer.appendChild(dagBox);
923
+ api('/api/collections/' + state.collection + '/dag?record=' + encodeURIComponent(id)).then(function (dag) {
924
+ renderDag(dagBox, dag, 500);
925
+ });
926
+
927
+ drawer.appendChild(el('h4', { text: 'Operation history (newest first)' }));
928
+ if (!data.operations.length) drawer.appendChild(el('div', { class: 'empty', text: 'No operations (compacted?).' }));
929
+ data.operations.forEach(function (o) {
930
+ var body = el('div', { class: 'op ' + o.type }, [
931
+ el('div', {}, [
932
+ el('span', { class: 'badge ' + o.type, text: o.type }),
933
+ el('span', { class: 'meta', text: ' ' + shortNode(o.nodeId) + ' \xB7 seq ' + o.sequenceNumber + ' \xB7 ' + (o.timestamp ? fmtDateTime(o.timestamp.wallTime) : '\xB7') })
934
+ ])
935
+ ]);
936
+ if (o.data) {
937
+ var pre = el('pre', { class: 'mono' });
938
+ pre.textContent = JSON.stringify(o.data, null, 1);
939
+ body.appendChild(pre);
940
+ }
941
+ drawer.appendChild(body);
942
+ });
943
+ drawer.className = 'drawer open';
944
+ }).catch(function (e) { alertError(e); });
945
+ }
946
+ function closeDrawer() { qs('#drawer').className = 'drawer'; }
947
+
948
+ // \u2500\u2500 Operations tab (DAG + table) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
949
+ function renderOps(main) {
950
+ if (!state.collection) { main.appendChild(el('div', { class: 'empty', text: 'No collections.' })); return; }
951
+ toolbar(main, state.collection + ' \u2014 operation log');
952
+ var dagBox = el('div', { class: 'dagwrap' });
953
+ main.appendChild(dagBox);
954
+ api('/api/collections/' + state.collection + '/dag?limit=120').then(function (dag) {
955
+ renderDag(dagBox, dag, null);
956
+ });
957
+
958
+ api('/api/collections/' + state.collection + '/ops?limit=' + PAGE + '&offset=' + (state.page * PAGE)).then(function (data) {
959
+ if (!data.operations.length) { main.appendChild(el('div', { class: 'empty', text: 'Empty (compacted?).' })); return; }
960
+ var thead = el('tr', {}, ['type', 'record', 'node', 'seq', 'time', 'data'].map(function (h) { return el('th', { text: h }); }));
961
+ var tbody = el('tbody', {}, data.operations.map(function (o) {
962
+ return el('tr', { class: 'clickable', onclick: function () { openRecord(o.recordId); } }, [
963
+ el('td', {}, [el('span', { class: 'badge ' + o.type, text: o.type })]),
964
+ el('td', { class: 'mono', text: shortId(o.recordId) }),
965
+ el('td', { class: 'mono', text: shortNode(o.nodeId) }),
966
+ el('td', { class: 'mono', text: String(o.sequenceNumber) }),
967
+ el('td', { class: 'mono', text: o.timestamp ? fmtDateTime(o.timestamp.wallTime) : '\xB7' }),
968
+ el('td', { class: 'mono', text: o.data ? JSON.stringify(o.data).slice(0, 70) : '\xB7' })
969
+ ]);
970
+ }));
971
+ main.appendChild(el('table', { class: 'grid' }, [el('thead', {}, [thead]), tbody]));
972
+ pager(main, data.total);
973
+ }).catch(showError(main));
974
+ }
975
+
976
+ // \u2500\u2500 Causal DAG renderer (SVG) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
977
+ function renderDag(container, dag, maxWidth) {
978
+ container.textContent = '';
979
+ if (!dag.nodes.length) { container.appendChild(el('div', { class: 'empty', text: 'No operations to graph.' })); return; }
980
+ var XSTEP = 46, YSTEP = 44, PADX = 90, PADY = 26, R = 7;
981
+ var width = PADX + dag.nodes.length * XSTEP + 30;
982
+ var height = PADY + dag.lanes.length * YSTEP + 16;
983
+ var svgns = 'http://www.w3.org/2000/svg';
984
+ var svg = document.createElementNS(svgns, 'svg');
985
+ svg.setAttribute('width', String(width));
986
+ svg.setAttribute('height', String(height));
987
+
988
+ var defs = document.createElementNS(svgns, 'defs');
989
+ defs.innerHTML = '<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#3a4356"/></marker>';
990
+ svg.appendChild(defs);
991
+
992
+ var colors = { insert: '#48bb78', update: '#4299e1', delete: '#f56565' };
993
+ var pos = {};
994
+ dag.nodes.forEach(function (n) {
995
+ pos[n.id] = { x: PADX + n.x * XSTEP, y: PADY + n.lane * YSTEP + YSTEP / 2 };
996
+ });
997
+
998
+ dag.lanes.forEach(function (lane, i) {
999
+ var y = PADY + i * YSTEP + YSTEP / 2;
1000
+ var line = document.createElementNS(svgns, 'line');
1001
+ line.setAttribute('x1', '10'); line.setAttribute('x2', String(width - 10));
1002
+ line.setAttribute('y1', String(y)); line.setAttribute('y2', String(y));
1003
+ line.setAttribute('class', 'dag-lane-line');
1004
+ svg.appendChild(line);
1005
+ var label = document.createElementNS(svgns, 'text');
1006
+ label.setAttribute('x', '10'); label.setAttribute('y', String(y - 8));
1007
+ label.setAttribute('class', 'dag-lane-label');
1008
+ label.textContent = lane.shortNodeId;
1009
+ svg.appendChild(label);
1010
+ });
1011
+
1012
+ dag.edges.forEach(function (e) {
1013
+ var a = pos[e.from], b = pos[e.to];
1014
+ if (!a || !b) return;
1015
+ var path = document.createElementNS(svgns, 'path');
1016
+ var midX = (a.x + b.x) / 2;
1017
+ path.setAttribute('d', 'M ' + a.x + ' ' + a.y + ' C ' + midX + ' ' + a.y + ', ' + midX + ' ' + b.y + ', ' + (b.x - R - 2) + ' ' + b.y);
1018
+ path.setAttribute('class', 'dag-edge');
1019
+ svg.appendChild(path);
1020
+ });
1021
+
1022
+ var tip = qs('#dagtip');
1023
+ dag.nodes.forEach(function (n) {
1024
+ var g = document.createElementNS(svgns, 'g');
1025
+ g.setAttribute('class', 'dag-node');
1026
+ var c = document.createElementNS(svgns, 'circle');
1027
+ var p = pos[n.id];
1028
+ c.setAttribute('cx', String(p.x)); c.setAttribute('cy', String(p.y)); c.setAttribute('r', String(R));
1029
+ c.setAttribute('fill', '#161a22');
1030
+ c.setAttribute('stroke', colors[n.type] || '#8b93a7');
1031
+ g.appendChild(c);
1032
+ g.addEventListener('mousemove', function (ev) {
1033
+ tip.style.display = 'block';
1034
+ tip.style.left = (ev.clientX + 14) + 'px';
1035
+ tip.style.top = (ev.clientY + 14) + 'px';
1036
+ tip.innerHTML = '<b>' + n.type + '</b> by ' + n.shortNodeId + ' \xB7 seq ' + n.sequenceNumber +
1037
+ '<br/>record ' + shortId(n.recordId) +
1038
+ (n.dataPreview ? '<br/><span class="mono">' + escapeHtml(n.dataPreview) + '</span>' : '');
1039
+ });
1040
+ g.addEventListener('mouseleave', function () { tip.style.display = 'none'; });
1041
+ g.addEventListener('click', function () { openRecord(n.recordId); });
1042
+ svg.appendChild(g);
1043
+ });
1044
+
1045
+ if (maxWidth) container.style.maxWidth = maxWidth + 'px';
1046
+ container.appendChild(svg);
1047
+ container.scrollLeft = width;
1048
+ }
1049
+ function escapeHtml(s) {
1050
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1051
+ }
1052
+
1053
+ // \u2500\u2500 Time travel tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1054
+ function renderTimeTravel(main) {
1055
+ if (!state.collection) { main.appendChild(el('div', { class: 'empty', text: 'No collections.' })); return; }
1056
+ toolbar(main, state.collection + ' \u2014 time travel');
1057
+ api('/api/collections/' + state.collection + '/replay').then(function (full) {
1058
+ var total = full.totalCount;
1059
+ if (!total) { main.appendChild(el('div', { class: 'empty', text: 'No operations to replay.' })); return; }
1060
+ if (state.ttPosition === null || state.ttPosition > total) state.ttPosition = total;
1061
+
1062
+ var posLabel = el('span', { class: 'mono', text: '' });
1063
+ var slider = el('input', { type: 'range', min: '1', max: String(total), value: String(state.ttPosition) });
1064
+ var playBtn = el('button', { text: '\u25B6 replay' });
1065
+ var cutBox = el('div', { class: 'tt-cut mono' });
1066
+ var stateBox = el('div', {});
1067
+
1068
+ function show(position) {
1069
+ state.ttPosition = position;
1070
+ posLabel.textContent = position + ' / ' + total + ' ops';
1071
+ api('/api/collections/' + state.collection + '/replay?upTo=' + encodeURIComponent(opIdAt(position))).then(function (cut) {
1072
+ var o = cut.cutOperation;
1073
+ cutBox.innerHTML = o
1074
+ ? 'cut at <b>' + o.type + '</b> by ' + shortNode(o.nodeId) + ' \xB7 ' + (o.timestamp ? fmtDateTime(o.timestamp.wallTime) : '') +
1075
+ (o.data ? ' \xB7 ' + escapeHtml(JSON.stringify(o.data).slice(0, 90)) : '')
1076
+ : '';
1077
+ renderReplayState(stateBox, cut);
1078
+ });
1079
+ }
1080
+ // The full replay result orders ops by HLC; fetch the ordered id list once.
1081
+ var orderedIds = [];
1082
+ api('/api/collections/' + state.collection + '/dag?limit=20000').then(function (dag) {
1083
+ orderedIds = dag.nodes.map(function (n) { return n.id; });
1084
+ show(Math.min(state.ttPosition, orderedIds.length));
1085
+ });
1086
+ function opIdAt(position) { return orderedIds[position - 1]; }
1087
+
1088
+ slider.oninput = function () { show(Number(slider.value)); };
1089
+ playBtn.onclick = function () {
1090
+ if (state.ttPlaying) { clearInterval(state.ttPlaying); state.ttPlaying = null; playBtn.textContent = '\u25B6 replay'; return; }
1091
+ var pos = 1;
1092
+ playBtn.textContent = '\u23F8 pause';
1093
+ state.ttPlaying = setInterval(function () {
1094
+ if (pos > orderedIds.length) { clearInterval(state.ttPlaying); state.ttPlaying = null; playBtn.textContent = '\u25B6 replay'; return; }
1095
+ slider.value = String(pos);
1096
+ show(pos);
1097
+ pos++;
1098
+ }, 350);
1099
+ };
1100
+
1101
+ main.appendChild(el('div', { class: 'tt-controls' }, [playBtn, slider, posLabel]));
1102
+ main.appendChild(cutBox);
1103
+ main.appendChild(stateBox);
1104
+ }).catch(showError(main));
1105
+ }
1106
+
1107
+ function renderReplayState(box, cut) {
1108
+ box.textContent = '';
1109
+ if (!cut.records.length) { box.appendChild(el('div', { class: 'empty', text: 'No records at this point.' })); return; }
1110
+ var fieldSet = {};
1111
+ cut.records.forEach(function (r) { Object.keys(r.fields).forEach(function (f) { fieldSet[f] = true; }); });
1112
+ var fields = Object.keys(fieldSet);
1113
+ var thead = el('tr', {}, [el('th', { text: 'id' })].concat(fields.map(function (f) { return el('th', { text: f }); })));
1114
+ var tbody = el('tbody', {}, cut.records.map(function (r) {
1115
+ return el('tr', { class: r.deleted ? 'tombstone' : '' },
1116
+ [el('td', { class: 'mono', text: shortId(r.id) })].concat(fields.map(function (f) {
1117
+ return el('td', { text: r.fields[f] === undefined ? '' : fmtVal(r.fields[f]).slice(0, 40) });
1118
+ })));
1119
+ }));
1120
+ box.appendChild(el('table', { class: 'grid' }, [el('thead', {}, [thead]), tbody]));
1121
+ }
1122
+
1123
+ // \u2500\u2500 Merges tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1124
+ function renderMerges(main) {
1125
+ toolbar(main, 'Merge audit trail');
1126
+ api('/api/audit?limit=200').then(function (data) {
1127
+ if (!data.traces.length) { main.appendChild(el('div', { class: 'empty', text: 'No merge audit traces recorded.' })); return; }
1128
+ var thead = el('tr', {}, ['when', 'event', 'collection', 'record', 'field', 'strategy', 'tier'].map(function (h) { return el('th', { text: h }); }));
1129
+ var tbody = el('tbody', {}, data.traces.map(function (t) {
1130
+ return el('tr', {}, [
1131
+ el('td', { class: 'mono', text: fmtDateTime(t.recordedAt) }),
1132
+ el('td', { text: t.eventType }),
1133
+ el('td', { text: t.collection }),
1134
+ el('td', { class: 'mono', text: shortId(t.recordId) }),
1135
+ el('td', { text: t.field }),
1136
+ el('td', { text: t.strategy }),
1137
+ el('td', { text: String(t.tier) })
1138
+ ]);
1139
+ }));
1140
+ main.appendChild(el('table', { class: 'grid' }, [el('thead', {}, [thead]), tbody]));
1141
+ }).catch(showError(main));
1142
+ }
1143
+
1144
+ // \u2500\u2500 Sync tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1145
+ function renderSync(main) {
1146
+ toolbar(main, 'Sync state' + (state.device ? ' \u2014 ' + state.device : ''));
1147
+ var o = state.overview;
1148
+ main.appendChild(el('div', { class: 'panel' }, [
1149
+ el('h3', { text: 'Version vector \u2014 what this store has seen from each node' }),
1150
+ el('table', { class: 'grid' }, [
1151
+ el('thead', {}, [el('tr', {}, [el('th', { text: 'node' }), el('th', { text: 'max sequence' })])]),
1152
+ el('tbody', {}, o.versionVector.map(function (v) {
1153
+ return el('tr', {}, [el('td', { class: 'mono', text: v.nodeId }), el('td', { class: 'mono', text: String(v.sequenceNumber) })]);
1154
+ }))
1155
+ ])
1156
+ ]));
1157
+ main.appendChild(el('div', { class: 'panel' }, [
1158
+ el('h3', { text: 'Outbound queue' }),
1159
+ el('div', { text: o.pendingSyncOps + ' operation(s) waiting to sync' })
1160
+ ]));
1161
+ main.appendChild(el('div', { class: 'panel' }, [
1162
+ el('h3', { text: 'Store meta' }),
1163
+ el('table', { class: 'grid' }, [
1164
+ el('tbody', {}, o.meta.map(function (m) {
1165
+ return el('tr', {}, [el('td', { class: 'mono', text: m.key }), el('td', { class: 'mono', text: m.value })]);
1166
+ }))
1167
+ ])
1168
+ ]));
1169
+ if (state.mode === 'spectator') {
1170
+ var feedBox = el('div', { class: 'lab-feed', style: 'width:100%;max-height:340px' }, [
1171
+ el('h3', { text: 'Live events from the server' }),
1172
+ el('div', { class: 'feed', id: 'feed' })
1173
+ ]);
1174
+ main.appendChild(feedBox);
1175
+ state.feed.slice(-200).forEach(appendFeedLine);
1176
+ }
1177
+ }
1178
+
1179
+ // \u2500\u2500 Lab tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1180
+ function renderLab(full) {
1181
+ var main = qs('#main');
1182
+ qs('#sidebar').style.display = 'none';
1183
+ Promise.all([fetch('/api/lab/state').then(function (r) { return r.json(); }),
1184
+ fetch('/api/lab/convergence').then(function (r) { return r.json(); })])
1185
+ .then(function (results) {
1186
+ state.labState = results[0];
1187
+ state.devices = results[0].devices.map(function (d) { return d.name; });
1188
+ var convergence = results[1];
1189
+ main.textContent = '';
1190
+ qs('#dbpath').textContent = 'sync laboratory \xB7 ' + results[0].serverOperations + ' ops on server';
1191
+
1192
+ var conv = el('div', { class: 'convergence ' + (convergence.converged ? 'ok' : 'bad') }, [
1193
+ el('div', { text: convergence.converged
1194
+ ? '\u2713 ALL ' + convergence.deviceCount + ' DEVICES CONVERGED'
1195
+ : '\u2717 DEVICES DIVERGED (' + convergence.differences.length + ' difference(s)) \u2014 sync to converge' })
1196
+ ]);
1197
+ if (!convergence.converged) {
1198
+ conv.appendChild(el('div', { class: 'diffs', text: convergence.differences.slice(0, 4).join(' \xB7 ') }));
1199
+ }
1200
+ var addBtn = el('button', { class: 'primary', text: '+ add device', onclick: function () {
1201
+ post('/api/lab/devices', {}).then(function () { render(true); }).catch(alertError);
1202
+ } });
1203
+ var syncAllBtn = el('button', { text: 'sync all', onclick: function () {
1204
+ var chain = Promise.resolve();
1205
+ state.devices.forEach(function (d) { chain = chain.then(function () { return post('/api/lab/devices/' + d + '/sync', {}); }); });
1206
+ chain.then(function () { return post('/api/lab/devices/' + state.devices[0] + '/sync', {}); })
1207
+ .then(function () { render(true); }).catch(alertError);
1208
+ } });
1209
+ main.appendChild(el('div', { class: 'lab-top' }, [conv, addBtn, syncAllBtn]));
1210
+
1211
+ var devicesBox = el('div', { class: 'lab-devices' });
1212
+ results[0].devices.forEach(function (d) { devicesBox.appendChild(deviceCard(d, results[0].collections)); });
1213
+
1214
+ var feedBox = el('div', { class: 'lab-feed' }, [
1215
+ el('h3', { text: 'Live events' }),
1216
+ el('div', { class: 'feed', id: 'feed' })
1217
+ ]);
1218
+ main.appendChild(el('div', { class: 'lab-grid' }, [devicesBox, feedBox]));
1219
+ state.feed.slice(-200).forEach(appendFeedLine);
1220
+ })
1221
+ .catch(showError(qs('#main')));
1222
+ }
1223
+
1224
+ function deviceCard(d, collections) {
1225
+ var collection = collections[0];
1226
+ var card = el('div', { class: 'device-card' + (d.connected ? '' : ' offline') });
1227
+ card.appendChild(el('div', { class: 'device-head' }, [
1228
+ el('span', { class: 'name ' + deviceColor(d.name), text: d.name }),
1229
+ el('span', { class: 'node mono', text: shortNode(d.nodeId) }),
1230
+ el('span', { class: 'badge ' + (d.connected ? 'ok' : 'bad'), text: d.connected ? 'online' : 'offline' }),
1231
+ d.pendingOperations ? el('span', { class: 'badge neutral', text: d.pendingOperations + ' queued' }) : null,
1232
+ el('span', { class: 'spacer' }),
1233
+ el('button', { text: d.connected ? 'disconnect' : 'connect', onclick: function () {
1234
+ post('/api/lab/devices/' + d.name + '/' + (d.connected ? 'disconnect' : 'connect'), {})
1235
+ .then(function () { render(true); }).catch(alertError);
1236
+ } }),
1237
+ el('button', { text: 'sync', onclick: function () {
1238
+ post('/api/lab/devices/' + d.name + '/sync', {}).then(function () { render(true); }).catch(alertError);
1239
+ } })
1240
+ ]));
1241
+
1242
+ var body = el('div', { class: 'device-body' });
1243
+ card.appendChild(body);
1244
+
1245
+ // Chaos controls
1246
+ body.appendChild(el('h5', { text: 'Network chaos (applies on next connect)' }));
1247
+ var chaosDefs = [['dropRate', 'drop'], ['duplicateRate', 'duplicate'], ['reorderRate', 'reorder']];
1248
+ chaosDefs.forEach(function (cd) {
1249
+ var key = cd[0];
1250
+ var valueLabel = el('span', { class: 'mono', text: String(d.chaos[key]) });
1251
+ var range = el('input', { type: 'range', min: '0', max: '0.5', step: '0.05', value: String(d.chaos[key]) });
1252
+ range.oninput = function () { valueLabel.textContent = range.value; };
1253
+ range.onchange = function () {
1254
+ var body2 = {}; body2[key] = Number(range.value);
1255
+ post('/api/lab/devices/' + d.name + '/chaos', body2).catch(alertError);
1256
+ };
1257
+ body.appendChild(el('div', { class: 'chaos-row' }, [el('span', { text: cd[1] }), range, valueLabel]));
1258
+ });
1259
+
1260
+ // Records with inline editing
1261
+ body.appendChild(el('h5', { text: 'records (' + collection.name + ') \u2014 edits happen ON this device' }));
1262
+ var recBox = el('div', {});
1263
+ body.appendChild(recBox);
1264
+ api('/api/collections/' + collection.name + '/records?limit=20&device=' + encodeURIComponent(d.name)).then(function (data) {
1265
+ data.records.forEach(function (r) {
1266
+ var titleInput = el('input', { type: 'text', value: String(r.fields.title || '') });
1267
+ titleInput.onchange = function () {
1268
+ post('/api/lab/devices/' + d.name + '/update', { collection: collection.name, id: r.id, data: { title: titleInput.value } }).catch(alertError);
1269
+ };
1270
+ var doneCb = el('input', { type: 'checkbox', title: 'done' });
1271
+ doneCb.checked = !!r.fields.done;
1272
+ doneCb.onchange = function () {
1273
+ post('/api/lab/devices/' + d.name + '/update', { collection: collection.name, id: r.id, data: { done: doneCb.checked } }).catch(alertError);
1274
+ };
1275
+ var incBtn = el('button', { text: '+1', title: 'atomic increment \u2014 composes across devices', onclick: function () {
1276
+ post('/api/lab/devices/' + d.name + '/update', { collection: collection.name, id: r.id, data: {}, increments: { points: 1 } })
1277
+ .then(function () { debounceRefresh(); }).catch(alertError);
1278
+ } });
1279
+ var delBtn = el('button', { class: 'danger', text: '\u2715', title: 'delete', onclick: function () {
1280
+ post('/api/lab/devices/' + d.name + '/delete', { collection: collection.name, id: r.id })
1281
+ .then(function () { debounceRefresh(); }).catch(alertError);
1282
+ } });
1283
+ recBox.appendChild(el('div', { class: 'rec-line' }, [
1284
+ doneCb, titleInput,
1285
+ el('span', { class: 'pts mono', text: String(r.fields.points === undefined ? '' : r.fields.points) }),
1286
+ incBtn, delBtn,
1287
+ el('button', { text: '\u2315', title: 'inspect', onclick: function () { state.device = d.name; state.collection = collection.name; openRecord(r.id); } })
1288
+ ]));
1289
+ });
1290
+ if (!data.records.length) recBox.appendChild(el('div', { class: 'empty', text: 'no records yet' }));
1291
+ });
1292
+
1293
+ // Insert form
1294
+ var newTitle = el('input', { type: 'text', placeholder: 'new task title\u2026' });
1295
+ var insertBtn = el('button', { class: 'primary', text: 'insert', onclick: function () {
1296
+ if (!newTitle.value.trim()) return;
1297
+ post('/api/lab/devices/' + d.name + '/insert', { collection: collection.name, data: { title: newTitle.value.trim() } })
1298
+ .then(function () { newTitle.value = ''; debounceRefresh(); }).catch(alertError);
1299
+ } });
1300
+ newTitle.addEventListener('keydown', function (e) { if (e.key === 'Enter') insertBtn.click(); });
1301
+ body.appendChild(el('div', { class: 'insert-form' }, [newTitle, insertBtn]));
1302
+ return card;
1303
+ }
1304
+
1305
+ function appendFeedLine(event) {
1306
+ var feed = qs('#feed');
1307
+ if (!feed) return;
1308
+ var cls = 'feed-line';
1309
+ if (event.type === 'operation:created') cls += ' evt-created';
1310
+ if (event.type === 'operation:applied') cls += ' evt-applied';
1311
+ if (event.type === 'merge:conflict') cls += ' evt-conflict';
1312
+ if (event.type === 'sync:apply-failed') cls += ' evt-failed';
1313
+ feed.appendChild(el('div', { class: cls }, [
1314
+ el('span', { class: 'mono', text: fmtTime(event.at) }),
1315
+ el('span', { class: 'dev ' + deviceColor(event.device), text: event.device }),
1316
+ el('span', { class: 'what', text: event.type.replace('operation:', '').replace('sync:', '') + ' \xB7 ' + event.summary })
1317
+ ]));
1318
+ feed.scrollTop = feed.scrollHeight;
1319
+ }
1320
+
1321
+ // \u2500\u2500 Misc \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1322
+ function showError(main) {
1323
+ return function (e) { main.appendChild(el('div', { class: 'error', text: e.message })); };
1324
+ }
1325
+ function alertError(e) { console.error(e); qs('#dbpath').textContent = 'error: ' + e.message; }
1326
+
1327
+ qs('#refresh').onclick = function () { render(true); };
1328
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeDrawer(); });
1329
+ })();
1330
+ `;
1331
+ }
1332
+ });
1333
+
1334
+ // src/commands/studio/studio-ui-css.ts
1335
+ var STUDIO_CSS;
1336
+ var init_studio_ui_css = __esm({
1337
+ "src/commands/studio/studio-ui-css.ts"() {
1338
+ "use strict";
1339
+ STUDIO_CSS = `
1340
+ :root {
1341
+ --bg: #0e1015; --panel: #161a22; --panel-2: #1c212c; --panel-3: #232936;
1342
+ --border: #262c3a; --text: #e6e9f0; --muted: #8b93a7;
1343
+ --accent: #4fd1c5; --accent-2: #9f7aea;
1344
+ --insert: #48bb78; --update: #4299e1; --delete: #f56565; --warn: #ed8936;
1345
+ --ok-bg: rgba(72,187,120,.12); --bad-bg: rgba(245,101,101,.12);
1346
+ }
1347
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1348
+ html, body { height: 100%; }
1349
+ body {
1350
+ background: var(--bg); color: var(--text); overflow: hidden;
1351
+ font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
1352
+ }
1353
+ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
1354
+ button {
1355
+ background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
1356
+ border-radius: 6px; padding: 5px 12px; cursor: pointer; font-size: 13px;
1357
+ }
1358
+ button:hover { border-color: var(--accent); }
1359
+ button.primary { background: rgba(79,209,197,.12); border-color: var(--accent); color: var(--accent); }
1360
+ button.danger { color: var(--delete); }
1361
+ button:disabled { opacity: .4; cursor: default; }
1362
+ input, select {
1363
+ background: var(--bg); color: var(--text); border: 1px solid var(--border);
1364
+ border-radius: 6px; padding: 4px 8px; font-size: 13px;
1365
+ }
1366
+ input:focus, select:focus { outline: none; border-color: var(--accent); }
1367
+ input[type="checkbox"] { width: auto; }
1368
+ input[type="range"] { padding: 0; }
1369
+
1370
+ header {
1371
+ display: flex; align-items: center; gap: 14px; padding: 10px 16px;
1372
+ background: var(--panel); border-bottom: 1px solid var(--border); height: 48px;
1373
+ }
1374
+ header .logo { font-weight: 700; letter-spacing: .3px; white-space: nowrap; }
1375
+ header .logo span { color: var(--accent); }
1376
+ header .modebadge {
1377
+ font-size: 10px; text-transform: uppercase; letter-spacing: 1px; padding: 2px 8px;
1378
+ border-radius: 10px; background: rgba(159,122,234,.15); color: var(--accent-2);
1379
+ }
1380
+ header .dbpath { color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
1381
+ header .live { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 12px; }
1382
+ header .live .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
1383
+ header .live.on .dot { background: var(--insert); box-shadow: 0 0 8px rgba(72,187,120,.8); }
1384
+
1385
+ .tabs { display: flex; gap: 2px; padding: 0 16px; background: var(--panel); border-bottom: 1px solid var(--border); }
1386
+ .tabs .tab {
1387
+ padding: 8px 16px; cursor: pointer; color: var(--muted); font-size: 13px;
1388
+ border-bottom: 2px solid transparent; user-select: none;
1389
+ }
1390
+ .tabs .tab:hover { color: var(--text); }
1391
+ .tabs .tab.active { color: var(--accent); border-bottom-color: var(--accent); }
1392
+ .tabs .spacer { flex: 1; }
1393
+ .tabs .devicepick { display: flex; align-items: center; gap: 6px; padding: 6px 0; color: var(--muted); font-size: 12px; }
1394
+
1395
+ .layout { display: flex; height: calc(100vh - 85px); }
1396
+ nav.sidebar {
1397
+ width: 220px; background: var(--panel); border-right: 1px solid var(--border);
1398
+ padding: 12px 0; overflow-y: auto; flex-shrink: 0;
1399
+ }
1400
+ nav.sidebar h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .8px; color: var(--muted); padding: 8px 16px 4px; }
1401
+ nav.sidebar .item {
1402
+ display: flex; justify-content: space-between; align-items: center;
1403
+ padding: 6px 16px; cursor: pointer; border-left: 2px solid transparent; font-size: 13px;
1404
+ }
1405
+ nav.sidebar .item:hover { background: var(--panel-2); }
1406
+ nav.sidebar .item.active { border-left-color: var(--accent); background: var(--panel-2); }
1407
+ nav.sidebar .item .count { color: var(--muted); font-size: 11px; }
1408
+ main { flex: 1; overflow: auto; padding: 16px 20px; }
1409
+
1410
+ .toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
1411
+ .toolbar h2 { font-size: 16px; margin-right: auto; }
1412
+ .toolbar label { color: var(--muted); font-size: 12px; display: flex; align-items: center; gap: 5px; cursor: pointer; }
1413
+
1414
+ table.grid { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
1415
+ table.grid th, table.grid td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
1416
+ table.grid th { background: var(--panel-2); color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .5px; position: sticky; top: 0; z-index: 1; }
1417
+ table.grid tbody tr.clickable { cursor: pointer; }
1418
+ table.grid tbody tr.clickable:hover { background: var(--panel-2); }
1419
+ tr.tombstone td { color: var(--muted); text-decoration: line-through; }
1420
+
1421
+ .badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; }
1422
+ .badge.insert { background: rgba(72,187,120,.15); color: var(--insert); }
1423
+ .badge.update { background: rgba(66,153,225,.15); color: var(--update); }
1424
+ .badge.delete { background: rgba(245,101,101,.15); color: var(--delete); }
1425
+ .badge.neutral { background: var(--panel-3); color: var(--muted); }
1426
+ .badge.ok { background: var(--ok-bg); color: var(--insert); }
1427
+ .badge.bad { background: var(--bad-bg); color: var(--delete); }
1428
+
1429
+ .chip {
1430
+ display: inline-flex; gap: 6px; align-items: center; background: var(--panel-2);
1431
+ border: 1px solid var(--border); border-radius: 6px; padding: 2px 8px; font-size: 11px; color: var(--muted);
1432
+ }
1433
+ .chip b { color: var(--accent-2); font-weight: 600; }
1434
+
1435
+ .pager { display: flex; gap: 8px; align-items: center; margin-top: 10px; color: var(--muted); font-size: 12px; }
1436
+ .empty { color: var(--muted); padding: 40px; text-align: center; }
1437
+ .error { color: var(--delete); padding: 20px; }
1438
+
1439
+ .drawer {
1440
+ position: fixed; top: 85px; right: 0; bottom: 0; width: 560px; max-width: 92vw;
1441
+ background: var(--panel); border-left: 1px solid var(--border); overflow-y: auto;
1442
+ padding: 16px; box-shadow: -12px 0 30px rgba(0,0,0,.45); display: none; z-index: 20;
1443
+ }
1444
+ .drawer.open { display: block; }
1445
+ .drawer h3 { font-size: 14px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }
1446
+ .drawer h4 { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; margin: 16px 0 8px; }
1447
+ .drawer .close { cursor: pointer; color: var(--muted); padding: 4px 8px; }
1448
+ .fieldrow { padding: 8px 10px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 6px; }
1449
+ .fieldrow .fname { color: var(--accent); font-size: 12px; }
1450
+ .fieldrow .fval { word-break: break-word; margin: 2px 0 4px; }
1451
+ .fieldrow .preview { color: var(--muted); font-style: italic; }
1452
+
1453
+ .op { border-left: 2px solid var(--border); margin-left: 6px; padding: 6px 0 6px 14px; position: relative; }
1454
+ .op::before { content: ""; position: absolute; left: -5px; top: 13px; width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
1455
+ .op.insert::before { background: var(--insert); } .op.update::before { background: var(--update); } .op.delete::before { background: var(--delete); }
1456
+ .op .meta { color: var(--muted); font-size: 11px; }
1457
+ .op pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; margin-top: 4px; overflow-x: auto; }
1458
+
1459
+ .panel { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 14px; }
1460
+ .panel h3 { font-size: 13px; margin-bottom: 8px; color: var(--accent); }
1461
+
1462
+ /* DAG */
1463
+ .dagwrap { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; overflow-x: auto; margin-bottom: 14px; }
1464
+ .dagwrap svg { display: block; }
1465
+ .dag-node { cursor: pointer; }
1466
+ .dag-node circle { stroke-width: 2; }
1467
+ .dag-node text { fill: var(--muted); font-size: 10px; font-family: ui-monospace, Menlo, monospace; }
1468
+ .dag-edge { stroke: #3a4356; stroke-width: 1.4; fill: none; marker-end: url(#arrow); }
1469
+ .dag-lane-label { fill: var(--accent-2); font-size: 11px; font-family: ui-monospace, Menlo, monospace; }
1470
+ .dag-lane-line { stroke: #1d2330; stroke-width: 1; }
1471
+ .dag-tip {
1472
+ position: fixed; background: var(--panel-3); border: 1px solid var(--border); border-radius: 6px;
1473
+ padding: 6px 10px; font-size: 12px; pointer-events: none; z-index: 50; max-width: 380px; display: none;
1474
+ }
1475
+
1476
+ /* Time travel */
1477
+ .tt-controls { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
1478
+ .tt-controls input[type="range"] { flex: 1; }
1479
+ .tt-cut { background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 14px; font-size: 12px; }
1480
+
1481
+ /* Lab */
1482
+ .lab-top { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }
1483
+ .convergence { padding: 8px 14px; border-radius: 8px; font-weight: 600; font-size: 13px; }
1484
+ .convergence.ok { background: var(--ok-bg); color: var(--insert); border: 1px solid rgba(72,187,120,.4); }
1485
+ .convergence.bad { background: var(--bad-bg); color: var(--delete); border: 1px solid rgba(245,101,101,.4); }
1486
+ .convergence .diffs { font-weight: 400; font-size: 11px; margin-top: 4px; color: var(--muted); }
1487
+ .lab-grid { display: flex; gap: 14px; align-items: flex-start; }
1488
+ .lab-devices { flex: 1; display: grid; grid-template-columns: repeat(auto-fill, minmax(390px, 1fr)); gap: 14px; }
1489
+ .device-card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
1490
+ .device-card.offline { border-color: rgba(245,101,101,.35); }
1491
+ .device-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--panel-2); }
1492
+ .device-head .name { font-weight: 700; }
1493
+ .device-head .node { color: var(--muted); font-size: 11px; }
1494
+ .device-head .spacer { flex: 1; }
1495
+ .device-body { padding: 10px 12px; }
1496
+ .device-body h5 { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; margin: 10px 0 6px; }
1497
+ .chaos-row { display: grid; grid-template-columns: 78px 1fr 44px; gap: 6px; align-items: center; font-size: 11px; color: var(--muted); margin-bottom: 3px; }
1498
+ .rec-line { display: flex; align-items: center; gap: 6px; padding: 5px 6px; border-bottom: 1px solid var(--border); font-size: 12px; flex-wrap: wrap; }
1499
+ .rec-line:hover { background: var(--panel-2); }
1500
+ .rec-line .title { flex: 1; min-width: 120px; }
1501
+ .rec-line input[type="text"] { font-size: 12px; padding: 2px 6px; width: 130px; }
1502
+ .rec-line .pts { color: var(--accent); min-width: 30px; text-align: right; }
1503
+ .insert-form { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
1504
+ .insert-form input { flex: 1; min-width: 120px; }
1505
+ .lab-feed { width: 360px; flex-shrink: 0; background: var(--panel); border: 1px solid var(--border); border-radius: 10px; display: flex; flex-direction: column; max-height: calc(100vh - 200px); }
1506
+ .lab-feed h3 { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; padding: 10px 12px; border-bottom: 1px solid var(--border); }
1507
+ .lab-feed .feed { overflow-y: auto; padding: 6px 0; flex: 1; }
1508
+ .feed-line { display: flex; gap: 8px; padding: 3px 12px; font-size: 11px; align-items: baseline; }
1509
+ .feed-line .dev { min-width: 66px; font-weight: 600; }
1510
+ .feed-line .what { color: var(--muted); }
1511
+ .feed-line.evt-created .what, .feed-line.evt-applied .what { color: var(--text); }
1512
+ .feed-line.evt-conflict .what { color: var(--warn); }
1513
+ .feed-line.evt-failed .what { color: var(--delete); font-weight: 700; }
1514
+ .dev-c0 { color: #4fd1c5; } .dev-c1 { color: #9f7aea; } .dev-c2 { color: #f6ad55; }
1515
+ .dev-c3 { color: #63b3ed; } .dev-c4 { color: #f687b3; } .dev-c5 { color: #68d391; }
1516
+ `;
1517
+ }
1518
+ });
1519
+
1520
+ // src/commands/studio/studio-server.ts
1521
+ var studio_server_exports = {};
1522
+ __export(studio_server_exports, {
1523
+ startStudioServer: () => startStudioServer
1524
+ });
1525
+ async function startStudioServer(options) {
1526
+ const host = options.host ?? "127.0.0.1";
1527
+ const dbPath = options.spectator ? options.spectator.dbPath : options.dbPath;
1528
+ const context = {
1529
+ mode: options.spectator ? "spectator" : options.lab ? "lab" : "file",
1530
+ lab: options.lab ?? null,
1531
+ spectator: options.spectator ?? null,
1532
+ mainReader: dbPath ? await StudioDbReader.open(dbPath) : null,
1533
+ deviceReaders: /* @__PURE__ */ new Map()
1534
+ };
1535
+ const sseClients = /* @__PURE__ */ new Set();
1536
+ const broadcast = (event, data) => {
1537
+ const payload = `event: ${event}
1538
+ data: ${JSON.stringify(data)}
1539
+
1540
+ `;
1541
+ for (const client of sseClients) {
1542
+ client.write(payload);
1543
+ }
1544
+ };
1545
+ let unsubscribeSpectator = null;
1546
+ if (context.spectator) {
1547
+ unsubscribeSpectator = context.spectator.onEvent((event) => {
1548
+ broadcast("spectator", event);
1549
+ broadcast("change", { at: event.at });
1550
+ });
1551
+ }
1552
+ let pollTimer = null;
1553
+ if (context.mode === "file" && context.mainReader) {
1554
+ let lastFingerprint = context.mainReader.fingerprint();
1555
+ pollTimer = setInterval(() => {
1556
+ try {
1557
+ const current = context.mainReader?.fingerprint();
1558
+ if (current !== void 0 && current !== lastFingerprint) {
1559
+ lastFingerprint = current;
1560
+ broadcast("change", { at: Date.now() });
1561
+ }
1562
+ } catch {
1563
+ }
1564
+ }, SSE_POLL_MS);
1565
+ }
1566
+ let unsubscribeLab = null;
1567
+ if (context.lab) {
1568
+ unsubscribeLab = context.lab.onEvent((event) => {
1569
+ broadcast("lab", event);
1570
+ broadcast("change", { at: event.at });
1571
+ });
1572
+ }
1573
+ const server = (0, import_node_http.createServer)((req, res) => {
1574
+ handleRequest(context, sseClients, req, res).catch((error) => {
1575
+ const message = error instanceof Error ? error.message : "Internal error";
1576
+ if (!res.headersSent) {
1577
+ sendJson(res, 500, { error: message });
1578
+ } else {
1579
+ res.end();
1580
+ }
1581
+ });
1582
+ });
1583
+ await new Promise((resolve12, reject) => {
1584
+ server.once("error", reject);
1585
+ server.listen(options.port, host, () => {
1586
+ server.removeListener("error", reject);
1587
+ resolve12();
1588
+ });
1589
+ });
1590
+ const address = server.address();
1591
+ const port = typeof address === "object" && address !== null ? address.port : options.port;
1592
+ return {
1593
+ server,
1594
+ port,
1595
+ url: `http://${host}:${port}`,
1596
+ close: async () => {
1597
+ if (pollTimer) {
1598
+ clearInterval(pollTimer);
1599
+ }
1600
+ unsubscribeSpectator?.();
1601
+ unsubscribeLab?.();
1602
+ for (const client of sseClients) {
1603
+ client.end();
1604
+ }
1605
+ sseClients.clear();
1606
+ context.mainReader?.close();
1607
+ for (const [, reader] of context.deviceReaders) {
1608
+ reader.close();
1609
+ }
1610
+ context.deviceReaders.clear();
1611
+ await new Promise((resolve12, reject) => {
1612
+ server.close((error) => error ? reject(error) : resolve12());
1613
+ });
1614
+ }
1615
+ };
1616
+ }
1617
+ async function resolveReader(context, url) {
1618
+ const device = url.searchParams.get("device");
1619
+ if (device && context.lab) {
1620
+ const cached = context.deviceReaders.get(device);
1621
+ if (cached) {
1622
+ return cached;
1623
+ }
1624
+ const state = context.lab.deviceState(device);
1625
+ const reader = await StudioDbReader.open(state.dbPath);
1626
+ context.deviceReaders.set(device, reader);
1627
+ return reader;
1628
+ }
1629
+ if (context.mainReader) {
1630
+ return context.mainReader;
1631
+ }
1632
+ if (context.lab) {
1633
+ const first = context.lab.listDevices()[0];
1634
+ if (first) {
1635
+ return resolveReader(context, new URL(`${url.origin}${url.pathname}?device=${first.name}`));
1636
+ }
1637
+ }
1638
+ throw new Error("No database available");
1639
+ }
1640
+ async function handleRequest(context, sseClients, req, res) {
1641
+ const url = new URL(req.url ?? "/", "http://studio.local");
1642
+ const path = url.pathname;
1643
+ const method = req.method ?? "GET";
1644
+ if (method === "GET" && (path === "/" || path === "/index.html")) {
1645
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1646
+ res.end(STUDIO_HTML);
1647
+ return;
1648
+ }
1649
+ if (method === "GET" && path === "/app.js") {
1650
+ res.writeHead(200, { "content-type": "text/javascript; charset=utf-8" });
1651
+ res.end(STUDIO_APP_JS);
1652
+ return;
1653
+ }
1654
+ if (method === "GET" && path === "/style.css") {
1655
+ res.writeHead(200, { "content-type": "text/css; charset=utf-8" });
1656
+ res.end(STUDIO_CSS);
1657
+ return;
1658
+ }
1659
+ if (method === "GET" && path === "/api/events") {
1660
+ res.writeHead(200, {
1661
+ "content-type": "text/event-stream",
1662
+ "cache-control": "no-store",
1663
+ connection: "keep-alive"
1664
+ });
1665
+ res.write(`event: hello
1666
+ data: ${JSON.stringify({ mode: context.mode })}
1667
+
1668
+ `);
1669
+ if (context.lab) {
1670
+ for (const event of context.lab.recentEvents().slice(-100)) {
1671
+ res.write(`event: lab
1672
+ data: ${JSON.stringify(event)}
1673
+
1674
+ `);
1675
+ }
1676
+ }
1677
+ if (context.spectator) {
1678
+ for (const event of context.spectator.recentEvents().slice(-100)) {
1679
+ res.write(`event: spectator
1680
+ data: ${JSON.stringify(event)}
1681
+
1682
+ `);
1683
+ }
1684
+ }
1685
+ sseClients.add(res);
1686
+ req.on("close", () => {
1687
+ sseClients.delete(res);
1688
+ });
1689
+ return;
1690
+ }
1691
+ if (path.startsWith("/api/lab")) {
1692
+ if (!context.lab) {
1693
+ sendJson(res, 404, {
1694
+ error: "Studio is not running in lab mode. Start with: kora studio --lab"
1695
+ });
1696
+ return;
1697
+ }
1698
+ await handleLabRoute(context, context.lab, req, res, url);
1699
+ return;
1700
+ }
1701
+ if (method !== "GET") {
1702
+ sendJson(res, 405, { error: "Studio data routes are read-only: GET only." });
1703
+ return;
1704
+ }
1705
+ if (path === "/api/mode") {
1706
+ sendJson(res, 200, {
1707
+ mode: context.mode,
1708
+ devices: context.lab?.listDevices().map((d) => d.name) ?? [],
1709
+ ...context.spectator ? { spectator: context.spectator.status() } : {}
1710
+ });
1711
+ return;
1712
+ }
1713
+ if (path === "/api/spectator/status") {
1714
+ if (!context.spectator) {
1715
+ sendJson(res, 404, { error: "Not in spectator mode" });
1716
+ return;
1717
+ }
1718
+ sendJson(res, 200, context.spectator.status());
1719
+ return;
1720
+ }
1721
+ if (path === "/api/overview") {
1722
+ const reader = await resolveReader(context, url);
1723
+ sendJson(res, 200, reader.overview());
1724
+ return;
1725
+ }
1726
+ if (path === "/api/audit") {
1727
+ const reader = await resolveReader(context, url);
1728
+ sendJson(res, 200, { traces: reader.auditTraces(intParam(url, "limit") ?? 100) });
1729
+ return;
1730
+ }
1731
+ const collectionMatch = path.match(
1732
+ /^\/api\/collections\/([a-zA-Z0-9_]+)\/(records|ops|replay|dag)(?:\/([^/]+))?(?:\/(ops))?$/
1733
+ );
1734
+ if (collectionMatch) {
1735
+ const [, collection, kind, recordId, sub] = collectionMatch;
1736
+ if (!collection || !kind) {
1737
+ sendJson(res, 400, { error: "Bad request" });
1738
+ return;
1739
+ }
1740
+ const reader = await resolveReader(context, url);
1741
+ if (!reader.listCollections().includes(collection)) {
1742
+ sendJson(res, 404, { error: `Unknown collection "${collection}"` });
1743
+ return;
1744
+ }
1745
+ if (kind === "replay") {
1746
+ const ops = reader.allOperations(collection);
1747
+ const upTo = url.searchParams.get("upTo");
1748
+ sendJson(res, 200, replayToOperation(ops, upTo));
1749
+ return;
1750
+ }
1751
+ if (kind === "dag") {
1752
+ const record = url.searchParams.get("record");
1753
+ const limit = intParam(url, "limit") ?? 200;
1754
+ let ops = reader.allOperations(collection);
1755
+ if (record) {
1756
+ ops = ops.filter((o) => o.recordId === record);
1757
+ } else {
1758
+ ops = ops.slice(-limit);
1759
+ }
1760
+ sendJson(res, 200, buildCausalDag(ops));
1761
+ return;
1762
+ }
1763
+ if (kind === "ops") {
1764
+ sendJson(
1765
+ res,
1766
+ 200,
1767
+ reader.operations(collection, {
1768
+ limit: intParam(url, "limit"),
1769
+ offset: intParam(url, "offset")
1770
+ })
1771
+ );
1772
+ return;
1773
+ }
1774
+ if (recordId && sub === "ops") {
1775
+ sendJson(res, 200, { operations: reader.recordOperations(collection, recordId) });
1776
+ return;
1777
+ }
1778
+ if (recordId) {
1779
+ const record = reader.record(collection, recordId);
1780
+ if (!record) {
1781
+ sendJson(res, 404, { error: `Record "${recordId}" not found` });
1782
+ return;
1783
+ }
1784
+ const richtextPreviews = await decodeRichtextFields(
1785
+ reader,
1786
+ collection,
1787
+ recordId,
1788
+ record.fields
1789
+ );
1790
+ sendJson(res, 200, {
1791
+ record,
1792
+ richtextPreviews,
1793
+ operations: reader.recordOperations(collection, recordId)
1794
+ });
1795
+ return;
1796
+ }
1797
+ sendJson(
1798
+ res,
1799
+ 200,
1800
+ reader.records(collection, {
1801
+ limit: intParam(url, "limit"),
1802
+ offset: intParam(url, "offset"),
1803
+ includeDeleted: url.searchParams.get("includeDeleted") === "true",
1804
+ search: url.searchParams.get("search") ?? void 0
1805
+ })
1806
+ );
1807
+ return;
1808
+ }
1809
+ sendJson(res, 404, { error: `No route for ${path}` });
1810
+ }
1811
+ async function handleLabRoute(context, lab, req, res, url) {
1812
+ const path = url.pathname;
1813
+ const method = req.method ?? "GET";
1814
+ if (method === "GET" && path === "/api/lab/state") {
1815
+ const schema = lab.getSchema();
1816
+ sendJson(res, 200, {
1817
+ devices: lab.listDevices(),
1818
+ serverOperations: lab.serverOperationCount(),
1819
+ collections: Object.entries(schema.collections).map(([name, def]) => ({
1820
+ name,
1821
+ fields: Object.entries(def.fields).map(([fieldName, descriptor]) => ({
1822
+ name: fieldName,
1823
+ kind: descriptor.kind,
1824
+ optional: !descriptor.required,
1825
+ enumValues: descriptor.enumValues ?? null,
1826
+ defaultValue: descriptor.defaultValue ?? null
1827
+ }))
1828
+ }))
1829
+ });
1830
+ return;
1831
+ }
1832
+ if (method === "GET" && path === "/api/lab/convergence") {
1833
+ sendJson(res, 200, await lab.convergence());
1834
+ return;
1835
+ }
1836
+ if (method !== "POST") {
1837
+ sendJson(res, 405, { error: "POST required" });
1838
+ return;
1839
+ }
1840
+ const body = await readJsonBody(req);
1841
+ if (path === "/api/lab/devices") {
1842
+ const state = await lab.addDevice(typeof body.name === "string" ? body.name : void 0);
1843
+ sendJson(res, 200, state);
1844
+ return;
1845
+ }
1846
+ const deviceMatch = path.match(
1847
+ /^\/api\/lab\/devices\/([a-zA-Z0-9_-]+)\/(connect|disconnect|sync|chaos|insert|update|delete)$/
1848
+ );
1849
+ if (!deviceMatch) {
1850
+ sendJson(res, 404, { error: `No lab route for ${path}` });
1851
+ return;
1852
+ }
1853
+ const [, device, action] = deviceMatch;
1854
+ if (!device || !action) {
1855
+ sendJson(res, 400, { error: "Bad request" });
1856
+ return;
1857
+ }
1858
+ switch (action) {
1859
+ case "connect":
1860
+ await lab.connect(device);
1861
+ break;
1862
+ case "disconnect": {
1863
+ await lab.disconnect(device);
1864
+ break;
1865
+ }
1866
+ case "sync":
1867
+ await lab.sync(device);
1868
+ break;
1869
+ case "chaos":
1870
+ sendJson(
1871
+ res,
1872
+ 200,
1873
+ lab.setChaos(device, body)
1874
+ );
1875
+ return;
1876
+ case "insert": {
1877
+ const record = await lab.insert(
1878
+ device,
1879
+ String(body.collection ?? ""),
1880
+ body.data ?? {}
1881
+ );
1882
+ sendJson(res, 200, { record });
1883
+ return;
1884
+ }
1885
+ case "update": {
1886
+ const record = await lab.update(
1887
+ device,
1888
+ String(body.collection ?? ""),
1889
+ String(body.id ?? ""),
1890
+ body.data ?? {},
1891
+ body.increments ?? void 0
1892
+ );
1893
+ sendJson(res, 200, { record });
1894
+ return;
1895
+ }
1896
+ case "delete":
1897
+ await lab.delete(device, String(body.collection ?? ""), String(body.id ?? ""));
1898
+ break;
1899
+ }
1900
+ sendJson(res, 200, { ok: true, device: lab.deviceState(device) });
1901
+ }
1902
+ async function decodeRichtextFields(reader, collection, recordId, fields) {
1903
+ const previews = {};
1904
+ const binaryFields = Object.entries(fields).filter(
1905
+ ([, value]) => typeof value === "string" && value.startsWith("<binary ")
1906
+ );
1907
+ if (binaryFields.length === 0) {
1908
+ return previews;
1909
+ }
1910
+ let toPlainText = null;
1911
+ try {
1912
+ const storePkg = await import("@korajs/store");
1913
+ toPlainText = storePkg.richtextToPlainText ?? null;
1914
+ } catch {
1915
+ return previews;
1916
+ }
1917
+ if (!toPlainText) {
1918
+ return previews;
1919
+ }
1920
+ for (const [field] of binaryFields) {
1921
+ try {
1922
+ const raw = reader.rawFieldValue(collection, recordId, field);
1923
+ if (raw instanceof Uint8Array || Buffer.isBuffer(raw)) {
1924
+ previews[field] = toPlainText(new Uint8Array(raw));
1925
+ }
1926
+ } catch {
1927
+ }
1928
+ }
1929
+ return previews;
1930
+ }
1931
+ async function readJsonBody(req) {
1932
+ const chunks = [];
1933
+ let size = 0;
1934
+ for await (const chunk of req) {
1935
+ size += chunk.length;
1936
+ if (size > 256 * 1024) {
1937
+ throw new Error("Request body too large");
1938
+ }
1939
+ chunks.push(chunk);
1940
+ }
1941
+ if (chunks.length === 0) {
1942
+ return {};
1943
+ }
1944
+ try {
1945
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1946
+ } catch {
1947
+ throw new Error("Invalid JSON body");
1948
+ }
1949
+ }
1950
+ function intParam(url, name) {
1951
+ const raw = url.searchParams.get(name);
1952
+ if (raw === null) {
1953
+ return void 0;
1954
+ }
1955
+ const parsed = Number.parseInt(raw, 10);
1956
+ return Number.isNaN(parsed) ? void 0 : parsed;
1957
+ }
1958
+ function sendJson(res, status, body) {
1959
+ res.writeHead(status, {
1960
+ "content-type": "application/json; charset=utf-8",
1961
+ "cache-control": "no-store"
1962
+ });
1963
+ res.end(JSON.stringify(body));
1964
+ }
1965
+ var import_node_http, SSE_POLL_MS;
1966
+ var init_studio_server = __esm({
1967
+ "src/commands/studio/studio-server.ts"() {
1968
+ "use strict";
1969
+ import_node_http = require("http");
1970
+ init_db_reader();
1971
+ init_studio_replay();
1972
+ init_studio_ui();
1973
+ init_studio_ui_app();
1974
+ init_studio_ui_css();
1975
+ SSE_POLL_MS = 700;
1976
+ }
1977
+ });
1978
+
1979
+ // src/commands/studio/spectator-manager.ts
1980
+ var spectator_manager_exports = {};
1981
+ __export(spectator_manager_exports, {
1982
+ SpectatorManager: () => SpectatorManager
1983
+ });
1984
+ var import_node_fs6, import_node_os, import_node_path27, FORWARDED_EVENTS, MAX_EVENT_BUFFER, SpectatorManager;
1985
+ var init_spectator_manager = __esm({
1986
+ "src/commands/studio/spectator-manager.ts"() {
1987
+ "use strict";
1988
+ import_node_fs6 = require("fs");
1989
+ import_node_os = require("os");
1990
+ import_node_path27 = require("path");
1991
+ FORWARDED_EVENTS = [
1992
+ "merge:started",
1993
+ "merge:conflict",
1994
+ "merge:completed",
1995
+ "sync:connected",
1996
+ "sync:disconnected",
1997
+ "sync:received",
1998
+ "sync:apply-failed"
1999
+ ];
2000
+ MAX_EVENT_BUFFER = 500;
2001
+ SpectatorManager = class {
2002
+ constructor(options) {
2003
+ this.options = options;
2004
+ this.tmpDir = (0, import_node_fs6.mkdtempSync)((0, import_node_path27.join)((0, import_node_os.tmpdir)(), "kora-studio-spectator-"));
2005
+ this.dbPath = (0, import_node_path27.join)(this.tmpDir, "spectator-replica.db");
2006
+ }
2007
+ options;
2008
+ runtime = null;
2009
+ tmpDir = null;
2010
+ connected = false;
2011
+ operationsReceived = 0;
2012
+ lastEventAt = null;
2013
+ eventSeq = 0;
2014
+ eventBuffer = [];
2015
+ eventListeners = /* @__PURE__ */ new Set();
2016
+ dbPath;
2017
+ async start() {
2018
+ let mods;
2019
+ try {
2020
+ mods = {
2021
+ Store: (await import("@korajs/store")).Store,
2022
+ BetterSqlite3Adapter: (await import("@korajs/store/better-sqlite3")).BetterSqlite3Adapter,
2023
+ MergeEngine: (await import("@korajs/merge")).MergeEngine,
2024
+ SyncEngine: (await import("@korajs/sync")).SyncEngine,
2025
+ WebSocketTransport: (await import("@korajs/sync")).WebSocketTransport,
2026
+ testing: await import("korajs/testing"),
2027
+ SimpleEventEmitter: (await import("@korajs/core/internal")).SimpleEventEmitter
2028
+ };
2029
+ } catch (error) {
2030
+ throw new Error(
2031
+ `Spectator mode needs the Kora runtime packages installed (pnpm add -D korajs @korajs/store @korajs/merge @korajs/sync better-sqlite3). Underlying error: ${error instanceof Error ? error.message : String(error)}`
2032
+ );
2033
+ }
2034
+ const emitter = new mods.SimpleEventEmitter();
2035
+ const adapter = new mods.BetterSqlite3Adapter(this.dbPath);
2036
+ const store = new mods.Store({ schema: this.options.schema, adapter, emitter });
2037
+ await store.open();
2038
+ const mergeEngine = new mods.MergeEngine();
2039
+ const pipeline = new mods.testing.ApplyPipeline({ store, mergeEngine, emitter });
2040
+ store.setLocalMutationHandler(pipeline);
2041
+ const unsubscribeAudit = mods.testing.wireAuditPersistence(store, emitter);
2042
+ const syncStore = new mods.testing.MergeAwareSyncStore(store, mergeEngine, emitter);
2043
+ const engine = new mods.SyncEngine({
2044
+ transport: new mods.WebSocketTransport(),
2045
+ store: syncStore,
2046
+ queueStorage: new mods.testing.StoreQueueStorage(adapter),
2047
+ syncState: new mods.testing.StoreSyncStatePersistence(store),
2048
+ config: {
2049
+ url: this.options.url,
2050
+ schemaVersion: this.options.schemaVersion ?? this.options.schema.version,
2051
+ ...this.options.token ? { auth: async () => ({ token: this.options.token }) } : {}
2052
+ },
2053
+ emitter
2054
+ });
2055
+ const unsubscribers = FORWARDED_EVENTS.map(
2056
+ (type) => emitter.on(type, (event) => {
2057
+ this.lastEventAt = Date.now();
2058
+ if (type === "sync:connected") {
2059
+ this.connected = true;
2060
+ }
2061
+ if (type === "sync:disconnected") {
2062
+ this.connected = false;
2063
+ }
2064
+ if (type === "sync:received" && Array.isArray(event.operations)) {
2065
+ this.operationsReceived += event.operations.length;
2066
+ }
2067
+ this.pushEvent(type, event);
2068
+ })
2069
+ );
2070
+ unsubscribers.push(unsubscribeAudit);
2071
+ this.runtime = { store, engine, unsubscribers };
2072
+ await engine.start();
2073
+ }
2074
+ status() {
2075
+ return {
2076
+ url: this.options.url,
2077
+ connected: this.connected,
2078
+ operationsReceived: this.operationsReceived,
2079
+ lastEventAt: this.lastEventAt,
2080
+ dbPath: this.dbPath
2081
+ };
2082
+ }
2083
+ recentEvents() {
2084
+ return [...this.eventBuffer];
2085
+ }
2086
+ onEvent(listener) {
2087
+ this.eventListeners.add(listener);
2088
+ return () => this.eventListeners.delete(listener);
2089
+ }
2090
+ async close() {
2091
+ if (this.runtime) {
2092
+ for (const unsub of this.runtime.unsubscribers) {
2093
+ unsub();
2094
+ }
2095
+ await this.runtime.engine.stop().catch(() => {
2096
+ });
2097
+ await this.runtime.store.close().catch(() => {
2098
+ });
2099
+ this.runtime = null;
2100
+ }
2101
+ if (this.tmpDir) {
2102
+ (0, import_node_fs6.rmSync)(this.tmpDir, { recursive: true, force: true });
2103
+ this.tmpDir = null;
2104
+ }
2105
+ }
2106
+ pushEvent(type, payload) {
2107
+ const summary = type === "sync:received" && Array.isArray(payload.operations) ? `received ${payload.operations.length} op(s)` : type.replace("sync:", "").replace("merge:", "merge ");
2108
+ const event = {
2109
+ seq: ++this.eventSeq,
2110
+ at: Date.now(),
2111
+ type,
2112
+ summary
2113
+ };
2114
+ this.eventBuffer.push(event);
2115
+ if (this.eventBuffer.length > MAX_EVENT_BUFFER) {
2116
+ this.eventBuffer.shift();
2117
+ }
2118
+ for (const listener of this.eventListeners) {
2119
+ listener(event);
2120
+ }
2121
+ }
2122
+ };
2123
+ }
2124
+ });
2125
+
2126
+ // src/commands/studio/lab-manager.ts
2127
+ var lab_manager_exports = {};
2128
+ __export(lab_manager_exports, {
2129
+ LabManager: () => LabManager,
2130
+ defaultLabSchema: () => defaultLabSchema
2131
+ });
2132
+ function defaultLabSchema() {
2133
+ return (0, import_core3.defineSchema)({
2134
+ version: 1,
2135
+ collections: {
2136
+ tasks: {
2137
+ fields: {
2138
+ title: import_core3.t.string(),
2139
+ assignee: import_core3.t.string().optional(),
2140
+ priority: import_core3.t.enum(["low", "medium", "high"]).default("medium"),
2141
+ done: import_core3.t.boolean().default(false),
2142
+ tags: import_core3.t.array(import_core3.t.string()).default([]),
2143
+ points: import_core3.t.number().default(0)
2144
+ }
2145
+ }
2146
+ }
2147
+ });
2148
+ }
2149
+ function clamp01(n) {
2150
+ return Math.min(1, Math.max(0, n));
2151
+ }
2152
+ function summarizeEvent(type, payload) {
2153
+ const op = payload.operation;
2154
+ switch (type) {
2155
+ case "operation:created":
2156
+ return op ? `${op.type} ${op.collection}/${String(op.recordId).slice(0, 8)}\u2026 ${op.data ? JSON.stringify(op.data).slice(0, 60) : ""}` : "operation created";
2157
+ case "operation:applied":
2158
+ return op ? `applied ${op.type} ${op.collection}/${String(op.recordId).slice(0, 8)}\u2026` : "applied";
2159
+ case "merge:conflict":
2160
+ return "merge conflict resolved";
2161
+ case "merge:started":
2162
+ return "merge started";
2163
+ case "merge:completed":
2164
+ return "merge completed";
2165
+ case "sync:sent": {
2166
+ const count = Array.isArray(payload.operations) ? payload.operations.length : 0;
2167
+ return `sent ${count} op(s)`;
2168
+ }
2169
+ case "sync:received": {
2170
+ const count = Array.isArray(payload.operations) ? payload.operations.length : 0;
2171
+ return `received ${count} op(s)`;
2172
+ }
2173
+ case "sync:connected":
2174
+ return "connected to server";
2175
+ case "sync:disconnected":
2176
+ return "disconnected";
2177
+ case "sync:apply-failed":
2178
+ return "APPLY FAILED";
2179
+ case "lab:chaos-changed":
2180
+ return `chaos \u2192 drop ${payload.dropRate} dup ${payload.duplicateRate} reorder ${payload.reorderRate} latency ${payload.maxLatency}ms`;
2181
+ default:
2182
+ return type.replace("lab:", "");
2183
+ }
2184
+ }
2185
+ var import_node_fs7, import_node_os2, import_node_path28, import_core3, DEFAULT_CHAOS, FORWARDED_EVENTS2, MAX_EVENT_BUFFER2, LabManager;
2186
+ var init_lab_manager = __esm({
2187
+ "src/commands/studio/lab-manager.ts"() {
2188
+ "use strict";
2189
+ import_node_fs7 = require("fs");
2190
+ import_node_os2 = require("os");
2191
+ import_node_path28 = require("path");
2192
+ import_core3 = require("@korajs/core");
2193
+ DEFAULT_CHAOS = {
2194
+ dropRate: 0,
2195
+ duplicateRate: 0,
2196
+ reorderRate: 0,
2197
+ maxLatency: 0
2198
+ };
2199
+ FORWARDED_EVENTS2 = [
2200
+ "operation:created",
2201
+ "operation:applied",
2202
+ "merge:started",
2203
+ "merge:conflict",
2204
+ "merge:completed",
2205
+ "sync:connected",
2206
+ "sync:disconnected",
2207
+ "sync:sent",
2208
+ "sync:received",
2209
+ "sync:apply-failed"
2210
+ ];
2211
+ MAX_EVENT_BUFFER2 = 500;
2212
+ LabManager = class {
2213
+ constructor(schema) {
2214
+ this.schema = schema;
2215
+ }
2216
+ schema;
2217
+ devices = /* @__PURE__ */ new Map();
2218
+ server = null;
2219
+ harness = null;
2220
+ tmpDir = null;
2221
+ eventSeq = 0;
2222
+ eventBuffer = [];
2223
+ eventListeners = /* @__PURE__ */ new Set();
2224
+ deviceCounter = 0;
2225
+ /** Load the harness lazily so file-mode Studio never needs @korajs/test. */
2226
+ async loadHarness() {
2227
+ if (this.harness) {
2228
+ return this.harness;
2229
+ }
2230
+ try {
2231
+ const testPkg = await import("@korajs/test");
2232
+ const serverInternal = await import("@korajs/server/internal");
2233
+ this.harness = {
2234
+ TestServer: testPkg.TestServer,
2235
+ TestDevice: testPkg.TestDevice,
2236
+ checkConvergence: testPkg.checkConvergence,
2237
+ ChaosTransport: testPkg.ChaosTransport,
2238
+ createServerTransportPair: serverInternal.createServerTransportPair
2239
+ };
2240
+ return this.harness;
2241
+ } catch (error) {
2242
+ throw new Error(
2243
+ `Kora Studio Lab needs "@korajs/test" and "@korajs/server" installed (pnpm add -D @korajs/test @korajs/server). Underlying error: ${error instanceof Error ? error.message : String(error)}`
2244
+ );
2245
+ }
2246
+ }
2247
+ async start(initialDevices = 2) {
2248
+ const harness = await this.loadHarness();
2249
+ this.tmpDir = (0, import_node_fs7.mkdtempSync)((0, import_node_path28.join)((0, import_node_os2.tmpdir)(), "kora-studio-lab-"));
2250
+ this.server = new harness.TestServer(this.schema);
2251
+ for (let i = 0; i < initialDevices; i++) {
2252
+ await this.addDevice();
2253
+ }
2254
+ }
2255
+ async addDevice(name) {
2256
+ const harness = await this.loadHarness();
2257
+ if (!this.server || !this.tmpDir) {
2258
+ throw new Error("Lab is not started");
2259
+ }
2260
+ const deviceName = name?.trim() || `device-${String.fromCharCode(65 + this.deviceCounter)}`;
2261
+ this.deviceCounter++;
2262
+ if (this.devices.has(deviceName)) {
2263
+ throw new Error(`Device "${deviceName}" already exists`);
2264
+ }
2265
+ const chaos = { ...DEFAULT_CHAOS };
2266
+ const entry = { chaos };
2267
+ const device = new harness.TestDevice({
2268
+ name: deviceName,
2269
+ schema: this.schema,
2270
+ server: this.server,
2271
+ // Read the CURRENT chaos settings each time a connection is made, so
2272
+ // toggling chaos + reconnecting applies it without rebuilding the device.
2273
+ createTransportPair: () => {
2274
+ const pair = harness.createServerTransportPair();
2275
+ const hasChaos = entry.chaos.dropRate > 0 || entry.chaos.duplicateRate > 0 || entry.chaos.reorderRate > 0 || entry.chaos.maxLatency > 0;
2276
+ return {
2277
+ client: hasChaos ? new harness.ChaosTransport(pair.client, { ...entry.chaos }) : pair.client,
2278
+ serverTransport: pair.server
2279
+ };
2280
+ },
2281
+ tmpDir: this.tmpDir
2282
+ });
2283
+ await device.open();
2284
+ const unsubscribes = FORWARDED_EVENTS2.map(
2285
+ (type) => device.emitter.on(type, (event) => this.pushEvent(deviceName, type, event))
2286
+ );
2287
+ this.devices.set(deviceName, { device, chaos, unsubscribes });
2288
+ this.pushEvent(deviceName, "lab:device-added", {});
2289
+ return this.deviceState(deviceName);
2290
+ }
2291
+ listDevices() {
2292
+ return [...this.devices.keys()].map((name) => this.deviceState(name));
2293
+ }
2294
+ deviceState(name) {
2295
+ const entry = this.mustGet(name);
2296
+ return {
2297
+ name,
2298
+ nodeId: entry.device.getNodeId(),
2299
+ connected: entry.device.isConnected(),
2300
+ chaos: { ...entry.chaos },
2301
+ dbPath: (0, import_node_path28.join)(this.tmpDir ?? "", `test-device-${name}.db`),
2302
+ pendingOperations: entry.device.getSyncEngine()?.getStatus().pendingOperations ?? 0
2303
+ };
2304
+ }
2305
+ async connect(name) {
2306
+ await this.mustGet(name).device.sync();
2307
+ this.pushEvent(name, "lab:connected", {});
2308
+ }
2309
+ async disconnect(name) {
2310
+ await this.mustGet(name).device.disconnect();
2311
+ this.pushEvent(name, "lab:disconnected", {});
2312
+ }
2313
+ async sync(name) {
2314
+ await this.mustGet(name).device.sync();
2315
+ }
2316
+ setChaos(name, chaos) {
2317
+ const entry = this.mustGet(name);
2318
+ entry.chaos.dropRate = clamp01(chaos.dropRate ?? entry.chaos.dropRate);
2319
+ entry.chaos.duplicateRate = clamp01(chaos.duplicateRate ?? entry.chaos.duplicateRate);
2320
+ entry.chaos.reorderRate = clamp01(chaos.reorderRate ?? entry.chaos.reorderRate);
2321
+ entry.chaos.maxLatency = Math.max(0, chaos.maxLatency ?? entry.chaos.maxLatency);
2322
+ this.pushEvent(name, "lab:chaos-changed", { ...entry.chaos });
2323
+ return { ...entry.chaos };
2324
+ }
2325
+ async insert(name, collection, data) {
2326
+ return this.mustGet(name).device.collection(collection).insert(data);
2327
+ }
2328
+ async update(name, collection, recordId, data, increments) {
2329
+ const payload = { ...data };
2330
+ for (const [field, delta] of Object.entries(increments ?? {})) {
2331
+ payload[field] = import_core3.op.increment(delta);
2332
+ }
2333
+ return this.mustGet(name).device.collection(collection).update(recordId, payload);
2334
+ }
2335
+ async delete(name, collection, recordId) {
2336
+ await this.mustGet(name).device.collection(collection).delete(recordId);
2337
+ }
2338
+ async convergence() {
2339
+ const harness = await this.loadHarness();
2340
+ const devices = [...this.devices.values()].map((e) => e.device);
2341
+ if (devices.length < 2) {
2342
+ return { converged: true, deviceCount: devices.length, differences: [] };
2343
+ }
2344
+ const result = await harness.checkConvergence(devices, this.schema);
2345
+ return {
2346
+ converged: result.converged,
2347
+ deviceCount: devices.length,
2348
+ differences: result.differences.map(
2349
+ (d) => d.description ?? `collection "${d.collection}" differs`
2350
+ )
2351
+ };
2352
+ }
2353
+ serverOperationCount() {
2354
+ return this.server?.getAllOperations().length ?? 0;
2355
+ }
2356
+ getSchema() {
2357
+ return this.schema;
2358
+ }
2359
+ /** Recent events (for initial page load), oldest first. */
2360
+ recentEvents() {
2361
+ return [...this.eventBuffer];
2362
+ }
2363
+ onEvent(listener) {
2364
+ this.eventListeners.add(listener);
2365
+ return () => this.eventListeners.delete(listener);
2366
+ }
2367
+ async close() {
2368
+ for (const [, entry] of this.devices) {
2369
+ for (const unsub of entry.unsubscribes) {
2370
+ unsub();
2371
+ }
2372
+ await entry.device.close().catch(() => {
2373
+ });
2374
+ }
2375
+ this.devices.clear();
2376
+ await this.server?.close().catch(() => {
2377
+ });
2378
+ this.server = null;
2379
+ if (this.tmpDir) {
2380
+ (0, import_node_fs7.rmSync)(this.tmpDir, { recursive: true, force: true });
2381
+ this.tmpDir = null;
2382
+ }
2383
+ }
2384
+ mustGet(name) {
2385
+ const entry = this.devices.get(name);
2386
+ if (!entry) {
2387
+ throw new Error(`Unknown device "${name}"`);
2388
+ }
2389
+ return entry;
2390
+ }
2391
+ pushEvent(device, type, payload) {
2392
+ const event = {
2393
+ seq: ++this.eventSeq,
2394
+ at: Date.now(),
2395
+ device,
2396
+ type,
2397
+ summary: summarizeEvent(type, payload)
2398
+ };
2399
+ this.eventBuffer.push(event);
2400
+ if (this.eventBuffer.length > MAX_EVENT_BUFFER2) {
2401
+ this.eventBuffer.shift();
2402
+ }
2403
+ for (const listener of this.eventListeners) {
2404
+ listener(event);
2405
+ }
2406
+ }
2407
+ };
2408
+ }
2409
+ });
24
2410
 
25
2411
  // src/bin.ts
26
- var import_citty11 = require("citty");
2412
+ var import_citty12 = require("citty");
27
2413
 
28
2414
  // src/commands/backup/backup-command.ts
29
2415
  var import_promises = require("fs/promises");
@@ -294,93 +2680,15 @@ var DevServerError = class extends import_core.KoraError {
294
2680
  searchPath;
295
2681
  };
296
2682
 
297
- // src/utils/fs-helpers.ts
298
- var import_promises2 = require("fs/promises");
299
- var import_node_path2 = require("path");
300
- async function directoryExists(path) {
301
- try {
302
- await (0, import_promises2.access)(path);
303
- return true;
304
- } catch {
305
- return false;
306
- }
307
- }
308
- async function findProjectRoot(startDir) {
309
- let current = (0, import_node_path2.resolve)(startDir ?? process.cwd());
310
- for (; ; ) {
311
- const pkgPath = (0, import_node_path2.join)(current, "package.json");
312
- try {
313
- const content = await (0, import_promises2.readFile)(pkgPath, "utf-8");
314
- const pkg = JSON.parse(content);
315
- if (isKoraProject(pkg)) {
316
- return current;
317
- }
318
- } catch {
319
- }
320
- const parent = (0, import_node_path2.dirname)(current);
321
- if (parent === current) break;
322
- current = parent;
323
- }
324
- return null;
325
- }
326
- async function findSchemaFile(projectRoot) {
327
- const candidates = [
328
- (0, import_node_path2.join)(projectRoot, "src", "schema.ts"),
329
- (0, import_node_path2.join)(projectRoot, "schema.ts"),
330
- (0, import_node_path2.join)(projectRoot, "src", "schema.js"),
331
- (0, import_node_path2.join)(projectRoot, "schema.js")
332
- ];
333
- for (const candidate of candidates) {
334
- try {
335
- await (0, import_promises2.access)(candidate);
336
- return candidate;
337
- } catch {
338
- }
339
- }
340
- return null;
341
- }
342
- async function hasTsxInstalled(projectRoot) {
343
- try {
344
- await (0, import_promises2.access)((0, import_node_path2.join)(projectRoot, "node_modules", "tsx", "package.json"));
345
- return true;
346
- } catch {
347
- return false;
348
- }
349
- }
350
- async function resolveProjectBinaryEntryPoint(projectRoot, packageName, binaryName) {
351
- const pkgJsonPath = (0, import_node_path2.join)(projectRoot, "node_modules", packageName, "package.json");
352
- try {
353
- const content = await (0, import_promises2.readFile)(pkgJsonPath, "utf-8");
354
- const pkg = JSON.parse(content);
355
- let binPath;
356
- if (typeof pkg.bin === "string") {
357
- binPath = pkg.bin;
358
- } else if (typeof pkg.bin === "object" && pkg.bin !== null) {
359
- binPath = pkg.bin[binaryName];
360
- }
361
- if (!binPath) return null;
362
- const fullPath = (0, import_node_path2.join)(projectRoot, "node_modules", packageName, binPath);
363
- await (0, import_promises2.access)(fullPath);
364
- return fullPath;
365
- } catch {
366
- return null;
367
- }
368
- }
369
- function isKoraProject(pkg) {
370
- if (typeof pkg !== "object" || pkg === null) return false;
371
- const record = pkg;
372
- return hasKoraDep(record.dependencies) || hasKoraDep(record.devDependencies);
373
- }
374
- function hasKoraDep(deps) {
375
- if (typeof deps !== "object" || deps === null) return false;
376
- return Object.keys(deps).some((key) => key === "kora" || key.startsWith("@korajs/"));
377
- }
2683
+ // src/commands/compact/compact-command.ts
2684
+ init_fs_helpers();
378
2685
 
379
2686
  // src/commands/dev/kora-config.ts
380
2687
  var import_node_child_process = require("child_process");
381
2688
  var import_promises3 = require("fs/promises");
382
2689
  var import_node_path3 = require("path");
383
2690
  var import_node_url = require("url");
2691
+ init_fs_helpers();
384
2692
  var CONFIG_CANDIDATES = [
385
2693
  "kora.config.ts",
386
2694
  "kora.config.mts",
@@ -430,7 +2738,7 @@ async function loadTypeScriptConfig(configPath, projectRoot) {
430
2738
  }
431
2739
  }
432
2740
  async function runCommand(command, args, cwd) {
433
- return await new Promise((resolve11, reject) => {
2741
+ return await new Promise((resolve12, reject) => {
434
2742
  const child = (0, import_node_child_process.spawn)(command, args, {
435
2743
  cwd,
436
2744
  stdio: ["ignore", "pipe", "pipe"],
@@ -449,7 +2757,7 @@ async function runCommand(command, args, cwd) {
449
2757
  });
450
2758
  child.on("exit", (code) => {
451
2759
  if (code === 0) {
452
- resolve11(stdout.trim());
2760
+ resolve12(stdout.trim());
453
2761
  return;
454
2762
  }
455
2763
  reject(new Error(`Failed to load kora config (exit ${code ?? "unknown"}): ${stderr.trim()}`));
@@ -467,80 +2775,8 @@ function toConfigObject(mod) {
467
2775
  return value;
468
2776
  }
469
2777
 
470
- // src/commands/migrate/schema-loader.ts
471
- var import_node_child_process2 = require("child_process");
472
- var import_node_path4 = require("path");
473
- var import_node_url2 = require("url");
474
- async function loadSchemaDefinition(schemaPath, projectRoot) {
475
- const ext = (0, import_node_path4.extname)(schemaPath);
476
- const moduleValue = ext === ".ts" || ext === ".mts" || ext === ".cts" ? await loadTypeScriptModule(schemaPath, projectRoot) : await import(`${(0, import_node_url2.pathToFileURL)(schemaPath).href}?t=${Date.now()}-${Math.random()}`);
477
- return extractSchema(moduleValue);
478
- }
479
- function extractSchema(value) {
480
- if (typeof value !== "object" || value === null) {
481
- throw new Error("Schema module must export an object.");
482
- }
483
- const moduleRecord = value;
484
- const candidate = moduleRecord.default ?? moduleRecord;
485
- if (!isSchemaDefinition(candidate)) {
486
- throw new Error("Schema module must export a valid SchemaDefinition as default export.");
487
- }
488
- return candidate;
489
- }
490
- function isSchemaDefinition(value) {
491
- if (typeof value !== "object" || value === null) return false;
492
- const object = value;
493
- return typeof object.version === "number" && typeof object.collections === "object" && object.collections !== null && typeof object.relations === "object" && object.relations !== null;
494
- }
495
- async function loadTypeScriptModule(schemaPath, projectRoot) {
496
- if (!await hasTsxInstalled(projectRoot)) {
497
- throw new Error(
498
- `Schema file is TypeScript (${schemaPath}) but local "tsx" was not found. Install tsx in the project.`
499
- );
500
- }
501
- const script = "const modulePath = process.argv[process.argv.length - 1];import('node:url').then(u => import(u.pathToFileURL(modulePath).href)).then(mod => { const v = mod.default ?? mod; process.stdout.write(JSON.stringify(v)) }).catch(e => { process.stderr.write(String(e)); process.exit(1) })";
502
- const output = await runCommand2(
503
- process.execPath,
504
- ["--import", "tsx", "--eval", script, schemaPath],
505
- projectRoot
506
- );
507
- try {
508
- return JSON.parse(output);
509
- } catch {
510
- throw new Error(`Failed to parse schema module output for ${schemaPath}`);
511
- }
512
- }
513
- async function runCommand2(command, args, cwd) {
514
- return await new Promise((resolve11, reject) => {
515
- const child = (0, import_node_child_process2.spawn)(command, args, {
516
- cwd,
517
- stdio: ["ignore", "pipe", "pipe"],
518
- env: process.env
519
- });
520
- let stdout = "";
521
- let stderr = "";
522
- child.stdout?.on("data", (chunk) => {
523
- stdout += chunk.toString("utf-8");
524
- });
525
- child.stderr?.on("data", (chunk) => {
526
- stderr += chunk.toString("utf-8");
527
- });
528
- child.on("error", (error) => {
529
- reject(error);
530
- });
531
- child.on("exit", (code) => {
532
- if (code === 0) {
533
- resolve11(stdout.trim());
534
- return;
535
- }
536
- reject(
537
- new Error(`Failed to load TypeScript schema (exit ${code ?? "unknown"}): ${stderr.trim()}`)
538
- );
539
- });
540
- });
541
- }
542
-
543
2778
  // src/commands/compact/compact-command.ts
2779
+ init_schema_loader();
544
2780
  var compactCommand = (0, import_citty2.defineCommand)({
545
2781
  meta: {
546
2782
  name: "compact",
@@ -661,18 +2897,18 @@ var import_prompts = require("@clack/prompts");
661
2897
  // src/utils/prompt.ts
662
2898
  var import_node_readline = require("readline");
663
2899
  function promptText(message, defaultValue, options) {
664
- return new Promise((resolve11) => {
2900
+ return new Promise((resolve12) => {
665
2901
  const rl = createReadline(options);
666
2902
  const suffix = defaultValue !== void 0 ? ` (${defaultValue})` : "";
667
2903
  rl.question(` ? ${message}${suffix}: `, (answer) => {
668
2904
  rl.close();
669
2905
  const trimmed = answer.trim();
670
- resolve11(trimmed || defaultValue || "");
2906
+ resolve12(trimmed || defaultValue || "");
671
2907
  });
672
2908
  });
673
2909
  }
674
2910
  function promptSelect(message, choices, options) {
675
- return new Promise((resolve11) => {
2911
+ return new Promise((resolve12) => {
676
2912
  const rl = createReadline(options);
677
2913
  const out = options?.output ?? process.stdout;
678
2914
  out.write(` ? ${message}
@@ -690,7 +2926,7 @@ function promptSelect(message, choices, options) {
690
2926
  const selected = choices[index];
691
2927
  if (selected) {
692
2928
  rl.close();
693
- resolve11(selected.value);
2929
+ resolve12(selected.value);
694
2930
  } else {
695
2931
  out.write(` Please enter a number between 1 and ${choices.length}
696
2932
  `);
@@ -702,7 +2938,7 @@ function promptSelect(message, choices, options) {
702
2938
  });
703
2939
  }
704
2940
  function promptConfirm(message, defaultValue = false, options) {
705
- return new Promise((resolve11) => {
2941
+ return new Promise((resolve12) => {
706
2942
  const rl = createReadline(options);
707
2943
  const suffix = defaultValue ? "Y/n" : "y/N";
708
2944
  const ask = () => {
@@ -710,17 +2946,17 @@ function promptConfirm(message, defaultValue = false, options) {
710
2946
  const normalized = answer.trim().toLowerCase();
711
2947
  if (normalized.length === 0) {
712
2948
  rl.close();
713
- resolve11(defaultValue);
2949
+ resolve12(defaultValue);
714
2950
  return;
715
2951
  }
716
2952
  if (normalized === "y" || normalized === "yes") {
717
2953
  rl.close();
718
- resolve11(true);
2954
+ resolve12(true);
719
2955
  return;
720
2956
  }
721
2957
  if (normalized === "n" || normalized === "no") {
722
2958
  rl.close();
723
- resolve11(false);
2959
+ resolve12(false);
724
2960
  return;
725
2961
  }
726
2962
  ;
@@ -841,6 +3077,9 @@ var TEMPLATES = [
841
3077
  "tauri-react"
842
3078
  ];
843
3079
 
3080
+ // src/commands/create/create-command.ts
3081
+ init_fs_helpers();
3082
+
844
3083
  // src/utils/package-manager.ts
845
3084
  var import_node_child_process3 = require("child_process");
846
3085
  function detectPackageManager() {
@@ -1441,7 +3680,9 @@ function createCompatibilityLayerPlan(templateName) {
1441
3680
  case "vue-tailwind-sync":
1442
3681
  return {
1443
3682
  compatibilityTarget: templateName,
1444
- layers: [{ category: "base", name: "vue-tailwind-sync", sourceTemplate: "vue-tailwind-sync" }]
3683
+ layers: [
3684
+ { category: "base", name: "vue-tailwind-sync", sourceTemplate: "vue-tailwind-sync" }
3685
+ ]
1445
3686
  };
1446
3687
  case "vue-tailwind":
1447
3688
  return {
@@ -1452,7 +3693,11 @@ function createCompatibilityLayerPlan(templateName) {
1452
3693
  return {
1453
3694
  compatibilityTarget: templateName,
1454
3695
  layers: [
1455
- { category: "base", name: "svelte-tailwind-sync", sourceTemplate: "svelte-tailwind-sync" }
3696
+ {
3697
+ category: "base",
3698
+ name: "svelte-tailwind-sync",
3699
+ sourceTemplate: "svelte-tailwind-sync"
3700
+ }
1456
3701
  ]
1457
3702
  };
1458
3703
  case "svelte-tailwind":
@@ -1839,6 +4084,7 @@ function resolveKoraVersion() {
1839
4084
  var import_node_fs4 = require("fs");
1840
4085
  var import_node_path20 = require("path");
1841
4086
  var import_citty4 = require("citty");
4087
+ init_fs_helpers();
1842
4088
 
1843
4089
  // src/commands/deploy/adapters/adapter.ts
1844
4090
  var DEPLOY_PLATFORMS = [
@@ -1863,6 +4109,7 @@ var import_node_child_process5 = require("child_process");
1863
4109
  var import_node_fs3 = require("fs");
1864
4110
  var import_promises7 = require("fs/promises");
1865
4111
  var import_node_path10 = require("path");
4112
+ init_fs_helpers();
1866
4113
  async function buildClient(options) {
1867
4114
  const viteEntryPoint = await resolveProjectBinaryEntryPoint(options.projectRoot, "vite", "vite");
1868
4115
  if (!viteEntryPoint) {
@@ -1906,7 +4153,7 @@ async function patchSqliteWasmAssets(projectRoot, outDir) {
1906
4153
  }
1907
4154
  }
1908
4155
  async function runProcess(command, args, cwd) {
1909
- await new Promise((resolve11, reject) => {
4156
+ await new Promise((resolve12, reject) => {
1910
4157
  const child = (0, import_node_child_process5.spawn)(command, args, {
1911
4158
  cwd,
1912
4159
  stdio: "inherit",
@@ -1917,7 +4164,7 @@ async function runProcess(command, args, cwd) {
1917
4164
  });
1918
4165
  child.on("exit", (code) => {
1919
4166
  if (code === 0) {
1920
- resolve11();
4167
+ resolve12();
1921
4168
  return;
1922
4169
  }
1923
4170
  reject(new Error(`Client build failed with exit code ${String(code ?? "unknown")}.`));
@@ -2315,7 +4562,7 @@ var AwsEcsAdapter = class {
2315
4562
  };
2316
4563
  var NodeAwsCommandRunner = class {
2317
4564
  async run(command, args, cwd) {
2318
- return new Promise((resolve11) => {
4565
+ return new Promise((resolve12) => {
2319
4566
  const child = (0, import_node_child_process6.spawn)(command, args, {
2320
4567
  cwd,
2321
4568
  env: process.env,
@@ -2330,11 +4577,11 @@ var NodeAwsCommandRunner = class {
2330
4577
  stderr += chunk.toString("utf-8");
2331
4578
  });
2332
4579
  child.on("error", (error) => {
2333
- resolve11({ exitCode: 1, stdout, stderr: `${stderr}
4580
+ resolve12({ exitCode: 1, stdout, stderr: `${stderr}
2334
4581
  ${error.message}` });
2335
4582
  });
2336
4583
  child.on("exit", (code) => {
2337
- resolve11({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
4584
+ resolve12({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
2338
4585
  });
2339
4586
  });
2340
4587
  }
@@ -2662,7 +4909,7 @@ var AwsLightsailAdapter = class {
2662
4909
  };
2663
4910
  var NodeAwsLightsailCommandRunner = class {
2664
4911
  async run(command, args, cwd) {
2665
- return new Promise((resolve11) => {
4912
+ return new Promise((resolve12) => {
2666
4913
  const child = (0, import_node_child_process7.spawn)(command, args, {
2667
4914
  cwd,
2668
4915
  env: process.env,
@@ -2677,11 +4924,11 @@ var NodeAwsLightsailCommandRunner = class {
2677
4924
  stderr += chunk.toString("utf-8");
2678
4925
  });
2679
4926
  child.on("error", (error) => {
2680
- resolve11({ exitCode: 1, stdout, stderr: `${stderr}
4927
+ resolve12({ exitCode: 1, stdout, stderr: `${stderr}
2681
4928
  ${error.message}` });
2682
4929
  });
2683
4930
  child.on("exit", (code) => {
2684
- resolve11({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
4931
+ resolve12({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
2685
4932
  });
2686
4933
  });
2687
4934
  }
@@ -3006,7 +5253,7 @@ var FlyAdapter = class {
3006
5253
  };
3007
5254
  var NodeFlyCommandRunner = class {
3008
5255
  async run(command, args, cwd) {
3009
- return await new Promise((resolve11) => {
5256
+ return await new Promise((resolve12) => {
3010
5257
  const child = (0, import_node_child_process8.spawn)(command, args, {
3011
5258
  cwd,
3012
5259
  env: process.env,
@@ -3021,7 +5268,7 @@ var NodeFlyCommandRunner = class {
3021
5268
  stderr += chunk.toString("utf-8");
3022
5269
  });
3023
5270
  child.on("error", (error) => {
3024
- resolve11({
5271
+ resolve12({
3025
5272
  exitCode: 1,
3026
5273
  stdout,
3027
5274
  stderr: `${stderr}
@@ -3029,7 +5276,7 @@ ${error.message}`
3029
5276
  });
3030
5277
  });
3031
5278
  child.on("exit", (code) => {
3032
- resolve11({
5279
+ resolve12({
3033
5280
  exitCode: code ?? 1,
3034
5281
  stdout: stdout.trim(),
3035
5282
  stderr: stderr.trim()
@@ -3342,7 +5589,7 @@ var RailwayAdapter = class {
3342
5589
  };
3343
5590
  var NodeRailwayCommandRunner = class {
3344
5591
  async run(command, args, cwd) {
3345
- return await new Promise((resolve11) => {
5592
+ return await new Promise((resolve12) => {
3346
5593
  const child = (0, import_node_child_process9.spawn)(command, args, {
3347
5594
  cwd,
3348
5595
  env: process.env,
@@ -3357,7 +5604,7 @@ var NodeRailwayCommandRunner = class {
3357
5604
  stderr += chunk.toString("utf-8");
3358
5605
  });
3359
5606
  child.on("error", (error) => {
3360
- resolve11({
5607
+ resolve12({
3361
5608
  exitCode: 1,
3362
5609
  stdout,
3363
5610
  stderr: `${stderr}
@@ -3365,7 +5612,7 @@ ${error.message}`
3365
5612
  });
3366
5613
  });
3367
5614
  child.on("exit", (code) => {
3368
- resolve11({
5615
+ resolve12({
3369
5616
  exitCode: code ?? 1,
3370
5617
  stdout: stdout.trim(),
3371
5618
  stderr: stderr.trim()
@@ -4053,6 +6300,7 @@ var import_promises14 = require("fs/promises");
4053
6300
  var import_node_path22 = require("path");
4054
6301
  var import_node_path23 = require("path");
4055
6302
  var import_citty5 = require("citty");
6303
+ init_fs_helpers();
4056
6304
 
4057
6305
  // src/commands/dev/process-manager.ts
4058
6306
  var import_node_child_process10 = require("child_process");
@@ -4068,8 +6316,8 @@ var ProcessManager = class {
4068
6316
  let resolveExit;
4069
6317
  const runningProcess = {
4070
6318
  child,
4071
- exitPromise: new Promise((resolve11) => {
4072
- resolveExit = resolve11;
6319
+ exitPromise: new Promise((resolve12) => {
6320
+ resolveExit = resolve12;
4073
6321
  }),
4074
6322
  stdoutBuffer: "",
4075
6323
  stderrBuffer: ""
@@ -4136,8 +6384,8 @@ var ProcessManager = class {
4136
6384
  }
4137
6385
  };
4138
6386
  function delay(ms) {
4139
- return new Promise((resolve11) => {
4140
- setTimeout(resolve11, ms);
6387
+ return new Promise((resolve12) => {
6388
+ setTimeout(resolve12, ms);
4141
6389
  });
4142
6390
  }
4143
6391
 
@@ -4145,6 +6393,7 @@ function delay(ms) {
4145
6393
  var import_node_child_process11 = require("child_process");
4146
6394
  var import_node_fs5 = require("fs");
4147
6395
  var import_node_path21 = require("path");
6396
+ init_fs_helpers();
4148
6397
  var SchemaWatcher = class {
4149
6398
  constructor(config) {
4150
6399
  this.config = config;
@@ -4199,7 +6448,7 @@ var SchemaWatcher = class {
4199
6448
  }
4200
6449
  };
4201
6450
  async function spawnCommand(command, args, cwd) {
4202
- await new Promise((resolve11, reject) => {
6451
+ await new Promise((resolve12, reject) => {
4203
6452
  const child = (0, import_node_child_process11.spawn)(command, args, {
4204
6453
  cwd,
4205
6454
  stdio: ["ignore", "pipe", "pipe"],
@@ -4216,7 +6465,7 @@ async function spawnCommand(command, args, cwd) {
4216
6465
  });
4217
6466
  child.on("exit", (code) => {
4218
6467
  if (code === 0) {
4219
- resolve11();
6468
+ resolve12();
4220
6469
  return;
4221
6470
  }
4222
6471
  reject(new Error(`Type generation exited with code ${code ?? "unknown"}.`));
@@ -4326,8 +6575,8 @@ var devCommand = (0, import_citty5.defineCommand)({
4326
6575
  let schemaWatcher = null;
4327
6576
  let shuttingDown = false;
4328
6577
  let resolveFinished;
4329
- const finished = new Promise((resolve11) => {
4330
- resolveFinished = resolve11;
6578
+ const finished = new Promise((resolve12) => {
6579
+ resolveFinished = resolve12;
4331
6580
  });
4332
6581
  const onManagedProcessExit = () => {
4333
6582
  if (!processManager.hasRunning() && !shuttingDown) {
@@ -4527,10 +6776,13 @@ await new Promise(() => {});
4527
6776
 
4528
6777
  // src/commands/doctor/doctor-command.ts
4529
6778
  var import_citty6 = require("citty");
6779
+ init_fs_helpers();
4530
6780
 
4531
6781
  // src/commands/doctor/doctor-checks.ts
4532
6782
  var import_promises15 = require("fs/promises");
4533
6783
  var import_node_path24 = require("path");
6784
+ init_fs_helpers();
6785
+ init_schema_loader();
4534
6786
  var STATUS_ENDPOINT = "/__kora/status";
4535
6787
  async function runDoctorChecks(options) {
4536
6788
  const { projectRoot } = options;
@@ -4844,6 +7096,8 @@ var doctorCommand = (0, import_citty6.defineCommand)({
4844
7096
  var import_promises16 = require("fs/promises");
4845
7097
  var import_node_path25 = require("path");
4846
7098
  var import_citty7 = require("citty");
7099
+ init_fs_helpers();
7100
+ init_schema_loader();
4847
7101
 
4848
7102
  // src/commands/generate/hook-generator.ts
4849
7103
  function generateCollectionHooks(schema) {
@@ -5236,6 +7490,7 @@ var logsCommand = (0, import_citty8.defineCommand)({
5236
7490
  var import_promises17 = require("fs/promises");
5237
7491
  var import_node_path26 = require("path");
5238
7492
  var import_citty9 = require("citty");
7493
+ init_fs_helpers();
5239
7494
 
5240
7495
  // src/commands/migrate/migration-generator.ts
5241
7496
  var import_core2 = require("@korajs/core");
@@ -5826,6 +8081,7 @@ function generateOperationTransformModule(fromVersion, toVersion) {
5826
8081
  }
5827
8082
 
5828
8083
  // src/commands/migrate/migrate-command.ts
8084
+ init_schema_loader();
5829
8085
  var SNAPSHOT_PATH = "kora/schema.snapshot.json";
5830
8086
  var MIGRATIONS_DIR = "kora/migrations";
5831
8087
  var migrateCommand = (0, import_citty9.defineCommand)({
@@ -6288,8 +8544,131 @@ function printStatus(status, url, logger) {
6288
8544
  }
6289
8545
  }
6290
8546
 
8547
+ // src/commands/studio/studio-command.ts
8548
+ var import_node_fs8 = require("fs");
8549
+ var import_node_path29 = require("path");
8550
+ var import_citty11 = require("citty");
8551
+ var studioCommand = (0, import_citty11.defineCommand)({
8552
+ meta: {
8553
+ name: "studio",
8554
+ description: "Kora Studio: visualize records, operations, and sync \u2014 or run the sync lab"
8555
+ },
8556
+ args: {
8557
+ db: {
8558
+ type: "string",
8559
+ description: "Path to a Kora SQLite database file (file mode, read-only)"
8560
+ },
8561
+ lab: {
8562
+ type: "boolean",
8563
+ description: "Start the interactive multi-device sync laboratory"
8564
+ },
8565
+ connect: {
8566
+ type: "string",
8567
+ description: "Spectator mode: WebSocket URL of a live Kora sync server (read-only replica)"
8568
+ },
8569
+ token: {
8570
+ type: "string",
8571
+ description: "Spectator mode: bearer token for the sync auth handshake"
8572
+ },
8573
+ devices: {
8574
+ type: "string",
8575
+ description: "Lab mode: number of initial devices (default 2)"
8576
+ },
8577
+ schema: {
8578
+ type: "string",
8579
+ description: "Lab mode: path to a schema file (defaults to a built-in demo schema)"
8580
+ },
8581
+ port: {
8582
+ type: "string",
8583
+ description: "Port for the Studio UI (default 4321, 0 for a random free port)"
8584
+ }
8585
+ },
8586
+ async run({ args }) {
8587
+ const logger = createLogger();
8588
+ const port = args.port !== void 0 ? Number.parseInt(args.port, 10) : 4321;
8589
+ if (Number.isNaN(port) || port < 0 || port > 65535) {
8590
+ throw new Error(`Invalid port "${args.port}"`);
8591
+ }
8592
+ const { startStudioServer: startStudioServer2 } = await Promise.resolve().then(() => (init_studio_server(), studio_server_exports));
8593
+ if (typeof args.connect === "string") {
8594
+ if (typeof args.schema !== "string") {
8595
+ throw new Error(
8596
+ "Spectator mode needs your schema to materialize records: kora studio --connect wss://\u2026 --schema ./kora/schema.ts"
8597
+ );
8598
+ }
8599
+ const { loadSchemaDefinition: loadSchemaDefinition2 } = await Promise.resolve().then(() => (init_schema_loader(), schema_loader_exports));
8600
+ const schema = await loadSchemaDefinition2((0, import_node_path29.resolve)(process.cwd(), args.schema), process.cwd());
8601
+ const { SpectatorManager: SpectatorManager2 } = await Promise.resolve().then(() => (init_spectator_manager(), spectator_manager_exports));
8602
+ const spectator = new SpectatorManager2({
8603
+ url: args.connect,
8604
+ schema,
8605
+ ...typeof args.token === "string" ? { token: args.token } : {}
8606
+ });
8607
+ await spectator.start();
8608
+ const server2 = await startStudioServer2({ port, spectator });
8609
+ logger.banner();
8610
+ logger.success(`Kora Studio SPECTATOR running at ${server2.url}`);
8611
+ logger.step(`Watching ${args.connect} (read-only replica \u2014 nothing is ever pushed)`);
8612
+ logger.step("Press Ctrl+C to stop.");
8613
+ const shutdown2 = async () => {
8614
+ await server2.close();
8615
+ await spectator.close();
8616
+ process.exit(0);
8617
+ };
8618
+ process.on("SIGINT", () => void shutdown2());
8619
+ process.on("SIGTERM", () => void shutdown2());
8620
+ return;
8621
+ }
8622
+ if (args.lab) {
8623
+ const { LabManager: LabManager2, defaultLabSchema: defaultLabSchema2 } = await Promise.resolve().then(() => (init_lab_manager(), lab_manager_exports));
8624
+ let schema = defaultLabSchema2();
8625
+ if (typeof args.schema === "string") {
8626
+ const { loadSchemaDefinition: loadSchemaDefinition2 } = await Promise.resolve().then(() => (init_schema_loader(), schema_loader_exports));
8627
+ schema = await loadSchemaDefinition2((0, import_node_path29.resolve)(process.cwd(), args.schema), process.cwd());
8628
+ }
8629
+ const initialDevices = args.devices !== void 0 ? Math.max(1, Number.parseInt(args.devices, 10) || 2) : 2;
8630
+ const lab = new LabManager2(schema);
8631
+ await lab.start(initialDevices);
8632
+ const server2 = await startStudioServer2({ port, lab });
8633
+ logger.banner();
8634
+ logger.success(`Kora Studio LAB running at ${server2.url}`);
8635
+ logger.step(`${initialDevices} device(s) started against an in-process sync server.`);
8636
+ logger.step("Everything in the lab is throwaway \u2014 experiment freely.");
8637
+ logger.step("Press Ctrl+C to stop.");
8638
+ const shutdown2 = async () => {
8639
+ await server2.close();
8640
+ await lab.close();
8641
+ process.exit(0);
8642
+ };
8643
+ process.on("SIGINT", () => void shutdown2());
8644
+ process.on("SIGTERM", () => void shutdown2());
8645
+ return;
8646
+ }
8647
+ if (typeof args.db !== "string") {
8648
+ throw new Error(
8649
+ "Pass --db <path> to inspect a database, --lab for the sync laboratory, or --connect <wss url> for live spectator mode."
8650
+ );
8651
+ }
8652
+ const dbPath = (0, import_node_path29.resolve)(process.cwd(), args.db);
8653
+ if (!(0, import_node_fs8.existsSync)(dbPath)) {
8654
+ throw new Error(`Database file not found: ${dbPath}`);
8655
+ }
8656
+ const server = await startStudioServer2({ port, dbPath });
8657
+ logger.banner();
8658
+ logger.success(`Kora Studio running at ${server.url}`);
8659
+ logger.step(`Database: ${dbPath} (read-only)`);
8660
+ logger.step("Press Ctrl+C to stop.");
8661
+ const shutdown = async () => {
8662
+ await server.close();
8663
+ process.exit(0);
8664
+ };
8665
+ process.on("SIGINT", () => void shutdown());
8666
+ process.on("SIGTERM", () => void shutdown());
8667
+ }
8668
+ });
8669
+
6291
8670
  // src/bin.ts
6292
- var main = (0, import_citty11.defineCommand)({
8671
+ var main = (0, import_citty12.defineCommand)({
6293
8672
  meta: {
6294
8673
  name: "kora",
6295
8674
  description: "Kora.js \u2014 Offline-first application framework"
@@ -6304,8 +8683,9 @@ var main = (0, import_citty11.defineCommand)({
6304
8683
  generate: generateCommand,
6305
8684
  logs: logsCommand,
6306
8685
  migrate: migrateCommand,
6307
- status: statusCommand
8686
+ status: statusCommand,
8687
+ studio: studioCommand
6308
8688
  }
6309
8689
  });
6310
- (0, import_citty11.runMain)(main);
8690
+ (0, import_citty12.runMain)(main);
6311
8691
  //# sourceMappingURL=bin.cjs.map