dsh-update-status 0.4.0 → 0.4.2

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.
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as parseSystemdMainPid, d as bootStampFile, f as countRecentRestarts, h as serviceLogDir, i as SYSTEMD_UNIT, m as recordBootStamp, p as dshHomeOf, r as LAUNCHD_LABEL, s as parseLaunchdTrackedPid, t as planFor, u as RESTART_STORM_WINDOW_MS } from "./plan-BornHBEZ.js";
1
+ import { C as recordBootStamp, S as dshHomeOf, _ as parseSystemdMainPid, a as planFor, b as bootStampFile, f as LAUNCHD_LABEL, g as parseLaunchdTrackedPid, p as SYSTEMD_UNIT, w as serviceLogDir, x as countRecentRestarts, y as RESTART_STORM_WINDOW_MS } from "./plan-D9Ybaj-3.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
4
4
  import { basename, dirname, join, normalize, sep } from "node:path";
@@ -21,7 +21,8 @@ const VERIFIED_DSH_VERSIONS = [
21
21
  "0.1.7-rc.2",
22
22
  "0.2.0-rc.1",
23
23
  "0.2.0-rc.2",
24
- "0.2.1-alpha.1"
24
+ "0.2.1-alpha.1",
25
+ "0.2.1-alpha.3"
25
26
  ];
26
27
  function isCacheTtlMinutes(value) {
27
28
  return typeof value === "number" && Number.isInteger(value) && value >= 30 && value <= 1440;
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
3
- import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  //#region src/shared/restart-evidence.ts
6
6
  /**
@@ -165,15 +165,135 @@ function parseWindowsTaskRunning(output) {
165
165
  return /(?:^|\W)running(?:\W|$)/i.test(output);
166
166
  }
167
167
  //#endregion
168
+ //#region src/service/service-path.ts
169
+ /** Resolve one audited PATH that preflight and the native service both consume. */
170
+ /** A service definition must stay inspectable; never silently truncate it. */
171
+ const MAX_SERVICE_PATH_LENGTH = 8192;
172
+ const MINIMAL_POSIX = {
173
+ darwin: [
174
+ "/usr/local/bin",
175
+ "/usr/bin",
176
+ "/bin",
177
+ "/usr/sbin",
178
+ "/sbin"
179
+ ],
180
+ linux: [
181
+ "/usr/local/bin",
182
+ "/usr/bin",
183
+ "/bin"
184
+ ]
185
+ };
186
+ const DEFAULT_WINDOWS_PATHEXT = ".COM;.EXE;.BAT;.CMD";
187
+ function normalizeServicePathExt(value) {
188
+ const raw = value?.trim() || DEFAULT_WINDOWS_PATHEXT;
189
+ validateText(raw);
190
+ const seen = /* @__PURE__ */ new Set();
191
+ const extensions = [];
192
+ for (const original of raw.split(";")) {
193
+ const extension = original.trim();
194
+ if (extension === "") continue;
195
+ if (!/^\.[A-Za-z0-9]+$/.test(extension)) throw new Error(`invalid PATHEXT entry: ${extension}`);
196
+ const key = extension.toUpperCase();
197
+ if (seen.has(key)) continue;
198
+ seen.add(key);
199
+ extensions.push(extension);
200
+ }
201
+ if (extensions.length === 0) throw new Error("PATHEXT has no usable extension");
202
+ return extensions.join(";");
203
+ }
204
+ function delimiter(platform) {
205
+ return platform === "win32" ? ";" : ":";
206
+ }
207
+ function nodeDirectory(platform, nodePath) {
208
+ return platform === "win32" ? win32.dirname(nodePath) : posix.dirname(nodePath);
209
+ }
210
+ function isAbsolute$1(platform, value) {
211
+ return platform === "win32" ? win32.isAbsolute(value) : posix.isAbsolute(value);
212
+ }
213
+ function minimalEntries(platform, nodePath, override) {
214
+ const node = nodeDirectory(platform, nodePath);
215
+ if (override !== void 0 && override.trim() !== "") return [node, ...override.split(delimiter(platform))];
216
+ if (platform === "win32") return [node];
217
+ return [node, ...MINIMAL_POSIX[platform]];
218
+ }
219
+ function sourceValue(options) {
220
+ if (options.source === "minimal") return minimalEntries(options.platform, options.nodePath, options.minimalPath).join(delimiter(options.platform));
221
+ if (options.source === "explicit") {
222
+ if (options.explicitPath === void 0 || options.explicitPath.trim() === "") throw new Error("`--service-path` is required when `--path-source explicit` is used");
223
+ return options.explicitPath;
224
+ }
225
+ if (options.currentPath === void 0 || options.currentPath.trim() === "") throw new Error("current PATH is empty; use `--path-source minimal` or `--service-path`");
226
+ return options.currentPath;
227
+ }
228
+ function validateText(value) {
229
+ if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) throw new Error("service PATH must not contain control characters or line breaks");
230
+ if (value.length > 8192) throw new Error(`service PATH is too long (maximum ${String(MAX_SERVICE_PATH_LENGTH)} characters); use --service-path with a shorter value`);
231
+ }
232
+ /**
233
+ * Resolve at install/plan time — no user rc file is executed. Missing absolute
234
+ * directories are preserved (they may be mounted or created later) and reported.
235
+ */
236
+ function resolveServicePath(options) {
237
+ const split = delimiter(options.platform);
238
+ const raw = sourceValue(options);
239
+ validateText(raw);
240
+ if (options.platform !== "win32" && /(?:^|;)[A-Za-z]:[\\/]/.test(raw)) throw new Error("Windows-style PATH cannot be used for Linux/WSL; provide a colon-separated PATH");
241
+ const exists = options.exists ?? existsSync;
242
+ const warnings = [];
243
+ const values = [nodeDirectory(options.platform, options.nodePath), ...raw.split(split)];
244
+ const seen = /* @__PURE__ */ new Set();
245
+ const entries = [];
246
+ let warnedEmpty = false;
247
+ for (const rawEntry of values) {
248
+ if (rawEntry === "") {
249
+ if (!warnedEmpty) warnings.push("ignored empty PATH entry (current-directory lookup)");
250
+ warnedEmpty = true;
251
+ continue;
252
+ }
253
+ const entry = options.platform === "win32" && rawEntry.length >= 2 && rawEntry.startsWith("\"") && rawEntry.endsWith("\"") ? rawEntry.slice(1, -1) : rawEntry;
254
+ if (!isAbsolute$1(options.platform, entry)) {
255
+ warnings.push(`ignored relative PATH entry: ${entry}`);
256
+ continue;
257
+ }
258
+ const key = options.platform === "win32" ? entry.toLowerCase() : entry;
259
+ if (seen.has(key)) continue;
260
+ seen.add(key);
261
+ entries.push(entry);
262
+ if (!exists(entry)) warnings.push(`PATH directory does not exist yet: ${entry}`);
263
+ }
264
+ const value = entries.join(split);
265
+ if (value === "") throw new Error("service PATH has no absolute directory");
266
+ validateText(value);
267
+ const pathExt = options.platform === "win32" ? normalizeServicePathExt(options.currentPathExt) : void 0;
268
+ return {
269
+ source: options.source,
270
+ value,
271
+ ...pathExt === void 0 ? {} : { pathExt },
272
+ warnings
273
+ };
274
+ }
275
+ /** Old receipts had no PATH fields; keep their exact historical minimal shape. */
276
+ function servicePathOf(spec) {
277
+ return spec.servicePath ?? minimalEntries(spec.platform, spec.nodePath).join(delimiter(spec.platform));
278
+ }
279
+ //#endregion
168
280
  //#region src/service/plan.ts
169
281
  /** Build transparent per-OS user-service definitions; do not install them here. */
170
282
  const SERVICE_LABEL = LAUNCHD_LABEL;
171
283
  const SUPERVISOR_MARKER = "dsh-update-status";
284
+ function nativeServiceCommandCandidates(platform, systemRoot) {
285
+ if (platform === "darwin") return ["/bin/launchctl"];
286
+ if (platform === "linux") return ["/usr/bin/systemctl", "/bin/systemctl"];
287
+ return [`${systemRoot ?? "C:\\Windows"}\\System32\\schtasks.exe`];
288
+ }
289
+ function nativeServiceCommandOf(spec) {
290
+ return spec.nativeServiceCommand ?? nativeServiceCommandCandidates(spec.platform)[0];
291
+ }
172
292
  function xml(value) {
173
293
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
174
294
  }
175
295
  function systemd(value) {
176
- return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
296
+ return value.replace(/%/g, "%%").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
177
297
  }
178
298
  function powerShell(value) {
179
299
  return value.replace(/'/g, "''");
@@ -216,6 +336,24 @@ function servicePlatform(value) {
216
336
  if (value === "darwin" || value === "linux" || value === "win32") return value;
217
337
  throw new Error(`unsupported platform: ${value}; supported: macOS, Linux, Windows`);
218
338
  }
339
+ function envValue(env, key, platform) {
340
+ if (platform !== "win32") return env[key];
341
+ return Object.entries(env).find(([name]) => name.toLowerCase() === key.toLowerCase())?.[1];
342
+ }
343
+ function pathSourceOf(args) {
344
+ const explicit = valueAfter(args, "--service-path");
345
+ const raw = valueAfter(args, "--path-source");
346
+ if (raw === void 0) return explicit === void 0 ? "current" : "explicit";
347
+ if (raw !== "current" && raw !== "minimal" && raw !== "explicit") throw new Error("`--path-source` must be current, minimal, or explicit");
348
+ if (explicit !== void 0 && raw !== "explicit") throw new Error("`--service-path` requires `--path-source explicit` (or omit --path-source)");
349
+ return raw;
350
+ }
351
+ function serviceStateFileFromArgs(args, env) {
352
+ const home = env.home || homedir();
353
+ const dshHome = valueAfter(args, "--dsh-home") ?? (env.env.DSH_HOME?.trim() || void 0);
354
+ if (dshHome !== void 0 && !isAbsolute(dshHome)) throw new Error("`--dsh-home` must be an absolute path");
355
+ return join(dshHome ?? join(home, ".dsh"), "dsh-update-status-service.json");
356
+ }
219
357
  function specFromArgs(args, env) {
220
358
  const platform = servicePlatform(env.platform);
221
359
  const profile = valueAfter(args, "--profile") ?? env.env.DSH_PROFILE ?? "web";
@@ -233,6 +371,20 @@ function specFromArgs(args, env) {
233
371
  if (!isAbsolute(dshPath)) throw new Error("`--dsh` must be an absolute executable path");
234
372
  if (!isAbsolute(workspace)) throw new Error("`--workspace` must be an absolute path");
235
373
  if (!isAbsolute(logDir)) throw new Error("`--log-dir` must be an absolute path");
374
+ const systemRoot = envValue(env.env, "SYSTEMROOT", platform);
375
+ const pathExtOverride = valueAfter(args, "--service-pathext");
376
+ if (pathExtOverride !== void 0 && platform !== "win32") throw new Error("`--service-pathext` is supported only on Windows");
377
+ const candidates = nativeServiceCommandCandidates(platform, systemRoot);
378
+ const nativeServiceCommand = platform === "linux" ? candidates.find((candidate) => existsSync(candidate)) ?? candidates[0] : candidates[0];
379
+ const servicePath = resolveServicePath({
380
+ platform,
381
+ nodePath,
382
+ source: pathSourceOf(args),
383
+ currentPath: env.path ?? envValue(env.env, "PATH", platform),
384
+ currentPathExt: pathExtOverride ?? envValue(env.env, "PATHEXT", platform),
385
+ explicitPath: valueAfter(args, "--service-path"),
386
+ minimalPath: platform === "win32" && systemRoot !== void 0 ? `${systemRoot}\\System32;${systemRoot}` : void 0
387
+ });
236
388
  return {
237
389
  schemaVersion: 1,
238
390
  platform,
@@ -246,6 +398,12 @@ function specFromArgs(args, env) {
246
398
  home,
247
399
  ...dshHome === void 0 ? {} : { dshHome },
248
400
  logDir,
401
+ servicePath: servicePath.value,
402
+ servicePathSource: servicePath.source,
403
+ servicePathCapturedAt: (/* @__PURE__ */ new Date()).toISOString(),
404
+ ...servicePath.pathExt === void 0 ? {} : { servicePathExt: servicePath.pathExt },
405
+ ...servicePath.warnings.length === 0 ? {} : { servicePathWarnings: servicePath.warnings },
406
+ nativeServiceCommand,
249
407
  supervisorMarker: SUPERVISOR_MARKER
250
408
  };
251
409
  }
@@ -292,7 +450,7 @@ function webArguments(spec) {
292
450
  }
293
451
  function launchdWrapper(spec, paths, uid) {
294
452
  const args = [spec.nodePath, ...webArguments(spec)].map(shQuote).join(" ");
295
- const kickstart = `launchctl kickstart -k gui/${String(uid)}/${spec.label}`;
453
+ const kickstart = `${shQuote(nativeServiceCommandOf(spec))} kickstart -k gui/${String(uid)}/${spec.label}`;
296
454
  return `#!/bin/sh
297
455
  # Managed by dsh-update-status. This wrapper, not launchd, owns the restart loop.
298
456
  #
@@ -349,7 +507,7 @@ function launchdDefinition(spec, paths) {
349
507
  const args = ["/bin/sh", wrapper].map((value) => ` <string>${xml(value)}</string>`).join("\n");
350
508
  const envXml = [
351
509
  ["HOME", spec.home],
352
- ["PATH", `${dirname(spec.nodePath)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`],
510
+ ["PATH", servicePathOf(spec)],
353
511
  ["DSH_WEB_SUPERVISOR", spec.supervisorMarker],
354
512
  ...spec.dshHome === void 0 ? [] : [["DSH_HOME", spec.dshHome]]
355
513
  ].map(([key, value]) => ` <key>${xml(key)}</key>\n <string>${xml(value)}</string>`).join("\n");
@@ -383,7 +541,7 @@ function systemdDefinition(spec) {
383
541
  const command = [spec.nodePath, ...webArguments(spec)].map((value) => `"${systemd(value)}"`).join(" ");
384
542
  const env = [
385
543
  `Environment="HOME=${systemd(spec.home)}"`,
386
- `Environment="PATH=${systemd(`${dirname(spec.nodePath)}:/usr/local/bin:/usr/bin:/bin`)}"`,
544
+ `Environment="PATH=${systemd(servicePathOf(spec))}"`,
387
545
  `Environment="DSH_WEB_SUPERVISOR=${spec.supervisorMarker}"`,
388
546
  ...spec.dshHome === void 0 ? [] : [`Environment="DSH_HOME=${systemd(spec.dshHome)}"`]
389
547
  ].join("\n");
@@ -418,7 +576,7 @@ function windowsWrapper(spec, paths) {
418
576
  const dshHome = spec.dshHome === void 0 ? "" : `$env:DSH_HOME = '${powerShell(spec.dshHome)}'\n`;
419
577
  return `$ErrorActionPreference = 'Stop'
420
578
  $env:HOME = '${home}'
421
- $env:DSH_WEB_SUPERVISOR = '${spec.supervisorMarker}'
579
+ ${spec.servicePath === void 0 ? "" : `$env:PATH = '${powerShell(spec.servicePath)}'\n$env:PATHEXT = '${powerShell(spec.servicePathExt ?? ".COM;.EXE;.BAT;.CMD")}'\n`}$env:DSH_WEB_SUPERVISOR = '${spec.supervisorMarker}'
422
580
  ${dshHome}Set-Location -LiteralPath '${workspace}'
423
581
  $rapidWindowSeconds = ${String(30)}
424
582
  $rapidLimit = ${String(5)}
@@ -463,6 +621,7 @@ function windowsDefinition(spec, paths) {
463
621
  /** Generate a service plan without writing, starting, or stopping anything. */
464
622
  function planFor(spec, uid = 0) {
465
623
  const paths = pathsFor(spec);
624
+ const control = nativeServiceCommandOf(spec);
466
625
  if (spec.platform === "darwin") {
467
626
  const domain = `gui/${uid}`;
468
627
  return {
@@ -471,30 +630,30 @@ function planFor(spec, uid = 0) {
471
630
  definition: launchdDefinition(spec, paths),
472
631
  wrapper: launchdWrapper(spec, paths, uid),
473
632
  installCommand: [
474
- "launchctl",
633
+ control,
475
634
  "bootstrap",
476
635
  domain,
477
636
  paths.definitionFile
478
637
  ],
479
638
  startCommand: [
480
- "launchctl",
639
+ control,
481
640
  "kickstart",
482
641
  "-k",
483
642
  `${domain}/${spec.label}`
484
643
  ],
485
- recoverCommand: `launchctl kickstart -k ${domain}/${spec.label}`,
644
+ recoverCommand: `${shQuote(control)} kickstart -k ${domain}/${spec.label}`,
486
645
  statusCommand: [
487
- "launchctl",
646
+ control,
488
647
  "print",
489
648
  `${domain}/${spec.label}`
490
649
  ],
491
650
  stopCommand: [
492
- "launchctl",
651
+ control,
493
652
  "bootout",
494
653
  `${domain}/${spec.label}`
495
654
  ],
496
655
  uninstallCommand: [
497
- "launchctl",
656
+ control,
498
657
  "bootout",
499
658
  `${domain}/${spec.label}`
500
659
  ]
@@ -505,33 +664,33 @@ function planFor(spec, uid = 0) {
505
664
  paths,
506
665
  definition: systemdDefinition(spec),
507
666
  installCommand: [
508
- "systemctl",
667
+ control,
509
668
  "--user",
510
669
  "daemon-reload"
511
670
  ],
512
671
  startCommand: [
513
- "systemctl",
672
+ control,
514
673
  "--user",
515
674
  "enable",
516
675
  "--now",
517
676
  "dsh-update-status-web.service"
518
677
  ],
519
- recoverCommand: `systemctl --user reset-failed ${SYSTEMD_UNIT} && systemctl --user enable --now ${SYSTEMD_UNIT}`,
678
+ recoverCommand: `${shQuote(control)} --user reset-failed ${SYSTEMD_UNIT} && ${shQuote(control)} --user enable --now ${SYSTEMD_UNIT}`,
520
679
  statusCommand: [
521
- "systemctl",
680
+ control,
522
681
  "--user",
523
682
  "status",
524
683
  "dsh-update-status-web.service"
525
684
  ],
526
685
  stopCommand: [
527
- "systemctl",
686
+ control,
528
687
  "--user",
529
688
  "disable",
530
689
  "--now",
531
690
  "dsh-update-status-web.service"
532
691
  ],
533
692
  uninstallCommand: [
534
- "systemctl",
693
+ control,
535
694
  "--user",
536
695
  "disable",
537
696
  "--now",
@@ -544,7 +703,7 @@ function planFor(spec, uid = 0) {
544
703
  definition: windowsDefinition(spec, paths),
545
704
  wrapper: windowsWrapper(spec, paths),
546
705
  installCommand: [
547
- "schtasks.exe",
706
+ control,
548
707
  "/Create",
549
708
  "/TN",
550
709
  "DSH Update Status Web",
@@ -553,14 +712,14 @@ function planFor(spec, uid = 0) {
553
712
  "/F"
554
713
  ],
555
714
  startCommand: [
556
- "schtasks.exe",
715
+ control,
557
716
  "/Run",
558
717
  "/TN",
559
718
  "DSH Update Status Web"
560
719
  ],
561
- recoverCommand: "schtasks.exe /Run /TN \"DSH Update Status Web\"",
720
+ recoverCommand: `"${control}" /Run /TN "DSH Update Status Web"`,
562
721
  statusCommand: [
563
- "schtasks.exe",
722
+ control,
564
723
  "/Query",
565
724
  "/TN",
566
725
  "DSH Update Status Web",
@@ -569,13 +728,13 @@ function planFor(spec, uid = 0) {
569
728
  "LIST"
570
729
  ],
571
730
  stopCommand: [
572
- "schtasks.exe",
731
+ control,
573
732
  "/End",
574
733
  "/TN",
575
734
  "DSH Update Status Web"
576
735
  ],
577
736
  uninstallCommand: [
578
- "schtasks.exe",
737
+ control,
579
738
  "/Delete",
580
739
  "/TN",
581
740
  "DSH Update Status Web",
@@ -584,4 +743,4 @@ function planFor(spec, uid = 0) {
584
743
  };
585
744
  }
586
745
  //#endregion
587
- export { WINDOWS_TASK_NAME as a, parseSystemdMainPid as c, bootStampFile as d, countRecentRestarts as f, serviceLogDir as h, SYSTEMD_UNIT as i, parseWindowsTaskRunning as l, recordBootStamp as m, specFromArgs as n, WINDOWS_WRAPPER_BASENAME as o, dshHomeOf as p, LAUNCHD_LABEL as r, parseLaunchdTrackedPid as s, planFor as t, RESTART_STORM_WINDOW_MS as u };
746
+ export { recordBootStamp as C, dshHomeOf as S, parseSystemdMainPid as _, planFor as a, bootStampFile as b, specFromArgs as c, servicePathOf as d, LAUNCHD_LABEL as f, parseLaunchdTrackedPid as g, WINDOWS_WRAPPER_BASENAME as h, pathsFor as i, MAX_SERVICE_PATH_LENGTH as l, WINDOWS_TASK_NAME as m, nativeServiceCommandCandidates as n, servicePlatform as o, SYSTEMD_UNIT as p, nativeServiceCommandOf as r, serviceStateFileFromArgs as s, SERVICE_LABEL as t, normalizeServicePathExt as u, parseWindowsTaskRunning as v, serviceLogDir as w, countRecentRestarts as x, RESTART_STORM_WINDOW_MS as y };
@@ -16,9 +16,44 @@ interface ServiceSpec {
16
16
  readonly home: string;
17
17
  readonly dshHome?: string;
18
18
  readonly logDir: string;
19
+ /** Final audited PATH persisted by install and reused by every later action. */
20
+ readonly servicePath?: string;
21
+ readonly servicePathSource?: 'current' | 'minimal' | 'explicit';
22
+ readonly servicePathCapturedAt?: string;
23
+ /** Windows command extension lookup, persisted alongside PATH. */
24
+ readonly servicePathExt?: string;
25
+ /** Human-readable warnings retained in the receipt and dry-run output. */
26
+ readonly servicePathWarnings?: readonly string[];
27
+ /** Absolute launchctl/systemctl/schtasks path; later actions never search PATH. */
28
+ readonly nativeServiceCommand?: string;
19
29
  /** Written by the user-service definition, never inferred from a TTY. */
20
30
  readonly supervisorMarker: 'dsh-update-status';
21
31
  }
32
+ interface ServicePaths {
33
+ readonly stateFile: string;
34
+ readonly definitionFile: string;
35
+ /** macOS and Windows: the wrapper that owns the restart loop. */
36
+ readonly wrapperFile?: string;
37
+ readonly stdoutFile: string;
38
+ readonly stderrFile: string;
39
+ }
40
+ interface ServicePlan {
41
+ readonly spec: ServiceSpec;
42
+ readonly paths: ServicePaths;
43
+ readonly definition: string;
44
+ readonly wrapper?: string;
45
+ readonly installCommand: readonly string[];
46
+ readonly startCommand: readonly string[];
47
+ /**
48
+ * One copy-only line that lets the service take over again after it stopped
49
+ * (a tripped start limit, a given-up wrapper). Displayed only — never run by
50
+ * the plugin.
51
+ */
52
+ readonly recoverCommand: string;
53
+ readonly statusCommand: readonly string[];
54
+ readonly stopCommand: readonly string[];
55
+ readonly uninstallCommand: readonly string[];
56
+ }
22
57
  interface CliEnvironment {
23
58
  readonly platform: NodeJS.Platform;
24
59
  readonly home: string;
@@ -49,6 +84,7 @@ type ServiceVerification = {
49
84
  };
50
85
  //#endregion
51
86
  //#region src/service/cli.d.ts
87
+ export declare function serialisePlan(plan: ServicePlan): string;
52
88
  export interface CliHooks {
53
89
  readonly preflight?: (spec: ServiceSpec) => void;
54
90
  /** Injected in tests; defaults to the real post-activation verification. */