export type NativeServiceBackendKind = "systemd" | "launchd"; export type NativeServiceMode = "production" | "development"; export type NativeServiceId = "sessiond" | "web" | "uiDev"; export type ProductionNativeServiceId = Extract; export type NativeServiceShellName = "bash" | "zsh" | "fish"; export type NativeServiceRestartPolicy = "on-failure" | "never"; export interface NativeServiceBackend { kind: NativeServiceBackendKind; label: string; } export interface NativeServiceShell { name: NativeServiceShellName; executable: string; source: "detected" | "fallback"; detectedExecutable: string | null; } export interface NativeServiceManagerRef { systemdName: string; launchdLabel: string; launchdPlistName: string; logName: string; } export type NativeServiceCommandStrategy = | { kind: "configured-override"; command: string; verification: "unverified"; } | { kind: "named-command"; command: string; selectedBy: "authoritative-backend-probe"; } | { kind: "bundled-entrypoint"; command: "node"; entrypointPath: string; namedCommand: string; namedCommandFailure: string | null; } | { kind: "development-npm-script"; script: string; } | { kind: "development-npm-script-group"; scripts: readonly string[]; interpreter: "bash"; }; export type NativeServicePrerequisite = | { id: string; kind: "command-available"; command: string; description: string; } | { id: string; kind: "node-version"; command: "node"; minimumMajor: number; description: string; } | { id: string; kind: "readable-file"; path: string; description: string; } | { id: string; kind: "package-scripts"; packageJsonPath: string; scripts: readonly string[]; description: string; }; export interface NativeServicePlanService { id: NativeServiceId; manager: NativeServiceManagerRef; description: string; shellCommand: string; strategy: NativeServiceCommandStrategy; restart: NativeServiceRestartPolicy; environment: Readonly>; workingDirectory: string | null; after: readonly NativeServiceId[]; wants: readonly NativeServiceId[]; prerequisites: readonly NativeServicePrerequisite[]; } export interface NativeServicePlan { mode: NativeServiceMode; backend: NativeServiceBackend; shell: NativeServiceShell; services: readonly NativeServicePlanService[]; } export interface NativeServiceProbeRequest { purpose: "executable-selection" | "plan-validation"; backend: NativeServiceBackend; shell: NativeServiceShell; environment: Readonly>; workingDirectory: string | null; prerequisites: readonly NativeServicePrerequisite[]; } export interface NativeServicePrerequisiteOutcome { prerequisiteId: string; status: "satisfied" | "unsatisfied"; detail: string | null; } export type NativeServiceProbeResult = | { kind: "completed"; outcomes: readonly NativeServicePrerequisiteOutcome[]; } | { kind: "infrastructure-failure"; message: string; }; /** * Runs requirements in the real native service-manager context represented by * the request. Implementations must not treat the caller shell or a simulated * `env -i` environment as authoritative. Timeouts, manager failures, malformed * output, and cleanup failures are infrastructure failures; a missing command * is a completed probe with an unsatisfied outcome. */ export interface NativeServiceAuthoritativeProbe { run(request: NativeServiceProbeRequest): Promise; } export interface ProductionNativeServiceExecutableInput { configuredCommand: string | undefined; namedCommand: string; bundledEntrypointPath: string; } export interface ProductionNativeServicePlanInput { backend: NativeServiceBackend; shell: NativeServiceShell; environment: Readonly>; executables: Readonly>; } export interface DevelopmentNativeServicePlanInput { backend: NativeServiceBackend; shell: NativeServiceShell; environment: Readonly>; workingDirectory: string; packageJsonPath: string; } export interface NativeServicePlanDependencies { probe: NativeServiceAuthoritativeProbe; fileExists(path: string): boolean; } export type NativeServicePlanFailure = | { kind: "probe-infrastructure"; serviceIds: readonly ProductionNativeServiceId[]; message: string; } | { kind: "entrypoint-inspection-failure"; serviceId: ProductionNativeServiceId; entrypointPath: string; message: string; } | { kind: "executable-unavailable"; serviceId: ProductionNativeServiceId; namedCommand: string; namedCommandFailure: string | null; bundledEntrypointPath: string; }; export type NativeServicePlanResolution = | { ok: true; plan: NativeServicePlan } | { ok: false; failures: readonly NativeServicePlanFailure[] }; const nativeServiceRefs: Readonly> = { sessiond: { systemdName: "pi-web-sessiond.service", launchdLabel: "com.pi-web.sessiond", launchdPlistName: "com.pi-web.sessiond.plist", logName: "sessiond.log", }, web: { systemdName: "pi-web.service", launchdLabel: "com.pi-web.web", launchdPlistName: "com.pi-web.web.plist", logName: "web.log", }, uiDev: { systemdName: "pi-web-ui-dev.service", launchdLabel: "com.pi-web.ui-dev", launchdPlistName: "com.pi-web.ui-dev.plist", logName: "ui-dev.log", }, }; const productionServiceIds = ["sessiond", "web"] as const satisfies readonly ProductionNativeServiceId[]; export async function resolveProductionNativeServicePlan( input: ProductionNativeServicePlanInput, dependencies: NativeServicePlanDependencies, ): Promise { const configuredStrategies = new Map(); const selectionRequirements: NativeServicePrerequisite[] = []; const serviceIdsToProbe: ProductionNativeServiceId[] = []; for (const serviceId of productionServiceIds) { const executable = input.executables[serviceId]; if (hasConfiguredCommand(executable.configuredCommand)) { configuredStrategies.set(serviceId, { kind: "configured-override", command: executable.configuredCommand, verification: "unverified", }); continue; } serviceIdsToProbe.push(serviceId); selectionRequirements.push(commandRequirement(serviceId, executable.namedCommand)); } let outcomes = new Map(); if (selectionRequirements.length > 0) { const probeResult = await runSelectionProbe(input, selectionRequirements, dependencies.probe); if (probeResult.kind === "infrastructure-failure") { return { ok: false, failures: [{ kind: "probe-infrastructure", serviceIds: serviceIdsToProbe, message: probeResult.message }], }; } const parsedOutcomes = probeOutcomes(selectionRequirements, probeResult.outcomes); if (parsedOutcomes.kind === "infrastructure-failure") { return { ok: false, failures: [{ kind: "probe-infrastructure", serviceIds: serviceIdsToProbe, message: parsedOutcomes.message }], }; } outcomes = parsedOutcomes.outcomes; } const strategies = new Map(configuredStrategies); const failures: NativeServicePlanFailure[] = []; for (const serviceId of serviceIdsToProbe) { const executable = input.executables[serviceId]; const outcome = outcomes.get(commandRequirementId(serviceId, executable.namedCommand)); if (outcome?.status === "satisfied") { strategies.set(serviceId, { kind: "named-command", command: executable.namedCommand, selectedBy: "authoritative-backend-probe", }); continue; } let entrypointExists: boolean; try { entrypointExists = dependencies.fileExists(executable.bundledEntrypointPath); } catch (error: unknown) { failures.push({ kind: "entrypoint-inspection-failure", serviceId, entrypointPath: executable.bundledEntrypointPath, message: errorMessage(error), }); continue; } if (entrypointExists) { strategies.set(serviceId, { kind: "bundled-entrypoint", command: "node", entrypointPath: executable.bundledEntrypointPath, namedCommand: executable.namedCommand, namedCommandFailure: outcome?.detail ?? null, }); continue; } failures.push({ kind: "executable-unavailable", serviceId, namedCommand: executable.namedCommand, namedCommandFailure: outcome?.detail ?? null, bundledEntrypointPath: executable.bundledEntrypointPath, }); } if (failures.length > 0) return { ok: false, failures }; return { ok: true, plan: { mode: "production", backend: input.backend, shell: input.shell, services: productionServiceIds.map((serviceId) => productionService(input, serviceId, requiredStrategy(strategies, serviceId))), }, }; } export function createDevelopmentNativeServicePlan(input: DevelopmentNativeServicePlanInput): NativeServicePlan { const environment = copyEnvironment(input.environment); const sessiondScripts = ["start:sessiond"] as const; const uiDevScripts = ["dev:web", "dev:client"] as const; const uiDevCommand = 'trap "kill 0" EXIT; npm run dev:web & npm run dev:client & wait'; return { mode: "development", backend: input.backend, shell: input.shell, services: [ { id: "sessiond", manager: nativeServiceRefs.sessiond, description: "PI WEB session daemon (dev)", shellCommand: "exec npm run start:sessiond", strategy: { kind: "development-npm-script", script: "start:sessiond" }, restart: "never", environment, workingDirectory: input.workingDirectory, after: [], wants: [], prerequisites: [ nodeRequirement("sessiond"), commandRequirement("sessiond", "npm"), packageScriptsRequirement("sessiond", input.packageJsonPath, sessiondScripts), ], }, { id: "uiDev", manager: nativeServiceRefs.uiDev, description: "PI WEB UI dev server", shellCommand: `exec /usr/bin/env bash -c ${shellSingleQuote(input.shell.name, uiDevCommand)}`, strategy: { kind: "development-npm-script-group", scripts: uiDevScripts, interpreter: "bash" }, restart: "never", environment, workingDirectory: input.workingDirectory, after: ["sessiond"], wants: ["sessiond"], prerequisites: [ nodeRequirement("uiDev"), commandRequirement("uiDev", "npm"), commandRequirement("uiDev", "bash"), packageScriptsRequirement("uiDev", input.packageJsonPath, uiDevScripts), ], }, ], }; } export function planValidationProbeRequests(plan: NativeServicePlan): readonly NativeServiceProbeRequest[] { return plan.services.flatMap((service) => service.prerequisites.length === 0 ? [] : [{ purpose: "plan-validation" as const, backend: plan.backend, shell: plan.shell, environment: service.environment, workingDirectory: service.workingDirectory, prerequisites: service.prerequisites, }]); } function productionService( input: ProductionNativeServicePlanInput, serviceId: ProductionNativeServiceId, strategy: NativeServiceCommandStrategy, ): NativeServicePlanService { const isWeb = serviceId === "web"; return { id: serviceId, manager: nativeServiceRefs[serviceId], description: isWeb ? "PI WEB server" : "PI WEB session daemon", shellCommand: `exec ${strategyCommand(input.shell, strategy)}`, strategy, restart: "on-failure", environment: copyEnvironment(input.environment), workingDirectory: null, after: isWeb ? ["sessiond"] : [], wants: isWeb ? ["sessiond"] : [], prerequisites: strategyPrerequisites(serviceId, strategy), }; } function strategyCommand(shell: NativeServiceShell, strategy: NativeServiceCommandStrategy): string { switch (strategy.kind) { case "configured-override": case "named-command": return strategy.command; case "bundled-entrypoint": return `${strategy.command} ${shellSingleQuote(shell.name, strategy.entrypointPath)}`; case "development-npm-script": return `npm run ${strategy.script}`; case "development-npm-script-group": throw new Error("Development script groups define their complete service shell command"); } } function strategyPrerequisites(serviceId: ProductionNativeServiceId, strategy: NativeServiceCommandStrategy): readonly NativeServicePrerequisite[] { switch (strategy.kind) { case "configured-override": return []; case "named-command": return [commandRequirement(serviceId, strategy.command), nodeRequirement(serviceId)]; case "bundled-entrypoint": return [nodeRequirement(serviceId), readableFileRequirement(serviceId, strategy.entrypointPath)]; case "development-npm-script": case "development-npm-script-group": throw new Error(`Unexpected ${strategy.kind} strategy in a production plan`); } } async function runSelectionProbe( input: ProductionNativeServicePlanInput, prerequisites: readonly NativeServicePrerequisite[], probe: NativeServiceAuthoritativeProbe, ): Promise { try { return await probe.run({ purpose: "executable-selection", backend: input.backend, shell: input.shell, environment: copyEnvironment(input.environment), workingDirectory: null, prerequisites, }); } catch (error: unknown) { return { kind: "infrastructure-failure", message: errorMessage(error) }; } } function probeOutcomes( prerequisites: readonly NativeServicePrerequisite[], outcomes: readonly NativeServicePrerequisiteOutcome[], ): { kind: "completed"; outcomes: Map } | { kind: "infrastructure-failure"; message: string } { const expectedIds = new Set(prerequisites.map((prerequisite) => prerequisite.id)); const byId = new Map(); for (const outcome of outcomes) { if (!expectedIds.has(outcome.prerequisiteId)) { return { kind: "infrastructure-failure", message: `Authoritative probe returned unexpected outcome ${outcome.prerequisiteId}.` }; } if (byId.has(outcome.prerequisiteId)) { return { kind: "infrastructure-failure", message: `Authoritative probe returned duplicate outcome ${outcome.prerequisiteId}.` }; } byId.set(outcome.prerequisiteId, outcome); } const missing = prerequisites.find((prerequisite) => !byId.has(prerequisite.id)); if (missing !== undefined) { return { kind: "infrastructure-failure", message: `Authoritative probe returned no outcome for ${missing.id}.` }; } return { kind: "completed", outcomes: byId }; } function requiredStrategy( strategies: ReadonlyMap, serviceId: ProductionNativeServiceId, ): NativeServiceCommandStrategy { const strategy = strategies.get(serviceId); if (strategy === undefined) throw new Error(`Missing executable strategy for ${serviceId}`); return strategy; } function hasConfiguredCommand(command: string | undefined): command is string { return command !== undefined && command.trim() !== ""; } function commandRequirementId(serviceId: NativeServiceId, command: string): string { return `${serviceId}.command.${command}`; } function commandRequirement(serviceId: NativeServiceId, command: string): NativeServicePrerequisite { return { id: commandRequirementId(serviceId, command), kind: "command-available", command, description: `${command} is available to the service shell`, }; } function nodeRequirement(serviceId: NativeServiceId): NativeServicePrerequisite { return { id: `${serviceId}.node`, kind: "node-version", command: "node", minimumMajor: 22, description: "node >= 22 is available to the service shell", }; } function readableFileRequirement(serviceId: NativeServiceId, path: string): NativeServicePrerequisite { return { id: `${serviceId}.entrypoint`, kind: "readable-file", path, description: `bundled entrypoint is readable: ${path}`, }; } function packageScriptsRequirement( serviceId: NativeServiceId, packageJsonPath: string, scripts: readonly string[], ): NativeServicePrerequisite { return { id: `${serviceId}.package-scripts`, kind: "package-scripts", packageJsonPath, scripts, description: `package.json defines scripts: ${scripts.join(", ")}`, }; } function shellSingleQuote(shell: NativeServiceShellName, value: string): string { if (shell === "fish") return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`; return `'${value.replaceAll("'", "'\\''")}'`; } function copyEnvironment(environment: Readonly>): Readonly> { return { ...environment }; } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); }