import { readFile } from 'node:fs/promises'; import { hostname } from 'node:os'; import { randomUUID } from 'node:crypto'; import { buildAndVerifyCompanionReleaseBundle } from '../src/companion/index.js'; type Platform = 'macos' | 'ios' | 'android' | 'linux' | 'windows' | 'unknown'; function usage(): string { return [ 'Usage:', ' pnpm companion:bundle -- --output [options]', '', 'Options:', ' --output Required output directory', ' --url Gateway URL (default: ws://127.0.0.1:18800)', ' --token Gateway token', ' --platform macos|ios|android|linux|windows|unknown (default: macos)', ' --node-id Node ID (default: --)', ' --role Node role (default: companion)', ' --capability Comma-delimited capabilities', ' --heartbeat Heartbeat interval (default: 30)', ' --handoff-timeout Handoff timeout (default: 120000)', ' --signing-key Optional private-key PEM path', ' --signing-key-id Optional signing key identifier', ].join('\n'); } function getArg(args: string[], name: string): string | undefined { const index = args.indexOf(name); if (index < 0) { return undefined; } return args[index + 1]; } function requireArg(args: string[], name: string): string { const value = getArg(args, name); if (!value || value.startsWith('--')) { throw new Error(`Missing value for ${name}`); } return value; } function parseIntArg(value: string, name: string): number { const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed <= 0) { throw new Error(`${name} must be a positive integer`); } return parsed; } function resolveDefaultCapabilities(platform: Platform): string[] { if (platform === 'ios' || platform === 'macos' || platform === 'android') { return ['ui.canvas', 'node.status.write', 'node.location.write', 'node.push.register']; } return ['ui.canvas', 'node.status.write']; } async function main(): Promise { const args = process.argv.slice(2); if (args.includes('--help') || args.includes('-h')) { console.log(usage()); return; } const outputDir = requireArg(args, '--output'); const url = getArg(args, '--url') ?? 'ws://127.0.0.1:18800'; const token = getArg(args, '--token'); const platformRaw = (getArg(args, '--platform') ?? 'macos') as Platform; if (!['macos', 'ios', 'android', 'linux', 'windows', 'unknown'].includes(platformRaw)) { throw new Error(`Unsupported platform: ${platformRaw}`); } const role = getArg(args, '--role') ?? 'companion'; const nodeId = getArg(args, '--node-id') ?? `${platformRaw}-${hostname()}-${randomUUID().slice(0, 8)}`; const heartbeatSeconds = parseIntArg(getArg(args, '--heartbeat') ?? '30', '--heartbeat'); const handoffTimeoutMs = parseIntArg(getArg(args, '--handoff-timeout') ?? '120000', '--handoff-timeout'); const capabilities = (getArg(args, '--capability') ?? '') .split(',') .map((value) => value.trim()) .filter(Boolean); const resolvedCapabilities = capabilities.length > 0 ? capabilities : resolveDefaultCapabilities(platformRaw); const signingKeyPath = getArg(args, '--signing-key'); const signingKeyId = getArg(args, '--signing-key-id'); const signingKeyPem = signingKeyPath ? await readFile(signingKeyPath, 'utf8') : undefined; const result = await buildAndVerifyCompanionReleaseBundle({ outputDir, gatewayUrl: url, gatewayToken: token, nodeId, role, platform: platformRaw, capabilities: resolvedCapabilities, heartbeatSeconds, handoffTimeoutMs, autoReconnect: true, signingKeyPem, signingKeyId: signingKeyId ?? undefined, }); console.log(`Companion bundle created and verified: ${outputDir}`); console.log(`- node_id: ${result.manifest.node.nodeId}`); console.log(`- checksums: ${result.bundle.checksumsPath}`); if (result.bundle.signaturePath) { console.log(`- signature: ${result.bundle.signaturePath}`); } console.log(`- files verified: ${result.verification.verifiedFiles.length}`); } main().catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error); console.error(`companion:bundle failed: ${message}`); process.exitCode = 1; });