@fabrika/app 0.9.0-dev.2 → 0.9.0-dev.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -9
- package/skill/SKILL.md +1 -1
- package/skill/references/app-setup.md +8 -8
- package/src/dev/launcher.ts +83 -20
- package/src/dev/output.ts +13 -3
- package/src/dev/wire.ts +9 -0
- package/types/src/dev/output.d.ts +8 -2
- package/types/src/dev/wire.d.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/app",
|
|
3
|
-
"version": "0.9.0-dev.
|
|
3
|
+
"version": "0.9.0-dev.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -93,6 +93,11 @@
|
|
|
93
93
|
"types": "./types/src/dev/wire.d.ts",
|
|
94
94
|
"default": "./src/dev/wire.ts"
|
|
95
95
|
},
|
|
96
|
+
"./dev/changes": {
|
|
97
|
+
"bun": "./src/dev/changes.ts",
|
|
98
|
+
"types": "./types/src/dev/changes.d.ts",
|
|
99
|
+
"default": "./src/dev/changes.ts"
|
|
100
|
+
},
|
|
96
101
|
"./agent-skill": {
|
|
97
102
|
"bun": "./src/agent-skill.ts",
|
|
98
103
|
"types": "./types/src/agent-skill.d.ts",
|
|
@@ -104,14 +109,14 @@
|
|
|
104
109
|
"test": "bun test --path-ignore-patterns='src/react/**' && bun test --isolate src/react"
|
|
105
110
|
},
|
|
106
111
|
"dependencies": {
|
|
107
|
-
"@fabrika/auth": "0.9.0-dev.
|
|
108
|
-
"@fabrika/auth-core": "0.9.0-dev.
|
|
109
|
-
"@fabrika/db": "0.9.0-dev.
|
|
110
|
-
"@fabrika/email": "0.9.0-dev.
|
|
111
|
-
"@fabrika/jwks": "0.9.0-dev.
|
|
112
|
-
"@fabrika/platform": "0.9.0-dev.
|
|
113
|
-
"@fabrika/platform-node": "0.9.0-dev.
|
|
114
|
-
"@fabrika/proxy-core": "0.9.0-dev.
|
|
112
|
+
"@fabrika/auth": "0.9.0-dev.4",
|
|
113
|
+
"@fabrika/auth-core": "0.9.0-dev.4",
|
|
114
|
+
"@fabrika/db": "0.9.0-dev.4",
|
|
115
|
+
"@fabrika/email": "0.9.0-dev.4",
|
|
116
|
+
"@fabrika/jwks": "0.9.0-dev.4",
|
|
117
|
+
"@fabrika/platform": "0.9.0-dev.4",
|
|
118
|
+
"@fabrika/platform-node": "0.9.0-dev.4",
|
|
119
|
+
"@fabrika/proxy-core": "0.9.0-dev.4",
|
|
115
120
|
"altcha-lib": "2.6.0",
|
|
116
121
|
"jose": "^5.9.0"
|
|
117
122
|
},
|
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fabrika-app
|
|
3
|
-
version: 0.9.0-dev.
|
|
3
|
+
version: 0.9.0-dev.4
|
|
4
4
|
description: >
|
|
5
5
|
How to write and change a Fabrika application: src/vocabulary.ts, db/migrations/, src/db/tables.ts, the
|
|
6
6
|
slices' endpoints.ts with their handlers, fabrika.contract.ts, jobs, files, webhooks, share links, connectors
|
|
@@ -19,20 +19,20 @@ the SPA is added with it.
|
|
|
19
19
|
"test": "bun test --isolate"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@fabrika/app": "0.9.0-dev.
|
|
23
|
-
"@fabrika/db": "0.9.0-dev.
|
|
24
|
-
"@fabrika/provider-zerops": "0.9.0-dev.
|
|
25
|
-
"@fabrika/proxy-core": "0.9.0-dev.
|
|
26
|
-
"@fabrika/ui": "0.9.0-dev.
|
|
22
|
+
"@fabrika/app": "0.9.0-dev.4",
|
|
23
|
+
"@fabrika/db": "0.9.0-dev.4",
|
|
24
|
+
"@fabrika/provider-zerops": "0.9.0-dev.4",
|
|
25
|
+
"@fabrika/proxy-core": "0.9.0-dev.4",
|
|
26
|
+
"@fabrika/ui": "0.9.0-dev.4",
|
|
27
27
|
"kysely": "0.29.6",
|
|
28
28
|
"react": "^19.0.0",
|
|
29
29
|
"react-dom": "^19.0.0",
|
|
30
30
|
"zod": "^3.25.76"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@fabrika/check": "0.9.0-dev.
|
|
34
|
-
"@fabrika/cli": "0.9.0-dev.
|
|
35
|
-
"@fabrika/platform": "0.9.0-dev.
|
|
33
|
+
"@fabrika/check": "0.9.0-dev.4",
|
|
34
|
+
"@fabrika/cli": "0.9.0-dev.4",
|
|
35
|
+
"@fabrika/platform": "0.9.0-dev.4",
|
|
36
36
|
"@happy-dom/global-registrator": "^20.11.2",
|
|
37
37
|
"@types/bun": "latest",
|
|
38
38
|
"@types/react": "^19.0.0",
|
package/src/dev/launcher.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
18
18
|
import { z } from 'zod/v4'
|
|
19
19
|
import { makeProcessUndumpable } from '../undumpable.js'
|
|
20
20
|
import { bootstrapReason, migrationsChanged, webSourceChanged } from './changes.js'
|
|
21
|
-
import { OutputRing, type OutputSink, processOutput, readLines } from './output.js'
|
|
21
|
+
import { OutputRing, type OutputSink, type OutputStream, processOutput, readLines } from './output.js'
|
|
22
22
|
import {
|
|
23
23
|
DEV_SOURCE_REPORT_PATH,
|
|
24
24
|
DEV_SOURCE_WAIT_MS,
|
|
@@ -123,13 +123,23 @@ interface RunningApplication {
|
|
|
123
123
|
|
|
124
124
|
interface CommandResult {
|
|
125
125
|
readonly exitCode: number | null
|
|
126
|
+
/** The signal that ended the command, when one did: it never exited on its own. */
|
|
127
|
+
readonly signal: string | null
|
|
126
128
|
readonly lines: readonly string[]
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
/** Bun echoes the script it runs (`$ bun build-web.ts`) on stderr; it names the command, not a failure. */
|
|
132
|
+
const BUN_SCRIPT_ECHO = /^\$ \S/
|
|
133
|
+
|
|
129
134
|
type SyncOutcome =
|
|
130
|
-
| { readonly kind: 'applied'; readonly migrations: boolean; readonly
|
|
135
|
+
| { readonly kind: 'applied'; readonly migrations: boolean; readonly spaFailure: string | null }
|
|
131
136
|
| { readonly kind: 'refused'; readonly reason: string }
|
|
132
137
|
|
|
138
|
+
/** Control answers 409 to the container of a bootstrap run that a newer one replaced. */
|
|
139
|
+
class SupersededError extends Error {}
|
|
140
|
+
|
|
141
|
+
const SUPERSEDED = 'a newer bootstrap run replaced this container; it stops following Control and serves until it is stopped'
|
|
142
|
+
|
|
133
143
|
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
|
|
134
144
|
|
|
135
145
|
/** Every value of the environment a log line must never show: the database credentials and every key, secret or token. */
|
|
@@ -176,6 +186,8 @@ class Launcher {
|
|
|
176
186
|
private crashes = 0
|
|
177
187
|
private stopped = false
|
|
178
188
|
private reportFailing = false
|
|
189
|
+
/** A newer bootstrap run replaced this container: it keeps serving until the platform stops it, and asks nothing. */
|
|
190
|
+
private superseded = false
|
|
179
191
|
|
|
180
192
|
constructor(private readonly options: DevLauncherOptions) {
|
|
181
193
|
this.root = options.root
|
|
@@ -225,7 +237,12 @@ class Launcher {
|
|
|
225
237
|
while (!this.stopped) {
|
|
226
238
|
try {
|
|
227
239
|
return await this.wait(false)
|
|
228
|
-
} catch {
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error instanceof SupersededError) {
|
|
242
|
+
this.say(SUPERSEDED, 'stderr')
|
|
243
|
+
this.superseded = true
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
229
246
|
if (Date.now() >= deadline) {
|
|
230
247
|
this.say('Control did not answer at boot; serving the files this container holds', 'stderr')
|
|
231
248
|
return null
|
|
@@ -238,13 +255,18 @@ class Launcher {
|
|
|
238
255
|
|
|
239
256
|
private async follow(): Promise<void> {
|
|
240
257
|
let backoff = 1_000
|
|
241
|
-
while (!this.stopped) {
|
|
258
|
+
while (!this.stopped && !this.superseded) {
|
|
242
259
|
let answer: DevWaitAnswer
|
|
243
260
|
try {
|
|
244
261
|
answer = await this.wait(true)
|
|
245
262
|
backoff = 1_000
|
|
246
|
-
} catch {
|
|
263
|
+
} catch (error) {
|
|
247
264
|
if (this.stopped) return
|
|
265
|
+
if (error instanceof SupersededError) {
|
|
266
|
+
this.say(SUPERSEDED, 'stderr')
|
|
267
|
+
this.superseded = true
|
|
268
|
+
return
|
|
269
|
+
}
|
|
248
270
|
this.say(`the long poll on Control failed; retrying in ${backoff / 1000} s`, 'stderr')
|
|
249
271
|
await sleep(backoff)
|
|
250
272
|
backoff = Math.min(backoff * 2, 15_000)
|
|
@@ -255,9 +277,19 @@ class Launcher {
|
|
|
255
277
|
}
|
|
256
278
|
|
|
257
279
|
private async wait(hold: boolean): Promise<DevWaitAnswer> {
|
|
258
|
-
const body = encodeDevWaitRequest({
|
|
280
|
+
const body = encodeDevWaitRequest({
|
|
281
|
+
bootstrap: this.config.head,
|
|
282
|
+
head: this.state.head,
|
|
283
|
+
reset: this.state.reset,
|
|
284
|
+
checks: this.state.checks,
|
|
285
|
+
hold,
|
|
286
|
+
})
|
|
259
287
|
const signal = AbortSignal.any([this.controller.signal, AbortSignal.timeout(DEV_SOURCE_WAIT_MS + 15_000)])
|
|
260
288
|
const response = await this.fetch(this.request(DEV_SOURCE_WAIT_PATH, body, signal))
|
|
289
|
+
if (response.status === 409) {
|
|
290
|
+
await response.body?.cancel()
|
|
291
|
+
throw new SupersededError()
|
|
292
|
+
}
|
|
261
293
|
if (!response.ok) throw new Error(`the source receiver answered ${response.status}`)
|
|
262
294
|
const parsed = parseDevWaitAnswer(await response.json())
|
|
263
295
|
if (!parsed.ok) throw new Error('the source receiver answered a body this release does not read')
|
|
@@ -274,7 +306,9 @@ class Launcher {
|
|
|
274
306
|
}
|
|
275
307
|
|
|
276
308
|
private async report(): Promise<void> {
|
|
309
|
+
if (this.superseded) return
|
|
277
310
|
const body = encodeDevReport({
|
|
311
|
+
bootstrap: this.config.head,
|
|
278
312
|
head: this.state.head,
|
|
279
313
|
status: this.status,
|
|
280
314
|
target: this.target,
|
|
@@ -299,7 +333,9 @@ class Launcher {
|
|
|
299
333
|
if (this.state.checks === null) this.state = { ...this.state, checks: answer.checks }
|
|
300
334
|
let changed = boot
|
|
301
335
|
let migrate = boot
|
|
302
|
-
let
|
|
336
|
+
let spaFailure: string | null = null
|
|
337
|
+
const reset = answer.reset > (this.state.reset ?? answer.reset)
|
|
338
|
+
if (answer.sync !== null || reset) this.ring.mark()
|
|
303
339
|
if (answer.sync !== null) {
|
|
304
340
|
const outcome = await this.writeSync(answer.sync)
|
|
305
341
|
if (outcome.kind === 'refused') {
|
|
@@ -311,16 +347,15 @@ class Launcher {
|
|
|
311
347
|
} else {
|
|
312
348
|
changed = true
|
|
313
349
|
migrate ||= outcome.migrations
|
|
314
|
-
|
|
350
|
+
spaFailure = outcome.spaFailure
|
|
315
351
|
}
|
|
316
352
|
}
|
|
317
|
-
|
|
318
|
-
if (changed || reset) await this.converge({ migrate: migrate || reset, reset, web, resetTo: answer.reset })
|
|
353
|
+
if (changed || reset) await this.converge({ migrate: migrate || reset, reset, spaFailure, resetTo: answer.reset })
|
|
319
354
|
if (answer.checks > (this.state.checks ?? answer.checks)) await this.check(answer.checks)
|
|
320
355
|
}
|
|
321
356
|
|
|
322
357
|
/** Bring the database, the SPA and the running application to the files on disk, then report. */
|
|
323
|
-
private async converge(steps: { migrate: boolean; reset: boolean;
|
|
358
|
+
private async converge(steps: { migrate: boolean; reset: boolean; spaFailure: string | null; resetTo: number }): Promise<void> {
|
|
324
359
|
this.status = steps.reset ? 'resetting' : 'syncing'
|
|
325
360
|
void this.report()
|
|
326
361
|
let failure: string | null = null
|
|
@@ -331,7 +366,7 @@ class Launcher {
|
|
|
331
366
|
this.state = { ...this.state, reset: steps.resetTo }
|
|
332
367
|
}
|
|
333
368
|
if (failure === null && steps.migrate) failure = await this.timed('the migration', () => this.migrate())
|
|
334
|
-
if (failure === null
|
|
369
|
+
if (failure === null) failure = steps.spaFailure
|
|
335
370
|
const served = await this.timed('the restart until healthy', () => this.restartApplication())
|
|
336
371
|
failure ??= served
|
|
337
372
|
this.status = failure === null ? 'serving' : 'failed'
|
|
@@ -360,14 +395,21 @@ class Launcher {
|
|
|
360
395
|
sync.files.map(async (file) => ({ path: file.path, content: await readFile(join(this.root, file.path)).catch(() => null) })),
|
|
361
396
|
)
|
|
362
397
|
await this.writeFiles(sync.files)
|
|
363
|
-
const
|
|
398
|
+
const web = this.config.spaBuild.length > 0 && webSourceChanged(paths)
|
|
399
|
+
// The SPA build runs beside the digest: a refusal is rare, and then the build runs again over the restored files.
|
|
400
|
+
const [changedContract, spaFailure] = await Promise.all([this.contractChange(), web ? this.spaBuild() : null])
|
|
364
401
|
if (changedContract !== null) {
|
|
365
402
|
await this.writeFiles(previous)
|
|
403
|
+
if (web) await this.spaBuild()
|
|
366
404
|
return { kind: 'refused', reason: changedContract }
|
|
367
405
|
}
|
|
368
406
|
this.state = { ...this.state, head: sync.to }
|
|
369
407
|
this.say(`synced ${sync.files.length} files to ${sync.to}`)
|
|
370
|
-
return { kind: 'applied', migrations: migrationsChanged(paths),
|
|
408
|
+
return { kind: 'applied', migrations: migrationsChanged(paths), spaFailure }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private spaBuild(): Promise<string | null> {
|
|
412
|
+
return this.timed('the SPA build', () => this.buildSpa())
|
|
371
413
|
}
|
|
372
414
|
|
|
373
415
|
private async writeFiles(files: readonly { readonly path: string; readonly content: string | Uint8Array | null }[]): Promise<void> {
|
|
@@ -447,7 +489,12 @@ class Launcher {
|
|
|
447
489
|
|
|
448
490
|
private async buildSpa(): Promise<string | null> {
|
|
449
491
|
for (const command of this.config.spaBuild) {
|
|
450
|
-
const result = await this.run(
|
|
492
|
+
const result = await this.run(
|
|
493
|
+
['sh', '-c', command],
|
|
494
|
+
this.commandEnvironment,
|
|
495
|
+
CHECK_TIMEOUT_MS,
|
|
496
|
+
(stream, line) => stream === 'stderr' && BUN_SCRIPT_ECHO.test(line) ? 'stdout' : stream,
|
|
497
|
+
)
|
|
451
498
|
if (result.exitCode !== 0) return `the SPA command \`${command}\` failed: ${lastLine(result)}`
|
|
452
499
|
}
|
|
453
500
|
return null
|
|
@@ -460,12 +507,17 @@ class Launcher {
|
|
|
460
507
|
const results: DevCheckResult[] = []
|
|
461
508
|
for (const check of this.config.checks) {
|
|
462
509
|
this.say(`check ${check.name}: ${check.command}`)
|
|
510
|
+
const started = performance.now()
|
|
463
511
|
const result = await this.run(['sh', '-c', check.command], this.commandEnvironment, CHECK_TIMEOUT_MS)
|
|
512
|
+
const durationMs = Math.round(performance.now() - started)
|
|
513
|
+
const killed = result.signal === null ? [] : [killedLine(check.name, result.signal, durationMs >= CHECK_TIMEOUT_MS)]
|
|
514
|
+
for (const line of killed) this.say(line, 'stderr')
|
|
464
515
|
results.push({
|
|
465
516
|
name: check.name,
|
|
466
517
|
ok: result.exitCode === 0,
|
|
467
518
|
exitCode: result.exitCode,
|
|
468
|
-
|
|
519
|
+
durationMs,
|
|
520
|
+
output: [...result.lines.slice(-(CHECK_OUTPUT_LINES - killed.length)), ...killed].map((line) => line.slice(0, 2_000)),
|
|
469
521
|
})
|
|
470
522
|
}
|
|
471
523
|
this.checkResults = { request, head: this.state.head, results }
|
|
@@ -477,17 +529,22 @@ class Launcher {
|
|
|
477
529
|
}
|
|
478
530
|
|
|
479
531
|
/** Run a command in the root; its output goes to the log, and its lines are returned. */
|
|
480
|
-
private async run(
|
|
532
|
+
private async run(
|
|
533
|
+
argv: readonly string[],
|
|
534
|
+
env: Record<string, string>,
|
|
535
|
+
timeoutMs = CHECK_TIMEOUT_MS,
|
|
536
|
+
classify: (stream: OutputStream, line: string) => OutputStream = (stream) => stream,
|
|
537
|
+
): Promise<CommandResult> {
|
|
481
538
|
const child = Bun.spawn([...argv], { cwd: this.root, env, stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' })
|
|
482
539
|
const lines: string[] = []
|
|
483
|
-
const collect = (stream:
|
|
540
|
+
const collect = (stream: OutputStream) => (line: string) => {
|
|
484
541
|
lines.push(line)
|
|
485
|
-
this.ring.push(stream, line)
|
|
542
|
+
this.ring.push(classify(stream, line), line)
|
|
486
543
|
}
|
|
487
544
|
const timer = setTimeout(() => child.kill('SIGKILL'), timeoutMs)
|
|
488
545
|
await Promise.all([readLines(child.stdout, collect('stdout')), readLines(child.stderr, collect('stderr')), child.exited])
|
|
489
546
|
clearTimeout(timer)
|
|
490
|
-
return { exitCode: child.exitCode, lines }
|
|
547
|
+
return { exitCode: child.exitCode, signal: child.signalCode === null ? null : String(child.signalCode), lines }
|
|
491
548
|
}
|
|
492
549
|
|
|
493
550
|
private async restartApplication(): Promise<string | null> {
|
|
@@ -571,6 +628,12 @@ class Launcher {
|
|
|
571
628
|
}
|
|
572
629
|
}
|
|
573
630
|
|
|
631
|
+
/** Why a check has no exit code: the launcher's timeout, or a signal from outside, most often the out-of-memory killer. */
|
|
632
|
+
const killedLine = (name: string, signal: string, timedOut: boolean): string =>
|
|
633
|
+
timedOut
|
|
634
|
+
? `check ${name} was killed (${signal}) after the ${CHECK_TIMEOUT_MS / 60_000}-minute timeout; it failed`
|
|
635
|
+
: `check ${name} was killed (${signal}) before it finished, most likely because the container ran out of memory; it failed`
|
|
636
|
+
|
|
574
637
|
const lastLine = (result: CommandResult): string => [...result.lines].reverse().find((line) => line.trim() !== '')?.trim() ?? 'no output'
|
|
575
638
|
|
|
576
639
|
/** The process environment with its string values only. */
|
package/src/dev/output.ts
CHANGED
|
@@ -16,10 +16,14 @@ export const processOutput: OutputSink = {
|
|
|
16
16
|
},
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The newest `capacity` lines, numbered from 0 since the launcher started, with every known secret value replaced. A
|
|
21
|
+
* mark starts the lines a report carries: those of the newest sync or reset, and the output after it.
|
|
22
|
+
*/
|
|
20
23
|
export class OutputRing {
|
|
21
24
|
private readonly lines: DevLogLine[] = []
|
|
22
25
|
private next = 0
|
|
26
|
+
private marked = 0
|
|
23
27
|
|
|
24
28
|
constructor(private readonly capacity: number, private readonly secrets: readonly string[], private readonly sink: OutputSink) {}
|
|
25
29
|
|
|
@@ -31,9 +35,15 @@ export class OutputRing {
|
|
|
31
35
|
if (this.lines.length > this.capacity) this.lines.shift()
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
/**
|
|
38
|
+
/** Report from the next line on: the lines before it belong to an older head. */
|
|
39
|
+
mark(): void {
|
|
40
|
+
this.marked = this.next
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The newest `count` lines since the mark, oldest first. */
|
|
35
44
|
tail(count: number): DevLogLine[] {
|
|
36
|
-
|
|
45
|
+
const since = this.lines.filter((line) => line.n >= this.marked)
|
|
46
|
+
return since.slice(Math.max(0, since.length - count))
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
private redact(text: string): string {
|
package/src/dev/wire.ts
CHANGED
|
@@ -32,7 +32,14 @@ export function isSourcePath(path: string): boolean {
|
|
|
32
32
|
|
|
33
33
|
const sourcePath = z.string().refine(isSourcePath, 'not a repository-relative path')
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The commit the container's bootstrap run built (`DevLauncherConfig.head`). Control answers only the container of the
|
|
37
|
+
* newest bootstrap run: a container a newer bootstrap replaces still runs for a while, and must not sync or report.
|
|
38
|
+
*/
|
|
39
|
+
const bootstrap = commitSha
|
|
40
|
+
|
|
35
41
|
export const devWaitRequestSchema = z.object({
|
|
42
|
+
bootstrap,
|
|
36
43
|
/** The commit whose files the container holds now. */
|
|
37
44
|
head: commitSha,
|
|
38
45
|
/** The reset generation it applied last; null for a container with no record of one, which adopts the answer's. */
|
|
@@ -81,6 +88,7 @@ export const devCheckResultSchema = z.object({
|
|
|
81
88
|
name: z.string().min(1).max(64),
|
|
82
89
|
ok: z.boolean(),
|
|
83
90
|
exitCode: z.number().int().nullable(),
|
|
91
|
+
durationMs: z.number().int().min(0),
|
|
84
92
|
/** The last lines of its output. */
|
|
85
93
|
output: z.array(z.string().max(DEV_REPORT_MAX_LINE_CHARACTERS)).max(DEV_REPORT_MAX_LINES),
|
|
86
94
|
}).strict()
|
|
@@ -88,6 +96,7 @@ export const devCheckResultSchema = z.object({
|
|
|
88
96
|
export type DevCheckResult = z.infer<typeof devCheckResultSchema>
|
|
89
97
|
|
|
90
98
|
export const devReportSchema = z.object({
|
|
99
|
+
bootstrap,
|
|
91
100
|
/** The commit whose files the container holds now. */
|
|
92
101
|
head: commitSha,
|
|
93
102
|
status: z.enum(DEV_STATUSES),
|
|
@@ -5,16 +5,22 @@ export interface OutputSink {
|
|
|
5
5
|
}
|
|
6
6
|
/** The launcher process's own stdout and stderr. */
|
|
7
7
|
export declare const processOutput: OutputSink;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The newest `capacity` lines, numbered from 0 since the launcher started, with every known secret value replaced. A
|
|
10
|
+
* mark starts the lines a report carries: those of the newest sync or reset, and the output after it.
|
|
11
|
+
*/
|
|
9
12
|
export declare class OutputRing {
|
|
10
13
|
private readonly capacity;
|
|
11
14
|
private readonly secrets;
|
|
12
15
|
private readonly sink;
|
|
13
16
|
private readonly lines;
|
|
14
17
|
private next;
|
|
18
|
+
private marked;
|
|
15
19
|
constructor(capacity: number, secrets: readonly string[], sink: OutputSink);
|
|
16
20
|
push(stream: OutputStream, text: string): void;
|
|
17
|
-
/**
|
|
21
|
+
/** Report from the next line on: the lines before it belong to an older head. */
|
|
22
|
+
mark(): void;
|
|
23
|
+
/** The newest `count` lines since the mark, oldest first. */
|
|
18
24
|
tail(count: number): DevLogLine[];
|
|
19
25
|
private redact;
|
|
20
26
|
}
|
package/types/src/dev/wire.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const DEV_REPORT_MAX_MESSAGE_CHARACTERS = 500;
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function isSourcePath(path: string): boolean;
|
|
17
17
|
export declare const devWaitRequestSchema: z.ZodObject<{
|
|
18
|
+
bootstrap: z.ZodString;
|
|
18
19
|
head: z.ZodString;
|
|
19
20
|
reset: z.ZodNullable<z.ZodNumber>;
|
|
20
21
|
checks: z.ZodNullable<z.ZodNumber>;
|
|
@@ -70,10 +71,12 @@ export declare const devCheckResultSchema: z.ZodObject<{
|
|
|
70
71
|
name: z.ZodString;
|
|
71
72
|
ok: z.ZodBoolean;
|
|
72
73
|
exitCode: z.ZodNullable<z.ZodNumber>;
|
|
74
|
+
durationMs: z.ZodNumber;
|
|
73
75
|
output: z.ZodArray<z.ZodString>;
|
|
74
76
|
}, z.core.$strict>;
|
|
75
77
|
export type DevCheckResult = z.infer<typeof devCheckResultSchema>;
|
|
76
78
|
export declare const devReportSchema: z.ZodObject<{
|
|
79
|
+
bootstrap: z.ZodString;
|
|
77
80
|
head: z.ZodString;
|
|
78
81
|
status: z.ZodEnum<{
|
|
79
82
|
failed: "failed";
|
|
@@ -101,6 +104,7 @@ export declare const devReportSchema: z.ZodObject<{
|
|
|
101
104
|
name: z.ZodString;
|
|
102
105
|
ok: z.ZodBoolean;
|
|
103
106
|
exitCode: z.ZodNullable<z.ZodNumber>;
|
|
107
|
+
durationMs: z.ZodNumber;
|
|
104
108
|
output: z.ZodArray<z.ZodString>;
|
|
105
109
|
}, z.core.$strict>>;
|
|
106
110
|
}, z.core.$strict>>;
|