@letrquan/book 0.2.0 → 0.3.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.
Files changed (157) hide show
  1. package/CHANGELOG.md +2747 -40
  2. package/README.md +132 -1311
  3. package/dist/additional-roots-H7UNAEHS.js +31 -0
  4. package/dist/{app-MOXXWEOY.js → app-GE4OHJOC.js} +5740 -4045
  5. package/dist/app-GE4OHJOC.js.map +1 -0
  6. package/dist/{chunk-5GDZ22YP.js → chunk-224FJZLH.js} +99 -108
  7. package/dist/chunk-224FJZLH.js.map +1 -0
  8. package/dist/chunk-2AAOF5B5.js +28 -0
  9. package/dist/chunk-2AAOF5B5.js.map +1 -0
  10. package/dist/{chunk-CAGOB2N4.js → chunk-4AS7LKKQ.js} +6 -4
  11. package/dist/chunk-4AS7LKKQ.js.map +1 -0
  12. package/dist/chunk-525CAZPY.js +120 -0
  13. package/dist/chunk-525CAZPY.js.map +1 -0
  14. package/dist/{chunk-URLQAXTR.js → chunk-56SIEMDX.js} +2 -2
  15. package/dist/chunk-56SIEMDX.js.map +1 -0
  16. package/dist/{chunk-XIFFTATU.js → chunk-5YHTKWJN.js} +5420 -4031
  17. package/dist/chunk-5YHTKWJN.js.map +1 -0
  18. package/dist/chunk-62QL5QUP.js +283 -0
  19. package/dist/chunk-62QL5QUP.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-67UZEWWF.js} +3 -3
  21. package/dist/chunk-67UZEWWF.js.map +1 -0
  22. package/dist/chunk-6OQQOMZR.js +785 -0
  23. package/dist/chunk-6OQQOMZR.js.map +1 -0
  24. package/dist/{chunk-GTVB4HEM.js → chunk-AQEQX4FF.js} +344 -315
  25. package/dist/chunk-AQEQX4FF.js.map +1 -0
  26. package/dist/chunk-C7ADUNPR.js +273 -0
  27. package/dist/chunk-C7ADUNPR.js.map +1 -0
  28. package/dist/{chunk-E75IPST5.js → chunk-EFJ2NH6E.js} +12 -58
  29. package/dist/chunk-EFJ2NH6E.js.map +1 -0
  30. package/dist/{chunk-5KLVH3PY.js → chunk-HQI7NYGX.js} +5 -3
  31. package/dist/chunk-HQI7NYGX.js.map +1 -0
  32. package/dist/chunk-IME7R2UI.js +6541 -0
  33. package/dist/chunk-IME7R2UI.js.map +1 -0
  34. package/dist/chunk-J4F2QLM2.js +273 -0
  35. package/dist/chunk-J4F2QLM2.js.map +1 -0
  36. package/dist/{chunk-MAQXCUR4.js → chunk-J62VZYOB.js} +444 -112
  37. package/dist/chunk-J62VZYOB.js.map +1 -0
  38. package/dist/chunk-JTPOI3KU.js +826 -0
  39. package/dist/chunk-JTPOI3KU.js.map +1 -0
  40. package/dist/{chunk-WKBO5R4O.js → chunk-KBQS35OV.js} +2 -2
  41. package/dist/chunk-KBQS35OV.js.map +1 -0
  42. package/dist/chunk-KXAYUIKP.js +90 -0
  43. package/dist/chunk-KXAYUIKP.js.map +1 -0
  44. package/dist/{chunk-VZQXX3WW.js → chunk-QYBD22H5.js} +1 -1
  45. package/dist/chunk-QYBD22H5.js.map +1 -0
  46. package/dist/{chunk-V4BOYR52.js → chunk-RFJ6Q6LE.js} +763 -198
  47. package/dist/chunk-RFJ6Q6LE.js.map +1 -0
  48. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  49. package/dist/chunk-RT5SATF7.js.map +1 -0
  50. package/dist/{chunk-C7D4NJ2D.js → chunk-S4JMID4G.js} +68 -26
  51. package/dist/chunk-S4JMID4G.js.map +1 -0
  52. package/dist/{chunk-ANSJPYGI.js → chunk-SOW4WRQP.js} +3 -3
  53. package/dist/chunk-SOW4WRQP.js.map +1 -0
  54. package/dist/{chunk-J7MIZ5CA.js → chunk-TLX3H2CL.js} +518 -79
  55. package/dist/chunk-TLX3H2CL.js.map +1 -0
  56. package/dist/{chunk-DGMQEI6Y.js → chunk-VPNLCMVR.js} +22 -9
  57. package/dist/chunk-VPNLCMVR.js.map +1 -0
  58. package/dist/{chunk-BTZZUCLI.js → chunk-W3XFHEF4.js} +173 -29
  59. package/dist/chunk-W3XFHEF4.js.map +1 -0
  60. package/dist/{chunk-574C6MQR.js → chunk-X4NJT436.js} +130 -60
  61. package/dist/chunk-X4NJT436.js.map +1 -0
  62. package/dist/chunk-X66I2NQO.js +137 -0
  63. package/dist/chunk-X66I2NQO.js.map +1 -0
  64. package/dist/chunk-XHAO3K7I.js +18 -0
  65. package/dist/chunk-XHAO3K7I.js.map +1 -0
  66. package/dist/{chunk-3U5IBM24.js → chunk-Y2VHN7TC.js} +2 -2
  67. package/dist/chunk-Y2VHN7TC.js.map +1 -0
  68. package/dist/chunk-YC6W4EJH.js +947 -0
  69. package/dist/chunk-YC6W4EJH.js.map +1 -0
  70. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  71. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  72. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-VBIU6BLM.js} +6 -5
  73. package/dist/context-CBOBDY5Q.js +38 -0
  74. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-ZVT4V7IM.js} +5 -5
  75. package/dist/index.js +996 -155
  76. package/dist/index.js.map +1 -1
  77. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-T4I4SEHU.js} +13 -9
  78. package/dist/interactive-assets-T4I4SEHU.js.map +1 -0
  79. package/dist/job-runner.js +159 -111
  80. package/dist/job-runner.js.map +1 -1
  81. package/dist/job-supervisor.d.ts +2 -0
  82. package/dist/job-supervisor.js +112 -0
  83. package/dist/job-supervisor.js.map +1 -0
  84. package/dist/loader-S77OVTHH.js +18 -0
  85. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-HJ2VDYMC.js} +5 -5
  86. package/dist/{mcp-config-YIJIES2C.js → mcp-config-RNB5LEVE.js} +3 -3
  87. package/dist/memory-store-DZUT5HUH.js +75 -0
  88. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-MTSQ5JDJ.js} +8 -4
  89. package/dist/openai-compatible-2OB54YST.js +14 -0
  90. package/dist/{permission-approvals-76PVREFD.js → permission-approvals-YLV6UC5W.js} +5 -5
  91. package/dist/permissions-DHOKF3W2.js +45 -0
  92. package/dist/sandbox-WRYTZH3D.js +41 -0
  93. package/dist/{scrollback-LUKLIBVI.js → scrollback-35ME2GFX.js} +32 -22
  94. package/dist/scrollback-35ME2GFX.js.map +1 -0
  95. package/dist/sdk.d.ts +2219 -2361
  96. package/dist/sdk.js +53 -38
  97. package/dist/sdk.js.map +1 -1
  98. package/dist/settings-loader-JUNPGAAM.js +35 -0
  99. package/dist/settings-loader-JUNPGAAM.js.map +1 -0
  100. package/dist/{shell-selection-V6QAYXPX.js → shell-selection-CDHGJ4GC.js} +2 -2
  101. package/dist/shell-selection-CDHGJ4GC.js.map +1 -0
  102. package/package.json +23 -22
  103. package/dist/app-MOXXWEOY.js.map +0 -1
  104. package/dist/chunk-24AXE6SP.js.map +0 -1
  105. package/dist/chunk-3U5IBM24.js.map +0 -1
  106. package/dist/chunk-574C6MQR.js.map +0 -1
  107. package/dist/chunk-5GDZ22YP.js.map +0 -1
  108. package/dist/chunk-5KLVH3PY.js.map +0 -1
  109. package/dist/chunk-7N4J557C.js.map +0 -1
  110. package/dist/chunk-A7JLBK2Y.js +0 -3506
  111. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  112. package/dist/chunk-ANSJPYGI.js.map +0 -1
  113. package/dist/chunk-BTZZUCLI.js.map +0 -1
  114. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  115. package/dist/chunk-CAGOB2N4.js.map +0 -1
  116. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  117. package/dist/chunk-E75IPST5.js.map +0 -1
  118. package/dist/chunk-EYGD6X2T.js +0 -180
  119. package/dist/chunk-EYGD6X2T.js.map +0 -1
  120. package/dist/chunk-GTVB4HEM.js.map +0 -1
  121. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  122. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  123. package/dist/chunk-L6LK2QIQ.js +0 -657
  124. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  125. package/dist/chunk-MAQXCUR4.js.map +0 -1
  126. package/dist/chunk-S4XL7HOM.js +0 -402
  127. package/dist/chunk-S4XL7HOM.js.map +0 -1
  128. package/dist/chunk-URLQAXTR.js.map +0 -1
  129. package/dist/chunk-V4BOYR52.js.map +0 -1
  130. package/dist/chunk-VZQXX3WW.js.map +0 -1
  131. package/dist/chunk-WKBO5R4O.js.map +0 -1
  132. package/dist/chunk-XIFFTATU.js.map +0 -1
  133. package/dist/context-ZC5IFEAX.js +0 -25
  134. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  135. package/dist/loader-4IDS2RIE.js +0 -15
  136. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  137. package/dist/permissions-CHCKTFQQ.js +0 -28
  138. package/dist/sandbox-CNVAQDA7.js +0 -23
  139. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  140. package/dist/settings-loader-M2QP5TX2.js +0 -21
  141. package/patches/ink+6.8.0.patch +0 -13
  142. package/scripts/apply-ink-patch.mjs +0 -29
  143. package/scripts/ink-patch.mjs +0 -59
  144. package/scripts/verify-ink-patch.mjs +0 -11
  145. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-H7UNAEHS.js.map} +0 -0
  146. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-VBIU6BLM.js.map} +0 -0
  147. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-CBOBDY5Q.js.map} +0 -0
  148. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-ZVT4V7IM.js.map} +0 -0
  149. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-S77OVTHH.js.map} +0 -0
  150. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-HJ2VDYMC.js.map} +0 -0
  151. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-RNB5LEVE.js.map} +0 -0
  152. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-DZUT5HUH.js.map} +0 -0
  153. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-MTSQ5JDJ.js.map} +0 -0
  154. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-2OB54YST.js.map} +0 -0
  155. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-YLV6UC5W.js.map} +0 -0
  156. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-DHOKF3W2.js.map} +0 -0
  157. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-WRYTZH3D.js.map} +0 -0
@@ -1,17 +1,21 @@
1
1
  import {
2
+ DEFAULT_THEME_NAME,
2
3
  listCustomThemes,
3
4
  resolveTheme
4
- } from "./chunk-L6LK2QIQ.js";
5
+ } from "./chunk-J4F2QLM2.js";
5
6
  import {
6
7
  discoverSkills
7
- } from "./chunk-KWHQ2XXL.js";
8
+ } from "./chunk-67UZEWWF.js";
8
9
  import {
9
10
  discoverCommands
10
- } from "./chunk-E75IPST5.js";
11
- import "./chunk-5KLVH3PY.js";
12
- import "./chunk-DGMQEI6Y.js";
13
- import "./chunk-574C6MQR.js";
14
- import "./chunk-24AXE6SP.js";
11
+ } from "./chunk-EFJ2NH6E.js";
12
+ import "./chunk-KXAYUIKP.js";
13
+ import "./chunk-HQI7NYGX.js";
14
+ import "./chunk-2AAOF5B5.js";
15
+ import "./chunk-XHAO3K7I.js";
16
+ import "./chunk-VPNLCMVR.js";
17
+ import "./chunk-X4NJT436.js";
18
+ import "./chunk-ZPSZZUWF.js";
15
19
 
16
20
  // src/tui/interactive-assets.ts
17
21
  function loadInteractiveAssets(config) {
@@ -22,10 +26,10 @@ function loadInteractiveAssets(config) {
22
26
  enabled: config.settings.skills.enabled
23
27
  }),
24
28
  customThemes: listCustomThemes(config.workspace),
25
- initialTheme: resolveTheme(config.workspace, config.settings.theme ?? "apple")
29
+ initialTheme: resolveTheme(config.workspace, config.settings.theme ?? DEFAULT_THEME_NAME)
26
30
  };
27
31
  }
28
32
  export {
29
33
  loadInteractiveAssets
30
34
  };
31
- //# sourceMappingURL=interactive-assets-CCAAWVKH.js.map
35
+ //# sourceMappingURL=interactive-assets-T4I4SEHU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tui/interactive-assets.ts"],"sourcesContent":["import { discoverCommands } from '../commands/loader.js';\nimport { discoverSkills, type Skill } from '../skills.js';\nimport type { SlashCommand } from '../types/commands.js';\nimport type { AgentConfig } from '../types/runtime.js';\nimport { DEFAULT_THEME_NAME, listCustomThemes, resolveTheme, type ResolvedTheme } from './theme.js';\n\nexport interface InteractiveAssets {\n commands: SlashCommand[];\n skills: Skill[];\n customThemes: string[];\n initialTheme: ResolvedTheme | null;\n}\n\n/** Load filesystem-backed UI metadata before React begins rendering. */\nexport function loadInteractiveAssets(\n config: Pick<AgentConfig, 'workspace' | 'settings'>,\n): InteractiveAssets {\n return {\n commands: discoverCommands(config.workspace),\n skills: discoverSkills(config.workspace, config.settings.skills.overrides, {\n executionOverrides: config.settings.skills.execution,\n enabled: config.settings.skills.enabled,\n }),\n customThemes: listCustomThemes(config.workspace),\n initialTheme: resolveTheme(config.workspace, config.settings.theme ?? DEFAULT_THEME_NAME),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAcO,SAAS,sBACd,QACmB;AACnB,SAAO;AAAA,IACL,UAAU,iBAAiB,OAAO,SAAS;AAAA,IAC3C,QAAQ,eAAe,OAAO,WAAW,OAAO,SAAS,OAAO,WAAW;AAAA,MACzE,oBAAoB,OAAO,SAAS,OAAO;AAAA,MAC3C,SAAS,OAAO,SAAS,OAAO;AAAA,IAClC,CAAC;AAAA,IACD,cAAc,iBAAiB,OAAO,SAAS;AAAA,IAC/C,cAAc,aAAa,OAAO,WAAW,OAAO,SAAS,SAAS,kBAAkB;AAAA,EAC1F;AACF;","names":[]}
@@ -1,15 +1,24 @@
1
1
  import {
2
+ loadPersistentShellSpec,
2
3
  readJsonFile,
3
- signalProcessGroup,
4
- system32Executable,
5
- waitForProcessGroupExit,
6
4
  writeJsonAtomic
7
- } from "./chunk-S4XL7HOM.js";
8
- import "./chunk-7N4J557C.js";
9
- import "./chunk-24AXE6SP.js";
5
+ } from "./chunk-YC6W4EJH.js";
6
+ import {
7
+ TERMINATE_GRACE_MS,
8
+ signalProcessGroup,
9
+ terminateProcessTree,
10
+ waitForProcessClose,
11
+ waitForProcessGroupExit
12
+ } from "./chunk-C7ADUNPR.js";
13
+ import "./chunk-RT5SATF7.js";
14
+ import {
15
+ buildChildEnv
16
+ } from "./chunk-XHAO3K7I.js";
17
+ import "./chunk-224FJZLH.js";
18
+ import "./chunk-ZPSZZUWF.js";
10
19
 
11
20
  // src/job-runner.ts
12
- import { execFile, spawn } from "child_process";
21
+ import { spawn } from "child_process";
13
22
  import {
14
23
  appendFileSync,
15
24
  existsSync,
@@ -19,25 +28,23 @@ import {
19
28
  truncateSync,
20
29
  writeFileSync
21
30
  } from "fs";
22
- import { createHash } from "crypto";
31
+ import { dirname, extname, join } from "path";
32
+ import { fileURLToPath } from "url";
23
33
  var specPath = process.argv[2];
24
34
  if (!specPath) {
25
35
  process.exitCode = 2;
26
36
  throw new Error("Missing persistent shell specification path.");
27
37
  }
28
- var loadedSpec = readJsonFile(specPath);
29
- if (!loadedSpec || loadedSpec.version !== 1 || createHash("sha256").update(loadedSpec.token).digest("hex") !== loadedSpec.tokenHash) {
38
+ var loaded = loadPersistentShellSpec(specPath);
39
+ if ("error" in loaded) {
30
40
  process.exitCode = 2;
31
- throw new Error("Invalid persistent shell specification.");
41
+ throw new Error(loaded.error);
32
42
  }
33
- if (loadedSpec.sandboxed && !loadedSpec.exec) {
34
- process.exitCode = 2;
35
- throw new Error(
36
- "Invalid persistent shell specification: sandboxed is set but no sandboxed argv is present."
37
- );
38
- }
39
- var spec = loadedSpec;
40
- var TERMINATE_GRACE_MS = 1500;
43
+ var spec = loaded.spec;
44
+ var RECORD_RENAME_RETRY_BUDGET_MS = 1e3;
45
+ var BEST_EFFORT_RENAME_RETRY_BUDGET_MS = 50;
46
+ var TERMINAL_RECORD_RETRY_MS = 250;
47
+ var TERMINAL_RECORD_ATTEMPTS = 20;
41
48
  var child;
42
49
  var terminal = false;
43
50
  var terminationInFlight = false;
@@ -52,7 +59,7 @@ var state = {
52
59
  command: spec.command,
53
60
  title: spec.title,
54
61
  workdir: spec.workdir,
55
- status: "running",
62
+ status: "starting",
56
63
  notify: spec.notify,
57
64
  sandboxed: spec.sandboxed,
58
65
  runnerPid: process.pid,
@@ -72,15 +79,27 @@ var state = {
72
79
  completionDeliveredSequence: 0,
73
80
  completionAcknowledgedSequence: 0
74
81
  };
75
- function persist() {
82
+ function persist(budgetMs = RECORD_RENAME_RETRY_BUDGET_MS) {
76
83
  state = { ...state, revision: state.revision + 1, heartbeatAt: Date.now() };
77
- writeJsonAtomic(spec.recordPath, state);
84
+ writeJsonAtomic(spec.recordPath, state, { renameRetryBudgetMs: budgetMs });
85
+ }
86
+ function persistBestEffort(what) {
87
+ try {
88
+ persist(BEST_EFFORT_RENAME_RETRY_BUDGET_MS);
89
+ } catch (error) {
90
+ const code = error?.code ?? "unknown error";
91
+ state = {
92
+ ...state,
93
+ recordWriteFailures: (state.recordWriteFailures ?? 0) + 1,
94
+ lastRecordWriteError: `${what} write failed (${code})`
95
+ };
96
+ }
78
97
  }
79
98
  function appendBounded(data) {
80
99
  const chunk = Buffer.isBuffer(data) ? data : Buffer.from(String(data));
81
100
  if (chunk.length === 0) return;
82
- appendFileSync(spec.outputPath, chunk);
83
101
  try {
102
+ appendFileSync(spec.outputPath, chunk);
84
103
  const size = statSync(spec.outputPath).size;
85
104
  if (size <= spec.maxLogBytes) return;
86
105
  const retained = readFileSync(spec.outputPath).subarray(-spec.maxLogBytes);
@@ -92,61 +111,25 @@ function appendBounded(data) {
92
111
  outputRotationSequence: (state.outputRotationSequence ?? 0) + 1,
93
112
  truncatedBytes: (state.truncatedBytes ?? 0) + discarded
94
113
  };
95
- persist();
114
+ persistBestEffort("log rotation");
96
115
  } catch {
97
116
  }
98
117
  }
99
- function waitForChildClose(timeoutMs) {
100
- if (!child || child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);
101
- const proc = child;
102
- return new Promise((resolve) => {
103
- const timer = setTimeout(() => {
104
- cleanup();
105
- resolve(proc.exitCode !== null || proc.signalCode !== null);
106
- }, timeoutMs);
107
- const onClose = () => {
108
- cleanup();
109
- resolve(true);
110
- };
111
- const cleanup = () => {
112
- clearTimeout(timer);
113
- proc.off("close", onClose);
114
- };
115
- proc.once("close", onClose);
116
- });
117
- }
118
118
  async function terminateTree() {
119
- if (!child?.pid) return true;
120
- const pid = child.pid;
121
- if (process.platform !== "win32") {
122
- signalProcessGroup(child, pid, "SIGTERM");
123
- if (await waitForProcessGroupExit(pid, TERMINATE_GRACE_MS)) return true;
124
- signalProcessGroup(child, pid, "SIGKILL");
125
- return waitForProcessGroupExit(pid, TERMINATE_GRACE_MS);
126
- }
127
- const alreadyExited = child.exitCode !== null || child.signalCode !== null;
128
- if (alreadyExited) return true;
129
- const confirmed = await new Promise((resolve) => {
130
- execFile(
131
- system32Executable("taskkill"),
132
- ["/PID", String(pid), "/T", "/F"],
133
- { windowsHide: true, timeout: TERMINATE_GRACE_MS },
134
- (error) => resolve(!error)
135
- );
136
- });
137
- if (!confirmed) {
138
- try {
139
- child.kill("SIGTERM");
140
- } catch {
141
- }
119
+ const proc = child;
120
+ const pid = proc?.pid;
121
+ if (!proc || pid === void 0) return true;
122
+ if (process.platform === "win32") {
123
+ return terminateProcessTree(proc, pid, (timeoutMs) => waitForProcessClose(proc, timeoutMs));
142
124
  }
143
- if (await waitForChildClose(TERMINATE_GRACE_MS)) return confirmed;
144
125
  try {
145
- child.kill("SIGKILL");
126
+ process.kill(pid, "SIGTERM");
146
127
  } catch {
147
- return false;
128
+ signalProcessGroup(proc, pid, "SIGTERM");
148
129
  }
149
- return await waitForChildClose(TERMINATE_GRACE_MS) && confirmed;
130
+ if (await waitForProcessGroupExit(pid, TERMINATE_GRACE_MS)) return true;
131
+ signalProcessGroup(proc, pid, "SIGKILL");
132
+ return waitForProcessGroupExit(pid, TERMINATE_GRACE_MS);
150
133
  }
151
134
  function finish(status, code, signal, stopReason) {
152
135
  if (terminal) return;
@@ -167,47 +150,123 @@ function finish(status, code, signal, stopReason) {
167
150
  rmSync(specPath, { force: true });
168
151
  } catch {
169
152
  }
170
- persist();
171
153
  if (heartbeat) clearInterval(heartbeat);
172
154
  if (controlPoll) clearInterval(controlPoll);
173
155
  if (deadline) clearTimeout(deadline);
174
- setTimeout(() => process.exit(0), 10).unref();
156
+ publishTerminalRecord(1);
175
157
  }
176
- writeFileSync(spec.outputPath, "", { encoding: "utf8", mode: 384 });
177
- try {
178
- const spawnBase = {
179
- cwd: spec.workdir,
180
- env: { ...process.env, ...spec.env },
181
- detached: process.platform !== "win32",
182
- stdio: ["ignore", "pipe", "pipe"]
183
- };
184
- child = spec.exec ? spawn(spec.exec.file, spec.exec.args, { ...spawnBase, shell: false }) : spawn(spec.effectiveCommand, { ...spawnBase, shell: true });
185
- state = { ...state, childPid: child.pid };
186
- persist();
187
- } catch (error) {
188
- appendBounded(error instanceof Error ? `${error.message}
189
- ` : `${String(error)}
158
+ function publishTerminalRecord(attempt) {
159
+ try {
160
+ persist();
161
+ } catch (error) {
162
+ if (attempt < TERMINAL_RECORD_ATTEMPTS) {
163
+ setTimeout(() => publishTerminalRecord(attempt + 1), TERMINAL_RECORD_RETRY_MS);
164
+ return;
165
+ }
166
+ reportUnrecordedEnd(error);
167
+ process.exitCode = 1;
168
+ setTimeout(() => process.exit(1), 10).unref();
169
+ return;
170
+ }
171
+ setTimeout(() => process.exit(), 10).unref();
172
+ }
173
+ function reportUnrecordedEnd(error) {
174
+ const code = error?.code ?? "unknown error";
175
+ const outcome = state.exitCode === void 0 || state.exitCode === null ? state.status : `${state.status}, exit code ${state.exitCode}`;
176
+ try {
177
+ appendFileSync(
178
+ spec.outputPath,
179
+ `[runner: the job ended (${outcome}) but its record could not be written (${code}); Book will report it as lost]
180
+ `
181
+ );
182
+ } catch {
183
+ }
184
+ }
185
+ function supervisorPath() {
186
+ const self = fileURLToPath(import.meta.url);
187
+ return join(dirname(self), `job-supervisor${extname(self)}`);
188
+ }
189
+ function failStartup(what, error) {
190
+ const reason = error instanceof Error ? error.message : String(error);
191
+ process.stderr.write(`Persistent background runner could not ${what}: ${reason}
190
192
  `);
191
- finish("failed");
192
193
  process.exit(1);
193
194
  }
194
- child?.stdout?.on("data", appendBounded);
195
- child?.stderr?.on("data", appendBounded);
196
- child?.on("error", (error) => {
197
- appendBounded(`${error.message}
195
+ function supervisorEnv() {
196
+ const env = buildChildEnv(process.env, spec.env);
197
+ const nodeOptions = env.NODE_OPTIONS;
198
+ delete env.NODE_OPTIONS;
199
+ if (nodeOptions !== void 0) env.BOOK_SUPERVISED_NODE_OPTIONS = nodeOptions;
200
+ return env;
201
+ }
202
+ function startRunner() {
203
+ try {
204
+ writeFileSync(spec.outputPath, "", { encoding: "utf8", mode: 384 });
205
+ } catch (error) {
206
+ failStartup("create its job log", error);
207
+ }
208
+ try {
209
+ persist();
210
+ } catch (error) {
211
+ failStartup("write its job record", error);
212
+ }
213
+ let proc;
214
+ try {
215
+ proc = spawn(process.execPath, [...process.execArgv, supervisorPath(), specPath], {
216
+ cwd: spec.workdir,
217
+ env: supervisorEnv(),
218
+ detached: true,
219
+ stdio: ["pipe", "pipe", "pipe"],
220
+ windowsHide: true
221
+ });
222
+ } catch (error) {
223
+ appendBounded(error instanceof Error ? `${error.message}
224
+ ` : `${String(error)}
198
225
  `);
199
- if (state.status !== "stopping") finish("failed");
200
- });
201
- child?.on("close", (code, signal) => {
202
- if (terminal || state.status === "stopping") return;
203
- finish(code === 0 ? "exited" : "failed", code, signal);
204
- });
205
- heartbeat = setInterval(persist, 1e3);
226
+ process.exitCode = 1;
227
+ finish("failed");
228
+ return;
229
+ }
230
+ child = proc;
231
+ proc.stdin?.on("error", () => {
232
+ });
233
+ proc.stdout?.on("data", appendBounded);
234
+ proc.stderr?.on("data", appendBounded);
235
+ proc.on("error", (error) => {
236
+ appendBounded(`${error.message}
237
+ `);
238
+ if (state.status !== "stopping") finish("failed");
239
+ });
240
+ proc.on("close", (code, signal) => {
241
+ if (terminal || state.status === "stopping") return;
242
+ finish(code === 0 ? "exited" : "failed", code, signal);
243
+ });
244
+ state = { ...state, status: "running", childPid: proc.pid };
245
+ try {
246
+ persist();
247
+ } catch (error) {
248
+ terminal = true;
249
+ void terminateTree().finally(() => failStartup("write its job record", error));
250
+ return;
251
+ }
252
+ heartbeat = setInterval(() => persistBestEffort("heartbeat"), 1e3);
253
+ controlPoll = setInterval(() => {
254
+ if (!existsSync(spec.controlPath) || terminal) return;
255
+ const control = readJsonFile(
256
+ spec.controlPath
257
+ );
258
+ if (control?.token !== spec.token || control.action !== "stop") return;
259
+ void requestTermination("killed", control.reason ?? "requested");
260
+ }, 250);
261
+ deadline = spec.timeoutMs ? setTimeout(() => {
262
+ void requestTermination("timed_out", "timeout");
263
+ }, spec.timeoutMs) : void 0;
264
+ }
206
265
  async function requestTermination(status, reason) {
207
266
  if (terminal || terminationInFlight) return;
208
267
  terminationInFlight = true;
209
268
  state = { ...state, status: "stopping", stopReason: reason };
210
- persist();
269
+ persistBestEffort("stopping state");
211
270
  try {
212
271
  if (await terminateTree()) {
213
272
  finish(status, child?.exitCode, child?.signalCode, reason);
@@ -220,16 +279,5 @@ async function requestTermination(status, reason) {
220
279
  terminationInFlight = false;
221
280
  }
222
281
  }
223
- controlPoll = setInterval(() => {
224
- if (!existsSync(spec.controlPath) || terminal) return;
225
- const control = readJsonFile(
226
- spec.controlPath
227
- );
228
- if (control?.token !== spec.token || control.action !== "stop") return;
229
- void requestTermination("killed", control.reason ?? "requested");
230
- }, 250);
231
- deadline = spec.timeoutMs ? setTimeout(() => {
232
- void requestTermination("timed_out", "timeout");
233
- }, spec.timeoutMs) : void 0;
234
- persist();
282
+ startRunner();
235
283
  //# sourceMappingURL=job-runner.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/job-runner.ts"],"sourcesContent":["import { execFile, spawn, type ChildProcess, type SpawnOptions } from 'node:child_process';\r\nimport {\r\n appendFileSync,\r\n existsSync,\r\n readFileSync,\r\n rmSync,\r\n statSync,\r\n truncateSync,\r\n writeFileSync,\r\n} from 'node:fs';\r\nimport { createHash } from 'node:crypto';\r\nimport {\r\n readJsonFile,\r\n writeJsonAtomic,\r\n type PersistentShellSpec,\r\n type PersistentShellState,\r\n} from './jobs/persistent-store.js';\r\nimport { signalProcessGroup, waitForProcessGroupExit } from './jobs/process-tree.js';\r\nimport { system32Executable } from './system32.js';\r\n\r\nconst specPath = process.argv[2];\r\nif (!specPath) {\r\n process.exitCode = 2;\r\n throw new Error('Missing persistent shell specification path.');\r\n}\r\nconst loadedSpec = readJsonFile<PersistentShellSpec>(specPath);\r\nif (\r\n !loadedSpec ||\r\n loadedSpec.version !== 1 ||\r\n createHash('sha256').update(loadedSpec.token).digest('hex') !== loadedSpec.tokenHash\r\n) {\r\n process.exitCode = 2;\r\n throw new Error('Invalid persistent shell specification.');\r\n}\r\n// `effectiveCommand` is the raw user command now that sandboxing rides on\r\n// `exec`, so a spec claiming `sandboxed` without one would run completely\r\n// unconfined while the job panel and the [sandboxed] marker said otherwise.\r\n// Refuse to start rather than silently downgrade. The reverse is legitimate:\r\n// an unsandboxed command also carries an `exec` when the session shell is\r\n// spawned as argv (Git Bash or PowerShell on Windows).\r\nif (loadedSpec.sandboxed && !loadedSpec.exec) {\r\n process.exitCode = 2;\r\n throw new Error(\r\n 'Invalid persistent shell specification: sandboxed is set but no sandboxed argv is present.',\r\n );\r\n}\r\nconst spec: PersistentShellSpec = loadedSpec;\r\nconst TERMINATE_GRACE_MS = 1_500;\r\n\r\nlet child: ChildProcess | undefined;\r\nlet terminal = false;\r\nlet terminationInFlight = false;\r\n// These timers are assigned after the child is wired so startup failures can still call finish().\r\n// eslint-disable-next-line prefer-const\r\nlet heartbeat: NodeJS.Timeout | undefined;\r\n// eslint-disable-next-line prefer-const\r\nlet controlPoll: NodeJS.Timeout | undefined;\r\n// eslint-disable-next-line prefer-const\r\nlet deadline: NodeJS.Timeout | undefined;\r\nconst startedAt = Date.now();\r\nlet state: PersistentShellState = {\r\n version: 1,\r\n revision: 1,\r\n id: spec.id,\r\n command: spec.command,\r\n title: spec.title,\r\n workdir: spec.workdir,\r\n status: 'running',\r\n notify: spec.notify,\r\n sandboxed: spec.sandboxed,\r\n runnerPid: process.pid,\r\n tokenHash: spec.tokenHash,\r\n startedAt,\r\n heartbeatAt: startedAt,\r\n outputRotationSequence: 0,\r\n truncatedBytes: 0,\r\n timeoutMs: spec.timeoutMs,\r\n deadlineAt: spec.timeoutMs ? startedAt + spec.timeoutMs : undefined,\r\n parentSessionId: spec.parentSessionId,\r\n rootRunId: spec.rootRunId,\r\n parentRunId: spec.parentRunId,\r\n outputPath: spec.outputPath,\r\n controlPath: spec.controlPath,\r\n completionSequence: 0,\r\n completionDeliveredSequence: 0,\r\n completionAcknowledgedSequence: 0,\r\n};\r\n\r\nfunction persist(): void {\r\n state = { ...state, revision: state.revision + 1, heartbeatAt: Date.now() };\r\n writeJsonAtomic(spec.recordPath, state);\r\n}\r\n\r\nfunction appendBounded(data: unknown): void {\r\n const chunk = Buffer.isBuffer(data) ? data : Buffer.from(String(data));\r\n if (chunk.length === 0) return;\r\n appendFileSync(spec.outputPath, chunk);\r\n try {\r\n const size = statSync(spec.outputPath).size;\r\n if (size <= spec.maxLogBytes) return;\r\n const retained = readFileSync(spec.outputPath).subarray(-spec.maxLogBytes);\r\n const discarded = size - retained.length;\r\n truncateSync(spec.outputPath, 0);\r\n writeFileSync(spec.outputPath, retained);\r\n state = {\r\n ...state,\r\n outputRotationSequence: (state.outputRotationSequence ?? 0) + 1,\r\n truncatedBytes: (state.truncatedBytes ?? 0) + discarded,\r\n };\r\n persist();\r\n } catch {\r\n // Output retention is best effort; lifecycle state remains authoritative.\r\n }\r\n}\r\n\r\nfunction waitForChildClose(timeoutMs: number): Promise<boolean> {\r\n if (!child || child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);\r\n const proc = child;\r\n return new Promise((resolve) => {\r\n const timer = setTimeout(() => {\r\n cleanup();\r\n resolve(proc.exitCode !== null || proc.signalCode !== null);\r\n }, timeoutMs);\r\n const onClose = () => {\r\n cleanup();\r\n resolve(true);\r\n };\r\n const cleanup = () => {\r\n clearTimeout(timer);\r\n proc.off('close', onClose);\r\n };\r\n proc.once('close', onClose);\r\n });\r\n}\r\n\r\nasync function terminateTree(): Promise<boolean> {\r\n if (!child?.pid) return true;\r\n const pid = child.pid;\r\n if (process.platform !== 'win32') {\r\n // The direct child is the `sh -c` wrapper, which dies from SIGTERM even when the worker it\r\n // forked ignores it. Escalate on whether the group still holds processes, never on the\r\n // wrapper's own exit, or the surviving worker is recorded as killed while it keeps running.\r\n signalProcessGroup(child, pid, 'SIGTERM');\r\n if (await waitForProcessGroupExit(pid, TERMINATE_GRACE_MS)) return true;\r\n signalProcessGroup(child, pid, 'SIGKILL');\r\n return waitForProcessGroupExit(pid, TERMINATE_GRACE_MS);\r\n }\r\n // `taskkill /T /F` covers the tree, so the direct child's close speaks for the tree only when\r\n // taskkill actually ran.\r\n const alreadyExited = child.exitCode !== null || child.signalCode !== null;\r\n if (alreadyExited) return true;\r\n const confirmed = await new Promise<boolean>((resolve) => {\r\n execFile(\r\n system32Executable('taskkill'),\r\n ['/PID', String(pid), '/T', '/F'],\r\n { windowsHide: true, timeout: TERMINATE_GRACE_MS },\r\n (error) => resolve(!error),\r\n );\r\n });\r\n if (!confirmed) {\r\n try {\r\n child.kill('SIGTERM');\r\n } catch {\r\n // The process may have exited between taskkill and the fallback.\r\n }\r\n }\r\n if (await waitForChildClose(TERMINATE_GRACE_MS)) return confirmed;\r\n try {\r\n child.kill('SIGKILL');\r\n } catch {\r\n return false;\r\n }\r\n return (await waitForChildClose(TERMINATE_GRACE_MS)) && confirmed;\r\n}\r\n\r\nfunction finish(\r\n status: 'exited' | 'failed' | 'killed' | 'timed_out',\r\n code?: number | null,\r\n signal?: NodeJS.Signals | string | null,\r\n stopReason?: string,\r\n): void {\r\n if (terminal) return;\r\n terminal = true;\r\n // Stop capturing output at terminality: once the record reads terminal, the\r\n // manager may dismiss the job and delete its files, so a straggling stream\r\n // chunk must not re-create the log after that point.\r\n child?.stdout?.off('data', appendBounded);\r\n child?.stderr?.off('data', appendBounded);\r\n state = {\r\n ...state,\r\n status,\r\n exitCode: code,\r\n signal,\r\n stopReason,\r\n finishedAt: Date.now(),\r\n completionSequence: state.completionSequence + 1,\r\n };\r\n // Remove the runner-owned control/spec files BEFORE persisting the terminal\r\n // record. The manager treats an observable terminal record as \"stop\r\n // complete\" and immediately expects these files to be gone, so the removals\r\n // must happen-before the rename that publishes the terminal state — the old\r\n // order left a window where a preempted runner had published \"killed\" while\r\n // the spec file still existed.\r\n try {\r\n rmSync(spec.controlPath, { force: true });\r\n rmSync(specPath, { force: true });\r\n } catch {\r\n // Best effort: a leaked control/spec file is cleaned up by the manager's\r\n // lost-job reconciliation or dismissal, whereas skipping the terminal\r\n // persist below would strand the job as \"stopping\" forever.\r\n }\r\n persist();\r\n if (heartbeat) clearInterval(heartbeat);\r\n if (controlPoll) clearInterval(controlPoll);\r\n if (deadline) clearTimeout(deadline);\r\n setTimeout(() => process.exit(0), 10).unref();\r\n}\r\n\r\nwriteFileSync(spec.outputPath, '', { encoding: 'utf8', mode: 0o600 });\r\ntry {\r\n const spawnBase: SpawnOptions = {\r\n cwd: spec.workdir,\r\n env: { ...process.env, ...spec.env },\r\n detached: process.platform !== 'win32',\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n };\r\n // Sandboxed specs carry an argv; only unsandboxed ones go through a shell.\r\n child = spec.exec\r\n ? spawn(spec.exec.file, spec.exec.args, { ...spawnBase, shell: false })\r\n : spawn(spec.effectiveCommand, { ...spawnBase, shell: true });\r\n state = { ...state, childPid: child.pid };\r\n persist();\r\n} catch (error) {\r\n appendBounded(error instanceof Error ? `${error.message}\\n` : `${String(error)}\\n`);\r\n finish('failed');\r\n process.exit(1);\r\n}\r\n\r\nchild?.stdout?.on('data', appendBounded);\r\nchild?.stderr?.on('data', appendBounded);\r\nchild?.on('error', (error) => {\r\n appendBounded(`${error.message}\\n`);\r\n if (state.status !== 'stopping') finish('failed');\r\n});\r\nchild?.on('close', (code, signal) => {\r\n if (terminal || state.status === 'stopping') return;\r\n finish(code === 0 ? 'exited' : 'failed', code, signal);\r\n});\r\n\r\nheartbeat = setInterval(persist, 1_000);\r\nasync function requestTermination(status: 'killed' | 'timed_out', reason: string): Promise<void> {\r\n if (terminal || terminationInFlight) return;\r\n terminationInFlight = true;\r\n state = { ...state, status: 'stopping', stopReason: reason };\r\n persist();\r\n try {\r\n if (await terminateTree()) {\r\n finish(status, child?.exitCode, child?.signalCode, reason);\r\n } else {\r\n appendBounded('[termination requested; process is still running; retrying]\\n');\r\n const retry = setTimeout(() => void requestTermination(status, reason), 500);\r\n retry.unref();\r\n }\r\n } finally {\r\n terminationInFlight = false;\r\n }\r\n}\r\n\r\ncontrolPoll = setInterval(() => {\r\n if (!existsSync(spec.controlPath) || terminal) return;\r\n const control = readJsonFile<{ token?: string; action?: string; reason?: string }>(\r\n spec.controlPath,\r\n );\r\n if (control?.token !== spec.token || control.action !== 'stop') return;\r\n void requestTermination('killed', control.reason ?? 'requested');\r\n}, 250);\r\ndeadline = spec.timeoutMs\r\n ? setTimeout(() => {\r\n void requestTermination('timed_out', 'timeout');\r\n }, spec.timeoutMs)\r\n : undefined;\r\n\r\npersist();\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/job-runner.ts"],"sourcesContent":["import { spawn, type ChildProcess } from 'node:child_process';\nimport { buildChildEnv } from './child-env.js';\nimport {\n appendFileSync,\n existsSync,\n readFileSync,\n rmSync,\n statSync,\n truncateSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, extname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n loadPersistentShellSpec,\n readJsonFile,\n writeJsonAtomic,\n type PersistentShellSpec,\n type PersistentShellState,\n} from './jobs/persistent-store.js';\nimport {\n signalProcessGroup,\n terminateProcessTree,\n waitForProcessClose,\n waitForProcessGroupExit,\n TERMINATE_GRACE_MS,\n} from './jobs/process-tree.js';\n\nconst specPath = process.argv[2];\nif (!specPath) {\n process.exitCode = 2;\n throw new Error('Missing persistent shell specification path.');\n}\nconst loaded = loadPersistentShellSpec(specPath);\nif ('error' in loaded) {\n process.exitCode = 2;\n throw new Error(loaded.error);\n}\nconst spec: PersistentShellSpec = loaded.spec;\n/**\n * How long a contended record write may block before it counts as failed. The first record and\n * the terminal record wait up to a second for Windows to release the file: the job cannot start\n * without the first, and the manager waits on the second to call a stop complete. Every other\n * write (the heartbeat, the child pid, `stopping`, a log rotation) is repeated by the next\n * heartbeat, so it gives up almost at once instead of stalling the runner's timers.\n */\nconst RECORD_RENAME_RETRY_BUDGET_MS = 1_000;\nconst BEST_EFFORT_RENAME_RETRY_BUDGET_MS = 50;\n/** A terminal record that still failed to write is retried on a timer this often, this many times. */\nconst TERMINAL_RECORD_RETRY_MS = 250;\nconst TERMINAL_RECORD_ATTEMPTS = 20;\n\nlet child: ChildProcess | undefined;\nlet terminal = false;\nlet terminationInFlight = false;\n// These timers are assigned after the child is wired so startup failures can still call finish().\nlet heartbeat: NodeJS.Timeout | undefined;\nlet controlPoll: NodeJS.Timeout | undefined;\nlet deadline: NodeJS.Timeout | undefined;\nconst startedAt = Date.now();\nlet state: PersistentShellState = {\n version: 1,\n revision: 1,\n id: spec.id,\n command: spec.command,\n title: spec.title,\n workdir: spec.workdir,\n status: 'starting',\n notify: spec.notify,\n sandboxed: spec.sandboxed,\n runnerPid: process.pid,\n tokenHash: spec.tokenHash,\n startedAt,\n heartbeatAt: startedAt,\n outputRotationSequence: 0,\n truncatedBytes: 0,\n timeoutMs: spec.timeoutMs,\n deadlineAt: spec.timeoutMs ? startedAt + spec.timeoutMs : undefined,\n parentSessionId: spec.parentSessionId,\n rootRunId: spec.rootRunId,\n parentRunId: spec.parentRunId,\n outputPath: spec.outputPath,\n controlPath: spec.controlPath,\n completionSequence: 0,\n completionDeliveredSequence: 0,\n completionAcknowledgedSequence: 0,\n};\n\nfunction persist(budgetMs = RECORD_RENAME_RETRY_BUDGET_MS): void {\n state = { ...state, revision: state.revision + 1, heartbeatAt: Date.now() };\n writeJsonAtomic(spec.recordPath, state, { renameRetryBudgetMs: budgetMs });\n}\n\n/**\n * A non-terminal write: the heartbeat, the switch to `stopping`, a log rotation. One that fails\n * must not end the runner, or its job would keep running with nothing left to stop it; the next\n * heartbeat writes the same state again. The failure is counted on the record, which that next\n * write carries, rather than noted in the job's log: the log is the command's own output, and the\n * model reads it through BashOutput.\n */\nfunction persistBestEffort(what: string): void {\n try {\n persist(BEST_EFFORT_RENAME_RETRY_BUDGET_MS);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException | undefined)?.code ?? 'unknown error';\n state = {\n ...state,\n recordWriteFailures: (state.recordWriteFailures ?? 0) + 1,\n lastRecordWriteError: `${what} write failed (${code})`,\n };\n }\n}\n\nfunction appendBounded(data: unknown): void {\n const chunk = Buffer.isBuffer(data) ? data : Buffer.from(String(data));\n if (chunk.length === 0) return;\n try {\n appendFileSync(spec.outputPath, chunk);\n const size = statSync(spec.outputPath).size;\n if (size <= spec.maxLogBytes) return;\n const retained = readFileSync(spec.outputPath).subarray(-spec.maxLogBytes);\n const discarded = size - retained.length;\n truncateSync(spec.outputPath, 0);\n writeFileSync(spec.outputPath, retained);\n state = {\n ...state,\n outputRotationSequence: (state.outputRotationSequence ?? 0) + 1,\n truncatedBytes: (state.truncatedBytes ?? 0) + discarded,\n };\n persistBestEffort('log rotation');\n } catch {\n // Output and its retention are best effort; lifecycle state remains authoritative.\n }\n}\n\n/**\n * End the job's tree. Off Windows the first SIGTERM goes to the supervisor alone, because the\n * supervisor leads the job's own process group and passes the signal on to that group exactly once:\n * signalling the group from here as well would hand the command a second SIGTERM, which a command\n * that has installed its own handler or is midway through a graceful shutdown sees as two stops\n * rather than one. A supervisor that has already exited forwards nothing, so the signal falls\n * through to the group itself. From there the escalation is this process's own, exactly as before,\n * because the supervisor no longer passes SIGKILL along. Windows has no signal for a group, so\n * `taskkill /T /F` still walks the tree from the supervisor there.\n */\nasync function terminateTree(): Promise<boolean> {\n const proc = child;\n const pid = proc?.pid;\n if (!proc || pid === undefined) return true;\n if (process.platform === 'win32') {\n return terminateProcessTree(proc, pid, (timeoutMs) => waitForProcessClose(proc, timeoutMs));\n }\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n signalProcessGroup(proc, pid, 'SIGTERM');\n }\n if (await waitForProcessGroupExit(pid, TERMINATE_GRACE_MS)) return true;\n signalProcessGroup(proc, pid, 'SIGKILL');\n return waitForProcessGroupExit(pid, TERMINATE_GRACE_MS);\n}\n\nfunction finish(\n status: 'exited' | 'failed' | 'killed' | 'timed_out',\n code?: number | null,\n signal?: NodeJS.Signals | string | null,\n stopReason?: string,\n): void {\n if (terminal) return;\n terminal = true;\n // Stop capturing output at terminality: once the record reads terminal, the\n // manager may dismiss the job and delete its files, so a straggling stream\n // chunk must not re-create the log after that point.\n child?.stdout?.off('data', appendBounded);\n child?.stderr?.off('data', appendBounded);\n state = {\n ...state,\n status,\n exitCode: code,\n signal,\n stopReason,\n finishedAt: Date.now(),\n completionSequence: state.completionSequence + 1,\n };\n // Remove the runner-owned control/spec files BEFORE persisting the terminal\n // record. The manager treats an observable terminal record as \"stop\n // complete\" and immediately expects these files to be gone, so the removals\n // must happen-before the rename that publishes the terminal state — the old\n // order left a window where a preempted runner had published \"killed\" while\n // the spec file still existed.\n try {\n rmSync(spec.controlPath, { force: true });\n rmSync(specPath, { force: true });\n } catch {\n // Best effort: a leaked control/spec file is cleaned up by the manager's\n // lost-job reconciliation or dismissal, whereas skipping the terminal\n // persist below would strand the job as \"stopping\" forever.\n }\n if (heartbeat) clearInterval(heartbeat);\n if (controlPoll) clearInterval(controlPoll);\n if (deadline) clearTimeout(deadline);\n publishTerminalRecord(1);\n}\n\n/**\n * Write the terminal record, then exit. It is the one write the manager waits on to call a stop\n * complete, so a write that fails even after the retry budget is tried again on a timer rather\n * than ending the runner with its job still recorded as running. If every attempt fails, the\n * runner says at the end of the job's log what really happened, exits nonzero, and the manager's\n * heartbeat-staleness check reports the job lost.\n */\nfunction publishTerminalRecord(attempt: number): void {\n try {\n persist();\n } catch (error) {\n if (attempt < TERMINAL_RECORD_ATTEMPTS) {\n setTimeout(() => publishTerminalRecord(attempt + 1), TERMINAL_RECORD_RETRY_MS);\n return;\n }\n reportUnrecordedEnd(error);\n process.exitCode = 1;\n setTimeout(() => process.exit(1), 10).unref();\n return;\n }\n setTimeout(() => process.exit(), 10).unref();\n}\n\n/**\n * Every attempt at the terminal record failed, so the manager will find a stale heartbeat and\n * call the job lost. Say what really happened at the end of the job's log, the one place left\n * that someone reads. The log is safe to write: the record never turned terminal, so the\n * manager cannot have dismissed the job and deleted it.\n */\nfunction reportUnrecordedEnd(error: unknown): void {\n const code = (error as NodeJS.ErrnoException | undefined)?.code ?? 'unknown error';\n const outcome =\n state.exitCode === undefined || state.exitCode === null\n ? state.status\n : `${state.status}, exit code ${state.exitCode}`;\n try {\n appendFileSync(\n spec.outputPath,\n `[runner: the job ended (${outcome}) but its record could not be written (${code}); Book will report it as lost]\\n`,\n );\n } catch {\n // There is nowhere left to say it.\n }\n}\n\n/** The supervisor built beside this file: `.js` in dist, `.ts` when run from source through tsx. */\nfunction supervisorPath(): string {\n const self = fileURLToPath(import.meta.url);\n return join(dirname(self), `job-supervisor${extname(self)}`);\n}\n\nfunction failStartup(what: string, error: unknown): never {\n const reason = error instanceof Error ? error.message : String(error);\n process.stderr.write(`Persistent background runner could not ${what}: ${reason}\\n`);\n process.exit(1);\n}\n\n/**\n * The env the supervisor is started with. `NODE_OPTIONS` is meant for the command the user asked\n * for, not for the supervisor standing between them: left in place it makes the supervisor open the\n * inspector and write \"Debugger listening...\" into the job's log, which the runner appends to the\n * command's own output. It is therefore taken out here and carried beside it, under a name of this\n * runner's own, which the supervisor puts back for the command alone.\n *\n * This process inherited Book's environment, so a `NODE_ENV` Book defaulted for its own renderer\n * travels the whole chain: it is not the job's, and a persistent job outlives Book to be the one\n * that most certainly must not carry it.\n */\nfunction supervisorEnv(): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = buildChildEnv(process.env, spec.env);\n const nodeOptions = env.NODE_OPTIONS;\n delete env.NODE_OPTIONS;\n if (nodeOptions !== undefined) env.BOOK_SUPERVISED_NODE_OPTIONS = nodeOptions;\n return env;\n}\n\n/**\n * Create the log, write the first record, then start the command under its supervisor. The first\n * record says `starting`, and the job counts as started once the second, carrying the command's\n * pid, has landed: `start()` returns on the first record whose status is not `starting`, so a\n * record that claimed otherwise would report a persistent job with nothing to stop. That first\n * write is therefore the one that cannot be best effort — without it the manager gives up, forgets\n * the job and deletes its files while the command runs on with nothing left to stop it. So until\n * it has landed, a failure ends the runner before anything runs; a failure on the second ends the\n * command first, and both say why on stderr, which `start()` reports.\n */\nfunction startRunner(): void {\n try {\n writeFileSync(spec.outputPath, '', { encoding: 'utf8', mode: 0o600 });\n } catch (error) {\n failStartup('create its job log', error);\n }\n try {\n persist();\n } catch (error) {\n failStartup('write its job record', error);\n }\n let proc: ChildProcess;\n try {\n // `process.execArgv` carries tsx's loader when this runner itself runs from source.\n proc = spawn(process.execPath, [...process.execArgv, supervisorPath(), specPath], {\n cwd: spec.workdir,\n env: supervisorEnv(),\n detached: true,\n stdio: ['pipe', 'pipe', 'pipe'],\n windowsHide: true,\n });\n } catch (error) {\n appendBounded(error instanceof Error ? `${error.message}\\n` : `${String(error)}\\n`);\n process.exitCode = 1;\n finish('failed');\n return;\n }\n child = proc;\n // The supervisor's stdin is its lifeline to this process: never written and never closed, it\n // reaches end-of-file only when this process is gone. See job-supervisor.ts.\n proc.stdin?.on('error', () => {});\n proc.stdout?.on('data', appendBounded);\n proc.stderr?.on('data', appendBounded);\n proc.on('error', (error) => {\n appendBounded(`${error.message}\\n`);\n if (state.status !== 'stopping') finish('failed');\n });\n proc.on('close', (code, signal) => {\n if (terminal || state.status === 'stopping') return;\n finish(code === 0 ? 'exited' : 'failed', code, signal);\n });\n state = { ...state, status: 'running', childPid: proc.pid };\n try {\n persist();\n } catch (error) {\n // The manager calls the job started only once it reads this record. Without it, it would\n // give up, forget the job and delete its files while the command ran on: end the command\n // first, then say why.\n terminal = true;\n void terminateTree().finally(() => failStartup('write its job record', error));\n return;\n }\n heartbeat = setInterval(() => persistBestEffort('heartbeat'), 1_000);\n controlPoll = setInterval(() => {\n if (!existsSync(spec.controlPath) || terminal) return;\n const control = readJsonFile<{ token?: string; action?: string; reason?: string }>(\n spec.controlPath,\n );\n if (control?.token !== spec.token || control.action !== 'stop') return;\n void requestTermination('killed', control.reason ?? 'requested');\n }, 250);\n deadline = spec.timeoutMs\n ? setTimeout(() => {\n void requestTermination('timed_out', 'timeout');\n }, spec.timeoutMs)\n : undefined;\n}\n\nasync function requestTermination(status: 'killed' | 'timed_out', reason: string): Promise<void> {\n if (terminal || terminationInFlight) return;\n terminationInFlight = true;\n state = { ...state, status: 'stopping', stopReason: reason };\n persistBestEffort('stopping state');\n try {\n if (await terminateTree()) {\n finish(status, child?.exitCode, child?.signalCode, reason);\n } else {\n appendBounded('[termination requested; process is still running; retrying]\\n');\n const retry = setTimeout(() => void requestTermination(status, reason), 500);\n retry.unref();\n }\n } finally {\n terminationInFlight = false;\n }\n}\n\nstartRunner();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,SAAS,aAAgC;AAEzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS,SAAS,YAAY;AACvC,SAAS,qBAAqB;AAgB9B,IAAM,WAAW,QAAQ,KAAK,CAAC;AAC/B,IAAI,CAAC,UAAU;AACb,UAAQ,WAAW;AACnB,QAAM,IAAI,MAAM,8CAA8C;AAChE;AACA,IAAM,SAAS,wBAAwB,QAAQ;AAC/C,IAAI,WAAW,QAAQ;AACrB,UAAQ,WAAW;AACnB,QAAM,IAAI,MAAM,OAAO,KAAK;AAC9B;AACA,IAAM,OAA4B,OAAO;AAQzC,IAAM,gCAAgC;AACtC,IAAM,qCAAqC;AAE3C,IAAM,2BAA2B;AACjC,IAAM,2BAA2B;AAEjC,IAAI;AACJ,IAAI,WAAW;AACf,IAAI,sBAAsB;AAE1B,IAAI;AACJ,IAAI;AACJ,IAAI;AACJ,IAAM,YAAY,KAAK,IAAI;AAC3B,IAAI,QAA8B;AAAA,EAChC,SAAS;AAAA,EACT,UAAU;AAAA,EACV,IAAI,KAAK;AAAA,EACT,SAAS,KAAK;AAAA,EACd,OAAO,KAAK;AAAA,EACZ,SAAS,KAAK;AAAA,EACd,QAAQ;AAAA,EACR,QAAQ,KAAK;AAAA,EACb,WAAW,KAAK;AAAA,EAChB,WAAW,QAAQ;AAAA,EACnB,WAAW,KAAK;AAAA,EAChB;AAAA,EACA,aAAa;AAAA,EACb,wBAAwB;AAAA,EACxB,gBAAgB;AAAA,EAChB,WAAW,KAAK;AAAA,EAChB,YAAY,KAAK,YAAY,YAAY,KAAK,YAAY;AAAA,EAC1D,iBAAiB,KAAK;AAAA,EACtB,WAAW,KAAK;AAAA,EAChB,aAAa,KAAK;AAAA,EAClB,YAAY,KAAK;AAAA,EACjB,aAAa,KAAK;AAAA,EAClB,oBAAoB;AAAA,EACpB,6BAA6B;AAAA,EAC7B,gCAAgC;AAClC;AAEA,SAAS,QAAQ,WAAW,+BAAqC;AAC/D,UAAQ,EAAE,GAAG,OAAO,UAAU,MAAM,WAAW,GAAG,aAAa,KAAK,IAAI,EAAE;AAC1E,kBAAgB,KAAK,YAAY,OAAO,EAAE,qBAAqB,SAAS,CAAC;AAC3E;AASA,SAAS,kBAAkB,MAAoB;AAC7C,MAAI;AACF,YAAQ,kCAAkC;AAAA,EAC5C,SAAS,OAAO;AACd,UAAM,OAAQ,OAA6C,QAAQ;AACnE,YAAQ;AAAA,MACN,GAAG;AAAA,MACH,sBAAsB,MAAM,uBAAuB,KAAK;AAAA,MACxD,sBAAsB,GAAG,IAAI,kBAAkB,IAAI;AAAA,IACrD;AAAA,EACF;AACF;AAEA,SAAS,cAAc,MAAqB;AAC1C,QAAM,QAAQ,OAAO,SAAS,IAAI,IAAI,OAAO,OAAO,KAAK,OAAO,IAAI,CAAC;AACrE,MAAI,MAAM,WAAW,EAAG;AACxB,MAAI;AACF,mBAAe,KAAK,YAAY,KAAK;AACrC,UAAM,OAAO,SAAS,KAAK,UAAU,EAAE;AACvC,QAAI,QAAQ,KAAK,YAAa;AAC9B,UAAM,WAAW,aAAa,KAAK,UAAU,EAAE,SAAS,CAAC,KAAK,WAAW;AACzE,UAAM,YAAY,OAAO,SAAS;AAClC,iBAAa,KAAK,YAAY,CAAC;AAC/B,kBAAc,KAAK,YAAY,QAAQ;AACvC,YAAQ;AAAA,MACN,GAAG;AAAA,MACH,yBAAyB,MAAM,0BAA0B,KAAK;AAAA,MAC9D,iBAAiB,MAAM,kBAAkB,KAAK;AAAA,IAChD;AACA,sBAAkB,cAAc;AAAA,EAClC,QAAQ;AAAA,EAER;AACF;AAYA,eAAe,gBAAkC;AAC/C,QAAM,OAAO;AACb,QAAM,MAAM,MAAM;AAClB,MAAI,CAAC,QAAQ,QAAQ,OAAW,QAAO;AACvC,MAAI,QAAQ,aAAa,SAAS;AAChC,WAAO,qBAAqB,MAAM,KAAK,CAAC,cAAc,oBAAoB,MAAM,SAAS,CAAC;AAAA,EAC5F;AACA,MAAI;AACF,YAAQ,KAAK,KAAK,SAAS;AAAA,EAC7B,QAAQ;AACN,uBAAmB,MAAM,KAAK,SAAS;AAAA,EACzC;AACA,MAAI,MAAM,wBAAwB,KAAK,kBAAkB,EAAG,QAAO;AACnE,qBAAmB,MAAM,KAAK,SAAS;AACvC,SAAO,wBAAwB,KAAK,kBAAkB;AACxD;AAEA,SAAS,OACP,QACA,MACA,QACA,YACM;AACN,MAAI,SAAU;AACd,aAAW;AAIX,SAAO,QAAQ,IAAI,QAAQ,aAAa;AACxC,SAAO,QAAQ,IAAI,QAAQ,aAAa;AACxC,UAAQ;AAAA,IACN,GAAG;AAAA,IACH;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA,YAAY,KAAK,IAAI;AAAA,IACrB,oBAAoB,MAAM,qBAAqB;AAAA,EACjD;AAOA,MAAI;AACF,WAAO,KAAK,aAAa,EAAE,OAAO,KAAK,CAAC;AACxC,WAAO,UAAU,EAAE,OAAO,KAAK,CAAC;AAAA,EAClC,QAAQ;AAAA,EAIR;AACA,MAAI,UAAW,eAAc,SAAS;AACtC,MAAI,YAAa,eAAc,WAAW;AAC1C,MAAI,SAAU,cAAa,QAAQ;AACnC,wBAAsB,CAAC;AACzB;AASA,SAAS,sBAAsB,SAAuB;AACpD,MAAI;AACF,YAAQ;AAAA,EACV,SAAS,OAAO;AACd,QAAI,UAAU,0BAA0B;AACtC,iBAAW,MAAM,sBAAsB,UAAU,CAAC,GAAG,wBAAwB;AAC7E;AAAA,IACF;AACA,wBAAoB,KAAK;AACzB,YAAQ,WAAW;AACnB,eAAW,MAAM,QAAQ,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM;AAC5C;AAAA,EACF;AACA,aAAW,MAAM,QAAQ,KAAK,GAAG,EAAE,EAAE,MAAM;AAC7C;AAQA,SAAS,oBAAoB,OAAsB;AACjD,QAAM,OAAQ,OAA6C,QAAQ;AACnE,QAAM,UACJ,MAAM,aAAa,UAAa,MAAM,aAAa,OAC/C,MAAM,SACN,GAAG,MAAM,MAAM,eAAe,MAAM,QAAQ;AAClD,MAAI;AACF;AAAA,MACE,KAAK;AAAA,MACL,2BAA2B,OAAO,0CAA0C,IAAI;AAAA;AAAA,IAClF;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAGA,SAAS,iBAAyB;AAChC,QAAM,OAAO,cAAc,YAAY,GAAG;AAC1C,SAAO,KAAK,QAAQ,IAAI,GAAG,iBAAiB,QAAQ,IAAI,CAAC,EAAE;AAC7D;AAEA,SAAS,YAAY,MAAc,OAAuB;AACxD,QAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACpE,UAAQ,OAAO,MAAM,0CAA0C,IAAI,KAAK,MAAM;AAAA,CAAI;AAClF,UAAQ,KAAK,CAAC;AAChB;AAaA,SAAS,gBAAmC;AAC1C,QAAM,MAAyB,cAAc,QAAQ,KAAK,KAAK,GAAG;AAClE,QAAM,cAAc,IAAI;AACxB,SAAO,IAAI;AACX,MAAI,gBAAgB,OAAW,KAAI,+BAA+B;AAClE,SAAO;AACT;AAYA,SAAS,cAAoB;AAC3B,MAAI;AACF,kBAAc,KAAK,YAAY,IAAI,EAAE,UAAU,QAAQ,MAAM,IAAM,CAAC;AAAA,EACtE,SAAS,OAAO;AACd,gBAAY,sBAAsB,KAAK;AAAA,EACzC;AACA,MAAI;AACF,YAAQ;AAAA,EACV,SAAS,OAAO;AACd,gBAAY,wBAAwB,KAAK;AAAA,EAC3C;AACA,MAAI;AACJ,MAAI;AAEF,WAAO,MAAM,QAAQ,UAAU,CAAC,GAAG,QAAQ,UAAU,eAAe,GAAG,QAAQ,GAAG;AAAA,MAChF,KAAK,KAAK;AAAA,MACV,KAAK,cAAc;AAAA,MACnB,UAAU;AAAA,MACV,OAAO,CAAC,QAAQ,QAAQ,MAAM;AAAA,MAC9B,aAAa;AAAA,IACf,CAAC;AAAA,EACH,SAAS,OAAO;AACd,kBAAc,iBAAiB,QAAQ,GAAG,MAAM,OAAO;AAAA,IAAO,GAAG,OAAO,KAAK,CAAC;AAAA,CAAI;AAClF,YAAQ,WAAW;AACnB,WAAO,QAAQ;AACf;AAAA,EACF;AACA,UAAQ;AAGR,OAAK,OAAO,GAAG,SAAS,MAAM;AAAA,EAAC,CAAC;AAChC,OAAK,QAAQ,GAAG,QAAQ,aAAa;AACrC,OAAK,QAAQ,GAAG,QAAQ,aAAa;AACrC,OAAK,GAAG,SAAS,CAAC,UAAU;AAC1B,kBAAc,GAAG,MAAM,OAAO;AAAA,CAAI;AAClC,QAAI,MAAM,WAAW,WAAY,QAAO,QAAQ;AAAA,EAClD,CAAC;AACD,OAAK,GAAG,SAAS,CAAC,MAAM,WAAW;AACjC,QAAI,YAAY,MAAM,WAAW,WAAY;AAC7C,WAAO,SAAS,IAAI,WAAW,UAAU,MAAM,MAAM;AAAA,EACvD,CAAC;AACD,UAAQ,EAAE,GAAG,OAAO,QAAQ,WAAW,UAAU,KAAK,IAAI;AAC1D,MAAI;AACF,YAAQ;AAAA,EACV,SAAS,OAAO;AAId,eAAW;AACX,SAAK,cAAc,EAAE,QAAQ,MAAM,YAAY,wBAAwB,KAAK,CAAC;AAC7E;AAAA,EACF;AACA,cAAY,YAAY,MAAM,kBAAkB,WAAW,GAAG,GAAK;AACnE,gBAAc,YAAY,MAAM;AAC9B,QAAI,CAAC,WAAW,KAAK,WAAW,KAAK,SAAU;AAC/C,UAAM,UAAU;AAAA,MACd,KAAK;AAAA,IACP;AACA,QAAI,SAAS,UAAU,KAAK,SAAS,QAAQ,WAAW,OAAQ;AAChE,SAAK,mBAAmB,UAAU,QAAQ,UAAU,WAAW;AAAA,EACjE,GAAG,GAAG;AACN,aAAW,KAAK,YACZ,WAAW,MAAM;AACf,SAAK,mBAAmB,aAAa,SAAS;AAAA,EAChD,GAAG,KAAK,SAAS,IACjB;AACN;AAEA,eAAe,mBAAmB,QAAgC,QAA+B;AAC/F,MAAI,YAAY,oBAAqB;AACrC,wBAAsB;AACtB,UAAQ,EAAE,GAAG,OAAO,QAAQ,YAAY,YAAY,OAAO;AAC3D,oBAAkB,gBAAgB;AAClC,MAAI;AACF,QAAI,MAAM,cAAc,GAAG;AACzB,aAAO,QAAQ,OAAO,UAAU,OAAO,YAAY,MAAM;AAAA,IAC3D,OAAO;AACL,oBAAc,+DAA+D;AAC7E,YAAM,QAAQ,WAAW,MAAM,KAAK,mBAAmB,QAAQ,MAAM,GAAG,GAAG;AAC3E,YAAM,MAAM;AAAA,IACd;AAAA,EACF,UAAE;AACA,0BAAsB;AAAA,EACxB;AACF;AAEA,YAAY;","names":[]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,112 @@
1
+ import {
2
+ loadPersistentShellSpec
3
+ } from "./chunk-YC6W4EJH.js";
4
+ import {
5
+ TERMINATE_GRACE_MS,
6
+ runTaskkill
7
+ } from "./chunk-C7ADUNPR.js";
8
+ import "./chunk-RT5SATF7.js";
9
+ import {
10
+ buildChildEnv
11
+ } from "./chunk-XHAO3K7I.js";
12
+ import "./chunk-224FJZLH.js";
13
+ import "./chunk-ZPSZZUWF.js";
14
+
15
+ // src/job-supervisor.ts
16
+ import { spawn } from "child_process";
17
+ import { constants } from "os";
18
+ var RERAISABLE_SIGNALS = /* @__PURE__ */ new Set([
19
+ "SIGTERM",
20
+ "SIGINT",
21
+ "SIGHUP",
22
+ "SIGKILL"
23
+ ]);
24
+ var [specPath] = process.argv.slice(2);
25
+ if (!specPath) {
26
+ process.stderr.write("Missing persistent shell specification path.\n");
27
+ process.exit(2);
28
+ }
29
+ var loaded = loadPersistentShellSpec(specPath);
30
+ if ("error" in loaded) {
31
+ process.stderr.write(`${loaded.error}
32
+ `);
33
+ process.exit(2);
34
+ }
35
+ var spec = loaded.spec;
36
+ function commandEnv() {
37
+ const env = buildChildEnv(process.env);
38
+ const nodeOptions = env.BOOK_SUPERVISED_NODE_OPTIONS;
39
+ delete env.BOOK_SUPERVISED_NODE_OPTIONS;
40
+ if (nodeOptions !== void 0) env.NODE_OPTIONS = nodeOptions;
41
+ return env;
42
+ }
43
+ var options = {
44
+ stdio: ["ignore", "inherit", "inherit"],
45
+ windowsHide: true,
46
+ env: commandEnv()
47
+ };
48
+ var command = spec.exec ? spawn(spec.exec.file, spec.exec.args, { ...options, shell: false }) : spawn(spec.effectiveCommand, { ...options, shell: true });
49
+ var lifelineLost = false;
50
+ command.on("error", (error) => {
51
+ process.stderr.write(`${error.message}
52
+ `);
53
+ if (!lifelineLost) process.exit(1);
54
+ });
55
+ command.on("exit", (code, signal) => {
56
+ if (lifelineLost) return;
57
+ if (signal && process.platform !== "win32") {
58
+ if (RERAISABLE_SIGNALS.has(signal)) {
59
+ process.removeAllListeners(signal);
60
+ process.kill(process.pid, signal);
61
+ setTimeout(() => process.exit(1), 100);
62
+ return;
63
+ }
64
+ process.exit(128 + (constants.signals[signal] ?? 0));
65
+ }
66
+ process.exit(code ?? 1);
67
+ });
68
+ var FORWARDED_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
69
+ var forwarded = /* @__PURE__ */ new Set();
70
+ if (process.platform !== "win32") {
71
+ for (const kind of FORWARDED_SIGNALS) {
72
+ process.on(kind, () => {
73
+ if (lifelineLost || forwarded.has(kind)) return;
74
+ forwarded.add(kind);
75
+ try {
76
+ process.kill(-process.pid, kind);
77
+ } catch {
78
+ }
79
+ });
80
+ }
81
+ }
82
+ var TASKKILL_TIMEOUT_MS = 1e4;
83
+ var TASKKILL_ATTEMPTS = 3;
84
+ async function killCommandTree(pid) {
85
+ for (let attempt = 1; attempt <= TASKKILL_ATTEMPTS; attempt++) {
86
+ if (await runTaskkill(pid, TASKKILL_TIMEOUT_MS)) return;
87
+ }
88
+ }
89
+ function endCommandTree() {
90
+ if (lifelineLost) return;
91
+ lifelineLost = true;
92
+ if (process.platform === "win32") {
93
+ if (command.pid === void 0) process.exit(1);
94
+ void killCommandTree(command.pid).finally(() => process.exit(1));
95
+ return;
96
+ }
97
+ try {
98
+ process.kill(-process.pid, "SIGTERM");
99
+ } catch {
100
+ }
101
+ setTimeout(() => {
102
+ try {
103
+ process.kill(-process.pid, "SIGKILL");
104
+ } catch {
105
+ }
106
+ process.exit(1);
107
+ }, TERMINATE_GRACE_MS);
108
+ }
109
+ process.stdin.on("end", endCommandTree);
110
+ process.stdin.on("error", endCommandTree);
111
+ process.stdin.resume();
112
+ //# sourceMappingURL=job-supervisor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/job-supervisor.ts"],"sourcesContent":["/**\n * The supervisor that keeps a persistent background job's command from outliving its runner.\n *\n * The detached job runner is the only thing that stops a command, so when the runner died any\n * other way than through a stop request (a crash, Task Manager, `kill -9`), the command kept\n * running with nothing left to stop it. On Windows the runner's death did end the shell wrapper,\n * which Node puts in a job object that dies with its parent, but not the process the wrapper had\n * started. Elsewhere the command ran in its own process group, and nothing ended it at all.\n *\n * The runner now starts this process detached, so that it outlives the runner: outside the\n * runner's job object on Windows, as the leader of its own process group elsewhere. This process\n * starts the command. Its stdin is a pipe the runner holds open and never writes, a lifeline: when\n * the runner goes away, for any reason, the operating system closes the pipe, and this process\n * ends the command's whole tree and then itself. The pipe needs no polling and cannot be fooled by\n * a reused pid. The command writes straight into the runner's own pipes, and this process exits\n * with the command's exit code, so the runner records what it always recorded. The pid Book reports\n * for the job is this process's; off Windows a signal sent to it is passed to the whole job.\n *\n * Usage, from the runner: node job-supervisor.js <spec-path>\n */\nimport { spawn, type SpawnOptions } from 'node:child_process';\nimport { constants } from 'node:os';\nimport { buildChildEnv } from './child-env.js';\nimport { loadPersistentShellSpec } from './jobs/persistent-store.js';\nimport { runTaskkill, TERMINATE_GRACE_MS } from './jobs/process-tree.js';\n\n/** Signals this process can die of just as the command did: Node gives them no meaning of its own. */\nconst RERAISABLE_SIGNALS: ReadonlySet<NodeJS.Signals> = new Set([\n 'SIGTERM',\n 'SIGINT',\n 'SIGHUP',\n 'SIGKILL',\n]);\n\nconst [specPath] = process.argv.slice(2);\nif (!specPath) {\n process.stderr.write('Missing persistent shell specification path.\\n');\n process.exit(2);\n}\n// The same checks the runner applies: this process starts the command, so a spec it must not\n// execute is exactly as unsafe here as it is there.\nconst loaded = loadPersistentShellSpec(specPath);\nif ('error' in loaded) {\n process.stderr.write(`${loaded.error}\\n`);\n process.exit(2);\n}\nconst spec = loaded.spec;\n\n/**\n * The env the command is started with. This process was started without `NODE_OPTIONS`, so that the\n * options the user asked for apply to their command rather than opening an inspector here and\n * writing about it into the job's log; the runner carries them beside it for exactly that reason.\n *\n * A `NODE_ENV` Book defaulted for its own renderer is dropped here too, the last link of the\n * chain: the runner has already removed its own, and this process reads what it inherited.\n */\nfunction commandEnv(): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = buildChildEnv(process.env);\n const nodeOptions = env.BOOK_SUPERVISED_NODE_OPTIONS;\n delete env.BOOK_SUPERVISED_NODE_OPTIONS;\n if (nodeOptions !== undefined) env.NODE_OPTIONS = nodeOptions;\n return env;\n}\n\nconst options: SpawnOptions = {\n stdio: ['ignore', 'inherit', 'inherit'],\n windowsHide: true,\n env: commandEnv(),\n};\n// Sandboxed specs carry an argv; only unsandboxed ones go through a shell.\nconst command = spec.exec\n ? spawn(spec.exec.file, spec.exec.args, { ...options, shell: false })\n : spawn(spec.effectiveCommand, { ...options, shell: true });\n\n/** Set once the runner is gone and this process has begun ending the command's tree. */\nlet lifelineLost = false;\n\ncommand.on('error', (error) => {\n process.stderr.write(`${error.message}\\n`);\n if (!lifelineLost) process.exit(1);\n});\n\ncommand.on('exit', (code, signal) => {\n // Once the runner is gone, this process exits when the tree has been ended, not before.\n if (lifelineLost) return;\n if (signal && process.platform !== 'win32') {\n if (RERAISABLE_SIGNALS.has(signal)) {\n // Die of the command's signal, so the runner records it the way it did before this process\n // stood between them.\n // Drop the forwarding handler, or it would swallow the re-raised signal.\n process.removeAllListeners(signal);\n process.kill(process.pid, signal);\n setTimeout(() => process.exit(1), 100);\n return;\n }\n // A signal Node gives a meaning of its own cannot be re-raised: SIGUSR1 would open the\n // inspector and write \"Debugger listening...\" into the job's log, and SIGSEGV, SIGBUS, SIGABRT\n // and SIGQUIT would dump this process's core. Report the command's death the way a shell does.\n process.exit(128 + (constants.signals[signal] ?? 0));\n }\n process.exit(code ?? 1);\n});\n\n/**\n * Signals sent to this process, the pid Book reports for the job, are meant for the job: pass each\n * to the whole group, as the runner's own stop does, and exit with the command. Forwarded once per\n * kind, because the forwarded signal reaches this process too. Windows has no such signals; there\n * the runner's stop goes through taskkill.\n */\nconst FORWARDED_SIGNALS: readonly NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];\nconst forwarded = new Set<NodeJS.Signals>();\nif (process.platform !== 'win32') {\n for (const kind of FORWARDED_SIGNALS) {\n process.on(kind, () => {\n if (lifelineLost || forwarded.has(kind)) return;\n forwarded.add(kind);\n try {\n process.kill(-process.pid, kind);\n } catch {\n // The group has already exited.\n }\n });\n }\n}\n\n/** How long one `taskkill` may take, and how many times it is tried before the tree is given up on. */\nconst TASKKILL_TIMEOUT_MS = 10_000;\nconst TASKKILL_ATTEMPTS = 3;\n\n/**\n * `taskkill /T /F` on the command's tree, retried until one of them actually runs. A single attempt\n * that fails or times out leaves the command's descendants running, and here there is nobody to\n * escalate afterwards: this process is about to exit and the runner it served is already gone.\n */\nasync function killCommandTree(pid: number): Promise<void> {\n for (let attempt = 1; attempt <= TASKKILL_ATTEMPTS; attempt++) {\n if (await runTaskkill(pid, TASKKILL_TIMEOUT_MS)) return;\n }\n}\n\n/** The runner is gone: end the command's whole tree, then exit. */\nfunction endCommandTree(): void {\n if (lifelineLost) return;\n lifelineLost = true;\n if (process.platform === 'win32') {\n if (command.pid === undefined) process.exit(1);\n // taskkill walks the tree from the command's root, which is still alive because this process\n // holds it. Exit only when the tree kill is done: as a child of this process, it would die with\n // it, taking the walks it had left undone with it.\n void killCommandTree(command.pid).finally(() => process.exit(1));\n return;\n }\n // This process leads the command's process group, and the group signal below reaches it too. Its\n // own SIGTERM is already spared, by the forwarding handler above returning now that the lifeline\n // is lost; that is what lets the escalation below run, and it is why nothing else has to spare it\n // a second time.\n try {\n process.kill(-process.pid, 'SIGTERM');\n } catch {\n // The group has already exited.\n }\n // The group includes this process, so waitForProcessGroupExit could never report it empty:\n // escalate on the grace alone. SIGKILL ends this process too.\n setTimeout(() => {\n try {\n process.kill(-process.pid, 'SIGKILL');\n } catch {\n // The group has already exited.\n }\n process.exit(1);\n }, TERMINATE_GRACE_MS);\n}\n\nprocess.stdin.on('end', endCommandTree);\nprocess.stdin.on('error', endCommandTree);\nprocess.stdin.resume();\n"],"mappings":";;;;;;;;;;;;;;;AAoBA,SAAS,aAAgC;AACzC,SAAS,iBAAiB;AAM1B,IAAM,qBAAkD,oBAAI,IAAI;AAAA,EAC9D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,IAAM,CAAC,QAAQ,IAAI,QAAQ,KAAK,MAAM,CAAC;AACvC,IAAI,CAAC,UAAU;AACb,UAAQ,OAAO,MAAM,gDAAgD;AACrE,UAAQ,KAAK,CAAC;AAChB;AAGA,IAAM,SAAS,wBAAwB,QAAQ;AAC/C,IAAI,WAAW,QAAQ;AACrB,UAAQ,OAAO,MAAM,GAAG,OAAO,KAAK;AAAA,CAAI;AACxC,UAAQ,KAAK,CAAC;AAChB;AACA,IAAM,OAAO,OAAO;AAUpB,SAAS,aAAgC;AACvC,QAAM,MAAyB,cAAc,QAAQ,GAAG;AACxD,QAAM,cAAc,IAAI;AACxB,SAAO,IAAI;AACX,MAAI,gBAAgB,OAAW,KAAI,eAAe;AAClD,SAAO;AACT;AAEA,IAAM,UAAwB;AAAA,EAC5B,OAAO,CAAC,UAAU,WAAW,SAAS;AAAA,EACtC,aAAa;AAAA,EACb,KAAK,WAAW;AAClB;AAEA,IAAM,UAAU,KAAK,OACjB,MAAM,KAAK,KAAK,MAAM,KAAK,KAAK,MAAM,EAAE,GAAG,SAAS,OAAO,MAAM,CAAC,IAClE,MAAM,KAAK,kBAAkB,EAAE,GAAG,SAAS,OAAO,KAAK,CAAC;AAG5D,IAAI,eAAe;AAEnB,QAAQ,GAAG,SAAS,CAAC,UAAU;AAC7B,UAAQ,OAAO,MAAM,GAAG,MAAM,OAAO;AAAA,CAAI;AACzC,MAAI,CAAC,aAAc,SAAQ,KAAK,CAAC;AACnC,CAAC;AAED,QAAQ,GAAG,QAAQ,CAAC,MAAM,WAAW;AAEnC,MAAI,aAAc;AAClB,MAAI,UAAU,QAAQ,aAAa,SAAS;AAC1C,QAAI,mBAAmB,IAAI,MAAM,GAAG;AAIlC,cAAQ,mBAAmB,MAAM;AACjC,cAAQ,KAAK,QAAQ,KAAK,MAAM;AAChC,iBAAW,MAAM,QAAQ,KAAK,CAAC,GAAG,GAAG;AACrC;AAAA,IACF;AAIA,YAAQ,KAAK,OAAO,UAAU,QAAQ,MAAM,KAAK,EAAE;AAAA,EACrD;AACA,UAAQ,KAAK,QAAQ,CAAC;AACxB,CAAC;AAQD,IAAM,oBAA+C,CAAC,WAAW,UAAU,QAAQ;AACnF,IAAM,YAAY,oBAAI,IAAoB;AAC1C,IAAI,QAAQ,aAAa,SAAS;AAChC,aAAW,QAAQ,mBAAmB;AACpC,YAAQ,GAAG,MAAM,MAAM;AACrB,UAAI,gBAAgB,UAAU,IAAI,IAAI,EAAG;AACzC,gBAAU,IAAI,IAAI;AAClB,UAAI;AACF,gBAAQ,KAAK,CAAC,QAAQ,KAAK,IAAI;AAAA,MACjC,QAAQ;AAAA,MAER;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAGA,IAAM,sBAAsB;AAC5B,IAAM,oBAAoB;AAO1B,eAAe,gBAAgB,KAA4B;AACzD,WAAS,UAAU,GAAG,WAAW,mBAAmB,WAAW;AAC7D,QAAI,MAAM,YAAY,KAAK,mBAAmB,EAAG;AAAA,EACnD;AACF;AAGA,SAAS,iBAAuB;AAC9B,MAAI,aAAc;AAClB,iBAAe;AACf,MAAI,QAAQ,aAAa,SAAS;AAChC,QAAI,QAAQ,QAAQ,OAAW,SAAQ,KAAK,CAAC;AAI7C,SAAK,gBAAgB,QAAQ,GAAG,EAAE,QAAQ,MAAM,QAAQ,KAAK,CAAC,CAAC;AAC/D;AAAA,EACF;AAKA,MAAI;AACF,YAAQ,KAAK,CAAC,QAAQ,KAAK,SAAS;AAAA,EACtC,QAAQ;AAAA,EAER;AAGA,aAAW,MAAM;AACf,QAAI;AACF,cAAQ,KAAK,CAAC,QAAQ,KAAK,SAAS;AAAA,IACtC,QAAQ;AAAA,IAER;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB,GAAG,kBAAkB;AACvB;AAEA,QAAQ,MAAM,GAAG,OAAO,cAAc;AACtC,QAAQ,MAAM,GAAG,SAAS,cAAc;AACxC,QAAQ,MAAM,OAAO;","names":[]}
@@ -0,0 +1,18 @@
1
+ import {
2
+ discoverCommands,
3
+ generateCommandListing,
4
+ resolveCommandBody
5
+ } from "./chunk-EFJ2NH6E.js";
6
+ import "./chunk-KXAYUIKP.js";
7
+ import "./chunk-HQI7NYGX.js";
8
+ import "./chunk-2AAOF5B5.js";
9
+ import "./chunk-XHAO3K7I.js";
10
+ import "./chunk-VPNLCMVR.js";
11
+ import "./chunk-X4NJT436.js";
12
+ import "./chunk-ZPSZZUWF.js";
13
+ export {
14
+ discoverCommands,
15
+ generateCommandListing,
16
+ resolveCommandBody
17
+ };
18
+ //# sourceMappingURL=loader-S77OVTHH.js.map