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