@hatk/hatk 0.0.1-alpha.1 → 0.0.1-alpha.100

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 (226) hide show
  1. package/dist/adapter.d.ts +19 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/adapter.js +113 -0
  4. package/dist/backfill.d.ts +90 -1
  5. package/dist/backfill.d.ts.map +1 -1
  6. package/dist/backfill.js +254 -55
  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 +247 -986
  17. package/dist/config.d.ts +145 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +62 -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 +16 -0
  30. package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
  31. package/dist/database/adapters/sqlite-search.js +76 -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 +88 -0
  35. package/dist/{db.d.ts → database/db.d.ts} +94 -6
  36. package/dist/database/db.d.ts.map +1 -0
  37. package/dist/database/db.js +1833 -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/{fts.d.ts → database/fts.d.ts} +7 -0
  42. package/dist/database/fts.d.ts.map +1 -0
  43. package/dist/{fts.js → database/fts.js} +122 -34
  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 +50 -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 +91 -0
  51. package/dist/database/schema.d.ts.map +1 -0
  52. package/dist/{schema.js → database/schema.js} +153 -54
  53. package/dist/dev-entry.d.ts +8 -0
  54. package/dist/dev-entry.d.ts.map +1 -0
  55. package/dist/dev-entry.js +126 -0
  56. package/dist/feeds.d.ts +26 -8
  57. package/dist/feeds.d.ts.map +1 -1
  58. package/dist/feeds.js +72 -16
  59. package/dist/hooks.d.ts +102 -0
  60. package/dist/hooks.d.ts.map +1 -0
  61. package/dist/hooks.js +186 -0
  62. package/dist/hydrate.d.ts +55 -6
  63. package/dist/hydrate.d.ts.map +1 -1
  64. package/dist/hydrate.js +21 -20
  65. package/dist/indexer.d.ts +195 -3
  66. package/dist/indexer.d.ts.map +1 -1
  67. package/dist/indexer.js +634 -94
  68. package/dist/jetstream.d.ts +118 -0
  69. package/dist/jetstream.d.ts.map +1 -0
  70. package/dist/jetstream.js +238 -0
  71. package/dist/labels.d.ts +36 -0
  72. package/dist/labels.d.ts.map +1 -1
  73. package/dist/labels.js +71 -6
  74. package/dist/lex-types.d.ts +2 -0
  75. package/dist/lex-types.d.ts.map +1 -1
  76. package/dist/lexicon-resolve.d.ts.map +1 -1
  77. package/dist/lexicon-resolve.js +27 -112
  78. package/dist/lexicons/com/atproto/label/defs.json +75 -0
  79. package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
  80. package/dist/lexicons/com/atproto/repo/strongRef.json +21 -0
  81. package/dist/lexicons/dev/hatk/applyWrites.json +87 -0
  82. package/dist/lexicons/dev/hatk/createRecord.json +40 -0
  83. package/dist/lexicons/dev/hatk/createReport.json +48 -0
  84. package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
  85. package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
  86. package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
  87. package/dist/lexicons/dev/hatk/describeLabels.json +45 -0
  88. package/dist/lexicons/dev/hatk/getFeed.json +30 -0
  89. package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
  90. package/dist/lexicons/dev/hatk/getRecord.json +26 -0
  91. package/dist/lexicons/dev/hatk/getRecords.json +32 -0
  92. package/dist/lexicons/dev/hatk/putPreference.json +28 -0
  93. package/dist/lexicons/dev/hatk/putRecord.json +41 -0
  94. package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
  95. package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
  96. package/dist/logger.d.ts +29 -0
  97. package/dist/logger.d.ts.map +1 -1
  98. package/dist/logger.js +29 -0
  99. package/dist/main.js +197 -75
  100. package/dist/mst.d.ts +18 -1
  101. package/dist/mst.d.ts.map +1 -1
  102. package/dist/mst.js +19 -8
  103. package/dist/oauth/db.d.ts +27 -1
  104. package/dist/oauth/db.d.ts.map +1 -1
  105. package/dist/oauth/db.js +80 -19
  106. package/dist/oauth/discovery.d.ts +10 -0
  107. package/dist/oauth/discovery.d.ts.map +1 -1
  108. package/dist/oauth/discovery.js +54 -2
  109. package/dist/oauth/server.d.ts +131 -1
  110. package/dist/oauth/server.d.ts.map +1 -1
  111. package/dist/oauth/server.js +504 -39
  112. package/dist/oauth/service-describe.d.ts +42 -0
  113. package/dist/oauth/service-describe.d.ts.map +1 -0
  114. package/dist/oauth/service-describe.js +108 -0
  115. package/dist/oauth/session.d.ts +32 -0
  116. package/dist/oauth/session.d.ts.map +1 -0
  117. package/dist/oauth/session.js +131 -0
  118. package/dist/opengraph.d.ts +10 -0
  119. package/dist/opengraph.d.ts.map +1 -1
  120. package/dist/opengraph.js +94 -42
  121. package/dist/pds-proxy.d.ts +92 -0
  122. package/dist/pds-proxy.d.ts.map +1 -0
  123. package/dist/pds-proxy.js +396 -0
  124. package/dist/private-collections.d.ts +13 -0
  125. package/dist/private-collections.d.ts.map +1 -0
  126. package/dist/private-collections.js +27 -0
  127. package/dist/push.d.ts +54 -0
  128. package/dist/push.d.ts.map +1 -0
  129. package/dist/push.js +367 -0
  130. package/dist/renderer.d.ts +27 -0
  131. package/dist/renderer.d.ts.map +1 -0
  132. package/dist/renderer.js +46 -0
  133. package/dist/resolve-hatk.d.ts +6 -0
  134. package/dist/resolve-hatk.d.ts.map +1 -0
  135. package/dist/resolve-hatk.js +20 -0
  136. package/dist/response.d.ts +16 -0
  137. package/dist/response.d.ts.map +1 -0
  138. package/dist/response.js +69 -0
  139. package/dist/scanner.d.ts +21 -0
  140. package/dist/scanner.d.ts.map +1 -0
  141. package/dist/scanner.js +88 -0
  142. package/dist/seed.d.ts +19 -0
  143. package/dist/seed.d.ts.map +1 -1
  144. package/dist/seed.js +43 -4
  145. package/dist/server-init.d.ts +8 -0
  146. package/dist/server-init.d.ts.map +1 -0
  147. package/dist/server-init.js +62 -0
  148. package/dist/server.d.ts +34 -3
  149. package/dist/server.d.ts.map +1 -1
  150. package/dist/server.js +1053 -626
  151. package/dist/setup.d.ts +28 -1
  152. package/dist/setup.d.ts.map +1 -1
  153. package/dist/setup.js +50 -3
  154. package/dist/spaces/blob.d.ts +112 -0
  155. package/dist/spaces/blob.d.ts.map +1 -0
  156. package/dist/spaces/blob.js +460 -0
  157. package/dist/spaces/credential.d.ts +62 -0
  158. package/dist/spaces/credential.d.ts.map +1 -0
  159. package/dist/spaces/credential.js +184 -0
  160. package/dist/spaces/engine.d.ts +97 -0
  161. package/dist/spaces/engine.d.ts.map +1 -0
  162. package/dist/spaces/engine.js +559 -0
  163. package/dist/spaces/guard.d.ts +41 -0
  164. package/dist/spaces/guard.d.ts.map +1 -0
  165. package/dist/spaces/guard.js +79 -0
  166. package/dist/spaces/identity.d.ts +37 -0
  167. package/dist/spaces/identity.d.ts.map +1 -0
  168. package/dist/spaces/identity.js +88 -0
  169. package/dist/spaces/index.d.ts +55 -0
  170. package/dist/spaces/index.d.ts.map +1 -0
  171. package/dist/spaces/index.js +133 -0
  172. package/dist/spaces/notify.d.ts +51 -0
  173. package/dist/spaces/notify.d.ts.map +1 -0
  174. package/dist/spaces/notify.js +121 -0
  175. package/dist/spaces/records.d.ts +37 -0
  176. package/dist/spaces/records.d.ts.map +1 -0
  177. package/dist/spaces/records.js +63 -0
  178. package/dist/spaces/store.d.ts +51 -0
  179. package/dist/spaces/store.d.ts.map +1 -0
  180. package/dist/spaces/store.js +99 -0
  181. package/dist/spaces/uri.d.ts +60 -0
  182. package/dist/spaces/uri.d.ts.map +1 -0
  183. package/dist/spaces/uri.js +97 -0
  184. package/dist/spaces/verify.d.ts +46 -0
  185. package/dist/spaces/verify.d.ts.map +1 -0
  186. package/dist/spaces/verify.js +94 -0
  187. package/dist/spaces/viewer.d.ts +51 -0
  188. package/dist/spaces/viewer.d.ts.map +1 -0
  189. package/dist/spaces/viewer.js +167 -0
  190. package/dist/spaces/visibility.d.ts +57 -0
  191. package/dist/spaces/visibility.d.ts.map +1 -0
  192. package/dist/spaces/visibility.js +81 -0
  193. package/dist/templates/feed.tpl +18 -0
  194. package/dist/templates/hook.tpl +5 -0
  195. package/dist/templates/label.tpl +15 -0
  196. package/dist/templates/og.tpl +17 -0
  197. package/dist/templates/seed.tpl +11 -0
  198. package/dist/templates/setup.tpl +5 -0
  199. package/dist/templates/test-feed.tpl +19 -0
  200. package/dist/templates/test-xrpc.tpl +19 -0
  201. package/dist/templates/xrpc.tpl +53 -0
  202. package/dist/test.d.ts +3 -1
  203. package/dist/test.d.ts.map +1 -1
  204. package/dist/test.js +60 -36
  205. package/dist/views.js +1 -1
  206. package/dist/vite-plugin.d.ts +1 -1
  207. package/dist/vite-plugin.d.ts.map +1 -1
  208. package/dist/vite-plugin.js +254 -65
  209. package/dist/xrpc.d.ts +122 -12
  210. package/dist/xrpc.d.ts.map +1 -1
  211. package/dist/xrpc.js +270 -55
  212. package/package.json +34 -10
  213. package/public/admin-auth.js +271 -306
  214. package/public/admin.html +177 -61
  215. package/public/robots.txt +2 -0
  216. package/dist/db.d.ts.map +0 -1
  217. package/dist/db.js +0 -1361
  218. package/dist/fts.d.ts.map +0 -1
  219. package/dist/oauth/hooks.d.ts +0 -10
  220. package/dist/oauth/hooks.d.ts.map +0 -1
  221. package/dist/oauth/hooks.js +0 -40
  222. package/dist/schema.d.ts +0 -51
  223. package/dist/schema.d.ts.map +0 -1
  224. package/dist/test-browser.d.ts +0 -14
  225. package/dist/test-browser.d.ts.map +0 -1
  226. package/dist/test-browser.js +0 -26
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
3
  import { resolve, join } from 'node:path';
4
- import { execSync } from 'node:child_process';
5
- import { loadLexicons } from "./schema.js";
4
+ import { execSync, spawn } from 'node:child_process';
5
+ import { loadLexicons } 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))
@@ -44,15 +69,11 @@ function usage() {
44
69
  console.log(`
45
70
  Usage: hatk <command> [options]
46
71
 
47
- Getting Started
48
- new <name> [--svelte] [--template <t>] Create a new hatk project
49
-
50
72
  Running
51
73
  start Start the hatk server
52
74
  dev Start PDS, seed, and run hatk
53
75
  seed Seed local PDS with fixture data
54
76
  reset Reset database and PDS for a clean slate
55
- schema Show database schema from lexicons
56
77
 
57
78
  Code Quality
58
79
  check Type-check and lint the project
@@ -70,7 +91,8 @@ function usage() {
70
91
  generate xrpc <nsid> Generate an XRPC handler
71
92
  generate label <name> Generate a label definition
72
93
  generate og <name> Generate an OpenGraph route
73
- generate job <name> Generate a periodic job
94
+ generate hook <name> Generate a lifecycle hook
95
+ generate setup <name> Generate a setup script
74
96
  generate types Regenerate TypeScript types from lexicons
75
97
  destroy <type> <name> Remove a generated file
76
98
 
@@ -82,151 +104,16 @@ function usage() {
82
104
  if (!command)
83
105
  usage();
84
106
  // --- Templates ---
85
- const templates = {
86
- feed: (name) => `import { defineFeed } from '../hatk.generated.ts'
87
-
88
- export default defineFeed({
89
- collection: 'your.collection.here',
90
- label: '${name.charAt(0).toUpperCase() + name.slice(1)}',
91
-
92
- async generate(ctx) {
93
- const { rows, cursor } = await ctx.paginate<{ uri: string }>(
94
- \`SELECT uri, cid, indexed_at FROM "your.collection.here"\`,
95
- )
96
-
97
- return ctx.ok({ uris: rows.map((r) => r.uri), cursor })
98
- },
99
- })
100
- `,
101
- xrpc: (name) => `import { defineQuery } from '${xrpcImportPath(name)}'
102
-
103
- export default defineQuery('${name}', async (ctx) => {
104
- const { ok, db, params, packCursor, unpackCursor } = ctx
105
- const limit = params.limit ?? 30
106
- const cursor = params.cursor
107
-
108
- const conditions: string[] = []
109
- const sqlParams: (string | number)[] = []
110
- let paramIdx = 1
111
-
112
- if (cursor) {
113
- const parsed = unpackCursor(cursor)
114
- if (parsed) {
115
- conditions.push(\`(s.indexed_at < $\${paramIdx} OR (s.indexed_at = $\${paramIdx + 1} AND s.cid < $\${paramIdx + 2}))\`)
116
- sqlParams.push(parsed.primary, parsed.primary, parsed.cid)
117
- paramIdx += 3
118
- }
119
- }
120
-
121
- const where = conditions.length ? 'WHERE ' + conditions.join(' AND ') : ''
122
-
123
- const rows = await db.query(
124
- \`SELECT s.* FROM "your.collection.here" s \${where} ORDER BY s.indexed_at DESC, s.cid DESC LIMIT $\${paramIdx}\`,
125
- sqlParams.concat([limit + 1]),
126
- )
127
-
128
- const hasMore = rows.length > limit
129
- if (hasMore) rows.pop()
130
- const lastRow = rows[rows.length - 1]
131
-
132
- return ok({
133
- items: rows,
134
- cursor: hasMore && lastRow ? packCursor(lastRow.indexed_at, lastRow.cid) : undefined,
135
- })
136
- })
137
- `,
138
- label: (name) => `import type { LabelRuleContext } from '@hatk/hatk/labels'
139
-
140
- export default {
141
- definition: {
142
- identifier: '${name}',
143
- severity: 'inform',
144
- blurs: 'none',
145
- defaultSetting: 'warn',
146
- locales: [{ lang: 'en', name: '${name.charAt(0).toUpperCase() + name.slice(1)}', description: 'Description here' }],
147
- },
148
- async evaluate(ctx: LabelRuleContext) {
149
- // Return array of label identifiers to apply, or empty array
150
- return []
151
- },
152
- }
153
- `,
154
- og: (name) => `import type { OpengraphContext, OpengraphResult } from '@hatk/hatk/opengraph'
155
-
156
- export default {
157
- path: '/og/${name}/:id',
158
- async generate(ctx: OpengraphContext): Promise<OpengraphResult> {
159
- const { db, params } = ctx
160
- return {
161
- element: {
162
- type: 'div',
163
- props: {
164
- style: { display: 'flex', width: '100%', height: '100%', background: '#080b12', color: 'white', alignItems: 'center', justifyContent: 'center' },
165
- children: params.id,
166
- },
167
- },
168
- }
169
- },
107
+ const templateDir = join(import.meta.dirname, 'templates');
108
+ function loadTemplate(file, name) {
109
+ const raw = readFileSync(join(templateDir, file), 'utf-8');
110
+ const capitalized = name.charAt(0).toUpperCase() + name.slice(1);
111
+ return raw.replaceAll('{{name}}', name).replaceAll('{{Name}}', capitalized);
170
112
  }
171
- `,
172
- job: (_name) => `export default {
173
- interval: 300, // seconds
174
- async run(_ctx: any) {
175
- // Periodic task logic here
176
- },
177
- }
178
- `,
179
- };
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
- const testTemplates = {
189
- feed: (name) => `import { describe, test, expect, beforeAll, afterAll } from 'vitest'
190
- import { createTestContext } from '@hatk/hatk/test'
191
-
192
- let ctx: Awaited<ReturnType<typeof createTestContext>>
193
-
194
- beforeAll(async () => {
195
- ctx = await createTestContext()
196
- await ctx.loadFixtures()
197
- })
198
-
199
- afterAll(async () => ctx?.close())
200
-
201
- describe('${name} feed', () => {
202
- test('returns results', async () => {
203
- const feed = ctx.loadFeed('${name}')
204
- const result = await feed.generate(ctx.feedContext({ limit: 10 }))
205
- expect(result).toBeDefined()
206
- })
207
- })
208
- `,
209
- xrpc: (name) => `import { describe, test, expect, beforeAll, afterAll } from 'vitest'
210
- import { createTestContext } from '@hatk/hatk/test'
211
-
212
- let ctx: Awaited<ReturnType<typeof createTestContext>>
213
-
214
- beforeAll(async () => {
215
- ctx = await createTestContext()
216
- await ctx.loadFixtures()
217
- })
218
-
219
- afterAll(async () => ctx?.close())
220
-
221
- describe('${name}', () => {
222
- test('returns response', async () => {
223
- const handler = ctx.loadXrpc('${name}')
224
- const result = await handler.handler({ params: {} })
225
- expect(result).toBeDefined()
226
- })
227
- })
228
- `,
229
- };
113
+ const templateTypes = ['feed', 'xrpc', 'label', 'og', 'hook', 'setup'];
114
+ const testTemplateTypes = ['feed', 'xrpc'];
115
+ const templates = Object.fromEntries(templateTypes.map((t) => [t, (name) => loadTemplate(`${t}.tpl`, name)]));
116
+ const testTemplates = Object.fromEntries(testTemplateTypes.map((t) => [t, (name) => loadTemplate(`test-${t}.tpl`, name)]));
230
117
  const lexiconTemplates = {
231
118
  record: (nsid) => ({
232
119
  lexicon: 1,
@@ -293,762 +180,21 @@ const lexiconTemplates = {
293
180
  }),
294
181
  };
295
182
  const dirs = {
296
- feed: 'feeds',
297
- xrpc: 'xrpc',
298
- label: 'labels',
299
- og: 'og',
300
- job: 'jobs',
183
+ feed: 'server',
184
+ xrpc: 'server',
185
+ label: 'server',
186
+ og: 'server',
187
+ hook: 'server',
188
+ setup: 'server',
301
189
  };
302
190
  // --- Commands ---
303
191
  if (command === 'new') {
304
- const name = args[1];
305
- if (!name) {
306
- console.error('Usage: hatk new <name> [--svelte] [--template <template-name>]');
307
- process.exit(1);
308
- }
309
- const templateIdx = args.indexOf('--template');
310
- const templateName = templateIdx !== -1 ? args[templateIdx + 1] : null;
311
- if (templateIdx !== -1 && !templateName) {
312
- console.error('Usage: hatk new <name> --template <template-name>');
313
- process.exit(1);
314
- }
315
- const dir = resolve(name);
316
- if (existsSync(dir)) {
317
- console.error(`Directory ${name} already exists`);
318
- process.exit(1);
319
- }
320
- if (templateName) {
321
- const repo = `https://github.com/hatk-dev/hatk-template-${templateName}.git`;
322
- console.log(`Cloning template ${templateName}...`);
323
- try {
324
- execSync(`git clone --depth 1 ${repo} ${dir}`, { stdio: 'inherit' });
325
- }
326
- catch {
327
- console.error(`Failed to clone template: ${repo}`);
328
- process.exit(1);
329
- }
330
- execSync(`rm -rf ${join(dir, '.git')}`);
331
- const pkgPath = join(dir, 'package.json');
332
- if (existsSync(pkgPath)) {
333
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
334
- pkg.name = name;
335
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
336
- }
337
- console.log(`\nCreated ${name}/ from template ${templateName}`);
338
- console.log(`\n cd ${name}`);
339
- console.log(` npm install`);
340
- console.log(` hatk dev`);
341
- process.exit(0);
342
- }
343
- const withSvelte = args.includes('--svelte');
344
- mkdirSync(dir);
345
- const subs = [
346
- 'lexicons',
347
- 'feeds',
348
- 'xrpc',
349
- 'og',
350
- 'labels',
351
- 'jobs',
352
- 'seeds',
353
- 'setup',
354
- 'public',
355
- 'test',
356
- 'test/feeds',
357
- 'test/xrpc',
358
- 'test/integration',
359
- 'test/browser',
360
- 'test/fixtures',
361
- ];
362
- if (withSvelte)
363
- subs.push('src', 'src/routes', 'src/lib');
364
- for (const sub of subs) {
365
- mkdirSync(join(dir, sub));
366
- }
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: []
372
-
373
- backfill:
374
- parallelism: 10
375
- `);
376
- writeFileSync(join(dir, 'public', 'index.html'), `<!DOCTYPE html>
377
- <html><head><title>${name}</title></head>
378
- <body><h1>${name}</h1></body></html>
379
- `);
380
- // Scaffold core framework lexicons under dev.hatk namespace
381
- const coreLexDir = join(dir, 'lexicons', 'dev', 'hatk');
382
- mkdirSync(coreLexDir, { recursive: true });
383
- writeFileSync(join(coreLexDir, 'describeCollections.json'), JSON.stringify({
384
- lexicon: 1,
385
- id: 'dev.hatk.describeCollections',
386
- defs: {
387
- main: {
388
- type: 'query',
389
- description: 'List indexed collections and their schemas.',
390
- output: {
391
- encoding: 'application/json',
392
- schema: {
393
- type: 'object',
394
- properties: {
395
- collections: {
396
- type: 'array',
397
- items: {
398
- type: 'object',
399
- required: ['collection'],
400
- properties: {
401
- collection: { type: 'string' },
402
- columns: {
403
- type: 'array',
404
- items: {
405
- type: 'object',
406
- required: ['name', 'originalName', 'type', 'required'],
407
- properties: {
408
- name: { type: 'string' },
409
- originalName: { type: 'string' },
410
- type: { type: 'string' },
411
- required: { type: 'boolean' },
412
- },
413
- },
414
- },
415
- },
416
- },
417
- },
418
- },
419
- },
420
- },
421
- },
422
- },
423
- }, null, 2) + '\n');
424
- writeFileSync(join(coreLexDir, 'describeFeeds.json'), JSON.stringify({
425
- lexicon: 1,
426
- id: 'dev.hatk.describeFeeds',
427
- defs: {
428
- main: {
429
- type: 'query',
430
- description: 'List available feeds.',
431
- output: {
432
- encoding: 'application/json',
433
- schema: {
434
- type: 'object',
435
- properties: {
436
- feeds: {
437
- type: 'array',
438
- items: {
439
- type: 'object',
440
- required: ['name', 'label'],
441
- properties: {
442
- name: { type: 'string' },
443
- label: { type: 'string' },
444
- },
445
- },
446
- },
447
- },
448
- },
449
- },
450
- },
451
- },
452
- }, null, 2) + '\n');
453
- writeFileSync(join(coreLexDir, 'describeLabels.json'), JSON.stringify({
454
- lexicon: 1,
455
- id: 'dev.hatk.describeLabels',
456
- defs: {
457
- main: {
458
- type: 'query',
459
- description: 'List available label definitions.',
460
- output: {
461
- encoding: 'application/json',
462
- schema: {
463
- type: 'object',
464
- properties: {
465
- definitions: {
466
- type: 'array',
467
- items: {
468
- type: 'object',
469
- required: ['identifier', 'severity', 'blurs', 'defaultSetting'],
470
- properties: {
471
- identifier: { type: 'string' },
472
- severity: { type: 'string' },
473
- blurs: { type: 'string' },
474
- defaultSetting: { type: 'string' },
475
- },
476
- },
477
- },
478
- },
479
- },
480
- },
481
- },
482
- },
483
- }, null, 2) + '\n');
484
- writeFileSync(join(coreLexDir, 'createRecord.json'), JSON.stringify({
485
- lexicon: 1,
486
- id: 'dev.hatk.createRecord',
487
- defs: {
488
- main: {
489
- type: 'procedure',
490
- description: "Create a record via the user's PDS.",
491
- input: {
492
- encoding: 'application/json',
493
- schema: {
494
- type: 'object',
495
- required: ['collection', 'repo', 'record'],
496
- properties: {
497
- collection: { type: 'string' },
498
- repo: { type: 'string', format: 'did' },
499
- record: { type: 'unknown' },
500
- },
501
- },
502
- },
503
- output: {
504
- encoding: 'application/json',
505
- schema: {
506
- type: 'object',
507
- properties: {
508
- uri: { type: 'string', format: 'at-uri' },
509
- cid: { type: 'string', format: 'cid' },
510
- },
511
- },
512
- },
513
- },
514
- },
515
- }, null, 2) + '\n');
516
- writeFileSync(join(coreLexDir, 'deleteRecord.json'), JSON.stringify({
517
- lexicon: 1,
518
- id: 'dev.hatk.deleteRecord',
519
- defs: {
520
- main: {
521
- type: 'procedure',
522
- description: "Delete a record via the user's PDS.",
523
- input: {
524
- encoding: 'application/json',
525
- schema: {
526
- type: 'object',
527
- required: ['collection', 'rkey'],
528
- properties: {
529
- collection: { type: 'string' },
530
- rkey: { type: 'string' },
531
- },
532
- },
533
- },
534
- output: { encoding: 'application/json', schema: { type: 'object', properties: {} } },
535
- },
536
- },
537
- }, null, 2) + '\n');
538
- writeFileSync(join(coreLexDir, 'putRecord.json'), JSON.stringify({
539
- lexicon: 1,
540
- id: 'dev.hatk.putRecord',
541
- defs: {
542
- main: {
543
- type: 'procedure',
544
- description: "Create or update a record via the user's PDS.",
545
- input: {
546
- encoding: 'application/json',
547
- schema: {
548
- type: 'object',
549
- required: ['collection', 'rkey', 'record'],
550
- properties: {
551
- collection: { type: 'string' },
552
- rkey: { type: 'string' },
553
- record: { type: 'unknown' },
554
- repo: { type: 'string', format: 'did' },
555
- },
556
- },
557
- },
558
- output: {
559
- encoding: 'application/json',
560
- schema: {
561
- type: 'object',
562
- properties: {
563
- uri: { type: 'string', format: 'at-uri' },
564
- cid: { type: 'string', format: 'cid' },
565
- },
566
- },
567
- },
568
- },
569
- },
570
- }, null, 2) + '\n');
571
- writeFileSync(join(coreLexDir, 'uploadBlob.json'), JSON.stringify({
572
- lexicon: 1,
573
- id: 'dev.hatk.uploadBlob',
574
- defs: {
575
- main: {
576
- type: 'procedure',
577
- description: "Upload a blob via the user's PDS.",
578
- input: {
579
- encoding: '*/*',
580
- },
581
- output: {
582
- encoding: 'application/json',
583
- schema: {
584
- type: 'object',
585
- required: ['blob'],
586
- properties: {
587
- blob: { type: 'blob' },
588
- },
589
- },
590
- },
591
- },
592
- },
593
- }, null, 2) + '\n');
594
- writeFileSync(join(coreLexDir, 'getFeed.json'), JSON.stringify({
595
- lexicon: 1,
596
- id: 'dev.hatk.getFeed',
597
- defs: {
598
- main: {
599
- type: 'query',
600
- description: 'Retrieve a named feed of items.',
601
- parameters: {
602
- type: 'params',
603
- required: ['feed'],
604
- properties: {
605
- feed: { type: 'string', description: 'Feed name' },
606
- limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
607
- cursor: { type: 'string' },
608
- },
609
- },
610
- output: {
611
- encoding: 'application/json',
612
- schema: {
613
- type: 'object',
614
- properties: {
615
- items: { type: 'array', items: { type: 'unknown' } },
616
- cursor: { type: 'string' },
617
- },
618
- },
619
- },
620
- },
621
- },
622
- }, null, 2) + '\n');
623
- writeFileSync(join(coreLexDir, 'getRecord.json'), JSON.stringify({
624
- lexicon: 1,
625
- id: 'dev.hatk.getRecord',
626
- defs: {
627
- main: {
628
- type: 'query',
629
- description: 'Fetch a single record by AT URI.',
630
- parameters: {
631
- type: 'params',
632
- required: ['uri'],
633
- properties: {
634
- uri: { type: 'string', format: 'at-uri' },
635
- },
636
- },
637
- output: {
638
- encoding: 'application/json',
639
- schema: {
640
- type: 'object',
641
- properties: {
642
- record: { type: 'unknown' },
643
- },
644
- },
645
- },
646
- },
647
- },
648
- }, null, 2) + '\n');
649
- writeFileSync(join(coreLexDir, 'getRecords.json'), JSON.stringify({
650
- lexicon: 1,
651
- id: 'dev.hatk.getRecords',
652
- defs: {
653
- main: {
654
- type: 'query',
655
- description: 'List records from a collection with optional filters.',
656
- parameters: {
657
- type: 'params',
658
- required: ['collection'],
659
- properties: {
660
- collection: { type: 'string' },
661
- limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
662
- cursor: { type: 'string' },
663
- sort: { type: 'string' },
664
- order: { type: 'string' },
665
- },
666
- },
667
- output: {
668
- encoding: 'application/json',
669
- schema: {
670
- type: 'object',
671
- properties: {
672
- items: { type: 'array', items: { type: 'unknown' } },
673
- cursor: { type: 'string' },
674
- },
675
- },
676
- },
677
- },
678
- },
679
- }, null, 2) + '\n');
680
- writeFileSync(join(coreLexDir, 'searchRecords.json'), JSON.stringify({
681
- lexicon: 1,
682
- id: 'dev.hatk.searchRecords',
683
- defs: {
684
- main: {
685
- type: 'query',
686
- description: 'Full-text search across a collection.',
687
- parameters: {
688
- type: 'params',
689
- required: ['collection', 'q'],
690
- properties: {
691
- collection: { type: 'string' },
692
- q: { type: 'string', description: 'Search query' },
693
- limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
694
- cursor: { type: 'string' },
695
- fuzzy: { type: 'boolean', default: true },
696
- },
697
- },
698
- output: {
699
- encoding: 'application/json',
700
- schema: {
701
- type: 'object',
702
- properties: {
703
- items: { type: 'array', items: { type: 'unknown' } },
704
- cursor: { type: 'string' },
705
- },
706
- },
707
- },
708
- },
709
- },
710
- }, null, 2) + '\n');
711
- writeFileSync(join(dir, 'seeds', 'seed.ts'), `import { seed } from '../hatk.generated.ts'
712
-
713
- const { createAccount, createRecord } = seed()
714
-
715
- const alice = await createAccount('alice.test')
716
-
717
- // await createRecord(alice, 'your.collection.here', {
718
- // field: 'value',
719
- // }, { rkey: 'my-record' })
720
-
721
- console.log('\\n[seed] Done!')
722
- `);
723
- writeFileSync(join(dir, 'docker-compose.yml'), `services:
724
- plc:
725
- build:
726
- context: https://github.com/did-method-plc/did-method-plc.git
727
- dockerfile: packages/server/Dockerfile
728
- ports:
729
- - '2582:2582'
730
- environment:
731
- - DATABASE_URL=postgres://plc:plc@postgres:5432/plc
732
- - PORT=2582
733
- command: ['dumb-init', 'node', '--enable-source-maps', '../dist/bin.js']
734
- depends_on:
735
- postgres:
736
- condition: service_healthy
737
- healthcheck:
738
- test: ['CMD-SHELL', 'wget -q --spider http://localhost:2582/_health || exit 1']
739
- interval: 2s
740
- timeout: 5s
741
- retries: 15
742
-
743
- pds:
744
- image: ghcr.io/bluesky-social/pds:latest
745
- ports:
746
- - '2583:2583'
747
- environment:
748
- - PDS_HOSTNAME=localhost
749
- - PDS_PORT=2583
750
- - PDS_DID_PLC_URL=http://plc:2582
751
- - PDS_DATA_DIRECTORY=/pds
752
- - PDS_BLOBSTORE_DISK_LOCATION=/pds/blobs
753
- - PDS_JWT_SECRET=dev-jwt-secret
754
- - PDS_ADMIN_PASSWORD=dev-admin
755
- - PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
756
- - PDS_INVITE_REQUIRED=false
757
- - PDS_DEV_MODE=true
758
- - LOG_ENABLED=true
759
- volumes:
760
- - pds_data:/pds
761
- depends_on:
762
- plc:
763
- condition: service_healthy
764
- healthcheck:
765
- test: ['CMD-SHELL', 'wget -q --spider http://localhost:2583/xrpc/_health || exit 1']
766
- interval: 2s
767
- timeout: 5s
768
- retries: 15
769
-
770
- postgres:
771
- image: postgres:16-alpine
772
- environment:
773
- - POSTGRES_USER=plc
774
- - POSTGRES_PASSWORD=plc
775
- - POSTGRES_DB=plc
776
- volumes:
777
- - plc_data:/var/lib/postgresql/data
778
- healthcheck:
779
- test: ['CMD-SHELL', 'pg_isready -U plc']
780
- interval: 2s
781
- timeout: 5s
782
- retries: 10
783
-
784
- volumes:
785
- pds_data:
786
- plc_data:
787
- `);
788
- writeFileSync(join(dir, '.dockerignore'), `node_modules
789
- data
790
- .svelte-kit
791
- public
792
- `);
793
- writeFileSync(join(dir, 'Dockerfile'), `FROM node:25-slim
794
- WORKDIR /app
795
- COPY package.json package-lock.json ./
796
- RUN npm ci --omit=dev
797
- COPY . .
798
- RUN node_modules/.bin/hatk build
799
- EXPOSE 3000
800
- CMD ["node", "--experimental-strip-types", "--no-warnings", "node_modules/hatk/src/main.ts", "config.yaml"]
801
- `);
802
- const pkgDeps = { '@hatk/oauth-client': '*', hatk: '*' };
803
- const pkgDevDeps = {
804
- '@playwright/test': '^1',
805
- oxfmt: '^0.35.0',
806
- oxlint: '^1',
807
- typescript: '^5',
808
- vite: '^6',
809
- vitest: '^4',
810
- };
811
- if (withSvelte) {
812
- pkgDevDeps['@sveltejs/adapter-static'] = '^3';
813
- pkgDevDeps['@sveltejs/kit'] = '^2';
814
- pkgDevDeps['@sveltejs/vite-plugin-svelte'] = '^5';
815
- pkgDevDeps['svelte'] = '^5';
816
- pkgDevDeps['svelte-check'] = '^4';
817
- }
818
- writeFileSync(join(dir, 'package.json'), JSON.stringify({
819
- name,
820
- private: true,
821
- type: 'module',
822
- scripts: {
823
- start: 'hatk start',
824
- dev: 'hatk dev',
825
- build: 'hatk build',
826
- check: 'hatk check',
827
- format: 'hatk format',
828
- },
829
- dependencies: pkgDeps,
830
- devDependencies: pkgDevDeps,
831
- }, null, 2) + '\n');
832
- writeFileSync(join(dir, 'tsconfig.server.json'), JSON.stringify({
833
- compilerOptions: {
834
- target: 'ES2022',
835
- module: 'Node16',
836
- moduleResolution: 'Node16',
837
- strict: true,
838
- esModuleInterop: true,
839
- skipLibCheck: true,
840
- noEmit: true,
841
- allowImportingTsExtensions: true,
842
- resolveJsonModule: true,
843
- },
844
- include: ['feeds', 'xrpc', 'og', 'seeds', 'labels', 'jobs', 'setup', 'hatk.generated.ts'],
845
- }, null, 2) + '\n');
846
- writeFileSync(join(dir, 'playwright.config.ts'), `import { defineConfig } from '@playwright/test'
847
-
848
- export default defineConfig({
849
- testDir: 'test/browser',
850
- use: { baseURL: 'http://127.0.0.1:3000' },
851
- globalSetup: './test/browser/global-setup.ts',
852
- })
853
- `);
854
- writeFileSync(join(dir, 'test/browser/global-setup.ts'), `import { execSync } from 'node:child_process'
855
- import { existsSync } from 'node:fs'
856
-
857
- export default function globalSetup() {
858
- if (existsSync('src/app.html')) {
859
- execSync('npx vite build', { stdio: 'inherit' })
860
- }
861
- }
862
- `);
863
- writeFileSync(join(dir, '.gitignore'), `node_modules/
864
- *.db
865
- data/
866
- test-results/
867
- .svelte-kit/
868
- .DS_Store
869
- public/
870
- `);
871
- writeFileSync(join(dir, '.oxlintrc.json'), `{
872
- "ignorePatterns": ["public", "data", ".svelte-kit", "hatk.generated.ts"]
873
- }
874
- `);
875
- writeFileSync(join(dir, '.oxfmtrc.json'), `{
876
- "semi": false,
877
- "singleQuote": true,
878
- "trailingComma": "all",
879
- "printWidth": 120,
880
- "tabWidth": 2,
881
- "ignorePatterns": ["public", "data", ".svelte-kit", "hatk.generated.ts"]
882
- }
883
- `);
884
- if (withSvelte) {
885
- writeFileSync(join(dir, 'svelte.config.js'), `import adapter from '@sveltejs/adapter-static'
886
-
887
- export default {
888
- kit: {
889
- adapter: adapter({
890
- pages: 'public',
891
- assets: 'public',
892
- fallback: 'index.html',
893
- }),
894
- paths: { base: '' },
895
- alias: {
896
- $hatk: './hatk.generated.ts',
897
- },
898
- },
899
- }
900
- `);
901
- writeFileSync(join(dir, 'vite.config.ts'), `import { sveltekit } from '@sveltejs/kit/vite'
902
- import { hatk } from '@hatk/hatk/vite-plugin'
903
- import { defineConfig } from 'vite'
904
-
905
- export default defineConfig({
906
- plugins: [sveltekit(), hatk()],
907
- })
908
- `);
909
- writeFileSync(join(dir, 'tsconfig.json'), JSON.stringify({
910
- extends: './.svelte-kit/tsconfig.json',
911
- compilerOptions: {
912
- allowJs: true,
913
- checkJs: false,
914
- esModuleInterop: true,
915
- forceConsistentCasingInFileNames: true,
916
- resolveJsonModule: true,
917
- skipLibCheck: true,
918
- sourceMap: true,
919
- strict: true,
920
- moduleResolution: 'bundler',
921
- allowImportingTsExtensions: true,
922
- },
923
- }, null, 2) + '\n');
924
- writeFileSync(join(dir, 'src/app.html'), `<!doctype html>
925
- <html lang="en">
926
- <head>
927
- <meta charset="utf-8" />
928
- <meta name="viewport" content="width=device-width, initial-scale=1" />
929
- <title>${name}</title>
930
- %sveltekit.head%
931
- </head>
932
- <body data-sveltekit-preload-data="hover">
933
- <div style="display: contents">%sveltekit.body%</div>
934
- </body>
935
- </html>
936
- `);
937
- writeFileSync(join(dir, 'src/app.css'), `*,
938
- *::before,
939
- *::after {
940
- box-sizing: border-box;
941
- margin: 0;
942
- padding: 0;
943
- }
944
-
945
- :root {
946
- --bg-root: #080b12;
947
- --bg-surface: #0f1419;
948
- --bg-elevated: #161d27;
949
- --bg-hover: #1c2633;
950
- --border: #1e293b;
951
- --teal: #14b8a6;
952
- --text-primary: #e2e8f0;
953
- --text-secondary: #94a3b8;
954
- --text-muted: #64748b;
955
- }
956
-
957
- html {
958
- background: var(--bg-root);
959
- color: var(--text-primary);
960
- }
961
-
962
- body {
963
- font-family: -apple-system, system-ui, sans-serif;
964
- font-size: 15px;
965
- line-height: 1.5;
966
- min-height: 100vh;
967
- }
968
-
969
- a {
970
- color: inherit;
971
- text-decoration: none;
972
- }
973
- `);
974
- writeFileSync(join(dir, 'src/routes/+layout.svelte'), `<script lang="ts">
975
- import type { Snippet } from 'svelte'
976
- import '../app.css'
977
-
978
- let { children }: { children: Snippet } = $props()
979
- </script>
980
-
981
- {@render children()}
982
- `);
983
- writeFileSync(join(dir, 'src/routes/+page.svelte'), `<h1>${name}</h1>
984
- <p>Your hatk server is running.</p>
985
- `);
986
- writeFileSync(join(dir, 'src/error.html'), `<!doctype html>
987
- <html lang="en">
988
- <head>
989
- <meta charset="utf-8" />
990
- <meta name="viewport" content="width=device-width, initial-scale=1" />
991
- <title>%sveltekit.error.message% — ${name}</title>
992
- <style>
993
- * { box-sizing: border-box; margin: 0; padding: 0; }
994
- body {
995
- font-family: -apple-system, system-ui, sans-serif;
996
- background: #080b12; color: #e2e8f0;
997
- min-height: 100vh; display: flex; align-items: center; justify-content: center;
998
- }
999
- .error-page { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 8px; padding: 24px; }
1000
- .error-code { font-size: 72px; font-weight: 800; color: #14b8a6; line-height: 1; }
1001
- .error-title { font-size: 24px; font-weight: 800; }
1002
- .error-link {
1003
- margin-top: 16px; padding: 10px 24px; background: #14b8a6; color: #000;
1004
- border-radius: 20px; font-weight: 600; font-size: 14px; text-decoration: none;
1005
- }
1006
- </style>
1007
- </head>
1008
- <body>
1009
- <div class="error-page">
1010
- <span class="error-code">%sveltekit.status%</span>
1011
- <h1 class="error-title">%sveltekit.error.message%</h1>
1012
- <a href="/" class="error-link">Back to home</a>
1013
- </div>
1014
- </body>
1015
- </html>
1016
- `);
1017
- writeFileSync(join(dir, 'src/routes/+error.svelte'), `<script lang="ts">
1018
- import { page } from '$app/state'
1019
- </script>
1020
-
1021
- <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 80vh; gap: 8px;">
1022
- <span style="font-size: 72px; font-weight: 800; color: var(--teal);">{page.status}</span>
1023
- <h1 style="font-size: 24px; font-weight: 800;">{page.error?.message}</h1>
1024
- <a href="/" style="margin-top: 16px; padding: 10px 24px; background: var(--teal); color: #000; border-radius: 20px; font-weight: 600; font-size: 14px;">Back to home</a>
1025
- </div>
1026
- `);
1027
- }
1028
- console.log(`Created ${name}/`);
1029
- console.log(` config.yaml`);
1030
- console.log(` lexicons/ — lexicon JSON files (core + your own)`);
1031
- console.log(` feeds/ — feed generators`);
1032
- console.log(` xrpc/ — XRPC method handlers`);
1033
- console.log(` og/ — OpenGraph image routes`);
1034
- console.log(` labels/ — label definitions + rules`);
1035
- console.log(` jobs/ — periodic tasks`);
1036
- console.log(` seeds/ — seed fixture data (hatk seed)`);
1037
- console.log(` setup/ — boot-time setup scripts (run before server starts)`);
1038
- console.log(` test/ — test files (hatk test)`);
1039
- console.log(` public/ — static files`);
1040
- console.log(` docker-compose.yml — local PDS for development`);
1041
- console.log(` Dockerfile — production container`);
1042
- if (withSvelte) {
1043
- console.log(` src/ — SvelteKit frontend`);
1044
- console.log(` svelte.config.js`);
1045
- console.log(` vite.config.ts`);
1046
- }
1047
- // Generate types so the project is ready to go
1048
- execSync('npx hatk generate types', { stdio: 'inherit', cwd: dir });
1049
- if (withSvelte) {
1050
- execSync('npx svelte-kit sync', { stdio: 'inherit', cwd: dir });
1051
- }
192
+ console.error('`hatk new` has been removed. Create a new project with:');
193
+ console.error('');
194
+ console.error(' vp create github:hatk-dev/hatk-template-starter');
195
+ console.error('');
196
+ console.error('Install vp first: curl -fsSL https://vite.plus | bash');
197
+ process.exit(1);
1052
198
  }
1053
199
  else if (command === 'generate') {
1054
200
  const type = args[1];
@@ -1073,6 +219,21 @@ else if (command === 'generate') {
1073
219
  }
1074
220
  }
1075
221
  entries.sort((a, b) => a.nsid.localeCompare(b.nsid));
222
+ // Collect procedure nsids and blob-input nsids for client callXrpc
223
+ const procedureNsids = [];
224
+ const blobInputNsids = [];
225
+ for (const { nsid, defType } of entries) {
226
+ if (defType === 'procedure') {
227
+ const lex = lexicons.get(nsid);
228
+ const inputEncoding = lex?.defs?.main?.input?.encoding;
229
+ if (inputEncoding === '*/*') {
230
+ blobInputNsids.push(nsid);
231
+ }
232
+ else {
233
+ procedureNsids.push(nsid);
234
+ }
235
+ }
236
+ }
1076
237
  if (entries.length === 0) {
1077
238
  console.error('No lexicons found');
1078
239
  process.exit(1);
@@ -1120,7 +281,8 @@ else if (command === 'generate') {
1120
281
  let out = '// Auto-generated from lexicons. Do not edit.\n';
1121
282
  out += `import type { ${[...usedWrappers].sort().join(', ')}, LexServerParams, Checked, Prettify, StrictArg } from '@hatk/hatk/lex-types'\n`;
1122
283
  out += `import type { XrpcContext } from '@hatk/hatk/xrpc'\n`;
1123
- out += `import { defineFeed as _defineFeed, type FeedResult, type FeedContext, type HydrateContext } from '@hatk/hatk/feeds'\n`;
284
+ out += `import { callXrpc as _callXrpc } from '@hatk/hatk/xrpc'\n`;
285
+ out += `import { defineFeed as _defineFeed, type FeedResult, type FeedContext, type BaseContext, type Row } from '@hatk/hatk/feeds'\n`;
1124
286
  out += `import { seed as _seed, type SeedOpts } from '@hatk/hatk/seed'\n`;
1125
287
  // Emit ALL lexicons as `const ... = {...} as const` (including defs-only)
1126
288
  out += `\n// ─── Lexicon Definitions ────────────────────────────────────────────\n\n`;
@@ -1276,8 +438,13 @@ else if (command === 'generate') {
1276
438
  out += `}\n`;
1277
439
  // Emit Ctx helper for typesafe XRPC handler contexts
1278
440
  out += `\n// ─── XRPC Helpers ───────────────────────────────────────────────────\n\n`;
1279
- out += `export type { HydrateContext } from '@hatk/hatk/feeds'\n`;
441
+ out += `export type { BaseContext, Row } from '@hatk/hatk/feeds'\n`;
1280
442
  out += `export { InvalidRequestError, NotFoundError } from '@hatk/hatk/xrpc'\n`;
443
+ out += `export { defineSetup } from '@hatk/hatk/setup'\n`;
444
+ out += `export { defineHook } from '@hatk/hatk/hooks'\n`;
445
+ out += `export { defineLabel } from '@hatk/hatk/labels'\n`;
446
+ out += `export { defineOG } from '@hatk/hatk/opengraph'\n`;
447
+ out += `export { defineRenderer } from '@hatk/hatk/renderer'\n`;
1281
448
  out += `export type Ctx<K extends keyof XrpcSchema & keyof Registry> = XrpcContext<\n`;
1282
449
  out += ` LexServerParams<Registry[K], Registry>,\n`;
1283
450
  out += ` RecordRegistry,\n`;
@@ -1290,21 +457,29 @@ else if (command === 'generate') {
1290
457
  out += ` nsid: K,\n`;
1291
458
  out += ` handler: (ctx: Ctx<K> & { ok: <T extends OutputOf<K>>(value: StrictArg<T, OutputOf<K>>) => Checked<OutputOf<K>> }) => Promise<Checked<OutputOf<K>>>,\n`;
1292
459
  out += `) {\n`;
1293
- out += ` return { handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
460
+ out += ` return { __type: 'query' as const, nsid, handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
1294
461
  out += `}\n\n`;
1295
462
  out += `export function defineProcedure<K extends keyof XrpcSchema & string>(\n`;
1296
463
  out += ` nsid: K,\n`;
1297
464
  out += ` handler: (ctx: Ctx<K> & { ok: <T extends OutputOf<K>>(value: StrictArg<T, OutputOf<K>>) => Checked<OutputOf<K>> }) => Promise<Checked<OutputOf<K>>>,\n`;
1298
465
  out += `) {\n`;
1299
- out += ` return { handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
466
+ out += ` return { __type: 'procedure' as const, nsid, handler: (ctx: any) => handler({ ...ctx, ok: (v: any) => v }) }\n`;
467
+ out += `}\n\n`;
468
+ out += `// ─── Server-side XRPC Caller ────────────────────────────────────────\n\n`;
469
+ out += `type ExtractParams<T> = T extends { params: infer P } ? P : Record<string, unknown>\n`;
470
+ out += `export async function callXrpc<K extends keyof XrpcSchema & string>(\n`;
471
+ out += ` nsid: K,\n`;
472
+ out += ` params?: ExtractParams<XrpcSchema[K]>,\n`;
473
+ out += `): Promise<OutputOf<K>> {\n`;
474
+ out += ` return _callXrpc(nsid, params as any) as Promise<OutputOf<K>>\n`;
1300
475
  out += `}\n\n`;
1301
476
  out += `// ─── Feed & Seed Helpers ────────────────────────────────────────────\n\n`;
1302
477
  out += `type FeedGenerate = (ctx: FeedContext & { ok: (value: FeedResult) => Checked<FeedResult> }) => Promise<Checked<FeedResult>>\n`;
1303
478
  out += `export function defineFeed<K extends keyof RecordRegistry>(\n`;
1304
- out += ` opts: { collection: K; view?: string; label: string; generate: FeedGenerate; hydrate?: (ctx: HydrateContext<RecordRegistry[K]>) => Promise<unknown[]> }\n`;
479
+ out += ` opts: { collection: K; view?: string; label: string; generate: FeedGenerate; hydrate?: (ctx: BaseContext, items: Row<RecordRegistry[K]>[]) => Promise<unknown[]> }\n`;
1305
480
  out += `): ReturnType<typeof _defineFeed>\n`;
1306
481
  out += `export function defineFeed(\n`;
1307
- out += ` opts: { collection?: never; view?: never; label: string; generate: FeedGenerate; hydrate: (ctx: HydrateContext<any>) => Promise<unknown[]> }\n`;
482
+ out += ` opts: { collection?: never; view?: never; label: string; generate: FeedGenerate; hydrate: (ctx: BaseContext, items: Row<unknown>[]) => Promise<unknown[]> }\n`;
1308
483
  out += `): ReturnType<typeof _defineFeed>\n`;
1309
484
  out += `export function defineFeed(opts: any) { return _defineFeed(opts) }\n`;
1310
485
  out += `export function seed(opts?: SeedOpts) { return _seed<RecordRegistry>(opts) }\n`;
@@ -1327,7 +502,150 @@ else if (command === 'generate') {
1327
502
  out = out.replace(/import type \{ ([^}]+) \} from '@hatk\/hatk\/lex-types'/, `import type { ${[...usedWrappers].sort().join(', ')}, LexServerParams, Checked, Prettify, StrictArg } from '@hatk/hatk/lex-types'`);
1328
503
  }
1329
504
  writeFileSync(outPath, out);
505
+ // Generate client-safe version (types + callXrpc only, no server module re-exports)
506
+ // Types use `export type` from main file (erased at compile time, no runtime import).
507
+ // callXrpc imports from @hatk/hatk/xrpc directly to avoid pulling in server deps.
508
+ let clientOut = '// Auto-generated client-safe subset. Do not edit.\n';
509
+ clientOut += `// Import this in app components instead of hatk.generated.ts\n`;
510
+ clientOut += `// to avoid pulling in server-only dependencies.\n`;
511
+ clientOut += `export type { XrpcSchema } from './hatk.generated.ts'\n`;
512
+ clientOut += `import type { XrpcSchema } from './hatk.generated.ts'\n`;
513
+ // Re-export all types
514
+ const typeExports = [];
515
+ for (const { nsid, defType } of entries) {
516
+ if (!defType)
517
+ continue;
518
+ if (nsid === 'dev.hatk.createRecord' || nsid === 'dev.hatk.deleteRecord' || nsid === 'dev.hatk.putRecord')
519
+ continue;
520
+ typeExports.push(capitalize(varNames.get(nsid)));
521
+ }
522
+ if (recordEntries.length > 0) {
523
+ typeExports.push('RecordRegistry', 'CreateRecord', 'DeleteRecord', 'PutRecord');
524
+ }
525
+ // Named defs (views, objects) — collect from emittedDefNames minus main types
526
+ const mainTypeNames = new Set(entries.filter((e) => e.defType).map((e) => capitalize(varNames.get(e.nsid))));
527
+ for (const name of emittedDefNames) {
528
+ if (!mainTypeNames.has(name) && !typeExports.includes(name)) {
529
+ typeExports.push(name);
530
+ }
531
+ }
532
+ if (typeExports.length > 0) {
533
+ clientOut += `export type { ${typeExports.join(', ')} } from './hatk.generated.ts'\n`;
534
+ }
535
+ // Typed callXrpc — environment-aware:
536
+ // SSR: uses globalThis.__hatk_callXrpc bridge (direct handler invocation)
537
+ // Client: fetches via HTTP (GET for queries, POST for procedures, raw POST for blobs)
538
+ if (procedureNsids.length > 0) {
539
+ clientOut += `\nconst _procedures = new Set([${procedureNsids.map((n) => `'${n}'`).join(', ')}])\n`;
540
+ }
541
+ if (blobInputNsids.length > 0) {
542
+ clientOut += `const _blobInputs = new Set([${blobInputNsids.map((n) => `'${n}'`).join(', ')}])\n`;
543
+ }
544
+ clientOut += `\ntype CallArg<K extends keyof XrpcSchema> =\n`;
545
+ clientOut += ` XrpcSchema[K] extends { input: infer I } ? I :\n`;
546
+ clientOut += ` XrpcSchema[K] extends { params: infer P } ? P :\n`;
547
+ clientOut += ` Record<string, unknown>\n`;
548
+ clientOut += `type OutputOf<K extends keyof XrpcSchema> = XrpcSchema[K]['output']\n\n`;
549
+ clientOut += `export async function callXrpc<K extends keyof XrpcSchema & string>(\n`;
550
+ clientOut += ` nsid: K,\n`;
551
+ clientOut += ` arg?: CallArg<K>,\n`;
552
+ clientOut += ` customFetch?: typeof globalThis.fetch,\n`;
553
+ clientOut += `): Promise<OutputOf<K>> {\n`;
554
+ // Server-side bridge (skip when customFetch is provided — let SvelteKit's fetch handle it)
555
+ clientOut += ` if (typeof window === 'undefined' && !customFetch) {\n`;
556
+ clientOut += ` const bridge = (globalThis as any).__hatk_callXrpc\n`;
557
+ clientOut += ` if (!bridge) throw new Error('callXrpc: server bridge not available — is hatk initialized?')\n`;
558
+ if (procedureNsids.length > 0 || blobInputNsids.length > 0) {
559
+ const checks = [];
560
+ if (procedureNsids.length > 0)
561
+ checks.push('_procedures.has(nsid)');
562
+ if (blobInputNsids.length > 0)
563
+ checks.push('_blobInputs.has(nsid)');
564
+ clientOut += ` if (${checks.join(' || ')}) return bridge(nsid, {}, arg) as Promise<OutputOf<K>>\n`;
565
+ }
566
+ clientOut += ` return bridge(nsid, arg) as Promise<OutputOf<K>>\n`;
567
+ clientOut += ` }\n`;
568
+ // Client-side fetch (or server-side with customFetch for SSR deduplication)
569
+ clientOut += ` const _fetch = customFetch ?? globalThis.fetch\n`;
570
+ clientOut += ` // Use relative URL so SvelteKit's fetch can deduplicate server/client requests\n`;
571
+ clientOut += ` let path = \`/xrpc/\${nsid}\`\n`;
572
+ if (blobInputNsids.length > 0) {
573
+ clientOut += ` if (_blobInputs.has(nsid)) {\n`;
574
+ clientOut += ` const blob = arg as Blob | ArrayBuffer\n`;
575
+ clientOut += ` const ct = blob instanceof Blob ? blob.type : 'application/octet-stream'\n`;
576
+ clientOut += ` const res = await _fetch(path, { method: 'POST', headers: { 'Content-Type': ct }, body: blob })\n`;
577
+ clientOut += ` if (typeof window !== 'undefined' && res.status === 401) { const _b = await res.json().catch(() => ({})); const _h = _b.handle ?? getViewer()?.handle; window.location.href = _h ? \`/oauth/login?handle=\${encodeURIComponent(_h)}\` : '/oauth/login'; return new Promise(() => {}) as any }\n`;
578
+ clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n`;
579
+ clientOut += ` return res.json() as Promise<OutputOf<K>>\n`;
580
+ clientOut += ` }\n`;
581
+ }
582
+ if (procedureNsids.length > 0) {
583
+ clientOut += ` if (_procedures.has(nsid)) {\n`;
584
+ clientOut += ` const res = await _fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(arg) })\n`;
585
+ clientOut += ` if (typeof window !== 'undefined' && res.status === 401) { const _b = await res.json().catch(() => ({})); const _h = _b.handle ?? getViewer()?.handle; window.location.href = _h ? \`/oauth/login?handle=\${encodeURIComponent(_h)}\` : '/oauth/login'; return new Promise(() => {}) as any }\n`;
586
+ clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n`;
587
+ clientOut += ` return res.json() as Promise<OutputOf<K>>\n`;
588
+ clientOut += ` }\n`;
589
+ }
590
+ clientOut += ` const params = new URLSearchParams()\n`;
591
+ clientOut += ` for (const [k, v] of Object.entries(arg || {})) {\n`;
592
+ // An array goes out as a repeated key, the XRPC form: `?dids=a&dids=b`.
593
+ // Joined with commas it reaches the handler as one string.
594
+ clientOut += ` if (v == null) continue\n`;
595
+ clientOut += ` if (Array.isArray(v)) for (const item of v) params.append(k, String(item))\n`;
596
+ clientOut += ` else params.set(k, String(v))\n`;
597
+ clientOut += ` }\n`;
598
+ clientOut += ` const qs = params.toString()\n`;
599
+ clientOut += ` if (qs) path += \`?\${qs}\`\n`;
600
+ clientOut += ` const res = await _fetch(path)\n`;
601
+ clientOut += ` if (typeof window !== 'undefined' && res.status === 401) { const _b = await res.json().catch(() => ({})); const _h = _b.handle ?? getViewer()?.handle; window.location.href = _h ? \`/oauth/login?handle=\${encodeURIComponent(_h)}\` : '/oauth/login'; return new Promise(() => {}) as any }\n`;
602
+ clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n`;
603
+ clientOut += ` return res.json() as Promise<OutputOf<K>>\n`;
604
+ clientOut += `}\n`;
605
+ // getViewer — returns the viewer set by layout load (server) or $effect (client)
606
+ clientOut += `\nexport function getViewer(): { did: string; handle: string } | null {\n`;
607
+ clientOut += ` return (globalThis as any).__hatk_viewer ?? null\n`;
608
+ clientOut += `}\n`;
609
+ // Auth helpers — login, logout, viewerDid
610
+ clientOut += `\n// ─── Auth Helpers ────────────────────────────────────────────────────\n\n`;
611
+ clientOut += `export async function login(handle: string): Promise<void> {\n`;
612
+ clientOut += ` const res = await fetch(\`/oauth/login?handle=\${encodeURIComponent(handle)}\`, { redirect: 'manual' })\n`;
613
+ clientOut += ` if (res.type === 'opaqueredirect') {\n`;
614
+ clientOut += ` window.location.href = \`/oauth/login?handle=\${encodeURIComponent(handle)}\`\n`;
615
+ clientOut += ` return\n`;
616
+ clientOut += ` }\n`;
617
+ clientOut += ` if (res.ok) return\n`;
618
+ clientOut += ` const body = await res.json().catch(() => ({ error: 'Login failed' }))\n`;
619
+ clientOut += ` throw new Error(body.error || 'Login failed')\n`;
620
+ clientOut += `}\n\n`;
621
+ clientOut += `export async function logout(): Promise<void> {\n`;
622
+ clientOut += ` ;(globalThis as any).__hatk_viewer = null\n`;
623
+ clientOut += ` await fetch('/auth/logout', { method: 'POST' }).catch(() => {})\n`;
624
+ clientOut += `}\n\n`;
625
+ clientOut += `export function viewerDid(): string | null {\n`;
626
+ clientOut += ` if (typeof window === 'undefined') return null\n`;
627
+ clientOut += ` const viewer = (globalThis as any).__hatk_viewer\n`;
628
+ clientOut += ` return viewer?.did ?? null\n`;
629
+ clientOut += `}\n\n`;
630
+ clientOut += `// Expose viewer for getViewer() bridge\n`;
631
+ clientOut += `;(globalThis as any).__hatk_auth = { viewerDid }\n`;
632
+ // parseViewer — server-side session cookie resolution for +layout.server.ts
633
+ clientOut += `\n// ─── Server Helpers ──────────────────────────────────────────────────\n\n`;
634
+ clientOut += `export async function parseViewer(cookies: { get(name: string): string | undefined }): Promise<{ did: string; handle?: string } | null> {\n`;
635
+ clientOut += ` const parseSessionCookie = (globalThis as any).__hatk_parseSessionCookie\n`;
636
+ clientOut += ` if (!parseSessionCookie) return null\n`;
637
+ clientOut += ` const cookieValue = cookies.get('__hatk_session')\n`;
638
+ clientOut += ` if (!cookieValue) return null\n`;
639
+ clientOut += ` try {\n`;
640
+ clientOut += ` const request = new Request('http://localhost', { headers: { cookie: \`__hatk_session=\${cookieValue}\` } })\n`;
641
+ clientOut += ` const viewer = await parseSessionCookie(request)\n`;
642
+ clientOut += ` if (viewer) (globalThis as any).__hatk_viewer = viewer\n`;
643
+ clientOut += ` return viewer\n`;
644
+ clientOut += ` } catch { return null }\n`;
645
+ clientOut += `}\n`;
646
+ writeFileSync('./hatk.generated.client.ts', clientOut);
1330
647
  console.log(`Generated ${outPath} with ${entries.length} types: ${entries.map((e) => capitalize(varNames.get(e.nsid))).join(', ')}`);
648
+ console.log(`Generated ./hatk.generated.client.ts (client-safe subset)`);
1331
649
  }
1332
650
  else if (lexiconTemplates[type]) {
1333
651
  const nsid = args[2];
@@ -1355,18 +673,9 @@ else if (command === 'generate') {
1355
673
  process.exit(1);
1356
674
  }
1357
675
  const baseDir = dirs[type];
1358
- let filePath;
1359
- if (type === 'xrpc') {
1360
- // NSID → folder path: fm.teal.getStats → xrpc/fm/teal/getStats.ts
1361
- const parts = name.split('.');
1362
- const subDir = join(baseDir, ...parts.slice(0, -1));
1363
- mkdirSync(subDir, { recursive: true });
1364
- filePath = join(subDir, `${parts[parts.length - 1]}.ts`);
1365
- }
1366
- else {
1367
- mkdirSync(baseDir, { recursive: true });
1368
- filePath = join(baseDir, `${name}.ts`);
1369
- }
676
+ mkdirSync(baseDir, { recursive: true });
677
+ const fileName = type === 'xrpc' ? name.split('.').pop() : name;
678
+ const filePath = join(baseDir, `${fileName}.ts`);
1370
679
  if (existsSync(filePath)) {
1371
680
  console.error(`${filePath} already exists`);
1372
681
  process.exit(1);
@@ -1376,7 +685,7 @@ else if (command === 'generate') {
1376
685
  // Scaffold test file if template exists
1377
686
  const testTemplate = testTemplates[type];
1378
687
  if (testTemplate) {
1379
- const testDir = type === 'xrpc' ? 'test/xrpc' : `test/${baseDir}`;
688
+ const testDir = 'test/server';
1380
689
  mkdirSync(testDir, { recursive: true });
1381
690
  const testName = type === 'xrpc' ? name.split('.').pop() : name;
1382
691
  const testPath = join(testDir, `${testName}.test.ts`);
@@ -1395,18 +704,9 @@ else if (command === 'destroy') {
1395
704
  process.exit(1);
1396
705
  }
1397
706
  const baseDir = dirs[type];
1398
- let tsPath, jsPath;
1399
- if (type === 'xrpc') {
1400
- const parts = name.split('.');
1401
- const leaf = parts[parts.length - 1];
1402
- const subDir = join(baseDir, ...parts.slice(0, -1));
1403
- tsPath = join(subDir, `${leaf}.ts`);
1404
- jsPath = join(subDir, `${leaf}.js`);
1405
- }
1406
- else {
1407
- tsPath = join(baseDir, `${name}.ts`);
1408
- jsPath = join(baseDir, `${name}.js`);
1409
- }
707
+ const fileName = type === 'xrpc' ? name.split('.').pop() : name;
708
+ const tsPath = join(baseDir, `${fileName}.ts`);
709
+ const jsPath = join(baseDir, `${fileName}.js`);
1410
710
  const filePath = existsSync(tsPath) ? tsPath : existsSync(jsPath) ? jsPath : null;
1411
711
  if (!filePath) {
1412
712
  console.error(`No file found for ${type} "${name}"`);
@@ -1415,7 +715,7 @@ else if (command === 'destroy') {
1415
715
  unlinkSync(filePath);
1416
716
  console.log(`Removed ${filePath}`);
1417
717
  // Clean up test file
1418
- const testDir = type === 'xrpc' ? 'test/xrpc' : `test/${baseDir}`;
718
+ const testDir = 'test/server';
1419
719
  const testName = type === 'xrpc' ? name.split('.').pop() : name;
1420
720
  const testFile = join(testDir, `${testName}.test.ts`);
1421
721
  if (existsSync(testFile)) {
@@ -1429,21 +729,14 @@ else if (command === 'destroy') {
1429
729
  else if (command === 'dev') {
1430
730
  await ensurePds();
1431
731
  runSeed();
1432
- try {
1433
- if (existsSync(resolve('svelte.config.js')) && existsSync(resolve('src/app.html'))) {
1434
- // SvelteKit project — vite dev starts the hatk server via the plugin
1435
- execSync('npx vite dev', { stdio: 'inherit', cwd: process.cwd() });
1436
- }
1437
- else {
1438
- // No frontend — just run the hatk server directly
1439
- const mainPath = resolve(import.meta.dirname, 'main.ts');
1440
- execSync(`npx tsx ${mainPath} config.yaml`, { stdio: 'inherit', cwd: process.cwd() });
1441
- }
732
+ if (existsSync(resolve('vite.config.ts')) || existsSync(resolve('vite.config.js'))) {
733
+ // Vite project — vite dev starts the hatk server via the plugin
734
+ await spawnForward('npx', ['vite', 'dev']);
1442
735
  }
1443
- catch (e) {
1444
- if (e.signal === 'SIGINT' || e.signal === 'SIGTERM')
1445
- process.exit(0);
1446
- throw e;
736
+ else {
737
+ // No frontend — just run the hatk server directly
738
+ const mainPath = resolve(import.meta.dirname, 'main.js');
739
+ await spawnForward('npx', ['tsx', mainPath, 'hatk.config.ts'], { DEV_MODE: '1' });
1447
740
  }
1448
741
  }
1449
742
  else if (command === 'format' || command === 'fmt') {
@@ -1463,9 +756,9 @@ else if (command === 'build') {
1463
756
  }
1464
757
  }
1465
758
  else if (command === 'reset') {
1466
- const config = loadConfig(resolve('config.yaml'));
759
+ const config = await loadConfig(resolve('hatk.config.ts'));
1467
760
  if (config.database !== ':memory:') {
1468
- for (const suffix of ['', '.wal']) {
761
+ for (const suffix of ['', '.wal', '-shm', '-wal']) {
1469
762
  const file = config.database + suffix;
1470
763
  if (existsSync(file)) {
1471
764
  unlinkSync(file);
@@ -1622,41 +915,9 @@ else if (command === 'resolve') {
1622
915
  console.log(`\nResolved ${resolved.size} lexicon(s). Regenerating types...`);
1623
916
  execSync('npx hatk generate types', { stdio: 'inherit', cwd: process.cwd() });
1624
917
  }
1625
- else if (command === 'schema') {
1626
- const config = loadConfig(resolve('config.yaml'));
1627
- if (config.database === ':memory:') {
1628
- console.error('No database file configured (database is :memory:)');
1629
- process.exit(1);
1630
- }
1631
- if (!existsSync(config.database)) {
1632
- console.error(`Database not found: ${config.database}`);
1633
- console.error('Run "hatk dev" first to create it.');
1634
- process.exit(1);
1635
- }
1636
- const { DuckDBInstance } = await import('@duckdb/node-api');
1637
- const instance = await DuckDBInstance.create(config.database);
1638
- const con = await instance.connect();
1639
- const tables = (await (await con.runAndReadAll(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name`)).getRowObjects());
1640
- for (const { table_name } of tables) {
1641
- console.log(`"${table_name}"`);
1642
- 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());
1643
- for (const col of cols) {
1644
- const nullable = col.is_nullable === 'YES' ? '' : ' NOT NULL';
1645
- console.log(` ${col.column_name.padEnd(20)} ${col.data_type}${nullable}`);
1646
- }
1647
- console.log();
1648
- }
1649
- }
1650
918
  else if (command === 'start') {
1651
- try {
1652
- const mainPath = resolve(import.meta.dirname, 'main.ts');
1653
- execSync(`npx tsx ${mainPath} config.yaml`, { stdio: 'inherit', cwd: process.cwd() });
1654
- }
1655
- catch (e) {
1656
- if (e.signal === 'SIGINT' || e.signal === 'SIGTERM')
1657
- process.exit(0);
1658
- throw e;
1659
- }
919
+ const mainPath = resolve(import.meta.dirname, 'main.js');
920
+ await spawnForward('npx', ['tsx', mainPath, 'hatk.config.ts']);
1660
921
  }
1661
922
  else {
1662
923
  usage();