#!/usr/bin/env node import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' import { RevolutionaryTypedFetch } from './core/typed-fetch.js' import type { TypedFetchConfig } from './types/config.js' import type { TypeSnapshotOptions } from './discovery/type-generator.js' interface ParsedArgs { command: string base?: string out?: string namespace?: string banner?: string config?: string verbose?: boolean } const SHORT_FLAGS: Record = { b: 'base', o: 'out', n: 'namespace', c: 'config' } async function main(): Promise { const parsed = parseArgs(process.argv.slice(2)) const command = parsed.command || 'sync' switch (command) { case 'sync': await handleSync(parsed) break case 'help': case '--help': printHelp() break default: console.error(`Unknown command: ${command}`) printHelp() process.exitCode = 1 } } function parseArgs(argv: string[]): ParsedArgs { const args: ParsedArgs = { command: '' } const positionals: string[] = [] for (let i = 0; i < argv.length; i++) { const token = argv[i]! if (!token.startsWith('-')) { positionals.push(token) continue } if (token.startsWith('--')) { const segment = token.slice(2) const [rawFlag, maybeValue] = segment.split('=', 2) const flag = rawFlag ?? '' if (!flag) continue if (flag.startsWith('no-')) { ;(args as any)[flag.slice(3)] = false continue } if (maybeValue !== undefined) { ;(args as any)[flag] = maybeValue continue } const next = argv[i + 1] if (!next || next.startsWith('-')) { ;(args as any)[flag] = true } else { ;(args as any)[flag] = next i++ } continue } const short = token.replace(/^-+/, '') const mapped = SHORT_FLAGS[short] if (mapped) { const next = argv[i + 1] if (!next || next.startsWith('-')) { throw new Error(`Flag -${short} requires a value`) } ;(args as any)[mapped] = next i++ } } args.command = positionals[0] || args.command || 'sync' if (!args.base && positionals[1]) { args.base = positionals[1] } if (!args.out && positionals[2]) { args.out = positionals[2] } return args } async function handleSync(args: ParsedArgs): Promise { const config = await loadConfig(args.config) const client = new RevolutionaryTypedFetch(config) const baseURL = args.base || config?.request?.baseURL if (!baseURL) { throw new Error('A base URL is required. Pass --base or set request.baseURL in your config file.') } if (args.verbose) { console.log(`🔍 Discovering schema from ${baseURL}`) } await client.discover(baseURL) const snapshotOptions: TypeSnapshotOptions = {} if (args.out) snapshotOptions.outFile = args.out if (args.namespace) snapshotOptions.namespace = args.namespace if (args.banner) snapshotOptions.banner = args.banner const code = await client.exportTypes(snapshotOptions) if (args.out) { if (args.verbose) { console.log(`📄 Type snapshot written to ${resolve(args.out)}`) } else { console.log(`📄 Wrote ${args.out}`) } } else { process.stdout.write(code) } } async function loadConfig(path?: string): Promise { if (!path) return undefined const resolved = resolve(path) const raw = await readFile(resolved, 'utf8') return JSON.parse(raw) } function printHelp(): void { console.log(`typedfetch [options] Commands: sync [--base ] [--out ] Discover the API and emit TypeScript definitions help Show this help message Options: --base, -b Base URL to discover (falls back to config request.baseURL) --out, -o File path for the generated declaration file (prints to stdout if omitted) --namespace, -n Wrap declarations in a namespace --banner Add a custom banner comment to the snapshot --config, -c Path to a JSON config file matching TypedFetchConfig --verbose Print progress information `) } main().catch((error) => { console.error(error instanceof Error ? error.message : error) process.exitCode = 1 })