@defrex/autobuild 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SPEC.md +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
package/skills/tickets/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tickets
|
|
3
|
-
description: Work this repo's local ticket tracker — create or edit a ticket, amend blockedBy dependencies, report the backlog, move work between
|
|
3
|
+
description: Work this repo's local ticket tracker — create or edit a ticket, amend blockedBy dependencies, report the backlog, triage the inbox, move work between the lifecycle directories and any repository-defined states such as the icebox, or answer "what's the status of ticket X". Use whenever the user asks about tickets, blockers, the backlog, or wants something queued for autobuild to build.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /tickets
|
|
@@ -12,7 +12,11 @@ content and blocker relationships. The default file source needs no secret.
|
|
|
12
12
|
## Where it lives
|
|
13
13
|
|
|
14
14
|
`.autobuild/tickets/` by default (if `autobuild.toml` has a `[tickets] dir`,
|
|
15
|
-
that directory instead), holding
|
|
15
|
+
that directory instead), holding the four lifecycle state directories plus any others this repo adds.
|
|
16
|
+
`triage/`, `ready/`, `doing/`, and `done/` are the lifecycle directories the
|
|
17
|
+
dispatcher uses. Any other directory is a repository-defined state: the
|
|
18
|
+
dispatcher never moves tickets into it and never dispatches from it unless
|
|
19
|
+
`autobuild.toml` names it (for example `readyState` or `proposalState`).
|
|
16
20
|
|
|
17
21
|
```
|
|
18
22
|
.autobuild/tickets/
|
|
@@ -20,6 +24,7 @@ that directory instead), holding exactly four state directories:
|
|
|
20
24
|
ready/ # groomed; file lifecycle-state gate satisfied
|
|
21
25
|
doing/ # claimed; a build is running
|
|
22
26
|
done/ # merged
|
|
27
|
+
icebox/ # example of a repo-added state, present only once created
|
|
23
28
|
```
|
|
24
29
|
|
|
25
30
|
**A ticket's state is the directory it is in.** There is no `state` field
|
|
@@ -71,13 +76,17 @@ retry, and adding validates the blocker exists and is not the ticket itself.
|
|
|
71
76
|
## Report the backlog
|
|
72
77
|
|
|
73
78
|
```
|
|
79
|
+
ab ticket states # every state, lifecycle and custom: role, count, purpose
|
|
74
80
|
ls .autobuild/tickets/ready # groomed candidates in the ready lifecycle state
|
|
75
81
|
ls .autobuild/tickets/doing # what's building right now
|
|
76
82
|
```
|
|
77
83
|
|
|
78
84
|
The filename is the id. For a title, read the file's frontmatter. To report
|
|
79
|
-
the whole backlog,
|
|
80
|
-
|
|
85
|
+
the whole backlog, cover every state — lifecycle and custom — via
|
|
86
|
+
`ab ticket states` or by listing every directory under the tracker root, so the
|
|
87
|
+
report includes `icebox/` and any other category. `ab ticket list` without
|
|
88
|
+
filters shows only the ready state, regardless of custom states, so it is not a
|
|
89
|
+
backlog report.
|
|
81
90
|
|
|
82
91
|
## Groom / transition a ticket
|
|
83
92
|
|
|
@@ -113,6 +122,49 @@ bounces.
|
|
|
113
122
|
**Don't hand-move anything out of `doing/`** — a build owns it. If you need to
|
|
114
123
|
stop a build, that's `ab` (or a human), not `mv`.
|
|
115
124
|
|
|
125
|
+
## Icebox
|
|
126
|
+
|
|
127
|
+
`icebox/` is work deliberately deferred, groomed or not, that nothing
|
|
128
|
+
dispatches from. The first time it is needed, create it:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
ab ticket state create icebox --about "Deferred on purpose; nothing dispatches from here"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Move a ticket in with `ab ticket move file-3 icebox`. To revive one, move it to
|
|
135
|
+
`triage/` when it needs regrooming, or to `ready/` when its spec still holds.
|
|
136
|
+
|
|
137
|
+
## Triage the backlog
|
|
138
|
+
|
|
139
|
+
To run a pass over `triage/`:
|
|
140
|
+
|
|
141
|
+
1. Run `ab ticket states` to see which destinations exist.
|
|
142
|
+
2. For each ticket in `triage/`, read it with `ab ticket show <id>` and judge
|
|
143
|
+
whether its body meets the
|
|
144
|
+
[spec standard](../ab-guide/references/spec-standard.md).
|
|
145
|
+
3. Move it to `ready/` if it conforms and should be built; to `icebox/` if it
|
|
146
|
+
should wait; to another existing category if one fits; otherwise leave it in
|
|
147
|
+
`triage/` and tell the user why.
|
|
148
|
+
4. Report every move to the user in the session — id, title, destination, and
|
|
149
|
+
reason. A move leaves no comment on the ticket, so the report is the only
|
|
150
|
+
record of why.
|
|
151
|
+
|
|
152
|
+
## Inventing categories
|
|
153
|
+
|
|
154
|
+
Run `ab ticket states` before creating anything. Reuse an existing state whose
|
|
155
|
+
purpose fits. Create a new one only for a recurring reason that no existing
|
|
156
|
+
state covers, with a lowercase hyphenated noun name and a one-line `--about`
|
|
157
|
+
purpose. `icebox` is the first worked example; `rejected` is a second:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
ab ticket state create rejected --about "Considered and declined; kept for the record"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Never invent a second ready, doing, or done state, never move anything out of
|
|
164
|
+
`doing/`, and never create states for a build's internal progress — the
|
|
165
|
+
lifecycle belongs to the dispatcher. Remember that `ab ticket list` without
|
|
166
|
+
filters still shows only the ready state, however many custom states exist.
|
|
167
|
+
|
|
116
168
|
## Rules
|
|
117
169
|
|
|
118
170
|
- Never edit frontmatter to change state — the directory is the state.
|
|
@@ -132,7 +184,7 @@ listing under default config. That does not make `ready/` sufficient;
|
|
|
132
184
|
unresolved blockers are an independent dispatch gate.
|
|
133
185
|
|
|
134
186
|
No Linear lifecycle UI. Directory-based lifecycle operations (`ls`, `mv`, and
|
|
135
|
-
the
|
|
187
|
+
the state directories) are file-tracker-only. If this repo's
|
|
136
188
|
`autobuild.toml` sets `[tickets] source = "linear"`, do not apply that lifecycle
|
|
137
189
|
guidance: state and labels live in Linear. The source-agnostic `ab ticket block`
|
|
138
190
|
and `ab ticket unblock` commands still edit blocker relationships through the
|
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
import { readdirSync } from 'node:fs'
|
|
2
|
-
import { createRequire } from 'node:module'
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
4
|
-
import { Resvg } from '@resvg/resvg-js'
|
|
5
|
-
import { distributionPath } from '../../distribution'
|
|
6
|
-
import { graphemes } from './cells'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Deterministic dashboard-frame rendering.
|
|
10
|
-
*
|
|
11
|
-
* The dashboard deliberately emits a tiny terminal vocabulary: SGR reset,
|
|
12
|
-
* bold/dim and six named foreground colours, plus OSC 8 hyperlinks. This
|
|
13
|
-
* adapter accepts exactly that vocabulary. Unknown control traffic is an
|
|
14
|
-
* error rather than evidence that merely looks plausible after bytes were
|
|
15
|
-
* dropped.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const ESC = '\x1b'
|
|
19
|
-
const BEL = '\x07'
|
|
20
|
-
const FONT_FAMILY = 'DejaVu Sans Mono'
|
|
21
|
-
const FONT_FAMILIES = [
|
|
22
|
-
FONT_FAMILY,
|
|
23
|
-
'Dashboard CJK 113',
|
|
24
|
-
'Dashboard CJK 117',
|
|
25
|
-
'Dashboard CJK 118',
|
|
26
|
-
'Dashboard Emoji Flags',
|
|
27
|
-
'Dashboard Emoji Food',
|
|
28
|
-
'Dashboard Emoji People',
|
|
29
|
-
].join(', ')
|
|
30
|
-
const FONT_SIZE = 16
|
|
31
|
-
const CELL_WIDTH = 10
|
|
32
|
-
const LINE_HEIGHT = 20
|
|
33
|
-
const PADDING_X = 12
|
|
34
|
-
const PADDING_Y = 10
|
|
35
|
-
const BASELINE = 16
|
|
36
|
-
const EMOJI_CLUSTER = /\p{Extended_Pictographic}|\p{Regional_Indicator}/u
|
|
37
|
-
|
|
38
|
-
const PALETTE = {
|
|
39
|
-
background: '#0d1117',
|
|
40
|
-
foreground: '#c9d1d9',
|
|
41
|
-
red: '#ff7b72',
|
|
42
|
-
green: '#3fb950',
|
|
43
|
-
yellow: '#d29922',
|
|
44
|
-
blue: '#58a6ff',
|
|
45
|
-
cyan: '#39c5cf',
|
|
46
|
-
} as const
|
|
47
|
-
|
|
48
|
-
type Foreground = Exclude<keyof typeof PALETTE, 'background'>
|
|
49
|
-
|
|
50
|
-
interface Style {
|
|
51
|
-
foreground: Foreground
|
|
52
|
-
bold: boolean
|
|
53
|
-
dim: boolean
|
|
54
|
-
href?: string
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface TextRun {
|
|
58
|
-
column: number
|
|
59
|
-
cells: number
|
|
60
|
-
text: string
|
|
61
|
-
style: Style
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface ParsedLine {
|
|
65
|
-
text: string
|
|
66
|
-
cells: number
|
|
67
|
-
runs: TextRun[]
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface FrameImageOptions {
|
|
71
|
-
/** Declared terminal width. Every parsed line must fit this grid. */
|
|
72
|
-
columns: number
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface RenderedFrameImage {
|
|
76
|
-
/** ANSI/OSC-free text from the exact parsed cells, with a final newline. */
|
|
77
|
-
text: string
|
|
78
|
-
svg: string
|
|
79
|
-
png: Uint8Array
|
|
80
|
-
width: number
|
|
81
|
-
height: number
|
|
82
|
-
rows: number
|
|
83
|
-
columns: number
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function cloneStyle(style: Style): Style {
|
|
87
|
-
return {
|
|
88
|
-
foreground: style.foreground,
|
|
89
|
-
bold: style.bold,
|
|
90
|
-
dim: style.dim,
|
|
91
|
-
...(style.href !== undefined ? { href: style.href } : {}),
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function sameStyle(left: Style, right: Style): boolean {
|
|
96
|
-
return (
|
|
97
|
-
left.foreground === right.foreground &&
|
|
98
|
-
left.bold === right.bold &&
|
|
99
|
-
left.dim === right.dim &&
|
|
100
|
-
left.href === right.href
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function applySgr(style: Style, raw: string, line: number): void {
|
|
105
|
-
const codes = raw === '' ? [0] : raw.split(';').map((part) => Number(part))
|
|
106
|
-
if (codes.some((code) => !Number.isInteger(code))) {
|
|
107
|
-
throw new Error(`dashboard frame line ${line}: malformed SGR sequence ESC[${raw}m`)
|
|
108
|
-
}
|
|
109
|
-
for (const code of codes) {
|
|
110
|
-
switch (code) {
|
|
111
|
-
case 0:
|
|
112
|
-
style.foreground = 'foreground'
|
|
113
|
-
style.bold = false
|
|
114
|
-
style.dim = false
|
|
115
|
-
break
|
|
116
|
-
case 1:
|
|
117
|
-
style.bold = true
|
|
118
|
-
break
|
|
119
|
-
case 2:
|
|
120
|
-
style.dim = true
|
|
121
|
-
break
|
|
122
|
-
case 22:
|
|
123
|
-
style.bold = false
|
|
124
|
-
style.dim = false
|
|
125
|
-
break
|
|
126
|
-
case 31:
|
|
127
|
-
style.foreground = 'red'
|
|
128
|
-
break
|
|
129
|
-
case 32:
|
|
130
|
-
style.foreground = 'green'
|
|
131
|
-
break
|
|
132
|
-
case 33:
|
|
133
|
-
style.foreground = 'yellow'
|
|
134
|
-
break
|
|
135
|
-
case 34:
|
|
136
|
-
style.foreground = 'blue'
|
|
137
|
-
break
|
|
138
|
-
case 36:
|
|
139
|
-
style.foreground = 'cyan'
|
|
140
|
-
break
|
|
141
|
-
case 39:
|
|
142
|
-
style.foreground = 'foreground'
|
|
143
|
-
break
|
|
144
|
-
default:
|
|
145
|
-
throw new Error(`dashboard frame line ${line}: unsupported SGR code ${code} in ESC[${raw}m`)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function parseLine(value: string, lineNumber: number): ParsedLine {
|
|
151
|
-
const style: Style = {
|
|
152
|
-
foreground: 'foreground',
|
|
153
|
-
bold: false,
|
|
154
|
-
dim: false,
|
|
155
|
-
}
|
|
156
|
-
const runs: TextRun[] = []
|
|
157
|
-
let text = ''
|
|
158
|
-
let cells = 0
|
|
159
|
-
|
|
160
|
-
const append = (cluster: string, width: number): void => {
|
|
161
|
-
const previous = runs.at(-1)
|
|
162
|
-
// ASCII runs may coalesce. Unicode clusters stay independently pinned to
|
|
163
|
-
// their terminal cell columns so font fallback/advance cannot shift the
|
|
164
|
-
// run that follows a wide glyph.
|
|
165
|
-
if (
|
|
166
|
-
previous !== undefined &&
|
|
167
|
-
previous.column + previous.cells === cells &&
|
|
168
|
-
/^[\x20-\x7e]*$/.test(previous.text) &&
|
|
169
|
-
/^[\x20-\x7e]$/.test(cluster) &&
|
|
170
|
-
sameStyle(previous.style, style)
|
|
171
|
-
) {
|
|
172
|
-
previous.text += cluster
|
|
173
|
-
previous.cells += width
|
|
174
|
-
} else {
|
|
175
|
-
runs.push({ column: cells, cells: width, text: cluster, style: cloneStyle(style) })
|
|
176
|
-
}
|
|
177
|
-
text += cluster
|
|
178
|
-
cells += width
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
for (let index = 0; index < value.length; ) {
|
|
182
|
-
if (value[index] === ESC) {
|
|
183
|
-
const family = value[index + 1]
|
|
184
|
-
if (family === '[') {
|
|
185
|
-
const rest = value.slice(index + 2)
|
|
186
|
-
const match = /^([0-9;]*)m/.exec(rest)
|
|
187
|
-
if (match === null) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
`dashboard frame line ${lineNumber}: unsupported or unterminated CSI sequence`,
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
applySgr(style, match[1]!, lineNumber)
|
|
193
|
-
index += 2 + match[0].length
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
if (family === ']') {
|
|
197
|
-
const end = value.indexOf(BEL, index + 2)
|
|
198
|
-
if (end === -1) {
|
|
199
|
-
throw new Error(`dashboard frame line ${lineNumber}: unterminated OSC sequence`)
|
|
200
|
-
}
|
|
201
|
-
const payload = value.slice(index + 2, end)
|
|
202
|
-
const match = /^8;;(.*)$/.exec(payload)
|
|
203
|
-
if (match === null) {
|
|
204
|
-
throw new Error(
|
|
205
|
-
`dashboard frame line ${lineNumber}: unsupported OSC sequence ${JSON.stringify(payload)}`,
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
const href = match[1]!
|
|
209
|
-
style.href = href === '' ? undefined : href
|
|
210
|
-
index = end + 1
|
|
211
|
-
continue
|
|
212
|
-
}
|
|
213
|
-
throw new Error(
|
|
214
|
-
`dashboard frame line ${lineNumber}: unsupported escape family ${JSON.stringify(family ?? '')}`,
|
|
215
|
-
)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const nextEscape = value.indexOf(ESC, index)
|
|
219
|
-
const stop = nextEscape === -1 ? value.length : nextEscape
|
|
220
|
-
const plain = value.slice(index, stop)
|
|
221
|
-
for (const character of plain) {
|
|
222
|
-
const code = character.codePointAt(0)!
|
|
223
|
-
if (code < 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) {
|
|
224
|
-
throw new Error(
|
|
225
|
-
`dashboard frame line ${lineNumber}: unsupported control U+${code
|
|
226
|
-
.toString(16)
|
|
227
|
-
.toUpperCase()
|
|
228
|
-
.padStart(4, '0')}`,
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
for (const cluster of graphemes(plain)) append(cluster.text, cluster.width)
|
|
233
|
-
index = stop
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (style.href !== undefined) {
|
|
237
|
-
throw new Error(`dashboard frame line ${lineNumber}: OSC 8 hyperlink was not closed`)
|
|
238
|
-
}
|
|
239
|
-
if (style.foreground !== 'foreground' || style.bold || style.dim) {
|
|
240
|
-
throw new Error(
|
|
241
|
-
`dashboard frame line ${lineNumber}: SGR style was not reset before end of line`,
|
|
242
|
-
)
|
|
243
|
-
}
|
|
244
|
-
return { text, cells, runs }
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function xml(value: string): string {
|
|
248
|
-
return value
|
|
249
|
-
.replaceAll('&', '&')
|
|
250
|
-
.replaceAll('<', '<')
|
|
251
|
-
.replaceAll('>', '>')
|
|
252
|
-
.replaceAll('"', '"')
|
|
253
|
-
.replaceAll("'", ''')
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function svgFor(
|
|
257
|
-
parsed: ParsedLine[],
|
|
258
|
-
columns: number,
|
|
259
|
-
): {
|
|
260
|
-
svg: string
|
|
261
|
-
width: number
|
|
262
|
-
height: number
|
|
263
|
-
} {
|
|
264
|
-
const width = PADDING_X * 2 + columns * CELL_WIDTH
|
|
265
|
-
const height = PADDING_Y * 2 + Math.max(1, parsed.length) * LINE_HEIGHT
|
|
266
|
-
const content: string[] = []
|
|
267
|
-
|
|
268
|
-
for (const [row, line] of parsed.entries()) {
|
|
269
|
-
for (const run of line.runs) {
|
|
270
|
-
const x = PADDING_X + run.column * CELL_WIDTH
|
|
271
|
-
const y = PADDING_Y + BASELINE + row * LINE_HEIGHT
|
|
272
|
-
const opacity = run.style.dim ? ' fill-opacity="0.58"' : ''
|
|
273
|
-
const weight = run.style.bold ? ' font-weight="700"' : ' font-weight="400"'
|
|
274
|
-
// DejaVu's native advance is slightly narrower than the 10 px evidence
|
|
275
|
-
// grid. Fit coalesced ASCII runs to their exact terminal cells so the
|
|
276
|
-
// error cannot accumulate into a visible gap before a pinned Unicode
|
|
277
|
-
// cluster. Emoji get the same exact fit at a slightly larger size: the
|
|
278
|
-
// monochrome fallback remains readable without crossing its cell range.
|
|
279
|
-
const emoji = EMOJI_CLUSTER.test(run.text)
|
|
280
|
-
const geometry =
|
|
281
|
-
/^[\x20-\x7e]+$/.test(run.text) || emoji
|
|
282
|
-
? ` textLength="${run.cells * CELL_WIDTH}" lengthAdjust="spacingAndGlyphs"`
|
|
283
|
-
: ''
|
|
284
|
-
const size = emoji ? ' font-size="18"' : ''
|
|
285
|
-
const node =
|
|
286
|
-
`<text x="${x}" y="${y}" fill="${PALETTE[run.style.foreground]}"` +
|
|
287
|
-
`${weight}${opacity}${geometry}${size}>${xml(run.text)}</text>`
|
|
288
|
-
content.push(
|
|
289
|
-
run.style.href === undefined ? node : `<a href="${xml(run.style.href)}">${node}</a>`,
|
|
290
|
-
)
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
width,
|
|
296
|
-
height,
|
|
297
|
-
svg: [
|
|
298
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
|
|
299
|
-
`<rect width="${width}" height="${height}" fill="${PALETTE.background}"/>`,
|
|
300
|
-
`<g font-family="${FONT_FAMILIES}" font-size="${FONT_SIZE}" xml:space="preserve" text-rendering="geometricPrecision">`,
|
|
301
|
-
...content,
|
|
302
|
-
'</g>',
|
|
303
|
-
'</svg>',
|
|
304
|
-
].join(''),
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function fontFiles(): string[] {
|
|
309
|
-
const require = createRequire(import.meta.url)
|
|
310
|
-
const root = dirname(require.resolve('dejavu-fonts-ttf/package.json'))
|
|
311
|
-
const fallbacks = distributionPath('tools', 'fonts')
|
|
312
|
-
return [
|
|
313
|
-
join(root, 'ttf', 'DejaVuSansMono.ttf'),
|
|
314
|
-
join(root, 'ttf', 'DejaVuSansMono-Bold.ttf'),
|
|
315
|
-
...readdirSync(fallbacks)
|
|
316
|
-
.filter((name) => name.endsWith('.ttf'))
|
|
317
|
-
.sort()
|
|
318
|
-
.map((name) => join(fallbacks, name)),
|
|
319
|
-
]
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/** Parse once, then derive both evidence forms from the same exact cells. */
|
|
323
|
-
export function renderDashboardFrameImage(
|
|
324
|
-
lines: readonly string[],
|
|
325
|
-
options: FrameImageOptions,
|
|
326
|
-
): RenderedFrameImage {
|
|
327
|
-
if (!Number.isInteger(options.columns) || options.columns <= 0) {
|
|
328
|
-
throw new Error(`dashboard frame columns must be a positive integer, got ${options.columns}`)
|
|
329
|
-
}
|
|
330
|
-
if (lines.length === 0) {
|
|
331
|
-
throw new Error('dashboard frame is empty')
|
|
332
|
-
}
|
|
333
|
-
const parsed = lines.map((line, index) => parseLine(line, index + 1))
|
|
334
|
-
for (const [index, line] of parsed.entries()) {
|
|
335
|
-
if (line.cells > options.columns) {
|
|
336
|
-
throw new Error(
|
|
337
|
-
`dashboard frame line ${index + 1} is ${line.cells} cells wide, exceeding declared terminal width ${options.columns}`,
|
|
338
|
-
)
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const { svg, width, height } = svgFor(parsed, options.columns)
|
|
343
|
-
const rendered = new Resvg(svg, {
|
|
344
|
-
fitTo: { mode: 'original' },
|
|
345
|
-
background: PALETTE.background,
|
|
346
|
-
font: {
|
|
347
|
-
fontFiles: fontFiles(),
|
|
348
|
-
loadSystemFonts: false,
|
|
349
|
-
defaultFontFamily: FONT_FAMILY,
|
|
350
|
-
monospaceFamily: FONT_FAMILY,
|
|
351
|
-
defaultFontSize: FONT_SIZE,
|
|
352
|
-
},
|
|
353
|
-
shapeRendering: 2,
|
|
354
|
-
textRendering: 2,
|
|
355
|
-
imageRendering: 0,
|
|
356
|
-
logLevel: 'off',
|
|
357
|
-
}).render()
|
|
358
|
-
if (rendered.width !== width || rendered.height !== height) {
|
|
359
|
-
throw new Error(
|
|
360
|
-
`dashboard PNG dimensions ${rendered.width}x${rendered.height} did not match SVG ${width}x${height}`,
|
|
361
|
-
)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return {
|
|
365
|
-
text: `${parsed.map((line) => line.text).join('\n')}\n`,
|
|
366
|
-
svg,
|
|
367
|
-
png: new Uint8Array(rendered.asPng()),
|
|
368
|
-
width,
|
|
369
|
-
height,
|
|
370
|
-
rows: lines.length,
|
|
371
|
-
columns: options.columns,
|
|
372
|
-
}
|
|
373
|
-
}
|