import { mkdir, writeFile } from 'node:fs/promises'; import type { CompanionBootstrapManifest } from './bootstrapManifest.js'; export interface GenerateMacOSMenuBarAppInput { outputDir: string; manifest: CompanionBootstrapManifest; } export interface GenerateMacOSMenuBarAppResult { outputDir: string; files: string[]; } function packageSwift(): string { return `// swift-tools-version: 5.9 import PackageDescription let package = Package( name: "FlynnCompanionMenuBar", platforms: [ .macOS(.v13) ], products: [ .executable(name: "FlynnCompanionMenuBar", targets: ["FlynnCompanionMenuBar"]) ], targets: [ .executableTarget( name: "FlynnCompanionMenuBar", resources: [.copy("Resources/companion.bootstrap.json")] ) ] ) `; } function readmeBody(): string { return `# Flynn macOS Companion Menu Bar App (Reference MVP) This is a runnable Swift Package reference app that creates a macOS menu bar companion controller. ## Capabilities - Loads companion.bootstrap.json from package resources. - Starts/stops flynn companion as a child process from menu actions. - Runs one-shot handoff prompts (flynn companion --once --handoff ...). - Shows process state in the menu bar title (F, F* running, F! error). ## Build + Run Run: cd swift run FlynnCompanionMenuBar Prerequisites: - macOS with Swift toolchain. - flynn executable available in PATH. `; } function mainSwift(manifest: CompanionBootstrapManifest): string { return `import AppKit import Foundation struct CompanionBootstrap: Codable { struct Gateway: Codable { let url: String let token: String? } struct Node: Codable { let nodeId: String let role: String let platform: String let capabilities: [String] } struct Runtime: Codable { let heartbeatSeconds: Int let handoffTimeoutMs: Int let autoReconnect: Bool } let schemaVersion: Int let generatedAt: String let gateway: Gateway let node: Node let runtime: Runtime } final class CompanionProcessController { private var process: Process? private var isRunning = false private(set) var lastError: String? private let bootstrap: CompanionBootstrap init(bootstrap: CompanionBootstrap) { self.bootstrap = bootstrap } func start() { guard process == nil else { return } let proc = Process() proc.executableURL = URL(fileURLWithPath: "/usr/bin/env") var args: [String] = [ "flynn", "companion", "--url", bootstrap.gateway.url, "--node-id", bootstrap.node.nodeId, "--role", bootstrap.node.role, "--platform", bootstrap.node.platform, "--heartbeat", String(bootstrap.runtime.heartbeatSeconds), "--handoff-timeout", String(bootstrap.runtime.handoffTimeoutMs), ] if let token = bootstrap.gateway.token, !token.isEmpty { args += ["--token", token] } for capability in bootstrap.node.capabilities { args += ["--capability", capability] } if !bootstrap.runtime.autoReconnect { args.append("--once") } proc.arguments = args proc.terminationHandler = { [weak self] p in DispatchQueue.main.async { self?.process = nil self?.isRunning = false if p.terminationStatus != 0 { self?.lastError = "companion exited with status \\(p.terminationStatus)" } } } do { try proc.run() process = proc isRunning = true lastError = nil } catch { lastError = error.localizedDescription process = nil isRunning = false } } func stop() { process?.terminate() process = nil isRunning = false } func runHandoff(message: String) { guard !message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } let proc = Process() proc.executableURL = URL(fileURLWithPath: "/usr/bin/env") var args: [String] = [ "flynn", "companion", "--once", "--url", bootstrap.gateway.url, "--node-id", bootstrap.node.nodeId, "--role", bootstrap.node.role, "--platform", bootstrap.node.platform, "--handoff-timeout", String(bootstrap.runtime.handoffTimeoutMs), "--handoff", message, ] if let token = bootstrap.gateway.token, !token.isEmpty { args += ["--token", token] } proc.arguments = args do { try proc.run() } catch { lastError = "handoff failed: \\(error.localizedDescription)" } } var running: Bool { isRunning } } final class AppDelegate: NSObject, NSApplicationDelegate { private var statusItem: NSStatusItem! private var startItem: NSMenuItem! private var stopItem: NSMenuItem! private var handoffItem: NSMenuItem! private var errorItem: NSMenuItem! private var controller: CompanionProcessController! private var refreshTimer: Timer? func applicationDidFinishLaunching(_ notification: Notification) { let bootstrap = Self.loadBootstrap() controller = CompanionProcessController(bootstrap: bootstrap) statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) let menu = NSMenu() startItem = NSMenuItem(title: "Start Companion", action: #selector(startCompanion), keyEquivalent: "s") stopItem = NSMenuItem(title: "Stop Companion", action: #selector(stopCompanion), keyEquivalent: "x") handoffItem = NSMenuItem(title: "Send Handoff...", action: #selector(sendHandoff), keyEquivalent: "h") errorItem = NSMenuItem(title: "Last error: (none)", action: nil, keyEquivalent: "") errorItem.isEnabled = false menu.addItem(startItem) menu.addItem(stopItem) menu.addItem(handoffItem) menu.addItem(NSMenuItem.separator()) menu.addItem(errorItem) menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "Quit", action: #selector(quitApp), keyEquivalent: "q")) startItem.target = self stopItem.target = self handoffItem.target = self statusItem.menu = menu refreshUI() refreshTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in self?.refreshUI() } } func applicationWillTerminate(_ notification: Notification) { refreshTimer?.invalidate() controller.stop() } @objc private func startCompanion() { controller.start() refreshUI() } @objc private func stopCompanion() { controller.stop() refreshUI() } @objc private func sendHandoff() { let alert = NSAlert() alert.messageText = "Companion Handoff" alert.informativeText = "Send a one-shot handoff message through Flynn companion." let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24)) input.placeholderString = "message" alert.accessoryView = input alert.addButton(withTitle: "Send") alert.addButton(withTitle: "Cancel") let response = alert.runModal() if response == .alertFirstButtonReturn { controller.runHandoff(message: input.stringValue) refreshUI() } } @objc private func quitApp() { NSApplication.shared.terminate(nil) } private func refreshUI() { if controller.running { statusItem.button?.title = "F*" startItem.isEnabled = false stopItem.isEnabled = true } else { statusItem.button?.title = controller.lastError == nil ? "F" : "F!" startItem.isEnabled = true stopItem.isEnabled = false } let errorText = controller.lastError ?? "(none)" errorItem.title = "Last error: \\(errorText)" } static func loadBootstrap() -> CompanionBootstrap { guard let url = Bundle.module.url(forResource: "companion.bootstrap", withExtension: "json"), let data = try? Data(contentsOf: url), let decoded = try? JSONDecoder().decode(CompanionBootstrap.self, from: data) else { fatalError("Failed to load companion.bootstrap.json resource") } return decoded } } let app = NSApplication.shared let delegate = AppDelegate() app.setActivationPolicy(.accessory) app.delegate = delegate app.run() `; } export async function generateMacOSMenuBarApp( input: GenerateMacOSMenuBarAppInput, ): Promise { const packagePath = `${input.outputDir}/Package.swift`; const sourceDir = `${input.outputDir}/Sources/FlynnCompanionMenuBar`; const sourcePath = `${sourceDir}/main.swift`; const resourcesDir = `${sourceDir}/Resources`; const resourceBootstrapPath = `${resourcesDir}/companion.bootstrap.json`; const readmePath = `${input.outputDir}/README.md`; await mkdir(sourceDir, { recursive: true }); await mkdir(resourcesDir, { recursive: true }); await writeFile(packagePath, packageSwift(), 'utf8'); await writeFile(sourcePath, mainSwift(input.manifest), 'utf8'); await writeFile(resourceBootstrapPath, `${JSON.stringify(input.manifest, null, 2)}\n`, 'utf8'); await writeFile(readmePath, readmeBody(), 'utf8'); return { outputDir: input.outputDir, files: [packagePath, sourcePath, resourceBootstrapPath, readmePath], }; }