@korajs/cli 0.5.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 (200) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin.cjs +2656 -207
  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-3WNFM3QB.js → chunk-B5YS4STN.js} +78 -99
  11. package/dist/chunk-B5YS4STN.js.map +1 -0
  12. package/dist/{chunk-PSRM56B7.js → chunk-BWTKRKNJ.js} +60 -7
  13. package/dist/chunk-BWTKRKNJ.js.map +1 -0
  14. package/dist/{chunk-SOTZIWIF.js → chunk-EOWLAAIV.js} +5 -3
  15. package/dist/{chunk-SOTZIWIF.js.map → chunk-EOWLAAIV.js.map} +1 -1
  16. package/dist/create.cjs +82 -7
  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.cjs +65 -3
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +3 -3
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +3 -2
  25. package/dist/lab-manager-KUDII6BT.js +280 -0
  26. package/dist/lab-manager-KUDII6BT.js.map +1 -0
  27. package/dist/schema-loader-GGHECMTM.js +9 -0
  28. package/dist/schema-loader-GGHECMTM.js.map +1 -0
  29. package/dist/spectator-manager-E2LH2P54.js +142 -0
  30. package/dist/spectator-manager-E2LH2P54.js.map +1 -0
  31. package/dist/studio-server-NIFKZI4F.js +1738 -0
  32. package/dist/studio-server-NIFKZI4F.js.map +1 -0
  33. package/package.json +8 -6
  34. package/templates/react-basic/AGENTS.md +87 -0
  35. package/templates/react-sync/AGENTS.md +87 -0
  36. package/templates/react-sync/src/App.tsx +8 -0
  37. package/templates/react-sync/src/index.css +11 -0
  38. package/templates/react-tailwind/AGENTS.md +87 -0
  39. package/templates/react-tailwind-sync/AGENTS.md +87 -0
  40. package/templates/react-tailwind-sync/src/App.tsx +28 -0
  41. package/templates/svelte-basic/.env.example +3 -0
  42. package/templates/svelte-basic/AGENTS.md +76 -0
  43. package/templates/svelte-basic/README.md.hbs +62 -0
  44. package/templates/svelte-basic/index.html.hbs +12 -0
  45. package/templates/svelte-basic/kora.config.ts +12 -0
  46. package/templates/svelte-basic/package.json.hbs +27 -0
  47. package/templates/svelte-basic/src/App.svelte +127 -0
  48. package/templates/svelte-basic/src/Root.svelte +14 -0
  49. package/templates/svelte-basic/src/index.css +287 -0
  50. package/templates/svelte-basic/src/kora-worker.ts +9 -0
  51. package/templates/svelte-basic/src/main.ts +16 -0
  52. package/templates/svelte-basic/src/modules/todos/todo.mutations.ts +25 -0
  53. package/templates/svelte-basic/src/modules/todos/todo.queries.ts +5 -0
  54. package/templates/svelte-basic/src/modules/todos/todo.schema.ts +10 -0
  55. package/templates/svelte-basic/src/modules/todos/useTodos.ts +25 -0
  56. package/templates/svelte-basic/src/schema.ts +9 -0
  57. package/templates/svelte-basic/src/vite-env.d.ts +12 -0
  58. package/templates/svelte-basic/svelte.config.js +5 -0
  59. package/templates/svelte-basic/tsconfig.json.hbs +14 -0
  60. package/templates/svelte-basic/vite.config.ts +52 -0
  61. package/templates/svelte-sync/.env.example +25 -0
  62. package/templates/svelte-sync/AGENTS.md +76 -0
  63. package/templates/svelte-sync/README.md.hbs +105 -0
  64. package/templates/svelte-sync/index.html.hbs +12 -0
  65. package/templates/svelte-sync/kora.config.ts +17 -0
  66. package/templates/svelte-sync/package.json.hbs +43 -0
  67. package/templates/svelte-sync/server.ts +90 -0
  68. package/templates/svelte-sync/src/App.svelte +150 -0
  69. package/templates/svelte-sync/src/Root.svelte +22 -0
  70. package/templates/svelte-sync/src/auth.ts +25 -0
  71. package/templates/svelte-sync/src/index.css +339 -0
  72. package/templates/svelte-sync/src/kora-worker.ts +9 -0
  73. package/templates/svelte-sync/src/main.ts +32 -0
  74. package/templates/svelte-sync/src/modules/todos/todo.mutations.ts +25 -0
  75. package/templates/svelte-sync/src/modules/todos/todo.queries.ts +5 -0
  76. package/templates/svelte-sync/src/modules/todos/todo.schema.ts +10 -0
  77. package/templates/svelte-sync/src/modules/todos/useTodos.ts +25 -0
  78. package/templates/svelte-sync/src/schema.ts +9 -0
  79. package/templates/svelte-sync/src/vite-env.d.ts +21 -0
  80. package/templates/svelte-sync/svelte.config.js +5 -0
  81. package/templates/svelte-sync/tsconfig.json.hbs +14 -0
  82. package/templates/svelte-sync/vite.config.ts +79 -0
  83. package/templates/svelte-tailwind/AGENTS.md +76 -0
  84. package/templates/svelte-tailwind/README.md.hbs +62 -0
  85. package/templates/svelte-tailwind/index.html.hbs +12 -0
  86. package/templates/svelte-tailwind/kora.config.ts +12 -0
  87. package/templates/svelte-tailwind/package.json.hbs +30 -0
  88. package/templates/svelte-tailwind/src/App.svelte +75 -0
  89. package/templates/svelte-tailwind/src/Root.svelte +14 -0
  90. package/templates/svelte-tailwind/src/index.css +7 -0
  91. package/templates/svelte-tailwind/src/kora-worker.ts +9 -0
  92. package/templates/svelte-tailwind/src/main.ts +16 -0
  93. package/templates/svelte-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  94. package/templates/svelte-tailwind/src/modules/todos/todo.queries.ts +5 -0
  95. package/templates/svelte-tailwind/src/modules/todos/todo.schema.ts +10 -0
  96. package/templates/svelte-tailwind/src/modules/todos/useTodos.ts +25 -0
  97. package/templates/svelte-tailwind/src/schema.ts +9 -0
  98. package/templates/svelte-tailwind/src/vite-env.d.ts +12 -0
  99. package/templates/svelte-tailwind/svelte.config.js +5 -0
  100. package/templates/svelte-tailwind/tsconfig.json.hbs +14 -0
  101. package/templates/svelte-tailwind/vite.config.ts +53 -0
  102. package/templates/svelte-tailwind-sync/.env.example +25 -0
  103. package/templates/svelte-tailwind-sync/AGENTS.md +76 -0
  104. package/templates/svelte-tailwind-sync/README.md.hbs +105 -0
  105. package/templates/svelte-tailwind-sync/index.html.hbs +12 -0
  106. package/templates/svelte-tailwind-sync/kora.config.ts +17 -0
  107. package/templates/svelte-tailwind-sync/package.json.hbs +46 -0
  108. package/templates/svelte-tailwind-sync/server.ts +90 -0
  109. package/templates/svelte-tailwind-sync/src/App.svelte +192 -0
  110. package/templates/svelte-tailwind-sync/src/Root.svelte +26 -0
  111. package/templates/svelte-tailwind-sync/src/auth.ts +25 -0
  112. package/templates/svelte-tailwind-sync/src/index.css +7 -0
  113. package/templates/svelte-tailwind-sync/src/kora-worker.ts +9 -0
  114. package/templates/svelte-tailwind-sync/src/main.ts +32 -0
  115. package/templates/svelte-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  116. package/templates/svelte-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  117. package/templates/svelte-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  118. package/templates/svelte-tailwind-sync/src/modules/todos/useTodos.ts +25 -0
  119. package/templates/svelte-tailwind-sync/src/schema.ts +9 -0
  120. package/templates/svelte-tailwind-sync/src/vite-env.d.ts +21 -0
  121. package/templates/svelte-tailwind-sync/svelte.config.js +5 -0
  122. package/templates/svelte-tailwind-sync/tsconfig.json.hbs +14 -0
  123. package/templates/svelte-tailwind-sync/vite.config.ts +80 -0
  124. package/templates/tauri-react/AGENTS.md +91 -0
  125. package/templates/vue-basic/AGENTS.md +84 -0
  126. package/templates/vue-basic/README.md.hbs +62 -0
  127. package/templates/vue-basic/index.html.hbs +12 -0
  128. package/templates/vue-basic/kora.config.ts +12 -0
  129. package/templates/vue-basic/package.json.hbs +26 -0
  130. package/templates/vue-basic/src/App.vue +117 -0
  131. package/templates/vue-basic/src/index.css +287 -0
  132. package/templates/vue-basic/src/kora-worker.ts +9 -0
  133. package/templates/vue-basic/src/main.ts +20 -0
  134. package/templates/vue-basic/src/modules/todos/todo.mutations.ts +25 -0
  135. package/templates/vue-basic/src/modules/todos/todo.queries.ts +5 -0
  136. package/templates/vue-basic/src/modules/todos/todo.schema.ts +10 -0
  137. package/templates/vue-basic/src/modules/todos/useTodos.ts +32 -0
  138. package/templates/vue-basic/src/schema.ts +9 -0
  139. package/templates/vue-basic/src/vite-env.d.ts +17 -0
  140. package/templates/vue-basic/tsconfig.json.hbs +15 -0
  141. package/templates/vue-basic/vite.config.ts +69 -0
  142. package/templates/vue-sync/.env.example +25 -0
  143. package/templates/vue-sync/AGENTS.md +84 -0
  144. package/templates/vue-sync/README.md.hbs +105 -0
  145. package/templates/vue-sync/index.html.hbs +12 -0
  146. package/templates/vue-sync/kora.config.ts +17 -0
  147. package/templates/vue-sync/package.json.hbs +42 -0
  148. package/templates/vue-sync/server.ts +90 -0
  149. package/templates/vue-sync/src/App.vue +135 -0
  150. package/templates/vue-sync/src/auth.ts +25 -0
  151. package/templates/vue-sync/src/index.css +339 -0
  152. package/templates/vue-sync/src/kora-worker.ts +9 -0
  153. package/templates/vue-sync/src/main.ts +47 -0
  154. package/templates/vue-sync/src/modules/todos/todo.mutations.ts +25 -0
  155. package/templates/vue-sync/src/modules/todos/todo.queries.ts +5 -0
  156. package/templates/vue-sync/src/modules/todos/todo.schema.ts +10 -0
  157. package/templates/vue-sync/src/modules/todos/useTodos.ts +32 -0
  158. package/templates/vue-sync/src/schema.ts +9 -0
  159. package/templates/vue-sync/src/vite-env.d.ts +26 -0
  160. package/templates/vue-sync/tsconfig.json.hbs +15 -0
  161. package/templates/vue-sync/vite.config.ts +79 -0
  162. package/templates/vue-tailwind/AGENTS.md +84 -0
  163. package/templates/vue-tailwind/README.md.hbs +62 -0
  164. package/templates/vue-tailwind/index.html.hbs +12 -0
  165. package/templates/vue-tailwind/kora.config.ts +12 -0
  166. package/templates/vue-tailwind/package.json.hbs +29 -0
  167. package/templates/vue-tailwind/src/App.vue +135 -0
  168. package/templates/vue-tailwind/src/index.css +7 -0
  169. package/templates/vue-tailwind/src/kora-worker.ts +9 -0
  170. package/templates/vue-tailwind/src/main.ts +31 -0
  171. package/templates/vue-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  172. package/templates/vue-tailwind/src/modules/todos/todo.queries.ts +5 -0
  173. package/templates/vue-tailwind/src/modules/todos/todo.schema.ts +10 -0
  174. package/templates/vue-tailwind/src/modules/todos/useTodos.ts +32 -0
  175. package/templates/vue-tailwind/src/schema.ts +9 -0
  176. package/templates/vue-tailwind/src/vite-env.d.ts +17 -0
  177. package/templates/vue-tailwind/tsconfig.json.hbs +15 -0
  178. package/templates/vue-tailwind/vite.config.ts +70 -0
  179. package/templates/vue-tailwind-sync/.env.example +25 -0
  180. package/templates/vue-tailwind-sync/AGENTS.md +84 -0
  181. package/templates/vue-tailwind-sync/README.md.hbs +105 -0
  182. package/templates/vue-tailwind-sync/index.html.hbs +12 -0
  183. package/templates/vue-tailwind-sync/kora.config.ts +17 -0
  184. package/templates/vue-tailwind-sync/package.json.hbs +45 -0
  185. package/templates/vue-tailwind-sync/server.ts +90 -0
  186. package/templates/vue-tailwind-sync/src/App.vue +245 -0
  187. package/templates/vue-tailwind-sync/src/auth.ts +25 -0
  188. package/templates/vue-tailwind-sync/src/index.css +7 -0
  189. package/templates/vue-tailwind-sync/src/kora-worker.ts +9 -0
  190. package/templates/vue-tailwind-sync/src/main.ts +56 -0
  191. package/templates/vue-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  192. package/templates/vue-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  193. package/templates/vue-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  194. package/templates/vue-tailwind-sync/src/modules/todos/useTodos.ts +32 -0
  195. package/templates/vue-tailwind-sync/src/schema.ts +9 -0
  196. package/templates/vue-tailwind-sync/src/vite-env.d.ts +26 -0
  197. package/templates/vue-tailwind-sync/tsconfig.json.hbs +15 -0
  198. package/templates/vue-tailwind-sync/vite.config.ts +80 -0
  199. package/dist/chunk-3WNFM3QB.js.map +0 -1
  200. package/dist/chunk-PSRM56B7.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
  ;
@@ -830,9 +3066,20 @@ var TEMPLATES = [
830
3066
  "react-tailwind",
831
3067
  "react-sync",
832
3068
  "react-basic",
3069
+ "vue-sync",
3070
+ "vue-basic",
3071
+ "vue-tailwind-sync",
3072
+ "vue-tailwind",
3073
+ "svelte-sync",
3074
+ "svelte-basic",
3075
+ "svelte-tailwind-sync",
3076
+ "svelte-tailwind",
833
3077
  "tauri-react"
834
3078
  ];
835
3079
 
3080
+ // src/commands/create/create-command.ts
3081
+ init_fs_helpers();
3082
+
836
3083
  // src/utils/package-manager.ts
837
3084
  var import_node_child_process3 = require("child_process");
838
3085
  function detectPackageManager() {
@@ -976,6 +3223,19 @@ async function findNearestAncestorWithEntry(startDir, entryName) {
976
3223
  function determineTemplateFromSelections(input) {
977
3224
  if (input.platform === "desktop-tauri") return "tauri-react";
978
3225
  const shouldSync = input.sync && input.db !== "none";
3226
+ if (input.framework === "vue") {
3227
+ if (input.tailwind && shouldSync) return "vue-tailwind-sync";
3228
+ if (input.tailwind && !shouldSync) return "vue-tailwind";
3229
+ return shouldSync ? "vue-sync" : "vue-basic";
3230
+ }
3231
+ if (input.framework === "svelte") {
3232
+ if (input.tailwind && shouldSync) return "svelte-tailwind-sync";
3233
+ if (input.tailwind && !shouldSync) return "svelte-tailwind";
3234
+ return shouldSync ? "svelte-sync" : "svelte-basic";
3235
+ }
3236
+ if (input.framework === "solid") {
3237
+ throw new Error("Solid templates are not available yet. Use react, vue, or svelte.");
3238
+ }
979
3239
  if (input.tailwind && shouldSync) return "react-tailwind-sync";
980
3240
  if (input.tailwind && !shouldSync) return "react-tailwind";
981
3241
  if (!input.tailwind && shouldSync) return "react-sync";
@@ -996,6 +3256,9 @@ function isDatabaseValue(value) {
996
3256
  function isDatabaseProviderValue(value) {
997
3257
  return value === "none" || value === "local" || value === "supabase" || value === "neon" || value === "railway" || value === "vercel-postgres" || value === "custom";
998
3258
  }
3259
+ function isSupportedWebFramework(framework) {
3260
+ return framework === "react" || framework === "vue" || framework === "svelte";
3261
+ }
999
3262
 
1000
3263
  // src/commands/create/preferences-flow.ts
1001
3264
  async function resolveCreatePreferencesFlow(params) {
@@ -1089,8 +3352,8 @@ async function resolveCreatePreferencesFlow(params) {
1089
3352
  } else if (!flags.useDefaults && !usedStoredPreferences) {
1090
3353
  effective.framework = await prompts.select("UI framework:", [
1091
3354
  { label: "React", value: "react" },
1092
- { label: "Vue (coming soon)", value: "vue", disabled: true },
1093
- { label: "Svelte (coming soon)", value: "svelte", disabled: true },
3355
+ { label: "Vue 3", value: "vue" },
3356
+ { label: "Svelte 5", value: "svelte" },
1094
3357
  { label: "Solid (coming soon)", value: "solid", disabled: true }
1095
3358
  ]);
1096
3359
  }
@@ -1155,6 +3418,7 @@ async function resolveCreatePreferencesFlow(params) {
1155
3418
  }
1156
3419
  const template = determineTemplateFromSelections({
1157
3420
  platform: effective.platform,
3421
+ framework: effective.framework,
1158
3422
  tailwind: effective.tailwind,
1159
3423
  sync: effective.sync,
1160
3424
  db: effective.db
@@ -1265,7 +3529,7 @@ async function applySyncProviderPreset(options) {
1265
3529
  await (0, import_promises5.writeFile)(readmePath, readmeTemplate, "utf-8");
1266
3530
  }
1267
3531
  function isSyncTemplate(template) {
1268
- return template === "react-sync" || template === "react-tailwind-sync" || template === "tauri-react";
3532
+ return template === "react-sync" || template === "react-tailwind-sync" || template === "vue-sync" || template === "vue-tailwind-sync" || template === "svelte-sync" || template === "svelte-tailwind-sync" || template === "tauri-react";
1269
3533
  }
1270
3534
  function getProviderDisplayName(provider) {
1271
3535
  switch (provider) {
@@ -1393,6 +3657,54 @@ function createCompatibilityLayerPlan(templateName) {
1393
3657
  authLayer
1394
3658
  ]
1395
3659
  };
3660
+ case "vue-sync":
3661
+ return {
3662
+ compatibilityTarget: templateName,
3663
+ layers: [{ category: "base", name: "vue-sync", sourceTemplate: "vue-sync" }]
3664
+ };
3665
+ case "vue-basic":
3666
+ return {
3667
+ compatibilityTarget: templateName,
3668
+ layers: [{ category: "base", name: "vue-basic", sourceTemplate: "vue-basic" }]
3669
+ };
3670
+ case "svelte-sync":
3671
+ return {
3672
+ compatibilityTarget: templateName,
3673
+ layers: [{ category: "base", name: "svelte-sync", sourceTemplate: "svelte-sync" }]
3674
+ };
3675
+ case "svelte-basic":
3676
+ return {
3677
+ compatibilityTarget: templateName,
3678
+ layers: [{ category: "base", name: "svelte-basic", sourceTemplate: "svelte-basic" }]
3679
+ };
3680
+ case "vue-tailwind-sync":
3681
+ return {
3682
+ compatibilityTarget: templateName,
3683
+ layers: [
3684
+ { category: "base", name: "vue-tailwind-sync", sourceTemplate: "vue-tailwind-sync" }
3685
+ ]
3686
+ };
3687
+ case "vue-tailwind":
3688
+ return {
3689
+ compatibilityTarget: templateName,
3690
+ layers: [{ category: "base", name: "vue-tailwind", sourceTemplate: "vue-tailwind" }]
3691
+ };
3692
+ case "svelte-tailwind-sync":
3693
+ return {
3694
+ compatibilityTarget: templateName,
3695
+ layers: [
3696
+ {
3697
+ category: "base",
3698
+ name: "svelte-tailwind-sync",
3699
+ sourceTemplate: "svelte-tailwind-sync"
3700
+ }
3701
+ ]
3702
+ };
3703
+ case "svelte-tailwind":
3704
+ return {
3705
+ compatibilityTarget: templateName,
3706
+ layers: [{ category: "base", name: "svelte-tailwind", sourceTemplate: "svelte-tailwind" }]
3707
+ };
1396
3708
  }
1397
3709
  }
1398
3710
  async function composeTemplateLayers(plan, targetDir, context) {
@@ -1455,7 +3767,7 @@ var createCommand = (0, import_citty3.defineCommand)({
1455
3767
  },
1456
3768
  template: {
1457
3769
  type: "string",
1458
- description: "Project template (react-tailwind-sync, react-tailwind, react-sync, react-basic, tauri-react)"
3770
+ description: "Project template (react-tailwind-sync, react-sync, vue-sync, svelte-sync, tauri-react, ...)"
1459
3771
  },
1460
3772
  pm: {
1461
3773
  type: "string",
@@ -1540,8 +3852,10 @@ var createCommand = (0, import_citty3.defineCommand)({
1540
3852
  prompts,
1541
3853
  store: preferenceStore
1542
3854
  });
1543
- if (selection.framework !== "react") {
1544
- logger.error(`Framework "${selection.framework}" is not available yet. Use "react".`);
3855
+ if (!isSupportedWebFramework(selection.framework)) {
3856
+ logger.error(
3857
+ `Framework "${selection.framework}" is not available yet. Use react, vue, or svelte.`
3858
+ );
1545
3859
  if (!useDefaults) {
1546
3860
  prompts.outro("Project creation aborted.");
1547
3861
  }
@@ -1686,7 +4000,7 @@ function isValidPackageManager(value) {
1686
4000
  return PACKAGE_MANAGERS.includes(value);
1687
4001
  }
1688
4002
  function isSyncTemplate2(template) {
1689
- return template === "react-sync" || template === "react-tailwind-sync" || template === "tauri-react";
4003
+ return template === "react-sync" || template === "react-tailwind-sync" || template === "vue-sync" || template === "vue-tailwind-sync" || template === "svelte-sync" || template === "svelte-tailwind-sync" || template === "tauri-react";
1690
4004
  }
1691
4005
  function formatDbProviderForLog(dbProvider) {
1692
4006
  switch (dbProvider) {
@@ -1770,6 +4084,7 @@ function resolveKoraVersion() {
1770
4084
  var import_node_fs4 = require("fs");
1771
4085
  var import_node_path20 = require("path");
1772
4086
  var import_citty4 = require("citty");
4087
+ init_fs_helpers();
1773
4088
 
1774
4089
  // src/commands/deploy/adapters/adapter.ts
1775
4090
  var DEPLOY_PLATFORMS = [
@@ -1794,6 +4109,7 @@ var import_node_child_process5 = require("child_process");
1794
4109
  var import_node_fs3 = require("fs");
1795
4110
  var import_promises7 = require("fs/promises");
1796
4111
  var import_node_path10 = require("path");
4112
+ init_fs_helpers();
1797
4113
  async function buildClient(options) {
1798
4114
  const viteEntryPoint = await resolveProjectBinaryEntryPoint(options.projectRoot, "vite", "vite");
1799
4115
  if (!viteEntryPoint) {
@@ -1837,7 +4153,7 @@ async function patchSqliteWasmAssets(projectRoot, outDir) {
1837
4153
  }
1838
4154
  }
1839
4155
  async function runProcess(command, args, cwd) {
1840
- await new Promise((resolve11, reject) => {
4156
+ await new Promise((resolve12, reject) => {
1841
4157
  const child = (0, import_node_child_process5.spawn)(command, args, {
1842
4158
  cwd,
1843
4159
  stdio: "inherit",
@@ -1848,7 +4164,7 @@ async function runProcess(command, args, cwd) {
1848
4164
  });
1849
4165
  child.on("exit", (code) => {
1850
4166
  if (code === 0) {
1851
- resolve11();
4167
+ resolve12();
1852
4168
  return;
1853
4169
  }
1854
4170
  reject(new Error(`Client build failed with exit code ${String(code ?? "unknown")}.`));
@@ -2246,7 +4562,7 @@ var AwsEcsAdapter = class {
2246
4562
  };
2247
4563
  var NodeAwsCommandRunner = class {
2248
4564
  async run(command, args, cwd) {
2249
- return new Promise((resolve11) => {
4565
+ return new Promise((resolve12) => {
2250
4566
  const child = (0, import_node_child_process6.spawn)(command, args, {
2251
4567
  cwd,
2252
4568
  env: process.env,
@@ -2261,11 +4577,11 @@ var NodeAwsCommandRunner = class {
2261
4577
  stderr += chunk.toString("utf-8");
2262
4578
  });
2263
4579
  child.on("error", (error) => {
2264
- resolve11({ exitCode: 1, stdout, stderr: `${stderr}
4580
+ resolve12({ exitCode: 1, stdout, stderr: `${stderr}
2265
4581
  ${error.message}` });
2266
4582
  });
2267
4583
  child.on("exit", (code) => {
2268
- resolve11({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
4584
+ resolve12({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
2269
4585
  });
2270
4586
  });
2271
4587
  }
@@ -2593,7 +4909,7 @@ var AwsLightsailAdapter = class {
2593
4909
  };
2594
4910
  var NodeAwsLightsailCommandRunner = class {
2595
4911
  async run(command, args, cwd) {
2596
- return new Promise((resolve11) => {
4912
+ return new Promise((resolve12) => {
2597
4913
  const child = (0, import_node_child_process7.spawn)(command, args, {
2598
4914
  cwd,
2599
4915
  env: process.env,
@@ -2608,11 +4924,11 @@ var NodeAwsLightsailCommandRunner = class {
2608
4924
  stderr += chunk.toString("utf-8");
2609
4925
  });
2610
4926
  child.on("error", (error) => {
2611
- resolve11({ exitCode: 1, stdout, stderr: `${stderr}
4927
+ resolve12({ exitCode: 1, stdout, stderr: `${stderr}
2612
4928
  ${error.message}` });
2613
4929
  });
2614
4930
  child.on("exit", (code) => {
2615
- resolve11({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
4931
+ resolve12({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
2616
4932
  });
2617
4933
  });
2618
4934
  }
@@ -2937,7 +5253,7 @@ var FlyAdapter = class {
2937
5253
  };
2938
5254
  var NodeFlyCommandRunner = class {
2939
5255
  async run(command, args, cwd) {
2940
- return await new Promise((resolve11) => {
5256
+ return await new Promise((resolve12) => {
2941
5257
  const child = (0, import_node_child_process8.spawn)(command, args, {
2942
5258
  cwd,
2943
5259
  env: process.env,
@@ -2952,7 +5268,7 @@ var NodeFlyCommandRunner = class {
2952
5268
  stderr += chunk.toString("utf-8");
2953
5269
  });
2954
5270
  child.on("error", (error) => {
2955
- resolve11({
5271
+ resolve12({
2956
5272
  exitCode: 1,
2957
5273
  stdout,
2958
5274
  stderr: `${stderr}
@@ -2960,7 +5276,7 @@ ${error.message}`
2960
5276
  });
2961
5277
  });
2962
5278
  child.on("exit", (code) => {
2963
- resolve11({
5279
+ resolve12({
2964
5280
  exitCode: code ?? 1,
2965
5281
  stdout: stdout.trim(),
2966
5282
  stderr: stderr.trim()
@@ -3273,7 +5589,7 @@ var RailwayAdapter = class {
3273
5589
  };
3274
5590
  var NodeRailwayCommandRunner = class {
3275
5591
  async run(command, args, cwd) {
3276
- return await new Promise((resolve11) => {
5592
+ return await new Promise((resolve12) => {
3277
5593
  const child = (0, import_node_child_process9.spawn)(command, args, {
3278
5594
  cwd,
3279
5595
  env: process.env,
@@ -3288,7 +5604,7 @@ var NodeRailwayCommandRunner = class {
3288
5604
  stderr += chunk.toString("utf-8");
3289
5605
  });
3290
5606
  child.on("error", (error) => {
3291
- resolve11({
5607
+ resolve12({
3292
5608
  exitCode: 1,
3293
5609
  stdout,
3294
5610
  stderr: `${stderr}
@@ -3296,7 +5612,7 @@ ${error.message}`
3296
5612
  });
3297
5613
  });
3298
5614
  child.on("exit", (code) => {
3299
- resolve11({
5615
+ resolve12({
3300
5616
  exitCode: code ?? 1,
3301
5617
  stdout: stdout.trim(),
3302
5618
  stderr: stderr.trim()
@@ -3984,6 +6300,7 @@ var import_promises14 = require("fs/promises");
3984
6300
  var import_node_path22 = require("path");
3985
6301
  var import_node_path23 = require("path");
3986
6302
  var import_citty5 = require("citty");
6303
+ init_fs_helpers();
3987
6304
 
3988
6305
  // src/commands/dev/process-manager.ts
3989
6306
  var import_node_child_process10 = require("child_process");
@@ -3999,8 +6316,8 @@ var ProcessManager = class {
3999
6316
  let resolveExit;
4000
6317
  const runningProcess = {
4001
6318
  child,
4002
- exitPromise: new Promise((resolve11) => {
4003
- resolveExit = resolve11;
6319
+ exitPromise: new Promise((resolve12) => {
6320
+ resolveExit = resolve12;
4004
6321
  }),
4005
6322
  stdoutBuffer: "",
4006
6323
  stderrBuffer: ""
@@ -4067,8 +6384,8 @@ var ProcessManager = class {
4067
6384
  }
4068
6385
  };
4069
6386
  function delay(ms) {
4070
- return new Promise((resolve11) => {
4071
- setTimeout(resolve11, ms);
6387
+ return new Promise((resolve12) => {
6388
+ setTimeout(resolve12, ms);
4072
6389
  });
4073
6390
  }
4074
6391
 
@@ -4076,6 +6393,7 @@ function delay(ms) {
4076
6393
  var import_node_child_process11 = require("child_process");
4077
6394
  var import_node_fs5 = require("fs");
4078
6395
  var import_node_path21 = require("path");
6396
+ init_fs_helpers();
4079
6397
  var SchemaWatcher = class {
4080
6398
  constructor(config) {
4081
6399
  this.config = config;
@@ -4130,7 +6448,7 @@ var SchemaWatcher = class {
4130
6448
  }
4131
6449
  };
4132
6450
  async function spawnCommand(command, args, cwd) {
4133
- await new Promise((resolve11, reject) => {
6451
+ await new Promise((resolve12, reject) => {
4134
6452
  const child = (0, import_node_child_process11.spawn)(command, args, {
4135
6453
  cwd,
4136
6454
  stdio: ["ignore", "pipe", "pipe"],
@@ -4147,7 +6465,7 @@ async function spawnCommand(command, args, cwd) {
4147
6465
  });
4148
6466
  child.on("exit", (code) => {
4149
6467
  if (code === 0) {
4150
- resolve11();
6468
+ resolve12();
4151
6469
  return;
4152
6470
  }
4153
6471
  reject(new Error(`Type generation exited with code ${code ?? "unknown"}.`));
@@ -4257,8 +6575,8 @@ var devCommand = (0, import_citty5.defineCommand)({
4257
6575
  let schemaWatcher = null;
4258
6576
  let shuttingDown = false;
4259
6577
  let resolveFinished;
4260
- const finished = new Promise((resolve11) => {
4261
- resolveFinished = resolve11;
6578
+ const finished = new Promise((resolve12) => {
6579
+ resolveFinished = resolve12;
4262
6580
  });
4263
6581
  const onManagedProcessExit = () => {
4264
6582
  if (!processManager.hasRunning() && !shuttingDown) {
@@ -4458,10 +6776,13 @@ await new Promise(() => {});
4458
6776
 
4459
6777
  // src/commands/doctor/doctor-command.ts
4460
6778
  var import_citty6 = require("citty");
6779
+ init_fs_helpers();
4461
6780
 
4462
6781
  // src/commands/doctor/doctor-checks.ts
4463
6782
  var import_promises15 = require("fs/promises");
4464
6783
  var import_node_path24 = require("path");
6784
+ init_fs_helpers();
6785
+ init_schema_loader();
4465
6786
  var STATUS_ENDPOINT = "/__kora/status";
4466
6787
  async function runDoctorChecks(options) {
4467
6788
  const { projectRoot } = options;
@@ -4775,6 +7096,8 @@ var doctorCommand = (0, import_citty6.defineCommand)({
4775
7096
  var import_promises16 = require("fs/promises");
4776
7097
  var import_node_path25 = require("path");
4777
7098
  var import_citty7 = require("citty");
7099
+ init_fs_helpers();
7100
+ init_schema_loader();
4778
7101
 
4779
7102
  // src/commands/generate/hook-generator.ts
4780
7103
  function generateCollectionHooks(schema) {
@@ -5167,6 +7490,7 @@ var logsCommand = (0, import_citty8.defineCommand)({
5167
7490
  var import_promises17 = require("fs/promises");
5168
7491
  var import_node_path26 = require("path");
5169
7492
  var import_citty9 = require("citty");
7493
+ init_fs_helpers();
5170
7494
 
5171
7495
  // src/commands/migrate/migration-generator.ts
5172
7496
  var import_core2 = require("@korajs/core");
@@ -5757,6 +8081,7 @@ function generateOperationTransformModule(fromVersion, toVersion) {
5757
8081
  }
5758
8082
 
5759
8083
  // src/commands/migrate/migrate-command.ts
8084
+ init_schema_loader();
5760
8085
  var SNAPSHOT_PATH = "kora/schema.snapshot.json";
5761
8086
  var MIGRATIONS_DIR = "kora/migrations";
5762
8087
  var migrateCommand = (0, import_citty9.defineCommand)({
@@ -6219,8 +8544,131 @@ function printStatus(status, url, logger) {
6219
8544
  }
6220
8545
  }
6221
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
+
6222
8670
  // src/bin.ts
6223
- var main = (0, import_citty11.defineCommand)({
8671
+ var main = (0, import_citty12.defineCommand)({
6224
8672
  meta: {
6225
8673
  name: "kora",
6226
8674
  description: "Kora.js \u2014 Offline-first application framework"
@@ -6235,8 +8683,9 @@ var main = (0, import_citty11.defineCommand)({
6235
8683
  generate: generateCommand,
6236
8684
  logs: logsCommand,
6237
8685
  migrate: migrateCommand,
6238
- status: statusCommand
8686
+ status: statusCommand,
8687
+ studio: studioCommand
6239
8688
  }
6240
8689
  });
6241
- (0, import_citty11.runMain)(main);
8690
+ (0, import_citty12.runMain)(main);
6242
8691
  //# sourceMappingURL=bin.cjs.map