TypeFetched/src/cli.ts

163 lines
4.2 KiB
JavaScript

#!/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<string, keyof ParsedArgs> = {
b: 'base',
o: 'out',
n: 'namespace',
c: 'config'
}
async function main(): Promise<void> {
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<void> {
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<TypedFetchConfig | undefined> {
if (!path) return undefined
const resolved = resolve(path)
const raw = await readFile(resolved, 'utf8')
return JSON.parse(raw)
}
function printHelp(): void {
console.log(`typedfetch <command> [options]
Commands:
sync [--base <url>] [--out <file>] 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
})