feat(companion): add build-and-verify bundle automation pipeline
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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 <dir> [options]',
|
||||
'',
|
||||
'Options:',
|
||||
' --output <dir> Required output directory',
|
||||
' --url <ws-url> Gateway URL (default: ws://127.0.0.1:18800)',
|
||||
' --token <token> Gateway token',
|
||||
' --platform <platform> macos|ios|android|linux|windows|unknown (default: macos)',
|
||||
' --node-id <id> Node ID (default: <platform>-<hostname>-<suffix>)',
|
||||
' --role <role> Node role (default: companion)',
|
||||
' --capability <list> Comma-delimited capabilities',
|
||||
' --heartbeat <seconds> Heartbeat interval (default: 30)',
|
||||
' --handoff-timeout <ms> Handoff timeout (default: 120000)',
|
||||
' --signing-key <pem-path> Optional private-key PEM path',
|
||||
' --signing-key-id <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<void> {
|
||||
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;
|
||||
});
|
||||
Reference in New Issue
Block a user