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;
});