@korajs/cli 0.5.0 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin.cjs +2656 -207
  3. package/dist/bin.cjs.map +1 -1
  4. package/dist/bin.js +152 -96
  5. package/dist/bin.js.map +1 -1
  6. package/dist/chunk-5NI2FEQL.js +92 -0
  7. package/dist/chunk-5NI2FEQL.js.map +1 -0
  8. package/dist/chunk-6C4BHSRA.js +82 -0
  9. package/dist/chunk-6C4BHSRA.js.map +1 -0
  10. package/dist/{chunk-3WNFM3QB.js → chunk-B5YS4STN.js} +78 -99
  11. package/dist/chunk-B5YS4STN.js.map +1 -0
  12. package/dist/{chunk-PSRM56B7.js → chunk-BWTKRKNJ.js} +60 -7
  13. package/dist/chunk-BWTKRKNJ.js.map +1 -0
  14. package/dist/{chunk-SOTZIWIF.js → chunk-EOWLAAIV.js} +5 -3
  15. package/dist/{chunk-SOTZIWIF.js.map → chunk-EOWLAAIV.js.map} +1 -1
  16. package/dist/create.cjs +82 -7
  17. package/dist/create.cjs.map +1 -1
  18. package/dist/create.js +3 -2
  19. package/dist/create.js.map +1 -1
  20. package/dist/index.cjs +65 -3
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +3 -3
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +3 -2
  25. package/dist/lab-manager-KUDII6BT.js +280 -0
  26. package/dist/lab-manager-KUDII6BT.js.map +1 -0
  27. package/dist/schema-loader-GGHECMTM.js +9 -0
  28. package/dist/schema-loader-GGHECMTM.js.map +1 -0
  29. package/dist/spectator-manager-E2LH2P54.js +142 -0
  30. package/dist/spectator-manager-E2LH2P54.js.map +1 -0
  31. package/dist/studio-server-NIFKZI4F.js +1738 -0
  32. package/dist/studio-server-NIFKZI4F.js.map +1 -0
  33. package/package.json +8 -6
  34. package/templates/react-basic/AGENTS.md +87 -0
  35. package/templates/react-sync/AGENTS.md +87 -0
  36. package/templates/react-sync/src/App.tsx +8 -0
  37. package/templates/react-sync/src/index.css +11 -0
  38. package/templates/react-tailwind/AGENTS.md +87 -0
  39. package/templates/react-tailwind-sync/AGENTS.md +87 -0
  40. package/templates/react-tailwind-sync/src/App.tsx +28 -0
  41. package/templates/svelte-basic/.env.example +3 -0
  42. package/templates/svelte-basic/AGENTS.md +76 -0
  43. package/templates/svelte-basic/README.md.hbs +62 -0
  44. package/templates/svelte-basic/index.html.hbs +12 -0
  45. package/templates/svelte-basic/kora.config.ts +12 -0
  46. package/templates/svelte-basic/package.json.hbs +27 -0
  47. package/templates/svelte-basic/src/App.svelte +127 -0
  48. package/templates/svelte-basic/src/Root.svelte +14 -0
  49. package/templates/svelte-basic/src/index.css +287 -0
  50. package/templates/svelte-basic/src/kora-worker.ts +9 -0
  51. package/templates/svelte-basic/src/main.ts +16 -0
  52. package/templates/svelte-basic/src/modules/todos/todo.mutations.ts +25 -0
  53. package/templates/svelte-basic/src/modules/todos/todo.queries.ts +5 -0
  54. package/templates/svelte-basic/src/modules/todos/todo.schema.ts +10 -0
  55. package/templates/svelte-basic/src/modules/todos/useTodos.ts +25 -0
  56. package/templates/svelte-basic/src/schema.ts +9 -0
  57. package/templates/svelte-basic/src/vite-env.d.ts +12 -0
  58. package/templates/svelte-basic/svelte.config.js +5 -0
  59. package/templates/svelte-basic/tsconfig.json.hbs +14 -0
  60. package/templates/svelte-basic/vite.config.ts +52 -0
  61. package/templates/svelte-sync/.env.example +25 -0
  62. package/templates/svelte-sync/AGENTS.md +76 -0
  63. package/templates/svelte-sync/README.md.hbs +105 -0
  64. package/templates/svelte-sync/index.html.hbs +12 -0
  65. package/templates/svelte-sync/kora.config.ts +17 -0
  66. package/templates/svelte-sync/package.json.hbs +43 -0
  67. package/templates/svelte-sync/server.ts +90 -0
  68. package/templates/svelte-sync/src/App.svelte +150 -0
  69. package/templates/svelte-sync/src/Root.svelte +22 -0
  70. package/templates/svelte-sync/src/auth.ts +25 -0
  71. package/templates/svelte-sync/src/index.css +339 -0
  72. package/templates/svelte-sync/src/kora-worker.ts +9 -0
  73. package/templates/svelte-sync/src/main.ts +32 -0
  74. package/templates/svelte-sync/src/modules/todos/todo.mutations.ts +25 -0
  75. package/templates/svelte-sync/src/modules/todos/todo.queries.ts +5 -0
  76. package/templates/svelte-sync/src/modules/todos/todo.schema.ts +10 -0
  77. package/templates/svelte-sync/src/modules/todos/useTodos.ts +25 -0
  78. package/templates/svelte-sync/src/schema.ts +9 -0
  79. package/templates/svelte-sync/src/vite-env.d.ts +21 -0
  80. package/templates/svelte-sync/svelte.config.js +5 -0
  81. package/templates/svelte-sync/tsconfig.json.hbs +14 -0
  82. package/templates/svelte-sync/vite.config.ts +79 -0
  83. package/templates/svelte-tailwind/AGENTS.md +76 -0
  84. package/templates/svelte-tailwind/README.md.hbs +62 -0
  85. package/templates/svelte-tailwind/index.html.hbs +12 -0
  86. package/templates/svelte-tailwind/kora.config.ts +12 -0
  87. package/templates/svelte-tailwind/package.json.hbs +30 -0
  88. package/templates/svelte-tailwind/src/App.svelte +75 -0
  89. package/templates/svelte-tailwind/src/Root.svelte +14 -0
  90. package/templates/svelte-tailwind/src/index.css +7 -0
  91. package/templates/svelte-tailwind/src/kora-worker.ts +9 -0
  92. package/templates/svelte-tailwind/src/main.ts +16 -0
  93. package/templates/svelte-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  94. package/templates/svelte-tailwind/src/modules/todos/todo.queries.ts +5 -0
  95. package/templates/svelte-tailwind/src/modules/todos/todo.schema.ts +10 -0
  96. package/templates/svelte-tailwind/src/modules/todos/useTodos.ts +25 -0
  97. package/templates/svelte-tailwind/src/schema.ts +9 -0
  98. package/templates/svelte-tailwind/src/vite-env.d.ts +12 -0
  99. package/templates/svelte-tailwind/svelte.config.js +5 -0
  100. package/templates/svelte-tailwind/tsconfig.json.hbs +14 -0
  101. package/templates/svelte-tailwind/vite.config.ts +53 -0
  102. package/templates/svelte-tailwind-sync/.env.example +25 -0
  103. package/templates/svelte-tailwind-sync/AGENTS.md +76 -0
  104. package/templates/svelte-tailwind-sync/README.md.hbs +105 -0
  105. package/templates/svelte-tailwind-sync/index.html.hbs +12 -0
  106. package/templates/svelte-tailwind-sync/kora.config.ts +17 -0
  107. package/templates/svelte-tailwind-sync/package.json.hbs +46 -0
  108. package/templates/svelte-tailwind-sync/server.ts +90 -0
  109. package/templates/svelte-tailwind-sync/src/App.svelte +192 -0
  110. package/templates/svelte-tailwind-sync/src/Root.svelte +26 -0
  111. package/templates/svelte-tailwind-sync/src/auth.ts +25 -0
  112. package/templates/svelte-tailwind-sync/src/index.css +7 -0
  113. package/templates/svelte-tailwind-sync/src/kora-worker.ts +9 -0
  114. package/templates/svelte-tailwind-sync/src/main.ts +32 -0
  115. package/templates/svelte-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  116. package/templates/svelte-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  117. package/templates/svelte-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  118. package/templates/svelte-tailwind-sync/src/modules/todos/useTodos.ts +25 -0
  119. package/templates/svelte-tailwind-sync/src/schema.ts +9 -0
  120. package/templates/svelte-tailwind-sync/src/vite-env.d.ts +21 -0
  121. package/templates/svelte-tailwind-sync/svelte.config.js +5 -0
  122. package/templates/svelte-tailwind-sync/tsconfig.json.hbs +14 -0
  123. package/templates/svelte-tailwind-sync/vite.config.ts +80 -0
  124. package/templates/tauri-react/AGENTS.md +91 -0
  125. package/templates/vue-basic/AGENTS.md +84 -0
  126. package/templates/vue-basic/README.md.hbs +62 -0
  127. package/templates/vue-basic/index.html.hbs +12 -0
  128. package/templates/vue-basic/kora.config.ts +12 -0
  129. package/templates/vue-basic/package.json.hbs +26 -0
  130. package/templates/vue-basic/src/App.vue +117 -0
  131. package/templates/vue-basic/src/index.css +287 -0
  132. package/templates/vue-basic/src/kora-worker.ts +9 -0
  133. package/templates/vue-basic/src/main.ts +20 -0
  134. package/templates/vue-basic/src/modules/todos/todo.mutations.ts +25 -0
  135. package/templates/vue-basic/src/modules/todos/todo.queries.ts +5 -0
  136. package/templates/vue-basic/src/modules/todos/todo.schema.ts +10 -0
  137. package/templates/vue-basic/src/modules/todos/useTodos.ts +32 -0
  138. package/templates/vue-basic/src/schema.ts +9 -0
  139. package/templates/vue-basic/src/vite-env.d.ts +17 -0
  140. package/templates/vue-basic/tsconfig.json.hbs +15 -0
  141. package/templates/vue-basic/vite.config.ts +69 -0
  142. package/templates/vue-sync/.env.example +25 -0
  143. package/templates/vue-sync/AGENTS.md +84 -0
  144. package/templates/vue-sync/README.md.hbs +105 -0
  145. package/templates/vue-sync/index.html.hbs +12 -0
  146. package/templates/vue-sync/kora.config.ts +17 -0
  147. package/templates/vue-sync/package.json.hbs +42 -0
  148. package/templates/vue-sync/server.ts +90 -0
  149. package/templates/vue-sync/src/App.vue +135 -0
  150. package/templates/vue-sync/src/auth.ts +25 -0
  151. package/templates/vue-sync/src/index.css +339 -0
  152. package/templates/vue-sync/src/kora-worker.ts +9 -0
  153. package/templates/vue-sync/src/main.ts +47 -0
  154. package/templates/vue-sync/src/modules/todos/todo.mutations.ts +25 -0
  155. package/templates/vue-sync/src/modules/todos/todo.queries.ts +5 -0
  156. package/templates/vue-sync/src/modules/todos/todo.schema.ts +10 -0
  157. package/templates/vue-sync/src/modules/todos/useTodos.ts +32 -0
  158. package/templates/vue-sync/src/schema.ts +9 -0
  159. package/templates/vue-sync/src/vite-env.d.ts +26 -0
  160. package/templates/vue-sync/tsconfig.json.hbs +15 -0
  161. package/templates/vue-sync/vite.config.ts +79 -0
  162. package/templates/vue-tailwind/AGENTS.md +84 -0
  163. package/templates/vue-tailwind/README.md.hbs +62 -0
  164. package/templates/vue-tailwind/index.html.hbs +12 -0
  165. package/templates/vue-tailwind/kora.config.ts +12 -0
  166. package/templates/vue-tailwind/package.json.hbs +29 -0
  167. package/templates/vue-tailwind/src/App.vue +135 -0
  168. package/templates/vue-tailwind/src/index.css +7 -0
  169. package/templates/vue-tailwind/src/kora-worker.ts +9 -0
  170. package/templates/vue-tailwind/src/main.ts +31 -0
  171. package/templates/vue-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  172. package/templates/vue-tailwind/src/modules/todos/todo.queries.ts +5 -0
  173. package/templates/vue-tailwind/src/modules/todos/todo.schema.ts +10 -0
  174. package/templates/vue-tailwind/src/modules/todos/useTodos.ts +32 -0
  175. package/templates/vue-tailwind/src/schema.ts +9 -0
  176. package/templates/vue-tailwind/src/vite-env.d.ts +17 -0
  177. package/templates/vue-tailwind/tsconfig.json.hbs +15 -0
  178. package/templates/vue-tailwind/vite.config.ts +70 -0
  179. package/templates/vue-tailwind-sync/.env.example +25 -0
  180. package/templates/vue-tailwind-sync/AGENTS.md +84 -0
  181. package/templates/vue-tailwind-sync/README.md.hbs +105 -0
  182. package/templates/vue-tailwind-sync/index.html.hbs +12 -0
  183. package/templates/vue-tailwind-sync/kora.config.ts +17 -0
  184. package/templates/vue-tailwind-sync/package.json.hbs +45 -0
  185. package/templates/vue-tailwind-sync/server.ts +90 -0
  186. package/templates/vue-tailwind-sync/src/App.vue +245 -0
  187. package/templates/vue-tailwind-sync/src/auth.ts +25 -0
  188. package/templates/vue-tailwind-sync/src/index.css +7 -0
  189. package/templates/vue-tailwind-sync/src/kora-worker.ts +9 -0
  190. package/templates/vue-tailwind-sync/src/main.ts +56 -0
  191. package/templates/vue-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  192. package/templates/vue-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  193. package/templates/vue-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  194. package/templates/vue-tailwind-sync/src/modules/todos/useTodos.ts +32 -0
  195. package/templates/vue-tailwind-sync/src/schema.ts +9 -0
  196. package/templates/vue-tailwind-sync/src/vite-env.d.ts +26 -0
  197. package/templates/vue-tailwind-sync/tsconfig.json.hbs +15 -0
  198. package/templates/vue-tailwind-sync/vite.config.ts +80 -0
  199. package/dist/chunk-3WNFM3QB.js.map +0 -1
  200. package/dist/chunk-PSRM56B7.js.map +0 -1
package/dist/index.d.cts CHANGED
@@ -5,7 +5,7 @@ import * as citty from 'citty';
5
5
  declare const PACKAGE_MANAGERS: readonly ["pnpm", "npm", "yarn", "bun"];
6
6
  type PackageManager = (typeof PACKAGE_MANAGERS)[number];
7
7
  /** Available project templates */
8
- declare const TEMPLATES: readonly ["react-tailwind-sync", "react-tailwind", "react-sync", "react-basic", "tauri-react"];
8
+ declare const TEMPLATES: readonly ["react-tailwind-sync", "react-tailwind", "react-sync", "react-basic", "vue-sync", "vue-basic", "vue-tailwind-sync", "vue-tailwind", "svelte-sync", "svelte-basic", "svelte-tailwind-sync", "svelte-tailwind", "tauri-react"];
9
9
  type TemplateName = (typeof TEMPLATES)[number];
10
10
  /** Metadata for a project template */
11
11
  interface TemplateInfo {
@@ -556,13 +556,13 @@ type DatabaseOption = 'none' | 'sqlite' | 'postgres';
556
556
  type DatabaseProviderOption = 'none' | 'local' | 'supabase' | 'neon' | 'railway' | 'vercel-postgres' | 'custom';
557
557
  interface TemplateSelectionInput {
558
558
  platform: PlatformOption;
559
+ framework: FrameworkOption;
559
560
  tailwind: boolean;
560
561
  sync: boolean;
561
562
  db: DatabaseOption;
562
563
  }
563
564
  /**
564
- * Converts high-level scaffold selections into the currently supported
565
- * concrete template names.
565
+ * Converts high-level scaffold selections into a concrete template name.
566
566
  */
567
567
  declare function determineTemplateFromSelections(input: TemplateSelectionInput): TemplateName;
568
568
  declare function isFrameworkValue(value: string): value is FrameworkOption;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ import * as citty from 'citty';
5
5
  declare const PACKAGE_MANAGERS: readonly ["pnpm", "npm", "yarn", "bun"];
6
6
  type PackageManager = (typeof PACKAGE_MANAGERS)[number];
7
7
  /** Available project templates */
8
- declare const TEMPLATES: readonly ["react-tailwind-sync", "react-tailwind", "react-sync", "react-basic", "tauri-react"];
8
+ declare const TEMPLATES: readonly ["react-tailwind-sync", "react-tailwind", "react-sync", "react-basic", "vue-sync", "vue-basic", "vue-tailwind-sync", "vue-tailwind", "svelte-sync", "svelte-basic", "svelte-tailwind-sync", "svelte-tailwind", "tauri-react"];
9
9
  type TemplateName = (typeof TEMPLATES)[number];
10
10
  /** Metadata for a project template */
11
11
  interface TemplateInfo {
@@ -556,13 +556,13 @@ type DatabaseOption = 'none' | 'sqlite' | 'postgres';
556
556
  type DatabaseProviderOption = 'none' | 'local' | 'supabase' | 'neon' | 'railway' | 'vercel-postgres' | 'custom';
557
557
  interface TemplateSelectionInput {
558
558
  platform: PlatformOption;
559
+ framework: FrameworkOption;
559
560
  tailwind: boolean;
560
561
  sync: boolean;
561
562
  db: DatabaseOption;
562
563
  }
563
564
  /**
564
- * Converts high-level scaffold selections into the currently supported
565
- * concrete template names.
565
+ * Converts high-level scaffold selections into a concrete template name.
566
566
  */
567
567
  declare function determineTemplateFromSelections(input: TemplateSelectionInput): TemplateName;
568
568
  declare function isFrameworkValue(value: string): value is FrameworkOption;
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  writeDockerfileArtifact,
28
28
  writeFlyTomlArtifact,
29
29
  writeRailwayJsonArtifact
30
- } from "./chunk-SOTZIWIF.js";
30
+ } from "./chunk-EOWLAAIV.js";
31
31
  import {
32
32
  ClackPromptClient,
33
33
  CliError,
@@ -54,7 +54,8 @@ import {
54
54
  saveResolvedPreferences,
55
55
  shouldSavePreferences,
56
56
  validateProjectName
57
- } from "./chunk-3WNFM3QB.js";
57
+ } from "./chunk-B5YS4STN.js";
58
+ import "./chunk-5NI2FEQL.js";
58
59
  export {
59
60
  ClackPromptClient,
60
61
  CliError,
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/studio/lab-manager.ts
4
+ import { mkdtempSync, rmSync } from "fs";
5
+ import { tmpdir } from "os";
6
+ import { join } from "path";
7
+ import { op as atomicOp, defineSchema, t } from "@korajs/core";
8
+ function defaultLabSchema() {
9
+ return defineSchema({
10
+ version: 1,
11
+ collections: {
12
+ tasks: {
13
+ fields: {
14
+ title: t.string(),
15
+ assignee: t.string().optional(),
16
+ priority: t.enum(["low", "medium", "high"]).default("medium"),
17
+ done: t.boolean().default(false),
18
+ tags: t.array(t.string()).default([]),
19
+ points: t.number().default(0)
20
+ }
21
+ }
22
+ }
23
+ });
24
+ }
25
+ var DEFAULT_CHAOS = {
26
+ dropRate: 0,
27
+ duplicateRate: 0,
28
+ reorderRate: 0,
29
+ maxLatency: 0
30
+ };
31
+ var FORWARDED_EVENTS = [
32
+ "operation:created",
33
+ "operation:applied",
34
+ "merge:started",
35
+ "merge:conflict",
36
+ "merge:completed",
37
+ "sync:connected",
38
+ "sync:disconnected",
39
+ "sync:sent",
40
+ "sync:received",
41
+ "sync:apply-failed"
42
+ ];
43
+ var MAX_EVENT_BUFFER = 500;
44
+ var LabManager = class {
45
+ constructor(schema) {
46
+ this.schema = schema;
47
+ }
48
+ schema;
49
+ devices = /* @__PURE__ */ new Map();
50
+ server = null;
51
+ harness = null;
52
+ tmpDir = null;
53
+ eventSeq = 0;
54
+ eventBuffer = [];
55
+ eventListeners = /* @__PURE__ */ new Set();
56
+ deviceCounter = 0;
57
+ /** Load the harness lazily so file-mode Studio never needs @korajs/test. */
58
+ async loadHarness() {
59
+ if (this.harness) {
60
+ return this.harness;
61
+ }
62
+ try {
63
+ const testPkg = await import("@korajs/test");
64
+ const serverInternal = await import("@korajs/server/internal");
65
+ this.harness = {
66
+ TestServer: testPkg.TestServer,
67
+ TestDevice: testPkg.TestDevice,
68
+ checkConvergence: testPkg.checkConvergence,
69
+ ChaosTransport: testPkg.ChaosTransport,
70
+ createServerTransportPair: serverInternal.createServerTransportPair
71
+ };
72
+ return this.harness;
73
+ } catch (error) {
74
+ throw new Error(
75
+ `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)}`
76
+ );
77
+ }
78
+ }
79
+ async start(initialDevices = 2) {
80
+ const harness = await this.loadHarness();
81
+ this.tmpDir = mkdtempSync(join(tmpdir(), "kora-studio-lab-"));
82
+ this.server = new harness.TestServer(this.schema);
83
+ for (let i = 0; i < initialDevices; i++) {
84
+ await this.addDevice();
85
+ }
86
+ }
87
+ async addDevice(name) {
88
+ const harness = await this.loadHarness();
89
+ if (!this.server || !this.tmpDir) {
90
+ throw new Error("Lab is not started");
91
+ }
92
+ const deviceName = name?.trim() || `device-${String.fromCharCode(65 + this.deviceCounter)}`;
93
+ this.deviceCounter++;
94
+ if (this.devices.has(deviceName)) {
95
+ throw new Error(`Device "${deviceName}" already exists`);
96
+ }
97
+ const chaos = { ...DEFAULT_CHAOS };
98
+ const entry = { chaos };
99
+ const device = new harness.TestDevice({
100
+ name: deviceName,
101
+ schema: this.schema,
102
+ server: this.server,
103
+ // Read the CURRENT chaos settings each time a connection is made, so
104
+ // toggling chaos + reconnecting applies it without rebuilding the device.
105
+ createTransportPair: () => {
106
+ const pair = harness.createServerTransportPair();
107
+ const hasChaos = entry.chaos.dropRate > 0 || entry.chaos.duplicateRate > 0 || entry.chaos.reorderRate > 0 || entry.chaos.maxLatency > 0;
108
+ return {
109
+ client: hasChaos ? new harness.ChaosTransport(pair.client, { ...entry.chaos }) : pair.client,
110
+ serverTransport: pair.server
111
+ };
112
+ },
113
+ tmpDir: this.tmpDir
114
+ });
115
+ await device.open();
116
+ const unsubscribes = FORWARDED_EVENTS.map(
117
+ (type) => device.emitter.on(type, (event) => this.pushEvent(deviceName, type, event))
118
+ );
119
+ this.devices.set(deviceName, { device, chaos, unsubscribes });
120
+ this.pushEvent(deviceName, "lab:device-added", {});
121
+ return this.deviceState(deviceName);
122
+ }
123
+ listDevices() {
124
+ return [...this.devices.keys()].map((name) => this.deviceState(name));
125
+ }
126
+ deviceState(name) {
127
+ const entry = this.mustGet(name);
128
+ return {
129
+ name,
130
+ nodeId: entry.device.getNodeId(),
131
+ connected: entry.device.isConnected(),
132
+ chaos: { ...entry.chaos },
133
+ dbPath: join(this.tmpDir ?? "", `test-device-${name}.db`),
134
+ pendingOperations: entry.device.getSyncEngine()?.getStatus().pendingOperations ?? 0
135
+ };
136
+ }
137
+ async connect(name) {
138
+ await this.mustGet(name).device.sync();
139
+ this.pushEvent(name, "lab:connected", {});
140
+ }
141
+ async disconnect(name) {
142
+ await this.mustGet(name).device.disconnect();
143
+ this.pushEvent(name, "lab:disconnected", {});
144
+ }
145
+ async sync(name) {
146
+ await this.mustGet(name).device.sync();
147
+ }
148
+ setChaos(name, chaos) {
149
+ const entry = this.mustGet(name);
150
+ entry.chaos.dropRate = clamp01(chaos.dropRate ?? entry.chaos.dropRate);
151
+ entry.chaos.duplicateRate = clamp01(chaos.duplicateRate ?? entry.chaos.duplicateRate);
152
+ entry.chaos.reorderRate = clamp01(chaos.reorderRate ?? entry.chaos.reorderRate);
153
+ entry.chaos.maxLatency = Math.max(0, chaos.maxLatency ?? entry.chaos.maxLatency);
154
+ this.pushEvent(name, "lab:chaos-changed", { ...entry.chaos });
155
+ return { ...entry.chaos };
156
+ }
157
+ async insert(name, collection, data) {
158
+ return this.mustGet(name).device.collection(collection).insert(data);
159
+ }
160
+ async update(name, collection, recordId, data, increments) {
161
+ const payload = { ...data };
162
+ for (const [field, delta] of Object.entries(increments ?? {})) {
163
+ payload[field] = atomicOp.increment(delta);
164
+ }
165
+ return this.mustGet(name).device.collection(collection).update(recordId, payload);
166
+ }
167
+ async delete(name, collection, recordId) {
168
+ await this.mustGet(name).device.collection(collection).delete(recordId);
169
+ }
170
+ async convergence() {
171
+ const harness = await this.loadHarness();
172
+ const devices = [...this.devices.values()].map((e) => e.device);
173
+ if (devices.length < 2) {
174
+ return { converged: true, deviceCount: devices.length, differences: [] };
175
+ }
176
+ const result = await harness.checkConvergence(devices, this.schema);
177
+ return {
178
+ converged: result.converged,
179
+ deviceCount: devices.length,
180
+ differences: result.differences.map(
181
+ (d) => d.description ?? `collection "${d.collection}" differs`
182
+ )
183
+ };
184
+ }
185
+ serverOperationCount() {
186
+ return this.server?.getAllOperations().length ?? 0;
187
+ }
188
+ getSchema() {
189
+ return this.schema;
190
+ }
191
+ /** Recent events (for initial page load), oldest first. */
192
+ recentEvents() {
193
+ return [...this.eventBuffer];
194
+ }
195
+ onEvent(listener) {
196
+ this.eventListeners.add(listener);
197
+ return () => this.eventListeners.delete(listener);
198
+ }
199
+ async close() {
200
+ for (const [, entry] of this.devices) {
201
+ for (const unsub of entry.unsubscribes) {
202
+ unsub();
203
+ }
204
+ await entry.device.close().catch(() => {
205
+ });
206
+ }
207
+ this.devices.clear();
208
+ await this.server?.close().catch(() => {
209
+ });
210
+ this.server = null;
211
+ if (this.tmpDir) {
212
+ rmSync(this.tmpDir, { recursive: true, force: true });
213
+ this.tmpDir = null;
214
+ }
215
+ }
216
+ mustGet(name) {
217
+ const entry = this.devices.get(name);
218
+ if (!entry) {
219
+ throw new Error(`Unknown device "${name}"`);
220
+ }
221
+ return entry;
222
+ }
223
+ pushEvent(device, type, payload) {
224
+ const event = {
225
+ seq: ++this.eventSeq,
226
+ at: Date.now(),
227
+ device,
228
+ type,
229
+ summary: summarizeEvent(type, payload)
230
+ };
231
+ this.eventBuffer.push(event);
232
+ if (this.eventBuffer.length > MAX_EVENT_BUFFER) {
233
+ this.eventBuffer.shift();
234
+ }
235
+ for (const listener of this.eventListeners) {
236
+ listener(event);
237
+ }
238
+ }
239
+ };
240
+ function clamp01(n) {
241
+ return Math.min(1, Math.max(0, n));
242
+ }
243
+ function summarizeEvent(type, payload) {
244
+ const op = payload.operation;
245
+ switch (type) {
246
+ case "operation:created":
247
+ return op ? `${op.type} ${op.collection}/${String(op.recordId).slice(0, 8)}\u2026 ${op.data ? JSON.stringify(op.data).slice(0, 60) : ""}` : "operation created";
248
+ case "operation:applied":
249
+ return op ? `applied ${op.type} ${op.collection}/${String(op.recordId).slice(0, 8)}\u2026` : "applied";
250
+ case "merge:conflict":
251
+ return "merge conflict resolved";
252
+ case "merge:started":
253
+ return "merge started";
254
+ case "merge:completed":
255
+ return "merge completed";
256
+ case "sync:sent": {
257
+ const count = Array.isArray(payload.operations) ? payload.operations.length : 0;
258
+ return `sent ${count} op(s)`;
259
+ }
260
+ case "sync:received": {
261
+ const count = Array.isArray(payload.operations) ? payload.operations.length : 0;
262
+ return `received ${count} op(s)`;
263
+ }
264
+ case "sync:connected":
265
+ return "connected to server";
266
+ case "sync:disconnected":
267
+ return "disconnected";
268
+ case "sync:apply-failed":
269
+ return "APPLY FAILED";
270
+ case "lab:chaos-changed":
271
+ return `chaos \u2192 drop ${payload.dropRate} dup ${payload.duplicateRate} reorder ${payload.reorderRate} latency ${payload.maxLatency}ms`;
272
+ default:
273
+ return type.replace("lab:", "");
274
+ }
275
+ }
276
+ export {
277
+ LabManager,
278
+ defaultLabSchema
279
+ };
280
+ //# sourceMappingURL=lab-manager-KUDII6BT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/studio/lab-manager.ts"],"sourcesContent":["import { mkdtempSync, rmSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport type { Operation, SchemaDefinition } from '@korajs/core'\nimport { op as atomicOp, defineSchema, t } from '@korajs/core'\n\n/**\n * Default Lab schema: covers every merge tier so the Lab can demonstrate the\n * whole system — scalar LWW (title/assignee/priority/done), add-wins arrays\n * (tags), and atomic increment composition (points).\n */\nexport function defaultLabSchema(): SchemaDefinition {\n\treturn defineSchema({\n\t\tversion: 1,\n\t\tcollections: {\n\t\t\ttasks: {\n\t\t\t\tfields: {\n\t\t\t\t\ttitle: t.string(),\n\t\t\t\t\tassignee: t.string().optional(),\n\t\t\t\t\tpriority: t.enum(['low', 'medium', 'high']).default('medium'),\n\t\t\t\t\tdone: t.boolean().default(false),\n\t\t\t\t\ttags: t.array(t.string()).default([]),\n\t\t\t\t\tpoints: t.number().default(0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n}\n\n/**\n * Kora Studio Lab: an interactive, in-process multi-device sync laboratory.\n *\n * Every device is a REAL Kora client (its own SQLite store, sync engine, merge\n * pipeline) talking to a REAL sync server over in-memory transports, optionally\n * wrapped in a chaos transport. Nothing is simulated at the data layer, so what\n * the Lab shows is evidence, not illustration: conflicts, convergence, chaos\n * recovery, and atomic-op composition all run the exact code that ships.\n *\n * The Lab only ever touches throwaway databases in a temp directory — it can\n * never write to a user's real data.\n */\n\n/** Per-device chaos settings; applied on the device's NEXT connect. */\nexport interface LabChaosConfig {\n\tdropRate: number\n\tduplicateRate: number\n\treorderRate: number\n\tmaxLatency: number\n}\n\nexport interface LabDeviceState {\n\tname: string\n\tnodeId: string\n\tconnected: boolean\n\tchaos: LabChaosConfig\n\t/** Path of this device's SQLite DB — Studio's readers attach here. */\n\tdbPath: string\n\tpendingOperations: number\n}\n\nexport interface LabEvent {\n\tseq: number\n\tat: number\n\tdevice: string\n\ttype: string\n\tsummary: string\n}\n\nexport interface LabConvergenceReport {\n\tconverged: boolean\n\tdeviceCount: number\n\tdifferences: string[]\n}\n\n/** Structural interfaces for the lazily-imported @korajs/test harness. */\ninterface HarnessDevice {\n\treadonly name: string\n\treadonly emitter: {\n\t\ton(type: string, listener: (event: Record<string, unknown>) => void): () => void\n\t}\n\topen(): Promise<void>\n\tsync(): Promise<void>\n\tdisconnect(): Promise<void>\n\tclose(): Promise<void>\n\tcollection(name: string): {\n\t\tinsert(data: Record<string, unknown>): Promise<Record<string, unknown>>\n\t\tupdate(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>\n\t\tdelete(id: string): Promise<void>\n\t}\n\tgetNodeId(): string\n\tgetSyncEngine(): { getStatus(): { pendingOperations: number } } | null\n\tisConnected(): boolean\n}\n\ninterface Harness {\n\tTestServer: new (\n\t\tschema: SchemaDefinition,\n\t) => {\n\t\thandleConnection(transport: unknown): string\n\t\tgetAllOperations(): Operation[]\n\t\tgetConnectionCount(): number\n\t\tclose(): Promise<void>\n\t}\n\tTestDevice: new (options: {\n\t\tname: string\n\t\tschema: SchemaDefinition\n\t\tserver: unknown\n\t\tcreateTransportPair: () => { client: unknown; serverTransport: unknown }\n\t\ttmpDir: string\n\t}) => HarnessDevice\n\tcheckConvergence(\n\t\tdevices: unknown[],\n\t\tschema: SchemaDefinition,\n\t): Promise<{\n\t\tconverged: boolean\n\t\tdifferences: Array<{ collection: string; description?: string }>\n\t}>\n\tChaosTransport: new (inner: unknown, config: Record<string, unknown>) => unknown\n\t/** Returns { client, server } — note the property name is `server`. */\n\tcreateServerTransportPair: () => { client: unknown; server: unknown }\n}\n\nconst DEFAULT_CHAOS: LabChaosConfig = {\n\tdropRate: 0,\n\tduplicateRate: 0,\n\treorderRate: 0,\n\tmaxLatency: 0,\n}\n\n/** Event types forwarded into the Lab's live feed. */\nconst FORWARDED_EVENTS = [\n\t'operation:created',\n\t'operation:applied',\n\t'merge:started',\n\t'merge:conflict',\n\t'merge:completed',\n\t'sync:connected',\n\t'sync:disconnected',\n\t'sync:sent',\n\t'sync:received',\n\t'sync:apply-failed',\n] as const\n\nconst MAX_EVENT_BUFFER = 500\n\nexport class LabManager {\n\tprivate readonly devices = new Map<\n\t\tstring,\n\t\t{ device: HarnessDevice; chaos: LabChaosConfig; unsubscribes: Array<() => void> }\n\t>()\n\tprivate server: InstanceType<Harness['TestServer']> | null = null\n\tprivate harness: Harness | null = null\n\tprivate tmpDir: string | null = null\n\tprivate eventSeq = 0\n\tprivate readonly eventBuffer: LabEvent[] = []\n\tprivate readonly eventListeners = new Set<(event: LabEvent) => void>()\n\tprivate deviceCounter = 0\n\n\tconstructor(private readonly schema: SchemaDefinition) {}\n\n\t/** Load the harness lazily so file-mode Studio never needs @korajs/test. */\n\tprivate async loadHarness(): Promise<Harness> {\n\t\tif (this.harness) {\n\t\t\treturn this.harness\n\t\t}\n\t\ttry {\n\t\t\tconst testPkg = (await import('@korajs/test')) as unknown as Record<string, unknown>\n\t\t\tconst serverInternal = (await import('@korajs/server/internal')) as unknown as Record<\n\t\t\t\tstring,\n\t\t\t\tunknown\n\t\t\t>\n\t\t\tthis.harness = {\n\t\t\t\tTestServer: testPkg.TestServer as Harness['TestServer'],\n\t\t\t\tTestDevice: testPkg.TestDevice as Harness['TestDevice'],\n\t\t\t\tcheckConvergence: testPkg.checkConvergence as Harness['checkConvergence'],\n\t\t\t\tChaosTransport: testPkg.ChaosTransport as Harness['ChaosTransport'],\n\t\t\t\tcreateServerTransportPair:\n\t\t\t\t\tserverInternal.createServerTransportPair as Harness['createServerTransportPair'],\n\t\t\t}\n\t\t\treturn this.harness\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t`Kora Studio Lab needs \"@korajs/test\" and \"@korajs/server\" installed (pnpm add -D @korajs/test @korajs/server). Underlying error: ${\n\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t}`,\n\t\t\t)\n\t\t}\n\t}\n\n\tasync start(initialDevices = 2): Promise<void> {\n\t\tconst harness = await this.loadHarness()\n\t\tthis.tmpDir = mkdtempSync(join(tmpdir(), 'kora-studio-lab-'))\n\t\tthis.server = new harness.TestServer(this.schema)\n\t\tfor (let i = 0; i < initialDevices; i++) {\n\t\t\tawait this.addDevice()\n\t\t}\n\t}\n\n\tasync addDevice(name?: string): Promise<LabDeviceState> {\n\t\tconst harness = await this.loadHarness()\n\t\tif (!this.server || !this.tmpDir) {\n\t\t\tthrow new Error('Lab is not started')\n\t\t}\n\t\tconst deviceName = name?.trim() || `device-${String.fromCharCode(65 + this.deviceCounter)}`\n\t\tthis.deviceCounter++\n\t\tif (this.devices.has(deviceName)) {\n\t\t\tthrow new Error(`Device \"${deviceName}\" already exists`)\n\t\t}\n\n\t\tconst chaos: LabChaosConfig = { ...DEFAULT_CHAOS }\n\t\tconst entry = { chaos }\n\t\tconst device = new harness.TestDevice({\n\t\t\tname: deviceName,\n\t\t\tschema: this.schema,\n\t\t\tserver: this.server,\n\t\t\t// Read the CURRENT chaos settings each time a connection is made, so\n\t\t\t// toggling chaos + reconnecting applies it without rebuilding the device.\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = harness.createServerTransportPair()\n\t\t\t\tconst hasChaos =\n\t\t\t\t\tentry.chaos.dropRate > 0 ||\n\t\t\t\t\tentry.chaos.duplicateRate > 0 ||\n\t\t\t\t\tentry.chaos.reorderRate > 0 ||\n\t\t\t\t\tentry.chaos.maxLatency > 0\n\t\t\t\treturn {\n\t\t\t\t\tclient: hasChaos\n\t\t\t\t\t\t? new harness.ChaosTransport(pair.client, { ...entry.chaos })\n\t\t\t\t\t\t: pair.client,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t},\n\t\t\ttmpDir: this.tmpDir,\n\t\t})\n\t\tawait device.open()\n\n\t\tconst unsubscribes = FORWARDED_EVENTS.map((type) =>\n\t\t\tdevice.emitter.on(type, (event) => this.pushEvent(deviceName, type, event)),\n\t\t)\n\n\t\tthis.devices.set(deviceName, { device, chaos, unsubscribes })\n\t\tthis.pushEvent(deviceName, 'lab:device-added', {})\n\t\treturn this.deviceState(deviceName)\n\t}\n\n\tlistDevices(): LabDeviceState[] {\n\t\treturn [...this.devices.keys()].map((name) => this.deviceState(name))\n\t}\n\n\tdeviceState(name: string): LabDeviceState {\n\t\tconst entry = this.mustGet(name)\n\t\treturn {\n\t\t\tname,\n\t\t\tnodeId: entry.device.getNodeId(),\n\t\t\tconnected: entry.device.isConnected(),\n\t\t\tchaos: { ...entry.chaos },\n\t\t\tdbPath: join(this.tmpDir ?? '', `test-device-${name}.db`),\n\t\t\tpendingOperations: entry.device.getSyncEngine()?.getStatus().pendingOperations ?? 0,\n\t\t}\n\t}\n\n\tasync connect(name: string): Promise<void> {\n\t\tawait this.mustGet(name).device.sync()\n\t\tthis.pushEvent(name, 'lab:connected', {})\n\t}\n\n\tasync disconnect(name: string): Promise<void> {\n\t\tawait this.mustGet(name).device.disconnect()\n\t\tthis.pushEvent(name, 'lab:disconnected', {})\n\t}\n\n\tasync sync(name: string): Promise<void> {\n\t\tawait this.mustGet(name).device.sync()\n\t}\n\n\tsetChaos(name: string, chaos: Partial<LabChaosConfig>): LabChaosConfig {\n\t\tconst entry = this.mustGet(name)\n\t\tentry.chaos.dropRate = clamp01(chaos.dropRate ?? entry.chaos.dropRate)\n\t\tentry.chaos.duplicateRate = clamp01(chaos.duplicateRate ?? entry.chaos.duplicateRate)\n\t\tentry.chaos.reorderRate = clamp01(chaos.reorderRate ?? entry.chaos.reorderRate)\n\t\tentry.chaos.maxLatency = Math.max(0, chaos.maxLatency ?? entry.chaos.maxLatency)\n\t\tthis.pushEvent(name, 'lab:chaos-changed', { ...entry.chaos })\n\t\treturn { ...entry.chaos }\n\t}\n\n\tasync insert(\n\t\tname: string,\n\t\tcollection: string,\n\t\tdata: Record<string, unknown>,\n\t): Promise<Record<string, unknown>> {\n\t\treturn this.mustGet(name).device.collection(collection).insert(data)\n\t}\n\n\tasync update(\n\t\tname: string,\n\t\tcollection: string,\n\t\trecordId: string,\n\t\tdata: Record<string, unknown>,\n\t\tincrements?: Record<string, number>,\n\t): Promise<Record<string, unknown>> {\n\t\t// Intent-preserving atomic increments: concurrent increments on two\n\t\t// devices COMPOSE to the sum instead of last-write-wins — the Lab's most\n\t\t// striking demonstration.\n\t\tconst payload: Record<string, unknown> = { ...data }\n\t\tfor (const [field, delta] of Object.entries(increments ?? {})) {\n\t\t\tpayload[field] = atomicOp.increment(delta)\n\t\t}\n\t\treturn this.mustGet(name).device.collection(collection).update(recordId, payload)\n\t}\n\n\tasync delete(name: string, collection: string, recordId: string): Promise<void> {\n\t\tawait this.mustGet(name).device.collection(collection).delete(recordId)\n\t}\n\n\tasync convergence(): Promise<LabConvergenceReport> {\n\t\tconst harness = await this.loadHarness()\n\t\tconst devices = [...this.devices.values()].map((e) => e.device)\n\t\tif (devices.length < 2) {\n\t\t\treturn { converged: true, deviceCount: devices.length, differences: [] }\n\t\t}\n\t\tconst result = await harness.checkConvergence(devices, this.schema)\n\t\treturn {\n\t\t\tconverged: result.converged,\n\t\t\tdeviceCount: devices.length,\n\t\t\tdifferences: result.differences.map(\n\t\t\t\t(d) => d.description ?? `collection \"${d.collection}\" differs`,\n\t\t\t),\n\t\t}\n\t}\n\n\tserverOperationCount(): number {\n\t\treturn this.server?.getAllOperations().length ?? 0\n\t}\n\n\tgetSchema(): SchemaDefinition {\n\t\treturn this.schema\n\t}\n\n\t/** Recent events (for initial page load), oldest first. */\n\trecentEvents(): LabEvent[] {\n\t\treturn [...this.eventBuffer]\n\t}\n\n\tonEvent(listener: (event: LabEvent) => void): () => void {\n\t\tthis.eventListeners.add(listener)\n\t\treturn () => this.eventListeners.delete(listener)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tfor (const [, entry] of this.devices) {\n\t\t\tfor (const unsub of entry.unsubscribes) {\n\t\t\t\tunsub()\n\t\t\t}\n\t\t\tawait entry.device.close().catch(() => {})\n\t\t}\n\t\tthis.devices.clear()\n\t\tawait this.server?.close().catch(() => {})\n\t\tthis.server = null\n\t\tif (this.tmpDir) {\n\t\t\trmSync(this.tmpDir, { recursive: true, force: true })\n\t\t\tthis.tmpDir = null\n\t\t}\n\t}\n\n\tprivate mustGet(name: string): { device: HarnessDevice; chaos: LabChaosConfig } {\n\t\tconst entry = this.devices.get(name)\n\t\tif (!entry) {\n\t\t\tthrow new Error(`Unknown device \"${name}\"`)\n\t\t}\n\t\treturn entry\n\t}\n\n\tprivate pushEvent(device: string, type: string, payload: Record<string, unknown>): void {\n\t\tconst event: LabEvent = {\n\t\t\tseq: ++this.eventSeq,\n\t\t\tat: Date.now(),\n\t\t\tdevice,\n\t\t\ttype,\n\t\t\tsummary: summarizeEvent(type, payload),\n\t\t}\n\t\tthis.eventBuffer.push(event)\n\t\tif (this.eventBuffer.length > MAX_EVENT_BUFFER) {\n\t\t\tthis.eventBuffer.shift()\n\t\t}\n\t\tfor (const listener of this.eventListeners) {\n\t\t\tlistener(event)\n\t\t}\n\t}\n}\n\nfunction clamp01(n: number): number {\n\treturn Math.min(1, Math.max(0, n))\n}\n\nfunction summarizeEvent(type: string, payload: Record<string, unknown>): string {\n\tconst op = payload.operation as\n\t\t| { type?: string; collection?: string; recordId?: string; data?: Record<string, unknown> }\n\t\t| undefined\n\tswitch (type) {\n\t\tcase 'operation:created':\n\t\t\treturn op\n\t\t\t\t? `${op.type} ${op.collection}/${String(op.recordId).slice(0, 8)}… ${op.data ? JSON.stringify(op.data).slice(0, 60) : ''}`\n\t\t\t\t: 'operation created'\n\t\tcase 'operation:applied':\n\t\t\treturn op\n\t\t\t\t? `applied ${op.type} ${op.collection}/${String(op.recordId).slice(0, 8)}…`\n\t\t\t\t: 'applied'\n\t\tcase 'merge:conflict':\n\t\t\treturn 'merge conflict resolved'\n\t\tcase 'merge:started':\n\t\t\treturn 'merge started'\n\t\tcase 'merge:completed':\n\t\t\treturn 'merge completed'\n\t\tcase 'sync:sent': {\n\t\t\tconst count = Array.isArray(payload.operations) ? payload.operations.length : 0\n\t\t\treturn `sent ${count} op(s)`\n\t\t}\n\t\tcase 'sync:received': {\n\t\t\tconst count = Array.isArray(payload.operations) ? payload.operations.length : 0\n\t\t\treturn `received ${count} op(s)`\n\t\t}\n\t\tcase 'sync:connected':\n\t\t\treturn 'connected to server'\n\t\tcase 'sync:disconnected':\n\t\t\treturn 'disconnected'\n\t\tcase 'sync:apply-failed':\n\t\t\treturn 'APPLY FAILED'\n\t\tcase 'lab:chaos-changed':\n\t\t\treturn `chaos → drop ${payload.dropRate} dup ${payload.duplicateRate} reorder ${payload.reorderRate} latency ${payload.maxLatency}ms`\n\t\tdefault:\n\t\t\treturn type.replace('lab:', '')\n\t}\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadSchemaDefinition
4
+ } from "./chunk-6C4BHSRA.js";
5
+ import "./chunk-5NI2FEQL.js";
6
+ export {
7
+ loadSchemaDefinition
8
+ };
9
+ //# sourceMappingURL=schema-loader-GGHECMTM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/studio/spectator-manager.ts
4
+ import { mkdtempSync, rmSync } from "fs";
5
+ import { tmpdir } from "os";
6
+ import { join } from "path";
7
+ var FORWARDED_EVENTS = [
8
+ "merge:started",
9
+ "merge:conflict",
10
+ "merge:completed",
11
+ "sync:connected",
12
+ "sync:disconnected",
13
+ "sync:received",
14
+ "sync:apply-failed"
15
+ ];
16
+ var MAX_EVENT_BUFFER = 500;
17
+ var SpectatorManager = class {
18
+ constructor(options) {
19
+ this.options = options;
20
+ this.tmpDir = mkdtempSync(join(tmpdir(), "kora-studio-spectator-"));
21
+ this.dbPath = join(this.tmpDir, "spectator-replica.db");
22
+ }
23
+ options;
24
+ runtime = null;
25
+ tmpDir = null;
26
+ connected = false;
27
+ operationsReceived = 0;
28
+ lastEventAt = null;
29
+ eventSeq = 0;
30
+ eventBuffer = [];
31
+ eventListeners = /* @__PURE__ */ new Set();
32
+ dbPath;
33
+ async start() {
34
+ let mods;
35
+ try {
36
+ mods = {
37
+ Store: (await import("@korajs/store")).Store,
38
+ BetterSqlite3Adapter: (await import("@korajs/store/better-sqlite3")).BetterSqlite3Adapter,
39
+ MergeEngine: (await import("@korajs/merge")).MergeEngine,
40
+ SyncEngine: (await import("@korajs/sync")).SyncEngine,
41
+ WebSocketTransport: (await import("@korajs/sync")).WebSocketTransport,
42
+ testing: await import("korajs/testing"),
43
+ SimpleEventEmitter: (await import("@korajs/core/internal")).SimpleEventEmitter
44
+ };
45
+ } catch (error) {
46
+ throw new Error(
47
+ `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)}`
48
+ );
49
+ }
50
+ const emitter = new mods.SimpleEventEmitter();
51
+ const adapter = new mods.BetterSqlite3Adapter(this.dbPath);
52
+ const store = new mods.Store({ schema: this.options.schema, adapter, emitter });
53
+ await store.open();
54
+ const mergeEngine = new mods.MergeEngine();
55
+ const pipeline = new mods.testing.ApplyPipeline({ store, mergeEngine, emitter });
56
+ store.setLocalMutationHandler(pipeline);
57
+ const unsubscribeAudit = mods.testing.wireAuditPersistence(store, emitter);
58
+ const syncStore = new mods.testing.MergeAwareSyncStore(store, mergeEngine, emitter);
59
+ const engine = new mods.SyncEngine({
60
+ transport: new mods.WebSocketTransport(),
61
+ store: syncStore,
62
+ queueStorage: new mods.testing.StoreQueueStorage(adapter),
63
+ syncState: new mods.testing.StoreSyncStatePersistence(store),
64
+ config: {
65
+ url: this.options.url,
66
+ schemaVersion: this.options.schemaVersion ?? this.options.schema.version,
67
+ ...this.options.token ? { auth: async () => ({ token: this.options.token }) } : {}
68
+ },
69
+ emitter
70
+ });
71
+ const unsubscribers = FORWARDED_EVENTS.map(
72
+ (type) => emitter.on(type, (event) => {
73
+ this.lastEventAt = Date.now();
74
+ if (type === "sync:connected") {
75
+ this.connected = true;
76
+ }
77
+ if (type === "sync:disconnected") {
78
+ this.connected = false;
79
+ }
80
+ if (type === "sync:received" && Array.isArray(event.operations)) {
81
+ this.operationsReceived += event.operations.length;
82
+ }
83
+ this.pushEvent(type, event);
84
+ })
85
+ );
86
+ unsubscribers.push(unsubscribeAudit);
87
+ this.runtime = { store, engine, unsubscribers };
88
+ await engine.start();
89
+ }
90
+ status() {
91
+ return {
92
+ url: this.options.url,
93
+ connected: this.connected,
94
+ operationsReceived: this.operationsReceived,
95
+ lastEventAt: this.lastEventAt,
96
+ dbPath: this.dbPath
97
+ };
98
+ }
99
+ recentEvents() {
100
+ return [...this.eventBuffer];
101
+ }
102
+ onEvent(listener) {
103
+ this.eventListeners.add(listener);
104
+ return () => this.eventListeners.delete(listener);
105
+ }
106
+ async close() {
107
+ if (this.runtime) {
108
+ for (const unsub of this.runtime.unsubscribers) {
109
+ unsub();
110
+ }
111
+ await this.runtime.engine.stop().catch(() => {
112
+ });
113
+ await this.runtime.store.close().catch(() => {
114
+ });
115
+ this.runtime = null;
116
+ }
117
+ if (this.tmpDir) {
118
+ rmSync(this.tmpDir, { recursive: true, force: true });
119
+ this.tmpDir = null;
120
+ }
121
+ }
122
+ pushEvent(type, payload) {
123
+ const summary = type === "sync:received" && Array.isArray(payload.operations) ? `received ${payload.operations.length} op(s)` : type.replace("sync:", "").replace("merge:", "merge ");
124
+ const event = {
125
+ seq: ++this.eventSeq,
126
+ at: Date.now(),
127
+ type,
128
+ summary
129
+ };
130
+ this.eventBuffer.push(event);
131
+ if (this.eventBuffer.length > MAX_EVENT_BUFFER) {
132
+ this.eventBuffer.shift();
133
+ }
134
+ for (const listener of this.eventListeners) {
135
+ listener(event);
136
+ }
137
+ }
138
+ };
139
+ export {
140
+ SpectatorManager
141
+ };
142
+ //# sourceMappingURL=spectator-manager-E2LH2P54.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/studio/spectator-manager.ts"],"sourcesContent":["import { mkdtempSync, rmSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport type { SchemaDefinition } from '@korajs/core'\n\n/**\n * Kora Studio SPECTATOR: live, read-only inspection of a production sync\n * server.\n *\n * The spectator is a REAL Kora client — its own store, merge pipeline, and\n * sync engine — connected to the target server over the real WebSocket\n * protocol. It materializes its own replica into a throwaway database, which\n * every Studio view (records, per-field writers, op log, DAG, time travel,\n * merge audit) then reads. Nothing is simulated: what Studio shows is exactly\n * what any device syncing against that server computes.\n *\n * Read-only by construction: the spectator exposes NO mutation API, so it can\n * never push an operation to the server. It only receives, applies, and\n * observes. The server sees an ordinary client whose version vector starts\n * empty (so it receives the full history — which is what makes production\n * time travel possible) and which never sends operations.\n */\n\nexport interface SpectatorStatus {\n\turl: string\n\tconnected: boolean\n\toperationsReceived: number\n\tlastEventAt: number | null\n\tdbPath: string\n}\n\nexport interface SpectatorEvent {\n\tseq: number\n\tat: number\n\ttype: string\n\tsummary: string\n}\n\nexport interface SpectatorOptions {\n\turl: string\n\tschema: SchemaDefinition\n\t/** Bearer token forwarded through the sync auth handshake. */\n\ttoken?: string\n\t/** Client schema version override. Defaults to schema.version. */\n\tschemaVersion?: number\n}\n\n// NOTE: 'operation:applied' is in the event spec but not yet emitted by the\n// store — received-op counting uses 'sync:received' batches instead.\nconst FORWARDED_EVENTS = [\n\t'merge:started',\n\t'merge:conflict',\n\t'merge:completed',\n\t'sync:connected',\n\t'sync:disconnected',\n\t'sync:received',\n\t'sync:apply-failed',\n] as const\n\nconst MAX_EVENT_BUFFER = 500\n\n/** Structural types for the lazily-imported runtime packages. */\ninterface SpectatorRuntime {\n\tstore: {\n\t\topen(): Promise<void>\n\t\tclose(): Promise<void>\n\t\tsetLocalMutationHandler(handler: unknown): void\n\t}\n\tengine: { start(): Promise<void>; stop(): Promise<void> }\n\tunsubscribers: Array<() => void>\n}\n\nexport class SpectatorManager {\n\tprivate runtime: SpectatorRuntime | null = null\n\tprivate tmpDir: string | null = null\n\tprivate connected = false\n\tprivate operationsReceived = 0\n\tprivate lastEventAt: number | null = null\n\tprivate eventSeq = 0\n\tprivate readonly eventBuffer: SpectatorEvent[] = []\n\tprivate readonly eventListeners = new Set<(event: SpectatorEvent) => void>()\n\treadonly dbPath: string\n\n\tconstructor(private readonly options: SpectatorOptions) {\n\t\tthis.tmpDir = mkdtempSync(join(tmpdir(), 'kora-studio-spectator-'))\n\t\tthis.dbPath = join(this.tmpDir, 'spectator-replica.db')\n\t}\n\n\tasync start(): Promise<void> {\n\t\tlet mods: {\n\t\t\tStore: typeof import('@korajs/store').Store\n\t\t\tBetterSqlite3Adapter: typeof import('@korajs/store/better-sqlite3').BetterSqlite3Adapter\n\t\t\tMergeEngine: typeof import('@korajs/merge').MergeEngine\n\t\t\tSyncEngine: typeof import('@korajs/sync').SyncEngine\n\t\t\tWebSocketTransport: typeof import('@korajs/sync').WebSocketTransport\n\t\t\ttesting: typeof import('korajs/testing')\n\t\t\tSimpleEventEmitter: typeof import('@korajs/core/internal').SimpleEventEmitter\n\t\t}\n\t\ttry {\n\t\t\tmods = {\n\t\t\t\tStore: (await import('@korajs/store')).Store,\n\t\t\t\tBetterSqlite3Adapter: (await import('@korajs/store/better-sqlite3')).BetterSqlite3Adapter,\n\t\t\t\tMergeEngine: (await import('@korajs/merge')).MergeEngine,\n\t\t\t\tSyncEngine: (await import('@korajs/sync')).SyncEngine,\n\t\t\t\tWebSocketTransport: (await import('@korajs/sync')).WebSocketTransport,\n\t\t\t\ttesting: await import('korajs/testing'),\n\t\t\t\tSimpleEventEmitter: (await import('@korajs/core/internal')).SimpleEventEmitter,\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t`Spectator mode needs the Kora runtime packages installed (pnpm add -D korajs @korajs/store @korajs/merge @korajs/sync better-sqlite3). Underlying error: ${\n\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t}`,\n\t\t\t)\n\t\t}\n\n\t\tconst emitter = new mods.SimpleEventEmitter()\n\t\tconst adapter = new mods.BetterSqlite3Adapter(this.dbPath)\n\t\tconst store = new mods.Store({ schema: this.options.schema, adapter, emitter })\n\t\tawait store.open()\n\n\t\tconst mergeEngine = new mods.MergeEngine()\n\t\tconst pipeline = new mods.testing.ApplyPipeline({ store, mergeEngine, emitter })\n\t\tstore.setLocalMutationHandler(pipeline)\n\n\t\t// Production parity: merge decisions persist to the audit trail.\n\t\tconst unsubscribeAudit = mods.testing.wireAuditPersistence(store, emitter)\n\n\t\tconst syncStore = new mods.testing.MergeAwareSyncStore(store, mergeEngine, emitter)\n\t\tconst engine = new mods.SyncEngine({\n\t\t\ttransport: new mods.WebSocketTransport(),\n\t\t\tstore: syncStore,\n\t\t\tqueueStorage: new mods.testing.StoreQueueStorage(adapter),\n\t\t\tsyncState: new mods.testing.StoreSyncStatePersistence(store),\n\t\t\tconfig: {\n\t\t\t\turl: this.options.url,\n\t\t\t\tschemaVersion: this.options.schemaVersion ?? this.options.schema.version,\n\t\t\t\t...(this.options.token\n\t\t\t\t\t? { auth: async () => ({ token: this.options.token as string }) }\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t\temitter,\n\t\t})\n\n\t\tconst unsubscribers = FORWARDED_EVENTS.map((type) =>\n\t\t\temitter.on(type, (event: Record<string, unknown>) => {\n\t\t\t\tthis.lastEventAt = Date.now()\n\t\t\t\tif (type === 'sync:connected') {\n\t\t\t\t\tthis.connected = true\n\t\t\t\t}\n\t\t\t\tif (type === 'sync:disconnected') {\n\t\t\t\t\tthis.connected = false\n\t\t\t\t}\n\t\t\t\tif (type === 'sync:received' && Array.isArray(event.operations)) {\n\t\t\t\t\tthis.operationsReceived += event.operations.length\n\t\t\t\t}\n\t\t\t\tthis.pushEvent(type, event)\n\t\t\t}),\n\t\t)\n\t\tunsubscribers.push(unsubscribeAudit)\n\n\t\tthis.runtime = { store, engine, unsubscribers }\n\t\tawait engine.start()\n\t}\n\n\tstatus(): SpectatorStatus {\n\t\treturn {\n\t\t\turl: this.options.url,\n\t\t\tconnected: this.connected,\n\t\t\toperationsReceived: this.operationsReceived,\n\t\t\tlastEventAt: this.lastEventAt,\n\t\t\tdbPath: this.dbPath,\n\t\t}\n\t}\n\n\trecentEvents(): SpectatorEvent[] {\n\t\treturn [...this.eventBuffer]\n\t}\n\n\tonEvent(listener: (event: SpectatorEvent) => void): () => void {\n\t\tthis.eventListeners.add(listener)\n\t\treturn () => this.eventListeners.delete(listener)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.runtime) {\n\t\t\tfor (const unsub of this.runtime.unsubscribers) {\n\t\t\t\tunsub()\n\t\t\t}\n\t\t\tawait this.runtime.engine.stop().catch(() => {})\n\t\t\tawait this.runtime.store.close().catch(() => {})\n\t\t\tthis.runtime = null\n\t\t}\n\t\tif (this.tmpDir) {\n\t\t\trmSync(this.tmpDir, { recursive: true, force: true })\n\t\t\tthis.tmpDir = null\n\t\t}\n\t}\n\n\tprivate pushEvent(type: string, payload: Record<string, unknown>): void {\n\t\tconst summary =\n\t\t\ttype === 'sync:received' && Array.isArray(payload.operations)\n\t\t\t\t? `received ${payload.operations.length} op(s)`\n\t\t\t\t: type.replace('sync:', '').replace('merge:', 'merge ')\n\t\tconst event: SpectatorEvent = {\n\t\t\tseq: ++this.eventSeq,\n\t\t\tat: Date.now(),\n\t\t\ttype,\n\t\t\tsummary,\n\t\t}\n\t\tthis.eventBuffer.push(event)\n\t\tif (this.eventBuffer.length > MAX_EVENT_BUFFER) {\n\t\t\tthis.eventBuffer.shift()\n\t\t}\n\t\tfor (const listener of this.eventListeners) {\n\t\t\tlistener(event)\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}