AppView in a box as a Vite plugin thing hatk.dev
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: add design spec and implementation plan for TypeScript config migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+490
+389
docs/superpowers/plans/2026-03-12-typescript-config-plan.md
··· 1 + # Switch config.yaml to hatk.config.ts — Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Replace `config.yaml` with `hatk.config.ts` so users get type safety and autocompletion when configuring hatk. 6 + 7 + **Architecture:** Add `defineConfig` identity function for type inference. Rewrite `loadConfig` to use dynamic `import()` instead of YAML parsing. Update all call sites (main.ts, cli.ts, test.ts, vite-plugin.ts) and the `hatk new` scaffolder. 8 + 9 + **Tech Stack:** TypeScript, dynamic `import()`, no new dependencies. 10 + 11 + **Note:** The `yaml` package stays in dependencies — `test.ts` uses it for loading YAML fixture files in `loadFixtures()`. Only `config.ts` stops using it. 12 + 13 + --- 14 + 15 + ### Task 1: Rewrite config.ts — add defineConfig, make loadConfig async 16 + 17 + **Files:** 18 + - Modify: `packages/hatk/src/config.ts` 19 + 20 + **Step 1: Rewrite config.ts** 21 + 22 + Replace the entire file. Remove `readFileSync` and `YAML` imports. Add `defineConfig`. Make `loadConfig` async with `import()`. 23 + 24 + ```typescript 25 + import { resolve, dirname } from 'node:path' 26 + import { existsSync } from 'node:fs' 27 + 28 + export interface LabelLocale { 29 + lang: string 30 + name: string 31 + description: string 32 + } 33 + 34 + export interface LabelDefinition { 35 + identifier: string 36 + severity: 'alert' | 'inform' | 'none' 37 + blurs: 'media' | 'content' | 'none' 38 + defaultSetting: 'warn' | 'hide' | 'ignore' 39 + locales?: LabelLocale[] 40 + } 41 + 42 + export interface OAuthClientConfig { 43 + client_id: string 44 + client_name: string 45 + redirect_uris: string[] 46 + scope?: string 47 + } 48 + 49 + export interface OAuthConfig { 50 + issuer: string 51 + scopes: string[] 52 + clients: OAuthClientConfig[] 53 + } 54 + 55 + export interface BackfillConfig { 56 + signalCollections?: string[] // defaults to top-level collections 57 + repos?: string[] // pin specific DIDs to backfill 58 + fullNetwork: boolean 59 + parallelism: number 60 + fetchTimeout: number // seconds 61 + maxRetries: number // max retry attempts for failed repos (default 5) 62 + } 63 + 64 + export interface HatkConfig { 65 + relay: string 66 + plc: string // PLC directory URL for DID resolution 67 + port: number 68 + database: string // DuckDB file path (replaces :memory:) 69 + publicDir: string | null // static file directory (null to disable) 70 + collections: string[] // optional — auto-derived from lexicons if empty 71 + backfill: BackfillConfig 72 + ftsRebuildInterval: number // rebuild FTS index every N writes (lower = fresher search) 73 + oauth: OAuthConfig | null 74 + admins: string[] // DIDs allowed to access /admin/* endpoints 75 + } 76 + 77 + /** Identity function that provides type inference for hatk config files. */ 78 + export function defineConfig(config: Partial<HatkConfig>): Partial<HatkConfig> { 79 + return config 80 + } 81 + 82 + /** Derive HTTP URL from relay WebSocket URL (ws://host → http://host) */ 83 + export function relayHttpUrl(relay: string): string { 84 + return relay.replace(/^ws(s?):\/\//, 'http$1://') 85 + } 86 + 87 + export async function loadConfig(configPath: string): Promise<HatkConfig> { 88 + const resolved = resolve(configPath) 89 + 90 + if (!existsSync(resolved)) { 91 + console.error(`Config file not found: ${resolved}`) 92 + console.error(`hatk now uses hatk.config.ts instead of config.yaml.`) 93 + console.error(`Create a hatk.config.ts file or run 'hatk new' to scaffold a project.`) 94 + process.exit(1) 95 + } 96 + 97 + const configDir = dirname(resolved) 98 + const mod = await import(resolved) 99 + const parsed: Partial<HatkConfig> & Record<string, any> = mod.default || {} 100 + 101 + const backfillRaw = parsed.backfill || ({} as Partial<BackfillConfig>) 102 + const env = process.env 103 + 104 + const database = env.DATABASE || parsed.database 105 + const config: HatkConfig = { 106 + relay: env.RELAY || parsed.relay || 'ws://localhost:2583', 107 + plc: env.DID_PLC_URL || parsed.plc || 'https://plc.directory', 108 + port: parseInt(env.PORT || '') || parsed.port || 3000, 109 + database: database ? resolve(configDir, database) : ':memory:', 110 + publicDir: parsed.publicDir === null ? null : resolve(configDir, (parsed as any).public || parsed.publicDir || './public'), 111 + collections: parsed.collections || [], 112 + backfill: { 113 + signalCollections: backfillRaw.signalCollections || undefined, 114 + repos: env.BACKFILL_REPOS ? env.BACKFILL_REPOS.split(',').map((s) => s.trim()) : backfillRaw.repos || undefined, 115 + fullNetwork: env.BACKFILL_FULL_NETWORK ? env.BACKFILL_FULL_NETWORK === 'true' : backfillRaw.fullNetwork || false, 116 + parallelism: parseInt(env.BACKFILL_PARALLELISM || '') || backfillRaw.parallelism || 3, 117 + fetchTimeout: parseInt(env.BACKFILL_FETCH_TIMEOUT || '') || backfillRaw.fetchTimeout || 300, 118 + maxRetries: parseInt(env.BACKFILL_MAX_RETRIES || '') || backfillRaw.maxRetries || 5, 119 + }, 120 + ftsRebuildInterval: parseInt(env.FTS_REBUILD_INTERVAL || '') || parsed.ftsRebuildInterval || 5000, 121 + oauth: null, 122 + admins: env.ADMINS ? env.ADMINS.split(',').map((s) => s.trim()) : parsed.admins || [], 123 + } 124 + 125 + const oauthRaw = parsed.oauth 126 + if (oauthRaw) { 127 + config.oauth = { 128 + issuer: process.env.OAUTH_ISSUER || oauthRaw.issuer || `http://127.0.0.1:${config.port}`, 129 + scopes: oauthRaw.scopes || ['atproto'], 130 + clients: oauthRaw.clients || [], 131 + } 132 + } 133 + 134 + return config 135 + } 136 + ``` 137 + 138 + **Step 2: Verify the file compiles** 139 + 140 + Run: `cd packages/hatk && npx tsc --noEmit src/config.ts` 141 + 142 + --- 143 + 144 + ### Task 2: Update main.ts — await loadConfig, change default path 145 + 146 + **Files:** 147 + - Modify: `packages/hatk/src/main.ts:34,40` 148 + 149 + **Step 1: Update the config path default and await loadConfig** 150 + 151 + Change line 34 from: 152 + ```typescript 153 + const configPath = process.argv[2] || 'config.yaml' 154 + ``` 155 + to: 156 + ```typescript 157 + const configPath = process.argv[2] || 'hatk.config.ts' 158 + ``` 159 + 160 + Change line 40 from: 161 + ```typescript 162 + const config = loadConfig(configPath) 163 + ``` 164 + to: 165 + ```typescript 166 + const config = await loadConfig(configPath) 167 + ``` 168 + 169 + **Step 2: Verify it compiles** 170 + 171 + Run: `cd packages/hatk && npx tsc --noEmit src/main.ts` 172 + 173 + --- 174 + 175 + ### Task 3: Update cli.ts — all config.yaml references to hatk.config.ts, await loadConfig 176 + 177 + **Files:** 178 + - Modify: `packages/hatk/src/cli.ts` 179 + 180 + There are 8 references to `config.yaml` in cli.ts. Change all of them: 181 + 182 + **Step 1: Update `hatk new` scaffolder (line 378)** 183 + 184 + Change the file creation from writing `config.yaml` to writing `hatk.config.ts`: 185 + 186 + ```typescript 187 + writeFileSync( 188 + join(dir, 'hatk.config.ts'), 189 + `import { defineConfig } from 'hatk' 190 + 191 + export default defineConfig({ 192 + relay: 'ws://localhost:2583', 193 + plc: 'http://localhost:2582', 194 + port: 3000, 195 + database: 'data/hatk.db', 196 + admins: [], 197 + backfill: { 198 + parallelism: 10, 199 + }, 200 + }) 201 + `, 202 + ) 203 + ``` 204 + 205 + **Step 2: Update Dockerfile template (line 988)** 206 + 207 + Change: 208 + ``` 209 + CMD ["node", "--max-old-space-size=512", "node_modules/@hatk/hatk/dist/main.js", "config.yaml"] 210 + ``` 211 + to: 212 + ``` 213 + CMD ["node", "--max-old-space-size=512", "node_modules/@hatk/hatk/dist/main.js", "hatk.config.ts"] 214 + ``` 215 + 216 + **Step 3: Update scaffolding output message (line 1297)** 217 + 218 + Change: 219 + ```typescript 220 + console.log(` config.yaml`) 221 + ``` 222 + to: 223 + ```typescript 224 + console.log(` hatk.config.ts`) 225 + ``` 226 + 227 + **Step 4: Update `hatk dev` command (line 1740)** 228 + 229 + Change: 230 + ```typescript 231 + execSync(`npx tsx ${mainPath} config.yaml`, { 232 + ``` 233 + to: 234 + ```typescript 235 + execSync(`npx tsx ${mainPath} hatk.config.ts`, { 236 + ``` 237 + 238 + **Step 5: Update `hatk reset` command (line 1763)** 239 + 240 + Change: 241 + ```typescript 242 + const config = loadConfig(resolve('config.yaml')) 243 + ``` 244 + to: 245 + ```typescript 246 + const config = await loadConfig(resolve('hatk.config.ts')) 247 + ``` 248 + 249 + **Step 6: Update `hatk schema` command (line 1925)** 250 + 251 + Change: 252 + ```typescript 253 + const config = loadConfig(resolve('config.yaml')) 254 + ``` 255 + to: 256 + ```typescript 257 + const config = await loadConfig(resolve('hatk.config.ts')) 258 + ``` 259 + 260 + **Step 7: Update `hatk start` command (line 1963)** 261 + 262 + Change: 263 + ```typescript 264 + execSync(`npx tsx ${mainPath} config.yaml`, { stdio: 'inherit', cwd: process.cwd() }) 265 + ``` 266 + to: 267 + ```typescript 268 + execSync(`npx tsx ${mainPath} hatk.config.ts`, { stdio: 'inherit', cwd: process.cwd() }) 269 + ``` 270 + 271 + **Step 8: Verify it compiles** 272 + 273 + Run: `cd packages/hatk && npx tsc --noEmit src/cli.ts` 274 + 275 + --- 276 + 277 + ### Task 4: Update test.ts — await loadConfig, change default path 278 + 279 + **Files:** 280 + - Modify: `packages/hatk/src/test.ts:54-74` 281 + 282 + **Step 1: Update findConfigPath to look for hatk.config.ts** 283 + 284 + Change lines 57-61 from: 285 + ```typescript 286 + function findConfigPath(): string { 287 + const explicit = process.env.APPVIEW_CONFIG 288 + if (explicit) return resolve(explicit) 289 + return resolve('config.yaml') 290 + } 291 + ``` 292 + to: 293 + ```typescript 294 + function findConfigPath(): string { 295 + const explicit = process.env.APPVIEW_CONFIG 296 + if (explicit) return resolve(explicit) 297 + return resolve('hatk.config.ts') 298 + } 299 + ``` 300 + 301 + **Step 2: Update createTestContext to await loadConfig** 302 + 303 + Change line 74 from: 304 + ```typescript 305 + const config = loadConfig(configPath) 306 + ``` 307 + to: 308 + ```typescript 309 + const config = await loadConfig(configPath) 310 + ``` 311 + 312 + **Step 3: Verify it compiles** 313 + 314 + Run: `cd packages/hatk && npx tsc --noEmit src/test.ts` 315 + 316 + --- 317 + 318 + ### Task 5: Update vite-plugin.ts — change spawned server argument 319 + 320 + **Files:** 321 + - Modify: `packages/hatk/src/vite-plugin.ts:71` 322 + 323 + **Step 1: Change config.yaml to hatk.config.ts** 324 + 325 + Change line 71 from: 326 + ```typescript 327 + serverProcess = spawn('npx', ['tsx', 'watch', ...watchArgs, mainPath, 'config.yaml'], { 328 + ``` 329 + to: 330 + ```typescript 331 + serverProcess = spawn('npx', ['tsx', 'watch', ...watchArgs, mainPath, 'hatk.config.ts'], { 332 + ``` 333 + 334 + **Step 2: Verify it compiles** 335 + 336 + Run: `cd packages/hatk && npx tsc --noEmit src/vite-plugin.ts` 337 + 338 + --- 339 + 340 + ### Task 6: Add package export for defineConfig and HatkConfig 341 + 342 + **Files:** 343 + - Modify: `packages/hatk/package.json` 344 + 345 + **Step 1: Add config export to package.json exports field** 346 + 347 + Add this entry to the `"exports"` object: 348 + ```json 349 + "./config": "./dist/config.js", 350 + ``` 351 + 352 + This allows users to write `import { defineConfig } from 'hatk/config'` (or the package could also re-export from a root entry if one exists). 353 + 354 + **Note:** Since the package name is `@hatk/hatk` and users write `import { defineConfig } from 'hatk'` (which is a different package or alias), check how the project resolves this. The scaffolded `hatk.config.ts` uses `from 'hatk'` — if `hatk` is an alias for `@hatk/hatk`, the export should work. If not, the import path in the scaffold template may need to be `from '@hatk/hatk/config'`. 355 + 356 + --- 357 + 358 + ### Task 7: Update documentation 359 + 360 + **Files:** 361 + - Modify: `docs/site/src/content/docs/getting-started/configuration.mdx` 362 + 363 + **Step 1: Rewrite the docs page for TypeScript config** 364 + 365 + Replace the entire file with TypeScript-based configuration documentation. Change the YAML example to a TypeScript `defineConfig()` example. Update the frontmatter description. Keep all the options reference (relay, plc, port, database, etc.) and env var documentation intact. 366 + 367 + Key changes: 368 + - Title/description: reference `hatk.config.ts` instead of `config.yaml` 369 + - Complete example: TypeScript with `defineConfig()` instead of YAML 370 + - All option docs stay the same, just the format changes 371 + 372 + --- 373 + 374 + ### Task 8: Full build verification 375 + 376 + **Step 1: Run full TypeScript check** 377 + 378 + Run: `cd packages/hatk && npx tsc --noEmit` 379 + Expected: No errors. 380 + 381 + **Step 2: Search for any remaining config.yaml references** 382 + 383 + Run: `grep -r "config\.yaml" packages/hatk/src/` 384 + Expected: No results (test.ts still uses YAML for fixtures but doesn't reference `config.yaml` as a filename). 385 + 386 + **Step 3: Verify the build** 387 + 388 + Run: `cd packages/hatk && npm run build` 389 + Expected: Clean build, dist/ output.
+101
docs/superpowers/specs/2026-03-12-typescript-config-design.md
··· 1 + # Switch from config.yaml to hatk.config.ts 2 + 3 + ## Goal 4 + 5 + Replace `config.yaml` with `hatk.config.ts` for type safety, autocompletion, and single-language consistency. No YAML fallback. 6 + 7 + ## Design 8 + 9 + ### User-facing config file 10 + 11 + ```typescript 12 + // hatk.config.ts 13 + import { defineConfig } from 'hatk' 14 + 15 + export default defineConfig({ 16 + relay: 'ws://localhost:2583', 17 + port: 3000, 18 + database: 'data/hatk.db', 19 + collections: ['app.bsky.feed.post'], 20 + backfill: { 21 + parallelism: 10, 22 + fullNetwork: false, 23 + }, 24 + }) 25 + ``` 26 + 27 + ### `defineConfig` helper 28 + 29 + Identity function exported from the `hatk` package. Exists solely for type inference: 30 + 31 + ```typescript 32 + export function defineConfig(config: Partial<HatkConfig>): Partial<HatkConfig> { 33 + return config 34 + } 35 + ``` 36 + 37 + Takes `Partial<HatkConfig>` so users only specify what they need; defaults fill the rest. 38 + 39 + ### Loader (`config.ts`) 40 + 41 + `loadConfig` becomes async (dynamic import replaces `readFileSync` + YAML parse): 42 + 43 + ```typescript 44 + export async function loadConfig(configPath?: string): Promise<HatkConfig> { 45 + const resolved = resolve(configPath || 'hatk.config.ts') 46 + const configDir = dirname(resolved) 47 + const mod = await import(resolved) 48 + const raw: Partial<HatkConfig> = mod.default 49 + return applyDefaultsAndEnvOverrides(raw, configDir) 50 + } 51 + ``` 52 + 53 + - Relative paths still resolved against the config file's directory. 54 + - Environment variable overrides preserved (essential for container deployments). 55 + - Defaults unchanged from current behavior. 56 + 57 + ### Environment variable overrides 58 + 59 + Same as today. Env vars take precedence over the TS config values: 60 + 61 + | Env var | Config field | 62 + |---|---| 63 + | `DATABASE` | `database` | 64 + | `RELAY` | `relay` | 65 + | `DID_PLC_URL` | `plc` | 66 + | `PORT` | `port` | 67 + | `BACKFILL_REPOS` | `backfill.repos` | 68 + | `BACKFILL_FULL_NETWORK` | `backfill.fullNetwork` | 69 + | `BACKFILL_PARALLELISM` | `backfill.parallelism` | 70 + | `BACKFILL_FETCH_TIMEOUT` | `backfill.fetchTimeout` | 71 + | `BACKFILL_MAX_RETRIES` | `backfill.maxRetries` | 72 + | `FTS_REBUILD_INTERVAL` | `ftsRebuildInterval` | 73 + | `OAUTH_ISSUER` | `oauth.issuer` | 74 + | `ADMINS` | `admins` | 75 + 76 + ## Changes required 77 + 78 + ### `config.ts` 79 + - Remove `yaml` import and `readFileSync` 80 + - `loadConfig` becomes `async`, uses `await import()` instead of YAML parse 81 + - Add `defineConfig` export 82 + - Input type becomes `Partial<HatkConfig>` (users specify only what they need) 83 + 84 + ### Callers of `loadConfig` (4 files) 85 + - **`main.ts`** — already async context; change `loadConfig(configPath)` to `await loadConfig(configPath)`, update default from `'config.yaml'` to `'hatk.config.ts'` 86 + - **`cli.ts`** — change `loadConfig(resolve('config.yaml'))` to `await loadConfig(resolve('hatk.config.ts'))` (2 call sites) 87 + - **`test.ts`** — change to `await loadConfig(...)`, update fallback path 88 + - **`vite-plugin.ts`** — update spawned server argument from `'config.yaml'` to `'hatk.config.ts'` 89 + 90 + ### Package exports 91 + - Export `defineConfig` and `HatkConfig` from the package entry point so users can import them 92 + 93 + ### Dependencies 94 + - Remove `yaml` from `package.json` dependencies 95 + 96 + ### Documentation 97 + - Update `docs/site/src/content/docs/getting-started/configuration.mdx` to show TypeScript config instead of YAML 98 + 99 + ### Migration 100 + - No automatic migration. Clean break from YAML. 101 + - If `hatk.config.ts` is not found at startup, print a clear error message explaining the change.