@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
package/src/types.ts ADDED
@@ -0,0 +1,476 @@
1
+ import type { z } from 'zod';
2
+ import type { RunView } from './build-run-view.ts';
3
+ import type { RunPaths } from './build-state-paths.ts';
4
+ import type { SupervisorCall } from './call-supervisor.ts';
5
+ import type { StatePort } from './create-file-state.ts';
6
+ import type { Evidence } from './envelope.ts';
7
+ import type { Access, Host } from './host.ts';
8
+ import type { LaunchLine } from './launch-line-schema.ts';
9
+ import type { FingerprintHost } from './read-fingerprint.ts';
10
+ import type { Endpoints, RunID } from './run-events.ts';
11
+ import type { ProcessResult } from './run-process.ts';
12
+
13
+ export type Effect = 'read' | 'write';
14
+
15
+ export type Emit = (event: string, data: unknown) => void;
16
+
17
+ export interface EvidenceWriter {
18
+ readonly directory: string;
19
+ readonly writeFile: (
20
+ kind: string,
21
+ name: string,
22
+ content: string | Uint8Array,
23
+ ) => Promise<Evidence>;
24
+ readonly addFile: (kind: string, path: string) => Promise<Evidence>;
25
+ }
26
+
27
+ export interface ExecOptions {
28
+ readonly stdin?: string | undefined;
29
+ }
30
+
31
+ export type RepoExec = (argv: readonly string[], options?: ExecOptions) => Promise<ProcessResult>;
32
+
33
+ export type DriverExec = (
34
+ argv: readonly string[],
35
+ effect: Effect,
36
+ options?: ExecOptions,
37
+ ) => Promise<ProcessResult>;
38
+
39
+ export interface ChildLog {
40
+ readonly path: string;
41
+ }
42
+
43
+ export interface RunHandle {
44
+ readonly id: string;
45
+ readonly paths: RunPaths;
46
+ readonly childLog: (name: string) => ChildLog;
47
+ }
48
+
49
+ export interface DriverConfigContext {
50
+ readonly target: string;
51
+ readonly endpoints: Endpoints;
52
+ readonly run: RunHandle;
53
+ }
54
+
55
+ export interface ServiceCaller {
56
+ readonly call: (method: string, args: unknown) => Promise<unknown>;
57
+ }
58
+
59
+ // What a driver's client, commands and checks get on one target: the repo-wide options, the
60
+ // target's parsed config, the stream cursors `use` last marked, and the effect-checked exec and
61
+ // services core hands it.
62
+ export interface DriverSession<Options, TargetConfig> {
63
+ readonly target: string;
64
+ readonly access: Access;
65
+ readonly options: Options;
66
+ readonly config: TargetConfig;
67
+ readonly endpoints: Endpoints;
68
+ readonly run: RunHandle;
69
+ readonly exec: DriverExec;
70
+ readonly evidence: EvidenceWriter;
71
+ readonly cursors: Readonly<Record<string, number>>;
72
+ readonly signal: AbortSignal;
73
+
74
+ // The ms left before `signal` fires, for work that runs where the signal cannot reach, such as a
75
+ // database server that a client program talks to.
76
+ readonly timeLeftMs: () => number;
77
+ readonly service: (name: string) => ServiceCaller;
78
+ }
79
+
80
+ export interface ClientCall<Args, Result> {
81
+ readonly effect: (args: Args) => Effect;
82
+ readonly describe: (args: Args) => string;
83
+ readonly run: (args: Args) => Promise<Result>;
84
+ }
85
+
86
+ // Core hands each driver this function to wrap its client calls. A wrapped call checks its effect
87
+ // against the target's access before it runs.
88
+ export type Fence = <Args, Result>(
89
+ call: ClientCall<Args, Result>,
90
+ ) => (args: Args) => Promise<Result>;
91
+
92
+ export interface ServiceContext<Options, TargetConfig> {
93
+ readonly target: string;
94
+ readonly access: Access;
95
+ readonly options: Options;
96
+ readonly config: TargetConfig;
97
+ readonly endpoints: Endpoints;
98
+ readonly run: RunHandle;
99
+ readonly host: Host;
100
+ readonly exec: DriverExec;
101
+ readonly signal: AbortSignal;
102
+ }
103
+
104
+ // A method's signal aborts one second before the caller's deadline, or when the caller stops
105
+ // waiting, so the method can stop its work before the caller reports a timeout.
106
+ export type ServiceMethod = (args: unknown, signal: AbortSignal) => Promise<unknown>;
107
+
108
+ export interface ServiceInstance {
109
+ readonly methods: Readonly<Record<string, ServiceMethod>>;
110
+ readonly stop: () => Promise<void>;
111
+ }
112
+
113
+ export interface ServiceDefinition<Options, TargetConfig> {
114
+ readonly start: (ctx: ServiceContext<Options, TargetConfig>) => Promise<ServiceInstance>;
115
+ }
116
+
117
+ export interface CommandShape<Args, Data> {
118
+ readonly path: readonly string[];
119
+ readonly summary: string;
120
+ readonly args: z.ZodType<Args>;
121
+ readonly positionals?: readonly string[] | undefined;
122
+ readonly data: z.ZodType<Data>;
123
+ readonly effect?: Effect | undefined;
124
+ readonly needsRun?: boolean | undefined;
125
+ readonly needsDoctor?: boolean | undefined;
126
+ readonly timeoutMs?: number | undefined;
127
+ }
128
+
129
+ // A command with its argument and data types erased, as the registry and the CLI adapter see it. A
130
+ // command with `decideEffect` reads or writes by its arguments, so its `effect` is only a default.
131
+ export type CommandInfo = CommandShape<unknown, unknown> & { readonly decideEffect?: unknown };
132
+
133
+ export interface DriverCommandContext<Options, TargetConfig, Client> {
134
+ readonly session: DriverSession<Options, TargetConfig>;
135
+ readonly client: Client;
136
+ readonly feature: string | null;
137
+ readonly emit: Emit;
138
+ }
139
+
140
+ export interface DriverCommandSpec<Args, Data, Options, TargetConfig, Client> extends CommandShape<
141
+ Args,
142
+ Data
143
+ > {
144
+ readonly decideEffect?: ((args: Args, config: TargetConfig) => Effect) | undefined;
145
+ readonly run: (
146
+ args: Args,
147
+ ctx: DriverCommandContext<Options, TargetConfig, Client>,
148
+ ) => Promise<Data>;
149
+ }
150
+
151
+ export type ArgsResult<Prepared> =
152
+ | { readonly ok: true; readonly prepared: Prepared }
153
+ | { readonly ok: false; readonly message: string };
154
+
155
+ export interface PreparedDriverCommand<Options, TargetConfig, Client> {
156
+ readonly decideEffect: (config: TargetConfig) => Effect;
157
+ readonly run: (ctx: DriverCommandContext<Options, TargetConfig, Client>) => Promise<unknown>;
158
+ }
159
+
160
+ // One driver command with its own argument types closed over, so a driver's commands share one
161
+ // array type.
162
+ export interface DriverCommand<Options, TargetConfig, Client> {
163
+ readonly info: CommandInfo;
164
+ readonly prepare: (
165
+ raw: unknown,
166
+ ) => ArgsResult<PreparedDriverCommand<Options, TargetConfig, Client>>;
167
+ }
168
+
169
+ export type DriverCommandHelper<Options, TargetConfig, Client> = <Args, Data>(
170
+ spec: DriverCommandSpec<Args, Data, Options, TargetConfig, Client>,
171
+ ) => DriverCommand<Options, TargetConfig, Client>;
172
+
173
+ export interface DriverCheckDefinition<Options, TargetConfig, Client> {
174
+ readonly name: string;
175
+ readonly timeoutMs: number;
176
+ readonly run: (ctx: DriverCommandContext<Options, TargetConfig, Client>) => Promise<void>;
177
+ }
178
+
179
+ export interface SessionParts {
180
+ readonly target: string;
181
+ readonly access: Access;
182
+ readonly endpoints: Endpoints;
183
+ readonly run: RunHandle;
184
+ readonly exec: DriverExec;
185
+ readonly evidence: EvidenceWriter;
186
+ readonly cursors: Readonly<Record<string, number>>;
187
+ readonly signal: AbortSignal;
188
+ readonly timeLeftMs: () => number;
189
+ readonly fence: Fence;
190
+ readonly service: (name: string) => ServiceCaller;
191
+ }
192
+
193
+ export interface ServiceParts {
194
+ readonly target: string;
195
+ readonly access: Access;
196
+ readonly endpoints: Endpoints;
197
+ readonly run: RunHandle;
198
+ readonly host: Host;
199
+ readonly exec: DriverExec;
200
+ readonly signal: AbortSignal;
201
+ }
202
+
203
+ export interface PreparedRun {
204
+ readonly effect: Effect;
205
+ readonly run: (feature: string | null, emit: Emit) => Promise<unknown>;
206
+ }
207
+
208
+ export interface RunnableCheck {
209
+ readonly name: string;
210
+ readonly timeoutMs: number;
211
+ readonly run: (feature: string | null, emit: Emit) => Promise<void>;
212
+ }
213
+
214
+ export interface OpenedDriver {
215
+ readonly client: unknown;
216
+ readonly checks: readonly RunnableCheck[];
217
+ readonly prepare: (pathKey: string, raw: unknown) => ArgsResult<PreparedRun>;
218
+ }
219
+
220
+ export interface ConfiguredDriver {
221
+ readonly open: (parts: SessionParts) => OpenedDriver;
222
+ readonly startService: (name: string, parts: ServiceParts) => Promise<ServiceInstance>;
223
+ }
224
+
225
+ // Phantom types that only the type checker reads: the client a repo's code gets, and the config a
226
+ // target passes.
227
+ export interface DriverTypes<Client, ConfigInput> {
228
+ readonly client: Client;
229
+ readonly config: ConfigInput;
230
+ }
231
+
232
+ // A driver as a repo lists it, bound to the repo-wide options, with its own types closed over.
233
+ export interface Driver<Name extends string, Client, ConfigInput> {
234
+ readonly kind: 'driver';
235
+ readonly name: Name;
236
+ readonly commands: readonly CommandInfo[];
237
+ readonly serviceNames: readonly string[];
238
+ readonly configure: (input: unknown) => ArgsResult<ConfiguredDriver>;
239
+ readonly types?: DriverTypes<Client, ConfigInput>;
240
+ }
241
+
242
+ export type AnyDriver = Driver<string, unknown, unknown>;
243
+
244
+ export type AnyDrivers = readonly AnyDriver[];
245
+
246
+ export type DriverClients<Ds extends AnyDrivers> = {
247
+ readonly [D in Ds[number] as D['name']]: D extends Driver<string, infer Client, unknown>
248
+ ? Client
249
+ : never;
250
+ };
251
+
252
+ export type DriverConfigs<Ds extends AnyDrivers> = {
253
+ readonly [D in Ds[number] as D['name']]?: (
254
+ ctx: DriverConfigContext,
255
+ ) => D extends Driver<string, unknown, infer ConfigInput> ? ConfigInput : never;
256
+ };
257
+
258
+ export interface DriveContext<Ds extends AnyDrivers = AnyDrivers> {
259
+ readonly target: string;
260
+ readonly access: Access;
261
+ readonly feature: string | null;
262
+ readonly endpoints: Endpoints;
263
+ readonly drivers: DriverClients<Ds>;
264
+ readonly exec: RepoExec;
265
+ readonly evidence: EvidenceWriter;
266
+ readonly signal: AbortSignal;
267
+ readonly emit: Emit;
268
+ }
269
+
270
+ export interface CommandSpec<Args, Data, Ds extends AnyDrivers = AnyDrivers> extends CommandShape<
271
+ Args,
272
+ Data
273
+ > {
274
+ readonly decideEffect?: ((args: Args) => Effect) | undefined;
275
+ readonly run: (args: Args, ctx: DriveContext<Ds>) => Promise<Data>;
276
+ }
277
+
278
+ export interface ActionDefinition<Input, Data, Ds extends AnyDrivers = AnyDrivers> {
279
+ readonly name: string;
280
+ readonly summary: string;
281
+ readonly effect: Effect;
282
+ readonly targets?: readonly string[] | undefined;
283
+ readonly input: z.ZodType<Input>;
284
+ readonly data: z.ZodType<Data>;
285
+ readonly timeoutMs?: number | undefined;
286
+ readonly run: (input: Input, ctx: DriveContext<Ds>) => Promise<Data>;
287
+ }
288
+
289
+ export interface CheckDefinition<Input, Data, Ds extends AnyDrivers = AnyDrivers> {
290
+ readonly name: string;
291
+ readonly input: z.ZodType<Input>;
292
+ readonly data: z.ZodType<Data>;
293
+ readonly timeoutMs: number;
294
+ readonly run: (input: Input, ctx: DriveContext<Ds>) => Promise<Data>;
295
+ }
296
+
297
+ export interface FeatureDefinition {
298
+ readonly name: string;
299
+ readonly summary: string;
300
+ readonly paths: readonly string[];
301
+ }
302
+
303
+ export interface UploadRequest {
304
+ readonly path: string;
305
+ readonly kind: string;
306
+ readonly sha256: string;
307
+ }
308
+
309
+ export interface HookHandlers {
310
+ readonly upload: (file: UploadRequest) => Promise<string>;
311
+ }
312
+
313
+ export interface LaunchContext {
314
+ readonly runDirectory: string;
315
+ }
316
+
317
+ export interface LaunchPlan {
318
+ readonly command: readonly string[];
319
+ readonly env?: Readonly<Record<string, string>> | undefined;
320
+ readonly cwd?: string | undefined;
321
+ readonly ready: (line: LaunchLine) => Endpoints | null;
322
+ readonly stop?: readonly string[] | undefined;
323
+ }
324
+
325
+ interface TargetBase<Ds extends AnyDrivers> {
326
+ readonly access: Access;
327
+ readonly refusalHint?: string | undefined;
328
+ readonly host: Host;
329
+ readonly drivers?: DriverConfigs<Ds> | undefined;
330
+ }
331
+
332
+ export interface EndpointsContext {
333
+ readonly target: string;
334
+ }
335
+
336
+ export interface LaunchTarget<Ds extends AnyDrivers> extends TargetBase<Ds> {
337
+ readonly mode: 'launch';
338
+ readonly launch: (ctx: LaunchContext) => LaunchPlan;
339
+ readonly endpoints?: never;
340
+ }
341
+
342
+ export interface AttachTarget<Ds extends AnyDrivers> extends TargetBase<Ds> {
343
+ readonly mode: 'attach';
344
+ readonly endpoints?: ((ctx: EndpointsContext) => Endpoints | Promise<Endpoints>) | undefined;
345
+ readonly launch?: never;
346
+ }
347
+
348
+ export type TargetDefinition<Ds extends AnyDrivers = AnyDrivers> =
349
+ | LaunchTarget<Ds>
350
+ | AttachTarget<Ds>;
351
+
352
+ export interface Drive {
353
+ readonly ctx: DriveContext;
354
+ readonly drivers: ReadonlyMap<string, OpenedDriver>;
355
+ readonly evidence: readonly Evidence[];
356
+ }
357
+
358
+ // A deadline shorter than the command's, such as a doctor check's: the signal fires at `at`.
359
+ export interface Deadline {
360
+ readonly signal: AbortSignal;
361
+ readonly at: number;
362
+ }
363
+
364
+ export interface BuiltinContext {
365
+ readonly wringer: Wringer;
366
+ readonly state: StatePort;
367
+ readonly session: Session;
368
+ readonly stateDir: string;
369
+ readonly targetName: string;
370
+ readonly target: TargetDefinition;
371
+ readonly run: RunView | undefined;
372
+ readonly feature: string | null;
373
+ readonly signal: AbortSignal;
374
+
375
+ // The command's deadline, in ms since the epoch on the `now` clock.
376
+ readonly deadlineAt: number;
377
+ readonly now: () => Date;
378
+
379
+ // A poll waits between reads through this, the timer that goes with `now`.
380
+ readonly wait: (delayMs: number) => Promise<void>;
381
+ readonly emit: Emit;
382
+ readonly openDrive: (access: Access, deadline?: Deadline) => Drive;
383
+
384
+ // A lifecycle command names the run it started, reused or stopped while it holds the target's
385
+ // lock, and its envelope belongs to that run, whatever the pointers say once the lock is free.
386
+ readonly bindRun: (runID: RunID) => void;
387
+
388
+ // Work the command gave up on before it answered, such as a check past its timeout. The caller
389
+ // awaits it after the answer, so a failure it logs still reaches the run's log.
390
+ readonly keepLate: (work: Promise<void>) => void;
391
+ }
392
+
393
+ export interface BuiltinCommandSpec<Args, Data> extends CommandShape<Args, Data> {
394
+ readonly run: (args: Args, ctx: BuiltinContext) => Promise<Data>;
395
+ }
396
+
397
+ export interface PreparedCommand<Ctx> {
398
+ readonly effect: Effect;
399
+ readonly run: (ctx: Ctx) => Promise<unknown>;
400
+ }
401
+
402
+ export type RegisteredCommand =
403
+ | {
404
+ readonly kind: 'repo';
405
+ readonly info: CommandInfo;
406
+ readonly targets: readonly string[] | undefined;
407
+ readonly prepare: (raw: unknown) => ArgsResult<PreparedCommand<DriveContext>>;
408
+ }
409
+ | {
410
+ readonly kind: 'builtin';
411
+ readonly info: CommandInfo;
412
+ readonly prepare: (raw: unknown) => ArgsResult<PreparedCommand<BuiltinContext>>;
413
+ }
414
+ | { readonly kind: 'driver'; readonly info: CommandInfo; readonly driver: string };
415
+
416
+ export interface RunnableRepoCheck {
417
+ readonly name: string;
418
+ readonly timeoutMs: number;
419
+ readonly run: (ctx: DriveContext) => Promise<void>;
420
+ }
421
+
422
+ export interface Wringer {
423
+ readonly name: string;
424
+ readonly stateDir: string;
425
+ readonly drivers: AnyDrivers;
426
+ readonly targets: ReadonlyMap<string, TargetDefinition>;
427
+ readonly commands: ReadonlyMap<string, RegisteredCommand>;
428
+ readonly checks: readonly RunnableRepoCheck[];
429
+ readonly features: readonly FeatureDefinition[];
430
+ readonly hooks: Partial<HookHandlers>;
431
+ }
432
+
433
+ export interface SupervisorStartRequest {
434
+ readonly runID: string;
435
+ readonly target: string;
436
+ }
437
+
438
+ // How a command reaches a run's supervisor: it starts one, and sends one request over the run's
439
+ // socket and reads the answer.
440
+ export interface SupervisorPort {
441
+ readonly start: (request: SupervisorStartRequest) => Promise<void>;
442
+ readonly call: (call: SupervisorCall) => Promise<unknown>;
443
+ }
444
+
445
+ export interface OutputPort {
446
+ readonly writeLine: (line: string) => void;
447
+ }
448
+
449
+ // The disk operations that keep a command's evidence in the run's folder.
450
+ export interface EvidenceFiles {
451
+ readonly createDirectory: (path: string) => Promise<void>;
452
+
453
+ // False, with nothing written, when the path exists.
454
+ readonly writeNewFile: (path: string, bytes: Uint8Array) => Promise<boolean>;
455
+ readonly readBytes: (path: string) => Promise<Uint8Array>;
456
+ }
457
+
458
+ // Everything a command reads from or writes to the repo: its git, the files a command names, and
459
+ // its evidence.
460
+ export interface RepoHost extends FingerprintHost, EvidenceFiles {
461
+ readonly readText: (path: string) => Promise<string>;
462
+ }
463
+
464
+ // What the CLI adapter knows about one invocation.
465
+ export interface Session {
466
+ readonly root: string;
467
+ readonly state: StatePort;
468
+ readonly repoHost: RepoHost;
469
+ readonly target?: string | undefined;
470
+ readonly feature?: string | undefined;
471
+ readonly timeoutMs?: number | undefined;
472
+ readonly output: OutputPort;
473
+ readonly supervisor: SupervisorPort;
474
+ readonly now?: (() => Date) | undefined;
475
+ readonly wait?: ((delayMs: number) => Promise<void>) | undefined;
476
+ }
@@ -0,0 +1,37 @@
1
+ import { buildUnconfirmedError, supervisorCancelWaitMs } from './call-supervisor.ts';
2
+ import { findCancelFailure } from './find-cancel-failure.ts';
3
+ import { raceDeadline } from './race-deadline.ts';
4
+ import { toWringerError } from './to-wringer-error.ts';
5
+ import type { WringerError } from './wringer-error.ts';
6
+
7
+ // A call gives up on its supervisor's answer after its own wait, so this bound only ends a port
8
+ // that never settles.
9
+ const cancelBoundMs = supervisorCancelWaitMs + 1000;
10
+
11
+ // A service call the deadline cancelled, such as a browser action, answers once its work has
12
+ // stopped. Resolves with the failure of a cancel that could not stop its work, or null.
13
+ export async function waitForCancelledCalls(
14
+ calls: readonly Promise<unknown>[],
15
+ ): Promise<WringerError | null> {
16
+ const failures: WringerError[] = [];
17
+
18
+ const watch = async (call: Promise<unknown>): Promise<void> => {
19
+ try {
20
+ await call;
21
+ } catch (error) {
22
+ failures.push(toWringerError(error));
23
+ }
24
+ };
25
+
26
+ const watched = Promise.all(calls.map((call) => watch(call)));
27
+
28
+ const [settled] = await Promise.allSettled([
29
+ raceDeadline(watched, AbortSignal.timeout(cancelBoundMs)),
30
+ ]);
31
+
32
+ if (settled.status === 'rejected') {
33
+ failures.push(buildUnconfirmedError());
34
+ }
35
+
36
+ return findCancelFailure(failures);
37
+ }
@@ -0,0 +1,25 @@
1
+ import { defaultMessageFor } from './error-codes.ts';
2
+ import type { ErrorCode } from './error-codes.ts';
3
+
4
+ export interface WringerErrorDetails {
5
+ readonly message?: string;
6
+ readonly hint?: string;
7
+ readonly data?: unknown;
8
+ }
9
+
10
+ export class WringerError extends Error {
11
+ readonly code: ErrorCode;
12
+
13
+ readonly hint: string | undefined;
14
+
15
+ readonly data: unknown;
16
+
17
+ constructor(code: ErrorCode, details: WringerErrorDetails = {}) {
18
+ super(details.message ?? defaultMessageFor[code]);
19
+
20
+ this.name = 'WringerError';
21
+ this.code = code;
22
+ this.hint = details.hint;
23
+ this.data = details.data;
24
+ }
25
+ }
@@ -0,0 +1,72 @@
1
+ import type { RunView } from './build-run-view.ts';
2
+ import type { RunViewRefusal, StatePort } from './create-file-state.ts';
3
+ import type { Envelope } from './envelope.ts';
4
+ import { toWringerError } from './to-wringer-error.ts';
5
+ import type { WringerError } from './wringer-error.ts';
6
+
7
+ export interface LogRequest {
8
+ readonly drive: boolean;
9
+ readonly failure: WringerError | null;
10
+ readonly now: () => Date;
11
+
12
+ // Checked against the run as its log stands under the run's lock; a refusal keeps the envelope
13
+ // out of the log and becomes the envelope's failure.
14
+ readonly refuse: RunViewRefusal | null;
15
+ }
16
+
17
+ // The command has already run, so a log write that fails, such as an event over the log's cap,
18
+ // still yields an envelope, one that carries the failure.
19
+ export async function writeEnvelopeLog(
20
+ state: StatePort,
21
+ run: RunView,
22
+ envelope: Envelope,
23
+ request: LogRequest,
24
+ ): Promise<WringerError | null> {
25
+ try {
26
+ await logEnvelope(state, run, envelope, request);
27
+
28
+ return null;
29
+ } catch (error) {
30
+ return toWringerError(error);
31
+ }
32
+ }
33
+
34
+ async function logEnvelope(
35
+ state: StatePort,
36
+ run: RunView,
37
+ envelope: Envelope,
38
+ request: LogRequest,
39
+ ): Promise<void> {
40
+ const at = request.now().toISOString();
41
+
42
+ if (request.failure?.code === 'internal') {
43
+ await state.appendEvent(run.runID, {
44
+ type: 'internal-error',
45
+ at,
46
+ command: envelope.command,
47
+ stack: parseStack(request.failure.data),
48
+ });
49
+ }
50
+
51
+ const event = { type: 'envelope', at, drive: request.drive && envelope.ok, envelope } as const;
52
+
53
+ await (request.refuse === null
54
+ ? state.appendEvent(run.runID, event)
55
+ : state.appendEventUnless(run.runID, event, request.refuse));
56
+ }
57
+
58
+ // A stack this long is a runaway, and the log refuses an event past its cap.
59
+ const stackCapChars = 16 * 1024;
60
+
61
+ function parseStack(data: unknown): string {
62
+ if (
63
+ typeof data === 'object' &&
64
+ data !== null &&
65
+ 'stack' in data &&
66
+ typeof data.stack === 'string'
67
+ ) {
68
+ return data.stack.slice(0, stackCapChars);
69
+ }
70
+
71
+ return '';
72
+ }
@@ -0,0 +1,40 @@
1
+ import type { StatePort } from './create-file-state.ts';
2
+ import { pickLateFailureWrite } from './pick-late-failure-write.ts';
3
+ import type { RunID } from './run-events.ts';
4
+ import type { WringerError } from './wringer-error.ts';
5
+
6
+ export interface LateFailureRequest {
7
+ readonly state: StatePort;
8
+
9
+ // The run whose log takes the failure; with none, the state folder's late-failure log takes it.
10
+ readonly runID: RunID | null;
11
+ readonly command: string;
12
+
13
+ // The failure the envelope already answered with, which is no late failure.
14
+ readonly answered: WringerError | null;
15
+ readonly now: () => Date;
16
+ }
17
+
18
+ // The envelope answers at the deadline, but the command's work runs on, and its cleanup, such as
19
+ // an ssh remote stop, can fail after the answer. Only a log can still record that.
20
+ export async function writeLateFailure(
21
+ work: Promise<unknown>,
22
+ request: LateFailureRequest,
23
+ ): Promise<void> {
24
+ const [settled] = await Promise.allSettled([work]);
25
+
26
+ const plan = pickLateFailureWrite(settled, { ...request, at: request.now() });
27
+
28
+ if (plan === null) {
29
+ return;
30
+ }
31
+
32
+ const writing =
33
+ plan.runID === null
34
+ ? request.state.appendLateFailure(plan.event)
35
+ : request.state.appendEvent(plan.runID, plan.event);
36
+
37
+ await writing.catch(() => {
38
+ // Nothing is left to tell once the log refuses the write.
39
+ });
40
+ }
@@ -0,0 +1,17 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { isErrorCode } from './is-error-code.ts';
3
+
4
+ // Creates the file with the bytes, and returns false, writing nothing, when the path exists.
5
+ export async function writeNewFile(path: string, bytes: Uint8Array): Promise<boolean> {
6
+ try {
7
+ await writeFile(path, bytes, { flag: 'wx' });
8
+
9
+ return true;
10
+ } catch (error) {
11
+ if (isErrorCode(error, 'EEXIST')) {
12
+ return false;
13
+ }
14
+
15
+ throw error;
16
+ }
17
+ }