@korajs/cli 0.6.0 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/bin.cjs +2582 -202
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +152 -96
- package/dist/bin.js.map +1 -1
- package/dist/chunk-5NI2FEQL.js +92 -0
- package/dist/chunk-5NI2FEQL.js.map +1 -0
- package/dist/chunk-6C4BHSRA.js +82 -0
- package/dist/chunk-6C4BHSRA.js.map +1 -0
- package/dist/{chunk-VLTPEATY.js → chunk-B5YS4STN.js} +9 -96
- package/dist/chunk-B5YS4STN.js.map +1 -0
- package/dist/{chunk-EEZNRI5W.js → chunk-BWTKRKNJ.js} +13 -5
- package/dist/{chunk-EEZNRI5W.js.map → chunk-BWTKRKNJ.js.map} +1 -1
- package/dist/{chunk-Q2FBCOQD.js → chunk-EOWLAAIV.js} +5 -3
- package/dist/{chunk-Q2FBCOQD.js.map → chunk-EOWLAAIV.js.map} +1 -1
- package/dist/create.cjs +8 -2
- package/dist/create.cjs.map +1 -1
- package/dist/create.js +3 -2
- package/dist/create.js.map +1 -1
- package/dist/index.js +3 -2
- package/dist/lab-manager-KUDII6BT.js +280 -0
- package/dist/lab-manager-KUDII6BT.js.map +1 -0
- package/dist/schema-loader-GGHECMTM.js +9 -0
- package/dist/schema-loader-GGHECMTM.js.map +1 -0
- package/dist/spectator-manager-E2LH2P54.js +142 -0
- package/dist/spectator-manager-E2LH2P54.js.map +1 -0
- package/dist/studio-server-NIFKZI4F.js +1738 -0
- package/dist/studio-server-NIFKZI4F.js.map +1 -0
- package/package.json +8 -6
- package/templates/react-basic/AGENTS.md +87 -0
- package/templates/react-sync/AGENTS.md +87 -0
- package/templates/react-sync/src/App.tsx +8 -0
- package/templates/react-sync/src/index.css +11 -0
- package/templates/react-tailwind/AGENTS.md +87 -0
- package/templates/react-tailwind-sync/AGENTS.md +87 -0
- package/templates/react-tailwind-sync/src/App.tsx +28 -0
- package/templates/svelte-basic/AGENTS.md +76 -0
- package/templates/svelte-basic/src/App.svelte +27 -31
- package/templates/svelte-basic/src/Root.svelte +4 -4
- package/templates/svelte-basic/src/modules/todos/useTodos.ts +1 -1
- package/templates/svelte-basic/vite.config.ts +1 -1
- package/templates/svelte-sync/AGENTS.md +76 -0
- package/templates/svelte-sync/src/App.svelte +31 -35
- package/templates/svelte-sync/src/Root.svelte +6 -6
- package/templates/svelte-sync/src/modules/todos/useTodos.ts +1 -1
- package/templates/svelte-tailwind/AGENTS.md +76 -0
- package/templates/svelte-tailwind/src/App.svelte +17 -21
- package/templates/svelte-tailwind/src/Root.svelte +4 -4
- package/templates/svelte-tailwind/src/modules/todos/useTodos.ts +1 -1
- package/templates/svelte-tailwind/vite.config.ts +2 -2
- package/templates/svelte-tailwind-sync/AGENTS.md +76 -0
- package/templates/svelte-tailwind-sync/src/App.svelte +52 -56
- package/templates/svelte-tailwind-sync/src/Root.svelte +6 -6
- package/templates/svelte-tailwind-sync/src/modules/todos/useTodos.ts +1 -1
- package/templates/svelte-tailwind-sync/vite.config.ts +1 -1
- package/templates/tauri-react/AGENTS.md +91 -0
- package/templates/vue-basic/AGENTS.md +84 -0
- package/templates/vue-basic/src/modules/todos/useTodos.ts +1 -1
- package/templates/vue-sync/AGENTS.md +84 -0
- package/templates/vue-sync/src/main.ts +5 -1
- package/templates/vue-sync/src/modules/todos/useTodos.ts +1 -1
- package/templates/vue-tailwind/AGENTS.md +84 -0
- package/templates/vue-tailwind/src/App.vue +1 -8
- package/templates/vue-tailwind/src/modules/todos/useTodos.ts +1 -1
- package/templates/vue-tailwind-sync/AGENTS.md +84 -0
- package/templates/vue-tailwind-sync/src/modules/todos/useTodos.ts +1 -1
- package/dist/chunk-VLTPEATY.js.map +0 -1
package/dist/bin.cjs
CHANGED
|
@@ -5,25 +5,2411 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
8
15
|
var __copyProps = (to, from, except, desc) => {
|
|
9
16
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
17
|
for (let key of __getOwnPropNames(from))
|
|
11
18
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
19
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
20
|
}
|
|
14
|
-
return to;
|
|
15
|
-
};
|
|
16
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
-
mod
|
|
23
|
-
));
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
|
|
32
|
+
// src/utils/fs-helpers.ts
|
|
33
|
+
async function directoryExists(path) {
|
|
34
|
+
try {
|
|
35
|
+
await (0, import_promises2.access)(path);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function findProjectRoot(startDir) {
|
|
42
|
+
let current = (0, import_node_path2.resolve)(startDir ?? process.cwd());
|
|
43
|
+
for (; ; ) {
|
|
44
|
+
const pkgPath = (0, import_node_path2.join)(current, "package.json");
|
|
45
|
+
try {
|
|
46
|
+
const content = await (0, import_promises2.readFile)(pkgPath, "utf-8");
|
|
47
|
+
const pkg = JSON.parse(content);
|
|
48
|
+
if (isKoraProject(pkg)) {
|
|
49
|
+
return current;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
const parent = (0, import_node_path2.dirname)(current);
|
|
54
|
+
if (parent === current) break;
|
|
55
|
+
current = parent;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
async function findSchemaFile(projectRoot) {
|
|
60
|
+
const candidates = [
|
|
61
|
+
(0, import_node_path2.join)(projectRoot, "src", "schema.ts"),
|
|
62
|
+
(0, import_node_path2.join)(projectRoot, "schema.ts"),
|
|
63
|
+
(0, import_node_path2.join)(projectRoot, "src", "schema.js"),
|
|
64
|
+
(0, import_node_path2.join)(projectRoot, "schema.js")
|
|
65
|
+
];
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
try {
|
|
68
|
+
await (0, import_promises2.access)(candidate);
|
|
69
|
+
return candidate;
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
async function hasTsxInstalled(projectRoot) {
|
|
76
|
+
try {
|
|
77
|
+
await (0, import_promises2.access)((0, import_node_path2.join)(projectRoot, "node_modules", "tsx", "package.json"));
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function resolveProjectBinaryEntryPoint(projectRoot, packageName, binaryName) {
|
|
84
|
+
const pkgJsonPath = (0, import_node_path2.join)(projectRoot, "node_modules", packageName, "package.json");
|
|
85
|
+
try {
|
|
86
|
+
const content = await (0, import_promises2.readFile)(pkgJsonPath, "utf-8");
|
|
87
|
+
const pkg = JSON.parse(content);
|
|
88
|
+
let binPath;
|
|
89
|
+
if (typeof pkg.bin === "string") {
|
|
90
|
+
binPath = pkg.bin;
|
|
91
|
+
} else if (typeof pkg.bin === "object" && pkg.bin !== null) {
|
|
92
|
+
binPath = pkg.bin[binaryName];
|
|
93
|
+
}
|
|
94
|
+
if (!binPath) return null;
|
|
95
|
+
const fullPath = (0, import_node_path2.join)(projectRoot, "node_modules", packageName, binPath);
|
|
96
|
+
await (0, import_promises2.access)(fullPath);
|
|
97
|
+
return fullPath;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function isKoraProject(pkg) {
|
|
103
|
+
if (typeof pkg !== "object" || pkg === null) return false;
|
|
104
|
+
const record = pkg;
|
|
105
|
+
return hasKoraDep(record.dependencies) || hasKoraDep(record.devDependencies);
|
|
106
|
+
}
|
|
107
|
+
function hasKoraDep(deps) {
|
|
108
|
+
if (typeof deps !== "object" || deps === null) return false;
|
|
109
|
+
return Object.keys(deps).some((key) => key === "kora" || key.startsWith("@korajs/"));
|
|
110
|
+
}
|
|
111
|
+
var import_promises2, import_node_path2;
|
|
112
|
+
var init_fs_helpers = __esm({
|
|
113
|
+
"src/utils/fs-helpers.ts"() {
|
|
114
|
+
"use strict";
|
|
115
|
+
import_promises2 = require("fs/promises");
|
|
116
|
+
import_node_path2 = require("path");
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// src/commands/migrate/schema-loader.ts
|
|
121
|
+
var schema_loader_exports = {};
|
|
122
|
+
__export(schema_loader_exports, {
|
|
123
|
+
loadSchemaDefinition: () => loadSchemaDefinition
|
|
124
|
+
});
|
|
125
|
+
async function loadSchemaDefinition(schemaPath, projectRoot) {
|
|
126
|
+
const ext = (0, import_node_path4.extname)(schemaPath);
|
|
127
|
+
const moduleValue = ext === ".ts" || ext === ".mts" || ext === ".cts" ? await loadTypeScriptModule(schemaPath, projectRoot) : await import(`${(0, import_node_url2.pathToFileURL)(schemaPath).href}?t=${Date.now()}-${Math.random()}`);
|
|
128
|
+
return extractSchema(moduleValue);
|
|
129
|
+
}
|
|
130
|
+
function extractSchema(value) {
|
|
131
|
+
if (typeof value !== "object" || value === null) {
|
|
132
|
+
throw new Error("Schema module must export an object.");
|
|
133
|
+
}
|
|
134
|
+
const moduleRecord = value;
|
|
135
|
+
const candidate = moduleRecord.default ?? moduleRecord;
|
|
136
|
+
if (!isSchemaDefinition(candidate)) {
|
|
137
|
+
throw new Error("Schema module must export a valid SchemaDefinition as default export.");
|
|
138
|
+
}
|
|
139
|
+
return candidate;
|
|
140
|
+
}
|
|
141
|
+
function isSchemaDefinition(value) {
|
|
142
|
+
if (typeof value !== "object" || value === null) return false;
|
|
143
|
+
const object = value;
|
|
144
|
+
return typeof object.version === "number" && typeof object.collections === "object" && object.collections !== null && typeof object.relations === "object" && object.relations !== null;
|
|
145
|
+
}
|
|
146
|
+
async function loadTypeScriptModule(schemaPath, projectRoot) {
|
|
147
|
+
if (!await hasTsxInstalled(projectRoot)) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Schema file is TypeScript (${schemaPath}) but local "tsx" was not found. Install tsx in the project.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const script = "const modulePath = process.argv[process.argv.length - 1];import('node:url').then(u => import(u.pathToFileURL(modulePath).href)).then(mod => { const v = mod.default ?? mod; process.stdout.write(JSON.stringify(v)) }).catch(e => { process.stderr.write(String(e)); process.exit(1) })";
|
|
153
|
+
const output = await runCommand2(
|
|
154
|
+
process.execPath,
|
|
155
|
+
["--import", "tsx", "--eval", script, schemaPath],
|
|
156
|
+
projectRoot
|
|
157
|
+
);
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(output);
|
|
160
|
+
} catch {
|
|
161
|
+
throw new Error(`Failed to parse schema module output for ${schemaPath}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function runCommand2(command, args, cwd) {
|
|
165
|
+
return await new Promise((resolve12, reject) => {
|
|
166
|
+
const child = (0, import_node_child_process2.spawn)(command, args, {
|
|
167
|
+
cwd,
|
|
168
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
169
|
+
env: process.env
|
|
170
|
+
});
|
|
171
|
+
let stdout = "";
|
|
172
|
+
let stderr = "";
|
|
173
|
+
child.stdout?.on("data", (chunk) => {
|
|
174
|
+
stdout += chunk.toString("utf-8");
|
|
175
|
+
});
|
|
176
|
+
child.stderr?.on("data", (chunk) => {
|
|
177
|
+
stderr += chunk.toString("utf-8");
|
|
178
|
+
});
|
|
179
|
+
child.on("error", (error) => {
|
|
180
|
+
reject(error);
|
|
181
|
+
});
|
|
182
|
+
child.on("exit", (code) => {
|
|
183
|
+
if (code === 0) {
|
|
184
|
+
resolve12(stdout.trim());
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
reject(
|
|
188
|
+
new Error(`Failed to load TypeScript schema (exit ${code ?? "unknown"}): ${stderr.trim()}`)
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
var import_node_child_process2, import_node_path4, import_node_url2;
|
|
194
|
+
var init_schema_loader = __esm({
|
|
195
|
+
"src/commands/migrate/schema-loader.ts"() {
|
|
196
|
+
"use strict";
|
|
197
|
+
import_node_child_process2 = require("child_process");
|
|
198
|
+
import_node_path4 = require("path");
|
|
199
|
+
import_node_url2 = require("url");
|
|
200
|
+
init_fs_helpers();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// src/commands/studio/db-reader.ts
|
|
205
|
+
function parseVersionStamp(raw) {
|
|
206
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const parts = raw.split(":");
|
|
210
|
+
if (parts.length < 3) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const wallTime = Number.parseInt(parts[0] ?? "", 10);
|
|
214
|
+
const logical = Number.parseInt(parts[1] ?? "", 10);
|
|
215
|
+
if (Number.isNaN(wallTime) || Number.isNaN(logical)) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return { wallTime, logical, nodeId: parts.slice(2).join(":") };
|
|
219
|
+
}
|
|
220
|
+
function parseFieldVersionsColumn(raw) {
|
|
221
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
222
|
+
return {};
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(raw);
|
|
226
|
+
const result = {};
|
|
227
|
+
for (const [field, value] of Object.entries(parsed)) {
|
|
228
|
+
const version = parseVersionStamp(value);
|
|
229
|
+
if (version) {
|
|
230
|
+
result[field] = version;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
} catch {
|
|
235
|
+
return {};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function safeIdentifier(name) {
|
|
239
|
+
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
|
|
240
|
+
throw new Error(`Unsafe SQL identifier: "${name}"`);
|
|
241
|
+
}
|
|
242
|
+
return name;
|
|
243
|
+
}
|
|
244
|
+
function parseJsonColumn(raw) {
|
|
245
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
return JSON.parse(raw);
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function parseJsonArrayColumn(raw) {
|
|
255
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(raw);
|
|
260
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
261
|
+
} catch {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
var KORA_TABLE_PREFIX, OPS_TABLE_PREFIX, META_COLUMNS, StudioDbReader;
|
|
266
|
+
var init_db_reader = __esm({
|
|
267
|
+
"src/commands/studio/db-reader.ts"() {
|
|
268
|
+
"use strict";
|
|
269
|
+
KORA_TABLE_PREFIX = "_kora_";
|
|
270
|
+
OPS_TABLE_PREFIX = "_kora_ops_";
|
|
271
|
+
META_COLUMNS = /* @__PURE__ */ new Set([
|
|
272
|
+
"id",
|
|
273
|
+
"_created_at",
|
|
274
|
+
"_updated_at",
|
|
275
|
+
"_version",
|
|
276
|
+
"_field_versions",
|
|
277
|
+
"_deleted"
|
|
278
|
+
]);
|
|
279
|
+
StudioDbReader = class _StudioDbReader {
|
|
280
|
+
constructor(db, dbPath) {
|
|
281
|
+
this.db = db;
|
|
282
|
+
this.dbPath = dbPath;
|
|
283
|
+
}
|
|
284
|
+
db;
|
|
285
|
+
dbPath;
|
|
286
|
+
/**
|
|
287
|
+
* Open a Kora database strictly read-only.
|
|
288
|
+
* @throws when the file does not exist or is not a Kora database
|
|
289
|
+
*/
|
|
290
|
+
static async open(dbPath) {
|
|
291
|
+
let Database;
|
|
292
|
+
try {
|
|
293
|
+
Database = (await import("better-sqlite3")).default;
|
|
294
|
+
} catch {
|
|
295
|
+
throw new Error(
|
|
296
|
+
'Kora Studio needs the "better-sqlite3" package to open database files. Install it in your project: pnpm add -D better-sqlite3'
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
300
|
+
const reader = new _StudioDbReader(db, dbPath);
|
|
301
|
+
if (reader.listCollections().length === 0 && !reader.hasKoraTables()) {
|
|
302
|
+
db.close();
|
|
303
|
+
throw new Error(`"${dbPath}" does not look like a Kora database (no _kora_* tables found).`);
|
|
304
|
+
}
|
|
305
|
+
return reader;
|
|
306
|
+
}
|
|
307
|
+
close() {
|
|
308
|
+
this.db.close();
|
|
309
|
+
}
|
|
310
|
+
hasKoraTables() {
|
|
311
|
+
const row = this.db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name LIKE ?").get(`${KORA_TABLE_PREFIX}%`);
|
|
312
|
+
return row.count > 0;
|
|
313
|
+
}
|
|
314
|
+
/** Collections = tables that have a companion _kora_ops_<name> table. */
|
|
315
|
+
listCollections() {
|
|
316
|
+
const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all();
|
|
317
|
+
const names = new Set(tables.map((t2) => t2.name));
|
|
318
|
+
const collections = [];
|
|
319
|
+
for (const { name } of tables) {
|
|
320
|
+
if (name.startsWith(OPS_TABLE_PREFIX)) {
|
|
321
|
+
const collection = name.slice(OPS_TABLE_PREFIX.length);
|
|
322
|
+
if (names.has(collection)) {
|
|
323
|
+
collections.push(collection);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return collections.sort();
|
|
328
|
+
}
|
|
329
|
+
overview() {
|
|
330
|
+
const collections = this.listCollections().map((name) => {
|
|
331
|
+
const table = safeIdentifier(name);
|
|
332
|
+
const live = this.db.prepare(`SELECT COUNT(*) as count FROM ${table} WHERE _deleted = 0`).get();
|
|
333
|
+
const tombstones = this.db.prepare(`SELECT COUNT(*) as count FROM ${table} WHERE _deleted = 1`).get();
|
|
334
|
+
const operations = this.db.prepare(`SELECT COUNT(*) as count FROM ${OPS_TABLE_PREFIX}${table}`).get();
|
|
335
|
+
const columnRows = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
336
|
+
return {
|
|
337
|
+
name,
|
|
338
|
+
liveRecords: live.count,
|
|
339
|
+
tombstones: tombstones.count,
|
|
340
|
+
operations: operations.count,
|
|
341
|
+
columns: columnRows.map((c) => c.name).filter((c) => !META_COLUMNS.has(c))
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
const versionVector = this.tableExists("_kora_version_vector") ? this.db.prepare(
|
|
345
|
+
"SELECT node_id as nodeId, sequence_number as sequenceNumber FROM _kora_version_vector ORDER BY node_id"
|
|
346
|
+
).all() : [];
|
|
347
|
+
const meta = this.tableExists("_kora_meta") ? this.db.prepare("SELECT key, value FROM _kora_meta ORDER BY key").all() : [];
|
|
348
|
+
const pendingSyncOps = this.tableExists("_kora_sync_queue") ? this.db.prepare("SELECT COUNT(*) as count FROM _kora_sync_queue").get().count : 0;
|
|
349
|
+
const auditTraces = this.tableExists("_kora_audit_traces") ? this.db.prepare("SELECT COUNT(*) as count FROM _kora_audit_traces").get().count : 0;
|
|
350
|
+
return { dbPath: this.dbPath, collections, versionVector, meta, pendingSyncOps, auditTraces };
|
|
351
|
+
}
|
|
352
|
+
records(collection, options = {}) {
|
|
353
|
+
const table = safeIdentifier(collection);
|
|
354
|
+
const limit = Math.min(Math.max(options.limit ?? 50, 1), 500);
|
|
355
|
+
const offset = Math.max(options.offset ?? 0, 0);
|
|
356
|
+
const clauses = [];
|
|
357
|
+
const params = [];
|
|
358
|
+
if (!options.includeDeleted) {
|
|
359
|
+
clauses.push("_deleted = 0");
|
|
360
|
+
}
|
|
361
|
+
if (options.search && options.search.trim().length > 0) {
|
|
362
|
+
const textColumns = this.db.prepare(`PRAGMA table_info(${table})`).all().filter((c) => c.type.toUpperCase().includes("TEXT") || c.name === "id");
|
|
363
|
+
const term = `%${options.search.trim()}%`;
|
|
364
|
+
const likes = textColumns.map((c) => {
|
|
365
|
+
params.push(term);
|
|
366
|
+
return `${safeIdentifier(c.name)} LIKE ?`;
|
|
367
|
+
});
|
|
368
|
+
if (likes.length > 0) {
|
|
369
|
+
clauses.push(`(${likes.join(" OR ")})`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
373
|
+
const total = this.db.prepare(`SELECT COUNT(*) as count FROM ${table} ${where}`).get(...params).count;
|
|
374
|
+
const rows = this.db.prepare(`SELECT * FROM ${table} ${where} ORDER BY _updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
375
|
+
return { records: rows.map((row) => this.toStudioRecord(row)), total };
|
|
376
|
+
}
|
|
377
|
+
/** Full operation log for one collection (capped), for replay and the DAG. */
|
|
378
|
+
allOperations(collection, cap = 5e3) {
|
|
379
|
+
const table = safeIdentifier(collection);
|
|
380
|
+
const rows = this.db.prepare(`SELECT * FROM ${OPS_TABLE_PREFIX}${table} ORDER BY timestamp ASC LIMIT ?`).all(Math.min(Math.max(cap, 1), 2e4));
|
|
381
|
+
return rows.map((row) => this.toStudioOperation(row));
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Cheap change fingerprint for live updates. SQLite's `data_version` PRAGMA
|
|
385
|
+
* increments whenever ANOTHER connection commits — exactly what a read-only
|
|
386
|
+
* watcher needs to know "something changed, refetch".
|
|
387
|
+
*/
|
|
388
|
+
fingerprint() {
|
|
389
|
+
const row = this.db.prepare("PRAGMA data_version").get();
|
|
390
|
+
return String(row.data_version);
|
|
391
|
+
}
|
|
392
|
+
/** Raw value of a single column on a record (for richtext preview decode). */
|
|
393
|
+
rawFieldValue(collection, recordId, field) {
|
|
394
|
+
const table = safeIdentifier(collection);
|
|
395
|
+
const column = safeIdentifier(field);
|
|
396
|
+
const row = this.db.prepare(`SELECT ${column} as value FROM ${table} WHERE id = ?`).get(recordId);
|
|
397
|
+
return row?.value;
|
|
398
|
+
}
|
|
399
|
+
record(collection, recordId) {
|
|
400
|
+
const table = safeIdentifier(collection);
|
|
401
|
+
const row = this.db.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(recordId);
|
|
402
|
+
return row ? this.toStudioRecord(row) : null;
|
|
403
|
+
}
|
|
404
|
+
/** Full operation history for one record, newest first. */
|
|
405
|
+
recordOperations(collection, recordId) {
|
|
406
|
+
const table = safeIdentifier(collection);
|
|
407
|
+
const rows = this.db.prepare(
|
|
408
|
+
`SELECT * FROM ${OPS_TABLE_PREFIX}${table} WHERE record_id = ? ORDER BY timestamp DESC`
|
|
409
|
+
).all(recordId);
|
|
410
|
+
return rows.map((row) => this.toStudioOperation(row));
|
|
411
|
+
}
|
|
412
|
+
operations(collection, options = {}) {
|
|
413
|
+
const table = safeIdentifier(collection);
|
|
414
|
+
const limit = Math.min(Math.max(options.limit ?? 50, 1), 500);
|
|
415
|
+
const offset = Math.max(options.offset ?? 0, 0);
|
|
416
|
+
const total = this.db.prepare(`SELECT COUNT(*) as count FROM ${OPS_TABLE_PREFIX}${table}`).get().count;
|
|
417
|
+
const rows = this.db.prepare(`SELECT * FROM ${OPS_TABLE_PREFIX}${table} ORDER BY timestamp DESC LIMIT ? OFFSET ?`).all(limit, offset);
|
|
418
|
+
return { operations: rows.map((row) => this.toStudioOperation(row)), total };
|
|
419
|
+
}
|
|
420
|
+
auditTraces(limit = 100) {
|
|
421
|
+
if (!this.tableExists("_kora_audit_traces")) {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
const rows = this.db.prepare(
|
|
425
|
+
`SELECT id, recorded_at, event_type, collection, record_id, field, strategy, tier, constraint_name
|
|
426
|
+
FROM _kora_audit_traces ORDER BY recorded_at DESC LIMIT ?`
|
|
427
|
+
).all(Math.min(Math.max(limit, 1), 500));
|
|
428
|
+
return rows.map((row) => ({
|
|
429
|
+
id: String(row.id),
|
|
430
|
+
recordedAt: Number(row.recorded_at),
|
|
431
|
+
eventType: String(row.event_type),
|
|
432
|
+
collection: String(row.collection),
|
|
433
|
+
recordId: String(row.record_id),
|
|
434
|
+
field: String(row.field),
|
|
435
|
+
strategy: String(row.strategy),
|
|
436
|
+
tier: Number(row.tier),
|
|
437
|
+
constraintName: row.constraint_name === null ? null : String(row.constraint_name)
|
|
438
|
+
}));
|
|
439
|
+
}
|
|
440
|
+
tableExists(name) {
|
|
441
|
+
const row = this.db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name = ?").get(name);
|
|
442
|
+
return row.count > 0;
|
|
443
|
+
}
|
|
444
|
+
toStudioRecord(row) {
|
|
445
|
+
const fields = {};
|
|
446
|
+
for (const [key, value] of Object.entries(row)) {
|
|
447
|
+
if (!META_COLUMNS.has(key)) {
|
|
448
|
+
fields[key] = value instanceof Uint8Array || Buffer.isBuffer(value) ? `<binary ${value.byteLength} bytes>` : value;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
id: String(row.id),
|
|
453
|
+
fields,
|
|
454
|
+
createdAt: Number(row._created_at),
|
|
455
|
+
updatedAt: Number(row._updated_at),
|
|
456
|
+
deleted: row._deleted === 1,
|
|
457
|
+
version: parseVersionStamp(row._version),
|
|
458
|
+
fieldVersions: parseFieldVersionsColumn(row._field_versions)
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
toStudioOperation(row) {
|
|
462
|
+
return {
|
|
463
|
+
id: String(row.id),
|
|
464
|
+
nodeId: String(row.node_id),
|
|
465
|
+
type: String(row.type),
|
|
466
|
+
recordId: String(row.record_id),
|
|
467
|
+
data: parseJsonColumn(row.data),
|
|
468
|
+
previousData: parseJsonColumn(row.previous_data),
|
|
469
|
+
timestamp: parseVersionStamp(row.timestamp),
|
|
470
|
+
sequenceNumber: Number(row.sequence_number),
|
|
471
|
+
causalDeps: parseJsonArrayColumn(row.causal_deps),
|
|
472
|
+
schemaVersion: Number(row.schema_version)
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// src/commands/studio/studio-replay.ts
|
|
480
|
+
function compareVersion(a, b) {
|
|
481
|
+
if (!a || !b) {
|
|
482
|
+
return a === b ? 0 : a ? 1 : -1;
|
|
483
|
+
}
|
|
484
|
+
if (a.wallTime !== b.wallTime) {
|
|
485
|
+
return a.wallTime - b.wallTime;
|
|
486
|
+
}
|
|
487
|
+
if (a.logical !== b.logical) {
|
|
488
|
+
return a.logical - b.logical;
|
|
489
|
+
}
|
|
490
|
+
return a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : 0;
|
|
491
|
+
}
|
|
492
|
+
function sortByHlc(ops) {
|
|
493
|
+
return [...ops].sort((a, b) => compareVersion(a.timestamp, b.timestamp));
|
|
494
|
+
}
|
|
495
|
+
function replayToOperation(ops, upToOpId) {
|
|
496
|
+
const ordered = sortByHlc(ops);
|
|
497
|
+
let cutIndex = ordered.length - 1;
|
|
498
|
+
if (upToOpId !== null) {
|
|
499
|
+
const found = ordered.findIndex((o) => o.id === upToOpId);
|
|
500
|
+
if (found === -1) {
|
|
501
|
+
throw new Error(`Operation "${upToOpId}" not found in the log`);
|
|
502
|
+
}
|
|
503
|
+
cutIndex = found;
|
|
504
|
+
}
|
|
505
|
+
const records = /* @__PURE__ */ new Map();
|
|
506
|
+
for (let i = 0; i <= cutIndex; i++) {
|
|
507
|
+
const operation = ordered[i];
|
|
508
|
+
if (!operation) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const writer = operation.timestamp ? shortNode(operation.timestamp.nodeId) : "?";
|
|
512
|
+
if (operation.type === "insert" && operation.data) {
|
|
513
|
+
const writers = {};
|
|
514
|
+
for (const field of Object.keys(operation.data)) {
|
|
515
|
+
writers[field] = writer;
|
|
516
|
+
}
|
|
517
|
+
records.set(operation.recordId, {
|
|
518
|
+
fields: { ...operation.data },
|
|
519
|
+
deleted: false,
|
|
520
|
+
writers
|
|
521
|
+
});
|
|
522
|
+
} else if (operation.type === "update" && operation.data) {
|
|
523
|
+
const existing = records.get(operation.recordId);
|
|
524
|
+
if (existing) {
|
|
525
|
+
for (const [field, value] of Object.entries(operation.data)) {
|
|
526
|
+
existing.fields[field] = value;
|
|
527
|
+
existing.writers[field] = writer;
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
const writers = {};
|
|
531
|
+
for (const field of Object.keys(operation.data)) {
|
|
532
|
+
writers[field] = writer;
|
|
533
|
+
}
|
|
534
|
+
records.set(operation.recordId, {
|
|
535
|
+
fields: { ...operation.data },
|
|
536
|
+
deleted: false,
|
|
537
|
+
writers
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
} else if (operation.type === "delete") {
|
|
541
|
+
const existing = records.get(operation.recordId);
|
|
542
|
+
if (existing) {
|
|
543
|
+
existing.deleted = true;
|
|
544
|
+
} else {
|
|
545
|
+
records.set(operation.recordId, { fields: {}, deleted: true, writers: {} });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
records: [...records.entries()].map(([id, r]) => ({
|
|
551
|
+
id,
|
|
552
|
+
fields: r.fields,
|
|
553
|
+
deleted: r.deleted,
|
|
554
|
+
lastWriterByField: r.writers
|
|
555
|
+
})),
|
|
556
|
+
appliedCount: cutIndex + 1,
|
|
557
|
+
totalCount: ordered.length,
|
|
558
|
+
cutOperation: ordered[cutIndex] ?? null
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function buildCausalDag(ops) {
|
|
562
|
+
const ordered = sortByHlc(ops);
|
|
563
|
+
const laneByNode = /* @__PURE__ */ new Map();
|
|
564
|
+
const lanes = [];
|
|
565
|
+
for (const operation of ordered) {
|
|
566
|
+
if (!laneByNode.has(operation.nodeId)) {
|
|
567
|
+
laneByNode.set(operation.nodeId, lanes.length);
|
|
568
|
+
lanes.push({ nodeId: operation.nodeId, shortNodeId: shortNode(operation.nodeId) });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const idSet = new Set(ordered.map((o) => o.id));
|
|
572
|
+
const nodes = ordered.map((operation, index) => ({
|
|
573
|
+
id: operation.id,
|
|
574
|
+
type: operation.type,
|
|
575
|
+
recordId: operation.recordId,
|
|
576
|
+
nodeId: operation.nodeId,
|
|
577
|
+
shortNodeId: shortNode(operation.nodeId),
|
|
578
|
+
sequenceNumber: operation.sequenceNumber,
|
|
579
|
+
wallTime: operation.timestamp?.wallTime ?? 0,
|
|
580
|
+
x: index,
|
|
581
|
+
lane: laneByNode.get(operation.nodeId) ?? 0,
|
|
582
|
+
dataPreview: operation.data ? JSON.stringify(operation.data).slice(0, 80) : ""
|
|
583
|
+
}));
|
|
584
|
+
const edges = [];
|
|
585
|
+
for (const operation of ordered) {
|
|
586
|
+
for (const dep of operation.causalDeps) {
|
|
587
|
+
if (idSet.has(dep)) {
|
|
588
|
+
edges.push({ from: dep, to: operation.id });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return { nodes, edges, lanes };
|
|
593
|
+
}
|
|
594
|
+
function shortNode(nodeId) {
|
|
595
|
+
return nodeId.length > 8 ? nodeId.slice(-8) : nodeId;
|
|
596
|
+
}
|
|
597
|
+
var init_studio_replay = __esm({
|
|
598
|
+
"src/commands/studio/studio-replay.ts"() {
|
|
599
|
+
"use strict";
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// src/commands/studio/studio-ui.ts
|
|
604
|
+
var STUDIO_HTML;
|
|
605
|
+
var init_studio_ui = __esm({
|
|
606
|
+
"src/commands/studio/studio-ui.ts"() {
|
|
607
|
+
"use strict";
|
|
608
|
+
STUDIO_HTML = `<!DOCTYPE html>
|
|
609
|
+
<html lang="en">
|
|
610
|
+
<head>
|
|
611
|
+
<meta charset="utf-8" />
|
|
612
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
613
|
+
<title>Kora Studio</title>
|
|
614
|
+
<link rel="stylesheet" href="/style.css" />
|
|
615
|
+
</head>
|
|
616
|
+
<body>
|
|
617
|
+
<header>
|
|
618
|
+
<div class="logo">kora <span>studio</span></div>
|
|
619
|
+
<div class="modebadge" id="modebadge"></div>
|
|
620
|
+
<div class="dbpath mono" id="dbpath"></div>
|
|
621
|
+
<div class="live" id="live"><div class="dot"></div><span>live</span></div>
|
|
622
|
+
<button id="refresh">Refresh</button>
|
|
623
|
+
</header>
|
|
624
|
+
<div class="tabs" id="tabs"></div>
|
|
625
|
+
<div class="layout">
|
|
626
|
+
<nav class="sidebar" id="sidebar"></nav>
|
|
627
|
+
<main id="main"><div class="empty">Loading\u2026</div></main>
|
|
628
|
+
</div>
|
|
629
|
+
<div class="drawer" id="drawer"></div>
|
|
630
|
+
<div class="dag-tip" id="dagtip"></div>
|
|
631
|
+
<script src="/app.js"></script>
|
|
632
|
+
</body>
|
|
633
|
+
</html>
|
|
634
|
+
`;
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// src/commands/studio/studio-ui-app.ts
|
|
639
|
+
var STUDIO_APP_JS;
|
|
640
|
+
var init_studio_ui_app = __esm({
|
|
641
|
+
"src/commands/studio/studio-ui-app.ts"() {
|
|
642
|
+
"use strict";
|
|
643
|
+
STUDIO_APP_JS = `
|
|
644
|
+
(function () {
|
|
645
|
+
'use strict';
|
|
646
|
+
|
|
647
|
+
// \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
648
|
+
var state = {
|
|
649
|
+
mode: 'file', // 'file' | 'lab'
|
|
650
|
+
tab: 'data', // data | ops | timetravel | merges | sync | lab
|
|
651
|
+
device: null, // selected lab device for data-ish tabs
|
|
652
|
+
devices: [],
|
|
653
|
+
collection: null,
|
|
654
|
+
page: 0,
|
|
655
|
+
includeDeleted: false,
|
|
656
|
+
search: '',
|
|
657
|
+
overview: null,
|
|
658
|
+
ttPosition: null, // time-travel slider position
|
|
659
|
+
ttPlaying: null,
|
|
660
|
+
labState: null,
|
|
661
|
+
feed: [],
|
|
662
|
+
deviceColors: {},
|
|
663
|
+
live: false
|
|
664
|
+
};
|
|
665
|
+
var PAGE = 50;
|
|
666
|
+
var refreshTimer = null;
|
|
667
|
+
|
|
668
|
+
// \u2500\u2500 Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
669
|
+
function qs(sel) { return document.querySelector(sel); }
|
|
670
|
+
function api(path) {
|
|
671
|
+
var sep = path.indexOf('?') === -1 ? '?' : '&';
|
|
672
|
+
var url = state.device ? path + sep + 'device=' + encodeURIComponent(state.device) : path;
|
|
673
|
+
return fetch(url).then(function (r) {
|
|
674
|
+
if (!r.ok) { return r.json().then(function (b) { throw new Error(b.error || r.statusText); }); }
|
|
675
|
+
return r.json();
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function post(path, body) {
|
|
679
|
+
return fetch(path, {
|
|
680
|
+
method: 'POST',
|
|
681
|
+
headers: { 'content-type': 'application/json' },
|
|
682
|
+
body: JSON.stringify(body || {})
|
|
683
|
+
}).then(function (r) {
|
|
684
|
+
if (!r.ok) { return r.json().then(function (b) { throw new Error(b.error || r.statusText); }); }
|
|
685
|
+
return r.json();
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function el(tag, attrs, children) {
|
|
689
|
+
var node = document.createElement(tag);
|
|
690
|
+
if (attrs) { Object.keys(attrs).forEach(function (k) {
|
|
691
|
+
if (k === 'class') node.className = attrs[k];
|
|
692
|
+
else if (k === 'text') node.textContent = attrs[k];
|
|
693
|
+
else if (k === 'html') node.innerHTML = attrs[k];
|
|
694
|
+
else if (k.slice(0, 2) === 'on') node.addEventListener(k.slice(2), attrs[k]);
|
|
695
|
+
else node.setAttribute(k, attrs[k]);
|
|
696
|
+
}); }
|
|
697
|
+
(children || []).forEach(function (c) { if (c) node.appendChild(c); });
|
|
698
|
+
return node;
|
|
699
|
+
}
|
|
700
|
+
function fmtTime(ms) {
|
|
701
|
+
if (!ms) return '\xB7';
|
|
702
|
+
var d = new Date(ms);
|
|
703
|
+
return d.toLocaleTimeString() + '.' + ('00' + d.getMilliseconds()).slice(-3);
|
|
704
|
+
}
|
|
705
|
+
function fmtDateTime(ms) {
|
|
706
|
+
if (!ms) return '\xB7';
|
|
707
|
+
var d = new Date(ms);
|
|
708
|
+
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString();
|
|
709
|
+
}
|
|
710
|
+
function shortNode(id) { return id && id.length > 8 ? id.slice(-8) : (id || '\xB7'); }
|
|
711
|
+
function shortId(id) { return id && id.length > 13 ? id.slice(0, 13) + '\u2026' : (id || ''); }
|
|
712
|
+
function fmtVal(v) {
|
|
713
|
+
if (v === null || v === undefined) return 'null';
|
|
714
|
+
if (typeof v === 'object') return JSON.stringify(v);
|
|
715
|
+
return String(v);
|
|
716
|
+
}
|
|
717
|
+
function deviceColor(name) {
|
|
718
|
+
if (!(name in state.deviceColors)) {
|
|
719
|
+
state.deviceColors[name] = 'dev-c' + (Object.keys(state.deviceColors).length % 6);
|
|
720
|
+
}
|
|
721
|
+
return state.deviceColors[name];
|
|
722
|
+
}
|
|
723
|
+
function debounceRefresh() {
|
|
724
|
+
if (refreshTimer) clearTimeout(refreshTimer);
|
|
725
|
+
refreshTimer = setTimeout(function () { render(false); }, 250);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// \u2500\u2500 Boot + live events \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
729
|
+
fetch('/api/mode').then(function (r) { return r.json(); }).then(function (info) {
|
|
730
|
+
state.mode = info.mode;
|
|
731
|
+
state.devices = info.devices || [];
|
|
732
|
+
if (state.mode === 'lab') { state.tab = 'lab'; state.device = state.devices[0] || null; }
|
|
733
|
+
qs('#modebadge').textContent = state.mode;
|
|
734
|
+
if (info.spectator) {
|
|
735
|
+
qs('#dbpath').textContent = '\u21C4 ' + info.spectator.url +
|
|
736
|
+
(info.spectator.connected ? ' \xB7 connected' : ' \xB7 connecting\u2026') +
|
|
737
|
+
' \xB7 ' + info.spectator.operationsReceived + ' ops received';
|
|
738
|
+
}
|
|
739
|
+
connectEvents();
|
|
740
|
+
render(true);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
function connectEvents() {
|
|
744
|
+
var source = new EventSource('/api/events');
|
|
745
|
+
source.addEventListener('hello', function () {
|
|
746
|
+
state.live = true; qs('#live').className = 'live on';
|
|
747
|
+
});
|
|
748
|
+
source.addEventListener('change', function () {
|
|
749
|
+
if (state.tab !== 'timetravel') debounceRefresh();
|
|
750
|
+
});
|
|
751
|
+
source.addEventListener('lab', function (event) {
|
|
752
|
+
var data = JSON.parse(event.data);
|
|
753
|
+
state.feed.push(data);
|
|
754
|
+
if (state.feed.length > 400) state.feed.shift();
|
|
755
|
+
appendFeedLine(data);
|
|
756
|
+
});
|
|
757
|
+
source.addEventListener('spectator', function (event) {
|
|
758
|
+
var data = JSON.parse(event.data);
|
|
759
|
+
data.device = 'server';
|
|
760
|
+
state.feed.push(data);
|
|
761
|
+
if (state.feed.length > 400) state.feed.shift();
|
|
762
|
+
appendFeedLine(data);
|
|
763
|
+
// Keep the header status fresh as ops stream in.
|
|
764
|
+
fetch('/api/spectator/status').then(function (r) { return r.json(); }).then(function (s) {
|
|
765
|
+
qs('#dbpath').textContent = '\u21C4 ' + s.url +
|
|
766
|
+
(s.connected ? ' \xB7 connected' : ' \xB7 reconnecting\u2026') +
|
|
767
|
+
' \xB7 ' + s.operationsReceived + ' ops received';
|
|
768
|
+
}).catch(function () {});
|
|
769
|
+
});
|
|
770
|
+
source.onerror = function () {
|
|
771
|
+
state.live = false; qs('#live').className = 'live';
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// \u2500\u2500 Tabs + chrome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
776
|
+
function renderTabs() {
|
|
777
|
+
var tabs = qs('#tabs');
|
|
778
|
+
tabs.textContent = '';
|
|
779
|
+
var defs = [];
|
|
780
|
+
if (state.mode === 'lab') defs.push(['lab', 'Lab']);
|
|
781
|
+
defs.push(['data', 'Data'], ['ops', 'Operations'], ['timetravel', 'Time Travel'], ['merges', 'Merges'], ['sync', 'Sync']);
|
|
782
|
+
defs.forEach(function (d) {
|
|
783
|
+
tabs.appendChild(el('div', {
|
|
784
|
+
class: 'tab' + (state.tab === d[0] ? ' active' : ''),
|
|
785
|
+
text: d[1],
|
|
786
|
+
onclick: function () { state.tab = d[0]; state.page = 0; closeDrawer(); render(true); }
|
|
787
|
+
}));
|
|
788
|
+
});
|
|
789
|
+
tabs.appendChild(el('div', { class: 'spacer' }));
|
|
790
|
+
if (state.mode === 'lab' && state.tab !== 'lab') {
|
|
791
|
+
var pick = el('select', { onchange: function () { state.device = pick.value; state.page = 0; render(true); } },
|
|
792
|
+
state.devices.map(function (d) {
|
|
793
|
+
var o = el('option', { value: d, text: d });
|
|
794
|
+
if (d === state.device) o.selected = true;
|
|
795
|
+
return o;
|
|
796
|
+
}));
|
|
797
|
+
tabs.appendChild(el('div', { class: 'devicepick' }, [el('span', { text: 'device:' }), pick]));
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function renderSidebar() {
|
|
802
|
+
var nav = qs('#sidebar');
|
|
803
|
+
nav.textContent = '';
|
|
804
|
+
if (state.tab === 'lab' || state.tab === 'sync' || state.tab === 'merges') {
|
|
805
|
+
nav.style.display = 'none';
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
nav.style.display = '';
|
|
809
|
+
nav.appendChild(el('h3', { text: 'Collections' }));
|
|
810
|
+
(state.overview ? state.overview.collections : []).forEach(function (c) {
|
|
811
|
+
nav.appendChild(el('div', {
|
|
812
|
+
class: 'item' + (state.collection === c.name ? ' active' : ''),
|
|
813
|
+
onclick: function () { state.collection = c.name; state.page = 0; closeDrawer(); render(false); }
|
|
814
|
+
}, [
|
|
815
|
+
el('span', { text: c.name }),
|
|
816
|
+
el('span', { class: 'count', text: String(c.liveRecords) + (c.tombstones ? ' +' + c.tombstones + '\u2020' : '') })
|
|
817
|
+
]));
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// \u2500\u2500 Main render \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
822
|
+
function render(full) {
|
|
823
|
+
renderTabs();
|
|
824
|
+
if (state.tab === 'lab') { renderSidebar(); renderLab(full); return; }
|
|
825
|
+
api('/api/overview').then(function (overview) {
|
|
826
|
+
state.overview = overview;
|
|
827
|
+
qs('#dbpath').textContent = overview.dbPath || '';
|
|
828
|
+
if (!state.collection || !overview.collections.some(function (c) { return c.name === state.collection; })) {
|
|
829
|
+
state.collection = overview.collections.length ? overview.collections[0].name : null;
|
|
830
|
+
}
|
|
831
|
+
renderSidebar();
|
|
832
|
+
var main = qs('#main');
|
|
833
|
+
main.textContent = '';
|
|
834
|
+
if (state.tab === 'data') return renderData(main);
|
|
835
|
+
if (state.tab === 'ops') return renderOps(main);
|
|
836
|
+
if (state.tab === 'timetravel') return renderTimeTravel(main);
|
|
837
|
+
if (state.tab === 'merges') return renderMerges(main);
|
|
838
|
+
if (state.tab === 'sync') return renderSync(main);
|
|
839
|
+
}).catch(function (e) {
|
|
840
|
+
qs('#main').innerHTML = '<div class="error">' + e.message + '</div>';
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function toolbar(main, title, extra) {
|
|
845
|
+
main.appendChild(el('div', { class: 'toolbar' }, [el('h2', { text: title })].concat(extra || [])));
|
|
846
|
+
}
|
|
847
|
+
function pager(main, total) {
|
|
848
|
+
var pages = Math.max(1, Math.ceil(total / PAGE));
|
|
849
|
+
main.appendChild(el('div', { class: 'pager' }, [
|
|
850
|
+
el('button', { text: '\u2039 Prev', onclick: function () { if (state.page > 0) { state.page--; render(false); } } }),
|
|
851
|
+
el('span', { text: 'page ' + (state.page + 1) + ' / ' + pages + ' \xB7 ' + total + ' total' }),
|
|
852
|
+
el('button', { text: 'Next \u203A', onclick: function () { if (state.page < pages - 1) { state.page++; render(false); } } })
|
|
853
|
+
]));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// \u2500\u2500 Data tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
857
|
+
function renderData(main) {
|
|
858
|
+
if (!state.collection) { main.appendChild(el('div', { class: 'empty', text: 'No collections.' })); return; }
|
|
859
|
+
var searchBox = el('input', { type: 'text', placeholder: 'search\u2026', value: state.search });
|
|
860
|
+
searchBox.oninput = function () { state.search = searchBox.value; state.page = 0; debounceRefresh(); };
|
|
861
|
+
var deletedToggle = el('label', {}, [
|
|
862
|
+
(function () { var cb = el('input', { type: 'checkbox' }); cb.checked = state.includeDeleted;
|
|
863
|
+
cb.onchange = function () { state.includeDeleted = cb.checked; state.page = 0; render(false); }; return cb; })(),
|
|
864
|
+
el('span', { text: 'tombstones' })
|
|
865
|
+
]);
|
|
866
|
+
toolbar(main, state.collection, [searchBox, deletedToggle]);
|
|
867
|
+
|
|
868
|
+
var q = '/api/collections/' + state.collection + '/records?limit=' + PAGE +
|
|
869
|
+
'&offset=' + (state.page * PAGE) + '&includeDeleted=' + state.includeDeleted +
|
|
870
|
+
(state.search ? '&search=' + encodeURIComponent(state.search) : '');
|
|
871
|
+
api(q).then(function (data) {
|
|
872
|
+
var info = state.overview.collections.filter(function (c) { return c.name === state.collection; })[0];
|
|
873
|
+
var columns = info ? info.columns : [];
|
|
874
|
+
if (!data.records.length) { main.appendChild(el('div', { class: 'empty', text: 'No records.' })); return; }
|
|
875
|
+
var thead = el('tr', {}, [el('th', { text: 'id' })].concat(
|
|
876
|
+
columns.map(function (c) { return el('th', { text: c }); }),
|
|
877
|
+
[el('th', { text: 'updated' })]
|
|
878
|
+
));
|
|
879
|
+
var tbody = el('tbody', {}, data.records.map(function (r) {
|
|
880
|
+
return el('tr', { class: 'clickable' + (r.deleted ? ' tombstone' : ''), onclick: function () { openRecord(r.id); } },
|
|
881
|
+
[el('td', { class: 'mono', text: shortId(r.id) })].concat(
|
|
882
|
+
columns.map(function (c) { return el('td', { text: fmtVal(r.fields[c]).slice(0, 60) }); }),
|
|
883
|
+
[el('td', { class: 'mono', text: fmtDateTime(r.updatedAt) })]
|
|
884
|
+
));
|
|
885
|
+
}));
|
|
886
|
+
main.appendChild(el('table', { class: 'grid' }, [el('thead', {}, [thead]), tbody]));
|
|
887
|
+
pager(main, data.total);
|
|
888
|
+
}).catch(showError(main));
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// \u2500\u2500 Record drawer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
892
|
+
function openRecord(id) {
|
|
893
|
+
api('/api/collections/' + state.collection + '/records/' + encodeURIComponent(id)).then(function (data) {
|
|
894
|
+
var drawer = qs('#drawer');
|
|
895
|
+
drawer.textContent = '';
|
|
896
|
+
var r = data.record;
|
|
897
|
+
drawer.appendChild(el('h3', {}, [
|
|
898
|
+
el('span', { class: 'mono', text: r.id }),
|
|
899
|
+
el('span', { class: 'close', text: '\u2715', onclick: closeDrawer })
|
|
900
|
+
]));
|
|
901
|
+
if (r.deleted) drawer.appendChild(el('span', { class: 'badge delete', text: 'tombstone' }));
|
|
902
|
+
|
|
903
|
+
drawer.appendChild(el('h4', { text: 'Fields \u2014 with last writer per field' }));
|
|
904
|
+
Object.keys(r.fields).forEach(function (f) {
|
|
905
|
+
var fv = r.fieldVersions[f];
|
|
906
|
+
var val = fmtVal(r.fields[f]);
|
|
907
|
+
var preview = data.richtextPreviews && data.richtextPreviews[f];
|
|
908
|
+
drawer.appendChild(el('div', { class: 'fieldrow' }, [
|
|
909
|
+
el('div', { class: 'fname mono', text: f }),
|
|
910
|
+
el('div', { class: 'fval', text: val }),
|
|
911
|
+
preview ? el('div', { class: 'preview', text: '\u201C' + preview + '\u201D' }) : null,
|
|
912
|
+
fv ? el('span', { class: 'chip' }, [
|
|
913
|
+
el('span', { text: 'last writer' }),
|
|
914
|
+
el('b', { class: 'mono', text: shortNode(fv.nodeId) }),
|
|
915
|
+
el('span', { class: 'mono', text: fmtDateTime(fv.wallTime) })
|
|
916
|
+
]) : el('span', { class: 'chip', text: 'no field version' })
|
|
917
|
+
]));
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
drawer.appendChild(el('h4', { text: 'Causal graph for this record' }));
|
|
921
|
+
var dagBox = el('div', { class: 'dagwrap' });
|
|
922
|
+
drawer.appendChild(dagBox);
|
|
923
|
+
api('/api/collections/' + state.collection + '/dag?record=' + encodeURIComponent(id)).then(function (dag) {
|
|
924
|
+
renderDag(dagBox, dag, 500);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
drawer.appendChild(el('h4', { text: 'Operation history (newest first)' }));
|
|
928
|
+
if (!data.operations.length) drawer.appendChild(el('div', { class: 'empty', text: 'No operations (compacted?).' }));
|
|
929
|
+
data.operations.forEach(function (o) {
|
|
930
|
+
var body = el('div', { class: 'op ' + o.type }, [
|
|
931
|
+
el('div', {}, [
|
|
932
|
+
el('span', { class: 'badge ' + o.type, text: o.type }),
|
|
933
|
+
el('span', { class: 'meta', text: ' ' + shortNode(o.nodeId) + ' \xB7 seq ' + o.sequenceNumber + ' \xB7 ' + (o.timestamp ? fmtDateTime(o.timestamp.wallTime) : '\xB7') })
|
|
934
|
+
])
|
|
935
|
+
]);
|
|
936
|
+
if (o.data) {
|
|
937
|
+
var pre = el('pre', { class: 'mono' });
|
|
938
|
+
pre.textContent = JSON.stringify(o.data, null, 1);
|
|
939
|
+
body.appendChild(pre);
|
|
940
|
+
}
|
|
941
|
+
drawer.appendChild(body);
|
|
942
|
+
});
|
|
943
|
+
drawer.className = 'drawer open';
|
|
944
|
+
}).catch(function (e) { alertError(e); });
|
|
945
|
+
}
|
|
946
|
+
function closeDrawer() { qs('#drawer').className = 'drawer'; }
|
|
947
|
+
|
|
948
|
+
// \u2500\u2500 Operations tab (DAG + table) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
949
|
+
function renderOps(main) {
|
|
950
|
+
if (!state.collection) { main.appendChild(el('div', { class: 'empty', text: 'No collections.' })); return; }
|
|
951
|
+
toolbar(main, state.collection + ' \u2014 operation log');
|
|
952
|
+
var dagBox = el('div', { class: 'dagwrap' });
|
|
953
|
+
main.appendChild(dagBox);
|
|
954
|
+
api('/api/collections/' + state.collection + '/dag?limit=120').then(function (dag) {
|
|
955
|
+
renderDag(dagBox, dag, null);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
api('/api/collections/' + state.collection + '/ops?limit=' + PAGE + '&offset=' + (state.page * PAGE)).then(function (data) {
|
|
959
|
+
if (!data.operations.length) { main.appendChild(el('div', { class: 'empty', text: 'Empty (compacted?).' })); return; }
|
|
960
|
+
var thead = el('tr', {}, ['type', 'record', 'node', 'seq', 'time', 'data'].map(function (h) { return el('th', { text: h }); }));
|
|
961
|
+
var tbody = el('tbody', {}, data.operations.map(function (o) {
|
|
962
|
+
return el('tr', { class: 'clickable', onclick: function () { openRecord(o.recordId); } }, [
|
|
963
|
+
el('td', {}, [el('span', { class: 'badge ' + o.type, text: o.type })]),
|
|
964
|
+
el('td', { class: 'mono', text: shortId(o.recordId) }),
|
|
965
|
+
el('td', { class: 'mono', text: shortNode(o.nodeId) }),
|
|
966
|
+
el('td', { class: 'mono', text: String(o.sequenceNumber) }),
|
|
967
|
+
el('td', { class: 'mono', text: o.timestamp ? fmtDateTime(o.timestamp.wallTime) : '\xB7' }),
|
|
968
|
+
el('td', { class: 'mono', text: o.data ? JSON.stringify(o.data).slice(0, 70) : '\xB7' })
|
|
969
|
+
]);
|
|
970
|
+
}));
|
|
971
|
+
main.appendChild(el('table', { class: 'grid' }, [el('thead', {}, [thead]), tbody]));
|
|
972
|
+
pager(main, data.total);
|
|
973
|
+
}).catch(showError(main));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// \u2500\u2500 Causal DAG renderer (SVG) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
977
|
+
function renderDag(container, dag, maxWidth) {
|
|
978
|
+
container.textContent = '';
|
|
979
|
+
if (!dag.nodes.length) { container.appendChild(el('div', { class: 'empty', text: 'No operations to graph.' })); return; }
|
|
980
|
+
var XSTEP = 46, YSTEP = 44, PADX = 90, PADY = 26, R = 7;
|
|
981
|
+
var width = PADX + dag.nodes.length * XSTEP + 30;
|
|
982
|
+
var height = PADY + dag.lanes.length * YSTEP + 16;
|
|
983
|
+
var svgns = 'http://www.w3.org/2000/svg';
|
|
984
|
+
var svg = document.createElementNS(svgns, 'svg');
|
|
985
|
+
svg.setAttribute('width', String(width));
|
|
986
|
+
svg.setAttribute('height', String(height));
|
|
987
|
+
|
|
988
|
+
var defs = document.createElementNS(svgns, 'defs');
|
|
989
|
+
defs.innerHTML = '<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#3a4356"/></marker>';
|
|
990
|
+
svg.appendChild(defs);
|
|
991
|
+
|
|
992
|
+
var colors = { insert: '#48bb78', update: '#4299e1', delete: '#f56565' };
|
|
993
|
+
var pos = {};
|
|
994
|
+
dag.nodes.forEach(function (n) {
|
|
995
|
+
pos[n.id] = { x: PADX + n.x * XSTEP, y: PADY + n.lane * YSTEP + YSTEP / 2 };
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
dag.lanes.forEach(function (lane, i) {
|
|
999
|
+
var y = PADY + i * YSTEP + YSTEP / 2;
|
|
1000
|
+
var line = document.createElementNS(svgns, 'line');
|
|
1001
|
+
line.setAttribute('x1', '10'); line.setAttribute('x2', String(width - 10));
|
|
1002
|
+
line.setAttribute('y1', String(y)); line.setAttribute('y2', String(y));
|
|
1003
|
+
line.setAttribute('class', 'dag-lane-line');
|
|
1004
|
+
svg.appendChild(line);
|
|
1005
|
+
var label = document.createElementNS(svgns, 'text');
|
|
1006
|
+
label.setAttribute('x', '10'); label.setAttribute('y', String(y - 8));
|
|
1007
|
+
label.setAttribute('class', 'dag-lane-label');
|
|
1008
|
+
label.textContent = lane.shortNodeId;
|
|
1009
|
+
svg.appendChild(label);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
dag.edges.forEach(function (e) {
|
|
1013
|
+
var a = pos[e.from], b = pos[e.to];
|
|
1014
|
+
if (!a || !b) return;
|
|
1015
|
+
var path = document.createElementNS(svgns, 'path');
|
|
1016
|
+
var midX = (a.x + b.x) / 2;
|
|
1017
|
+
path.setAttribute('d', 'M ' + a.x + ' ' + a.y + ' C ' + midX + ' ' + a.y + ', ' + midX + ' ' + b.y + ', ' + (b.x - R - 2) + ' ' + b.y);
|
|
1018
|
+
path.setAttribute('class', 'dag-edge');
|
|
1019
|
+
svg.appendChild(path);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
var tip = qs('#dagtip');
|
|
1023
|
+
dag.nodes.forEach(function (n) {
|
|
1024
|
+
var g = document.createElementNS(svgns, 'g');
|
|
1025
|
+
g.setAttribute('class', 'dag-node');
|
|
1026
|
+
var c = document.createElementNS(svgns, 'circle');
|
|
1027
|
+
var p = pos[n.id];
|
|
1028
|
+
c.setAttribute('cx', String(p.x)); c.setAttribute('cy', String(p.y)); c.setAttribute('r', String(R));
|
|
1029
|
+
c.setAttribute('fill', '#161a22');
|
|
1030
|
+
c.setAttribute('stroke', colors[n.type] || '#8b93a7');
|
|
1031
|
+
g.appendChild(c);
|
|
1032
|
+
g.addEventListener('mousemove', function (ev) {
|
|
1033
|
+
tip.style.display = 'block';
|
|
1034
|
+
tip.style.left = (ev.clientX + 14) + 'px';
|
|
1035
|
+
tip.style.top = (ev.clientY + 14) + 'px';
|
|
1036
|
+
tip.innerHTML = '<b>' + n.type + '</b> by ' + n.shortNodeId + ' \xB7 seq ' + n.sequenceNumber +
|
|
1037
|
+
'<br/>record ' + shortId(n.recordId) +
|
|
1038
|
+
(n.dataPreview ? '<br/><span class="mono">' + escapeHtml(n.dataPreview) + '</span>' : '');
|
|
1039
|
+
});
|
|
1040
|
+
g.addEventListener('mouseleave', function () { tip.style.display = 'none'; });
|
|
1041
|
+
g.addEventListener('click', function () { openRecord(n.recordId); });
|
|
1042
|
+
svg.appendChild(g);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
if (maxWidth) container.style.maxWidth = maxWidth + 'px';
|
|
1046
|
+
container.appendChild(svg);
|
|
1047
|
+
container.scrollLeft = width;
|
|
1048
|
+
}
|
|
1049
|
+
function escapeHtml(s) {
|
|
1050
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
|
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/
|
|
298
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
2906
|
+
resolve12(trimmed || defaultValue || "");
|
|
671
2907
|
});
|
|
672
2908
|
});
|
|
673
2909
|
}
|
|
674
2910
|
function promptSelect(message, choices, options) {
|
|
675
|
-
return new Promise((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
2949
|
+
resolve12(defaultValue);
|
|
714
2950
|
return;
|
|
715
2951
|
}
|
|
716
2952
|
if (normalized === "y" || normalized === "yes") {
|
|
717
2953
|
rl.close();
|
|
718
|
-
|
|
2954
|
+
resolve12(true);
|
|
719
2955
|
return;
|
|
720
2956
|
}
|
|
721
2957
|
if (normalized === "n" || normalized === "no") {
|
|
722
2958
|
rl.close();
|
|
723
|
-
|
|
2959
|
+
resolve12(false);
|
|
724
2960
|
return;
|
|
725
2961
|
}
|
|
726
2962
|
;
|
|
@@ -841,6 +3077,9 @@ var TEMPLATES = [
|
|
|
841
3077
|
"tauri-react"
|
|
842
3078
|
];
|
|
843
3079
|
|
|
3080
|
+
// src/commands/create/create-command.ts
|
|
3081
|
+
init_fs_helpers();
|
|
3082
|
+
|
|
844
3083
|
// src/utils/package-manager.ts
|
|
845
3084
|
var import_node_child_process3 = require("child_process");
|
|
846
3085
|
function detectPackageManager() {
|
|
@@ -1441,7 +3680,9 @@ function createCompatibilityLayerPlan(templateName) {
|
|
|
1441
3680
|
case "vue-tailwind-sync":
|
|
1442
3681
|
return {
|
|
1443
3682
|
compatibilityTarget: templateName,
|
|
1444
|
-
layers: [
|
|
3683
|
+
layers: [
|
|
3684
|
+
{ category: "base", name: "vue-tailwind-sync", sourceTemplate: "vue-tailwind-sync" }
|
|
3685
|
+
]
|
|
1445
3686
|
};
|
|
1446
3687
|
case "vue-tailwind":
|
|
1447
3688
|
return {
|
|
@@ -1452,7 +3693,11 @@ function createCompatibilityLayerPlan(templateName) {
|
|
|
1452
3693
|
return {
|
|
1453
3694
|
compatibilityTarget: templateName,
|
|
1454
3695
|
layers: [
|
|
1455
|
-
{
|
|
3696
|
+
{
|
|
3697
|
+
category: "base",
|
|
3698
|
+
name: "svelte-tailwind-sync",
|
|
3699
|
+
sourceTemplate: "svelte-tailwind-sync"
|
|
3700
|
+
}
|
|
1456
3701
|
]
|
|
1457
3702
|
};
|
|
1458
3703
|
case "svelte-tailwind":
|
|
@@ -1839,6 +4084,7 @@ function resolveKoraVersion() {
|
|
|
1839
4084
|
var import_node_fs4 = require("fs");
|
|
1840
4085
|
var import_node_path20 = require("path");
|
|
1841
4086
|
var import_citty4 = require("citty");
|
|
4087
|
+
init_fs_helpers();
|
|
1842
4088
|
|
|
1843
4089
|
// src/commands/deploy/adapters/adapter.ts
|
|
1844
4090
|
var DEPLOY_PLATFORMS = [
|
|
@@ -1863,6 +4109,7 @@ var import_node_child_process5 = require("child_process");
|
|
|
1863
4109
|
var import_node_fs3 = require("fs");
|
|
1864
4110
|
var import_promises7 = require("fs/promises");
|
|
1865
4111
|
var import_node_path10 = require("path");
|
|
4112
|
+
init_fs_helpers();
|
|
1866
4113
|
async function buildClient(options) {
|
|
1867
4114
|
const viteEntryPoint = await resolveProjectBinaryEntryPoint(options.projectRoot, "vite", "vite");
|
|
1868
4115
|
if (!viteEntryPoint) {
|
|
@@ -1906,7 +4153,7 @@ async function patchSqliteWasmAssets(projectRoot, outDir) {
|
|
|
1906
4153
|
}
|
|
1907
4154
|
}
|
|
1908
4155
|
async function runProcess(command, args, cwd) {
|
|
1909
|
-
await new Promise((
|
|
4156
|
+
await new Promise((resolve12, reject) => {
|
|
1910
4157
|
const child = (0, import_node_child_process5.spawn)(command, args, {
|
|
1911
4158
|
cwd,
|
|
1912
4159
|
stdio: "inherit",
|
|
@@ -1917,7 +4164,7 @@ async function runProcess(command, args, cwd) {
|
|
|
1917
4164
|
});
|
|
1918
4165
|
child.on("exit", (code) => {
|
|
1919
4166
|
if (code === 0) {
|
|
1920
|
-
|
|
4167
|
+
resolve12();
|
|
1921
4168
|
return;
|
|
1922
4169
|
}
|
|
1923
4170
|
reject(new Error(`Client build failed with exit code ${String(code ?? "unknown")}.`));
|
|
@@ -2315,7 +4562,7 @@ var AwsEcsAdapter = class {
|
|
|
2315
4562
|
};
|
|
2316
4563
|
var NodeAwsCommandRunner = class {
|
|
2317
4564
|
async run(command, args, cwd) {
|
|
2318
|
-
return new Promise((
|
|
4565
|
+
return new Promise((resolve12) => {
|
|
2319
4566
|
const child = (0, import_node_child_process6.spawn)(command, args, {
|
|
2320
4567
|
cwd,
|
|
2321
4568
|
env: process.env,
|
|
@@ -2330,11 +4577,11 @@ var NodeAwsCommandRunner = class {
|
|
|
2330
4577
|
stderr += chunk.toString("utf-8");
|
|
2331
4578
|
});
|
|
2332
4579
|
child.on("error", (error) => {
|
|
2333
|
-
|
|
4580
|
+
resolve12({ exitCode: 1, stdout, stderr: `${stderr}
|
|
2334
4581
|
${error.message}` });
|
|
2335
4582
|
});
|
|
2336
4583
|
child.on("exit", (code) => {
|
|
2337
|
-
|
|
4584
|
+
resolve12({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
2338
4585
|
});
|
|
2339
4586
|
});
|
|
2340
4587
|
}
|
|
@@ -2662,7 +4909,7 @@ var AwsLightsailAdapter = class {
|
|
|
2662
4909
|
};
|
|
2663
4910
|
var NodeAwsLightsailCommandRunner = class {
|
|
2664
4911
|
async run(command, args, cwd) {
|
|
2665
|
-
return new Promise((
|
|
4912
|
+
return new Promise((resolve12) => {
|
|
2666
4913
|
const child = (0, import_node_child_process7.spawn)(command, args, {
|
|
2667
4914
|
cwd,
|
|
2668
4915
|
env: process.env,
|
|
@@ -2677,11 +4924,11 @@ var NodeAwsLightsailCommandRunner = class {
|
|
|
2677
4924
|
stderr += chunk.toString("utf-8");
|
|
2678
4925
|
});
|
|
2679
4926
|
child.on("error", (error) => {
|
|
2680
|
-
|
|
4927
|
+
resolve12({ exitCode: 1, stdout, stderr: `${stderr}
|
|
2681
4928
|
${error.message}` });
|
|
2682
4929
|
});
|
|
2683
4930
|
child.on("exit", (code) => {
|
|
2684
|
-
|
|
4931
|
+
resolve12({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
2685
4932
|
});
|
|
2686
4933
|
});
|
|
2687
4934
|
}
|
|
@@ -3006,7 +5253,7 @@ var FlyAdapter = class {
|
|
|
3006
5253
|
};
|
|
3007
5254
|
var NodeFlyCommandRunner = class {
|
|
3008
5255
|
async run(command, args, cwd) {
|
|
3009
|
-
return await new Promise((
|
|
5256
|
+
return await new Promise((resolve12) => {
|
|
3010
5257
|
const child = (0, import_node_child_process8.spawn)(command, args, {
|
|
3011
5258
|
cwd,
|
|
3012
5259
|
env: process.env,
|
|
@@ -3021,7 +5268,7 @@ var NodeFlyCommandRunner = class {
|
|
|
3021
5268
|
stderr += chunk.toString("utf-8");
|
|
3022
5269
|
});
|
|
3023
5270
|
child.on("error", (error) => {
|
|
3024
|
-
|
|
5271
|
+
resolve12({
|
|
3025
5272
|
exitCode: 1,
|
|
3026
5273
|
stdout,
|
|
3027
5274
|
stderr: `${stderr}
|
|
@@ -3029,7 +5276,7 @@ ${error.message}`
|
|
|
3029
5276
|
});
|
|
3030
5277
|
});
|
|
3031
5278
|
child.on("exit", (code) => {
|
|
3032
|
-
|
|
5279
|
+
resolve12({
|
|
3033
5280
|
exitCode: code ?? 1,
|
|
3034
5281
|
stdout: stdout.trim(),
|
|
3035
5282
|
stderr: stderr.trim()
|
|
@@ -3342,7 +5589,7 @@ var RailwayAdapter = class {
|
|
|
3342
5589
|
};
|
|
3343
5590
|
var NodeRailwayCommandRunner = class {
|
|
3344
5591
|
async run(command, args, cwd) {
|
|
3345
|
-
return await new Promise((
|
|
5592
|
+
return await new Promise((resolve12) => {
|
|
3346
5593
|
const child = (0, import_node_child_process9.spawn)(command, args, {
|
|
3347
5594
|
cwd,
|
|
3348
5595
|
env: process.env,
|
|
@@ -3357,7 +5604,7 @@ var NodeRailwayCommandRunner = class {
|
|
|
3357
5604
|
stderr += chunk.toString("utf-8");
|
|
3358
5605
|
});
|
|
3359
5606
|
child.on("error", (error) => {
|
|
3360
|
-
|
|
5607
|
+
resolve12({
|
|
3361
5608
|
exitCode: 1,
|
|
3362
5609
|
stdout,
|
|
3363
5610
|
stderr: `${stderr}
|
|
@@ -3365,7 +5612,7 @@ ${error.message}`
|
|
|
3365
5612
|
});
|
|
3366
5613
|
});
|
|
3367
5614
|
child.on("exit", (code) => {
|
|
3368
|
-
|
|
5615
|
+
resolve12({
|
|
3369
5616
|
exitCode: code ?? 1,
|
|
3370
5617
|
stdout: stdout.trim(),
|
|
3371
5618
|
stderr: stderr.trim()
|
|
@@ -4053,6 +6300,7 @@ var import_promises14 = require("fs/promises");
|
|
|
4053
6300
|
var import_node_path22 = require("path");
|
|
4054
6301
|
var import_node_path23 = require("path");
|
|
4055
6302
|
var import_citty5 = require("citty");
|
|
6303
|
+
init_fs_helpers();
|
|
4056
6304
|
|
|
4057
6305
|
// src/commands/dev/process-manager.ts
|
|
4058
6306
|
var import_node_child_process10 = require("child_process");
|
|
@@ -4068,8 +6316,8 @@ var ProcessManager = class {
|
|
|
4068
6316
|
let resolveExit;
|
|
4069
6317
|
const runningProcess = {
|
|
4070
6318
|
child,
|
|
4071
|
-
exitPromise: new Promise((
|
|
4072
|
-
resolveExit =
|
|
6319
|
+
exitPromise: new Promise((resolve12) => {
|
|
6320
|
+
resolveExit = resolve12;
|
|
4073
6321
|
}),
|
|
4074
6322
|
stdoutBuffer: "",
|
|
4075
6323
|
stderrBuffer: ""
|
|
@@ -4136,8 +6384,8 @@ var ProcessManager = class {
|
|
|
4136
6384
|
}
|
|
4137
6385
|
};
|
|
4138
6386
|
function delay(ms) {
|
|
4139
|
-
return new Promise((
|
|
4140
|
-
setTimeout(
|
|
6387
|
+
return new Promise((resolve12) => {
|
|
6388
|
+
setTimeout(resolve12, ms);
|
|
4141
6389
|
});
|
|
4142
6390
|
}
|
|
4143
6391
|
|
|
@@ -4145,6 +6393,7 @@ function delay(ms) {
|
|
|
4145
6393
|
var import_node_child_process11 = require("child_process");
|
|
4146
6394
|
var import_node_fs5 = require("fs");
|
|
4147
6395
|
var import_node_path21 = require("path");
|
|
6396
|
+
init_fs_helpers();
|
|
4148
6397
|
var SchemaWatcher = class {
|
|
4149
6398
|
constructor(config) {
|
|
4150
6399
|
this.config = config;
|
|
@@ -4199,7 +6448,7 @@ var SchemaWatcher = class {
|
|
|
4199
6448
|
}
|
|
4200
6449
|
};
|
|
4201
6450
|
async function spawnCommand(command, args, cwd) {
|
|
4202
|
-
await new Promise((
|
|
6451
|
+
await new Promise((resolve12, reject) => {
|
|
4203
6452
|
const child = (0, import_node_child_process11.spawn)(command, args, {
|
|
4204
6453
|
cwd,
|
|
4205
6454
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4216,7 +6465,7 @@ async function spawnCommand(command, args, cwd) {
|
|
|
4216
6465
|
});
|
|
4217
6466
|
child.on("exit", (code) => {
|
|
4218
6467
|
if (code === 0) {
|
|
4219
|
-
|
|
6468
|
+
resolve12();
|
|
4220
6469
|
return;
|
|
4221
6470
|
}
|
|
4222
6471
|
reject(new Error(`Type generation exited with code ${code ?? "unknown"}.`));
|
|
@@ -4326,8 +6575,8 @@ var devCommand = (0, import_citty5.defineCommand)({
|
|
|
4326
6575
|
let schemaWatcher = null;
|
|
4327
6576
|
let shuttingDown = false;
|
|
4328
6577
|
let resolveFinished;
|
|
4329
|
-
const finished = new Promise((
|
|
4330
|
-
resolveFinished =
|
|
6578
|
+
const finished = new Promise((resolve12) => {
|
|
6579
|
+
resolveFinished = resolve12;
|
|
4331
6580
|
});
|
|
4332
6581
|
const onManagedProcessExit = () => {
|
|
4333
6582
|
if (!processManager.hasRunning() && !shuttingDown) {
|
|
@@ -4527,10 +6776,13 @@ await new Promise(() => {});
|
|
|
4527
6776
|
|
|
4528
6777
|
// src/commands/doctor/doctor-command.ts
|
|
4529
6778
|
var import_citty6 = require("citty");
|
|
6779
|
+
init_fs_helpers();
|
|
4530
6780
|
|
|
4531
6781
|
// src/commands/doctor/doctor-checks.ts
|
|
4532
6782
|
var import_promises15 = require("fs/promises");
|
|
4533
6783
|
var import_node_path24 = require("path");
|
|
6784
|
+
init_fs_helpers();
|
|
6785
|
+
init_schema_loader();
|
|
4534
6786
|
var STATUS_ENDPOINT = "/__kora/status";
|
|
4535
6787
|
async function runDoctorChecks(options) {
|
|
4536
6788
|
const { projectRoot } = options;
|
|
@@ -4844,6 +7096,8 @@ var doctorCommand = (0, import_citty6.defineCommand)({
|
|
|
4844
7096
|
var import_promises16 = require("fs/promises");
|
|
4845
7097
|
var import_node_path25 = require("path");
|
|
4846
7098
|
var import_citty7 = require("citty");
|
|
7099
|
+
init_fs_helpers();
|
|
7100
|
+
init_schema_loader();
|
|
4847
7101
|
|
|
4848
7102
|
// src/commands/generate/hook-generator.ts
|
|
4849
7103
|
function generateCollectionHooks(schema) {
|
|
@@ -5236,6 +7490,7 @@ var logsCommand = (0, import_citty8.defineCommand)({
|
|
|
5236
7490
|
var import_promises17 = require("fs/promises");
|
|
5237
7491
|
var import_node_path26 = require("path");
|
|
5238
7492
|
var import_citty9 = require("citty");
|
|
7493
|
+
init_fs_helpers();
|
|
5239
7494
|
|
|
5240
7495
|
// src/commands/migrate/migration-generator.ts
|
|
5241
7496
|
var import_core2 = require("@korajs/core");
|
|
@@ -5826,6 +8081,7 @@ function generateOperationTransformModule(fromVersion, toVersion) {
|
|
|
5826
8081
|
}
|
|
5827
8082
|
|
|
5828
8083
|
// src/commands/migrate/migrate-command.ts
|
|
8084
|
+
init_schema_loader();
|
|
5829
8085
|
var SNAPSHOT_PATH = "kora/schema.snapshot.json";
|
|
5830
8086
|
var MIGRATIONS_DIR = "kora/migrations";
|
|
5831
8087
|
var migrateCommand = (0, import_citty9.defineCommand)({
|
|
@@ -6288,8 +8544,131 @@ function printStatus(status, url, logger) {
|
|
|
6288
8544
|
}
|
|
6289
8545
|
}
|
|
6290
8546
|
|
|
8547
|
+
// src/commands/studio/studio-command.ts
|
|
8548
|
+
var import_node_fs8 = require("fs");
|
|
8549
|
+
var import_node_path29 = require("path");
|
|
8550
|
+
var import_citty11 = require("citty");
|
|
8551
|
+
var studioCommand = (0, import_citty11.defineCommand)({
|
|
8552
|
+
meta: {
|
|
8553
|
+
name: "studio",
|
|
8554
|
+
description: "Kora Studio: visualize records, operations, and sync \u2014 or run the sync lab"
|
|
8555
|
+
},
|
|
8556
|
+
args: {
|
|
8557
|
+
db: {
|
|
8558
|
+
type: "string",
|
|
8559
|
+
description: "Path to a Kora SQLite database file (file mode, read-only)"
|
|
8560
|
+
},
|
|
8561
|
+
lab: {
|
|
8562
|
+
type: "boolean",
|
|
8563
|
+
description: "Start the interactive multi-device sync laboratory"
|
|
8564
|
+
},
|
|
8565
|
+
connect: {
|
|
8566
|
+
type: "string",
|
|
8567
|
+
description: "Spectator mode: WebSocket URL of a live Kora sync server (read-only replica)"
|
|
8568
|
+
},
|
|
8569
|
+
token: {
|
|
8570
|
+
type: "string",
|
|
8571
|
+
description: "Spectator mode: bearer token for the sync auth handshake"
|
|
8572
|
+
},
|
|
8573
|
+
devices: {
|
|
8574
|
+
type: "string",
|
|
8575
|
+
description: "Lab mode: number of initial devices (default 2)"
|
|
8576
|
+
},
|
|
8577
|
+
schema: {
|
|
8578
|
+
type: "string",
|
|
8579
|
+
description: "Lab mode: path to a schema file (defaults to a built-in demo schema)"
|
|
8580
|
+
},
|
|
8581
|
+
port: {
|
|
8582
|
+
type: "string",
|
|
8583
|
+
description: "Port for the Studio UI (default 4321, 0 for a random free port)"
|
|
8584
|
+
}
|
|
8585
|
+
},
|
|
8586
|
+
async run({ args }) {
|
|
8587
|
+
const logger = createLogger();
|
|
8588
|
+
const port = args.port !== void 0 ? Number.parseInt(args.port, 10) : 4321;
|
|
8589
|
+
if (Number.isNaN(port) || port < 0 || port > 65535) {
|
|
8590
|
+
throw new Error(`Invalid port "${args.port}"`);
|
|
8591
|
+
}
|
|
8592
|
+
const { startStudioServer: startStudioServer2 } = await Promise.resolve().then(() => (init_studio_server(), studio_server_exports));
|
|
8593
|
+
if (typeof args.connect === "string") {
|
|
8594
|
+
if (typeof args.schema !== "string") {
|
|
8595
|
+
throw new Error(
|
|
8596
|
+
"Spectator mode needs your schema to materialize records: kora studio --connect wss://\u2026 --schema ./kora/schema.ts"
|
|
8597
|
+
);
|
|
8598
|
+
}
|
|
8599
|
+
const { loadSchemaDefinition: loadSchemaDefinition2 } = await Promise.resolve().then(() => (init_schema_loader(), schema_loader_exports));
|
|
8600
|
+
const schema = await loadSchemaDefinition2((0, import_node_path29.resolve)(process.cwd(), args.schema), process.cwd());
|
|
8601
|
+
const { SpectatorManager: SpectatorManager2 } = await Promise.resolve().then(() => (init_spectator_manager(), spectator_manager_exports));
|
|
8602
|
+
const spectator = new SpectatorManager2({
|
|
8603
|
+
url: args.connect,
|
|
8604
|
+
schema,
|
|
8605
|
+
...typeof args.token === "string" ? { token: args.token } : {}
|
|
8606
|
+
});
|
|
8607
|
+
await spectator.start();
|
|
8608
|
+
const server2 = await startStudioServer2({ port, spectator });
|
|
8609
|
+
logger.banner();
|
|
8610
|
+
logger.success(`Kora Studio SPECTATOR running at ${server2.url}`);
|
|
8611
|
+
logger.step(`Watching ${args.connect} (read-only replica \u2014 nothing is ever pushed)`);
|
|
8612
|
+
logger.step("Press Ctrl+C to stop.");
|
|
8613
|
+
const shutdown2 = async () => {
|
|
8614
|
+
await server2.close();
|
|
8615
|
+
await spectator.close();
|
|
8616
|
+
process.exit(0);
|
|
8617
|
+
};
|
|
8618
|
+
process.on("SIGINT", () => void shutdown2());
|
|
8619
|
+
process.on("SIGTERM", () => void shutdown2());
|
|
8620
|
+
return;
|
|
8621
|
+
}
|
|
8622
|
+
if (args.lab) {
|
|
8623
|
+
const { LabManager: LabManager2, defaultLabSchema: defaultLabSchema2 } = await Promise.resolve().then(() => (init_lab_manager(), lab_manager_exports));
|
|
8624
|
+
let schema = defaultLabSchema2();
|
|
8625
|
+
if (typeof args.schema === "string") {
|
|
8626
|
+
const { loadSchemaDefinition: loadSchemaDefinition2 } = await Promise.resolve().then(() => (init_schema_loader(), schema_loader_exports));
|
|
8627
|
+
schema = await loadSchemaDefinition2((0, import_node_path29.resolve)(process.cwd(), args.schema), process.cwd());
|
|
8628
|
+
}
|
|
8629
|
+
const initialDevices = args.devices !== void 0 ? Math.max(1, Number.parseInt(args.devices, 10) || 2) : 2;
|
|
8630
|
+
const lab = new LabManager2(schema);
|
|
8631
|
+
await lab.start(initialDevices);
|
|
8632
|
+
const server2 = await startStudioServer2({ port, lab });
|
|
8633
|
+
logger.banner();
|
|
8634
|
+
logger.success(`Kora Studio LAB running at ${server2.url}`);
|
|
8635
|
+
logger.step(`${initialDevices} device(s) started against an in-process sync server.`);
|
|
8636
|
+
logger.step("Everything in the lab is throwaway \u2014 experiment freely.");
|
|
8637
|
+
logger.step("Press Ctrl+C to stop.");
|
|
8638
|
+
const shutdown2 = async () => {
|
|
8639
|
+
await server2.close();
|
|
8640
|
+
await lab.close();
|
|
8641
|
+
process.exit(0);
|
|
8642
|
+
};
|
|
8643
|
+
process.on("SIGINT", () => void shutdown2());
|
|
8644
|
+
process.on("SIGTERM", () => void shutdown2());
|
|
8645
|
+
return;
|
|
8646
|
+
}
|
|
8647
|
+
if (typeof args.db !== "string") {
|
|
8648
|
+
throw new Error(
|
|
8649
|
+
"Pass --db <path> to inspect a database, --lab for the sync laboratory, or --connect <wss url> for live spectator mode."
|
|
8650
|
+
);
|
|
8651
|
+
}
|
|
8652
|
+
const dbPath = (0, import_node_path29.resolve)(process.cwd(), args.db);
|
|
8653
|
+
if (!(0, import_node_fs8.existsSync)(dbPath)) {
|
|
8654
|
+
throw new Error(`Database file not found: ${dbPath}`);
|
|
8655
|
+
}
|
|
8656
|
+
const server = await startStudioServer2({ port, dbPath });
|
|
8657
|
+
logger.banner();
|
|
8658
|
+
logger.success(`Kora Studio running at ${server.url}`);
|
|
8659
|
+
logger.step(`Database: ${dbPath} (read-only)`);
|
|
8660
|
+
logger.step("Press Ctrl+C to stop.");
|
|
8661
|
+
const shutdown = async () => {
|
|
8662
|
+
await server.close();
|
|
8663
|
+
process.exit(0);
|
|
8664
|
+
};
|
|
8665
|
+
process.on("SIGINT", () => void shutdown());
|
|
8666
|
+
process.on("SIGTERM", () => void shutdown());
|
|
8667
|
+
}
|
|
8668
|
+
});
|
|
8669
|
+
|
|
6291
8670
|
// src/bin.ts
|
|
6292
|
-
var main = (0,
|
|
8671
|
+
var main = (0, import_citty12.defineCommand)({
|
|
6293
8672
|
meta: {
|
|
6294
8673
|
name: "kora",
|
|
6295
8674
|
description: "Kora.js \u2014 Offline-first application framework"
|
|
@@ -6304,8 +8683,9 @@ var main = (0, import_citty11.defineCommand)({
|
|
|
6304
8683
|
generate: generateCommand,
|
|
6305
8684
|
logs: logsCommand,
|
|
6306
8685
|
migrate: migrateCommand,
|
|
6307
|
-
status: statusCommand
|
|
8686
|
+
status: statusCommand,
|
|
8687
|
+
studio: studioCommand
|
|
6308
8688
|
}
|
|
6309
8689
|
});
|
|
6310
|
-
(0,
|
|
8690
|
+
(0, import_citty12.runMain)(main);
|
|
6311
8691
|
//# sourceMappingURL=bin.cjs.map
|