#!/usr/bin/env bun /** * Single-transport launcher (language-server style). * * A launcher that must be started over exactly one transport. dreamcli has no * native "mutually exclusive" / "exactly one of" modifier, so the rule is * expressed in `.derive()` — the resolution-time hook that runs after flags * resolve and before the action. The derived, fully typed `Transport` is then * handed to `.action()` through `ctx`. * * Demonstrates: an "exactly one of" rule via `.derive()` throwing `CLIError`, * declarative numeric constraints (`flag.number({ int: true, min: 1, max: 65535 })`), * a kebab-case flag name accessed with bracket notation (`flags['node-ipc']`), * and passing a typed result to the action through derived context. * * Usage: * npx tsx examples/transport-launcher.ts --stdio * npx tsx examples/transport-launcher.ts --node-ipc * npx tsx examples/transport-launcher.ts --socket 6009 * npx tsx examples/transport-launcher.ts # error: pick exactly one * npx tsx examples/transport-launcher.ts --stdio --node-ipc # error: only one * npx tsx examples/transport-launcher.ts --help */ import { CLIError, cli, command, flag } from '@kjanat/dreamcli'; // One resolved transport — illegal states are unrepresentable. type Transport = | { readonly kind: 'stdio' } | { readonly kind: 'node-ipc' } | { readonly kind: 'socket'; readonly port: number }; const launcher = command('lsp-server') .description('Launch the language server over exactly one transport') .flag('stdio', flag.boolean().describe('Use the stdio transport')) .flag('node-ipc', flag.boolean().describe('Use the Node IPC transport')) .flag( 'socket', flag.number({ int: true, min: 1, max: 65535 }).describe('Use a TCP socket on '), ) // `.derive()` runs after resolution and before the action — the idiomatic // place for a constraint dreamcli does not express declaratively. .derive(({ flags }): { transport: Transport } => { const selected: Transport[] = []; if (flags.stdio) selected.push({ kind: 'stdio' }); // Kebab-case flag names stay kebab in the handler: bracket access, no camelCasing. if (flags['node-ipc']) selected.push({ kind: 'node-ipc' }); // Port shape (integer, 1-65535) is already enforced declaratively by // the flag's numeric constraints before `.derive()` runs. if (flags.socket !== undefined) { selected.push({ kind: 'socket', port: flags.socket }); } const [transport, ...rest] = selected; if (transport === undefined) { throw new CLIError('No transport selected.', { code: 'NO_TRANSPORT', suggest: 'Pass exactly one of --stdio, --node-ipc, or --socket ', }); } if (rest.length > 0) { throw new CLIError('Choose exactly one transport flag.', { code: 'TOO_MANY_TRANSPORTS', suggest: 'Use only one of --stdio, --node-ipc, or --socket ', }); } return { transport }; }) .action(({ ctx, out }) => { // ctx.transport: Transport — fully typed, ready to hand to the server. switch (ctx.transport.kind) { case 'stdio': out.log('Starting language server over stdio'); break; case 'node-ipc': out.log('Starting language server over node-ipc'); break; case 'socket': out.log(`Starting language server over socket :${ctx.transport.port}`); break; } }); void cli('lsp-server').default(launcher).run();