@wringer/core 0.0.0 → 0.1.1

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 (258) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +71 -2
  3. package/dist/answer-supervisor-connection.d.ts +8 -0
  4. package/dist/app-machine.d.ts +76 -0
  5. package/dist/append-run-event.d.ts +16 -0
  6. package/dist/bind-drivers.d.ts +9 -0
  7. package/dist/build-commands.d.ts +5 -0
  8. package/dist/build-envelope-json-schema.d.ts +1 -0
  9. package/dist/build-envelope.d.ts +16 -0
  10. package/dist/build-launch-guard.d.ts +1 -0
  11. package/dist/build-manifest-write.d.ts +6 -0
  12. package/dist/build-promised-object-reads.d.ts +7 -0
  13. package/dist/build-report.d.ts +10 -0
  14. package/dist/build-run-handle.d.ts +2 -0
  15. package/dist/build-run-view.d.ts +29 -0
  16. package/dist/build-service-key.d.ts +1 -0
  17. package/dist/build-shut-down-steps.d.ts +14 -0
  18. package/dist/build-skip-worktree-marks.d.ts +7 -0
  19. package/dist/build-spawn-error.d.ts +2 -0
  20. package/dist/build-state-paths.d.ts +21 -0
  21. package/dist/build-submodule-adds.d.ts +7 -0
  22. package/dist/builtins/build-doctor-command.d.ts +2 -0
  23. package/dist/builtins/build-exec-command.d.ts +2 -0
  24. package/dist/builtins/build-lifecycle-commands.d.ts +3 -0
  25. package/dist/builtins/build-state-commands.d.ts +2 -0
  26. package/dist/builtins/build-use-pointers.d.ts +9 -0
  27. package/dist/builtins/build-verdict-commands.d.ts +2 -0
  28. package/dist/builtins/call-run-supervisor.d.ts +5 -0
  29. package/dist/builtins/pick-doctor-outcome.d.ts +13 -0
  30. package/dist/builtins/pick-start-action.d.ts +10 -0
  31. package/dist/builtins/pick-stop-plan.d.ts +14 -0
  32. package/dist/builtins/start-run.d.ts +22 -0
  33. package/dist/builtins/stop-data-schema.d.ts +6 -0
  34. package/dist/builtins/stop-run.d.ts +3 -0
  35. package/dist/call-supervisor.d.ts +10 -0
  36. package/dist/cap-data.d.ts +17 -0
  37. package/dist/collect-driver-configs.d.ts +7 -0
  38. package/dist/create-directory.d.ts +1 -0
  39. package/dist/create-evidence-writer.d.ts +13 -0
  40. package/dist/create-file-state.d.ts +36 -0
  41. package/dist/create-log-writer.d.ts +5 -0
  42. package/dist/create-node-repo-host.d.ts +2 -0
  43. package/dist/create-node-state-file-system.d.ts +10 -0
  44. package/dist/create-repo-state.d.ts +2 -0
  45. package/dist/create-socket-folder.d.ts +1 -0
  46. package/dist/create-wringer.d.ts +17 -0
  47. package/dist/decide-guard.d.ts +12 -0
  48. package/dist/define-driver.d.ts +13 -0
  49. package/dist/dispatch.d.ts +10 -0
  50. package/dist/envelope.d.ts +63 -0
  51. package/dist/error-codes.d.ts +25 -0
  52. package/dist/find-cancel-failure.d.ts +2 -0
  53. package/dist/find-colliding-service-key.d.ts +5 -0
  54. package/dist/find-own-value.d.ts +1 -0
  55. package/dist/find-remote-stop-failure.d.ts +2 -0
  56. package/dist/find-verdict-refusal.d.ts +3 -0
  57. package/dist/follow-process.d.ts +10 -0
  58. package/dist/get-lock-value.d.ts +2 -0
  59. package/dist/get-target.d.ts +2 -0
  60. package/dist/host.d.ts +19 -0
  61. package/dist/index.d.ts +58 -0
  62. package/dist/is-error-code.d.ts +1 -0
  63. package/dist/keep-cancelled-call.d.ts +1 -0
  64. package/dist/kill-process-group.d.ts +1 -0
  65. package/dist/launch-line-schema.d.ts +3 -0
  66. package/dist/listen-supervisor-socket.d.ts +8 -0
  67. package/dist/local.d.ts +2 -0
  68. package/dist/make-fence.d.ts +8 -0
  69. package/dist/make-fenced-exec.d.ts +10 -0
  70. package/dist/make-process-supervisor-port.d.ts +7 -0
  71. package/dist/open-drive.d.ts +23 -0
  72. package/dist/open-runless-drive.d.ts +17 -0
  73. package/dist/parse-launch-line.d.ts +2 -0
  74. package/dist/parse-patch-id.d.ts +2 -0
  75. package/dist/parse-report-block.d.ts +9 -0
  76. package/dist/parse-run-log.d.ts +2 -0
  77. package/dist/pick-access-refusal.d.ts +12 -0
  78. package/dist/pick-command-needs.d.ts +15 -0
  79. package/dist/pick-command-plan.d.ts +23 -0
  80. package/dist/pick-context-target.d.ts +7 -0
  81. package/dist/pick-drive-kind.d.ts +8 -0
  82. package/dist/pick-envelope-context.d.ts +1 -0
  83. package/dist/pick-envelope-payload.d.ts +12 -0
  84. package/dist/pick-envelope-run.d.ts +6 -0
  85. package/dist/pick-envelope-target.d.ts +4 -0
  86. package/dist/pick-late-failure-write.d.ts +13 -0
  87. package/dist/pick-log-refusal.d.ts +4 -0
  88. package/dist/pick-merge-base-failure.d.ts +2 -0
  89. package/dist/pick-merge-base-steps.d.ts +13 -0
  90. package/dist/pick-pending-cleanups.d.ts +10 -0
  91. package/dist/pick-submodule-refusal.d.ts +2 -0
  92. package/dist/pick-supervisor-operation.d.ts +16 -0
  93. package/dist/prepare-args.d.ts +3 -0
  94. package/dist/probe-process.d.ts +1 -0
  95. package/dist/quote-shell-word.d.ts +1 -0
  96. package/dist/race-deadline.d.ts +1 -0
  97. package/dist/read-changed-paths.d.ts +7 -0
  98. package/dist/read-default-base.d.ts +2 -0
  99. package/dist/read-fingerprint.d.ts +13 -0
  100. package/dist/read-head-sha.d.ts +2 -0
  101. package/dist/read-launch-host.d.ts +26 -0
  102. package/dist/read-pid-namespace.d.ts +1 -0
  103. package/dist/read-stream-line-batches.d.ts +2 -0
  104. package/dist/read-stream-lines.d.ts +2 -0
  105. package/dist/read-text.d.ts +1 -0
  106. package/dist/read-verdicts.d.ts +5 -0
  107. package/dist/remove-directory.d.ts +1 -0
  108. package/dist/remove-file.d.ts +1 -0
  109. package/dist/render-report-markdown.d.ts +4 -0
  110. package/dist/report-schema.d.ts +51 -0
  111. package/dist/run-events.d.ts +129 -0
  112. package/dist/run-git.d.ts +9 -0
  113. package/dist/run-process.d.ts +19 -0
  114. package/dist/run-supervisor-process.d.ts +2 -0
  115. package/dist/run-supervisor.d.ts +16 -0
  116. package/dist/settle-dispatch-answer.d.ts +9 -0
  117. package/dist/settle-envelope.d.ts +16 -0
  118. package/dist/ssh.d.ts +12 -0
  119. package/dist/start-launch.d.ts +20 -0
  120. package/dist/start-services.d.ts +16 -0
  121. package/dist/supervisor-machine.d.ts +78 -0
  122. package/dist/supervisor-protocol.d.ts +43 -0
  123. package/dist/to-wringer-error.d.ts +2 -0
  124. package/dist/types.d.ts +353 -0
  125. package/dist/wait-for-cancelled-calls.d.ts +2 -0
  126. package/dist/wringer-error.d.ts +12 -0
  127. package/dist/write-envelope-log.d.ts +11 -0
  128. package/dist/write-late-failure.d.ts +11 -0
  129. package/dist/write-new-file.d.ts +1 -0
  130. package/envelope.schema.json +175 -0
  131. package/package.json +21 -5
  132. package/src/answer-supervisor-connection.ts +117 -0
  133. package/src/app-machine.ts +174 -0
  134. package/src/append-run-event.ts +131 -0
  135. package/src/bind-drivers.ts +33 -0
  136. package/src/build-commands.ts +104 -0
  137. package/src/build-envelope-json-schema.ts +34 -0
  138. package/src/build-envelope.ts +40 -0
  139. package/src/build-launch-guard.ts +5 -0
  140. package/src/build-manifest-write.ts +26 -0
  141. package/src/build-promised-object-reads.ts +48 -0
  142. package/src/build-report.ts +55 -0
  143. package/src/build-run-handle.ts +13 -0
  144. package/src/build-run-view.ts +120 -0
  145. package/src/build-service-key.ts +4 -0
  146. package/src/build-shut-down-steps.ts +54 -0
  147. package/src/build-skip-worktree-marks.ts +36 -0
  148. package/src/build-spawn-error.ts +12 -0
  149. package/src/build-state-paths.ts +53 -0
  150. package/src/build-submodule-adds.ts +64 -0
  151. package/src/builtins/build-doctor-command.ts +163 -0
  152. package/src/builtins/build-exec-command.ts +41 -0
  153. package/src/builtins/build-lifecycle-commands.ts +115 -0
  154. package/src/builtins/build-state-commands.ts +152 -0
  155. package/src/builtins/build-use-pointers.ts +26 -0
  156. package/src/builtins/build-verdict-commands.ts +265 -0
  157. package/src/builtins/call-run-supervisor.ts +40 -0
  158. package/src/builtins/pick-doctor-outcome.ts +32 -0
  159. package/src/builtins/pick-start-action.ts +35 -0
  160. package/src/builtins/pick-stop-plan.ts +32 -0
  161. package/src/builtins/start-run.ts +281 -0
  162. package/src/builtins/stop-data-schema.ts +8 -0
  163. package/src/builtins/stop-run.ts +49 -0
  164. package/src/call-supervisor.ts +109 -0
  165. package/src/cap-data.ts +43 -0
  166. package/src/collect-driver-configs.ts +36 -0
  167. package/src/create-directory.ts +6 -0
  168. package/src/create-evidence-writer.ts +82 -0
  169. package/src/create-file-state.ts +193 -0
  170. package/src/create-log-writer.ts +38 -0
  171. package/src/create-node-repo-host.ts +20 -0
  172. package/src/create-node-state-file-system.ts +52 -0
  173. package/src/create-repo-state.ts +15 -0
  174. package/src/create-socket-folder.ts +24 -0
  175. package/src/create-wringer.ts +169 -0
  176. package/src/decide-guard.ts +47 -0
  177. package/src/define-driver.ts +165 -0
  178. package/src/dispatch.ts +589 -0
  179. package/src/envelope.ts +57 -0
  180. package/src/error-codes.ts +39 -0
  181. package/src/find-cancel-failure.ts +7 -0
  182. package/src/find-colliding-service-key.ts +16 -0
  183. package/src/find-own-value.ts +8 -0
  184. package/src/find-remote-stop-failure.ts +19 -0
  185. package/src/find-verdict-refusal.ts +26 -0
  186. package/src/follow-process.ts +147 -0
  187. package/src/get-lock-value.ts +17 -0
  188. package/src/get-target.ts +15 -0
  189. package/src/host.ts +26 -0
  190. package/src/index.ts +73 -0
  191. package/src/is-error-code.ts +3 -0
  192. package/src/keep-cancelled-call.ts +21 -0
  193. package/src/kill-process-group.ts +13 -0
  194. package/src/launch-line-schema.ts +6 -0
  195. package/src/listen-supervisor-socket.ts +75 -0
  196. package/src/local.ts +20 -0
  197. package/src/make-fence.ts +27 -0
  198. package/src/make-fenced-exec.ts +52 -0
  199. package/src/make-process-supervisor-port.ts +65 -0
  200. package/src/open-drive.ts +151 -0
  201. package/src/open-runless-drive.ts +71 -0
  202. package/src/parse-launch-line.ts +19 -0
  203. package/src/parse-patch-id.ts +8 -0
  204. package/src/parse-report-block.ts +39 -0
  205. package/src/parse-run-log.ts +34 -0
  206. package/src/pick-access-refusal.ts +32 -0
  207. package/src/pick-command-needs.ts +29 -0
  208. package/src/pick-command-plan.ts +51 -0
  209. package/src/pick-context-target.ts +22 -0
  210. package/src/pick-drive-kind.ts +23 -0
  211. package/src/pick-envelope-context.ts +10 -0
  212. package/src/pick-envelope-payload.ts +28 -0
  213. package/src/pick-envelope-run.ts +25 -0
  214. package/src/pick-envelope-target.ts +10 -0
  215. package/src/pick-late-failure-write.ts +44 -0
  216. package/src/pick-log-refusal.ts +18 -0
  217. package/src/pick-merge-base-failure.ts +15 -0
  218. package/src/pick-merge-base-steps.ts +37 -0
  219. package/src/pick-pending-cleanups.ts +23 -0
  220. package/src/pick-submodule-refusal.ts +46 -0
  221. package/src/pick-supervisor-operation.ts +52 -0
  222. package/src/prepare-args.ts +22 -0
  223. package/src/probe-process.ts +16 -0
  224. package/src/quote-shell-word.ts +9 -0
  225. package/src/race-deadline.ts +23 -0
  226. package/src/read-changed-paths.ts +39 -0
  227. package/src/read-default-base.ts +21 -0
  228. package/src/read-fingerprint.ts +271 -0
  229. package/src/read-head-sha.ts +23 -0
  230. package/src/read-launch-host.ts +64 -0
  231. package/src/read-pid-namespace.ts +18 -0
  232. package/src/read-stream-line-batches.ts +28 -0
  233. package/src/read-stream-lines.ts +11 -0
  234. package/src/read-text.ts +5 -0
  235. package/src/read-verdicts.ts +57 -0
  236. package/src/remove-directory.ts +6 -0
  237. package/src/remove-file.ts +6 -0
  238. package/src/render-report-markdown.ts +53 -0
  239. package/src/report-schema.ts +37 -0
  240. package/src/run-events.ts +71 -0
  241. package/src/run-git.ts +49 -0
  242. package/src/run-process.ts +168 -0
  243. package/src/run-supervisor-process.ts +51 -0
  244. package/src/run-supervisor.ts +327 -0
  245. package/src/settle-dispatch-answer.ts +26 -0
  246. package/src/settle-envelope.ts +34 -0
  247. package/src/ssh.ts +211 -0
  248. package/src/start-launch.ts +327 -0
  249. package/src/start-services.ts +108 -0
  250. package/src/supervisor-machine.ts +169 -0
  251. package/src/supervisor-protocol.ts +42 -0
  252. package/src/to-wringer-error.ts +20 -0
  253. package/src/types.ts +476 -0
  254. package/src/wait-for-cancelled-calls.ts +37 -0
  255. package/src/wringer-error.ts +25 -0
  256. package/src/write-envelope-log.ts +72 -0
  257. package/src/write-late-failure.ts +40 -0
  258. package/src/write-new-file.ts +17 -0
@@ -0,0 +1,28 @@
1
+ export const streamLineCapChars = 64 * 1024;
2
+
3
+ // Yields the whole lines of each chunk together, so a caller can handle a chunk's lines in one
4
+ // step. A line longer than the cap keeps its first characters and drops the rest up to its
5
+ // newline, so a source that prints without a newline cannot grow the buffer without bound.
6
+ export async function* readStreamLineBatches(
7
+ stream: ReadableStream<Uint8Array>,
8
+ ): AsyncGenerator<readonly string[], void, undefined> {
9
+ const decoder = new TextDecoder();
10
+
11
+ let pending = '';
12
+
13
+ for await (const chunk of stream) {
14
+ const lines = `${pending}${decoder.decode(chunk, { stream: true })}`.split('\n');
15
+
16
+ pending = (lines.pop() ?? '').slice(0, streamLineCapChars);
17
+
18
+ if (lines.length > 0) {
19
+ yield lines.map((line) => line.slice(0, streamLineCapChars));
20
+ }
21
+ }
22
+
23
+ pending = `${pending}${decoder.decode()}`.slice(0, streamLineCapChars);
24
+
25
+ if (pending !== '') {
26
+ yield [pending];
27
+ }
28
+ }
@@ -0,0 +1,11 @@
1
+ import { readStreamLineBatches } from './read-stream-line-batches.ts';
2
+
3
+ export { streamLineCapChars } from './read-stream-line-batches.ts';
4
+
5
+ export async function* readStreamLines(
6
+ stream: ReadableStream<Uint8Array>,
7
+ ): AsyncGenerator<string, void, undefined> {
8
+ for await (const batch of readStreamLineBatches(stream)) {
9
+ yield* batch;
10
+ }
11
+ }
@@ -0,0 +1,5 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ export function readText(path: string): Promise<string> {
4
+ return readFile(path, 'utf8');
5
+ }
@@ -0,0 +1,57 @@
1
+ import { join } from 'node:path';
2
+ import type { StatePort } from './create-file-state.ts';
3
+ import type { Envelope } from './envelope.ts';
4
+ import { verdictSchema } from './report-schema.ts';
5
+ import type { Verdict } from './report-schema.ts';
6
+ import type { RepoHost } from './types.ts';
7
+
8
+ export const doneCommand = 'done';
9
+
10
+ // Merges the verdicts of every run in the worktree: the latest `done` per feature wins.
11
+ export async function readVerdicts(
12
+ state: StatePort,
13
+ host: Pick<RepoHost, 'readText'>,
14
+ root: string,
15
+ ): Promise<ReadonlyMap<string, Verdict>> {
16
+ const runs = await state.listRuns();
17
+ const events = await Promise.all(runs.map((runID) => state.readRunEvents(runID)));
18
+
19
+ const envelopes = events
20
+ .flat()
21
+ .flatMap((event) =>
22
+ event.type === 'envelope' && event.envelope.command === doneCommand && event.envelope.ok
23
+ ? [event.envelope]
24
+ : [],
25
+ );
26
+
27
+ const verdicts = await Promise.all(
28
+ envelopes.map((envelope) => readVerdict(host, envelope, root)),
29
+ );
30
+
31
+ const latest = new Map<string, Verdict>();
32
+
33
+ for (const verdict of verdicts
34
+ .flat()
35
+ .toSorted((left, right) => left.at.localeCompare(right.at))) {
36
+ latest.set(verdict.feature, verdict);
37
+ }
38
+
39
+ return latest;
40
+ }
41
+
42
+ // A verdict over the data cap sits in the payload file the envelope names, not in the envelope.
43
+ async function readVerdict(
44
+ host: Pick<RepoHost, 'readText'>,
45
+ envelope: Envelope,
46
+ root: string,
47
+ ): Promise<readonly Verdict[]> {
48
+ const payload =
49
+ envelope.truncatedPath === undefined
50
+ ? null
51
+ : await host.readText(join(root, envelope.truncatedPath));
52
+
53
+ const data: unknown = payload === null ? envelope.data : JSON.parse(payload);
54
+ const parsed = verdictSchema.safeParse(data);
55
+
56
+ return parsed.success ? [parsed.data] : [];
57
+ }
@@ -0,0 +1,6 @@
1
+ import { rm } from 'node:fs/promises';
2
+
3
+ // Removes the folder and everything in it; a folder that is already gone is no error.
4
+ export async function removeDirectory(path: string): Promise<void> {
5
+ await rm(path, { recursive: true, force: true });
6
+ }
@@ -0,0 +1,6 @@
1
+ import { rm } from 'node:fs/promises';
2
+
3
+ // A file that is already gone is no error.
4
+ export async function removeFile(path: string): Promise<void> {
5
+ await rm(path, { force: true });
6
+ }
@@ -0,0 +1,53 @@
1
+ import type { Report } from './report-schema.ts';
2
+
3
+ export const reportBlockOpen = '<!-- wringer-report v1';
4
+ export const reportBlockClose = '-->';
5
+
6
+ const statusLabels = {
7
+ verified: 'verified',
8
+ stale: 'stale: verified at an older fingerprint',
9
+ missing: 'missing: no verdict',
10
+ unchanged: 'unchanged',
11
+ } as const;
12
+
13
+ // The readable summary comes first, then the JSON report in a hidden block that `gate` reads back.
14
+ // JSON never holds `-->`, because every `>` in it is escaped.
15
+ export function renderReportMarkdown(report: Report): string {
16
+ const lines = [
17
+ '## wringer report',
18
+ '',
19
+ `Fingerprint \`${report.fingerprint}\` against \`${report.base}\`: ${report.complete ? 'every changed feature is verified' : 'some changed features need a verdict'}.`,
20
+ '',
21
+ '| Feature | Status | Evidence |',
22
+ '| --- | --- | --- |',
23
+ ...report.features.map(
24
+ (feature) =>
25
+ `| ${feature.name} | ${statusLabels[feature.status]} | ${formatEvidence(feature.verdict?.evidence ?? [])} |`,
26
+ ),
27
+ '',
28
+ reportBlockOpen,
29
+ JSON.stringify(report).replaceAll('>', String.raw`\u003e`),
30
+ reportBlockClose,
31
+ '',
32
+ ];
33
+
34
+ return lines.join('\n');
35
+ }
36
+
37
+ function formatEvidence(
38
+ evidence: readonly {
39
+ readonly kind: string;
40
+ readonly path: string;
41
+ readonly url?: string | undefined;
42
+ }[],
43
+ ): string {
44
+ if (evidence.length === 0) {
45
+ return '-';
46
+ }
47
+
48
+ return evidence
49
+ .map((item) =>
50
+ item.url === undefined ? `${item.kind} \`${item.path}\`` : `[${item.kind}](${item.url})`,
51
+ )
52
+ .join(', ');
53
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ import { evidenceSchema } from './envelope.ts';
3
+
4
+ const linkedEvidenceSchema = evidenceSchema.extend({ url: z.string().optional() });
5
+
6
+ export const verdictSchema = z.strictObject({
7
+ feature: z.string(),
8
+ fingerprint: z.string(),
9
+ run: z.string(),
10
+ at: z.iso.datetime(),
11
+ evidence: z.array(linkedEvidenceSchema),
12
+ });
13
+
14
+ export type Verdict = z.infer<typeof verdictSchema>;
15
+
16
+ const featureStatusSchema = z.enum(['verified', 'stale', 'missing', 'unchanged']);
17
+
18
+ export type FeatureStatus = z.infer<typeof featureStatusSchema>;
19
+
20
+ const featureReportSchema = z.strictObject({
21
+ name: z.string(),
22
+ changed: z.boolean(),
23
+ status: featureStatusSchema,
24
+ verdict: verdictSchema.nullable(),
25
+ });
26
+
27
+ // The report is the one contract that leaves the worktree: the PR body carries it, and `gate` reads
28
+ // it back in CI. Its `version` changes only with a change a reader of an older report must handle.
29
+ export const reportSchema = z.strictObject({
30
+ version: z.literal(1),
31
+ base: z.string(),
32
+ fingerprint: z.string(),
33
+ complete: z.boolean(),
34
+ features: z.array(featureReportSchema),
35
+ });
36
+
37
+ export type Report = z.infer<typeof reportSchema>;
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod';
2
+ import { envelopeSchema } from './envelope.ts';
3
+
4
+ export const runIDSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{2,40}$/u);
5
+
6
+ export type RunID = z.infer<typeof runIDSchema>;
7
+
8
+ export const modeSchema = z.enum(['launch', 'attach']);
9
+
10
+ export type Mode = z.infer<typeof modeSchema>;
11
+
12
+ export const endpointsSchema = z.record(z.string(), z.string());
13
+
14
+ export type Endpoints = z.infer<typeof endpointsSchema>;
15
+
16
+ export const checkResultSchema = z.strictObject({
17
+ name: z.string(),
18
+ status: z.enum(['passed', 'failed']),
19
+ durationMs: z.number().int().nonnegative(),
20
+ code: z.string().nullable(),
21
+ message: z.string().nullable(),
22
+ });
23
+
24
+ export type CheckResult = z.infer<typeof checkResultSchema>;
25
+
26
+ const at = z.iso.datetime();
27
+
28
+ // A command's work that failed after the envelope had already answered with a timeout.
29
+ export const lateFailureEventSchema = z.strictObject({
30
+ type: z.literal('late-failure'),
31
+ at,
32
+ command: z.string(),
33
+ code: z.string(),
34
+ message: z.string(),
35
+ });
36
+
37
+ export type LateFailureEvent = z.infer<typeof lateFailureEventSchema>;
38
+
39
+ export const runEventSchema = z.discriminatedUnion('type', [
40
+ z.strictObject({
41
+ type: z.literal('run-started'),
42
+ at,
43
+ target: z.string(),
44
+ mode: modeSchema,
45
+ fingerprint: z.string(),
46
+ }),
47
+ z.strictObject({
48
+ type: z.literal('supervisor-ready'),
49
+ at,
50
+ pid: z.number().int(),
51
+ socket: z.string(),
52
+ }),
53
+ z.strictObject({ type: z.literal('launch-ready'), at, endpoints: endpointsSchema }),
54
+ z.strictObject({ type: z.literal('service-ready'), at, service: z.string() }),
55
+ z.strictObject({ type: z.literal('run-ready'), at }),
56
+ z.strictObject({
57
+ type: z.enum(['doctor-passed', 'doctor-failed']),
58
+ at,
59
+ results: z.array(checkResultSchema),
60
+ }),
61
+ z.strictObject({ type: z.literal('envelope'), at, drive: z.boolean(), envelope: envelopeSchema }),
62
+ z.strictObject({ type: z.literal('internal-error'), at, command: z.string(), stack: z.string() }),
63
+
64
+ lateFailureEventSchema,
65
+ z.strictObject({ type: z.literal('crashed'), at, reason: z.string() }),
66
+ z.strictObject({ type: z.literal('run-ended'), at }),
67
+ ]);
68
+
69
+ export type RunEvent = z.infer<typeof runEventSchema>;
70
+
71
+ export type RunEventType = RunEvent['type'];
package/src/run-git.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { runProcess, runProcessBytes } from './run-process.ts';
2
+ import type { ProcessResult } from './run-process.ts';
3
+ import { WringerError } from './wringer-error.ts';
4
+
5
+ export interface GitRequest {
6
+ readonly cwd: string;
7
+ readonly args: readonly string[];
8
+ readonly env?: Readonly<Record<string, string>> | undefined;
9
+ readonly stdin?: string | Uint8Array | undefined;
10
+ }
11
+
12
+ export async function runGit(request: GitRequest): Promise<ProcessResult> {
13
+ const result = await runProcess({
14
+ argv: ['git', ...request.args],
15
+ cwd: request.cwd,
16
+ env: request.env,
17
+ stdin: request.stdin,
18
+ });
19
+
20
+ if (result.exitCode !== 0) {
21
+ throw buildGitFailure(request, result.exitCode, result.stderr);
22
+ }
23
+
24
+ return result;
25
+ }
26
+
27
+ // Returns stdout as the bytes git wrote. A path or a line that is not UTF-8 would lose its bytes
28
+ // in a decoded string.
29
+ export async function runGitBytes(request: GitRequest): Promise<Uint8Array> {
30
+ const result = await runProcessBytes({
31
+ argv: ['git', ...request.args],
32
+ cwd: request.cwd,
33
+ env: request.env,
34
+ stdin: request.stdin,
35
+ });
36
+
37
+ if (result.exitCode !== 0) {
38
+ throw buildGitFailure(request, result.exitCode, result.stderr);
39
+ }
40
+
41
+ return result.stdout;
42
+ }
43
+
44
+ function buildGitFailure(request: GitRequest, exitCode: number, stderr: string): WringerError {
45
+ return new WringerError('environment', {
46
+ message: `git ${request.args[0] ?? ''} failed: ${stderr.trim()}`,
47
+ data: { exitCode, stderr },
48
+ });
49
+ }
@@ -0,0 +1,168 @@
1
+ import { buildSpawnError } from './build-spawn-error.ts';
2
+ import { killProcessGroup } from './kill-process-group.ts';
3
+ import { WringerError } from './wringer-error.ts';
4
+
5
+ export interface ProcessRequest {
6
+ readonly argv: readonly string[];
7
+ readonly cwd?: string | undefined;
8
+ readonly env?: Readonly<Record<string, string | undefined>> | undefined;
9
+ readonly stdin?: string | Uint8Array | undefined;
10
+ readonly signal?: AbortSignal | undefined;
11
+ }
12
+
13
+ export interface ProcessResult {
14
+ readonly exitCode: number;
15
+ readonly stdout: string;
16
+ readonly stderr: string;
17
+ }
18
+
19
+ export async function runProcess(request: ProcessRequest): Promise<ProcessResult> {
20
+ const result = await runProcessBytes(request);
21
+
22
+ return { ...result, stdout: new TextDecoder().decode(result.stdout) };
23
+ }
24
+
25
+ export interface ProcessBytesResult {
26
+ readonly exitCode: number;
27
+ readonly stdout: Uint8Array;
28
+ readonly stderr: string;
29
+ }
30
+
31
+ // Keeps stdout as the bytes the command wrote, for output that must reach another command unchanged.
32
+ export async function runProcessBytes(request: ProcessRequest): Promise<ProcessBytesResult> {
33
+ const env = { ...process.env, ...request.env };
34
+ const cwd = request.cwd ?? process.cwd();
35
+ const [command] = request.argv;
36
+ const executable = resolveExecutable(command, { PATH: env['PATH'] ?? '', cwd });
37
+
38
+ const plan = pickProcessPlan({
39
+ argv: request.argv,
40
+ executable,
41
+ aborted: hasAborted(request.signal),
42
+ });
43
+
44
+ if (plan instanceof WringerError) {
45
+ throw plan;
46
+ }
47
+
48
+ const child = startProcess(plan, { cwd, env, stdin: request.stdin });
49
+
50
+ const killGroup = (): void => {
51
+ killProcessGroup(child.pid);
52
+ };
53
+
54
+ request.signal?.addEventListener('abort', killGroup, { once: true });
55
+
56
+ const [stdout, stderr, exitCode] = await Promise.all([
57
+ new Response(child.stdout).bytes(),
58
+ new Response(child.stderr).text(),
59
+ child.exited,
60
+ ]);
61
+
62
+ request.signal?.removeEventListener('abort', killGroup);
63
+
64
+ const outcome = pickProcessOutcome({
65
+ command: plan.command,
66
+ aborted: hasAborted(request.signal),
67
+ result: { exitCode, stdout, stderr },
68
+ });
69
+
70
+ if (outcome instanceof WringerError) {
71
+ throw outcome;
72
+ }
73
+
74
+ return outcome;
75
+ }
76
+
77
+ interface ExecutableSearch {
78
+ readonly PATH: string;
79
+ readonly cwd: string;
80
+ }
81
+
82
+ // Bun.which resolves a relative command against the folder it is given, so it must be the folder
83
+ // the command runs in.
84
+ function resolveExecutable(command: string | undefined, search: ExecutableSearch): string | null {
85
+ if (command === undefined) {
86
+ return null;
87
+ }
88
+
89
+ return Bun.which(command, search);
90
+ }
91
+
92
+ interface ProcessPlanInput {
93
+ readonly argv: readonly string[];
94
+ readonly executable: string | null;
95
+ readonly aborted: boolean;
96
+ }
97
+
98
+ interface ProcessPlan {
99
+ readonly command: string;
100
+ readonly argv: readonly string[];
101
+ }
102
+
103
+ function pickProcessPlan(input: ProcessPlanInput): ProcessPlan | WringerError {
104
+ const [command, ...args] = input.argv;
105
+
106
+ if (command === undefined) {
107
+ return new WringerError('usage', { message: 'An exec needs a command.' });
108
+ }
109
+
110
+ if (input.executable === null) {
111
+ return new WringerError('environment', {
112
+ message: `The command ${command} is not on the PATH.`,
113
+ hint: `Install ${command}, or put it on the PATH.`,
114
+ });
115
+ }
116
+
117
+ // Bun.spawn throws a plain AbortError on a signal that has already fired.
118
+ if (input.aborted) {
119
+ return new WringerError('timeout', {
120
+ message: `${command} did not start: its deadline had passed.`,
121
+ });
122
+ }
123
+
124
+ return { command, argv: [input.executable, ...args] };
125
+ }
126
+
127
+ function hasAborted(signal: AbortSignal | undefined): boolean {
128
+ return signal?.aborted === true;
129
+ }
130
+
131
+ interface StartOptions {
132
+ readonly cwd: string;
133
+ readonly env: Readonly<Record<string, string | undefined>>;
134
+ readonly stdin?: string | Uint8Array | undefined;
135
+ }
136
+
137
+ // The command runs in a process group of its own, so the deadline also stops a child it started,
138
+ // which would otherwise hold the output pipes open past the deadline.
139
+ function startProcess(plan: ProcessPlan, options: StartOptions) {
140
+ try {
141
+ return Bun.spawn([...plan.argv], {
142
+ cwd: options.cwd,
143
+ env: { ...options.env },
144
+ stdin: options.stdin === undefined ? 'ignore' : Buffer.from(options.stdin),
145
+ stdout: 'pipe',
146
+ stderr: 'pipe',
147
+ detached: true,
148
+ });
149
+ } catch (error) {
150
+ throw buildSpawnError(plan.command, error);
151
+ }
152
+ }
153
+
154
+ interface ProcessOutcomeInput {
155
+ readonly command: string;
156
+ readonly aborted: boolean;
157
+ readonly result: ProcessBytesResult;
158
+ }
159
+
160
+ function pickProcessOutcome(input: ProcessOutcomeInput): ProcessBytesResult | WringerError {
161
+ if (input.aborted) {
162
+ return new WringerError('timeout', {
163
+ message: `${input.command} ran past its deadline and was stopped.`,
164
+ });
165
+ }
166
+
167
+ return input.result;
168
+ }
@@ -0,0 +1,51 @@
1
+ import { launchReadyTimeoutMs } from './builtins/build-lifecycle-commands.ts';
2
+ import { createRepoState } from './create-repo-state.ts';
3
+ import { readLaunchHost } from './read-launch-host.ts';
4
+ import { runSupervisor } from './run-supervisor.ts';
5
+ import { appStopLimitMs } from './start-launch.ts';
6
+ import { supervisorArgsSchema } from './supervisor-protocol.ts';
7
+ import type { Wringer } from './types.ts';
8
+ import { WringerError } from './wringer-error.ts';
9
+
10
+ // One cleanup's wait covers the app's longest stop, with a second to spare.
11
+ const stopWaitMs = appStopLimitMs + 1000;
12
+
13
+ // The entry of the supervisor process: argv is what follows the hidden command name. The process
14
+ // exits once its run has stopped.
15
+ export async function runSupervisorProcess(
16
+ wringer: Wringer,
17
+ argv: readonly string[],
18
+ ): Promise<void> {
19
+ const parsed = supervisorArgsSchema.safeParse(argv);
20
+
21
+ if (!parsed.success) {
22
+ throw new WringerError('usage', { message: 'The supervisor needs a run ID, target and root.' });
23
+ }
24
+
25
+ const signalled = new AbortController();
26
+
27
+ process.once('SIGTERM', () => {
28
+ signalled.abort();
29
+ });
30
+
31
+ process.once('SIGINT', () => {
32
+ signalled.abort();
33
+ });
34
+
35
+ await runSupervisor({
36
+ wringer,
37
+ launchHost: readLaunchHost(),
38
+ state: createRepoState(parsed.data.root, wringer.stateDir),
39
+ runID: parsed.data.runID,
40
+ target: parsed.data.target,
41
+ readyTimeoutMs: launchReadyTimeoutMs,
42
+ stopWaitMs,
43
+ now: () => new Date(),
44
+
45
+ // The process that starts the supervisor sends its stderr to the supervisor's log.
46
+ reportError: (message) => {
47
+ process.stderr.write(`${message}\n`);
48
+ },
49
+ signal: signalled.signal,
50
+ });
51
+ }