feat(companion): add generated macos ios android reference app surfaces

This commit is contained in:
William Valentin
2026-02-26 19:37:28 -08:00
parent 3839c04a7d
commit be8b1f29a4
22 changed files with 419 additions and 5 deletions
+6
View File
@@ -14,6 +14,7 @@ export { writeCompanionReleaseBundle } from './releaseBundle.js';
export { writeCompanionShellTemplate } from './shellTemplate.js';
export { verifyCompanionReleaseBundle } from './releaseVerify.js';
export { buildAndVerifyCompanionReleaseBundle } from './releasePipeline.js';
export { generateReferenceCompanionApps } from './referenceApps.js';
export type {
CompanionRuntimeClientOptions,
@@ -98,3 +99,8 @@ export type {
BuildAndVerifyCompanionReleaseBundleInput,
BuildAndVerifyCompanionReleaseBundleResult,
} from './releasePipeline.js';
export type {
GenerateReferenceCompanionAppsInput,
GenerateReferenceCompanionAppsResult,
GeneratedReferenceCompanionApp,
} from './referenceApps.js';
+30
View File
@@ -0,0 +1,30 @@
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { describe, expect, it } from 'vitest';
import { generateReferenceCompanionApps } from './referenceApps.js';
describe('generateReferenceCompanionApps', () => {
it('writes macos/ios/android reference app templates', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'flynn-reference-apps-'));
const outputDir = join(tempDir, 'apps');
const result = await generateReferenceCompanionApps({
outputDir,
gatewayUrl: 'ws://127.0.0.1:18800',
generatedAt: new Date('2026-02-27T00:00:00.000Z'),
});
expect(result.generated.map((entry) => entry.platform)).toEqual(['macos', 'ios', 'android']);
const macosTemplate = await readFile(`${outputDir}/macos/MenuBarCompanion.swift`, 'utf8');
const iosTemplate = await readFile(`${outputDir}/ios/CompanionBootstrap.swift`, 'utf8');
const androidTemplate = await readFile(`${outputDir}/android/CompanionBootstrap.kt`, 'utf8');
const rootReadme = await readFile(`${outputDir}/README.md`, 'utf8');
expect(macosTemplate).toContain('launchFlynnCompanion');
expect(iosTemplate).toContain('CompanionBootstrap');
expect(androidTemplate).toContain('data class CompanionBootstrap');
expect(rootReadme).toContain('Companion Reference Apps');
await rm(tempDir, { recursive: true, force: true });
});
});
+86
View File
@@ -0,0 +1,86 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { writeCompanionShellTemplate } from './shellTemplate.js';
import type { CompanionBootstrapPlatform } from './bootstrapManifest.js';
export interface GenerateReferenceCompanionAppsInput {
outputDir: string;
gatewayUrl: string;
generatedAt?: Date;
}
export interface GeneratedReferenceCompanionApp {
platform: 'macos' | 'ios' | 'android';
outputDir: string;
files: string[];
}
export interface GenerateReferenceCompanionAppsResult {
rootDir: string;
generated: GeneratedReferenceCompanionApp[];
readmePath: string;
}
function defaultCapabilities(platform: CompanionBootstrapPlatform): 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'];
}
function rootReadmeBody(): string {
return `# Companion Reference Apps
This directory contains generated companion starter shells for:
- macOS menu-bar style wrapper
- iOS shell
- Android shell
These are reference starters, not production binaries. Use them as a baseline for app packaging and distribution workflows.
`;
}
export async function generateReferenceCompanionApps(
input: GenerateReferenceCompanionAppsInput,
): Promise<GenerateReferenceCompanionAppsResult> {
await mkdir(input.outputDir, { recursive: true });
const generatedAt = input.generatedAt ?? new Date();
const generated: GeneratedReferenceCompanionApp[] = [];
for (const platform of ['macos', 'ios', 'android'] as const) {
const outDir = `${input.outputDir}/${platform}`;
const result = await writeCompanionShellTemplate({
outputDir: outDir,
platform,
manifest: {
schemaVersion: 1,
generatedAt: generatedAt.toISOString(),
gateway: {
url: input.gatewayUrl,
},
node: {
nodeId: `${platform}-reference-shell`,
role: 'companion',
platform,
capabilities: defaultCapabilities(platform),
},
runtime: {
heartbeatSeconds: 30,
handoffTimeoutMs: 120000,
autoReconnect: true,
},
},
});
generated.push({
platform,
outputDir: result.outputDir,
files: result.files,
});
}
const readmePath = `${input.outputDir}/README.md`;
await writeFile(readmePath, rootReadmeBody(), 'utf8');
return {
rootDir: input.outputDir,
generated,
readmePath,
};
}