@particle-academy/fancy-term-host 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Per-user OS-service layer (Tier 3+) — run the pty-host as a launchd
3
+ * LaunchAgent / systemd --user unit / Windows per-user scheduled task, on its
4
+ * OWN standalone Node runtime instead of the consumer's Electron binary.
5
+ *
6
+ * Why: the detached host launched via `process.execPath` (Electron-as-node)
7
+ * PINS the consumer's executable, so an Electron auto-update can't overwrite it
8
+ * without killing live sessions. A service on its own runtime is never pinned —
9
+ * terminals survive both quits AND updates.
10
+ *
11
+ * The wire protocol, `HostClient`, pidfile, and socket path are all UNCHANGED;
12
+ * only the host's launch + lifecycle move here. See docs/persistence.md.
13
+ */
14
+ /** The per-OS service mechanism. */
15
+ type ServicePlatform = 'launchd' | 'systemd' | 'windows-task';
16
+ /**
17
+ * The standalone runtime the service runs the host on. MUST NOT be the
18
+ * consumer's Electron binary (that reintroduces the pin) — a plain Node.
19
+ */
20
+ interface ServiceRuntime {
21
+ /** Absolute path to a standalone `node` (never `electron`). */
22
+ nodePath: string;
23
+ /**
24
+ * Directory holding an ABI-matched `node-pty` build for `nodePath` (added to
25
+ * the service's `NODE_PATH`). Omit if node-pty resolves normally from the
26
+ * host script's own `node_modules`.
27
+ */
28
+ nodePtyDir?: string;
29
+ /** How this runtime was resolved (diagnostics only). */
30
+ source: string;
31
+ }
32
+ /** Inputs describing the service to install for this user. */
33
+ interface HostServiceConfig {
34
+ /**
35
+ * Reverse-DNS-ish service label, stable per app + user, e.g.
36
+ * `"academy.particle.genie.ptyhost"`. Used as the launchd Label, the
37
+ * systemd unit name, and the Windows task name.
38
+ */
39
+ label: string;
40
+ /** User-data dir the host uses for its pidfile / socket / snapshots. */
41
+ userDataDir: string;
42
+ /** Path to the pty-host script. Defaults to `ptyHostScriptPath()`. */
43
+ hostScript?: string;
44
+ /** The standalone runtime. Defaults to `resolveServiceRuntime()`. */
45
+ runtime?: ServiceRuntime;
46
+ /** Extra environment variables for the host process. */
47
+ env?: Record<string, string>;
48
+ /**
49
+ * Service revision. Bump (or rely on the default, which encodes the host
50
+ * protocol version) to force a reinstall on upgrade so a stale unit doesn't
51
+ * keep running an incompatible host.
52
+ */
53
+ revision?: string;
54
+ /** Directory for the service's stdout/stderr logs. Defaults to userDataDir. */
55
+ logDir?: string;
56
+ }
57
+ type ServiceState = 'running' | 'installed' | 'not-installed' | 'unsupported' | 'unknown';
58
+ /** A snapshot of the installed service's state. */
59
+ interface ServiceStatus {
60
+ platform: ServicePlatform | 'unsupported';
61
+ state: ServiceState;
62
+ installed: boolean;
63
+ running: boolean;
64
+ label: string;
65
+ /** Path to the written unit/plist/launcher, when applicable. */
66
+ unitPath?: string;
67
+ /** The revision recorded in the installed unit, if any. */
68
+ installedRevision?: string;
69
+ /** Free-text diagnostic (last command output / error). */
70
+ detail?: string;
71
+ }
72
+ /** What `ensureHostService()` did. */
73
+ type EnsureAction = 'already-running' | 'started' | 'installed-and-started' | 'reinstalled' | 'failed' | 'unsupported';
74
+ /** Result of `ensureHostService()` — never throws; inspect `ok`. */
75
+ interface EnsureResult {
76
+ /** True when the service is installed AND running at the matching revision. */
77
+ ok: boolean;
78
+ installed: boolean;
79
+ running: boolean;
80
+ action: EnsureAction;
81
+ runtime?: ServiceRuntime;
82
+ /** Set when `ok` is false — the reason, so the caller can fall back. */
83
+ error?: string;
84
+ }
85
+ /**
86
+ * Injected IO so the orchestration is unit-testable without touching the real
87
+ * OS. `nodeServiceIo()` is the production implementation.
88
+ */
89
+ interface ServiceIo {
90
+ /** Run a command; resolves with the exit code + captured output. */
91
+ run(argv: string[]): Promise<{
92
+ code: number;
93
+ stdout: string;
94
+ stderr: string;
95
+ }>;
96
+ writeFile(path: string, contents: string, opts?: {
97
+ mode?: number;
98
+ }): Promise<void>;
99
+ readFile(path: string): Promise<string | null>;
100
+ mkdirp(dir: string): Promise<void>;
101
+ rm(path: string): Promise<void>;
102
+ exists(path: string): Promise<boolean>;
103
+ }
104
+
105
+ /**
106
+ * Default service revision. Encodes the host PROTOCOL_VERSION so that a protocol
107
+ * bump (which makes an old host incompatible) forces a reinstall — the installed
108
+ * unit carries this marker and `ensureHostService` reinstalls on a mismatch.
109
+ */
110
+ declare const SERVICE_REVISION = "svc1+proto2";
111
+ /** A fully-resolved, per-OS description of the service to install. */
112
+ interface ServiceDescriptor {
113
+ platform: ServicePlatform;
114
+ label: string;
115
+ revision: string;
116
+ /** The file we write + own (plist / unit / launcher .cmd). */
117
+ unitPath: string;
118
+ unitContents: string;
119
+ /** POSIX file mode for the unit (e.g. 0o600 plist, 0o700 launcher). */
120
+ unitMode: number;
121
+ /** Commands (after the file is written) to register + start the service. */
122
+ installArgv: string[][];
123
+ /** Commands to stop + deregister. */
124
+ uninstallArgv: string[][];
125
+ startArgv: string[][];
126
+ stopArgv: string[][];
127
+ /** Single command whose output tells us installed/running. */
128
+ statusArgv: string[];
129
+ /** Files to remove on uninstall (unit + any logs we created). */
130
+ removePaths: string[];
131
+ }
132
+ interface DescriptorContext {
133
+ /** Override the platform (tests). Defaults to the current OS. */
134
+ platform?: ServicePlatform;
135
+ /** Home dir (tests). Defaults to `os.homedir()`. */
136
+ home?: string;
137
+ /** Numeric uid for launchd domain targets (tests / non-posix). */
138
+ uid?: number;
139
+ }
140
+ /** Map `process.platform` to a service mechanism, or null when unsupported. */
141
+ declare function servicePlatformFor(platform?: NodeJS.Platform): ServicePlatform | null;
142
+ /**
143
+ * Build the per-OS descriptor for a service config. PURE — no fs, no spawning —
144
+ * so the generated units + command argv are fully unit-testable.
145
+ */
146
+ declare function buildServiceDescriptor(config: ResolvedServiceConfig, ctx?: DescriptorContext): ServiceDescriptor;
147
+ /**
148
+ * A config with all defaults filled in — produced by `resolveServiceConfig`
149
+ * (see index.ts) and consumed by the descriptor builder.
150
+ */
151
+ interface ResolvedServiceConfig {
152
+ label: string;
153
+ userDataDir: string;
154
+ hostScript: string;
155
+ runtime: ServiceRuntime;
156
+ env: Record<string, string>;
157
+ revision: string;
158
+ logDir: string;
159
+ }
160
+ /** Read the revision marker out of an installed unit's contents, or null. */
161
+ declare function parseInstalledRevision(unitContents: string): string | null;
162
+
163
+ /**
164
+ * Locate a STANDALONE Node runtime to run the host service on — explicitly NOT
165
+ * the consumer's Electron binary, whose whole problem is that running the host
166
+ * on it pins the executable across auto-updates.
167
+ *
168
+ * Precedence:
169
+ * 1. An explicit `nodePath` (the consumer ships/locates its own node).
170
+ * 2. `$FANCY_TERM_NODE` (a deploy-time override).
171
+ * 3. `process.execPath` — but ONLY if the current process is plain Node, not
172
+ * Electron (`process.versions.electron`), and the binary is named `node`.
173
+ * 4. A `node` / `node.exe` found on `$PATH`.
174
+ *
175
+ * Returns null when no safe standalone runtime is found — the caller should then
176
+ * fall back to the existing detached-spawn (which works for a normal quit) or
177
+ * in-process. Never throws.
178
+ */
179
+ interface ResolveRuntimeOptions {
180
+ /** Explicit standalone node path (wins). */
181
+ nodePath?: string;
182
+ /** Directory with an ABI-matched node-pty for that runtime. */
183
+ nodePtyDir?: string;
184
+ /** Environment to read overrides from. Defaults to `process.env`. */
185
+ env?: NodeJS.ProcessEnv;
186
+ /** Current process binary. Defaults to `process.execPath`. */
187
+ execPath?: string;
188
+ /** Whether the current process is Electron. Defaults to detecting it. */
189
+ isElectron?: boolean;
190
+ /** PATH lookup probe (injectable for tests). Defaults to an fs probe. */
191
+ pathProbe?: (binNames: string[], env: NodeJS.ProcessEnv) => string | null;
192
+ }
193
+ declare function resolveServiceRuntime(opts?: ResolveRuntimeOptions): ServiceRuntime | null;
194
+
195
+ /**
196
+ * Production {@link ServiceIo}: real fs + child_process. The orchestration in
197
+ * index.ts takes a ServiceIo so tests can inject a fake and never touch the OS.
198
+ */
199
+ declare function nodeServiceIo(): ServiceIo;
200
+
201
+ /**
202
+ * Per-user OS-service lifecycle for the pty-host. Install / start / stop /
203
+ * status / uninstall + a single `ensureHostService()` that does the
204
+ * install-if-missing-or-stale → start-if-stopped dance.
205
+ *
206
+ * Pairs with the existing detached-host path: the wire protocol, pidfile, and
207
+ * `HostClient` are unchanged, so the consumer connects exactly as today. The
208
+ * difference is WHO launches the host — a per-user service on a standalone Node
209
+ * runtime, not a child of the consumer's Electron binary.
210
+ *
211
+ * Everything here is graceful: `ensureHostService` never throws (inspect `ok`),
212
+ * so a consumer can try the service and, on failure, fall back to
213
+ * `HostSpawner.spawnDetached` (normal-quit survival) → in-process.
214
+ */
215
+
216
+ /** True when the current OS has a supported service mechanism. */
217
+ declare function isServiceSupported(): boolean;
218
+ /**
219
+ * Fill in every default (host script, runtime, revision, log dir) so the
220
+ * descriptor builder has a complete config. Throws only if no standalone Node
221
+ * runtime can be resolved — callers that want graceful behaviour should use
222
+ * `ensureHostService`, which catches this.
223
+ */
224
+ declare function resolveServiceConfig(config: HostServiceConfig): ResolvedServiceConfig;
225
+ /** Write the unit file(s) and register + start the service. */
226
+ declare function installHostService(config: HostServiceConfig, io?: ServiceIo): Promise<ServiceStatus>;
227
+ /** Stop + deregister the service and remove its unit file(s). */
228
+ declare function uninstallHostService(config: HostServiceConfig, io?: ServiceIo): Promise<void>;
229
+ declare function startHostService(config: HostServiceConfig, io?: ServiceIo): Promise<void>;
230
+ declare function stopHostService(config: HostServiceConfig, io?: ServiceIo): Promise<void>;
231
+ declare function isServiceInstalled(config: HostServiceConfig, io?: ServiceIo): Promise<boolean>;
232
+ declare function serviceStatus(config: HostServiceConfig, io?: ServiceIo): Promise<ServiceStatus>;
233
+ /**
234
+ * Ensure the service is installed at the current revision AND running.
235
+ *
236
+ * - already running at this revision → no-op
237
+ * - installed (this revision), stopped → start
238
+ * - installed at a DIFFERENT revision → uninstall + reinstall + start
239
+ * - not installed → install (+ start)
240
+ *
241
+ * NEVER throws. On any failure (no runtime, unsupported OS, a command error) it
242
+ * returns `{ ok: false, … }` with an `error` so the caller can fall back to the
243
+ * detached-spawn path. Snapshot live sessions before a reinstall if you need
244
+ * history to survive it (a reinstall restarts the host).
245
+ */
246
+ declare function ensureHostService(config: HostServiceConfig, io?: ServiceIo): Promise<EnsureResult>;
247
+
248
+ export { type EnsureAction, type EnsureResult, type HostServiceConfig, type ResolvedServiceConfig, SERVICE_REVISION, type ServiceDescriptor, type ServiceIo, type ServicePlatform, type ServiceRuntime, type ServiceState, type ServiceStatus, buildServiceDescriptor, ensureHostService, installHostService, isServiceInstalled, isServiceSupported, nodeServiceIo, parseInstalledRevision, resolveServiceConfig, resolveServiceRuntime, servicePlatformFor, serviceStatus, startHostService, stopHostService, uninstallHostService };
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Per-user OS-service layer (Tier 3+) — run the pty-host as a launchd
3
+ * LaunchAgent / systemd --user unit / Windows per-user scheduled task, on its
4
+ * OWN standalone Node runtime instead of the consumer's Electron binary.
5
+ *
6
+ * Why: the detached host launched via `process.execPath` (Electron-as-node)
7
+ * PINS the consumer's executable, so an Electron auto-update can't overwrite it
8
+ * without killing live sessions. A service on its own runtime is never pinned —
9
+ * terminals survive both quits AND updates.
10
+ *
11
+ * The wire protocol, `HostClient`, pidfile, and socket path are all UNCHANGED;
12
+ * only the host's launch + lifecycle move here. See docs/persistence.md.
13
+ */
14
+ /** The per-OS service mechanism. */
15
+ type ServicePlatform = 'launchd' | 'systemd' | 'windows-task';
16
+ /**
17
+ * The standalone runtime the service runs the host on. MUST NOT be the
18
+ * consumer's Electron binary (that reintroduces the pin) — a plain Node.
19
+ */
20
+ interface ServiceRuntime {
21
+ /** Absolute path to a standalone `node` (never `electron`). */
22
+ nodePath: string;
23
+ /**
24
+ * Directory holding an ABI-matched `node-pty` build for `nodePath` (added to
25
+ * the service's `NODE_PATH`). Omit if node-pty resolves normally from the
26
+ * host script's own `node_modules`.
27
+ */
28
+ nodePtyDir?: string;
29
+ /** How this runtime was resolved (diagnostics only). */
30
+ source: string;
31
+ }
32
+ /** Inputs describing the service to install for this user. */
33
+ interface HostServiceConfig {
34
+ /**
35
+ * Reverse-DNS-ish service label, stable per app + user, e.g.
36
+ * `"academy.particle.genie.ptyhost"`. Used as the launchd Label, the
37
+ * systemd unit name, and the Windows task name.
38
+ */
39
+ label: string;
40
+ /** User-data dir the host uses for its pidfile / socket / snapshots. */
41
+ userDataDir: string;
42
+ /** Path to the pty-host script. Defaults to `ptyHostScriptPath()`. */
43
+ hostScript?: string;
44
+ /** The standalone runtime. Defaults to `resolveServiceRuntime()`. */
45
+ runtime?: ServiceRuntime;
46
+ /** Extra environment variables for the host process. */
47
+ env?: Record<string, string>;
48
+ /**
49
+ * Service revision. Bump (or rely on the default, which encodes the host
50
+ * protocol version) to force a reinstall on upgrade so a stale unit doesn't
51
+ * keep running an incompatible host.
52
+ */
53
+ revision?: string;
54
+ /** Directory for the service's stdout/stderr logs. Defaults to userDataDir. */
55
+ logDir?: string;
56
+ }
57
+ type ServiceState = 'running' | 'installed' | 'not-installed' | 'unsupported' | 'unknown';
58
+ /** A snapshot of the installed service's state. */
59
+ interface ServiceStatus {
60
+ platform: ServicePlatform | 'unsupported';
61
+ state: ServiceState;
62
+ installed: boolean;
63
+ running: boolean;
64
+ label: string;
65
+ /** Path to the written unit/plist/launcher, when applicable. */
66
+ unitPath?: string;
67
+ /** The revision recorded in the installed unit, if any. */
68
+ installedRevision?: string;
69
+ /** Free-text diagnostic (last command output / error). */
70
+ detail?: string;
71
+ }
72
+ /** What `ensureHostService()` did. */
73
+ type EnsureAction = 'already-running' | 'started' | 'installed-and-started' | 'reinstalled' | 'failed' | 'unsupported';
74
+ /** Result of `ensureHostService()` — never throws; inspect `ok`. */
75
+ interface EnsureResult {
76
+ /** True when the service is installed AND running at the matching revision. */
77
+ ok: boolean;
78
+ installed: boolean;
79
+ running: boolean;
80
+ action: EnsureAction;
81
+ runtime?: ServiceRuntime;
82
+ /** Set when `ok` is false — the reason, so the caller can fall back. */
83
+ error?: string;
84
+ }
85
+ /**
86
+ * Injected IO so the orchestration is unit-testable without touching the real
87
+ * OS. `nodeServiceIo()` is the production implementation.
88
+ */
89
+ interface ServiceIo {
90
+ /** Run a command; resolves with the exit code + captured output. */
91
+ run(argv: string[]): Promise<{
92
+ code: number;
93
+ stdout: string;
94
+ stderr: string;
95
+ }>;
96
+ writeFile(path: string, contents: string, opts?: {
97
+ mode?: number;
98
+ }): Promise<void>;
99
+ readFile(path: string): Promise<string | null>;
100
+ mkdirp(dir: string): Promise<void>;
101
+ rm(path: string): Promise<void>;
102
+ exists(path: string): Promise<boolean>;
103
+ }
104
+
105
+ /**
106
+ * Default service revision. Encodes the host PROTOCOL_VERSION so that a protocol
107
+ * bump (which makes an old host incompatible) forces a reinstall — the installed
108
+ * unit carries this marker and `ensureHostService` reinstalls on a mismatch.
109
+ */
110
+ declare const SERVICE_REVISION = "svc1+proto2";
111
+ /** A fully-resolved, per-OS description of the service to install. */
112
+ interface ServiceDescriptor {
113
+ platform: ServicePlatform;
114
+ label: string;
115
+ revision: string;
116
+ /** The file we write + own (plist / unit / launcher .cmd). */
117
+ unitPath: string;
118
+ unitContents: string;
119
+ /** POSIX file mode for the unit (e.g. 0o600 plist, 0o700 launcher). */
120
+ unitMode: number;
121
+ /** Commands (after the file is written) to register + start the service. */
122
+ installArgv: string[][];
123
+ /** Commands to stop + deregister. */
124
+ uninstallArgv: string[][];
125
+ startArgv: string[][];
126
+ stopArgv: string[][];
127
+ /** Single command whose output tells us installed/running. */
128
+ statusArgv: string[];
129
+ /** Files to remove on uninstall (unit + any logs we created). */
130
+ removePaths: string[];
131
+ }
132
+ interface DescriptorContext {
133
+ /** Override the platform (tests). Defaults to the current OS. */
134
+ platform?: ServicePlatform;
135
+ /** Home dir (tests). Defaults to `os.homedir()`. */
136
+ home?: string;
137
+ /** Numeric uid for launchd domain targets (tests / non-posix). */
138
+ uid?: number;
139
+ }
140
+ /** Map `process.platform` to a service mechanism, or null when unsupported. */
141
+ declare function servicePlatformFor(platform?: NodeJS.Platform): ServicePlatform | null;
142
+ /**
143
+ * Build the per-OS descriptor for a service config. PURE — no fs, no spawning —
144
+ * so the generated units + command argv are fully unit-testable.
145
+ */
146
+ declare function buildServiceDescriptor(config: ResolvedServiceConfig, ctx?: DescriptorContext): ServiceDescriptor;
147
+ /**
148
+ * A config with all defaults filled in — produced by `resolveServiceConfig`
149
+ * (see index.ts) and consumed by the descriptor builder.
150
+ */
151
+ interface ResolvedServiceConfig {
152
+ label: string;
153
+ userDataDir: string;
154
+ hostScript: string;
155
+ runtime: ServiceRuntime;
156
+ env: Record<string, string>;
157
+ revision: string;
158
+ logDir: string;
159
+ }
160
+ /** Read the revision marker out of an installed unit's contents, or null. */
161
+ declare function parseInstalledRevision(unitContents: string): string | null;
162
+
163
+ /**
164
+ * Locate a STANDALONE Node runtime to run the host service on — explicitly NOT
165
+ * the consumer's Electron binary, whose whole problem is that running the host
166
+ * on it pins the executable across auto-updates.
167
+ *
168
+ * Precedence:
169
+ * 1. An explicit `nodePath` (the consumer ships/locates its own node).
170
+ * 2. `$FANCY_TERM_NODE` (a deploy-time override).
171
+ * 3. `process.execPath` — but ONLY if the current process is plain Node, not
172
+ * Electron (`process.versions.electron`), and the binary is named `node`.
173
+ * 4. A `node` / `node.exe` found on `$PATH`.
174
+ *
175
+ * Returns null when no safe standalone runtime is found — the caller should then
176
+ * fall back to the existing detached-spawn (which works for a normal quit) or
177
+ * in-process. Never throws.
178
+ */
179
+ interface ResolveRuntimeOptions {
180
+ /** Explicit standalone node path (wins). */
181
+ nodePath?: string;
182
+ /** Directory with an ABI-matched node-pty for that runtime. */
183
+ nodePtyDir?: string;
184
+ /** Environment to read overrides from. Defaults to `process.env`. */
185
+ env?: NodeJS.ProcessEnv;
186
+ /** Current process binary. Defaults to `process.execPath`. */
187
+ execPath?: string;
188
+ /** Whether the current process is Electron. Defaults to detecting it. */
189
+ isElectron?: boolean;
190
+ /** PATH lookup probe (injectable for tests). Defaults to an fs probe. */
191
+ pathProbe?: (binNames: string[], env: NodeJS.ProcessEnv) => string | null;
192
+ }
193
+ declare function resolveServiceRuntime(opts?: ResolveRuntimeOptions): ServiceRuntime | null;
194
+
195
+ /**
196
+ * Production {@link ServiceIo}: real fs + child_process. The orchestration in
197
+ * index.ts takes a ServiceIo so tests can inject a fake and never touch the OS.
198
+ */
199
+ declare function nodeServiceIo(): ServiceIo;
200
+
201
+ /**
202
+ * Per-user OS-service lifecycle for the pty-host. Install / start / stop /
203
+ * status / uninstall + a single `ensureHostService()` that does the
204
+ * install-if-missing-or-stale → start-if-stopped dance.
205
+ *
206
+ * Pairs with the existing detached-host path: the wire protocol, pidfile, and
207
+ * `HostClient` are unchanged, so the consumer connects exactly as today. The
208
+ * difference is WHO launches the host — a per-user service on a standalone Node
209
+ * runtime, not a child of the consumer's Electron binary.
210
+ *
211
+ * Everything here is graceful: `ensureHostService` never throws (inspect `ok`),
212
+ * so a consumer can try the service and, on failure, fall back to
213
+ * `HostSpawner.spawnDetached` (normal-quit survival) → in-process.
214
+ */
215
+
216
+ /** True when the current OS has a supported service mechanism. */
217
+ declare function isServiceSupported(): boolean;
218
+ /**
219
+ * Fill in every default (host script, runtime, revision, log dir) so the
220
+ * descriptor builder has a complete config. Throws only if no standalone Node
221
+ * runtime can be resolved — callers that want graceful behaviour should use
222
+ * `ensureHostService`, which catches this.
223
+ */
224
+ declare function resolveServiceConfig(config: HostServiceConfig): ResolvedServiceConfig;
225
+ /** Write the unit file(s) and register + start the service. */
226
+ declare function installHostService(config: HostServiceConfig, io?: ServiceIo): Promise<ServiceStatus>;
227
+ /** Stop + deregister the service and remove its unit file(s). */
228
+ declare function uninstallHostService(config: HostServiceConfig, io?: ServiceIo): Promise<void>;
229
+ declare function startHostService(config: HostServiceConfig, io?: ServiceIo): Promise<void>;
230
+ declare function stopHostService(config: HostServiceConfig, io?: ServiceIo): Promise<void>;
231
+ declare function isServiceInstalled(config: HostServiceConfig, io?: ServiceIo): Promise<boolean>;
232
+ declare function serviceStatus(config: HostServiceConfig, io?: ServiceIo): Promise<ServiceStatus>;
233
+ /**
234
+ * Ensure the service is installed at the current revision AND running.
235
+ *
236
+ * - already running at this revision → no-op
237
+ * - installed (this revision), stopped → start
238
+ * - installed at a DIFFERENT revision → uninstall + reinstall + start
239
+ * - not installed → install (+ start)
240
+ *
241
+ * NEVER throws. On any failure (no runtime, unsupported OS, a command error) it
242
+ * returns `{ ok: false, … }` with an `error` so the caller can fall back to the
243
+ * detached-spawn path. Snapshot live sessions before a reinstall if you need
244
+ * history to survive it (a reinstall restarts the host).
245
+ */
246
+ declare function ensureHostService(config: HostServiceConfig, io?: ServiceIo): Promise<EnsureResult>;
247
+
248
+ export { type EnsureAction, type EnsureResult, type HostServiceConfig, type ResolvedServiceConfig, SERVICE_REVISION, type ServiceDescriptor, type ServiceIo, type ServicePlatform, type ServiceRuntime, type ServiceState, type ServiceStatus, buildServiceDescriptor, ensureHostService, installHostService, isServiceInstalled, isServiceSupported, nodeServiceIo, parseInstalledRevision, resolveServiceConfig, resolveServiceRuntime, servicePlatformFor, serviceStatus, startHostService, stopHostService, uninstallHostService };