import { type VehicleRegistrar, type VehicleRegistrationOutcome, type VehicleSpec } from "@danypops/armada"; import { type ServiceInstallResult, type ServiceSpec } from "@danypops/vehicle-server/service"; import type { InstalledPkg } from "../packages/package.ts"; import { type DaemonPackageMaterializer } from "./isolated-service-install.ts"; export interface DaemonServiceManifest { /** Relative to the installed package's own root directory. */ binPath: string; /** Executes the entrypoint through Packed's absolute Bun runtime instead of its PATH-dependent shebang. */ runtime?: "bun"; args?: string[]; /** Defaults to the bare npm package name (scope stripped). */ name?: string; displayName?: string; /** Runtime handle filename inside the Vehicle's XDG runtime directory. */ handleFilename?: string; workingDirectory?: string; restartOnFailure?: boolean; restartSec?: number; } export type ResolveDaemonServiceResult = { ok: true; spec: ServiceSpec; } /** notADaemon: this package (and nothing one level into its own dependencies) is Vehicle-shaped at all -- the overwhelmingly common case for a non-daemon Pi package, distinct from a real failure (systemctl unavailable, spec resolved but install itself failed). A caller composing install + install-service (see cli.ts's `install` command) uses this to stay silent instead of surfacing a false alarm on every ordinary package install. */ | { ok: false; reason: string; notADaemon?: boolean; }; /** A resolved daemon entrypoint, already an absolute binPath -- the common shape both an explicit manifest and detection converge to before a ServiceSpec is built. */ interface ResolvedDaemonEntrypoint { binPath: string; runtime?: "bun"; args?: string[]; name: string; displayName?: string; handleFilename?: string; workingDirectory?: string; restartOnFailure?: boolean; restartSec?: number; version: string; contentSignature?: string; } export declare function detectVehicleDaemonService(packageDir: string, fallbackName: string, hoistedNodeModulesDir?: string): ResolvedDaemonEntrypoint | undefined; /** * Resolves the installed package's daemon entrypoint -- an explicit * `packed.daemonService` manifest first, then detectVehicleDaemonService() * -- and builds the exact ServiceSpec its own `service install` command * would build. npm sources only for now; git:/local sources resolve to a * different on-disk layout not yet supported here. */ export declare function resolveDaemonServiceSpec(piHome: string, source: string): ResolveDaemonServiceResult; export interface DaemonDependencyPkg { name: string; version: string; /** The Armada vehicle name resolveDaemonServiceSpec built for this package -- may differ * from `name`'s own bare unscoped form when a packed.daemonService manifest overrides it. */ vehicleName: string; } /** * Every top-level npm package physically installed under piHome/npm/node_modules * that resolveDaemonServiceSpec() itself already recognizes as a real Vehicle- * shaped daemon (an explicit packed.daemonService manifest, or * detectVehicleDaemonService()'s own-bin-plus-real-vehicle-dependency * convention), but that is NOT itself a pi:-configured extension * (readPackageDeclarations() has no entry for it) -- e.g. @danypops/lector, * added directly to piHome/npm/package.json as an independent pin so it can * run ahead of whatever version its own pi-lector wrapper's dependency tree * would otherwise resolve. These are real, running Armada vehicles that a * pi:-manifest-only "installed packages" notion makes permanently invisible * to `packed installed`/`packed update` -- see * packed-package-update-restart-service-cant-manage. * * Deliberately does NOT walk one level into each configured extension's own * dependencies the way resolveDaemonServiceSpec's own detection does for * install-service/restart-service/doctor (service-doctor.ts's * checkServiceUnitPaths already reaches that same dependency through its * wrapper extension's own row) -- duplicating that walk here would * double-report the same Vehicle under two different package names. This * only reports a dependency independently resolvable BY ITS OWN NAME at * piHome/npm's own top level -- exactly the shape a caller can actually run * `packed update npm:` against directly. */ export declare function listUnconfiguredDaemonDependencies(piHome: string): DaemonDependencyPkg[]; /** * True when `packageName` (a bare npm name) is a pi:-configured extension -- * present in readPackageDeclarations()'s own packages[] list, regardless of * whether it's pinned. The other half of the classification * classifyUpdateSource() needs: a name that resolves ok via * resolveDaemonServiceSpec() but returns false here is a "daemon-dependency" * class source, not an "extension" one. */ export declare function isConfiguredExtension(piHome: string, packageName: string): boolean; export type UpdateSourceKind = "extension" | "daemon-dependency"; /** * Classifies an npm: source for package.update's own two-path mutation: * "extension" (pi:-configured, settings.json's packages[] -- `pi update * --extension` already handles this correctly, unchanged) vs. * "daemon-dependency" (NOT pi:-configured, but resolveDaemonServiceSpec() * resolves it directly BY ITS OWN NAME -- pi-core can never see this one; * confirmed via this house's own failing-test repro, service/test/ * install.test.ts, of pi's real "No matching package found"). Needs the * alternate npm-level mutation path (Installer.updateDaemonDependency()) * instead of shelling to `pi update --extension` at all. * * undefined for a non-npm: source, or an npm: source that is neither * (never installed, or installed but not Vehicle-shaped) -- package.update's * existing catch-all path (calling `pi update --extension` and surfacing * whatever it reports) still applies unchanged to either of those, exactly * as it did before this classification existed. */ export declare function classifyUpdateSource(piHome: string, source: string): UpdateSourceKind | undefined; /** * `packed installed`'s real, whole-fleet listing: every pi:-configured * extension (`kind: "extension"`, exactly today's * readInstalledPackagesAcrossScopes() output, unchanged) PLUS every * top-level daemon-only dependency listUnconfiguredDaemonDependencies() can * independently resolve (`kind: "daemon-dependency"`) -- so a real, running * Armada vehicle like @danypops/lector shows up under its own name instead * of staying invisibly nested inside its pi-lector wrapper's own row. See * packed-package-update-restart-service-cant-manage. * * projectRoot only ever widens the extension half (matching * readInstalledPackagesAcrossScopes' own scope) -- daemon-dependency * detection is global-only for now, since the reported gap itself (an * independently-pinned piHome/npm/package.json dependency) is a global-scope * shape; a project-scoped .pi/npm equivalent is real future work, not * something this fixes today. */ export declare function listManagedPackages(piHome: string, projectRoot?: string): InstalledPkg[]; export interface ReconcileAllResult { /** `versionChanged` is true only when this pass's newly-resolved version genuinely differs * from what Armada had registered before this sweep started -- `installed: true` alone means * only "the register call itself succeeded", which is true on every healthy pass whether or * not anything actually changed underneath (register()/reconcile() is a safe no-op against * unchanged desired state). A caller wanting to know whether this pass genuinely restarted * something (vs. just re-confirmed a healthy, unchanged fleet) must check this, not `installed`. */ reconciled: Array<{ packageName: string; vehicleName: string; installed: boolean; versionChanged: boolean; reason?: string; }>; skipped: number; failed: Array<{ packageName: string; reason: string; }>; /** Every Vehicle unregistered because this sweep no longer discovers it at all -- see pruneStaleVehicles. */ pruned: Array<{ vehicleName: string; executable: string; }>; pruneFailed: Array<{ vehicleName: string; reason: string; }>; } /** Packed's own stable cross-daemon identity -- both its real Armada vehicle name (this file's own * self-registration guards below) and, since it's the same daemon, the name it registers under in * the shared Vehicle Handle Directory for Vehicle Shell broker mode (see daemon.ts's own * daemonOptions() -- vehicleName: PACKED_VEHICLE_NAME). One canonical name, not two. */ export declare const PACKED_VEHICLE_NAME = "pi-packed"; /** * Builds the command to launch armada's own `restart pi-packed` out-of-process. Plain * `spawn(..., { detached: true }).unref()` is NOT enough on Linux: systemd's default * KillMode=control-group kills every process in a unit's cgroup on `stop`, including a * detached+unref'd child -- setsid() escapes a Unix process group, not a cgroup. Confirmed * live: a bare detached spawn was killed mid-restart the instant the child's own `armada * restart`'s `stop` step fired, because the child was still inside armada-pi-packed's own * cgroup. `systemd-run --user --collect --unit=` launches it as an independent * transient unit with its own cgroup instead, immune to this unit's stop (confirmed live: * a probe unit's child outlived its own service being stopped). launchd/Windows don't tear * down a detached child's process tree the same way, so they use a plain spawn. */ export declare function selfRestartCommand(platform: NodeJS.Platform, execPath: string, cliPath: string, unitName: string): { readonly command: string; readonly arguments: readonly string[]; }; /** Fire-and-forget: schedule Packed's own Vehicle to restart from OUTSIDE this process (see * selfRestartCommand for why a plain detached spawn alone doesn't survive on Linux). Runs a * beat later so the response reporting "scheduled" has time to flush first. Swallows a * resolution/spawn failure silently: worst case is the pre-existing behavior (a human restarts * the service by hand), never a crash. */ export type SelfRestartScheduler = (spec: ServiceSpec) => void; export declare const scheduleSelfRestart: SelfRestartScheduler; /** * Sweeps every installed Packed package (global scope, plus a project's own * pins when projectRoot is given), resolves each to a Vehicle-shaped daemon * exactly as install()/restart() already do per-package, and upserts + * reconciles it through Armada. This is what makes Armada authoritative for * every Vehicle Packed knows about, not just the one source a single * install/update call happened to touch -- it also self-heals a Vehicle * whose daemon package version bumped as someone else's transitive * dependency, and a Vehicle a prior Packed version never registered at all. * Idempotent and safe to call unconditionally: a non-daemon package costs * one or two file reads (see resolveDaemonServiceSpec) and never reaches * Armada. Two packages that resolve to the same Vehicle (a Pi extension and * its own daemon dependency, both separately Packed-tracked) reconcile it * once, not twice. */ export declare function reconcileAllDaemonServices(piHome: string, projectRoot: string | undefined, installer: Pick): Promise; export interface DaemonServiceInstaller { install(piHome: string, source: string): Promise<{ ok: true; result: ServiceInstallResult; spec: ServiceSpec; } | { ok: false; reason: string; notADaemon?: boolean; }>; remove(piHome: string, source: string): Promise<{ ok: true; result: ServiceInstallResult; spec: ServiceSpec; } | { ok: false; reason: string; notADaemon?: boolean; }>; restart(piHome: string, source: string): Promise<{ ok: true; restarted: boolean; reason?: string; spec: ServiceSpec; } | { ok: false; reason: string; notADaemon?: boolean; }>; /** Every Vehicle Armada currently declares -- see pruneStaleVehicles' own doc comment for why this exists. */ listRegisteredVehicles(): Promise; /** * Unregisters by Armada vehicle NAME directly, distinct from remove()'s * own by-npm-source resolution -- pruning has no installed package left to * resolve a source from; the whole point is the vehicle survived past * whatever used to produce it. */ unregisterVehicleByName(name: string): Promise; /** True when an executable lives in this installer's private managed-service roots. */ ownsExecutable?(path: string): boolean; } /** * Calls Armada's own VehicleRegistrar directly (in-process), rather than * shelling out to its CLI as a subprocess -- the same registration logic * `@danypops/armada` exposes to any other library consumer, not a Packed- * specific reimplementation. One registrar per instance so its manifest * path/native controller are resolved once, not on every call. */ export declare class RealDaemonServiceInstaller implements DaemonServiceInstaller { private readonly registrar; private readonly selfRestart; private readonly materializer; constructor(registrar?: VehicleRegistrar, selfRestart?: SelfRestartScheduler, materializer?: DaemonPackageMaterializer); listRegisteredVehicles(): Promise; unregisterVehicleByName(name: string): Promise; ownsExecutable(path: string): boolean; install(piHome: string, source: string): Promise<{ ok: true; result: ServiceInstallResult; spec: ServiceSpec; } | { ok: false; reason: string; notADaemon?: boolean; }>; remove(piHome: string, source: string): Promise<{ ok: true; result: ServiceInstallResult; spec: ServiceSpec; } | { ok: false; reason: string; notADaemon?: boolean; }>; restart(piHome: string, source: string): Promise<{ ok: true; restarted: boolean; reason?: string; spec: ServiceSpec; } | { ok: false; reason: string; notADaemon?: boolean; }>; } export {}; //# sourceMappingURL=daemon-service.d.ts.map