@hatk/hatk 0.0.1-alpha.3 → 0.0.1-alpha.30

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 (132) hide show
  1. package/dist/adapter.d.ts +19 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/adapter.js +94 -0
  4. package/dist/backfill.d.ts +60 -1
  5. package/dist/backfill.d.ts.map +1 -1
  6. package/dist/backfill.js +166 -32
  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 +356 -123
  17. package/dist/config.d.ts +12 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +36 -9
  20. package/dist/database/adapter-factory.d.ts +6 -0
  21. package/dist/database/adapter-factory.d.ts.map +1 -0
  22. package/dist/database/adapter-factory.js +20 -0
  23. package/dist/database/adapters/duckdb-search.d.ts +12 -0
  24. package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
  25. package/dist/database/adapters/duckdb-search.js +27 -0
  26. package/dist/database/adapters/duckdb.d.ts +25 -0
  27. package/dist/database/adapters/duckdb.d.ts.map +1 -0
  28. package/dist/database/adapters/duckdb.js +161 -0
  29. package/dist/database/adapters/sqlite-search.d.ts +18 -0
  30. package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
  31. package/dist/database/adapters/sqlite-search.js +38 -0
  32. package/dist/database/adapters/sqlite.d.ts +18 -0
  33. package/dist/database/adapters/sqlite.d.ts.map +1 -0
  34. package/dist/database/adapters/sqlite.js +87 -0
  35. package/dist/database/db.d.ts +149 -0
  36. package/dist/database/db.d.ts.map +1 -0
  37. package/dist/database/db.js +1460 -0
  38. package/dist/database/dialect.d.ts +45 -0
  39. package/dist/database/dialect.d.ts.map +1 -0
  40. package/dist/database/dialect.js +72 -0
  41. package/dist/database/fts.d.ts +24 -0
  42. package/dist/database/fts.d.ts.map +1 -0
  43. package/dist/database/fts.js +777 -0
  44. package/dist/database/index.d.ts +7 -0
  45. package/dist/database/index.d.ts.map +1 -0
  46. package/dist/database/index.js +6 -0
  47. package/dist/database/ports.d.ts +44 -0
  48. package/dist/database/ports.d.ts.map +1 -0
  49. package/dist/database/ports.js +1 -0
  50. package/dist/database/schema.d.ts +60 -0
  51. package/dist/database/schema.d.ts.map +1 -0
  52. package/dist/database/schema.js +388 -0
  53. package/dist/db.d.ts +1 -1
  54. package/dist/db.d.ts.map +1 -1
  55. package/dist/db.js +4 -38
  56. package/dist/dev-entry.d.ts +8 -0
  57. package/dist/dev-entry.d.ts.map +1 -0
  58. package/dist/dev-entry.js +109 -0
  59. package/dist/feeds.d.ts +4 -0
  60. package/dist/feeds.d.ts.map +1 -1
  61. package/dist/feeds.js +42 -3
  62. package/dist/fts.d.ts.map +1 -1
  63. package/dist/fts.js +5 -0
  64. package/dist/hooks.d.ts +22 -0
  65. package/dist/hooks.d.ts.map +1 -0
  66. package/dist/hooks.js +75 -0
  67. package/dist/hydrate.js +1 -1
  68. package/dist/indexer.d.ts +20 -0
  69. package/dist/indexer.d.ts.map +1 -1
  70. package/dist/indexer.js +48 -6
  71. package/dist/labels.d.ts +34 -0
  72. package/dist/labels.d.ts.map +1 -1
  73. package/dist/labels.js +63 -3
  74. package/dist/logger.d.ts +29 -0
  75. package/dist/logger.d.ts.map +1 -1
  76. package/dist/logger.js +29 -0
  77. package/dist/main.js +131 -67
  78. package/dist/mst.d.ts +18 -1
  79. package/dist/mst.d.ts.map +1 -1
  80. package/dist/mst.js +19 -8
  81. package/dist/oauth/db.d.ts.map +1 -1
  82. package/dist/oauth/db.js +41 -15
  83. package/dist/oauth/server.d.ts +2 -0
  84. package/dist/oauth/server.d.ts.map +1 -1
  85. package/dist/oauth/server.js +102 -7
  86. package/dist/oauth/session.d.ts +9 -0
  87. package/dist/oauth/session.d.ts.map +1 -0
  88. package/dist/oauth/session.js +65 -0
  89. package/dist/opengraph.d.ts +10 -0
  90. package/dist/opengraph.d.ts.map +1 -1
  91. package/dist/opengraph.js +103 -5
  92. package/dist/pds-proxy.d.ts +39 -0
  93. package/dist/pds-proxy.d.ts.map +1 -0
  94. package/dist/pds-proxy.js +173 -0
  95. package/dist/renderer.d.ts +27 -0
  96. package/dist/renderer.d.ts.map +1 -0
  97. package/dist/renderer.js +46 -0
  98. package/dist/resolve-hatk.d.ts +6 -0
  99. package/dist/resolve-hatk.d.ts.map +1 -0
  100. package/dist/resolve-hatk.js +20 -0
  101. package/dist/response.d.ts +16 -0
  102. package/dist/response.d.ts.map +1 -0
  103. package/dist/response.js +69 -0
  104. package/dist/scanner.d.ts +21 -0
  105. package/dist/scanner.d.ts.map +1 -0
  106. package/dist/scanner.js +88 -0
  107. package/dist/schema.d.ts +8 -0
  108. package/dist/schema.d.ts.map +1 -1
  109. package/dist/schema.js +29 -0
  110. package/dist/seed.d.ts +19 -0
  111. package/dist/seed.d.ts.map +1 -1
  112. package/dist/seed.js +43 -4
  113. package/dist/server-init.d.ts +8 -0
  114. package/dist/server-init.d.ts.map +1 -0
  115. package/dist/server-init.js +59 -0
  116. package/dist/server.d.ts +26 -3
  117. package/dist/server.d.ts.map +1 -1
  118. package/dist/server.js +487 -616
  119. package/dist/setup.d.ts +28 -1
  120. package/dist/setup.d.ts.map +1 -1
  121. package/dist/setup.js +50 -3
  122. package/dist/test.d.ts +1 -1
  123. package/dist/test.d.ts.map +1 -1
  124. package/dist/test.js +38 -32
  125. package/dist/views.js +1 -1
  126. package/dist/vite-plugin.d.ts +1 -1
  127. package/dist/vite-plugin.d.ts.map +1 -1
  128. package/dist/vite-plugin.js +252 -66
  129. package/dist/xrpc.d.ts +36 -0
  130. package/dist/xrpc.d.ts.map +1 -1
  131. package/dist/xrpc.js +124 -3
  132. package/package.json +12 -5
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
@@ -71,6 +96,8 @@ function usage() {
71
96
  generate label <name> Generate a label definition
72
97
  generate og <name> Generate an OpenGraph route
73
98
  generate job <name> Generate a periodic job
99
+ generate hook <name> Generate a lifecycle hook
100
+ generate setup <name> Generate a setup script
74
101
  generate types Regenerate TypeScript types from lexicons
75
102
  destroy <type> <name> Remove a generated file
76
103
 
@@ -98,7 +125,7 @@ export default defineFeed({
98
125
  },
99
126
  })
100
127
  `,
101
- xrpc: (name) => `import { defineQuery } from '${xrpcImportPath(name)}'
128
+ xrpc: (name) => `import { defineQuery } from '../hatk.generated.ts'
102
129
 
103
130
  export default defineQuery('${name}', async (ctx) => {
104
131
  const { ok, db, params, packCursor, unpackCursor } = ctx
@@ -175,16 +202,20 @@ export default {
175
202
  // Periodic task logic here
176
203
  },
177
204
  }
205
+ `,
206
+ hook: (name) => `import { defineHook } from '../hatk.generated.ts'
207
+
208
+ export default defineHook('${name}', async (ctx) => {
209
+ // Hook logic here
210
+ })
211
+ `,
212
+ setup: (_name) => `import { defineSetup } from '../hatk.generated.ts'
213
+
214
+ export default defineSetup(async (ctx) => {
215
+ // Setup logic here — runs before the server starts
216
+ })
178
217
  `,
179
218
  };
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
219
  const testTemplates = {
189
220
  feed: (name) => `import { describe, test, expect, beforeAll, afterAll } from 'vitest'
190
221
  import { createTestContext } from '@hatk/hatk/test'
@@ -293,17 +324,19 @@ const lexiconTemplates = {
293
324
  }),
294
325
  };
295
326
  const dirs = {
296
- feed: 'feeds',
297
- xrpc: 'xrpc',
298
- label: 'labels',
299
- og: 'og',
300
- job: 'jobs',
327
+ feed: 'server',
328
+ xrpc: 'server',
329
+ label: 'server',
330
+ og: 'server',
331
+ job: 'server',
332
+ hook: 'server',
333
+ setup: 'server',
301
334
  };
302
335
  // --- Commands ---
303
336
  if (command === 'new') {
304
337
  const name = args[1];
305
338
  if (!name) {
306
- console.error('Usage: hatk new <name> [--svelte] [--template <template-name>]');
339
+ console.error('Usage: hatk new <name> [--svelte] [--duckdb] [--template <template-name>]');
307
340
  process.exit(1);
308
341
  }
309
342
  const templateIdx = args.indexOf('--template');
@@ -341,20 +374,16 @@ if (command === 'new') {
341
374
  process.exit(0);
342
375
  }
343
376
  const withSvelte = args.includes('--svelte');
377
+ const withDuckdb = args.includes('--duckdb');
378
+ const dbEngine = withDuckdb ? 'duckdb' : 'sqlite';
344
379
  mkdirSync(dir);
345
380
  const subs = [
346
381
  'lexicons',
347
- 'feeds',
348
- 'xrpc',
349
- 'og',
350
- 'labels',
351
- 'jobs',
382
+ 'server',
352
383
  'seeds',
353
- 'setup',
354
384
  'public',
355
385
  'test',
356
- 'test/feeds',
357
- 'test/xrpc',
386
+ 'test/server',
358
387
  'test/integration',
359
388
  'test/browser',
360
389
  'test/fixtures',
@@ -364,14 +393,19 @@ if (command === 'new') {
364
393
  for (const sub of subs) {
365
394
  mkdirSync(join(dir, sub));
366
395
  }
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: []
396
+ writeFileSync(join(dir, 'hatk.config.ts'), `import { defineConfig } from '@hatk/hatk/config'
372
397
 
373
- backfill:
374
- parallelism: 10
398
+ export default defineConfig({
399
+ relay: 'ws://localhost:2583',
400
+ plc: 'http://localhost:2582',
401
+ port: 3000,
402
+ databaseEngine: '${dbEngine}',
403
+ database: 'data/hatk.db',
404
+ admins: [],
405
+ backfill: {
406
+ parallelism: 10,
407
+ },
408
+ })
375
409
  `);
376
410
  writeFileSync(join(dir, 'public', 'index.html'), `<!DOCTYPE html>
377
411
  <html><head><title>${name}</title></head>
@@ -507,6 +541,14 @@ backfill:
507
541
  properties: {
508
542
  uri: { type: 'string', format: 'at-uri' },
509
543
  cid: { type: 'string', format: 'cid' },
544
+ commit: {
545
+ type: 'object',
546
+ properties: {
547
+ cid: { type: 'string', format: 'cid' },
548
+ rev: { type: 'string' },
549
+ },
550
+ },
551
+ validationStatus: { type: 'string' },
510
552
  },
511
553
  },
512
554
  },
@@ -562,6 +604,14 @@ backfill:
562
604
  properties: {
563
605
  uri: { type: 'string', format: 'at-uri' },
564
606
  cid: { type: 'string', format: 'cid' },
607
+ commit: {
608
+ type: 'object',
609
+ properties: {
610
+ cid: { type: 'string', format: 'cid' },
611
+ rev: { type: 'string' },
612
+ },
613
+ },
614
+ validationStatus: { type: 'string' },
565
615
  },
566
616
  },
567
617
  },
@@ -591,6 +641,53 @@ backfill:
591
641
  },
592
642
  },
593
643
  }, null, 2) + '\n');
644
+ writeFileSync(join(coreLexDir, 'getPreferences.json'), JSON.stringify({
645
+ lexicon: 1,
646
+ id: 'dev.hatk.getPreferences',
647
+ defs: {
648
+ main: {
649
+ type: 'query',
650
+ description: 'Get all preferences for the authenticated user.',
651
+ output: {
652
+ encoding: 'application/json',
653
+ schema: {
654
+ type: 'object',
655
+ properties: {
656
+ preferences: { type: 'unknown' },
657
+ },
658
+ },
659
+ },
660
+ },
661
+ },
662
+ }, null, 2) + '\n');
663
+ writeFileSync(join(coreLexDir, 'putPreference.json'), JSON.stringify({
664
+ lexicon: 1,
665
+ id: 'dev.hatk.putPreference',
666
+ defs: {
667
+ main: {
668
+ type: 'procedure',
669
+ description: 'Set a single preference by key.',
670
+ input: {
671
+ encoding: 'application/json',
672
+ schema: {
673
+ type: 'object',
674
+ required: ['key', 'value'],
675
+ properties: {
676
+ key: { type: 'string' },
677
+ value: { type: 'unknown' },
678
+ },
679
+ },
680
+ },
681
+ output: {
682
+ encoding: 'application/json',
683
+ schema: {
684
+ type: 'object',
685
+ properties: {},
686
+ },
687
+ },
688
+ },
689
+ },
690
+ }, null, 2) + '\n');
594
691
  writeFileSync(join(coreLexDir, 'getFeed.json'), JSON.stringify({
595
692
  lexicon: 1,
596
693
  id: 'dev.hatk.getFeed',
@@ -611,6 +708,7 @@ backfill:
611
708
  encoding: 'application/json',
612
709
  schema: {
613
710
  type: 'object',
711
+ required: ['items'],
614
712
  properties: {
615
713
  items: { type: 'array', items: { type: 'unknown' } },
616
714
  cursor: { type: 'string' },
@@ -668,6 +766,7 @@ backfill:
668
766
  encoding: 'application/json',
669
767
  schema: {
670
768
  type: 'object',
769
+ required: ['items'],
671
770
  properties: {
672
771
  items: { type: 'array', items: { type: 'unknown' } },
673
772
  cursor: { type: 'string' },
@@ -699,6 +798,7 @@ backfill:
699
798
  encoding: 'application/json',
700
799
  schema: {
701
800
  type: 'object',
801
+ required: ['items'],
702
802
  properties: {
703
803
  items: { type: 'array', items: { type: 'unknown' } },
704
804
  cursor: { type: 'string' },
@@ -793,13 +893,17 @@ public
793
893
  writeFileSync(join(dir, 'Dockerfile'), `FROM node:25-slim
794
894
  WORKDIR /app
795
895
  COPY package.json package-lock.json ./
796
- RUN npm ci --omit=dev
896
+ RUN npm ci
797
897
  COPY . .
798
898
  RUN node_modules/.bin/hatk build
899
+ RUN npm prune --omit=dev
799
900
  EXPOSE 3000
800
- CMD ["node", "node_modules/@hatk/hatk/dist/main.js", "config.yaml"]
901
+ CMD ["node", "--experimental-strip-types", "--max-old-space-size=512", "node_modules/@hatk/hatk/dist/main.js", "hatk.config.ts"]
801
902
  `);
802
903
  const pkgDeps = { '@hatk/oauth-client': '*', hatk: '*' };
904
+ if (!withDuckdb) {
905
+ pkgDeps['better-sqlite3'] = '^11';
906
+ }
803
907
  const pkgDevDeps = {
804
908
  '@playwright/test': '^1',
805
909
  oxfmt: '^0.35.0',
@@ -807,6 +911,7 @@ CMD ["node", "node_modules/@hatk/hatk/dist/main.js", "config.yaml"]
807
911
  typescript: '^5',
808
912
  vite: '^6',
809
913
  vitest: '^4',
914
+ '@types/node': '^22',
810
915
  };
811
916
  if (withSvelte) {
812
917
  pkgDevDeps['@sveltejs/adapter-static'] = '^3';
@@ -841,7 +946,7 @@ CMD ["node", "node_modules/@hatk/hatk/dist/main.js", "config.yaml"]
841
946
  allowImportingTsExtensions: true,
842
947
  resolveJsonModule: true,
843
948
  },
844
- include: ['feeds', 'xrpc', 'og', 'seeds', 'labels', 'jobs', 'setup', 'hatk.generated.ts'],
949
+ include: ['server', 'seeds', 'hatk.generated.ts', 'hatk.config.ts'],
845
950
  }, null, 2) + '\n');
846
951
  writeFileSync(join(dir, 'playwright.config.ts'), `import { defineConfig } from '@playwright/test'
847
952
 
@@ -1026,16 +1131,42 @@ a {
1026
1131
  </div>
1027
1132
  `);
1028
1133
  }
1134
+ writeFileSync(join(dir, 'AGENTS.md'), `# hatk project
1135
+
1136
+ This is an AT Protocol application built with [hatk](https://github.com/hatk-dev/hatk).
1137
+ Read the project's lexicons in \`lexicons/\` to understand the data model.
1138
+ Types are generated from lexicons into \`hatk.generated.ts\` — never edit this file directly.
1139
+
1140
+ ## Project structure
1141
+
1142
+ | Directory | Purpose |
1143
+ |-------------|------------------------------------------------------|
1144
+ | \`lexicons/\` | AT Protocol lexicon schemas (JSON). Defines collections and XRPC methods |
1145
+ | \`server/\` | All server-side code: feeds, XRPC handlers, hooks, labels, OG routes, jobs, setup scripts |
1146
+ | \`seeds/\` | Test data seeding scripts for local development |
1147
+ | \`test/\` | Test files (vitest). Run with \`hatk test\` |
1148
+ | \`public/\` | Static files served at the root |
1149
+
1150
+ ## Key files
1151
+
1152
+ - \`hatk.config.ts\` — project configuration (see \`defineConfig\` for type info)
1153
+ - \`hatk.generated.ts\` — auto-generated types and typed helpers. Regenerate with \`hatk generate types\`
1154
+
1155
+ ## Commands
1156
+
1157
+ Run \`npx hatk --help\` for the full list of commands.
1158
+
1159
+ Use \`npx hatk generate\` to scaffold new feeds, xrpc handlers, labels, and lexicons
1160
+ rather than creating files manually. These generate files with the correct imports
1161
+ from \`hatk.generated.ts\`.
1162
+
1163
+ After modifying lexicons, always run \`npx hatk generate types\` to update the generated types.
1164
+ `);
1029
1165
  console.log(`Created ${name}/`);
1030
- console.log(` config.yaml`);
1166
+ console.log(` hatk.config.ts`);
1031
1167
  console.log(` lexicons/ — lexicon JSON files (core + your own)`);
1032
- console.log(` feeds/ feed generators`);
1033
- console.log(` xrpc/ — XRPC method handlers`);
1034
- console.log(` og/ — OpenGraph image routes`);
1035
- console.log(` labels/ — label definitions + rules`);
1036
- console.log(` jobs/ — periodic tasks`);
1168
+ console.log(` server/ feeds, XRPC handlers, hooks, labels, OG routes, jobs, setup`);
1037
1169
  console.log(` seeds/ — seed fixture data (hatk seed)`);
1038
- console.log(` setup/ — boot-time setup scripts (run before server starts)`);
1039
1170
  console.log(` test/ — test files (hatk test)`);
1040
1171
  console.log(` public/ — static files`);
1041
1172
  console.log(` docker-compose.yml — local PDS for development`);
@@ -1074,6 +1205,21 @@ else if (command === 'generate') {
1074
1205
  }
1075
1206
  }
1076
1207
  entries.sort((a, b) => a.nsid.localeCompare(b.nsid));
1208
+ // Collect procedure nsids and blob-input nsids for client callXrpc
1209
+ const procedureNsids = [];
1210
+ const blobInputNsids = [];
1211
+ for (const { nsid, defType } of entries) {
1212
+ if (defType === 'procedure') {
1213
+ const lex = lexicons.get(nsid);
1214
+ const inputEncoding = lex?.defs?.main?.input?.encoding;
1215
+ if (inputEncoding === '*/*') {
1216
+ blobInputNsids.push(nsid);
1217
+ }
1218
+ else {
1219
+ procedureNsids.push(nsid);
1220
+ }
1221
+ }
1222
+ }
1077
1223
  if (entries.length === 0) {
1078
1224
  console.error('No lexicons found');
1079
1225
  process.exit(1);
@@ -1121,6 +1267,7 @@ else if (command === 'generate') {
1121
1267
  let out = '// Auto-generated from lexicons. Do not edit.\n';
1122
1268
  out += `import type { ${[...usedWrappers].sort().join(', ')}, LexServerParams, Checked, Prettify, StrictArg } from '@hatk/hatk/lex-types'\n`;
1123
1269
  out += `import type { XrpcContext } from '@hatk/hatk/xrpc'\n`;
1270
+ out += `import { callXrpc as _callXrpc } from '@hatk/hatk/xrpc'\n`;
1124
1271
  out += `import { defineFeed as _defineFeed, type FeedResult, type FeedContext, type HydrateContext } from '@hatk/hatk/feeds'\n`;
1125
1272
  out += `import { seed as _seed, type SeedOpts } from '@hatk/hatk/seed'\n`;
1126
1273
  // Emit ALL lexicons as `const ... = {...} as const` (including defs-only)
@@ -1279,6 +1426,11 @@ else if (command === 'generate') {
1279
1426
  out += `\n// ─── XRPC Helpers ───────────────────────────────────────────────────\n\n`;
1280
1427
  out += `export type { HydrateContext } from '@hatk/hatk/feeds'\n`;
1281
1428
  out += `export { InvalidRequestError, NotFoundError } from '@hatk/hatk/xrpc'\n`;
1429
+ out += `export { defineSetup } from '@hatk/hatk/setup'\n`;
1430
+ out += `export { defineHook } from '@hatk/hatk/hooks'\n`;
1431
+ out += `export { defineLabels } from '@hatk/hatk/labels'\n`;
1432
+ out += `export { defineOG } from '@hatk/hatk/opengraph'\n`;
1433
+ out += `export { defineRenderer } from '@hatk/hatk/renderer'\n`;
1282
1434
  out += `export type Ctx<K extends keyof XrpcSchema & keyof Registry> = XrpcContext<\n`;
1283
1435
  out += ` LexServerParams<Registry[K], Registry>,\n`;
1284
1436
  out += ` RecordRegistry,\n`;
@@ -1291,13 +1443,21 @@ else if (command === 'generate') {
1291
1443
  out += ` nsid: K,\n`;
1292
1444
  out += ` handler: (ctx: Ctx<K> & { ok: <T extends OutputOf<K>>(value: StrictArg<T, OutputOf<K>>) => Checked<OutputOf<K>> }) => Promise<Checked<OutputOf<K>>>,\n`;
1293
1445
  out += `) {\n`;
1294
- out += ` return { handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
1446
+ out += ` return { __type: 'query' as const, nsid, handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
1295
1447
  out += `}\n\n`;
1296
1448
  out += `export function defineProcedure<K extends keyof XrpcSchema & string>(\n`;
1297
1449
  out += ` nsid: K,\n`;
1298
1450
  out += ` handler: (ctx: Ctx<K> & { ok: <T extends OutputOf<K>>(value: StrictArg<T, OutputOf<K>>) => Checked<OutputOf<K>> }) => Promise<Checked<OutputOf<K>>>,\n`;
1299
1451
  out += `) {\n`;
1300
- out += ` return { handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
1452
+ out += ` return { __type: 'procedure' as const, nsid, handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
1453
+ out += `}\n\n`;
1454
+ out += `// ─── Server-side XRPC Caller ────────────────────────────────────────\n\n`;
1455
+ out += `type ExtractParams<T> = T extends { params: infer P } ? P : Record<string, unknown>\n`;
1456
+ out += `export async function callXrpc<K extends keyof XrpcSchema & string>(\n`;
1457
+ out += ` nsid: K,\n`;
1458
+ out += ` params?: ExtractParams<XrpcSchema[K]>,\n`;
1459
+ out += `): Promise<OutputOf<K>> {\n`;
1460
+ out += ` return _callXrpc(nsid, params as any) as Promise<OutputOf<K>>\n`;
1301
1461
  out += `}\n\n`;
1302
1462
  out += `// ─── Feed & Seed Helpers ────────────────────────────────────────────\n\n`;
1303
1463
  out += `type FeedGenerate = (ctx: FeedContext & { ok: (value: FeedResult) => Checked<FeedResult> }) => Promise<Checked<FeedResult>>\n`;
@@ -1328,7 +1488,125 @@ else if (command === 'generate') {
1328
1488
  out = out.replace(/import type \{ ([^}]+) \} from '@hatk\/hatk\/lex-types'/, `import type { ${[...usedWrappers].sort().join(', ')}, LexServerParams, Checked, Prettify, StrictArg } from '@hatk/hatk/lex-types'`);
1329
1489
  }
1330
1490
  writeFileSync(outPath, out);
1491
+ // Generate client-safe version (types + callXrpc only, no server module re-exports)
1492
+ // Types use `export type` from main file (erased at compile time, no runtime import).
1493
+ // callXrpc imports from @hatk/hatk/xrpc directly to avoid pulling in server deps.
1494
+ let clientOut = '// Auto-generated client-safe subset. Do not edit.\n';
1495
+ clientOut += `// Import this in app components instead of hatk.generated.ts\n`;
1496
+ clientOut += `// to avoid pulling in server-only dependencies.\n`;
1497
+ clientOut += `export type { XrpcSchema } from './hatk.generated.ts'\n`;
1498
+ clientOut += `import type { XrpcSchema } from './hatk.generated.ts'\n`;
1499
+ // Re-export all types
1500
+ const typeExports = [];
1501
+ for (const { nsid, defType } of entries) {
1502
+ if (!defType)
1503
+ continue;
1504
+ if (nsid === 'dev.hatk.createRecord' || nsid === 'dev.hatk.deleteRecord' || nsid === 'dev.hatk.putRecord')
1505
+ continue;
1506
+ typeExports.push(capitalize(varNames.get(nsid)));
1507
+ }
1508
+ if (recordEntries.length > 0) {
1509
+ typeExports.push('RecordRegistry', 'CreateRecord', 'DeleteRecord', 'PutRecord');
1510
+ }
1511
+ // Named defs (views, objects) — collect from emittedDefNames minus main types
1512
+ const mainTypeNames = new Set(entries.filter(e => e.defType).map(e => capitalize(varNames.get(e.nsid))));
1513
+ for (const name of emittedDefNames) {
1514
+ if (!mainTypeNames.has(name) && !typeExports.includes(name)) {
1515
+ typeExports.push(name);
1516
+ }
1517
+ }
1518
+ if (typeExports.length > 0) {
1519
+ clientOut += `export type { ${typeExports.join(', ')} } from './hatk.generated.ts'\n`;
1520
+ }
1521
+ // Typed callXrpc — environment-aware:
1522
+ // SSR: uses globalThis.__hatk_callXrpc bridge (direct handler invocation)
1523
+ // Client: fetches via HTTP (GET for queries, POST for procedures, raw POST for blobs)
1524
+ if (procedureNsids.length > 0) {
1525
+ clientOut += `\nconst _procedures = new Set([${procedureNsids.map(n => `'${n}'`).join(', ')}])\n`;
1526
+ }
1527
+ if (blobInputNsids.length > 0) {
1528
+ clientOut += `const _blobInputs = new Set([${blobInputNsids.map(n => `'${n}'`).join(', ')}])\n`;
1529
+ }
1530
+ clientOut += `\ntype CallArg<K extends keyof XrpcSchema> =\n`;
1531
+ clientOut += ` XrpcSchema[K] extends { input: infer I } ? I :\n`;
1532
+ clientOut += ` XrpcSchema[K] extends { params: infer P } ? P :\n`;
1533
+ clientOut += ` Record<string, unknown>\n`;
1534
+ clientOut += `type OutputOf<K extends keyof XrpcSchema> = XrpcSchema[K]['output']\n\n`;
1535
+ clientOut += `export async function callXrpc<K extends keyof XrpcSchema & string>(\n`;
1536
+ clientOut += ` nsid: K,\n`;
1537
+ clientOut += ` arg?: CallArg<K>,\n`;
1538
+ clientOut += `): Promise<OutputOf<K>> {\n`;
1539
+ // Server-side bridge
1540
+ clientOut += ` if (typeof window === 'undefined') {\n`;
1541
+ clientOut += ` const bridge = (globalThis as any).__hatk_callXrpc\n`;
1542
+ clientOut += ` if (!bridge) throw new Error('callXrpc: server bridge not available — is hatk initialized?')\n`;
1543
+ if (procedureNsids.length > 0 || blobInputNsids.length > 0) {
1544
+ const checks = [];
1545
+ if (procedureNsids.length > 0)
1546
+ checks.push('_procedures.has(nsid)');
1547
+ if (blobInputNsids.length > 0)
1548
+ checks.push('_blobInputs.has(nsid)');
1549
+ clientOut += ` if (${checks.join(' || ')}) return bridge(nsid, {}, arg) as Promise<OutputOf<K>>\n`;
1550
+ }
1551
+ clientOut += ` return bridge(nsid, arg) as Promise<OutputOf<K>>\n`;
1552
+ clientOut += ` }\n`;
1553
+ // Client-side fetch
1554
+ clientOut += ` const url = new URL(\`/xrpc/\${nsid}\`, window.location.origin)\n`;
1555
+ if (blobInputNsids.length > 0) {
1556
+ clientOut += ` if (_blobInputs.has(nsid)) {\n`;
1557
+ clientOut += ` const blob = arg as Blob | ArrayBuffer\n`;
1558
+ clientOut += ` const ct = blob instanceof Blob ? blob.type : 'application/octet-stream'\n`;
1559
+ clientOut += ` const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': ct }, body: blob })\n`;
1560
+ clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n`;
1561
+ clientOut += ` return res.json() as Promise<OutputOf<K>>\n`;
1562
+ clientOut += ` }\n`;
1563
+ }
1564
+ if (procedureNsids.length > 0) {
1565
+ clientOut += ` if (_procedures.has(nsid)) {\n`;
1566
+ clientOut += ` const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(arg) })\n`;
1567
+ clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n`;
1568
+ clientOut += ` return res.json() as Promise<OutputOf<K>>\n`;
1569
+ clientOut += ` }\n`;
1570
+ }
1571
+ clientOut += ` for (const [k, v] of Object.entries(arg || {})) {\n`;
1572
+ clientOut += ` if (v != null) url.searchParams.set(k, String(v))\n`;
1573
+ clientOut += ` }\n`;
1574
+ clientOut += ` const res = await fetch(url)\n`;
1575
+ clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n`;
1576
+ clientOut += ` return res.json() as Promise<OutputOf<K>>\n`;
1577
+ clientOut += `}\n`;
1578
+ // getViewer — async, resolves from cookies on server via getRequestEvent()
1579
+ clientOut += `\nexport async function getViewer(): Promise<{ did: string } | null> {\n`;
1580
+ clientOut += ` if (typeof window === 'undefined') {\n`;
1581
+ clientOut += ` try {\n`;
1582
+ clientOut += ` const parse = (globalThis as any).__hatk_parseSessionCookie\n`;
1583
+ clientOut += ` if (parse) {\n`;
1584
+ clientOut += ` const { getRequestEvent } = await import('$app/server')\n`;
1585
+ clientOut += ` const event = getRequestEvent()\n`;
1586
+ clientOut += ` const cookieName = (globalThis as any).__hatk_sessionCookieName ?? '__hatk_session'\n`;
1587
+ clientOut += ` const cookieValue = event.cookies.get(cookieName)\n`;
1588
+ clientOut += ` if (cookieValue) {\n`;
1589
+ clientOut += ` const request = new Request('http://localhost', {\n`;
1590
+ clientOut += ` headers: { cookie: \`\${cookieName}=\${cookieValue}\` },\n`;
1591
+ clientOut += ` })\n`;
1592
+ clientOut += ` return parse(request)\n`;
1593
+ clientOut += ` }\n`;
1594
+ clientOut += ` }\n`;
1595
+ clientOut += ` } catch {}\n`;
1596
+ clientOut += ` return (globalThis as any).__hatk_viewer ?? null\n`;
1597
+ clientOut += ` }\n`;
1598
+ clientOut += ` try {\n`;
1599
+ clientOut += ` const mod = (globalThis as any).__hatk_auth\n`;
1600
+ clientOut += ` if (mod?.viewerDid) {\n`;
1601
+ clientOut += ` const did = mod.viewerDid()\n`;
1602
+ clientOut += ` if (did) return { did }\n`;
1603
+ clientOut += ` }\n`;
1604
+ clientOut += ` } catch {}\n`;
1605
+ clientOut += ` return (globalThis as any).__hatk_viewer ?? null\n`;
1606
+ clientOut += `}\n`;
1607
+ writeFileSync('./hatk.generated.client.ts', clientOut);
1331
1608
  console.log(`Generated ${outPath} with ${entries.length} types: ${entries.map((e) => capitalize(varNames.get(e.nsid))).join(', ')}`);
1609
+ console.log(`Generated ./hatk.generated.client.ts (client-safe subset)`);
1332
1610
  }
1333
1611
  else if (lexiconTemplates[type]) {
1334
1612
  const nsid = args[2];
@@ -1356,18 +1634,9 @@ else if (command === 'generate') {
1356
1634
  process.exit(1);
1357
1635
  }
1358
1636
  const baseDir = dirs[type];
1359
- let filePath;
1360
- if (type === 'xrpc') {
1361
- // NSID folder path: fm.teal.getStats → xrpc/fm/teal/getStats.ts
1362
- const parts = name.split('.');
1363
- const subDir = join(baseDir, ...parts.slice(0, -1));
1364
- mkdirSync(subDir, { recursive: true });
1365
- filePath = join(subDir, `${parts[parts.length - 1]}.ts`);
1366
- }
1367
- else {
1368
- mkdirSync(baseDir, { recursive: true });
1369
- filePath = join(baseDir, `${name}.ts`);
1370
- }
1637
+ mkdirSync(baseDir, { recursive: true });
1638
+ const fileName = type === 'xrpc' ? name.split('.').pop() : name;
1639
+ const filePath = join(baseDir, `${fileName}.ts`);
1371
1640
  if (existsSync(filePath)) {
1372
1641
  console.error(`${filePath} already exists`);
1373
1642
  process.exit(1);
@@ -1377,7 +1646,7 @@ else if (command === 'generate') {
1377
1646
  // Scaffold test file if template exists
1378
1647
  const testTemplate = testTemplates[type];
1379
1648
  if (testTemplate) {
1380
- const testDir = type === 'xrpc' ? 'test/xrpc' : `test/${baseDir}`;
1649
+ const testDir = 'test/server';
1381
1650
  mkdirSync(testDir, { recursive: true });
1382
1651
  const testName = type === 'xrpc' ? name.split('.').pop() : name;
1383
1652
  const testPath = join(testDir, `${testName}.test.ts`);
@@ -1396,18 +1665,9 @@ else if (command === 'destroy') {
1396
1665
  process.exit(1);
1397
1666
  }
1398
1667
  const baseDir = dirs[type];
1399
- let tsPath, jsPath;
1400
- if (type === 'xrpc') {
1401
- const parts = name.split('.');
1402
- const leaf = parts[parts.length - 1];
1403
- const subDir = join(baseDir, ...parts.slice(0, -1));
1404
- tsPath = join(subDir, `${leaf}.ts`);
1405
- jsPath = join(subDir, `${leaf}.js`);
1406
- }
1407
- else {
1408
- tsPath = join(baseDir, `${name}.ts`);
1409
- jsPath = join(baseDir, `${name}.js`);
1410
- }
1668
+ const fileName = type === 'xrpc' ? name.split('.').pop() : name;
1669
+ const tsPath = join(baseDir, `${fileName}.ts`);
1670
+ const jsPath = join(baseDir, `${fileName}.js`);
1411
1671
  const filePath = existsSync(tsPath) ? tsPath : existsSync(jsPath) ? jsPath : null;
1412
1672
  if (!filePath) {
1413
1673
  console.error(`No file found for ${type} "${name}"`);
@@ -1416,7 +1676,7 @@ else if (command === 'destroy') {
1416
1676
  unlinkSync(filePath);
1417
1677
  console.log(`Removed ${filePath}`);
1418
1678
  // Clean up test file
1419
- const testDir = type === 'xrpc' ? 'test/xrpc' : `test/${baseDir}`;
1679
+ const testDir = 'test/server';
1420
1680
  const testName = type === 'xrpc' ? name.split('.').pop() : name;
1421
1681
  const testFile = join(testDir, `${testName}.test.ts`);
1422
1682
  if (existsSync(testFile)) {
@@ -1430,25 +1690,14 @@ else if (command === 'destroy') {
1430
1690
  else if (command === 'dev') {
1431
1691
  await ensurePds();
1432
1692
  runSeed();
1433
- try {
1434
- if (existsSync(resolve('svelte.config.js')) && existsSync(resolve('src/app.html'))) {
1435
- // SvelteKit project — vite dev starts the hatk server via the plugin
1436
- execSync('npx vite dev', { stdio: 'inherit', cwd: process.cwd() });
1437
- }
1438
- else {
1439
- // No frontend — just run the hatk server directly
1440
- const mainPath = resolve(import.meta.dirname, 'main.js');
1441
- execSync(`npx tsx ${mainPath} config.yaml`, {
1442
- stdio: 'inherit',
1443
- cwd: process.cwd(),
1444
- env: { ...process.env, DEV_MODE: '1' },
1445
- });
1446
- }
1693
+ if (existsSync(resolve('vite.config.ts')) || existsSync(resolve('vite.config.js'))) {
1694
+ // Vite project vite dev starts the hatk server via the plugin
1695
+ await spawnForward('npx', ['vite', 'dev']);
1447
1696
  }
1448
- catch (e) {
1449
- if (e.signal === 'SIGINT' || e.signal === 'SIGTERM')
1450
- process.exit(0);
1451
- throw e;
1697
+ else {
1698
+ // No frontend just run the hatk server directly
1699
+ const mainPath = resolve(import.meta.dirname, 'main.js');
1700
+ await spawnForward('npx', ['tsx', mainPath, 'hatk.config.ts'], { DEV_MODE: '1' });
1452
1701
  }
1453
1702
  }
1454
1703
  else if (command === 'format' || command === 'fmt') {
@@ -1468,9 +1717,9 @@ else if (command === 'build') {
1468
1717
  }
1469
1718
  }
1470
1719
  else if (command === 'reset') {
1471
- const config = loadConfig(resolve('config.yaml'));
1720
+ const config = await loadConfig(resolve('hatk.config.ts'));
1472
1721
  if (config.database !== ':memory:') {
1473
- for (const suffix of ['', '.wal']) {
1722
+ for (const suffix of ['', '.wal', '-shm', '-wal']) {
1474
1723
  const file = config.database + suffix;
1475
1724
  if (existsSync(file)) {
1476
1725
  unlinkSync(file);
@@ -1628,40 +1877,24 @@ else if (command === 'resolve') {
1628
1877
  execSync('npx hatk generate types', { stdio: 'inherit', cwd: process.cwd() });
1629
1878
  }
1630
1879
  else if (command === 'schema') {
1631
- const config = loadConfig(resolve('config.yaml'));
1632
- if (config.database === ':memory:') {
1633
- console.error('No database file configured (database is :memory:)');
1634
- process.exit(1);
1635
- }
1636
- if (!existsSync(config.database)) {
1637
- console.error(`Database not found: ${config.database}`);
1638
- console.error('Run "hatk dev" first to create it.');
1639
- process.exit(1);
1640
- }
1641
- const { DuckDBInstance } = await import('@duckdb/node-api');
1642
- const instance = await DuckDBInstance.create(config.database);
1643
- const con = await instance.connect();
1644
- const tables = (await (await con.runAndReadAll(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name`)).getRowObjects());
1645
- for (const { table_name } of tables) {
1646
- console.log(`"${table_name}"`);
1647
- 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());
1648
- for (const col of cols) {
1649
- const nullable = col.is_nullable === 'YES' ? '' : ' NOT NULL';
1650
- console.log(` ${col.column_name.padEnd(20)} ${col.data_type}${nullable}`);
1651
- }
1652
- console.log();
1880
+ const config = await loadConfig(resolve('hatk.config.ts'));
1881
+ const { initDatabase, getSchemaDump } = await import("./database/db.js");
1882
+ const { createAdapter } = await import("./database/adapter-factory.js");
1883
+ const { getDialect } = await import("./database/dialect.js");
1884
+ const configDir2 = resolve('.');
1885
+ const lexicons2 = loadLexicons(resolve(configDir2, 'lexicons'));
1886
+ const collections2 = config.collections.length > 0 ? config.collections : discoverCollections(lexicons2);
1887
+ const { schemas: schemas2, ddlStatements: ddl2 } = buildSchemas(lexicons2, collections2, getDialect(config.databaseEngine));
1888
+ if (config.database !== ':memory:') {
1889
+ mkdirSync(dirname(config.database), { recursive: true });
1653
1890
  }
1891
+ const { adapter: adapter2 } = await createAdapter(config.databaseEngine);
1892
+ await initDatabase(adapter2, config.database, schemas2, ddl2);
1893
+ console.log(await getSchemaDump());
1654
1894
  }
1655
1895
  else if (command === 'start') {
1656
- try {
1657
- const mainPath = resolve(import.meta.dirname, 'main.js');
1658
- execSync(`npx tsx ${mainPath} config.yaml`, { stdio: 'inherit', cwd: process.cwd() });
1659
- }
1660
- catch (e) {
1661
- if (e.signal === 'SIGINT' || e.signal === 'SIGTERM')
1662
- process.exit(0);
1663
- throw e;
1664
- }
1896
+ const mainPath = resolve(import.meta.dirname, 'main.js');
1897
+ await spawnForward('npx', ['tsx', mainPath, 'hatk.config.ts']);
1665
1898
  }
1666
1899
  else {
1667
1900
  usage();