prismcast 1.10.2 → 1.10.3

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.
Files changed (166) hide show
  1. package/dist/app.d.ts +5 -3
  2. package/dist/app.js +42 -41
  3. package/dist/app.js.map +1 -1
  4. package/dist/browser/hlsPlaylistObserver.d.ts +38 -0
  5. package/dist/browser/hlsPlaylistObserver.js +113 -0
  6. package/dist/browser/hlsPlaylistObserver.js.map +1 -0
  7. package/dist/browser/index.d.ts +26 -16
  8. package/dist/browser/index.js +95 -70
  9. package/dist/browser/index.js.map +1 -1
  10. package/dist/browser/manifestInterceptor.d.ts +29 -29
  11. package/dist/browser/manifestInterceptor.js +106 -212
  12. package/dist/browser/manifestInterceptor.js.map +1 -1
  13. package/dist/browser/tabNetworkObserver.d.ts +48 -0
  14. package/dist/browser/tabNetworkObserver.js +140 -0
  15. package/dist/browser/tabNetworkObserver.js.map +1 -0
  16. package/dist/config/consistencyProbe.js +13 -11
  17. package/dist/config/consistencyProbe.js.map +1 -1
  18. package/dist/config/paths.d.ts +3 -8
  19. package/dist/config/paths.js +3 -10
  20. package/dist/config/paths.js.map +1 -1
  21. package/dist/config/profiles.d.ts +1 -1
  22. package/dist/config/profiles.js +6 -6
  23. package/dist/config/profiles.js.map +1 -1
  24. package/dist/config/services.d.ts +5 -5
  25. package/dist/config/services.js +8 -8
  26. package/dist/config/services.js.map +1 -1
  27. package/dist/config/sites.d.ts +2 -2
  28. package/dist/config/sites.js +6 -6
  29. package/dist/config/sites.js.map +1 -1
  30. package/dist/config/userChannels.d.ts +108 -14
  31. package/dist/config/userChannels.js +291 -37
  32. package/dist/config/userChannels.js.map +1 -1
  33. package/dist/config/userConfig.js +1 -1
  34. package/dist/config/userConfig.js.map +1 -1
  35. package/dist/config/userProfiles.d.ts +3 -3
  36. package/dist/config/userProfiles.js +19 -19
  37. package/dist/config/userProfiles.js.map +1 -1
  38. package/dist/index.js +7 -9
  39. package/dist/index.js.map +1 -1
  40. package/dist/native/index.d.ts +1 -1
  41. package/dist/native/index.js +9 -18
  42. package/dist/native/index.js.map +1 -1
  43. package/dist/native/proxy.d.ts +1 -4
  44. package/dist/native/proxy.js +0 -9
  45. package/dist/native/proxy.js.map +1 -1
  46. package/dist/routes/clientActions.d.ts +97 -0
  47. package/dist/routes/clientActions.js +113 -0
  48. package/dist/routes/clientActions.js.map +1 -0
  49. package/dist/routes/components.d.ts +4 -3
  50. package/dist/routes/components.js +36 -39
  51. package/dist/routes/components.js.map +1 -1
  52. package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
  53. package/dist/routes/config/channels/endpoints/crud.js +37 -72
  54. package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
  55. package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
  56. package/dist/routes/config/channels/healthBridge.d.ts +23 -0
  57. package/dist/routes/config/channels/healthBridge.js +71 -0
  58. package/dist/routes/config/channels/healthBridge.js.map +1 -0
  59. package/dist/routes/config/channels/table.js +97 -84
  60. package/dist/routes/config/channels/table.js.map +1 -1
  61. package/dist/routes/config/services.js +25 -22
  62. package/dist/routes/config/services.js.map +1 -1
  63. package/dist/routes/config/settings.js +9 -6
  64. package/dist/routes/config/settings.js.map +1 -1
  65. package/dist/routes/debug.js +277 -139
  66. package/dist/routes/debug.js.map +1 -1
  67. package/dist/routes/logs.js +1 -1
  68. package/dist/routes/logs.js.map +1 -1
  69. package/dist/routes/root/content.js +18 -14
  70. package/dist/routes/root/content.js.map +1 -1
  71. package/dist/routes/root/index.js +17 -14
  72. package/dist/routes/root/index.js.map +1 -1
  73. package/dist/routes/root/scripts/channels.js +50 -21
  74. package/dist/routes/root/scripts/channels.js.map +1 -1
  75. package/dist/routes/root/scripts/config.js +72 -3
  76. package/dist/routes/root/scripts/config.js.map +1 -1
  77. package/dist/routes/root/scripts/shared.js +60 -0
  78. package/dist/routes/root/scripts/shared.js.map +1 -1
  79. package/dist/routes/root/scripts/status.handlers.d.ts +5 -27
  80. package/dist/routes/root/scripts/status.handlers.js +8 -60
  81. package/dist/routes/root/scripts/status.handlers.js.map +1 -1
  82. package/dist/routes/root/scripts/status.js +6 -1
  83. package/dist/routes/root/scripts/status.js.map +1 -1
  84. package/dist/routes/streams.js +5 -3
  85. package/dist/routes/streams.js.map +1 -1
  86. package/dist/service/commands.context.js +1 -1
  87. package/dist/service/commands.context.js.map +1 -1
  88. package/dist/service/generators.context.js +1 -1
  89. package/dist/service/generators.context.js.map +1 -1
  90. package/dist/service/generators.d.ts +1 -1
  91. package/dist/service/generators.js +3 -11
  92. package/dist/service/generators.js.map +1 -1
  93. package/dist/streaming/hls.js +1 -5
  94. package/dist/streaming/hls.js.map +1 -1
  95. package/dist/streaming/hlsSegments.d.ts +1 -1
  96. package/dist/streaming/hlsSegments.js.map +1 -1
  97. package/dist/streaming/preroll.d.ts +1 -1
  98. package/dist/streaming/preroll.js +1 -5
  99. package/dist/streaming/preroll.js.map +1 -1
  100. package/dist/streaming/statusEmitter.d.ts +5 -8
  101. package/dist/streaming/statusEmitter.js +0 -5
  102. package/dist/streaming/statusEmitter.js.map +1 -1
  103. package/dist/types/channels.d.ts +9 -0
  104. package/dist/types/channels.js.map +1 -1
  105. package/dist/types/profiles.d.ts +2 -2
  106. package/dist/upgrade/commands.context.js +14 -10
  107. package/dist/upgrade/commands.context.js.map +1 -1
  108. package/dist/upgrade/commands.d.ts +7 -15
  109. package/dist/upgrade/commands.js +27 -28
  110. package/dist/upgrade/commands.js.map +1 -1
  111. package/dist/upgrade/detection.context.js +2 -1
  112. package/dist/upgrade/detection.context.js.map +1 -1
  113. package/dist/upgrade/detection.d.ts +2 -1
  114. package/dist/upgrade/detection.js +20 -51
  115. package/dist/upgrade/detection.js.map +1 -1
  116. package/dist/upgrade/index.d.ts +1 -0
  117. package/dist/upgrade/index.js +1 -0
  118. package/dist/upgrade/index.js.map +1 -1
  119. package/dist/upgrade/lifecycle.context.d.ts +19 -0
  120. package/dist/upgrade/lifecycle.context.js +47 -0
  121. package/dist/upgrade/lifecycle.context.js.map +1 -0
  122. package/dist/upgrade/lifecycle.d.ts +72 -0
  123. package/dist/upgrade/lifecycle.js +176 -0
  124. package/dist/upgrade/lifecycle.js.map +1 -0
  125. package/dist/upgrade/pathHandle.d.ts +28 -0
  126. package/dist/upgrade/pathHandle.js +101 -0
  127. package/dist/upgrade/pathHandle.js.map +1 -0
  128. package/dist/utils/bootSession.context.d.ts +15 -0
  129. package/dist/utils/bootSession.context.js +59 -0
  130. package/dist/utils/bootSession.context.js.map +1 -0
  131. package/dist/utils/bootSession.d.ts +16 -0
  132. package/dist/utils/bootSession.js +20 -0
  133. package/dist/utils/bootSession.js.map +1 -0
  134. package/dist/utils/clock.js +1 -1
  135. package/dist/utils/ffmpeg.js +1 -1
  136. package/dist/utils/ffmpeg.js.map +1 -1
  137. package/dist/utils/index.d.ts +3 -0
  138. package/dist/utils/index.js +3 -0
  139. package/dist/utils/index.js.map +1 -1
  140. package/dist/utils/markup.d.ts +10 -0
  141. package/dist/utils/markup.js +23 -0
  142. package/dist/utils/markup.js.map +1 -1
  143. package/dist/utils/pid.d.ts +2 -25
  144. package/dist/utils/pid.js +10 -49
  145. package/dist/utils/pid.js.map +1 -1
  146. package/dist/utils/processInspector.context.d.ts +7 -0
  147. package/dist/utils/processInspector.context.js +104 -0
  148. package/dist/utils/processInspector.context.js.map +1 -0
  149. package/dist/utils/processInspector.d.ts +54 -0
  150. package/dist/utils/processInspector.js +125 -0
  151. package/dist/utils/processInspector.js.map +1 -0
  152. package/dist/utils/retry.d.ts +1 -1
  153. package/dist/utils/retry.js +1 -1
  154. package/dist/utils/retry.js.map +1 -1
  155. package/dist/utils/runtimeIdentity.context.d.ts +6 -0
  156. package/dist/utils/runtimeIdentity.context.js +13 -0
  157. package/dist/utils/runtimeIdentity.context.js.map +1 -0
  158. package/dist/utils/runtimeIdentity.d.ts +96 -0
  159. package/dist/utils/runtimeIdentity.js +171 -0
  160. package/dist/utils/runtimeIdentity.js.map +1 -0
  161. package/dist/utils/timing.d.ts +1 -1
  162. package/dist/utils/timing.js +0 -5
  163. package/dist/utils/timing.js.map +1 -1
  164. package/dist/utils/version.js +40 -10
  165. package/dist/utils/version.js.map +1 -1
  166. package/package.json +2 -2
@@ -0,0 +1,19 @@
1
+ import type { UpgradeLifecycleContext } from "./lifecycle.ts";
2
+ /**
3
+ * Builds the default UpgradeLifecycleContext from real runtime I/O.
4
+ *
5
+ * runCommand inherits the user's terminal so npm/brew output flows live; it captures only the exit outcome, not the captured stdio. spawnDetached launches a
6
+ * fully decoupled child process - detached, stdio ignored, windowsHide on - then unref's so the child can outlive the parent. The Windows handoff strategy is
7
+ * the sole caller; on POSIX the strategy never invokes it.
8
+ *
9
+ * serviceTaskName resolves to the documented PrismCast service identifier when running on Windows. It is the empty string on other platforms because the field
10
+ * is only consumed by the Windows handoff helper. We always send the identifier (rather than gating on isRunningAsService) because the helper itself probes
11
+ * Get-ScheduledTask for the running task and skips the stop/start steps cleanly when none is registered...the user may be running a global npm install of
12
+ * prismcast on Windows without having registered a service yet, and the upgrade flow must still work.
13
+ *
14
+ * upgradeLogPath sits inside the PrismCast data directory at `upgrade.log`. The helper appends timestamped lines and the upgrade command's captured stdout to
15
+ * this file; the user is told its location in the "handed-off" UpgradeStep so they can tail or read it.
16
+ *
17
+ * @returns An UpgradeLifecycleContext populated from the live process.
18
+ */
19
+ export declare function createDefaultLifecycleContext(): UpgradeLifecycleContext;
@@ -0,0 +1,47 @@
1
+ import { execSync, spawn } from "node:child_process";
2
+ import { SERVICE_NAME } from "../identity.js";
3
+ import { getDataDir } from "../config/paths.js";
4
+ import path from "node:path";
5
+ /**
6
+ * Builds the default UpgradeLifecycleContext from real runtime I/O.
7
+ *
8
+ * runCommand inherits the user's terminal so npm/brew output flows live; it captures only the exit outcome, not the captured stdio. spawnDetached launches a
9
+ * fully decoupled child process - detached, stdio ignored, windowsHide on - then unref's so the child can outlive the parent. The Windows handoff strategy is
10
+ * the sole caller; on POSIX the strategy never invokes it.
11
+ *
12
+ * serviceTaskName resolves to the documented PrismCast service identifier when running on Windows. It is the empty string on other platforms because the field
13
+ * is only consumed by the Windows handoff helper. We always send the identifier (rather than gating on isRunningAsService) because the helper itself probes
14
+ * Get-ScheduledTask for the running task and skips the stop/start steps cleanly when none is registered...the user may be running a global npm install of
15
+ * prismcast on Windows without having registered a service yet, and the upgrade flow must still work.
16
+ *
17
+ * upgradeLogPath sits inside the PrismCast data directory at `upgrade.log`. The helper appends timestamped lines and the upgrade command's captured stdout to
18
+ * this file; the user is told its location in the "handed-off" UpgradeStep so they can tail or read it.
19
+ *
20
+ * @returns An UpgradeLifecycleContext populated from the live process.
21
+ */
22
+ export function createDefaultLifecycleContext() {
23
+ return {
24
+ parentPid: process.pid,
25
+ platform: process.platform,
26
+ runCommand: (cmd, options) => {
27
+ try {
28
+ execSync(cmd, { cwd: options.cwd, encoding: "utf-8", stdio: "inherit" });
29
+ return { success: true };
30
+ }
31
+ catch {
32
+ return { success: false };
33
+ }
34
+ },
35
+ serviceTaskName: (process.platform === "win32") ? SERVICE_NAME : "",
36
+ spawnDetached: (command, args) => {
37
+ const child = spawn(command, [...args], {
38
+ detached: true,
39
+ stdio: "ignore",
40
+ windowsHide: true
41
+ });
42
+ child.unref();
43
+ },
44
+ upgradeLogPath: path.join(getDataDir(), "upgrade.log")
45
+ };
46
+ }
47
+ //# sourceMappingURL=lifecycle.context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.context.js","sourceRoot":"","sources":["../../src/upgrade/lifecycle.context.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,6BAA6B;IAE3C,OAAO;QAEL,SAAS,EAAE,OAAO,CAAC,GAAG;QACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,UAAU,EAAE,CAAC,GAAW,EAAE,OAAkC,EAAoB,EAAE;YAEhF,IAAI,CAAC;gBAEH,QAAQ,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;gBAEzE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAC3B,CAAC;YAAC,MAAM,CAAC;gBAEP,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YAC5B,CAAC;QACH,CAAC;QACD,eAAe,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE;QACnE,aAAa,EAAE,CAAC,OAAe,EAAE,IAAuB,EAAQ,EAAE;YAEhE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE;gBAEtC,QAAQ,EAAE,IAAI;gBACd,KAAK,EAAE,QAAQ;gBACf,WAAW,EAAE,IAAI;aAClB,CAAC,CAAC;YAEH,KAAK,CAAC,KAAK,EAAE,CAAC;QAChB,CAAC;QACD,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,aAAa,CAAC;KACvD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,72 @@
1
+ import type { InstallInfo } from "./detection.ts";
2
+ /**
3
+ * Outcome of a single upgrade attempt. Discriminated union over `kind`. Consumers narrow on it to choose between "the upgrade ran in-process, here is the
4
+ * outcome" and "a detached helper is taking it from here, exit immediately and trust the helper to finish the job."
5
+ *
6
+ * The "handed-off" variant is structurally permissible only because the strategy that produced it has already spawned the helper before returning; the field
7
+ * `logPath` is the path the helper writes its progress to so the user has a place to look while the upgrade is running.
8
+ */
9
+ export type UpgradeStep = {
10
+ readonly kind: "ran";
11
+ readonly success: boolean;
12
+ } | {
13
+ readonly kind: "handed-off";
14
+ readonly logPath: string;
15
+ };
16
+ /**
17
+ * Result of running an upgrade subprocess. Mirrors the same-named type in commands.ts...the in-process strategy passes this through verbatim, so callers of
18
+ * either layer see the same shape. The success flag distinguishes ran-without-error from threw-or-exited-non-zero; the runner does not surface the actual
19
+ * error because the user sees subprocess stderr directly on the terminal.
20
+ */
21
+ export interface UpgradeRunResult {
22
+ readonly success: boolean;
23
+ }
24
+ /**
25
+ * Runtime context the lifecycle strategies consume. Each field is one narrow capability some strategy needs. POSIX strategies use only `runCommand`; the
26
+ * Windows handoff strategy uses `parentPid`, `serviceTaskName`, `spawnDetached`, `upgradeLogPath`. Splitting them onto one struct keeps the adapter (and the
27
+ * test seam) a single object, even though no strategy uses every field.
28
+ */
29
+ export interface UpgradeLifecycleContext {
30
+ readonly parentPid: number;
31
+ readonly platform: NodeJS.Platform;
32
+ readonly runCommand: (cmd: string, options: {
33
+ readonly cwd?: string;
34
+ }) => UpgradeRunResult;
35
+ readonly serviceTaskName: string;
36
+ readonly spawnDetached: (command: string, args: readonly string[]) => void;
37
+ readonly upgradeLogPath: string;
38
+ }
39
+ /**
40
+ * A platform-strategy record. The registry is a flat tuple of these; the dispatcher picks the first whose `platforms` list includes the current OS. Pure
41
+ * function over UpgradeLifecycleContext + InstallInfo; no state is owned by the strategy itself.
42
+ */
43
+ export interface UpgradeLifecycleStrategy {
44
+ readonly id: string;
45
+ readonly platforms: readonly NodeJS.Platform[];
46
+ readonly perform: (ctx: UpgradeLifecycleContext, info: InstallInfo) => UpgradeStep;
47
+ }
48
+ /**
49
+ * The upgrade-lifecycle strategy registry. Same runtime data as STRATEGY_TUPLE; the public name carries the wide UpgradeLifecycleStrategy type so consumers
50
+ * and tests see one uniform shape. Adding a new platform-specific strategy is appending an entry to STRATEGY_TUPLE; the dispatcher and the adapter never
51
+ * change.
52
+ */
53
+ export declare const UPGRADE_LIFECYCLES: readonly UpgradeLifecycleStrategy[];
54
+ /**
55
+ * Selects the lifecycle strategy for a given platform. Falls back to the POSIX in-process strategy when no strategy lists the platform...this matches
56
+ * PrismCast's historical behavior for any Unix-like host that is not specifically called out and keeps the function total (no thrown error from a one-off
57
+ * platform name).
58
+ *
59
+ * @param platform - The OS platform identifier.
60
+ * @returns The matching strategy, or the POSIX default when no strategy serves the platform.
61
+ */
62
+ export declare function selectLifecycle(platform: NodeJS.Platform): UpgradeLifecycleStrategy;
63
+ /**
64
+ * Performs an upgrade by selecting the platform-appropriate strategy and invoking its `perform` callback. Pure dispatcher; all I/O happens inside the
65
+ * strategy's own callbacks (which themselves receive an UpgradeLifecycleContext that the adapter wires from real I/O). Tests inject context literals and
66
+ * exercise each strategy through this entry point.
67
+ *
68
+ * @param ctx - The lifecycle context.
69
+ * @param info - The detected install info.
70
+ * @returns The upgrade outcome.
71
+ */
72
+ export declare function performUpgrade(ctx: UpgradeLifecycleContext, info: InstallInfo): UpgradeStep;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * The PowerShell helper that the Windows handoff strategy spawns. Reads four positional parameters: the parent process ID, the optional service task name
3
+ * (empty string when no service is registered), the upgrade command exactly as detection.ts produced it, and the optional working directory for npm-local
4
+ * installs (empty string for npm-global).
5
+ *
6
+ * Sequence:
7
+ *
8
+ * 1. Wait for the parent process to exit. Wait-Process with -ErrorAction SilentlyContinue tolerates the (theoretical) case where the parent has already exited
9
+ * between us being spawned and this line running.
10
+ * 2. If a service task is registered AND running, disable it (so RestartOnFailure does not interfere) and stop it. Stop-ScheduledTask is non-blocking, so we
11
+ * poll for the task to transition out of Running with a 30-second ceiling - the same poll pattern as service/generators.ts's WINDOWS_REGISTER_SCRIPT.
12
+ * 3. Run the upgrade command via cmd.exe /c so npm/brew shell-style command lines parse correctly. Output is captured for the log; the exit code is captured
13
+ * so we can re-raise it as our own exit code at the very end.
14
+ * 4. If a service task is registered, re-enable it and start it. The re-enable is necessary because Disable-ScheduledTask persists across restarts; without
15
+ * this step, the next reboot would silently leave the service disabled.
16
+ * 5. Exit with the upgrade's exit code so the helper's own exit reflects the upgrade outcome (visible in the log via $LASTEXITCODE).
17
+ *
18
+ * Logging is best-effort: a failed Out-File call (e.g., the directory does not exist) is swallowed by SilentlyContinue so the helper still completes its
19
+ * primary job. The log itself is the only user-facing artifact of the helper, and we surface its path back to commands.ts as part of the UpgradeStep.
20
+ */
21
+ const WINDOWS_HANDOFF_SCRIPT = [
22
+ "param($ParentPid, $TaskName, $UpgradeCommand, $WorkingDir, $LogPath)",
23
+ "function Write-UpgradeLog($message) {",
24
+ " $stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'",
25
+ " try { Add-Content -Path $LogPath -Value ('[' + $stamp + '] ' + $message) -Encoding utf8 -ErrorAction SilentlyContinue } catch {}",
26
+ "}",
27
+ "Write-UpgradeLog ('Helper started; waiting for parent PID ' + $ParentPid + ' to exit.')",
28
+ "try { Wait-Process -Id $ParentPid -ErrorAction SilentlyContinue } catch {}",
29
+ "Write-UpgradeLog 'Parent exited.'",
30
+ "if($TaskName) {",
31
+ " $existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue",
32
+ " if($existing) {",
33
+ " Write-UpgradeLog ('Disabling and stopping task ' + $TaskName + '.')",
34
+ " Disable-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue | Out-Null",
35
+ " Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue | Out-Null",
36
+ " $deadline = (Get-Date).AddSeconds(30)",
37
+ " while((Get-Date) -lt $deadline) {",
38
+ " $state = (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue).State",
39
+ " if(($null -eq $state) -or ($state -ne 'Running')) { break }",
40
+ " Start-Sleep -Milliseconds 250",
41
+ " }",
42
+ " Write-UpgradeLog ('Task state after stop: ' + $state + '.')",
43
+ " } else {",
44
+ " Write-UpgradeLog 'No matching scheduled task found; skipping stop/start.'",
45
+ " }",
46
+ "}",
47
+ "if($WorkingDir) { Set-Location -Path $WorkingDir }",
48
+ "Write-UpgradeLog ('Running upgrade command: ' + $UpgradeCommand)",
49
+ "$upgradeOutput = (& cmd.exe /c $UpgradeCommand 2>&1) | Out-String",
50
+ "$upgradeExit = $LASTEXITCODE",
51
+ "try { Add-Content -Path $LogPath -Value $upgradeOutput -Encoding utf8 -ErrorAction SilentlyContinue } catch {}",
52
+ "Write-UpgradeLog ('Upgrade exit code: ' + $upgradeExit + '.')",
53
+ "if($TaskName) {",
54
+ " $existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue",
55
+ " if($existing) {",
56
+ " Write-UpgradeLog ('Re-enabling and starting task ' + $TaskName + '.')",
57
+ " Enable-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue | Out-Null",
58
+ " Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue | Out-Null",
59
+ " }",
60
+ "}",
61
+ "Write-UpgradeLog 'Helper finished.'",
62
+ "exit $upgradeExit"
63
+ ].join("\n");
64
+ /**
65
+ * Escapes a value for embedding inside a single-quoted PowerShell string. PowerShell's single-quoted strings are literal (no interpolation, no backslash
66
+ * escapes); the only character that requires escaping is the single quote itself, written as two consecutive single quotes. Mirrors the same helper in
67
+ * service/generators.ts so the two PowerShell-emitting modules agree on the one escape rule that matters.
68
+ *
69
+ * @param value - The value to escape.
70
+ * @returns The value with internal single quotes doubled.
71
+ */
72
+ function escapeSingleQuoted(value) {
73
+ return value.replaceAll("'", "''");
74
+ }
75
+ /**
76
+ * Wraps a value in a PowerShell single-quoted literal.
77
+ * @param value - The value to quote.
78
+ * @returns The literal including its surrounding quotes.
79
+ */
80
+ function powerShellLiteral(value) {
81
+ return "'" + escapeSingleQuoted(value) + "'";
82
+ }
83
+ /**
84
+ * Builds the PowerShell command string that the Windows handoff strategy passes to powershell.exe via -Command. The script body is wrapped in `& { ... }` and
85
+ * the positional arguments are appended after the closing brace, exactly as PowerShell's parser expects. Each argument is single-quote-escaped via
86
+ * powerShellLiteral, so there is one - and only one - escape surface (the single-quote doubling rule).
87
+ *
88
+ * @param args - The positional arguments to pass to the helper script's param() block, in declaration order.
89
+ * @returns The fully composed `-Command` string.
90
+ */
91
+ function buildHandoffCommand(args) {
92
+ const quoted = args.map((value) => powerShellLiteral(value)).join(" ");
93
+ return "& { " + WINDOWS_HANDOFF_SCRIPT + " } " + quoted;
94
+ }
95
+ /**
96
+ * POSIX in-process strategy. macOS and Linux can run `npm install -g` (or `brew upgrade`) directly from inside the running prismcast process because POSIX
97
+ * filesystems do not lock open files against directory rename. The strategy is a thin pass-through over ctx.runCommand. The caller (commands.ts) decides
98
+ * whether to exit afterward so the service manager (launchd KeepAlive, systemd Restart=) brings PrismCast back up.
99
+ */
100
+ const POSIX_IN_PROCESS_LIFECYCLE = {
101
+ id: "posix-in-process",
102
+ perform: (ctx, info) => {
103
+ const result = ctx.runCommand(info.upgradeCommand, { cwd: info.packageDir });
104
+ return { kind: "ran", success: result.success };
105
+ },
106
+ platforms: ["darwin", "linux"]
107
+ };
108
+ /**
109
+ * Windows handoff strategy. Composes the PowerShell helper command line, hands it to ctx.spawnDetached, and returns "handed-off." The helper waits for the
110
+ * current process to exit (so the prismcast directory's file locks release), runs the upgrade, then restarts the scheduled task if one exists. This is the
111
+ * Windows EBUSY + auto-restart fix: the parent process exits cleanly with an "upgrade running in the background" message; the detached helper handles the rest.
112
+ *
113
+ * The PowerShell launch is identical to the rest of the codebase: -NoProfile / -NonInteractive for a clean shell, -ExecutionPolicy Bypass so installer-style
114
+ * scripted scenarios are not gated by the interactive-use policy, -WindowStyle Hidden so the user sees no console window pop up, and -Command with our
115
+ * single-quote-escaped script string. The launcher is spawned detached, with stdio ignored and windowsHide set, so it survives the parent's exit.
116
+ */
117
+ const WINDOWS_HANDOFF_LIFECYCLE = {
118
+ id: "windows-handoff",
119
+ perform: (ctx, info) => {
120
+ const handoffCommand = buildHandoffCommand([
121
+ String(ctx.parentPid),
122
+ ctx.serviceTaskName,
123
+ info.upgradeCommand,
124
+ info.packageDir ?? "",
125
+ ctx.upgradeLogPath
126
+ ]);
127
+ ctx.spawnDetached("powershell.exe", [
128
+ "-NoProfile",
129
+ "-NonInteractive",
130
+ "-ExecutionPolicy", "Bypass",
131
+ "-WindowStyle", "Hidden",
132
+ "-Command", handoffCommand
133
+ ]);
134
+ return { kind: "handed-off", logPath: ctx.upgradeLogPath };
135
+ },
136
+ platforms: ["win32"]
137
+ };
138
+ /* The strategy registry as a narrow tuple. Declared `as const` so each entry retains its literal shape...the dispatcher walks this in order and picks the
139
+ * first match. POSIX comes first because it serves more platforms; Windows comes second because it serves only one. The default-fallback semantics live in
140
+ * performUpgrade so an unknown platform (e.g., freebsd) falls through to the POSIX strategy, which is the most permissive choice and matches PrismCast's
141
+ * historical pre-Windows behavior.
142
+ */
143
+ const STRATEGY_TUPLE = [
144
+ POSIX_IN_PROCESS_LIFECYCLE,
145
+ WINDOWS_HANDOFF_LIFECYCLE
146
+ ];
147
+ /**
148
+ * The upgrade-lifecycle strategy registry. Same runtime data as STRATEGY_TUPLE; the public name carries the wide UpgradeLifecycleStrategy type so consumers
149
+ * and tests see one uniform shape. Adding a new platform-specific strategy is appending an entry to STRATEGY_TUPLE; the dispatcher and the adapter never
150
+ * change.
151
+ */
152
+ export const UPGRADE_LIFECYCLES = STRATEGY_TUPLE;
153
+ /**
154
+ * Selects the lifecycle strategy for a given platform. Falls back to the POSIX in-process strategy when no strategy lists the platform...this matches
155
+ * PrismCast's historical behavior for any Unix-like host that is not specifically called out and keeps the function total (no thrown error from a one-off
156
+ * platform name).
157
+ *
158
+ * @param platform - The OS platform identifier.
159
+ * @returns The matching strategy, or the POSIX default when no strategy serves the platform.
160
+ */
161
+ export function selectLifecycle(platform) {
162
+ return UPGRADE_LIFECYCLES.find((strategy) => strategy.platforms.includes(platform)) ?? POSIX_IN_PROCESS_LIFECYCLE;
163
+ }
164
+ /**
165
+ * Performs an upgrade by selecting the platform-appropriate strategy and invoking its `perform` callback. Pure dispatcher; all I/O happens inside the
166
+ * strategy's own callbacks (which themselves receive an UpgradeLifecycleContext that the adapter wires from real I/O). Tests inject context literals and
167
+ * exercise each strategy through this entry point.
168
+ *
169
+ * @param ctx - The lifecycle context.
170
+ * @param info - The detected install info.
171
+ * @returns The upgrade outcome.
172
+ */
173
+ export function performUpgrade(ctx, info) {
174
+ return selectLifecycle(ctx.platform).perform(ctx, info);
175
+ }
176
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../src/upgrade/lifecycle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A platform-aware handle on an absolute filesystem path. Strategies reason about install layout exclusively through this type; the underlying separator,
3
+ * case-folding rules, and segment topology are encapsulated. Adding a new structural query is an additive edit to this interface; the factory below is the only
4
+ * implementation site.
5
+ */
6
+ export interface PathHandle {
7
+ readonly raw: string;
8
+ hasSegmentChain(...segments: readonly string[]): boolean;
9
+ isUnder(prefix: string): boolean;
10
+ join(...segments: readonly string[]): string;
11
+ parentBefore(...segments: readonly string[]): PathHandle | null;
12
+ }
13
+ /**
14
+ * Options accepted by createPathHandle. Today there is only the platform override, used by tests to produce Windows-flavored handles on a Linux host.
15
+ */
16
+ export interface PathHandleOptions {
17
+ readonly platform?: NodeJS.Platform;
18
+ }
19
+ /**
20
+ * Constructs a PathHandle from an absolute path string. The factory captures the platform once and dispatches every subsequent query through `path.win32` or
21
+ * `path.posix` accordingly, so behavior is independent of where the host process runs. The handle is logically immutable...returned fields are typed readonly
22
+ * and the closed-over state is never mutated.
23
+ *
24
+ * @param raw - The OS-native absolute path string. Empty or relative inputs produce a degenerate handle whose query methods return false / null defensively.
25
+ * @param options - Optional overrides. Today only the platform override is supported.
26
+ * @returns A platform-aware PathHandle.
27
+ */
28
+ export declare function createPathHandle(raw: string, options?: PathHandleOptions): PathHandle;
@@ -0,0 +1,101 @@
1
+ /* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * pathHandle.ts: A narrow value type that wraps an absolute filesystem path with platform-aware structural query primitives. This module is the single source of
4
+ * truth for path semantics in the upgrade-detection layer - separator selection, case-folding rules, segment splitting, and prefix containment all live here. The
5
+ * detection strategies receive a PathHandle (not a raw string) and reason about install layout exclusively through its narrow API. By construction a strategy
6
+ * cannot write `currentFile.includes("/node_modules/")` and accidentally hard-code POSIX semantics into a cross-platform code path, because PathHandle has no
7
+ * `.includes` method...the only way to test for a "node_modules/prismcast" run in the path is `hasSegmentChain("node_modules", "prismcast")`, which dispatches to
8
+ * the right separator and case-folding rule automatically.
9
+ *
10
+ * The factory accepts an explicit platform override so cross-platform tests can produce Windows-flavored handles on a Linux CI host. Internally, the factory
11
+ * dispatches every path operation to `path.win32` or `path.posix` rather than the ambient `path` module, so behavior is deterministic regardless of where the
12
+ * host process runs. macOS is treated as case-sensitive, matching Node's `path.posix` semantics; APFS volumes with case-sensitive flags work correctly, and
13
+ * macOS users on default case-insensitive volumes are unaffected because `import.meta.url` and any subprocess output agree on canonical casing in the install
14
+ * layouts PrismCast targets.
15
+ *
16
+ * The API is deliberately small. `raw` is the lone escape hatch, used for handoff to subprocess cwd (the npm-local resolver's packageDir) and for display. Every
17
+ * other operation is a structural query or platform-aware composition, so strategy authors never need to reason about separators or case-folding rules.
18
+ */
19
+ import path from "node:path";
20
+ /**
21
+ * Constructs a PathHandle from an absolute path string. The factory captures the platform once and dispatches every subsequent query through `path.win32` or
22
+ * `path.posix` accordingly, so behavior is independent of where the host process runs. The handle is logically immutable...returned fields are typed readonly
23
+ * and the closed-over state is never mutated.
24
+ *
25
+ * @param raw - The OS-native absolute path string. Empty or relative inputs produce a degenerate handle whose query methods return false / null defensively.
26
+ * @param options - Optional overrides. Today only the platform override is supported.
27
+ * @returns A platform-aware PathHandle.
28
+ */
29
+ export function createPathHandle(raw, options = {}) {
30
+ const platform = options.platform ?? process.platform;
31
+ const isWindows = (platform === "win32");
32
+ const pathLib = isWindows ? path.win32 : path.posix;
33
+ // Case-folding rule. Windows is the only platform where the OS treats the filesystem as case-insensitive for path comparisons. POSIX (including macOS at the
34
+ // Node API layer) is case-sensitive. Folding both sides of every comparison consistently is sufficient; we do not need to canonicalize the raw path itself.
35
+ const fold = (segment) => isWindows ? segment.toLowerCase() : segment;
36
+ // Pre-computed segment lists. rawSegments preserves the original casing for path reconstruction (parentBefore needs to splice the original spelling back
37
+ // together so the resulting handle's raw path is byte-faithful to the input). normalized is the folded form used for every comparison.
38
+ const rawSegments = raw.split(pathLib.sep);
39
+ const normalized = rawSegments.map(fold);
40
+ // Locates the first index at which a normalized segment chain matches the path. Returns -1 when there is no match, and also when the chain is empty...we
41
+ // explicitly refuse the vacuous-truth case so an empty rest-parameter expansion never produces a surprising hit.
42
+ const findChain = (chain) => {
43
+ if (chain.length === 0) {
44
+ return -1;
45
+ }
46
+ const target = chain.map(fold);
47
+ const lastStart = normalized.length - target.length;
48
+ for (let i = 0; i <= lastStart; i++) {
49
+ if (target.every((segment, j) => normalized[i + j] === segment)) {
50
+ return i;
51
+ }
52
+ }
53
+ return -1;
54
+ };
55
+ return {
56
+ hasSegmentChain(...segments) {
57
+ return findChain(segments) !== -1;
58
+ },
59
+ isUnder(prefix) {
60
+ // Defensive guard. An empty or relative prefix is not anchorable in the install-detection sense...relative containment is undefined for absolute filesystem
61
+ // paths, and an empty prefix would trivially match every path, which is never useful here.
62
+ if (!prefix || !pathLib.isAbsolute(prefix)) {
63
+ return false;
64
+ }
65
+ // We compare segment lists rather than calling pathLib.relative because Node's relative() has implementation-defined case-handling on Windows that varies
66
+ // by version and is undocumented at the API surface. A hand-rolled split-and-compare gives us one explicit rule that we own and can test. Empty segments
67
+ // (which arise from leading separators on POSIX and the UNC double-leading on Windows) are filtered out symmetrically so the comparison is well-defined
68
+ // for every absolute layout we care about.
69
+ const prefixSegments = pathLib.normalize(prefix).split(pathLib.sep).map(fold).filter((segment) => segment.length > 0);
70
+ const haystack = normalized.filter((segment) => segment.length > 0);
71
+ if ((prefixSegments.length === 0) || (prefixSegments.length >= haystack.length)) {
72
+ return false;
73
+ }
74
+ for (let i = 0; i < prefixSegments.length; i++) {
75
+ if (haystack[i] !== prefixSegments[i]) {
76
+ return false;
77
+ }
78
+ }
79
+ return true;
80
+ },
81
+ join(...segments) {
82
+ return pathLib.join(raw, ...segments);
83
+ },
84
+ parentBefore(...segments) {
85
+ const index = findChain(segments);
86
+ // index <= 0 means no match, or a match at position zero with no parent above it. Either way there is no meaningful parent path to return.
87
+ if (index <= 0) {
88
+ return null;
89
+ }
90
+ const parentRaw = rawSegments.slice(0, index).join(pathLib.sep);
91
+ // The reconstructed parent must be a meaningful absolute path. A bare Windows drive letter ("C:") or the POSIX root with no segments above the chain
92
+ // ("") fails this check and we return null...these are degenerate install layouts (prismcast at the filesystem root) that have no usable project root.
93
+ if (!pathLib.isAbsolute(parentRaw)) {
94
+ return null;
95
+ }
96
+ return createPathHandle(parentRaw, { platform });
97
+ },
98
+ raw
99
+ };
100
+ }
101
+ //# sourceMappingURL=pathHandle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pathHandle.js","sourceRoot":"","sources":["../../src/upgrade/pathHandle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { BootSessionContext } from "./bootSession.ts";
2
+ import type { Nullable } from "../types/index.ts";
3
+ /**
4
+ * Builds the default BootSessionContext from real runtime I/O, snapshotting every reading at construction time so subsequent accessor calls return frozen values.
5
+ * @returns A BootSessionContext whose readings are frozen at the moment of this call.
6
+ */
7
+ export declare function createDefaultBootSessionContext(): BootSessionContext;
8
+ /**
9
+ * Parses field 22 (starttime) out of a /proc/<pid>/stat payload. The format embeds the executable name in parentheses at field 2; that name can itself contain
10
+ * whitespace and parentheses, so we anchor on the last closing paren before splitting the remainder on whitespace. The first field after the closing paren is
11
+ * field 3 (state), which makes the starttime we want land at index 19 of the post-paren split. Exported for direct unit testing.
12
+ * @param raw - The /proc/<pid>/stat payload as a string.
13
+ * @returns The starttime field as a string, or null when the payload is malformed.
14
+ */
15
+ export declare function parsePid1StartTicks(raw: string): Nullable<string>;
@@ -0,0 +1,59 @@
1
+ import fs from "node:fs";
2
+ import { isRunningInContainer } from "./platform.js";
3
+ import os from "node:os";
4
+ /**
5
+ * Builds the default BootSessionContext from real runtime I/O, snapshotting every reading at construction time so subsequent accessor calls return frozen values.
6
+ * @returns A BootSessionContext whose readings are frozen at the moment of this call.
7
+ */
8
+ export function createDefaultBootSessionContext() {
9
+ // Snapshot the host boot minute by computing it from system uptime. os.uptime() is sleep-inclusive on Linux, macOS, and Windows (Node delegates to platform
10
+ // primitives that all include suspended time), so this value remains stable across sleep/wake cycles. Rounding to the nearest minute absorbs sub-minute NTP
11
+ // drift between writes and reads. Future upgrade: on Linux, /proc/sys/kernel/random/boot_id is a kernel-generated UUID that is regenerated each boot and
12
+ // immune to clock drift; switching this branch to read that file would eliminate the minute-boundary jitter entirely. macOS (sysctl kern.boottime) and
13
+ // Windows (Win32_OS.LastBootUpTime) have equivalent kernel facts but require a subprocess; deferred until a real boundary-jitter incident motivates the work.
14
+ const hostBootMinuteSnapshot = Math.round((Date.now() - (os.uptime() * 1000)) / 60000);
15
+ // Snapshot the container detection result. The project-wide helper reads PRISMCAST_CONTAINER and /.dockerenv; both are stable for the process lifetime.
16
+ const inContainerSnapshot = isRunningInContainer();
17
+ // Snapshot the container instance tag only when in a container. Outside a container /proc/1/stat is the host init, which would not distinguish container
18
+ // restarts within the same host boot; skipping the read avoids spurious work on macOS and Windows where /proc does not exist.
19
+ const containerInstanceTagSnapshot = inContainerSnapshot ? readPid1StartTicks() : null;
20
+ return {
21
+ containerInstanceTag: () => containerInstanceTagSnapshot,
22
+ hostBootMinute: () => hostBootMinuteSnapshot,
23
+ inContainer: () => inContainerSnapshot
24
+ };
25
+ }
26
+ /**
27
+ * Reads field 22 (starttime, clock ticks since boot) from /proc/1/stat. ENOENT on non-Linux platforms and any read failure resolve to null. The parsing is
28
+ * delegated to parsePid1StartTicks so it can be exercised in tests without an injectable filesystem.
29
+ * @returns The container init's starttime as a string, or null when /proc/1/stat is unavailable or malformed.
30
+ */
31
+ function readPid1StartTicks() {
32
+ try {
33
+ return parsePid1StartTicks(fs.readFileSync("/proc/1/stat", "utf-8"));
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /**
40
+ * Parses field 22 (starttime) out of a /proc/<pid>/stat payload. The format embeds the executable name in parentheses at field 2; that name can itself contain
41
+ * whitespace and parentheses, so we anchor on the last closing paren before splitting the remainder on whitespace. The first field after the closing paren is
42
+ * field 3 (state), which makes the starttime we want land at index 19 of the post-paren split. Exported for direct unit testing.
43
+ * @param raw - The /proc/<pid>/stat payload as a string.
44
+ * @returns The starttime field as a string, or null when the payload is malformed.
45
+ */
46
+ export function parsePid1StartTicks(raw) {
47
+ const rparen = raw.lastIndexOf(")");
48
+ if (rparen === -1) {
49
+ return null;
50
+ }
51
+ const fields = raw.slice(rparen + 1).trim().split(/\s+/);
52
+ if (fields.length < 20) {
53
+ return null;
54
+ }
55
+ // Coalesce to null even though the length guard above proves the index is in range; TypeScript's noUncheckedIndexedAccess still types fields[19] as
56
+ // string | undefined and the explicit ?? null keeps the return type honest.
57
+ return fields[19] ?? null;
58
+ }
59
+ //# sourceMappingURL=bootSession.context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootSession.context.js","sourceRoot":"","sources":["../../src/utils/bootSession.context.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB;;;GAGG;AACH,MAAM,UAAU,+BAA+B;IAE7C,4JAA4J;IAC5J,4JAA4J;IAC5J,yJAAyJ;IACzJ,uJAAuJ;IACvJ,8JAA8J;IAC9J,MAAM,sBAAsB,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC;IAEvF,wJAAwJ;IACxJ,MAAM,mBAAmB,GAAG,oBAAoB,EAAE,CAAC;IAEnD,yJAAyJ;IACzJ,8HAA8H;IAC9H,MAAM,4BAA4B,GAAG,mBAAmB,CAAC,CAAC,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAEvF,OAAO;QAEL,oBAAoB,EAAE,GAAG,EAAE,CAAC,4BAA4B;QACxD,cAAc,EAAE,GAAG,EAAE,CAAC,sBAAsB;QAC5C,WAAW,EAAE,GAAG,EAAE,CAAC,mBAAmB;KACvC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,kBAAkB;IAEzB,IAAI,CAAC;QAEH,OAAO,mBAAmB,CAAC,EAAE,CAAC,YAAY,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QAEP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAE7C,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAEpC,IAAG,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QAEjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAEzD,IAAG,MAAM,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QAEtB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,oJAAoJ;IACpJ,4EAA4E;IAC5E,OAAO,MAAM,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { Nullable } from "../types/index.ts";
2
+ /**
3
+ * The runtime capability set getBootSessionId consumes. Production wires it from real I/O via createDefaultBootSessionContext; tests pass a context literal.
4
+ */
5
+ export interface BootSessionContext {
6
+ readonly containerInstanceTag: () => Nullable<string>;
7
+ readonly hostBootMinute: () => number;
8
+ readonly inContainer: () => boolean;
9
+ }
10
+ /**
11
+ * Returns the boot session identifier for the current process. Outside a container the identifier is just the host boot minute; inside, it is composed with
12
+ * the container init's instance tag so each container restart yields a distinct value even within the same host boot.
13
+ * @param ctx - The boot session context. Defaults to a module-load snapshot of real runtime I/O.
14
+ * @returns A string that is stable across calls in the same kernel/container session and differs across reboots or container restarts.
15
+ */
16
+ export declare function getBootSessionId(ctx?: BootSessionContext): string;
@@ -0,0 +1,20 @@
1
+ import { createDefaultBootSessionContext } from "./bootSession.context.js";
2
+ /* The default context is created once at module load. createDefaultBootSessionContext() snapshots its readings, so this constant captures the boot identifier
3
+ * for the entire process lifetime. Tests bypass it by passing their own context literal.
4
+ */
5
+ const defaultContext = createDefaultBootSessionContext();
6
+ /**
7
+ * Returns the boot session identifier for the current process. Outside a container the identifier is just the host boot minute; inside, it is composed with
8
+ * the container init's instance tag so each container restart yields a distinct value even within the same host boot.
9
+ * @param ctx - The boot session context. Defaults to a module-load snapshot of real runtime I/O.
10
+ * @returns A string that is stable across calls in the same kernel/container session and differs across reboots or container restarts.
11
+ */
12
+ export function getBootSessionId(ctx = defaultContext) {
13
+ const host = String(ctx.hostBootMinute());
14
+ if (!ctx.inContainer()) {
15
+ return host;
16
+ }
17
+ const tag = ctx.containerInstanceTag();
18
+ return (tag === null) ? host : (host + "::" + tag);
19
+ }
20
+ //# sourceMappingURL=bootSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootSession.js","sourceRoot":"","sources":["../../src/utils/bootSession.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,+BAA+B,EAAE,MAAM,0BAA0B,CAAC;AAkB3E;;GAEG;AACH,MAAM,cAAc,GAAG,+BAA+B,EAAE,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAA0B,cAAc;IAEvE,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC;IAE1C,IAAG,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC;QAEtB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,GAAG,GAAG,GAAG,CAAC,oBAAoB,EAAE,CAAC;IAEvC,OAAO,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,GAAG,GAAG,CAAC,CAAC;AACrD,CAAC"}
@@ -5,7 +5,7 @@
5
5
  * default realClock delegates to delay() and raceWithTimeout() in delay.ts and to performance.now() in the runtime; tests pass a fake-clock literal that resolves
6
6
  * sleeps instantly and chooses raceWithTimeout outcomes explicitly.
7
7
  *
8
- * Why this exists. Node's built-in node:test mock.timers exposes only synchronous tick() and runAll() - it has never shipped a tickAsync/runAllAsync variant on
8
+ * Why this exists. Node's builtin node:test mock.timers exposes only synchronous tick() and runAll() - it has never shipped a tickAsync/runAllAsync variant on
9
9
  * any version. Synchronous tick advances the fake clock but does not drain microtask chains across nested Promise.race / .finally / await delay() patterns; the
10
10
  * promises stay pending past the tick. retryOperation is the canonical case: its internal raceWithTimeout -> Promise.race -> .finally -> await delay() chain is
11
11
  * exactly the shape mock.timers cannot deterministically resolve. Injecting a Clock sidesteps the runtime gap entirely - tests provide a sleep() that resolves
@@ -198,7 +198,7 @@ function spawnFFmpegProcess({ args, ffmpegBin, label, onError, streamId }) {
198
198
  export function buildSpawnFFmpegArgs(audioBitrate, options = {}) {
199
199
  const platform = options.platform ?? process.platform;
200
200
  const comment = options.comment;
201
- // Use Apple's AudioToolbox AAC encoder on macOS for better quality and performance. Fall back to FFmpeg's built-in AAC encoder on other platforms.
201
+ // Use Apple's AudioToolbox AAC encoder on macOS for better quality and performance. Fall back to FFmpeg's builtin AAC encoder on other platforms.
202
202
  const aacEncoder = platform === "darwin" ? "aac_at" : "aac";
203
203
  const ffmpegArgs = [
204
204
  ...FFMPEG_INPUT_FLAGS,