@hatk/hatk 0.0.1-alpha.4 → 0.0.1-alpha.40

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 (150) hide show
  1. package/dist/adapter.d.ts +19 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/adapter.js +107 -0
  4. package/dist/backfill.d.ts +60 -1
  5. package/dist/backfill.d.ts.map +1 -1
  6. package/dist/backfill.js +167 -33
  7. package/dist/car.d.ts +59 -1
  8. package/dist/car.d.ts.map +1 -1
  9. package/dist/car.js +179 -7
  10. package/dist/cbor.d.ts +37 -0
  11. package/dist/cbor.d.ts.map +1 -1
  12. package/dist/cbor.js +36 -3
  13. package/dist/cid.d.ts +37 -0
  14. package/dist/cid.d.ts.map +1 -1
  15. package/dist/cid.js +38 -3
  16. package/dist/cli.js +417 -133
  17. package/dist/cloudflare/container.d.ts +73 -0
  18. package/dist/cloudflare/container.d.ts.map +1 -0
  19. package/dist/cloudflare/container.js +232 -0
  20. package/dist/cloudflare/hooks.d.ts +33 -0
  21. package/dist/cloudflare/hooks.d.ts.map +1 -0
  22. package/dist/cloudflare/hooks.js +40 -0
  23. package/dist/cloudflare/init.d.ts +27 -0
  24. package/dist/cloudflare/init.d.ts.map +1 -0
  25. package/dist/cloudflare/init.js +103 -0
  26. package/dist/cloudflare/worker.d.ts +27 -0
  27. package/dist/cloudflare/worker.d.ts.map +1 -0
  28. package/dist/cloudflare/worker.js +54 -0
  29. package/dist/config.d.ts +12 -1
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +36 -9
  32. package/dist/database/adapter-factory.d.ts +6 -0
  33. package/dist/database/adapter-factory.d.ts.map +1 -0
  34. package/dist/database/adapter-factory.js +20 -0
  35. package/dist/database/adapters/d1.d.ts +56 -0
  36. package/dist/database/adapters/d1.d.ts.map +1 -0
  37. package/dist/database/adapters/d1.js +108 -0
  38. package/dist/database/adapters/duckdb-search.d.ts +12 -0
  39. package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
  40. package/dist/database/adapters/duckdb-search.js +27 -0
  41. package/dist/database/adapters/duckdb.d.ts +25 -0
  42. package/dist/database/adapters/duckdb.d.ts.map +1 -0
  43. package/dist/database/adapters/duckdb.js +161 -0
  44. package/dist/database/adapters/sqlite-search.d.ts +23 -0
  45. package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
  46. package/dist/database/adapters/sqlite-search.js +74 -0
  47. package/dist/database/adapters/sqlite.d.ts +18 -0
  48. package/dist/database/adapters/sqlite.d.ts.map +1 -0
  49. package/dist/database/adapters/sqlite.js +87 -0
  50. package/dist/database/db.d.ts +159 -0
  51. package/dist/database/db.d.ts.map +1 -0
  52. package/dist/database/db.js +1445 -0
  53. package/dist/database/dialect.d.ts +45 -0
  54. package/dist/database/dialect.d.ts.map +1 -0
  55. package/dist/database/dialect.js +72 -0
  56. package/dist/database/fts.d.ts +27 -0
  57. package/dist/database/fts.d.ts.map +1 -0
  58. package/dist/database/fts.js +846 -0
  59. package/dist/database/index.d.ts +7 -0
  60. package/dist/database/index.d.ts.map +1 -0
  61. package/dist/database/index.js +6 -0
  62. package/dist/database/ports.d.ts +50 -0
  63. package/dist/database/ports.d.ts.map +1 -0
  64. package/dist/database/ports.js +1 -0
  65. package/dist/database/schema.d.ts +61 -0
  66. package/dist/database/schema.d.ts.map +1 -0
  67. package/dist/database/schema.js +394 -0
  68. package/dist/db.d.ts +1 -1
  69. package/dist/db.d.ts.map +1 -1
  70. package/dist/db.js +4 -38
  71. package/dist/dev-entry.d.ts +8 -0
  72. package/dist/dev-entry.d.ts.map +1 -0
  73. package/dist/dev-entry.js +110 -0
  74. package/dist/feeds.d.ts +12 -8
  75. package/dist/feeds.d.ts.map +1 -1
  76. package/dist/feeds.js +45 -6
  77. package/dist/fts.d.ts.map +1 -1
  78. package/dist/fts.js +5 -0
  79. package/dist/hooks.d.ts +22 -0
  80. package/dist/hooks.d.ts.map +1 -0
  81. package/dist/hooks.js +75 -0
  82. package/dist/hydrate.d.ts +6 -5
  83. package/dist/hydrate.d.ts.map +1 -1
  84. package/dist/hydrate.js +4 -16
  85. package/dist/indexer.d.ts +20 -0
  86. package/dist/indexer.d.ts.map +1 -1
  87. package/dist/indexer.js +53 -7
  88. package/dist/labels.d.ts +34 -0
  89. package/dist/labels.d.ts.map +1 -1
  90. package/dist/labels.js +66 -6
  91. package/dist/logger.d.ts +29 -0
  92. package/dist/logger.d.ts.map +1 -1
  93. package/dist/logger.js +29 -0
  94. package/dist/main.js +134 -67
  95. package/dist/mst.d.ts +18 -1
  96. package/dist/mst.d.ts.map +1 -1
  97. package/dist/mst.js +19 -8
  98. package/dist/oauth/db.d.ts.map +1 -1
  99. package/dist/oauth/db.js +43 -17
  100. package/dist/oauth/server.d.ts +2 -0
  101. package/dist/oauth/server.d.ts.map +1 -1
  102. package/dist/oauth/server.js +102 -7
  103. package/dist/oauth/session.d.ts +11 -0
  104. package/dist/oauth/session.d.ts.map +1 -0
  105. package/dist/oauth/session.js +65 -0
  106. package/dist/opengraph.d.ts +10 -0
  107. package/dist/opengraph.d.ts.map +1 -1
  108. package/dist/opengraph.js +73 -39
  109. package/dist/pds-proxy.d.ts +42 -0
  110. package/dist/pds-proxy.d.ts.map +1 -0
  111. package/dist/pds-proxy.js +189 -0
  112. package/dist/renderer.d.ts +27 -0
  113. package/dist/renderer.d.ts.map +1 -0
  114. package/dist/renderer.js +46 -0
  115. package/dist/resolve-hatk.d.ts +6 -0
  116. package/dist/resolve-hatk.d.ts.map +1 -0
  117. package/dist/resolve-hatk.js +20 -0
  118. package/dist/response.d.ts +16 -0
  119. package/dist/response.d.ts.map +1 -0
  120. package/dist/response.js +69 -0
  121. package/dist/scanner.d.ts +21 -0
  122. package/dist/scanner.d.ts.map +1 -0
  123. package/dist/scanner.js +88 -0
  124. package/dist/schema.d.ts +8 -0
  125. package/dist/schema.d.ts.map +1 -1
  126. package/dist/schema.js +29 -0
  127. package/dist/seed.d.ts +19 -0
  128. package/dist/seed.d.ts.map +1 -1
  129. package/dist/seed.js +43 -4
  130. package/dist/server-init.d.ts +8 -0
  131. package/dist/server-init.d.ts.map +1 -0
  132. package/dist/server-init.js +61 -0
  133. package/dist/server.d.ts +26 -3
  134. package/dist/server.d.ts.map +1 -1
  135. package/dist/server.js +528 -635
  136. package/dist/setup.d.ts +28 -1
  137. package/dist/setup.d.ts.map +1 -1
  138. package/dist/setup.js +50 -3
  139. package/dist/test.d.ts +1 -1
  140. package/dist/test.d.ts.map +1 -1
  141. package/dist/test.js +38 -32
  142. package/dist/views.js +1 -1
  143. package/dist/vite-plugin.d.ts +1 -1
  144. package/dist/vite-plugin.d.ts.map +1 -1
  145. package/dist/vite-plugin.js +254 -66
  146. package/dist/xrpc.d.ts +46 -10
  147. package/dist/xrpc.d.ts.map +1 -1
  148. package/dist/xrpc.js +128 -39
  149. package/package.json +13 -6
  150. package/public/admin.html +0 -54
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { mkdirSync, writeFileSync, existsSync, unlinkSync, readdirSync, readFileSync } from 'node:fs';
3
- import { resolve, join } from 'node:path';
4
- import { execSync } from 'node:child_process';
5
- import { loadLexicons } from "./schema.js";
3
+ import { resolve, join, dirname } from 'node:path';
4
+ import { execSync, spawn } from 'node:child_process';
5
+ import { loadLexicons, discoverCollections, buildSchemas } from "./database/schema.js";
6
6
  import { loadConfig } from "./config.js";
7
7
  const args = process.argv.slice(2);
8
8
  const command = args[0];
@@ -34,6 +34,31 @@ async function ensurePds() {
34
34
  console.error('[dev] PDS failed to start');
35
35
  process.exit(1);
36
36
  }
37
+ /** Spawn a long-running process and forward SIGINT/SIGTERM for clean shutdown. */
38
+ function spawnForward(cmd, args, env) {
39
+ return new Promise((resolve, reject) => {
40
+ const child = spawn(cmd, args, {
41
+ stdio: 'inherit',
42
+ cwd: process.cwd(),
43
+ env: { ...process.env, ...env },
44
+ });
45
+ const onSignal = (sig) => {
46
+ child.kill(sig);
47
+ };
48
+ process.on('SIGINT', onSignal);
49
+ process.on('SIGTERM', onSignal);
50
+ child.on('close', (code, signal) => {
51
+ process.removeListener('SIGINT', onSignal);
52
+ process.removeListener('SIGTERM', onSignal);
53
+ if (signal === 'SIGINT' || signal === 'SIGTERM')
54
+ process.exit(0);
55
+ if (code === 0 || code === null)
56
+ resolve();
57
+ else
58
+ reject(new Error(`Process exited with code ${code}`));
59
+ });
60
+ });
61
+ }
37
62
  function runSeed() {
38
63
  const seedFile = resolve('seeds/seed.ts');
39
64
  if (!existsSync(seedFile))
@@ -45,7 +70,7 @@ function usage() {
45
70
  Usage: hatk <command> [options]
46
71
 
47
72
  Getting Started
48
- new <name> [--svelte] [--template <t>] Create a new hatk project
73
+ new <name> [--svelte] [--duckdb] [--template <t>] Create a new hatk project
49
74
 
50
75
  Running
51
76
  start Start the hatk server
@@ -70,7 +95,8 @@ function usage() {
70
95
  generate xrpc <nsid> Generate an XRPC handler
71
96
  generate label <name> Generate a label definition
72
97
  generate og <name> Generate an OpenGraph route
73
- generate job <name> Generate a periodic job
98
+ generate hook <name> Generate a lifecycle hook
99
+ generate setup <name> Generate a setup script
74
100
  generate types Regenerate TypeScript types from lexicons
75
101
  destroy <type> <name> Remove a generated file
76
102
 
@@ -98,7 +124,7 @@ export default defineFeed({
98
124
  },
99
125
  })
100
126
  `,
101
- xrpc: (name) => `import { defineQuery } from '${xrpcImportPath(name)}'
127
+ xrpc: (name) => `import { defineQuery } from '../hatk.generated.ts'
102
128
 
103
129
  export default defineQuery('${name}', async (ctx) => {
104
130
  const { ok, db, params, packCursor, unpackCursor } = ctx
@@ -169,22 +195,19 @@ export default {
169
195
  },
170
196
  }
171
197
  `,
172
- job: (_name) => `export default {
173
- interval: 300, // seconds
174
- async run(_ctx: any) {
175
- // Periodic task logic here
176
- },
177
- }
198
+ hook: (name) => `import { defineHook } from '../hatk.generated.ts'
199
+
200
+ export default defineHook('${name}', async (ctx) => {
201
+ // Hook logic here
202
+ })
203
+ `,
204
+ setup: (_name) => `import { defineSetup } from '../hatk.generated.ts'
205
+
206
+ export default defineSetup(async (ctx) => {
207
+ // Setup logic here — runs before the server starts
208
+ })
178
209
  `,
179
210
  };
180
- // Compute relative import path from xrpc/ns/id/method.ts back to hatk.generated.ts
181
- // e.g. fm.teal.getStats → xrpc/fm/teal/getStats.ts → needs ../../../hatk.generated.ts
182
- // Parts: [fm, teal, getStats] → 2 namespace dirs + xrpc/ dir = 3 levels up
183
- function xrpcImportPath(nsid) {
184
- const parts = nsid.split('.');
185
- const namespaceDirs = parts.length - 1; // dirs created from namespace segments
186
- return '../'.repeat(namespaceDirs + 1) + 'hatk.generated.ts'; // +1 for xrpc/ dir itself
187
- }
188
211
  const testTemplates = {
189
212
  feed: (name) => `import { describe, test, expect, beforeAll, afterAll } from 'vitest'
190
213
  import { createTestContext } from '@hatk/hatk/test'
@@ -293,17 +316,18 @@ const lexiconTemplates = {
293
316
  }),
294
317
  };
295
318
  const dirs = {
296
- feed: 'feeds',
297
- xrpc: 'xrpc',
298
- label: 'labels',
299
- og: 'og',
300
- job: 'jobs',
319
+ feed: 'server',
320
+ xrpc: 'server',
321
+ label: 'server',
322
+ og: 'server',
323
+ hook: 'server',
324
+ setup: 'server',
301
325
  };
302
326
  // --- Commands ---
303
327
  if (command === 'new') {
304
328
  const name = args[1];
305
329
  if (!name) {
306
- console.error('Usage: hatk new <name> [--svelte] [--template <template-name>]');
330
+ console.error('Usage: hatk new <name> [--svelte] [--duckdb] [--template <template-name>]');
307
331
  process.exit(1);
308
332
  }
309
333
  const templateIdx = args.indexOf('--template');
@@ -341,20 +365,16 @@ if (command === 'new') {
341
365
  process.exit(0);
342
366
  }
343
367
  const withSvelte = args.includes('--svelte');
368
+ const withDuckdb = args.includes('--duckdb');
369
+ const dbEngine = withDuckdb ? 'duckdb' : 'sqlite';
344
370
  mkdirSync(dir);
345
371
  const subs = [
346
372
  'lexicons',
347
- 'feeds',
348
- 'xrpc',
349
- 'og',
350
- 'labels',
351
- 'jobs',
373
+ 'server',
352
374
  'seeds',
353
- 'setup',
354
375
  'public',
355
376
  'test',
356
- 'test/feeds',
357
- 'test/xrpc',
377
+ 'test/server',
358
378
  'test/integration',
359
379
  'test/browser',
360
380
  'test/fixtures',
@@ -364,14 +384,19 @@ if (command === 'new') {
364
384
  for (const sub of subs) {
365
385
  mkdirSync(join(dir, sub));
366
386
  }
367
- writeFileSync(join(dir, 'config.yaml'), `relay: ws://localhost:2583
368
- plc: http://localhost:2582
369
- port: 3000
370
- database: data/hatk.db
371
- admins: []
387
+ writeFileSync(join(dir, 'hatk.config.ts'), `import { defineConfig } from '@hatk/hatk/config'
372
388
 
373
- backfill:
374
- parallelism: 10
389
+ export default defineConfig({
390
+ relay: 'ws://localhost:2583',
391
+ plc: 'http://localhost:2582',
392
+ port: 3000,
393
+ databaseEngine: '${dbEngine}',
394
+ database: 'data/hatk.db',
395
+ admins: [],
396
+ backfill: {
397
+ parallelism: 10,
398
+ },
399
+ })
375
400
  `);
376
401
  writeFileSync(join(dir, 'public', 'index.html'), `<!DOCTYPE html>
377
402
  <html><head><title>${name}</title></head>
@@ -507,6 +532,14 @@ backfill:
507
532
  properties: {
508
533
  uri: { type: 'string', format: 'at-uri' },
509
534
  cid: { type: 'string', format: 'cid' },
535
+ commit: {
536
+ type: 'object',
537
+ properties: {
538
+ cid: { type: 'string', format: 'cid' },
539
+ rev: { type: 'string' },
540
+ },
541
+ },
542
+ validationStatus: { type: 'string' },
510
543
  },
511
544
  },
512
545
  },
@@ -562,6 +595,14 @@ backfill:
562
595
  properties: {
563
596
  uri: { type: 'string', format: 'at-uri' },
564
597
  cid: { type: 'string', format: 'cid' },
598
+ commit: {
599
+ type: 'object',
600
+ properties: {
601
+ cid: { type: 'string', format: 'cid' },
602
+ rev: { type: 'string' },
603
+ },
604
+ },
605
+ validationStatus: { type: 'string' },
565
606
  },
566
607
  },
567
608
  },
@@ -591,6 +632,53 @@ backfill:
591
632
  },
592
633
  },
593
634
  }, null, 2) + '\n');
635
+ writeFileSync(join(coreLexDir, 'getPreferences.json'), JSON.stringify({
636
+ lexicon: 1,
637
+ id: 'dev.hatk.getPreferences',
638
+ defs: {
639
+ main: {
640
+ type: 'query',
641
+ description: 'Get all preferences for the authenticated user.',
642
+ output: {
643
+ encoding: 'application/json',
644
+ schema: {
645
+ type: 'object',
646
+ properties: {
647
+ preferences: { type: 'unknown' },
648
+ },
649
+ },
650
+ },
651
+ },
652
+ },
653
+ }, null, 2) + '\n');
654
+ writeFileSync(join(coreLexDir, 'putPreference.json'), JSON.stringify({
655
+ lexicon: 1,
656
+ id: 'dev.hatk.putPreference',
657
+ defs: {
658
+ main: {
659
+ type: 'procedure',
660
+ description: 'Set a single preference by key.',
661
+ input: {
662
+ encoding: 'application/json',
663
+ schema: {
664
+ type: 'object',
665
+ required: ['key', 'value'],
666
+ properties: {
667
+ key: { type: 'string' },
668
+ value: { type: 'unknown' },
669
+ },
670
+ },
671
+ },
672
+ output: {
673
+ encoding: 'application/json',
674
+ schema: {
675
+ type: 'object',
676
+ properties: {},
677
+ },
678
+ },
679
+ },
680
+ },
681
+ }, null, 2) + '\n');
594
682
  writeFileSync(join(coreLexDir, 'getFeed.json'), JSON.stringify({
595
683
  lexicon: 1,
596
684
  id: 'dev.hatk.getFeed',
@@ -801,9 +889,12 @@ COPY . .
801
889
  RUN node_modules/.bin/hatk build
802
890
  RUN npm prune --omit=dev
803
891
  EXPOSE 3000
804
- CMD ["node", "node_modules/@hatk/hatk/dist/main.js", "config.yaml"]
892
+ CMD ["node", "--experimental-strip-types", "--max-old-space-size=512", "node_modules/@hatk/hatk/dist/main.js", "hatk.config.ts"]
805
893
  `);
806
894
  const pkgDeps = { '@hatk/oauth-client': '*', hatk: '*' };
895
+ if (!withDuckdb) {
896
+ pkgDeps['better-sqlite3'] = '^11';
897
+ }
807
898
  const pkgDevDeps = {
808
899
  '@playwright/test': '^1',
809
900
  oxfmt: '^0.35.0',
@@ -846,7 +937,7 @@ CMD ["node", "node_modules/@hatk/hatk/dist/main.js", "config.yaml"]
846
937
  allowImportingTsExtensions: true,
847
938
  resolveJsonModule: true,
848
939
  },
849
- include: ['feeds', 'xrpc', 'og', 'seeds', 'labels', 'jobs', 'setup', 'hatk.generated.ts'],
940
+ include: ['server', 'seeds', 'hatk.generated.ts', 'hatk.config.ts'],
850
941
  }, null, 2) + '\n');
851
942
  writeFileSync(join(dir, 'playwright.config.ts'), `import { defineConfig } from '@playwright/test'
852
943
 
@@ -1031,16 +1122,87 @@ a {
1031
1122
  </div>
1032
1123
  `);
1033
1124
  }
1125
+ let agentsMd = `# hatk project
1126
+
1127
+ This is an AT Protocol application built with [hatk](https://github.com/hatk-dev/hatk).
1128
+ Read the project's lexicons in \`lexicons/\` to understand the data model.
1129
+ Types are generated from lexicons into \`hatk.generated.ts\` — never edit this file directly.
1130
+
1131
+ ## Project structure
1132
+
1133
+ | Directory | Purpose |
1134
+ |-------------|------------------------------------------------------|
1135
+ | \`lexicons/\` | AT Protocol lexicon schemas (JSON). Defines collections and XRPC methods |
1136
+ | \`server/\` | All server-side code: feeds, XRPC handlers, hooks, labels, OG routes, setup scripts |
1137
+ | \`seeds/\` | Test data seeding scripts for local development |
1138
+ | \`test/\` | Test files (vitest). Run with \`vp test\` |
1139
+ | \`public/\` | Static files served at the root |
1140
+ `;
1141
+ if (withSvelte) {
1142
+ agentsMd += `| \`src/\` | SvelteKit frontend (routes, components, styles) |
1143
+
1144
+ `;
1145
+ }
1146
+ else {
1147
+ agentsMd += `
1148
+ `;
1149
+ }
1150
+ agentsMd += `## Key files
1151
+
1152
+ - \`hatk.config.ts\` — project configuration (see \`defineConfig\` for type info)
1153
+ - \`hatk.generated.ts\` — auto-generated server types and helpers. Regenerate with \`hatk generate types\`
1154
+ - \`hatk.generated.client.ts\` — auto-generated client-safe types and \`callXrpc\`. Never import \`hatk.generated.ts\` from frontend code
1155
+
1156
+ ## The \`$hatk\` alias
1157
+
1158
+ Server files in \`server/\` import from \`$hatk\`:
1159
+ \`\`\`ts
1160
+ import { defineFeed, views, type Status } from "$hatk"
1161
+ \`\`\`
1162
+ `;
1163
+ if (withSvelte) {
1164
+ agentsMd += `
1165
+ SvelteKit routes and components import from \`$hatk/client\`:
1166
+ \`\`\`ts
1167
+ import { callXrpc, getViewer } from "$hatk/client"
1168
+ \`\`\`
1169
+
1170
+ \`$hatk\` resolves to \`hatk.generated.ts\` and \`$hatk/client\` to \`hatk.generated.client.ts\`.
1171
+ The Vite plugin handles this in dev/build. In tests and production, a Node.js module resolve hook handles it.
1172
+ `;
1173
+ }
1174
+ else {
1175
+ agentsMd += `
1176
+ \`$hatk\` resolves to \`hatk.generated.ts\`. The Vite plugin handles this in dev/build.
1177
+ In tests and production, a Node.js module resolve hook handles it.
1178
+ `;
1179
+ }
1180
+ agentsMd += `
1181
+ ## Commands
1182
+
1183
+ Run \`npx hatk --help\` for the full list of commands.
1184
+
1185
+ Use \`npx hatk generate\` to scaffold new feeds, xrpc handlers, labels, and lexicons
1186
+ rather than creating files manually. These generate files with the correct imports.
1187
+
1188
+ After modifying lexicons, always run \`npx hatk generate types\` to update the generated types.
1189
+ `;
1190
+ if (withSvelte) {
1191
+ agentsMd += `
1192
+ ## Running
1193
+
1194
+ - \`vp dev\` — start dev server (hatk + SvelteKit + PDS)
1195
+ - \`vp build\` — build for production (SvelteKit outputs to \`build/\`)
1196
+ - \`hatk start\` — start production server (hatk + SvelteKit via \`build/handler.js\`)
1197
+ - \`vp test\` — run tests
1198
+ `;
1199
+ }
1200
+ writeFileSync(join(dir, 'AGENTS.md'), agentsMd);
1034
1201
  console.log(`Created ${name}/`);
1035
- console.log(` config.yaml`);
1202
+ console.log(` hatk.config.ts`);
1036
1203
  console.log(` lexicons/ — lexicon JSON files (core + your own)`);
1037
- console.log(` feeds/ feed generators`);
1038
- console.log(` xrpc/ — XRPC method handlers`);
1039
- console.log(` og/ — OpenGraph image routes`);
1040
- console.log(` labels/ — label definitions + rules`);
1041
- console.log(` jobs/ — periodic tasks`);
1204
+ console.log(` server/ feeds, XRPC handlers, hooks, labels, OG routes, setup`);
1042
1205
  console.log(` seeds/ — seed fixture data (hatk seed)`);
1043
- console.log(` setup/ — boot-time setup scripts (run before server starts)`);
1044
1206
  console.log(` test/ — test files (hatk test)`);
1045
1207
  console.log(` public/ — static files`);
1046
1208
  console.log(` docker-compose.yml — local PDS for development`);
@@ -1079,6 +1241,21 @@ else if (command === 'generate') {
1079
1241
  }
1080
1242
  }
1081
1243
  entries.sort((a, b) => a.nsid.localeCompare(b.nsid));
1244
+ // Collect procedure nsids and blob-input nsids for client callXrpc
1245
+ const procedureNsids = [];
1246
+ const blobInputNsids = [];
1247
+ for (const { nsid, defType } of entries) {
1248
+ if (defType === 'procedure') {
1249
+ const lex = lexicons.get(nsid);
1250
+ const inputEncoding = lex?.defs?.main?.input?.encoding;
1251
+ if (inputEncoding === '*/*') {
1252
+ blobInputNsids.push(nsid);
1253
+ }
1254
+ else {
1255
+ procedureNsids.push(nsid);
1256
+ }
1257
+ }
1258
+ }
1082
1259
  if (entries.length === 0) {
1083
1260
  console.error('No lexicons found');
1084
1261
  process.exit(1);
@@ -1126,7 +1303,8 @@ else if (command === 'generate') {
1126
1303
  let out = '// Auto-generated from lexicons. Do not edit.\n';
1127
1304
  out += `import type { ${[...usedWrappers].sort().join(', ')}, LexServerParams, Checked, Prettify, StrictArg } from '@hatk/hatk/lex-types'\n`;
1128
1305
  out += `import type { XrpcContext } from '@hatk/hatk/xrpc'\n`;
1129
- out += `import { defineFeed as _defineFeed, type FeedResult, type FeedContext, type HydrateContext } from '@hatk/hatk/feeds'\n`;
1306
+ out += `import { callXrpc as _callXrpc } from '@hatk/hatk/xrpc'\n`;
1307
+ out += `import { defineFeed as _defineFeed, type FeedResult, type FeedContext, type BaseContext, type Row } from '@hatk/hatk/feeds'\n`;
1130
1308
  out += `import { seed as _seed, type SeedOpts } from '@hatk/hatk/seed'\n`;
1131
1309
  // Emit ALL lexicons as `const ... = {...} as const` (including defs-only)
1132
1310
  out += `\n// ─── Lexicon Definitions ────────────────────────────────────────────\n\n`;
@@ -1282,8 +1460,13 @@ else if (command === 'generate') {
1282
1460
  out += `}\n`;
1283
1461
  // Emit Ctx helper for typesafe XRPC handler contexts
1284
1462
  out += `\n// ─── XRPC Helpers ───────────────────────────────────────────────────\n\n`;
1285
- out += `export type { HydrateContext } from '@hatk/hatk/feeds'\n`;
1463
+ out += `export type { BaseContext, Row } from '@hatk/hatk/feeds'\n`;
1286
1464
  out += `export { InvalidRequestError, NotFoundError } from '@hatk/hatk/xrpc'\n`;
1465
+ out += `export { defineSetup } from '@hatk/hatk/setup'\n`;
1466
+ out += `export { defineHook } from '@hatk/hatk/hooks'\n`;
1467
+ out += `export { defineLabel } from '@hatk/hatk/labels'\n`;
1468
+ out += `export { defineOG } from '@hatk/hatk/opengraph'\n`;
1469
+ out += `export { defineRenderer } from '@hatk/hatk/renderer'\n`;
1287
1470
  out += `export type Ctx<K extends keyof XrpcSchema & keyof Registry> = XrpcContext<\n`;
1288
1471
  out += ` LexServerParams<Registry[K], Registry>,\n`;
1289
1472
  out += ` RecordRegistry,\n`;
@@ -1296,21 +1479,29 @@ else if (command === 'generate') {
1296
1479
  out += ` nsid: K,\n`;
1297
1480
  out += ` handler: (ctx: Ctx<K> & { ok: <T extends OutputOf<K>>(value: StrictArg<T, OutputOf<K>>) => Checked<OutputOf<K>> }) => Promise<Checked<OutputOf<K>>>,\n`;
1298
1481
  out += `) {\n`;
1299
- out += ` return { handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
1482
+ out += ` return { __type: 'query' as const, nsid, handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
1300
1483
  out += `}\n\n`;
1301
1484
  out += `export function defineProcedure<K extends keyof XrpcSchema & string>(\n`;
1302
1485
  out += ` nsid: K,\n`;
1303
1486
  out += ` handler: (ctx: Ctx<K> & { ok: <T extends OutputOf<K>>(value: StrictArg<T, OutputOf<K>>) => Checked<OutputOf<K>> }) => Promise<Checked<OutputOf<K>>>,\n`;
1304
1487
  out += `) {\n`;
1305
- out += ` return { handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
1488
+ out += ` return { __type: 'procedure' as const, nsid, handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
1489
+ out += `}\n\n`;
1490
+ out += `// ─── Server-side XRPC Caller ────────────────────────────────────────\n\n`;
1491
+ out += `type ExtractParams<T> = T extends { params: infer P } ? P : Record<string, unknown>\n`;
1492
+ out += `export async function callXrpc<K extends keyof XrpcSchema & string>(\n`;
1493
+ out += ` nsid: K,\n`;
1494
+ out += ` params?: ExtractParams<XrpcSchema[K]>,\n`;
1495
+ out += `): Promise<OutputOf<K>> {\n`;
1496
+ out += ` return _callXrpc(nsid, params as any) as Promise<OutputOf<K>>\n`;
1306
1497
  out += `}\n\n`;
1307
1498
  out += `// ─── Feed & Seed Helpers ────────────────────────────────────────────\n\n`;
1308
1499
  out += `type FeedGenerate = (ctx: FeedContext & { ok: (value: FeedResult) => Checked<FeedResult> }) => Promise<Checked<FeedResult>>\n`;
1309
1500
  out += `export function defineFeed<K extends keyof RecordRegistry>(\n`;
1310
- out += ` opts: { collection: K; view?: string; label: string; generate: FeedGenerate; hydrate?: (ctx: HydrateContext<RecordRegistry[K]>) => Promise<unknown[]> }\n`;
1501
+ out += ` opts: { collection: K; view?: string; label: string; generate: FeedGenerate; hydrate?: (ctx: BaseContext, items: Row<RecordRegistry[K]>[]) => Promise<unknown[]> }\n`;
1311
1502
  out += `): ReturnType<typeof _defineFeed>\n`;
1312
1503
  out += `export function defineFeed(\n`;
1313
- out += ` opts: { collection?: never; view?: never; label: string; generate: FeedGenerate; hydrate: (ctx: HydrateContext<any>) => Promise<unknown[]> }\n`;
1504
+ out += ` opts: { collection?: never; view?: never; label: string; generate: FeedGenerate; hydrate: (ctx: BaseContext, items: Row<unknown>[]) => Promise<unknown[]> }\n`;
1314
1505
  out += `): ReturnType<typeof _defineFeed>\n`;
1315
1506
  out += `export function defineFeed(opts: any) { return _defineFeed(opts) }\n`;
1316
1507
  out += `export function seed(opts?: SeedOpts) { return _seed<RecordRegistry>(opts) }\n`;
@@ -1333,7 +1524,145 @@ else if (command === 'generate') {
1333
1524
  out = out.replace(/import type \{ ([^}]+) \} from '@hatk\/hatk\/lex-types'/, `import type { ${[...usedWrappers].sort().join(', ')}, LexServerParams, Checked, Prettify, StrictArg } from '@hatk/hatk/lex-types'`);
1334
1525
  }
1335
1526
  writeFileSync(outPath, out);
1527
+ // Generate client-safe version (types + callXrpc only, no server module re-exports)
1528
+ // Types use `export type` from main file (erased at compile time, no runtime import).
1529
+ // callXrpc imports from @hatk/hatk/xrpc directly to avoid pulling in server deps.
1530
+ let clientOut = '// Auto-generated client-safe subset. Do not edit.\n';
1531
+ clientOut += `// Import this in app components instead of hatk.generated.ts\n`;
1532
+ clientOut += `// to avoid pulling in server-only dependencies.\n`;
1533
+ clientOut += `export type { XrpcSchema } from './hatk.generated.ts'\n`;
1534
+ clientOut += `import type { XrpcSchema } from './hatk.generated.ts'\n`;
1535
+ // Re-export all types
1536
+ const typeExports = [];
1537
+ for (const { nsid, defType } of entries) {
1538
+ if (!defType)
1539
+ continue;
1540
+ if (nsid === 'dev.hatk.createRecord' || nsid === 'dev.hatk.deleteRecord' || nsid === 'dev.hatk.putRecord')
1541
+ continue;
1542
+ typeExports.push(capitalize(varNames.get(nsid)));
1543
+ }
1544
+ if (recordEntries.length > 0) {
1545
+ typeExports.push('RecordRegistry', 'CreateRecord', 'DeleteRecord', 'PutRecord');
1546
+ }
1547
+ // Named defs (views, objects) — collect from emittedDefNames minus main types
1548
+ const mainTypeNames = new Set(entries.filter((e) => e.defType).map((e) => capitalize(varNames.get(e.nsid))));
1549
+ for (const name of emittedDefNames) {
1550
+ if (!mainTypeNames.has(name) && !typeExports.includes(name)) {
1551
+ typeExports.push(name);
1552
+ }
1553
+ }
1554
+ if (typeExports.length > 0) {
1555
+ clientOut += `export type { ${typeExports.join(', ')} } from './hatk.generated.ts'\n`;
1556
+ }
1557
+ // Typed callXrpc — environment-aware:
1558
+ // SSR: uses globalThis.__hatk_callXrpc bridge (direct handler invocation)
1559
+ // Client: fetches via HTTP (GET for queries, POST for procedures, raw POST for blobs)
1560
+ if (procedureNsids.length > 0) {
1561
+ clientOut += `\nconst _procedures = new Set([${procedureNsids.map((n) => `'${n}'`).join(', ')}])\n`;
1562
+ }
1563
+ if (blobInputNsids.length > 0) {
1564
+ clientOut += `const _blobInputs = new Set([${blobInputNsids.map((n) => `'${n}'`).join(', ')}])\n`;
1565
+ }
1566
+ clientOut += `\ntype CallArg<K extends keyof XrpcSchema> =\n`;
1567
+ clientOut += ` XrpcSchema[K] extends { input: infer I } ? I :\n`;
1568
+ clientOut += ` XrpcSchema[K] extends { params: infer P } ? P :\n`;
1569
+ clientOut += ` Record<string, unknown>\n`;
1570
+ clientOut += `type OutputOf<K extends keyof XrpcSchema> = XrpcSchema[K]['output']\n\n`;
1571
+ clientOut += `export async function callXrpc<K extends keyof XrpcSchema & string>(\n`;
1572
+ clientOut += ` nsid: K,\n`;
1573
+ clientOut += ` arg?: CallArg<K>,\n`;
1574
+ clientOut += ` customFetch?: typeof globalThis.fetch,\n`;
1575
+ clientOut += `): Promise<OutputOf<K>> {\n`;
1576
+ // Server-side bridge (skip when customFetch is provided — let SvelteKit's fetch handle it)
1577
+ clientOut += ` if (typeof window === 'undefined' && !customFetch) {\n`;
1578
+ clientOut += ` const bridge = (globalThis as any).__hatk_callXrpc\n`;
1579
+ clientOut += ` if (!bridge) throw new Error('callXrpc: server bridge not available — is hatk initialized?')\n`;
1580
+ if (procedureNsids.length > 0 || blobInputNsids.length > 0) {
1581
+ const checks = [];
1582
+ if (procedureNsids.length > 0)
1583
+ checks.push('_procedures.has(nsid)');
1584
+ if (blobInputNsids.length > 0)
1585
+ checks.push('_blobInputs.has(nsid)');
1586
+ clientOut += ` if (${checks.join(' || ')}) return bridge(nsid, {}, arg) as Promise<OutputOf<K>>\n`;
1587
+ }
1588
+ clientOut += ` return bridge(nsid, arg) as Promise<OutputOf<K>>\n`;
1589
+ clientOut += ` }\n`;
1590
+ // Client-side fetch (or server-side with customFetch for SSR deduplication)
1591
+ clientOut += ` const _fetch = customFetch ?? globalThis.fetch\n`;
1592
+ clientOut += ` // Use relative URL so SvelteKit's fetch can deduplicate server/client requests\n`;
1593
+ clientOut += ` let path = \`/xrpc/\${nsid}\`\n`;
1594
+ if (blobInputNsids.length > 0) {
1595
+ clientOut += ` if (_blobInputs.has(nsid)) {\n`;
1596
+ clientOut += ` const blob = arg as Blob | ArrayBuffer\n`;
1597
+ clientOut += ` const ct = blob instanceof Blob ? blob.type : 'application/octet-stream'\n`;
1598
+ clientOut += ` const res = await _fetch(path, { method: 'POST', headers: { 'Content-Type': ct }, body: blob })\n`;
1599
+ clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n`;
1600
+ clientOut += ` return res.json() as Promise<OutputOf<K>>\n`;
1601
+ clientOut += ` }\n`;
1602
+ }
1603
+ if (procedureNsids.length > 0) {
1604
+ clientOut += ` if (_procedures.has(nsid)) {\n`;
1605
+ clientOut += ` const res = await _fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(arg) })\n`;
1606
+ clientOut += ` if (typeof window !== 'undefined' && res.status === 401) { const _h = getViewer()?.handle; window.location.href = _h ? \`/oauth/login?handle=\${encodeURIComponent(_h)}\` : '/oauth/login'; return new Promise(() => {}) as any }\n`;
1607
+ clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n`;
1608
+ clientOut += ` return res.json() as Promise<OutputOf<K>>\n`;
1609
+ clientOut += ` }\n`;
1610
+ }
1611
+ clientOut += ` const params = new URLSearchParams()\n`;
1612
+ clientOut += ` for (const [k, v] of Object.entries(arg || {})) {\n`;
1613
+ clientOut += ` if (v != null) params.set(k, String(v))\n`;
1614
+ clientOut += ` }\n`;
1615
+ clientOut += ` const qs = params.toString()\n`;
1616
+ clientOut += ` if (qs) path += \`?\${qs}\`\n`;
1617
+ clientOut += ` const res = await _fetch(path)\n`;
1618
+ clientOut += ` if (typeof window !== 'undefined' && res.status === 401) { window.location.href = '/oauth/login'; return new Promise(() => {}) as any }\n`;
1619
+ clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n`;
1620
+ clientOut += ` return res.json() as Promise<OutputOf<K>>\n`;
1621
+ clientOut += `}\n`;
1622
+ // getViewer — returns the viewer set by layout load (server) or $effect (client)
1623
+ clientOut += `\nexport function getViewer(): { did: string; handle: string } | null {\n`;
1624
+ clientOut += ` return (globalThis as any).__hatk_viewer ?? null\n`;
1625
+ clientOut += `}\n`;
1626
+ // Auth helpers — login, logout, viewerDid
1627
+ clientOut += `\n// ─── Auth Helpers ────────────────────────────────────────────────────\n\n`;
1628
+ clientOut += `export async function login(handle: string): Promise<void> {\n`;
1629
+ clientOut += ` const res = await fetch(\`/oauth/login?handle=\${encodeURIComponent(handle)}\`, { redirect: 'manual' })\n`;
1630
+ clientOut += ` if (res.type === 'opaqueredirect') {\n`;
1631
+ clientOut += ` window.location.href = \`/oauth/login?handle=\${encodeURIComponent(handle)}\`\n`;
1632
+ clientOut += ` return\n`;
1633
+ clientOut += ` }\n`;
1634
+ clientOut += ` if (res.ok) return\n`;
1635
+ clientOut += ` const body = await res.json().catch(() => ({ error: 'Login failed' }))\n`;
1636
+ clientOut += ` throw new Error(body.error || 'Login failed')\n`;
1637
+ clientOut += `}\n\n`;
1638
+ clientOut += `export async function logout(): Promise<void> {\n`;
1639
+ clientOut += ` ;(globalThis as any).__hatk_viewer = null\n`;
1640
+ clientOut += ` await fetch('/auth/logout', { method: 'POST' }).catch(() => {})\n`;
1641
+ clientOut += `}\n\n`;
1642
+ clientOut += `export function viewerDid(): string | null {\n`;
1643
+ clientOut += ` if (typeof window === 'undefined') return null\n`;
1644
+ clientOut += ` const viewer = (globalThis as any).__hatk_viewer\n`;
1645
+ clientOut += ` return viewer?.did ?? null\n`;
1646
+ clientOut += `}\n\n`;
1647
+ clientOut += `// Expose viewer for getViewer() bridge\n`;
1648
+ clientOut += `;(globalThis as any).__hatk_auth = { viewerDid }\n`;
1649
+ // parseViewer — server-side session cookie resolution for +layout.server.ts
1650
+ clientOut += `\n// ─── Server Helpers ──────────────────────────────────────────────────\n\n`;
1651
+ clientOut += `export async function parseViewer(cookies: { get(name: string): string | undefined }): Promise<{ did: string; handle?: string } | null> {\n`;
1652
+ clientOut += ` const parseSessionCookie = (globalThis as any).__hatk_parseSessionCookie\n`;
1653
+ clientOut += ` if (!parseSessionCookie) return null\n`;
1654
+ clientOut += ` const cookieValue = cookies.get('__hatk_session')\n`;
1655
+ clientOut += ` if (!cookieValue) return null\n`;
1656
+ clientOut += ` try {\n`;
1657
+ clientOut += ` const request = new Request('http://localhost', { headers: { cookie: \`__hatk_session=\${cookieValue}\` } })\n`;
1658
+ clientOut += ` const viewer = await parseSessionCookie(request)\n`;
1659
+ clientOut += ` if (viewer) (globalThis as any).__hatk_viewer = viewer\n`;
1660
+ clientOut += ` return viewer\n`;
1661
+ clientOut += ` } catch { return null }\n`;
1662
+ clientOut += `}\n`;
1663
+ writeFileSync('./hatk.generated.client.ts', clientOut);
1336
1664
  console.log(`Generated ${outPath} with ${entries.length} types: ${entries.map((e) => capitalize(varNames.get(e.nsid))).join(', ')}`);
1665
+ console.log(`Generated ./hatk.generated.client.ts (client-safe subset)`);
1337
1666
  }
1338
1667
  else if (lexiconTemplates[type]) {
1339
1668
  const nsid = args[2];
@@ -1361,18 +1690,9 @@ else if (command === 'generate') {
1361
1690
  process.exit(1);
1362
1691
  }
1363
1692
  const baseDir = dirs[type];
1364
- let filePath;
1365
- if (type === 'xrpc') {
1366
- // NSID folder path: fm.teal.getStats → xrpc/fm/teal/getStats.ts
1367
- const parts = name.split('.');
1368
- const subDir = join(baseDir, ...parts.slice(0, -1));
1369
- mkdirSync(subDir, { recursive: true });
1370
- filePath = join(subDir, `${parts[parts.length - 1]}.ts`);
1371
- }
1372
- else {
1373
- mkdirSync(baseDir, { recursive: true });
1374
- filePath = join(baseDir, `${name}.ts`);
1375
- }
1693
+ mkdirSync(baseDir, { recursive: true });
1694
+ const fileName = type === 'xrpc' ? name.split('.').pop() : name;
1695
+ const filePath = join(baseDir, `${fileName}.ts`);
1376
1696
  if (existsSync(filePath)) {
1377
1697
  console.error(`${filePath} already exists`);
1378
1698
  process.exit(1);
@@ -1382,7 +1702,7 @@ else if (command === 'generate') {
1382
1702
  // Scaffold test file if template exists
1383
1703
  const testTemplate = testTemplates[type];
1384
1704
  if (testTemplate) {
1385
- const testDir = type === 'xrpc' ? 'test/xrpc' : `test/${baseDir}`;
1705
+ const testDir = 'test/server';
1386
1706
  mkdirSync(testDir, { recursive: true });
1387
1707
  const testName = type === 'xrpc' ? name.split('.').pop() : name;
1388
1708
  const testPath = join(testDir, `${testName}.test.ts`);
@@ -1401,18 +1721,9 @@ else if (command === 'destroy') {
1401
1721
  process.exit(1);
1402
1722
  }
1403
1723
  const baseDir = dirs[type];
1404
- let tsPath, jsPath;
1405
- if (type === 'xrpc') {
1406
- const parts = name.split('.');
1407
- const leaf = parts[parts.length - 1];
1408
- const subDir = join(baseDir, ...parts.slice(0, -1));
1409
- tsPath = join(subDir, `${leaf}.ts`);
1410
- jsPath = join(subDir, `${leaf}.js`);
1411
- }
1412
- else {
1413
- tsPath = join(baseDir, `${name}.ts`);
1414
- jsPath = join(baseDir, `${name}.js`);
1415
- }
1724
+ const fileName = type === 'xrpc' ? name.split('.').pop() : name;
1725
+ const tsPath = join(baseDir, `${fileName}.ts`);
1726
+ const jsPath = join(baseDir, `${fileName}.js`);
1416
1727
  const filePath = existsSync(tsPath) ? tsPath : existsSync(jsPath) ? jsPath : null;
1417
1728
  if (!filePath) {
1418
1729
  console.error(`No file found for ${type} "${name}"`);
@@ -1421,7 +1732,7 @@ else if (command === 'destroy') {
1421
1732
  unlinkSync(filePath);
1422
1733
  console.log(`Removed ${filePath}`);
1423
1734
  // Clean up test file
1424
- const testDir = type === 'xrpc' ? 'test/xrpc' : `test/${baseDir}`;
1735
+ const testDir = 'test/server';
1425
1736
  const testName = type === 'xrpc' ? name.split('.').pop() : name;
1426
1737
  const testFile = join(testDir, `${testName}.test.ts`);
1427
1738
  if (existsSync(testFile)) {
@@ -1435,25 +1746,14 @@ else if (command === 'destroy') {
1435
1746
  else if (command === 'dev') {
1436
1747
  await ensurePds();
1437
1748
  runSeed();
1438
- try {
1439
- if (existsSync(resolve('svelte.config.js')) && existsSync(resolve('src/app.html'))) {
1440
- // SvelteKit project — vite dev starts the hatk server via the plugin
1441
- execSync('npx vite dev', { stdio: 'inherit', cwd: process.cwd() });
1442
- }
1443
- else {
1444
- // No frontend — just run the hatk server directly
1445
- const mainPath = resolve(import.meta.dirname, 'main.js');
1446
- execSync(`npx tsx ${mainPath} config.yaml`, {
1447
- stdio: 'inherit',
1448
- cwd: process.cwd(),
1449
- env: { ...process.env, DEV_MODE: '1' },
1450
- });
1451
- }
1749
+ if (existsSync(resolve('vite.config.ts')) || existsSync(resolve('vite.config.js'))) {
1750
+ // Vite project vite dev starts the hatk server via the plugin
1751
+ await spawnForward('npx', ['vite', 'dev']);
1452
1752
  }
1453
- catch (e) {
1454
- if (e.signal === 'SIGINT' || e.signal === 'SIGTERM')
1455
- process.exit(0);
1456
- throw e;
1753
+ else {
1754
+ // No frontend just run the hatk server directly
1755
+ const mainPath = resolve(import.meta.dirname, 'main.js');
1756
+ await spawnForward('npx', ['tsx', mainPath, 'hatk.config.ts'], { DEV_MODE: '1' });
1457
1757
  }
1458
1758
  }
1459
1759
  else if (command === 'format' || command === 'fmt') {
@@ -1473,9 +1773,9 @@ else if (command === 'build') {
1473
1773
  }
1474
1774
  }
1475
1775
  else if (command === 'reset') {
1476
- const config = loadConfig(resolve('config.yaml'));
1776
+ const config = await loadConfig(resolve('hatk.config.ts'));
1477
1777
  if (config.database !== ':memory:') {
1478
- for (const suffix of ['', '.wal']) {
1778
+ for (const suffix of ['', '.wal', '-shm', '-wal']) {
1479
1779
  const file = config.database + suffix;
1480
1780
  if (existsSync(file)) {
1481
1781
  unlinkSync(file);
@@ -1633,40 +1933,24 @@ else if (command === 'resolve') {
1633
1933
  execSync('npx hatk generate types', { stdio: 'inherit', cwd: process.cwd() });
1634
1934
  }
1635
1935
  else if (command === 'schema') {
1636
- const config = loadConfig(resolve('config.yaml'));
1637
- if (config.database === ':memory:') {
1638
- console.error('No database file configured (database is :memory:)');
1639
- process.exit(1);
1640
- }
1641
- if (!existsSync(config.database)) {
1642
- console.error(`Database not found: ${config.database}`);
1643
- console.error('Run "hatk dev" first to create it.');
1644
- process.exit(1);
1645
- }
1646
- const { DuckDBInstance } = await import('@duckdb/node-api');
1647
- const instance = await DuckDBInstance.create(config.database);
1648
- const con = await instance.connect();
1649
- const tables = (await (await con.runAndReadAll(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name`)).getRowObjects());
1650
- for (const { table_name } of tables) {
1651
- console.log(`"${table_name}"`);
1652
- const cols = (await (await con.runAndReadAll(`SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = '${table_name}' ORDER BY ordinal_position`)).getRowObjects());
1653
- for (const col of cols) {
1654
- const nullable = col.is_nullable === 'YES' ? '' : ' NOT NULL';
1655
- console.log(` ${col.column_name.padEnd(20)} ${col.data_type}${nullable}`);
1656
- }
1657
- console.log();
1936
+ const config = await loadConfig(resolve('hatk.config.ts'));
1937
+ const { initDatabase, getSchemaDump } = await import("./database/db.js");
1938
+ const { createAdapter } = await import("./database/adapter-factory.js");
1939
+ const { getDialect } = await import("./database/dialect.js");
1940
+ const configDir2 = resolve('.');
1941
+ const lexicons2 = loadLexicons(resolve(configDir2, 'lexicons'));
1942
+ const collections2 = config.collections.length > 0 ? config.collections : discoverCollections(lexicons2);
1943
+ const { schemas: schemas2, ddlStatements: ddl2 } = buildSchemas(lexicons2, collections2, getDialect(config.databaseEngine));
1944
+ if (config.database !== ':memory:') {
1945
+ mkdirSync(dirname(config.database), { recursive: true });
1658
1946
  }
1947
+ const { adapter: adapter2 } = await createAdapter(config.databaseEngine);
1948
+ await initDatabase(adapter2, config.database, schemas2, ddl2);
1949
+ console.log(await getSchemaDump());
1659
1950
  }
1660
1951
  else if (command === 'start') {
1661
- try {
1662
- const mainPath = resolve(import.meta.dirname, 'main.js');
1663
- execSync(`npx tsx ${mainPath} config.yaml`, { stdio: 'inherit', cwd: process.cwd() });
1664
- }
1665
- catch (e) {
1666
- if (e.signal === 'SIGINT' || e.signal === 'SIGTERM')
1667
- process.exit(0);
1668
- throw e;
1669
- }
1952
+ const mainPath = resolve(import.meta.dirname, 'main.js');
1953
+ await spawnForward('npx', ['tsx', mainPath, 'hatk.config.ts']);
1670
1954
  }
1671
1955
  else {
1672
1956
  usage();