syncorejs 0.1.0 → 0.2.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/dist/{core/src/cli.d.ts → _vendor/cli/app.d.mts} +4 -2
  2. package/dist/_vendor/cli/app.d.mts.map +1 -0
  3. package/dist/_vendor/cli/app.mjs +997 -0
  4. package/dist/_vendor/cli/app.mjs.map +1 -0
  5. package/dist/_vendor/cli/context.mjs +180 -0
  6. package/dist/_vendor/cli/context.mjs.map +1 -0
  7. package/dist/_vendor/cli/dev-session.mjs +49 -0
  8. package/dist/_vendor/cli/dev-session.mjs.map +1 -0
  9. package/dist/_vendor/cli/doctor.mjs +80 -0
  10. package/dist/_vendor/cli/doctor.mjs.map +1 -0
  11. package/dist/_vendor/cli/errors.mjs +22 -0
  12. package/dist/_vendor/cli/errors.mjs.map +1 -0
  13. package/dist/_vendor/cli/help.mjs +26 -0
  14. package/dist/_vendor/cli/help.mjs.map +1 -0
  15. package/dist/_vendor/cli/index.d.mts +2 -0
  16. package/dist/_vendor/cli/index.mjs +23 -0
  17. package/dist/_vendor/cli/index.mjs.map +1 -0
  18. package/dist/_vendor/cli/messages.mjs +32 -0
  19. package/dist/_vendor/cli/messages.mjs.map +1 -0
  20. package/dist/_vendor/cli/preflight.mjs +35 -0
  21. package/dist/_vendor/cli/preflight.mjs.map +1 -0
  22. package/dist/_vendor/cli/project.mjs +583 -0
  23. package/dist/_vendor/cli/project.mjs.map +1 -0
  24. package/dist/_vendor/cli/render.mjs +133 -0
  25. package/dist/_vendor/cli/render.mjs.map +1 -0
  26. package/dist/_vendor/cli/targets.mjs +87 -0
  27. package/dist/_vendor/cli/targets.mjs.map +1 -0
  28. package/dist/_vendor/core/cli.d.mts +59 -1
  29. package/dist/_vendor/core/cli.d.mts.map +1 -1
  30. package/dist/_vendor/core/cli.mjs +528 -75
  31. package/dist/_vendor/core/cli.mjs.map +1 -1
  32. package/dist/_vendor/core/index.d.mts +12 -4
  33. package/dist/_vendor/core/index.d.mts.map +1 -0
  34. package/dist/_vendor/core/index.mjs +4 -3
  35. package/dist/_vendor/core/index.mjs.map +1 -1
  36. package/dist/_vendor/core/runtime/devtools.d.mts +32 -6
  37. package/dist/_vendor/core/runtime/devtools.d.mts.map +1 -1
  38. package/dist/_vendor/core/runtime/devtools.mjs +397 -182
  39. package/dist/_vendor/core/runtime/devtools.mjs.map +1 -1
  40. package/dist/_vendor/core/runtime/functions.mjs.map +1 -1
  41. package/dist/_vendor/core/runtime/runtime.d.mts +89 -7
  42. package/dist/_vendor/core/runtime/runtime.d.mts.map +1 -1
  43. package/dist/_vendor/core/runtime/runtime.mjs +303 -32
  44. package/dist/_vendor/core/runtime/runtime.mjs.map +1 -1
  45. package/dist/_vendor/devtools-protocol/index.d.ts +189 -82
  46. package/dist/_vendor/devtools-protocol/index.d.ts.map +1 -1
  47. package/dist/_vendor/devtools-protocol/index.js +39 -0
  48. package/dist/_vendor/devtools-protocol/index.js.map +1 -0
  49. package/dist/_vendor/next/config.d.ts.map +1 -1
  50. package/dist/_vendor/next/config.js +2 -5
  51. package/dist/_vendor/next/config.js.map +1 -1
  52. package/dist/_vendor/platform-expo/index.d.ts +15 -5
  53. package/dist/_vendor/platform-expo/index.d.ts.map +1 -1
  54. package/dist/_vendor/platform-expo/index.js +33 -3
  55. package/dist/_vendor/platform-expo/index.js.map +1 -1
  56. package/dist/_vendor/platform-expo/react.js.map +1 -1
  57. package/dist/_vendor/platform-node/index.d.mts +10 -5
  58. package/dist/_vendor/platform-node/index.d.mts.map +1 -1
  59. package/dist/_vendor/platform-node/index.mjs +145 -35
  60. package/dist/_vendor/platform-node/index.mjs.map +1 -1
  61. package/dist/_vendor/platform-node/ipc-react.mjs.map +1 -1
  62. package/dist/_vendor/platform-web/external-change.d.ts +39 -0
  63. package/dist/_vendor/platform-web/external-change.d.ts.map +1 -0
  64. package/dist/_vendor/platform-web/external-change.js +61 -0
  65. package/dist/_vendor/platform-web/external-change.js.map +1 -0
  66. package/dist/_vendor/platform-web/index.d.ts +27 -5
  67. package/dist/_vendor/platform-web/index.d.ts.map +1 -1
  68. package/dist/_vendor/platform-web/index.js +310 -44
  69. package/dist/_vendor/platform-web/index.js.map +1 -1
  70. package/dist/_vendor/platform-web/indexeddb.js.map +1 -1
  71. package/dist/_vendor/platform-web/opfs.js.map +1 -1
  72. package/dist/_vendor/platform-web/persistence.js.map +1 -1
  73. package/dist/_vendor/platform-web/react.js.map +1 -1
  74. package/dist/_vendor/platform-web/sqljs.js +22 -2
  75. package/dist/_vendor/platform-web/sqljs.js.map +1 -1
  76. package/dist/_vendor/schema/definition.js.map +1 -1
  77. package/dist/_vendor/schema/planner.js.map +1 -1
  78. package/dist/_vendor/schema/validators.js.map +1 -1
  79. package/dist/browser-react.d.ts +1 -1
  80. package/dist/browser-react.js +1 -1
  81. package/dist/browser.d.ts +6 -7
  82. package/dist/browser.d.ts.map +1 -1
  83. package/dist/browser.js +4 -5
  84. package/dist/browser.js.map +1 -1
  85. package/dist/cli.d.ts +1 -1
  86. package/dist/cli.js +12 -3
  87. package/dist/cli.js.map +1 -1
  88. package/dist/expo-react.d.ts +1 -1
  89. package/dist/expo-react.js +1 -1
  90. package/dist/expo.d.ts +1 -2
  91. package/dist/expo.js +1 -2
  92. package/dist/index.d.ts +3 -7
  93. package/dist/index.js +3 -8
  94. package/dist/next-config.d.ts +1 -2
  95. package/dist/next-config.js +1 -2
  96. package/dist/next.d.ts +1 -3
  97. package/dist/next.js +1 -3
  98. package/dist/node-ipc-react.d.ts +1 -1
  99. package/dist/node-ipc-react.js +1 -1
  100. package/dist/node-ipc.d.ts +1 -2
  101. package/dist/node-ipc.js +1 -2
  102. package/dist/node.d.ts +1 -4
  103. package/dist/node.js +1 -3
  104. package/dist/react.d.ts +1 -2
  105. package/dist/react.js +1 -2
  106. package/dist/svelte.d.ts +1 -2
  107. package/dist/svelte.js +1 -2
  108. package/package.json +6 -3
  109. package/dist/core/src/cli.d.ts.map +0 -1
  110. package/dist/core/src/cli.js +0 -1196
  111. package/dist/core/src/cli.js.map +0 -1
  112. package/dist/core/src/index.js +0 -7
  113. package/dist/core/src/runtime/devtools.d.ts +0 -7
  114. package/dist/core/src/runtime/devtools.d.ts.map +0 -1
  115. package/dist/core/src/runtime/devtools.js +0 -300
  116. package/dist/core/src/runtime/devtools.js.map +0 -1
  117. package/dist/core/src/runtime/functions.d.ts +0 -123
  118. package/dist/core/src/runtime/functions.d.ts.map +0 -1
  119. package/dist/core/src/runtime/functions.js +0 -71
  120. package/dist/core/src/runtime/functions.js.map +0 -1
  121. package/dist/core/src/runtime/id.d.ts +0 -13
  122. package/dist/core/src/runtime/id.d.ts.map +0 -1
  123. package/dist/core/src/runtime/id.js +0 -28
  124. package/dist/core/src/runtime/id.js.map +0 -1
  125. package/dist/core/src/runtime/runtime.d.ts +0 -371
  126. package/dist/core/src/runtime/runtime.d.ts.map +0 -1
  127. package/dist/core/src/runtime/runtime.js +0 -1143
  128. package/dist/core/src/runtime/runtime.js.map +0 -1
  129. package/dist/devtools-protocol/src/index.d.ts +0 -201
  130. package/dist/devtools-protocol/src/index.d.ts.map +0 -1
  131. package/dist/next/src/config.d.ts +0 -17
  132. package/dist/next/src/config.d.ts.map +0 -1
  133. package/dist/next/src/config.js +0 -73
  134. package/dist/next/src/config.js.map +0 -1
  135. package/dist/next/src/index.d.ts +0 -80
  136. package/dist/next/src/index.d.ts.map +0 -1
  137. package/dist/next/src/index.js +0 -82
  138. package/dist/next/src/index.js.map +0 -1
  139. package/dist/platform-expo/src/index.d.ts +0 -96
  140. package/dist/platform-expo/src/index.d.ts.map +0 -1
  141. package/dist/platform-expo/src/index.js +0 -198
  142. package/dist/platform-expo/src/index.js.map +0 -1
  143. package/dist/platform-expo/src/react.d.ts +0 -26
  144. package/dist/platform-expo/src/react.d.ts.map +0 -1
  145. package/dist/platform-expo/src/react.js +0 -30
  146. package/dist/platform-expo/src/react.js.map +0 -1
  147. package/dist/platform-node/src/index.d.ts +0 -145
  148. package/dist/platform-node/src/index.d.ts.map +0 -1
  149. package/dist/platform-node/src/index.js +0 -407
  150. package/dist/platform-node/src/index.js.map +0 -1
  151. package/dist/platform-node/src/ipc-react.d.ts +0 -25
  152. package/dist/platform-node/src/ipc-react.d.ts.map +0 -1
  153. package/dist/platform-node/src/ipc-react.js +0 -21
  154. package/dist/platform-node/src/ipc-react.js.map +0 -1
  155. package/dist/platform-node/src/ipc.d.ts +0 -76
  156. package/dist/platform-node/src/ipc.d.ts.map +0 -1
  157. package/dist/platform-node/src/ipc.js +0 -344
  158. package/dist/platform-node/src/ipc.js.map +0 -1
  159. package/dist/platform-web/src/index.d.ts +0 -106
  160. package/dist/platform-web/src/index.d.ts.map +0 -1
  161. package/dist/platform-web/src/index.js +0 -311
  162. package/dist/platform-web/src/index.js.map +0 -1
  163. package/dist/platform-web/src/indexeddb.js +0 -125
  164. package/dist/platform-web/src/indexeddb.js.map +0 -1
  165. package/dist/platform-web/src/opfs.js +0 -146
  166. package/dist/platform-web/src/opfs.js.map +0 -1
  167. package/dist/platform-web/src/persistence.d.ts +0 -20
  168. package/dist/platform-web/src/persistence.d.ts.map +0 -1
  169. package/dist/platform-web/src/persistence.js +0 -23
  170. package/dist/platform-web/src/persistence.js.map +0 -1
  171. package/dist/platform-web/src/react.d.ts +0 -35
  172. package/dist/platform-web/src/react.d.ts.map +0 -1
  173. package/dist/platform-web/src/react.js +0 -42
  174. package/dist/platform-web/src/react.js.map +0 -1
  175. package/dist/platform-web/src/sqljs.js +0 -133
  176. package/dist/platform-web/src/sqljs.js.map +0 -1
  177. package/dist/platform-web/src/worker.d.ts +0 -79
  178. package/dist/platform-web/src/worker.d.ts.map +0 -1
  179. package/dist/platform-web/src/worker.js +0 -308
  180. package/dist/platform-web/src/worker.js.map +0 -1
  181. package/dist/react/src/index.d.ts +0 -59
  182. package/dist/react/src/index.d.ts.map +0 -1
  183. package/dist/react/src/index.js +0 -151
  184. package/dist/react/src/index.js.map +0 -1
  185. package/dist/schema/src/definition.d.ts +0 -98
  186. package/dist/schema/src/definition.d.ts.map +0 -1
  187. package/dist/schema/src/definition.js +0 -84
  188. package/dist/schema/src/definition.js.map +0 -1
  189. package/dist/schema/src/planner.d.ts +0 -42
  190. package/dist/schema/src/planner.d.ts.map +0 -1
  191. package/dist/schema/src/planner.js +0 -131
  192. package/dist/schema/src/planner.js.map +0 -1
  193. package/dist/schema/src/validators.d.ts +0 -194
  194. package/dist/schema/src/validators.d.ts.map +0 -1
  195. package/dist/schema/src/validators.js +0 -158
  196. package/dist/schema/src/validators.js.map +0 -1
  197. package/dist/svelte/src/index.d.ts +0 -44
  198. package/dist/svelte/src/index.d.ts.map +0 -1
  199. package/dist/svelte/src/index.js +0 -75
  200. package/dist/svelte/src/index.js.map +0 -1
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { generateId } from "./runtime/id.mjs";
3
+ import { SyncoreRuntime } from "./runtime/runtime.mjs";
4
+ import { createDevtoolsCommandHandler, createDevtoolsSubscriptionHost } from "./runtime/devtools.mjs";
3
5
  import { src_exports } from "./index.mjs";
4
- import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
6
+ import { appendFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
5
7
  import { createServer } from "node:http";
6
8
  import { connect } from "node:net";
7
9
  import path from "node:path";
@@ -10,12 +12,13 @@ import { DatabaseSync } from "node:sqlite";
10
12
  import { Command } from "commander";
11
13
  import { tsImport } from "tsx/esm/api";
12
14
  import WebSocket, { WebSocketServer } from "ws";
15
+ import { createPublicRuntimeId, createPublicTargetId } from "../devtools-protocol/index.js";
13
16
  //#region src/cli.ts
14
17
  const COMBINED_DEV_COMMAND = "concurrently --kill-others-on-fail --names syncore,app --prefix-colors yellow,cyan \"bun run syncorejs:dev\" \"bun run dev:app\"";
15
18
  const program = new Command();
16
19
  const CORE_PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
- const migrationSnapshotFileName = "_schema_snapshot.json";
18
- const validTemplates = [
20
+ const SYNCORE_MIGRATION_SNAPSHOT_FILE_NAME = "_schema_snapshot.json";
21
+ const VALID_SYNCORE_TEMPLATES = [
19
22
  "minimal",
20
23
  "node",
21
24
  "react-web",
@@ -25,8 +28,9 @@ const validTemplates = [
25
28
  ];
26
29
  let pendingDevBootstrap;
27
30
  let devBootstrapInFlight = false;
31
+ const PROJECT_TARGET_RUNTIME_ID = "syncore-project-target";
28
32
  program.name("syncorejs").description("Syncore local-first toolkit CLI").version("0.1.0");
29
- program.command("init").description("Scaffold Syncore in the current directory").option("--template <template>", `Template to scaffold (${validTemplates.join(", ")}, or auto)`, "auto").option("--force", "Overwrite Syncore-managed files when they already exist").action(async (options) => {
33
+ program.command("init").description("Scaffold Syncore in the current directory").option("--template <template>", `Template to scaffold (${VALID_SYNCORE_TEMPLATES.join(", ")}, or auto)`, "auto").option("--force", "Overwrite Syncore-managed files when they already exist").action(async (options) => {
30
34
  const cwd = process.cwd();
31
35
  const result = await scaffoldProject(cwd, {
32
36
  template: await resolveRequestedTemplate(cwd, options.template),
@@ -105,7 +109,7 @@ program.command("migrate:apply").description("Apply SQL migrations from syncore/
105
109
  const appliedCount = await applyProjectMigrations(process.cwd());
106
110
  console.log(`Applied ${appliedCount} migration(s).`);
107
111
  });
108
- program.command("dev").description("Start the Syncore dev loop and devtools hub").option("--template <template>", `Template to scaffold when Syncore is missing (${validTemplates.join(", ")}, or auto)`, "auto").action(async (options) => {
112
+ program.command("dev").description("Start the Syncore dev loop and devtools hub").option("--template <template>", `Template to scaffold when Syncore is missing (${VALID_SYNCORE_TEMPLATES.join(", ")}, or auto)`, "auto").action(async (options) => {
109
113
  const cwd = process.cwd();
110
114
  await startDevHub({
111
115
  cwd,
@@ -337,11 +341,7 @@ function buildTemplateFiles(template) {
337
341
  const files = [
338
342
  {
339
343
  path: "syncore.config.ts",
340
- content: `export default {
341
- databasePath: ".syncore/syncore.db",
342
- storageDirectory: ".syncore/storage"
343
- };
344
- `
344
+ content: renderSyncoreConfigTemplate(template)
345
345
  },
346
346
  {
347
347
  path: path.join("syncore", "schema.ts"),
@@ -513,6 +513,17 @@ export function createAppSyncoreRuntime() {
513
513
  }
514
514
  return files;
515
515
  }
516
+ function renderSyncoreConfigTemplate(template) {
517
+ if (template === "node" || template === "electron") return `export default {
518
+ projectTarget: {
519
+ databasePath: ".syncore/syncore.db",
520
+ storageDirectory: ".syncore/storage"
521
+ }
522
+ };
523
+ `;
524
+ return `export default {};
525
+ `;
526
+ }
516
527
  function logScaffoldResult(result, heading) {
517
528
  console.log(heading);
518
529
  console.log(`Using template: ${result.template}`);
@@ -529,7 +540,7 @@ async function hasSyncoreProject(cwd) {
529
540
  }
530
541
  async function resolveRequestedTemplate(cwd, requestedTemplate) {
531
542
  if (requestedTemplate !== "auto") {
532
- if (!validTemplates.includes(requestedTemplate)) throw new Error(`Unknown template ${JSON.stringify(requestedTemplate)}. Expected one of ${validTemplates.join(", ")} or auto.`);
543
+ if (!VALID_SYNCORE_TEMPLATES.includes(requestedTemplate)) throw new Error(`Unknown template ${JSON.stringify(requestedTemplate)}. Expected one of ${VALID_SYNCORE_TEMPLATES.join(", ")} or auto.`);
533
544
  return requestedTemplate;
534
545
  }
535
546
  return detectProjectTemplate(cwd);
@@ -622,9 +633,9 @@ async function writeManagedFile(filePath, content, force = false) {
622
633
  async function importJsonlIntoProject(cwd, tableName, sourcePath) {
623
634
  const schema = await loadProjectSchema(cwd);
624
635
  const table = schema.getTable(tableName);
625
- const config = await loadProjectConfig(cwd);
626
- const databasePath = path.resolve(cwd, config.databasePath);
627
- const storageDirectory = path.resolve(cwd, config.storageDirectory);
636
+ const projectTarget = requireProjectTargetConfig(await loadProjectConfig(cwd));
637
+ const databasePath = path.resolve(cwd, projectTarget.databasePath);
638
+ const storageDirectory = path.resolve(cwd, projectTarget.storageDirectory);
628
639
  const sourceFilePath = path.resolve(cwd, sourcePath);
629
640
  await mkdir(path.dirname(databasePath), { recursive: true });
630
641
  await mkdir(storageDirectory, { recursive: true });
@@ -798,10 +809,27 @@ function renderFunctionTypeImports(functionEntries, extension) {
798
809
  function renderFunctionImportName(entry) {
799
810
  return [...entry.pathParts.map((segment) => segment.replace(/[^a-zA-Z0-9_$]/g, "_")), entry.exportName].join("__");
800
811
  }
812
+ function resolveProjectTargetConfig(config) {
813
+ if (config.projectTarget && typeof config.projectTarget === "object" && typeof config.projectTarget.databasePath === "string" && typeof config.projectTarget.storageDirectory === "string") return config.projectTarget;
814
+ if (typeof config.databasePath === "string" && typeof config.storageDirectory === "string") return {
815
+ databasePath: config.databasePath,
816
+ storageDirectory: config.storageDirectory
817
+ };
818
+ return null;
819
+ }
801
820
  async function loadProjectConfig(cwd) {
802
821
  const config = await loadDefaultExport(path.join(cwd, "syncore.config.ts"));
803
- if (!config || typeof config !== "object" || typeof config.databasePath !== "string" || typeof config.storageDirectory !== "string") throw new Error("syncore.config.ts must export { databasePath, storageDirectory }.");
804
- return config;
822
+ if (!config || typeof config !== "object") throw new Error("syncore.config.ts must default export a Syncore config object.");
823
+ const projectTarget = resolveProjectTargetConfig(config);
824
+ return projectTarget ? {
825
+ ...config,
826
+ projectTarget
827
+ } : config;
828
+ }
829
+ function requireProjectTargetConfig(config) {
830
+ const projectTarget = resolveProjectTargetConfig(config);
831
+ if (!projectTarget) throw new Error("This Syncore project does not define a projectTarget. Use a connected client target instead.");
832
+ return projectTarget;
805
833
  }
806
834
  async function loadProjectSchema(cwd) {
807
835
  const schema = await loadDefaultExport(path.join(cwd, "syncore", "schema.ts"));
@@ -817,15 +845,267 @@ async function loadDefaultExport(filePath) {
817
845
  if (resolvedDefault === void 0) throw new Error(`File ${path.relative(process.cwd(), filePath)} exported undefined.`);
818
846
  return resolvedDefault;
819
847
  }
848
+ async function loadNamedExport(filePath, exportName) {
849
+ if (!await fileExists(filePath)) throw new Error(`Missing file: ${path.relative(process.cwd(), filePath)}`);
850
+ const moduleUrl = pathToFileURL(filePath).href;
851
+ const loaded = await tsImport(moduleUrl, { parentURL: import.meta.url });
852
+ const defaultExport = loaded.default && typeof loaded.default === "object" && exportName in loaded.default ? loaded.default[exportName] : void 0;
853
+ if (!(exportName in loaded) && defaultExport === void 0) throw new Error(`File ${path.relative(process.cwd(), filePath)} must export ${exportName}.`);
854
+ const resolvedValue = unwrapDefaultExport(loaded[exportName] ?? defaultExport);
855
+ if (resolvedValue === void 0) throw new Error(`File ${path.relative(process.cwd(), filePath)} exported undefined for ${exportName}.`);
856
+ return resolvedValue;
857
+ }
858
+ async function loadProjectFunctions(cwd) {
859
+ const functions = await loadNamedExport(path.join(cwd, "syncore", "_generated", "functions.ts"), "functions");
860
+ if (!functions || typeof functions !== "object") throw new Error("syncore/_generated/functions.ts must export a functions registry.");
861
+ return functions;
862
+ }
863
+ var HubSqliteDriver = class {
864
+ database;
865
+ transactionDepth = 0;
866
+ constructor(filename) {
867
+ this.database = new DatabaseSync(filename);
868
+ this.database.exec("PRAGMA foreign_keys = ON;");
869
+ this.database.exec("PRAGMA journal_mode = WAL;");
870
+ }
871
+ async exec(sql) {
872
+ this.database.exec(sql);
873
+ }
874
+ async run(sql, params = []) {
875
+ const result = this.database.prepare(sql).run(...toSqlParameters(params));
876
+ return {
877
+ changes: Number(result.changes ?? 0),
878
+ lastInsertRowid: typeof result.lastInsertRowid === "bigint" ? Number(result.lastInsertRowid) : result.lastInsertRowid
879
+ };
880
+ }
881
+ async get(sql, params = []) {
882
+ return this.database.prepare(sql).get(...toSqlParameters(params));
883
+ }
884
+ async all(sql, params = []) {
885
+ return this.database.prepare(sql).all(...toSqlParameters(params));
886
+ }
887
+ async withTransaction(callback) {
888
+ if (this.transactionDepth > 0) return this.withSavepoint(`nested_${this.transactionDepth}`, callback);
889
+ this.transactionDepth += 1;
890
+ this.database.exec("BEGIN");
891
+ try {
892
+ const result = await callback();
893
+ this.database.exec("COMMIT");
894
+ return result;
895
+ } catch (error) {
896
+ this.database.exec("ROLLBACK");
897
+ throw error;
898
+ } finally {
899
+ this.transactionDepth -= 1;
900
+ }
901
+ }
902
+ async withSavepoint(name, callback) {
903
+ const safeName = name.replaceAll(/[^a-zA-Z0-9_]/g, "_");
904
+ this.database.exec(`SAVEPOINT ${safeName}`);
905
+ try {
906
+ const result = await callback();
907
+ this.database.exec(`RELEASE SAVEPOINT ${safeName}`);
908
+ return result;
909
+ } catch (error) {
910
+ this.database.exec(`ROLLBACK TO SAVEPOINT ${safeName}`);
911
+ this.database.exec(`RELEASE SAVEPOINT ${safeName}`);
912
+ throw error;
913
+ }
914
+ }
915
+ async close() {
916
+ this.database.close();
917
+ }
918
+ };
919
+ var HubFileStorageAdapter = class {
920
+ constructor(directory) {
921
+ this.directory = directory;
922
+ }
923
+ filePath(id) {
924
+ return path.join(this.directory, id);
925
+ }
926
+ async put(id, input) {
927
+ await mkdir(this.directory, { recursive: true });
928
+ const filePath = this.filePath(id);
929
+ const bytes = normalizeStorageInput(input.data);
930
+ await writeFile(filePath, bytes);
931
+ return {
932
+ id,
933
+ path: filePath,
934
+ size: bytes.byteLength,
935
+ contentType: input.contentType ?? null
936
+ };
937
+ }
938
+ async get(id) {
939
+ const filePath = this.filePath(id);
940
+ try {
941
+ return {
942
+ id,
943
+ path: filePath,
944
+ size: (await stat(filePath)).size,
945
+ contentType: null
946
+ };
947
+ } catch {
948
+ return null;
949
+ }
950
+ }
951
+ async read(id) {
952
+ try {
953
+ return await readFile(this.filePath(id));
954
+ } catch {
955
+ return null;
956
+ }
957
+ }
958
+ async delete(id) {
959
+ await rm(this.filePath(id), { force: true });
960
+ }
961
+ async list() {
962
+ try {
963
+ const entries = await readdir(this.directory, { withFileTypes: true });
964
+ return Promise.all(entries.filter((entry) => entry.isFile()).map(async (entry) => {
965
+ const filePath = this.filePath(entry.name);
966
+ const info = await stat(filePath);
967
+ return {
968
+ id: entry.name,
969
+ path: filePath,
970
+ size: info.size,
971
+ contentType: null
972
+ };
973
+ }));
974
+ } catch {
975
+ return [];
976
+ }
977
+ }
978
+ };
979
+ const hubDevtoolsSqlSupport = {
980
+ analyzeSqlStatement(query) {
981
+ const firstKeyword = query.trim().replace(/^\(+/, "").toUpperCase().split(/\s+/, 1)[0] ?? "";
982
+ if (firstKeyword === "SELECT" || firstKeyword === "WITH" || firstKeyword === "PRAGMA" || firstKeyword === "EXPLAIN") return {
983
+ mode: "read",
984
+ readTables: [],
985
+ writeTables: [],
986
+ schemaChanged: false,
987
+ observedScopes: ["all"]
988
+ };
989
+ if (firstKeyword === "INSERT" || firstKeyword === "UPDATE" || firstKeyword === "DELETE" || firstKeyword === "REPLACE") return {
990
+ mode: "write",
991
+ readTables: [],
992
+ writeTables: [],
993
+ schemaChanged: false,
994
+ observedScopes: ["all"]
995
+ };
996
+ if (firstKeyword === "CREATE" || firstKeyword === "DROP" || firstKeyword === "ALTER") return {
997
+ mode: "ddl",
998
+ readTables: [],
999
+ writeTables: [],
1000
+ schemaChanged: true,
1001
+ observedScopes: ["all", "schema.tables"]
1002
+ };
1003
+ throw new Error(`Unsupported SQL statement type: ${firstKeyword || "unknown"}`);
1004
+ },
1005
+ ensureSqlMode(analysis, expected) {
1006
+ if (expected === "watch") {
1007
+ if (analysis.mode !== "read") throw new Error("Live mode supports read-only SQL only.");
1008
+ return;
1009
+ }
1010
+ if (analysis.mode !== expected) {
1011
+ if (expected === "read") throw new Error("Use SQL Write for mutating statements.");
1012
+ throw new Error("Use SQL Read or SQL Live for read-only statements.");
1013
+ }
1014
+ },
1015
+ runReadonlyQuery(databasePath, query) {
1016
+ const analysis = this.analyzeSqlStatement(query);
1017
+ this.ensureSqlMode(analysis, "read");
1018
+ const database = new DatabaseSync(databasePath, { readOnly: true });
1019
+ try {
1020
+ const statement = database.prepare(query);
1021
+ const rows = statement.all();
1022
+ const columns = statement.columns().map((column) => column.name);
1023
+ return {
1024
+ columns,
1025
+ rows: rows.map((row) => columns.map((column) => row[column])),
1026
+ observedTables: []
1027
+ };
1028
+ } finally {
1029
+ database.close();
1030
+ }
1031
+ }
1032
+ };
1033
+ async function createProjectTargetBackend(cwd) {
1034
+ const projectTarget = resolveProjectTargetConfig(await loadProjectConfig(cwd));
1035
+ if (!projectTarget) return null;
1036
+ const schema = await loadProjectSchema(cwd);
1037
+ const functions = await loadProjectFunctions(cwd);
1038
+ const databasePath = path.resolve(cwd, projectTarget.databasePath);
1039
+ const storageDirectory = path.resolve(cwd, projectTarget.storageDirectory);
1040
+ await mkdir(path.dirname(databasePath), { recursive: true });
1041
+ await mkdir(storageDirectory, { recursive: true });
1042
+ const driver = new HubSqliteDriver(databasePath);
1043
+ const runtime = new SyncoreRuntime({
1044
+ schema,
1045
+ functions,
1046
+ driver,
1047
+ storage: new HubFileStorageAdapter(storageDirectory),
1048
+ platform: "project"
1049
+ });
1050
+ await runtime.prepareForDirectAccess();
1051
+ const commandHandler = createDevtoolsCommandHandler({
1052
+ driver,
1053
+ schema,
1054
+ functions,
1055
+ runtime,
1056
+ sql: hubDevtoolsSqlSupport
1057
+ });
1058
+ const subscriptionHost = createDevtoolsSubscriptionHost({
1059
+ driver,
1060
+ schema,
1061
+ functions,
1062
+ runtime,
1063
+ sql: hubDevtoolsSqlSupport
1064
+ });
1065
+ return {
1066
+ hello: {
1067
+ type: "hello",
1068
+ runtimeId: PROJECT_TARGET_RUNTIME_ID,
1069
+ platform: "project",
1070
+ sessionLabel: "Project Target",
1071
+ targetKind: "project",
1072
+ storageProtocol: "file",
1073
+ databaseLabel: path.basename(databasePath),
1074
+ storageIdentity: `file::${databasePath}`
1075
+ },
1076
+ handleCommand: commandHandler,
1077
+ subscribe(subscriptionId, payload, listener) {
1078
+ return subscriptionHost.subscribe(subscriptionId, payload, listener);
1079
+ },
1080
+ unsubscribe(subscriptionId) {
1081
+ subscriptionHost.unsubscribe(subscriptionId);
1082
+ },
1083
+ async dispose() {
1084
+ subscriptionHost.dispose();
1085
+ await runtime.stop();
1086
+ }
1087
+ };
1088
+ }
1089
+ function normalizeStorageInput(input) {
1090
+ if (typeof input === "string") return Buffer.from(input);
1091
+ if (input instanceof Uint8Array) return input;
1092
+ return new Uint8Array(input);
1093
+ }
1094
+ function toSqlParameters(params) {
1095
+ return params.map((value) => {
1096
+ if (value instanceof Uint8Array) return Buffer.from(value);
1097
+ return value;
1098
+ });
1099
+ }
820
1100
  async function readStoredSnapshot(cwd) {
821
- const snapshotPath = path.join(cwd, "syncore", "migrations", migrationSnapshotFileName);
1101
+ const snapshotPath = path.join(cwd, "syncore", "migrations", SYNCORE_MIGRATION_SNAPSHOT_FILE_NAME);
822
1102
  if (!await fileExists(snapshotPath)) return null;
823
1103
  return (0, src_exports.parseSchemaSnapshot)(await readFile(snapshotPath, "utf8"));
824
1104
  }
825
1105
  async function writeStoredSnapshot(cwd, snapshot) {
826
1106
  const migrationsDirectory = path.join(cwd, "syncore", "migrations");
827
1107
  await mkdir(migrationsDirectory, { recursive: true });
828
- await writeFile(path.join(migrationsDirectory, migrationSnapshotFileName), `${JSON.stringify(snapshot, null, 2)}\n`);
1108
+ await writeFile(path.join(migrationsDirectory, SYNCORE_MIGRATION_SNAPSHOT_FILE_NAME), `${JSON.stringify(snapshot, null, 2)}\n`);
829
1109
  }
830
1110
  async function getNextMigrationNumber(directory) {
831
1111
  if (!await fileExists(directory)) return 1;
@@ -834,25 +1114,20 @@ async function getNextMigrationNumber(directory) {
834
1114
  return Math.max(...migrationNumbers) + 1;
835
1115
  }
836
1116
  async function applyProjectMigrations(cwd) {
837
- const config = await loadProjectConfig(cwd);
838
- const databasePath = path.resolve(cwd, config.databasePath);
839
- const storageDirectory = path.resolve(cwd, config.storageDirectory);
1117
+ const projectTarget = requireProjectTargetConfig(await loadProjectConfig(cwd));
1118
+ const databasePath = path.resolve(cwd, projectTarget.databasePath);
1119
+ const storageDirectory = path.resolve(cwd, projectTarget.storageDirectory);
840
1120
  await mkdir(path.dirname(databasePath), { recursive: true });
841
1121
  await mkdir(storageDirectory, { recursive: true });
842
1122
  const database = new DatabaseSync(databasePath);
843
- database.exec(`
844
- CREATE TABLE IF NOT EXISTS "_syncore_migrations" (
845
- name TEXT PRIMARY KEY,
846
- applied_at TEXT NOT NULL
847
- );
848
- `);
1123
+ ensureCliMigrationTrackingTable(database);
849
1124
  const migrationsDirectory = path.join(cwd, "syncore", "migrations");
850
1125
  if (!await fileExists(migrationsDirectory)) {
851
1126
  database.close();
852
1127
  return 0;
853
1128
  }
854
- const appliedRows = database.prepare(`SELECT name FROM "_syncore_migrations" ORDER BY name ASC`).all();
855
- const appliedNames = new Set(appliedRows.map((row) => row.name));
1129
+ const appliedRows = database.prepare(`SELECT id FROM "_syncore_migrations" ORDER BY id ASC`).all();
1130
+ const appliedNames = new Set(appliedRows.map((row) => row.id));
856
1131
  const migrationFiles = (await readdir(migrationsDirectory)).filter((name) => /\.sql$/i.test(name)).sort((left, right) => left.localeCompare(right));
857
1132
  let appliedCount = 0;
858
1133
  for (const fileName of migrationFiles) {
@@ -861,7 +1136,7 @@ async function applyProjectMigrations(cwd) {
861
1136
  database.exec("BEGIN");
862
1137
  try {
863
1138
  applyMigrationSql(database, sql, fileName);
864
- database.prepare(`INSERT INTO "_syncore_migrations" (name, applied_at) VALUES (?, ?)`).run(fileName, (/* @__PURE__ */ new Date()).toISOString());
1139
+ database.prepare(`INSERT OR REPLACE INTO "_syncore_migrations" (id, applied_at, sql) VALUES (?, ?, ?)`).run(fileName, Date.now(), sql);
865
1140
  database.exec("COMMIT");
866
1141
  appliedCount += 1;
867
1142
  } catch (error) {
@@ -873,6 +1148,41 @@ async function applyProjectMigrations(cwd) {
873
1148
  database.close();
874
1149
  return appliedCount;
875
1150
  }
1151
+ function ensureCliMigrationTrackingTable(database) {
1152
+ if (!(database.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = '_syncore_migrations'`).get()?.name === "_syncore_migrations")) {
1153
+ database.exec(`
1154
+ CREATE TABLE "_syncore_migrations" (
1155
+ id TEXT PRIMARY KEY,
1156
+ applied_at INTEGER NOT NULL,
1157
+ sql TEXT NOT NULL
1158
+ );
1159
+ `);
1160
+ return;
1161
+ }
1162
+ const columns = database.prepare(`PRAGMA table_info("_syncore_migrations")`).all();
1163
+ const columnNames = new Set(columns.map((column) => column.name));
1164
+ if (columnNames.has("id") && columnNames.has("applied_at") && columnNames.has("sql")) return;
1165
+ database.exec(`
1166
+ ALTER TABLE "_syncore_migrations" RENAME TO "_syncore_migrations_legacy";
1167
+ CREATE TABLE "_syncore_migrations" (
1168
+ id TEXT PRIMARY KEY,
1169
+ applied_at INTEGER NOT NULL,
1170
+ sql TEXT NOT NULL
1171
+ );
1172
+ `);
1173
+ if (columnNames.has("name")) database.exec(`
1174
+ INSERT INTO "_syncore_migrations" (id, applied_at, sql)
1175
+ SELECT
1176
+ name,
1177
+ CASE
1178
+ WHEN typeof(applied_at) = 'integer' THEN applied_at
1179
+ ELSE CAST(strftime('%s', applied_at) AS INTEGER) * 1000
1180
+ END,
1181
+ ''
1182
+ FROM "_syncore_migrations_legacy";
1183
+ `);
1184
+ database.exec(`DROP TABLE "_syncore_migrations_legacy";`);
1185
+ }
876
1186
  async function fileExists(filePath) {
877
1187
  try {
878
1188
  await stat(filePath);
@@ -882,15 +1192,14 @@ async function fileExists(filePath) {
882
1192
  }
883
1193
  }
884
1194
  async function resolveFunctionImportExtension(cwd) {
885
- const candidateConfigFiles = (await readdir(cwd, { withFileTypes: true })).filter((entry) => entry.isFile() && /^tsconfig.*\.json$/i.test(entry.name)).map((entry) => path.join(cwd, entry.name)).sort((left, right) => left.localeCompare(right));
886
- for (const filePath of candidateConfigFiles) try {
887
- const rawConfig = JSON.parse(await readFile(filePath, "utf8"));
888
- const moduleResolution = rawConfig.compilerOptions?.moduleResolution?.toLowerCase();
889
- const moduleTarget = rawConfig.compilerOptions?.module?.toLowerCase();
890
- if (moduleResolution === "nodenext" || moduleTarget === "nodenext") return ".js";
891
- } catch {
892
- continue;
893
- }
1195
+ const tsconfigFiles = (await readdir(cwd, { withFileTypes: true })).filter((entry) => entry.isFile() && /^tsconfig(\..+)?\.json$/u.test(entry.name)).map((entry) => path.join(cwd, entry.name)).sort();
1196
+ for (const configPath of tsconfigFiles) try {
1197
+ const source = await readFile(configPath, "utf8");
1198
+ const parsed = JSON.parse(source);
1199
+ const moduleResolution = parsed.compilerOptions?.moduleResolution?.toLowerCase();
1200
+ const moduleKind = parsed.compilerOptions?.module?.toLowerCase();
1201
+ if (moduleResolution === "nodenext" || moduleResolution === "node16" || moduleKind === "nodenext" || moduleKind === "node16") return ".js";
1202
+ } catch {}
894
1203
  return "";
895
1204
  }
896
1205
  function formatError(error) {
@@ -930,12 +1239,22 @@ function applyMigrationSql(database, sql, fileName) {
930
1239
  async function startDevHub(options) {
931
1240
  const dashboardPort = resolvePortFromEnv("SYNCORE_DASHBOARD_PORT", 4310);
932
1241
  const devtoolsPort = resolvePortFromEnv("SYNCORE_DEVTOOLS_PORT", 4311);
1242
+ const logsDirectory = path.join(options.cwd, ".syncore", "logs");
1243
+ const logFilePath = path.join(logsDirectory, "runtime.jsonl");
1244
+ await mkdir(logsDirectory, { recursive: true });
1245
+ await writeFile(logFilePath, "");
933
1246
  await runDevProjectBootstrap(options.cwd, options.template);
934
1247
  await setupDevProjectWatch(options.cwd, options.template);
935
1248
  if (await isLocalPortInUse(devtoolsPort)) {
936
- console.log(`Syncore devtools hub already running at ws://127.0.0.1:${devtoolsPort}. Reusing existing hub/dashboard.`);
1249
+ console.log(`Syncore devtools hub already running at ws://localhost:${devtoolsPort}. Reusing existing hub/dashboard.`);
937
1250
  return;
938
1251
  }
1252
+ let projectTargetBackend = null;
1253
+ try {
1254
+ projectTargetBackend = await createProjectTargetBackend(options.cwd);
1255
+ } catch (error) {
1256
+ console.warn(`Project target fallback unavailable: ${formatError(error)}`);
1257
+ }
939
1258
  const httpServer = createServer((_request, response) => {
940
1259
  response.writeHead(200, { "content-type": "application/json" });
941
1260
  response.end(JSON.stringify({
@@ -944,19 +1263,56 @@ async function startDevHub(options) {
944
1263
  }));
945
1264
  });
946
1265
  const websocketServer = new WebSocketServer({ server: httpServer });
947
- const latestSnapshots = /* @__PURE__ */ new Map();
948
1266
  const runtimeSockets = /* @__PURE__ */ new Map();
1267
+ const runtimeHellos = /* @__PURE__ */ new Map();
1268
+ const runtimeEvents = /* @__PURE__ */ new Map();
949
1269
  const socketRuntimeIds = /* @__PURE__ */ new Map();
950
1270
  const dashboardSockets = /* @__PURE__ */ new Set();
1271
+ const dashboardSubscriptions = /* @__PURE__ */ new Map();
951
1272
  const hello = {
952
1273
  type: "hello",
953
1274
  runtimeId: "syncore-dev-hub",
954
1275
  platform: "dev"
955
1276
  };
1277
+ if (projectTargetBackend) {
1278
+ runtimeHellos.set(PROJECT_TARGET_RUNTIME_ID, projectTargetBackend.hello);
1279
+ runtimeEvents.set(PROJECT_TARGET_RUNTIME_ID, []);
1280
+ }
1281
+ const appendHubLog = async (event) => {
1282
+ const runtimeHello = runtimeHellos.get(event.runtimeId);
1283
+ const clientRuntimeIds = [...runtimeHellos.values()].filter((hello) => hello.runtimeId !== "syncore-dev-hub" && hello.targetKind !== "project").map((hello) => hello.runtimeId).sort();
1284
+ const clientTargetKeys = [...runtimeHellos.values()].filter((hello) => hello.runtimeId !== "syncore-dev-hub" && hello.targetKind !== "project").map((hello) => hello.storageIdentity ?? `runtime::${hello.runtimeId}`).sort();
1285
+ const targetIdentity = runtimeHello?.storageIdentity ?? `runtime::${event.runtimeId}`;
1286
+ const targetId = event.runtimeId === "syncore-dev-hub" ? "all" : runtimeHello?.targetKind === "project" ? "project" : createPublicTargetId(targetIdentity, clientTargetKeys);
1287
+ const publicRuntimeId = event.runtimeId === "syncore-dev-hub" ? void 0 : createPublicRuntimeId(event.runtimeId, clientRuntimeIds);
1288
+ const category = event.type === "query.executed" ? "query" : event.type === "mutation.committed" ? "mutation" : event.type === "action.completed" ? "action" : "system";
1289
+ const message = event.type === "log" ? event.message : event.type === "query.executed" || event.type === "mutation.committed" || event.type === "action.completed" ? event.functionName : event.type;
1290
+ await appendFile(logFilePath, `${JSON.stringify({
1291
+ version: 2,
1292
+ timestamp: event.timestamp,
1293
+ runtimeId: event.runtimeId,
1294
+ targetId,
1295
+ ...publicRuntimeId ? { publicRuntimeId } : {},
1296
+ ...runtimeHello?.platform ? { platform: runtimeHello.platform } : {},
1297
+ eventType: event.type,
1298
+ category,
1299
+ message,
1300
+ event
1301
+ })}\n`);
1302
+ };
956
1303
  websocketServer.on("connection", (socket) => {
957
1304
  dashboardSockets.add(socket);
958
1305
  socket.send(JSON.stringify(hello));
959
- for (const snapshot of latestSnapshots.values()) socket.send(JSON.stringify(snapshot));
1306
+ for (const runtimeHello of runtimeHellos.values()) socket.send(JSON.stringify(runtimeHello));
1307
+ for (const [runtimeId, history] of runtimeEvents) {
1308
+ if (!runtimeHellos.has(runtimeId)) continue;
1309
+ if (history.length === 0) continue;
1310
+ socket.send(JSON.stringify({
1311
+ type: "event.batch",
1312
+ runtimeId,
1313
+ events: [...history]
1314
+ }));
1315
+ }
960
1316
  socket.on("message", (payload) => {
961
1317
  const rawPayload = decodeWebSocketPayload(payload);
962
1318
  if (rawPayload.length === 0) return;
@@ -965,32 +1321,110 @@ async function startDevHub(options) {
965
1321
  socket.send(JSON.stringify({ type: "pong" }));
966
1322
  return;
967
1323
  }
968
- if (message.type === "request") {
1324
+ if (message.type === "command") {
1325
+ const targetRuntimeId = message.targetRuntimeId;
1326
+ if (!targetRuntimeId) return;
1327
+ if (targetRuntimeId === PROJECT_TARGET_RUNTIME_ID && projectTargetBackend) {
1328
+ (async () => {
1329
+ const payload = await projectTargetBackend.handleCommand(message.payload);
1330
+ if (socket.readyState !== WebSocket.OPEN) return;
1331
+ socket.send(JSON.stringify({
1332
+ type: "command.result",
1333
+ commandId: message.commandId,
1334
+ runtimeId: PROJECT_TARGET_RUNTIME_ID,
1335
+ payload
1336
+ }));
1337
+ })();
1338
+ return;
1339
+ }
1340
+ const target = runtimeSockets.get(targetRuntimeId);
1341
+ if (target && target.readyState === WebSocket.OPEN) target.send(JSON.stringify(message));
1342
+ return;
1343
+ }
1344
+ if (message.type === "subscribe") {
969
1345
  const targetRuntimeId = message.targetRuntimeId;
970
1346
  if (!targetRuntimeId) return;
1347
+ const subscriptions = dashboardSubscriptions.get(socket) ?? /* @__PURE__ */ new Map();
1348
+ subscriptions.set(message.subscriptionId, {
1349
+ runtimeId: targetRuntimeId,
1350
+ payload: message
1351
+ });
1352
+ dashboardSubscriptions.set(socket, subscriptions);
1353
+ if (targetRuntimeId === PROJECT_TARGET_RUNTIME_ID && projectTargetBackend) {
1354
+ projectTargetBackend.subscribe(message.subscriptionId, message.payload, (payload) => {
1355
+ if (socket.readyState !== WebSocket.OPEN) return;
1356
+ socket.send(JSON.stringify({
1357
+ type: "subscription.data",
1358
+ subscriptionId: message.subscriptionId,
1359
+ runtimeId: PROJECT_TARGET_RUNTIME_ID,
1360
+ payload
1361
+ }));
1362
+ }).catch((error) => {
1363
+ if (socket.readyState !== WebSocket.OPEN) return;
1364
+ socket.send(JSON.stringify({
1365
+ type: "subscription.error",
1366
+ subscriptionId: message.subscriptionId,
1367
+ runtimeId: PROJECT_TARGET_RUNTIME_ID,
1368
+ error: formatError(error)
1369
+ }));
1370
+ });
1371
+ return;
1372
+ }
971
1373
  const target = runtimeSockets.get(targetRuntimeId);
972
1374
  if (target && target.readyState === WebSocket.OPEN) target.send(JSON.stringify(message));
973
1375
  return;
974
1376
  }
1377
+ if (message.type === "unsubscribe") {
1378
+ const subscriptions = dashboardSubscriptions.get(socket);
1379
+ const subscription = subscriptions?.get(message.subscriptionId);
1380
+ if (!subscription) return;
1381
+ if (subscription.runtimeId === PROJECT_TARGET_RUNTIME_ID && projectTargetBackend) {
1382
+ projectTargetBackend.unsubscribe(message.subscriptionId);
1383
+ subscriptions?.delete(message.subscriptionId);
1384
+ if (subscriptions && subscriptions.size === 0) dashboardSubscriptions.delete(socket);
1385
+ return;
1386
+ }
1387
+ const target = runtimeSockets.get(subscription.runtimeId);
1388
+ if (target && target.readyState === WebSocket.OPEN) {
1389
+ const runtimeMessage = {
1390
+ type: "unsubscribe",
1391
+ subscriptionId: message.subscriptionId,
1392
+ targetRuntimeId: subscription.runtimeId
1393
+ };
1394
+ target.send(JSON.stringify(runtimeMessage));
1395
+ }
1396
+ subscriptions?.delete(message.subscriptionId);
1397
+ if (subscriptions && subscriptions.size === 0) dashboardSubscriptions.delete(socket);
1398
+ return;
1399
+ }
975
1400
  if (message.type === "hello") {
976
1401
  dashboardSockets.delete(socket);
977
1402
  runtimeSockets.set(message.runtimeId, socket);
1403
+ runtimeHellos.set(message.runtimeId, message);
1404
+ runtimeEvents.set(message.runtimeId, []);
978
1405
  const runtimeIds = socketRuntimeIds.get(socket) ?? /* @__PURE__ */ new Set();
979
1406
  runtimeIds.add(message.runtimeId);
980
1407
  socketRuntimeIds.set(socket, runtimeIds);
1408
+ for (const [dashboardSocket, subscriptions] of dashboardSubscriptions) {
1409
+ if (dashboardSocket.readyState !== WebSocket.OPEN) continue;
1410
+ for (const subscription of subscriptions.values()) {
1411
+ if (subscription.runtimeId !== message.runtimeId) continue;
1412
+ socket.send(JSON.stringify(subscription.payload));
1413
+ }
1414
+ }
981
1415
  for (const client of dashboardSockets) if (client.readyState === WebSocket.OPEN) client.send(JSON.stringify(message));
982
1416
  return;
983
1417
  }
984
- if (message.type === "snapshot") {
985
- latestSnapshots.set(message.snapshot.runtimeId, message);
986
- dashboardSockets.delete(socket);
987
- runtimeSockets.set(message.snapshot.runtimeId, socket);
988
- const runtimeIds = socketRuntimeIds.get(socket) ?? /* @__PURE__ */ new Set();
989
- runtimeIds.add(message.snapshot.runtimeId);
990
- socketRuntimeIds.set(socket, runtimeIds);
991
- }
992
1418
  const encoded = JSON.stringify(message);
993
- if (message.type === "response") {
1419
+ if (message.type === "event" && message.event.type === "runtime.disconnected") runtimeHellos.delete(message.event.runtimeId);
1420
+ if (message.type === "event" && message.event.runtimeId !== "syncore-dev-hub") {
1421
+ const history = runtimeEvents.get(message.event.runtimeId) ?? [];
1422
+ history.unshift(message.event);
1423
+ runtimeEvents.set(message.event.runtimeId, history.slice(0, 200));
1424
+ if (message.event.type === "runtime.disconnected") runtimeEvents.delete(message.event.runtimeId);
1425
+ appendHubLog(message.event);
1426
+ } else if (message.type === "event") appendHubLog(message.event);
1427
+ if (message.type === "command.result" || message.type === "subscription.data" || message.type === "subscription.error") {
994
1428
  for (const client of dashboardSockets) if (client.readyState === WebSocket.OPEN) client.send(encoded);
995
1429
  return;
996
1430
  }
@@ -1001,38 +1435,57 @@ async function startDevHub(options) {
1001
1435
  });
1002
1436
  socket.on("close", () => {
1003
1437
  dashboardSockets.delete(socket);
1438
+ const subscriptions = dashboardSubscriptions.get(socket);
1439
+ if (subscriptions) {
1440
+ for (const [subscriptionId, subscription] of subscriptions) {
1441
+ if (subscription.runtimeId === PROJECT_TARGET_RUNTIME_ID && projectTargetBackend) {
1442
+ projectTargetBackend.unsubscribe(subscriptionId);
1443
+ continue;
1444
+ }
1445
+ const target = runtimeSockets.get(subscription.runtimeId);
1446
+ if (target && target.readyState === WebSocket.OPEN) {
1447
+ const message = {
1448
+ type: "unsubscribe",
1449
+ subscriptionId,
1450
+ targetRuntimeId: subscription.runtimeId
1451
+ };
1452
+ target.send(JSON.stringify(message));
1453
+ }
1454
+ }
1455
+ dashboardSubscriptions.delete(socket);
1456
+ }
1004
1457
  const runtimeIds = socketRuntimeIds.get(socket);
1005
1458
  if (!runtimeIds) return;
1006
- for (const runtimeId of runtimeIds) {
1007
- latestSnapshots.delete(runtimeId);
1008
- if (runtimeSockets.get(runtimeId) === socket) runtimeSockets.delete(runtimeId);
1459
+ for (const runtimeId of runtimeIds) if (runtimeSockets.get(runtimeId) === socket) {
1460
+ if (runtimeHellos.has(runtimeId)) {
1461
+ const disconnectedEvent = {
1462
+ type: "event",
1463
+ event: {
1464
+ type: "runtime.disconnected",
1465
+ runtimeId,
1466
+ timestamp: Date.now()
1467
+ }
1468
+ };
1469
+ const payload = JSON.stringify(disconnectedEvent);
1470
+ appendHubLog(disconnectedEvent.event);
1471
+ for (const client of dashboardSockets) if (client.readyState === WebSocket.OPEN) client.send(payload);
1472
+ }
1473
+ runtimeSockets.delete(runtimeId);
1474
+ runtimeHellos.delete(runtimeId);
1475
+ runtimeEvents.delete(runtimeId);
1009
1476
  }
1010
1477
  socketRuntimeIds.delete(socket);
1011
1478
  });
1012
1479
  });
1013
- const heartbeat = setInterval(() => {
1014
- const event = {
1015
- type: "event",
1016
- event: {
1017
- type: "log",
1018
- runtimeId: "syncore-dev-hub",
1019
- level: "info",
1020
- message: "Syncore devtools hub is alive.",
1021
- timestamp: Date.now()
1022
- }
1023
- };
1024
- const payload = JSON.stringify(event);
1025
- for (const client of websocketServer.clients) client.send(payload);
1026
- }, 4e3);
1027
1480
  httpServer.on("error", (error) => {
1028
1481
  console.error(`Syncore devtools hub failed: ${formatError(error)}`);
1029
1482
  process.exit(1);
1030
1483
  });
1031
1484
  httpServer.listen(devtoolsPort, "127.0.0.1", () => {
1032
1485
  (async () => {
1033
- console.log(`Syncore devtools hub: ws://127.0.0.1:${devtoolsPort}`);
1034
- console.log(`Electron/Node runtimes: set devtoolsUrl to ws://127.0.0.1:${devtoolsPort}.`);
1035
- console.log(`Web/Next apps: connect the dashboard or worker bridge to ws://127.0.0.1:${devtoolsPort}.`);
1486
+ console.log(`Syncore devtools hub: ws://localhost:${devtoolsPort}`);
1487
+ console.log(`Electron/Node runtimes: set devtoolsUrl to ws://localhost:${devtoolsPort}.`);
1488
+ console.log(`Web/Next apps: connect the dashboard or worker bridge to ws://localhost:${devtoolsPort}.`);
1036
1489
  console.log("Expo apps: use the same hub URL through LAN or adb reverse while developing.");
1037
1490
  const dashboardRoot = path.resolve(CORE_PACKAGE_ROOT, "..", "..", "apps", "dashboard");
1038
1491
  if (await fileExists(path.join(dashboardRoot, "vite.config.ts"))) try {
@@ -1041,14 +1494,14 @@ async function startDevHub(options) {
1041
1494
  root: dashboardRoot,
1042
1495
  server: { port: dashboardPort }
1043
1496
  })).listen();
1044
- console.log(`Dashboard shell: http://127.0.0.1:${dashboardPort}`);
1497
+ console.log(`Dashboard shell: http://localhost:${dashboardPort}`);
1045
1498
  } catch (error) {
1046
1499
  console.log(`Dashboard source not started automatically: ${formatError(error)}`);
1047
1500
  }
1048
1501
  })();
1049
1502
  });
1050
1503
  const close = () => {
1051
- clearInterval(heartbeat);
1504
+ projectTargetBackend?.dispose();
1052
1505
  websocketServer.close();
1053
1506
  httpServer.close();
1054
1507
  process.exit(0);
@@ -1191,6 +1644,6 @@ function toSearchValue(value) {
1191
1644
  return stableStringify(value);
1192
1645
  }
1193
1646
  //#endregion
1194
- export { runSyncoreCli };
1647
+ export { SYNCORE_MIGRATION_SNAPSHOT_FILE_NAME, VALID_SYNCORE_TEMPLATES, applyProjectMigrations, detectProjectTemplate, fileExists, formatError, getNextMigrationNumber, hasSyncoreProject, importJsonlIntoProject, isLocalPortInUse, loadProjectConfig, loadProjectFunctions, loadProjectSchema, logScaffoldResult, readPackageJson, readStoredSnapshot, resolveDefaultSeedFile, resolvePortFromEnv, resolveProjectTargetConfig, resolveRequestedTemplate, runCodegen, runDevProjectBootstrap, runSyncoreCli, scaffoldProject, slugify, startDevHub, writeStoredSnapshot };
1195
1648
 
1196
1649
  //# sourceMappingURL=cli.mjs.map