/** * Help text and version display for the 1sat CLI. * * COMMANDS is the single source of truth. Both text help and `--json` * agent-discovery output are rendered from it. */ import { readFileSync } from 'node:fs' import chalk from 'chalk' const VERSION = (() => { try { const pkg = JSON.parse( readFileSync(new URL('../package.json', import.meta.url), 'utf8'), ) return pkg.version || '0.0.11' } catch { return '0.0.11' } })() export function getVersion(): string { return VERSION } export function printVersion(): void { console.log(`1sat ${getVersion()}`) } export interface ArgSpec { flag: string values?: string description?: string required?: boolean } export interface SubcommandSpec { name: string description: string positional?: string args?: ArgSpec[] unavailable?: boolean notes?: string } export interface CommandSpec { group: string name: string description: string positional?: string args?: ArgSpec[] subcommands?: SubcommandSpec[] notes?: string } export const GLOBAL_OPTIONS: ArgSpec[] = [ { flag: '--json', description: 'Output as JSON' }, { flag: '--quiet', description: 'Suppress output (also -q)' }, { flag: '--yes', description: 'Skip confirmation prompts (also -y)' }, { flag: '--chain', values: '', description: 'Network (default: main)', }, { flag: '--help', description: 'Show help (also -h)' }, { flag: '--version', description: 'Show version (also -v)' }, ] export const ENV_VARS: { name: string; description: string }[] = [ { name: 'PRIVATE_KEY_WIF', description: 'WIF private key (bypasses encrypted keyfile)', }, { name: 'ONESAT_PASSWORD', description: 'Password for encrypted keyfile' }, { name: 'ONESAT_PORT', description: 'Override server port (used by `1sat serve`)', }, { name: 'ONESAT_MCP_URL', description: 'Override wallet-desktop MCP URL (default http://127.0.0.1:3322)', }, ] export const COMMANDS: CommandSpec[] = [ // Setup { group: 'Setup', name: 'init', description: 'Interactive wallet setup wizard (creates encrypted keyfile and config)', }, { group: 'Setup', name: 'config', description: 'Manage CLI configuration in ~/.1sat/cli/config.json', subcommands: [ { name: 'show', description: 'Display current configuration' }, { name: 'set', description: 'Set a config value', positional: ' ', }, { name: 'unset', description: 'Remove a configuration key', positional: '', }, { name: 'path', description: 'Print config directory path' }, ], }, { group: 'Setup', name: 'remote', description: 'Manage remote wallet storage', subcommands: [ { name: 'add', description: 'Add a remote storage as backup', positional: '', }, { name: 'list', description: 'List configured remotes and active storage', }, { name: 'delete', description: 'Remove a remote from the backup list', positional: '', }, { name: 'set-active', description: 'Switch active storage to a remote or back to local', positional: '', }, { name: 'status', description: 'Fetch GET /account/status from a remote', positional: '[url]', }, { name: 'topup', description: 'Buy capacity on a remote', positional: '[url]', args: [{ flag: '--units', values: '' }], }, ], }, // Wallet { group: 'Wallet', name: 'wallet', description: 'Wallet operations (balance, send, BRC-100 interface)', subcommands: [ { name: 'balance', description: 'Show wallet balance in satoshis' }, { name: 'address', description: 'Show BRC-29 deposit address(es)', args: [ { flag: '--prefix', values: '

', description: 'BRC-29 prefix (default: 1sat)', }, { flag: '--start-index', values: '' }, { flag: '--count', values: '' }, ], }, { name: 'send', description: 'Send BSV. Specify exactly one of --to, --script, or --data-asm', args: [ { flag: '--to', values: '

' }, { flag: '--script', values: '' }, { flag: '--data-asm', values: '""' }, { flag: '--sats', values: '', description: 'Required with --to or --script', }, ], }, { name: 'send-all', description: 'Send all BSV to an address (empties the wallet)', args: [{ flag: '--to', values: '
', required: true }], }, { name: 'sync', description: 'Sync inbound payments at BRC-29 deposit addresses', args: [ { flag: '--prefix', values: '

' }, { flag: '--start-index', values: '' }, { flag: '--count', values: '' }, ], }, { name: 'info', description: 'Show address, identity key, balance, and network', }, { name: 'list-outputs', description: 'List wallet outputs in a basket (BRC-100)', args: [ { flag: '--basket', values: '', required: true }, { flag: '--tags', values: '' }, { flag: '--limit', values: '' }, { flag: '--include-tags' }, { flag: '--include', values: '' }, ], }, { name: 'relinquish-output', description: 'Remove output from basket (BRC-100)', args: [ { flag: '--basket', values: '', required: true }, { flag: '--output', values: '', required: true }, ], }, { name: 'list-actions', description: 'List wallet actions (BRC-100)', args: [ { flag: '--labels', values: '' }, { flag: '--limit', values: '' }, ], }, { name: 'create-action', description: 'Create a raw action (BRC-100)', positional: "''", }, { name: 'sign-action', description: 'Sign a raw action (BRC-100)', positional: "''", }, { name: 'abort-action', description: 'Abort a pending action (BRC-100)', args: [{ flag: '--reference', values: '', required: true }], }, { name: 'list-certificates', description: 'List certificates (BRC-100)', args: [ { flag: '--certifiers', values: '' }, { flag: '--types', values: '' }, { flag: '--limit', values: '' }, ], }, { name: 'relinquish-certificate', description: 'Relinquish a certificate (BRC-100)', args: [ { flag: '--type', values: '', required: true }, { flag: '--serialNumber', values: '', required: true }, { flag: '--certifier', values: '', required: true }, ], }, ], }, // Ordinals { group: 'Ordinals', name: 'ordinals', description: '1Sat Ordinal inscriptions', subcommands: [ { name: 'list', description: 'List owned ordinals/inscriptions' }, { name: 'mint', description: 'Mint a new ordinal inscription', args: [ { flag: '--file', values: '', required: true }, { flag: '--type', values: '', description: 'Override auto-detected MIME type', }, { flag: '--map', values: '', description: 'MAP metadata as JSON object', }, { flag: '--sign-with-bap', description: 'Sign inscription with BAP identity', }, ], }, { name: 'transfer', description: 'Transfer an ordinal', args: [ { flag: '--outpoint', values: '', required: true }, { flag: '--to', values: '

', required: true }, ], }, { name: 'sell', description: 'List an ordinal for sale (OrdLock)', args: [ { flag: '--outpoint', values: '', required: true }, { flag: '--price', values: '', required: true }, ], }, { name: 'cancel', description: 'Cancel an ordinal listing', args: [{ flag: '--outpoint', values: '', required: true }], }, { name: 'buy', description: 'Purchase a listed ordinal', args: [{ flag: '--outpoint', values: '', required: true }], }, { name: 'burn', description: 'Burn ordinals permanently', args: [ { flag: '--outpoints', values: '', required: true }, ], }, ], }, // Tokens { group: 'Tokens (BSV21)', name: 'tokens', description: 'BSV21 fungible tokens', subcommands: [ { name: 'balances', description: 'Show token balances by token ID' }, { name: 'list', description: 'List owned token UTXOs', args: [{ flag: '--token-id', values: '' }], }, { name: 'send', description: 'Transfer tokens', args: [ { flag: '--token-id', values: '', required: true }, { flag: '--amount', values: '', required: true }, { flag: '--to', values: '
' }, { flag: '--counterparty', values: '' }, { flag: '--locking-script', values: '' }, ], }, { name: 'deploy-mint', description: 'Deploy a new BSV21 token with fixed supply (deploy+mint)', args: [ { flag: '--symbol', values: '', required: true }, { flag: '--amount', values: '', required: true }, { flag: '--decimals', values: '<0-18>' }, { flag: '--icon', values: '' }, { flag: '--to', values: '
' }, { flag: '--counterparty', values: '' }, { flag: '--locking-script', values: '' }, ], }, { name: 'deploy-auth', description: 'Deploy a new BSV21 token with mintable supply via auth UTXOs (deploy+auth)', args: [ { flag: '--symbol', values: '', required: true }, { flag: '--decimals', values: '<0-18>' }, { flag: '--icon', values: '' }, { flag: '--to', values: '
' }, { flag: '--counterparty', values: '' }, { flag: '--locking-script', values: '' }, ], }, { name: 'mint', description: 'Spend an auth UTXO to mint new supply, re-issue authority, or burn it', args: [ { flag: '--token-id', values: '', required: true }, { flag: '--amount', values: '' }, { flag: '--to', values: '
' }, { flag: '--counterparty', values: '' }, { flag: '--locking-script', values: '' }, { flag: '--auth-to', values: '
' }, { flag: '--auth-counterparty', values: '' }, { flag: '--auth-locking-script', values: '' }, { flag: '--end-minting' }, ], }, { name: 'buy', description: 'Purchase listed tokens', args: [ { flag: '--outpoint', values: '', required: true }, { flag: '--token-id', values: '', required: true }, { flag: '--amount', values: '', required: true }, ], }, ], }, // Locks { group: 'Locks', name: 'locks', description: 'Time-locked BSV', subcommands: [ { name: 'info', description: 'Show locked totals and maturity status' }, { name: 'lock', description: 'Time-lock BSV until a block height', args: [ { flag: '--sats', values: '', required: true }, { flag: '--blocks', values: '', required: true, description: 'Target unlock block height', }, ], }, { name: 'unlock', description: 'Unlock all matured locks' }, ], }, // Identity { group: 'Identity (BAP)', name: 'identity', description: 'BAP (Bitcoin Attestation Protocol) identity management', subcommands: [ { name: 'create', description: 'Create/publish a BAP identity on-chain' }, { name: 'update-profile', description: 'Update BAP identity profile', args: [{ flag: '--profile', values: '', required: true }], }, { name: 'info', description: 'Show identity public key' }, { name: 'sign', description: 'Sign a message with identity key (BSM)', args: [ { flag: '--message', values: '', required: true }, { flag: '--encoding', values: '' }, ], }, { name: 'verify', description: 'Verify a signed message', unavailable: true, notes: 'Not yet implemented.', args: [ { flag: '--message', values: '', required: true }, { flag: '--sig', values: '', required: true }, { flag: '--address', values: '', required: true }, ], }, ], }, // Social { group: 'Social', name: 'social', description: 'On-chain social posts (BSocial)', subcommands: [ { name: 'post', description: 'Create an on-chain social post', args: [ { flag: '--content', values: '', required: true }, { flag: '--app', values: '' }, { flag: '--content-type', values: '', }, { flag: '--tags', values: '' }, ], }, ], }, // OpNS { group: 'OpNS', name: 'opns', description: 'Ordinals Name System — bind BAP identity to a name', subcommands: [ { name: 'register', description: 'Register identity on an OpNS name', args: [{ flag: '--outpoint', values: '', required: true }], }, { name: 'deregister', description: 'Deregister identity from an OpNS name', args: [{ flag: '--outpoint', values: '', required: true }], }, { name: 'lookup', description: 'List OpNS names from wallet' }, ], }, // Sweep { group: 'Sweep', name: 'sweep', description: 'Import assets from external private keys', subcommands: [ { name: 'scan', description: 'Scan an address for sweepable UTXOs (BSV, ordinals, BSV21)', args: [{ flag: '--wif', values: '', required: true }], }, { name: 'import', description: 'Sweep UTXOs from a WIF into the wallet', args: [{ flag: '--wif', values: '', required: true }], }, ], }, // Server { group: 'Server', name: 'serve', description: 'Run wallet HTTP server and/or monitor daemon (config under server.* in config.json)', subcommands: [ { name: '(no subcommand)', description: 'Wallet server + monitor daemon', }, { name: 'wallet', description: 'Wallet server only (BRC-100 HTTP)' }, { name: 'monitor', description: 'Monitor daemon only' }, { name: 'messagebox', description: 'BSV message-box server (port 8771 default; uses wallet identity)', }, ], }, // MCP { group: 'MCP', name: 'mcp-proxy', description: 'stdio JSON-RPC bridge to a running wallet-desktop MCP server (default http://127.0.0.1:3322). Performs BRC-31 handshake using ~/.1sat-wallet/mcp-agent.key.', }, // Advanced { group: 'Advanced', name: 'action', description: 'Execute any registered @1sat/actions action by name. Run with no args to list all actions.', positional: "[ ['']]", }, { group: 'Advanced', name: 'tx', description: 'Transaction utilities', subcommands: [ { name: 'decode', description: 'Decode a raw transaction hex', positional: '', }, ], }, // Help { group: 'Help', name: 'help', description: 'Show this help. Use --json for machine-readable output. Use `1sat help` for per-command help.', }, ] export function getCommand(name: string): CommandSpec | undefined { return COMMANDS.find((c) => c.name === name) } function formatArgUsage(arg: ArgSpec): string { const core = arg.values ? `${arg.flag} ${arg.values}` : arg.flag return arg.required ? core : `[${core}]` } function formatSubcommandLine(sub: SubcommandSpec): string { const parts: string[] = [] if (sub.positional) parts.push(sub.positional) if (sub.args) parts.push(...sub.args.map(formatArgUsage)) return parts.join(' ') } export function printHelp(json = false): void { if (json) { printHelpJson() return } printHelpText() } function printHelpJson(): void { const tree = COMMANDS.map((cmd) => ({ group: cmd.group, name: cmd.name, description: cmd.description, ...(cmd.positional ? { positional: cmd.positional } : {}), ...(cmd.args ? { args: cmd.args } : {}), ...(cmd.notes ? { notes: cmd.notes } : {}), ...(cmd.subcommands ? { subcommands: cmd.subcommands.map((s) => ({ name: s.name, description: s.description, ...(s.positional ? { positional: s.positional } : {}), ...(s.args ? { args: s.args } : {}), ...(s.unavailable ? { unavailable: true } : {}), ...(s.notes ? { notes: s.notes } : {}), })), } : {}), })) console.log( JSON.stringify( { name: '1sat', version: getVersion(), description: 'CLI for 1Sat Ordinals SDK', usage: '1sat [subcommand] [options]', configDir: '~/.1sat/cli/', globalOptions: GLOBAL_OPTIONS, envVars: ENV_VARS, commands: tree, }, null, 2, ), ) } function printHelpText(): void { const dim = chalk.dim const cyan = chalk.cyan const bold = chalk.bold const lines: string[] = [] lines.push('') lines.push(`${bold('1sat')} - CLI for 1Sat Ordinals SDK`) lines.push('') lines.push(bold('Usage:')) lines.push(' 1sat [subcommand] [options]') lines.push(' 1sat help Per-command help') lines.push(' 1sat help --json Machine-readable command tree') lines.push('') let currentGroup = '' for (const cmd of COMMANDS) { if (cmd.group !== currentGroup) { lines.push(bold(`${cmd.group}:`)) currentGroup = cmd.group } const head = cmd.subcommands ? `${cmd.name} ` : cmd.positional ? `${cmd.name} ${cmd.positional}` : cmd.name lines.push(` ${cyan(head.padEnd(28))} ${dim(cmd.description)}`) if (cmd.subcommands) { for (const sub of cmd.subcommands) { const usage = formatSubcommandLine(sub) const label = `${cmd.name} ${sub.name}${usage ? ` ${usage}` : ''}` const tag = sub.unavailable ? ' (unavailable)' : '' lines.push( ` ${cyan(label.padEnd(58))} ${dim(sub.description + tag)}`, ) } } lines.push('') } lines.push(bold('Global Options:')) for (const opt of GLOBAL_OPTIONS) { const usage = opt.values ? `${opt.flag} ${opt.values}` : opt.flag lines.push(` ${dim(usage.padEnd(26))} ${dim(opt.description ?? '')}`) } lines.push('') lines.push(bold('Environment Variables:')) for (const env of ENV_VARS) { lines.push(` ${dim(env.name.padEnd(26))} ${dim(env.description)}`) } lines.push('') lines.push(`${bold('Config:')} ~/.1sat/cli/`) lines.push('') console.log(lines.join('\n')) } export function printCommandHelp(commandName: string, json = false): void { const spec = getCommand(commandName) if (!spec) { if (json) { console.log(JSON.stringify({ error: `Unknown command: ${commandName}` })) } else { console.error(`Unknown command: ${commandName}`) } return } if (json) { console.log(JSON.stringify(spec, null, 2)) return } const bold = chalk.bold const cyan = chalk.cyan const dim = chalk.dim console.log() console.log(`${bold(`1sat ${spec.name}`)} - ${spec.description}`) if (spec.notes) console.log(dim(` ${spec.notes}`)) console.log() if (spec.subcommands) { console.log(bold('Subcommands:')) for (const sub of spec.subcommands) { const usage = formatSubcommandLine(sub) const label = `${sub.name}${usage ? ` ${usage}` : ''}` const tag = sub.unavailable ? ' (unavailable)' : '' console.log(` ${cyan(label.padEnd(56))} ${dim(sub.description + tag)}`) if (sub.notes) console.log(dim(` ${sub.notes}`)) } console.log() } if (spec.args && spec.args.length > 0) { console.log(bold('Options:')) for (const arg of spec.args) { const usage = formatArgUsage(arg) console.log(` ${cyan(usage.padEnd(36))} ${dim(arg.description ?? '')}`) } console.log() } }