diff --git a/README.md b/README.md index 93eb538..f23e159 100644 --- a/README.md +++ b/README.md @@ -1195,7 +1195,7 @@ Companion runtime helper: - `MacOSCompanionClient` (`platform: "macos"`, APNs push registration) - `IOSCompanionClient` (`platform: "ios"`, APNs push registration) - `AndroidCompanionClient` (`platform: "android"`, FCM push registration) - - shared `bootstrap()` helper (`register` + `getCapabilities`) for startup handshakes + - shared `bootstrap()` helper (`register` + `getCapabilities`, optional `system.capabilities`) for startup handshakes - shared `publishHeartbeat()` helper for periodic `node.status.set` updates with safe defaults - `createHeartbeatLoop()` convenience helper that returns a bound `CompanionHeartbeatLoop` - optional `defaultSessionId` for canvas helper calls so `sessionId` can be omitted per call diff --git a/docs/plans/state.json b/docs/plans/state.json index 7198a23..5143b72 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -500,6 +500,20 @@ ], "test_status": "pnpm test:run src/companion/heartbeatLoop.test.ts src/companion/runtimeClient.test.ts src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing" }, + "companion-platform-bootstrap-system-capabilities-option": { + "status": "completed", + "date": "2026-02-17", + "updated": "2026-02-17", + "summary": "Extended platform client `bootstrap()` to delegate to `runtime.bootstrapNode()` and optionally include `system.capabilities` in bootstrap responses.", + "files_modified": [ + "src/companion/platformClients.ts", + "src/companion/platformClients.test.ts", + "src/companion/index.ts", + "README.md", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts src/companion/heartbeatLoop.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing" + }, "browser-tools-activation-clarity": { "status": "completed", "date": "2026-02-17", diff --git a/src/companion/index.ts b/src/companion/index.ts index aa0bd1a..3193c24 100644 --- a/src/companion/index.ts +++ b/src/companion/index.ts @@ -49,6 +49,7 @@ export type { SharedStatusInput, HeartbeatStatusInput, PlatformBootstrapResult, + PlatformBootstrapOptions, PlatformPutCanvasArtifactInput, PlatformGetCanvasArtifactInput, PlatformDeleteCanvasArtifactInput, diff --git a/src/companion/platformClients.test.ts b/src/companion/platformClients.test.ts index 91df41a..f1cc913 100644 --- a/src/companion/platformClients.test.ts +++ b/src/companion/platformClients.test.ts @@ -12,6 +12,7 @@ function createRuntimeMock(): { disconnect: ReturnType; dispose: ReturnType; registerNode: ReturnType; + bootstrapNode: ReturnType; getNodeCapabilities: ReturnType; setNodeStatus: ReturnType; setNodeLocation: ReturnType; @@ -29,6 +30,14 @@ function createRuntimeMock(): { const disconnect = vi.fn(() => undefined); const dispose = vi.fn(() => undefined); const registerNode = vi.fn(async () => ({ registered: true })); + const bootstrapNode = vi.fn(async () => ({ + register: { registered: true }, + capabilities: { + node: { id: 'n1', role: 'companion', registeredAt: Date.now() }, + protocol: { serverVersion: 1, nodeVersion: 1, negotiatedVersion: 1 }, + capabilities: { declared: [], enabled: [], featureGates: {} }, + }, + })); const getNodeCapabilities = vi.fn(async () => ({ node: { id: 'n1', role: 'companion', registeredAt: Date.now() }, protocol: { serverVersion: 1, nodeVersion: 1, negotiatedVersion: 1 }, capabilities: { declared: [], enabled: [], featureGates: {} } })); const setNodeStatus = vi.fn(async () => ({ updated: true })); const setNodeLocation = vi.fn(async () => ({ updated: true })); @@ -47,6 +56,7 @@ function createRuntimeMock(): { disconnect, dispose, registerNode, + bootstrapNode, getNodeCapabilities, setNodeStatus, setNodeLocation, @@ -67,6 +77,7 @@ function createRuntimeMock(): { disconnect, dispose, registerNode, + bootstrapNode, getNodeCapabilities, setNodeStatus, setNodeLocation, @@ -170,14 +181,11 @@ describe('platform companion clients', () => { const result = await client.bootstrap(); - expect(mock.registerNode).toHaveBeenCalledOnce(); - expect(mock.getNodeCapabilities).toHaveBeenCalledOnce(); - expect(mock.registerNode.mock.invocationCallOrder[0]).toBeLessThan( - mock.getNodeCapabilities.mock.invocationCallOrder[0], - ); + expect(mock.bootstrapNode).toHaveBeenCalledOnce(); expect(result).toEqual({ register: { registered: true }, capabilities: expect.any(Object), + systemCapabilities: undefined, }); }); @@ -250,4 +258,36 @@ describe('platform companion clients', () => { }), ); }); + + it('bootstrap can request system capabilities snapshot', async () => { + const mock = createRuntimeMock(); + mock.bootstrapNode.mockResolvedValueOnce({ + register: { registered: true }, + capabilities: { + node: { id: 'n1', role: 'companion', registeredAt: Date.now() }, + protocol: { serverVersion: 1, nodeVersion: 1, negotiatedVersion: 1 }, + capabilities: { declared: [], enabled: [], featureGates: {} }, + }, + systemCapabilities: { + protocol: { version: 1 }, + nodes: { + enabled: true, + locationEnabled: true, + pushEnabled: true, + allowedRoles: ['companion'], + registered: true, + }, + featureGates: {}, + }, + }); + const client = new MacOSCompanionClient({ runtime: mock.runtime, nodeId: 'mac-node' }); + + const result = await client.bootstrap({ includeSystemCapabilities: true }); + + expect(mock.bootstrapNode).toHaveBeenCalledWith( + expect.objectContaining({ nodeId: 'mac-node' }), + { includeSystemCapabilities: true }, + ); + expect(result.systemCapabilities?.nodes.enabled).toBe(true); + }); }); diff --git a/src/companion/platformClients.ts b/src/companion/platformClients.ts index 5815010..9223378 100644 --- a/src/companion/platformClients.ts +++ b/src/companion/platformClients.ts @@ -56,6 +56,11 @@ export interface HeartbeatStatusInput { export interface PlatformBootstrapResult { register: NodeRegisterResult; capabilities: NodeCapabilitiesResult; + systemCapabilities?: SystemCapabilitiesResult; +} + +export interface PlatformBootstrapOptions { + includeSystemCapabilities?: boolean; } export interface PlatformPutCanvasArtifactInput extends Omit { @@ -108,10 +113,21 @@ export class MacOSCompanionClient { }); } - async bootstrap(): Promise { - const register = await this.register(); - const capabilities = await this.getCapabilities(); - return { register, capabilities }; + async bootstrap(options?: PlatformBootstrapOptions): Promise { + const result = await this.runtime.bootstrapNode( + { + nodeId: this.nodeId, + role: this.role, + protocolVersion: this.protocolVersion, + capabilities: this.capabilities, + }, + { includeSystemCapabilities: options?.includeSystemCapabilities }, + ); + return { + register: result.register, + capabilities: result.capabilities, + systemCapabilities: result.systemCapabilities, + }; } getCapabilities(): Promise { @@ -248,10 +264,21 @@ export class IOSCompanionClient { }); } - async bootstrap(): Promise { - const register = await this.register(); - const capabilities = await this.getCapabilities(); - return { register, capabilities }; + async bootstrap(options?: PlatformBootstrapOptions): Promise { + const result = await this.runtime.bootstrapNode( + { + nodeId: this.nodeId, + role: this.role, + protocolVersion: this.protocolVersion, + capabilities: this.capabilities, + }, + { includeSystemCapabilities: options?.includeSystemCapabilities }, + ); + return { + register: result.register, + capabilities: result.capabilities, + systemCapabilities: result.systemCapabilities, + }; } getCapabilities(): Promise { @@ -388,10 +415,21 @@ export class AndroidCompanionClient { }); } - async bootstrap(): Promise { - const register = await this.register(); - const capabilities = await this.getCapabilities(); - return { register, capabilities }; + async bootstrap(options?: PlatformBootstrapOptions): Promise { + const result = await this.runtime.bootstrapNode( + { + nodeId: this.nodeId, + role: this.role, + protocolVersion: this.protocolVersion, + capabilities: this.capabilities, + }, + { includeSystemCapabilities: options?.includeSystemCapabilities }, + ); + return { + register: result.register, + capabilities: result.capabilities, + systemCapabilities: result.systemCapabilities, + }; } getCapabilities(): Promise {