@fabrika/app 0.9.0-dev.3 → 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 +34 -9
- package/src/dev/output.ts +13 -3
- package/types/src/dev/output.d.ts +8 -2
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,9 +123,14 @@ 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
135
|
| { readonly kind: 'applied'; readonly migrations: boolean; readonly spaFailure: string | null }
|
|
131
136
|
| { readonly kind: 'refused'; readonly reason: string }
|
|
@@ -329,6 +334,8 @@ class Launcher {
|
|
|
329
334
|
let changed = boot
|
|
330
335
|
let migrate = boot
|
|
331
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()
|
|
332
339
|
if (answer.sync !== null) {
|
|
333
340
|
const outcome = await this.writeSync(answer.sync)
|
|
334
341
|
if (outcome.kind === 'refused') {
|
|
@@ -343,7 +350,6 @@ class Launcher {
|
|
|
343
350
|
spaFailure = outcome.spaFailure
|
|
344
351
|
}
|
|
345
352
|
}
|
|
346
|
-
const reset = answer.reset > (this.state.reset ?? answer.reset)
|
|
347
353
|
if (changed || reset) await this.converge({ migrate: migrate || reset, reset, spaFailure, resetTo: answer.reset })
|
|
348
354
|
if (answer.checks > (this.state.checks ?? answer.checks)) await this.check(answer.checks)
|
|
349
355
|
}
|
|
@@ -483,7 +489,12 @@ class Launcher {
|
|
|
483
489
|
|
|
484
490
|
private async buildSpa(): Promise<string | null> {
|
|
485
491
|
for (const command of this.config.spaBuild) {
|
|
486
|
-
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
|
+
)
|
|
487
498
|
if (result.exitCode !== 0) return `the SPA command \`${command}\` failed: ${lastLine(result)}`
|
|
488
499
|
}
|
|
489
500
|
return null
|
|
@@ -498,12 +509,15 @@ class Launcher {
|
|
|
498
509
|
this.say(`check ${check.name}: ${check.command}`)
|
|
499
510
|
const started = performance.now()
|
|
500
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')
|
|
501
515
|
results.push({
|
|
502
516
|
name: check.name,
|
|
503
517
|
ok: result.exitCode === 0,
|
|
504
518
|
exitCode: result.exitCode,
|
|
505
|
-
durationMs
|
|
506
|
-
output: result.lines.slice(-CHECK_OUTPUT_LINES).map((line) => line.slice(0, 2_000)),
|
|
519
|
+
durationMs,
|
|
520
|
+
output: [...result.lines.slice(-(CHECK_OUTPUT_LINES - killed.length)), ...killed].map((line) => line.slice(0, 2_000)),
|
|
507
521
|
})
|
|
508
522
|
}
|
|
509
523
|
this.checkResults = { request, head: this.state.head, results }
|
|
@@ -515,17 +529,22 @@ class Launcher {
|
|
|
515
529
|
}
|
|
516
530
|
|
|
517
531
|
/** Run a command in the root; its output goes to the log, and its lines are returned. */
|
|
518
|
-
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> {
|
|
519
538
|
const child = Bun.spawn([...argv], { cwd: this.root, env, stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' })
|
|
520
539
|
const lines: string[] = []
|
|
521
|
-
const collect = (stream:
|
|
540
|
+
const collect = (stream: OutputStream) => (line: string) => {
|
|
522
541
|
lines.push(line)
|
|
523
|
-
this.ring.push(stream, line)
|
|
542
|
+
this.ring.push(classify(stream, line), line)
|
|
524
543
|
}
|
|
525
544
|
const timer = setTimeout(() => child.kill('SIGKILL'), timeoutMs)
|
|
526
545
|
await Promise.all([readLines(child.stdout, collect('stdout')), readLines(child.stderr, collect('stderr')), child.exited])
|
|
527
546
|
clearTimeout(timer)
|
|
528
|
-
return { exitCode: child.exitCode, lines }
|
|
547
|
+
return { exitCode: child.exitCode, signal: child.signalCode === null ? null : String(child.signalCode), lines }
|
|
529
548
|
}
|
|
530
549
|
|
|
531
550
|
private async restartApplication(): Promise<string | null> {
|
|
@@ -609,6 +628,12 @@ class Launcher {
|
|
|
609
628
|
}
|
|
610
629
|
}
|
|
611
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
|
+
|
|
612
637
|
const lastLine = (result: CommandResult): string => [...result.lines].reverse().find((line) => line.trim() !== '')?.trim() ?? 'no output'
|
|
613
638
|
|
|
614
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 {
|
|
@@ -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
|
}
|