@vzn/vx 0.0.646 → 0.0.648
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 +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
package/src/report/events.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { exitSignal } from '../exec/index.js'
|
|
10
10
|
import type { InputChanges, TaskNode, TaskOutcome } from '../graph/index.js'
|
|
11
11
|
import { isGroupTask } from '../graph/index.js'
|
|
12
|
-
import { maskedCommand, secretMask } from '../util/index.js'
|
|
12
|
+
import { heldSecrets, maskedCommand, type SecretMask, secretMask } from '../util/index.js'
|
|
13
13
|
import type { Logger } from './logger.js'
|
|
14
14
|
import type { RunContext } from './summary.js'
|
|
15
15
|
|
|
@@ -122,10 +122,19 @@ export function busLogger(bus: EventBus): Logger {
|
|
|
122
122
|
let done = false
|
|
123
123
|
// A status line carries plugin warnings, and a remote layer's warning
|
|
124
124
|
// quotes the server's reply, which may echo what it was sent (L-40).
|
|
125
|
-
|
|
125
|
+
// Built again once a plugin's keychain answered: its token is in no env.
|
|
126
|
+
let secrets: SecretMask | null = null
|
|
127
|
+
let built = -1
|
|
128
|
+
const masked = (line: string): string => {
|
|
129
|
+
if (built !== heldSecrets()) {
|
|
130
|
+
built = heldSecrets()
|
|
131
|
+
secrets = secretMask([process.env])
|
|
132
|
+
}
|
|
133
|
+
return secrets?.mask(line) ?? line
|
|
134
|
+
}
|
|
126
135
|
return {
|
|
127
|
-
status: (line) => bus.emit({ kind: 'run:status', line:
|
|
128
|
-
warn: (line) => bus.emit({ kind: 'run:status', line:
|
|
136
|
+
status: (line) => bus.emit({ kind: 'run:status', line: masked(line) }),
|
|
137
|
+
warn: (line) => bus.emit({ kind: 'run:status', line: masked(line), warn: true }),
|
|
129
138
|
taskStdout: (node, chunk) => bus.emit({ kind: 'task:stdout', node, chunk }),
|
|
130
139
|
taskStderr: (node, chunk) => bus.emit({ kind: 'task:stderr', node, chunk }),
|
|
131
140
|
taskComplete: (node, outcome) => bus.emit({ kind: 'task:complete', node, outcome }),
|
|
@@ -212,7 +221,7 @@ export function terminalSubscriber(sink: Logger): RunEventSubscriber {
|
|
|
212
221
|
export function wireForwarder(send: (event: WireEvent) => void): RunEventSubscriber {
|
|
213
222
|
// Which tasks the consumer has been told about. A skipped task never
|
|
214
223
|
// reaches the scheduler's onStart, so its completion arrives with no
|
|
215
|
-
// preceding task:start — and
|
|
224
|
+
// preceding task:start — and a consumer resolves a completion's
|
|
216
225
|
// node from the start it recorded, so it would drop the task entirely
|
|
217
226
|
// while the forwarded footer still counted it. Synthesize the start here,
|
|
218
227
|
// where the live TaskNode is in hand, so the projection stays full
|
|
@@ -292,6 +301,8 @@ export interface OutcomeView {
|
|
|
292
301
|
/** A skipped task: the failed (or aborted) task at the root of the block
|
|
293
302
|
* (see `TaskOutcome`); absent for fail-fast's skip, which nothing blocked. */
|
|
294
303
|
blockedBy?: string
|
|
304
|
+
/** A skipped task whose `exec.platforms` leave out this machine (see `TaskOutcome`). */
|
|
305
|
+
otherPlatform?: { readonly needs: readonly string[]; readonly host: string }
|
|
295
306
|
/** How long an `admit` policy held the task with a worker free (see `TaskOutcome`). */
|
|
296
307
|
admissionHeldMs?: number
|
|
297
308
|
/** How long the task waited ready for a worker (see `TaskOutcome`). */
|
|
@@ -319,7 +330,8 @@ export interface OutcomeView {
|
|
|
319
330
|
* header and footer, the focused one-liner, the hash-only audit line,
|
|
320
331
|
* the verbose summary table: `success` / `restored-local` /
|
|
321
332
|
* `restored-remote` / `up-to-date` / `failed` / `skipped`. A hit that
|
|
322
|
-
* materialized nothing is up-to-date
|
|
333
|
+
* materialized nothing is up-to-date; a remote hit always restored
|
|
334
|
+
* (`hitOutcome`), so it is never up to date.
|
|
323
335
|
*/
|
|
324
336
|
export function outcomeWord(o: Pick<OutcomeView, 'status' | 'restored'>): string {
|
|
325
337
|
switch (o.status) {
|
|
@@ -344,26 +356,32 @@ export function outcomeLabel(
|
|
|
344
356
|
| 'notReady'
|
|
345
357
|
| 'inputMoved'
|
|
346
358
|
| 'blockedBy'
|
|
359
|
+
| 'otherPlatform'
|
|
347
360
|
>,
|
|
348
361
|
): string {
|
|
349
362
|
if (o.status === 'failed')
|
|
350
363
|
return failedLabel(o.exitCode, o.timedOut, o.sandboxViolations, o.notReady, o.inputMoved)
|
|
351
|
-
if (o.status === 'skipped') return skippedLabel(o
|
|
364
|
+
if (o.status === 'skipped') return skippedLabel(o)
|
|
352
365
|
return outcomeWord(o)
|
|
353
366
|
}
|
|
354
367
|
|
|
355
368
|
/**
|
|
356
|
-
* Why a task was skipped, one copy for every surface: `
|
|
357
|
-
*
|
|
369
|
+
* Why a task was skipped, one copy for every surface: `needs darwin-arm64,
|
|
370
|
+
* this is linux-x64` (its `exec.platforms`), `blocked by lib#build` (the
|
|
371
|
+
* task at the root of the block), or '' for fail-fast's skip — the run stopped,
|
|
358
372
|
* nothing blocked it, and a label claiming an upstream failure would lie.
|
|
359
373
|
*/
|
|
360
|
-
export function skippedReason(
|
|
361
|
-
|
|
374
|
+
export function skippedReason(o: SkipView = {}): string {
|
|
375
|
+
if (o.otherPlatform !== undefined)
|
|
376
|
+
return `needs ${o.otherPlatform.needs.join(' or ')}, this is ${o.otherPlatform.host}`
|
|
377
|
+
return o.blockedBy === undefined ? '' : `blocked by ${o.blockedBy}`
|
|
362
378
|
}
|
|
363
379
|
|
|
380
|
+
type SkipView = Pick<OutcomeView, 'blockedBy' | 'otherPlatform'>
|
|
381
|
+
|
|
364
382
|
/** `skipped (blocked by lib#build)`, or a bare `skipped` for fail-fast's. */
|
|
365
|
-
function skippedLabel(
|
|
366
|
-
const why = skippedReason(
|
|
383
|
+
function skippedLabel(o: SkipView): string {
|
|
384
|
+
const why = skippedReason(o)
|
|
367
385
|
return why === '' ? 'skipped' : `skipped (${why})`
|
|
368
386
|
}
|
|
369
387
|
|
|
@@ -448,6 +466,7 @@ export function projectOutcome(outcome: TaskOutcome): OutcomeView {
|
|
|
448
466
|
if (outcome.notReady !== undefined) view.notReady = outcome.notReady
|
|
449
467
|
if (outcome.inputMoved !== undefined) view.inputMoved = outcome.inputMoved
|
|
450
468
|
if (outcome.blockedBy !== undefined) view.blockedBy = outcome.blockedBy
|
|
469
|
+
if (outcome.otherPlatform !== undefined) view.otherPlatform = outcome.otherPlatform
|
|
451
470
|
if (outcome.restored !== undefined) view.restored = outcome.restored
|
|
452
471
|
if (outcome.sandboxViolations !== undefined) view.sandboxViolations = outcome.sandboxViolations
|
|
453
472
|
if (outcome.sandboxViolationLines !== undefined)
|
package/src/report/forecast.ts
CHANGED
|
@@ -17,9 +17,16 @@ export interface LiveForecast {
|
|
|
17
17
|
/**
|
|
18
18
|
* Tasks not started because a dependency has not finished, in the order
|
|
19
19
|
* the scheduler would start them, at most one per worker: what an idle
|
|
20
|
-
* worker row holds (
|
|
20
|
+
* worker row holds (2026-10-15).
|
|
21
21
|
*/
|
|
22
22
|
blocked?: readonly BlockedTask[]
|
|
23
|
+
/**
|
|
24
|
+
* Each running task's count of the tasks not started that depend on it
|
|
25
|
+
* directly (through a group, which runs nothing): its row's `blocked`
|
|
26
|
+
* cell, why they are not running. A task waiting on two running tasks
|
|
27
|
+
* counts under both.
|
|
28
|
+
*/
|
|
29
|
+
blockedBy: ReadonlyMap<string, number>
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/** A task an unfinished dependency holds back, and what holds it: the running task its longest chain waits on, else that dependency. */
|
|
@@ -34,16 +41,13 @@ export type Forecast = (
|
|
|
34
41
|
nowMs: number,
|
|
35
42
|
) => LiveForecast
|
|
36
43
|
|
|
37
|
-
/** How far past its p50 a running task may go before the forecast gives up. */
|
|
38
|
-
const OVERRUN = 1.25
|
|
39
|
-
|
|
40
44
|
/**
|
|
41
45
|
* The later of the longest unfinished chain and the unfinished work spread
|
|
42
46
|
* over the workers. A task still to run costs its p50 (a pending task may
|
|
43
47
|
* yet hit, so this leans long, and the hits finish first); a running one
|
|
44
48
|
* costs what its p50 has left, never below zero; a group or a task with no
|
|
45
|
-
* history costs nothing.
|
|
46
|
-
*
|
|
49
|
+
* history costs nothing. Zero while tasks still run means the run is past
|
|
50
|
+
* its estimate: the live region counts the time since (2026-10-27).
|
|
47
51
|
*/
|
|
48
52
|
export function createForecast(
|
|
49
53
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
@@ -84,6 +88,22 @@ export function createForecast(
|
|
|
84
88
|
// graph order: once per run, as the scheduler computes it.
|
|
85
89
|
const rank = computeReverseDepCount(nodes)
|
|
86
90
|
const position = new Map(order.map((n, i) => [n.id, i]))
|
|
91
|
+
// The tasks each node waits on directly, a group's own looked through.
|
|
92
|
+
const direct = new Map<string, readonly string[]>()
|
|
93
|
+
const through = (id: string, into: Set<string>, seen: Set<string>): void => {
|
|
94
|
+
if (seen.has(id)) return
|
|
95
|
+
seen.add(id)
|
|
96
|
+
const d = nodes.get(id)
|
|
97
|
+
if (d === undefined || !isGroupTask(d)) into.add(id)
|
|
98
|
+
else for (const x of d.deps) through(x, into, seen)
|
|
99
|
+
}
|
|
100
|
+
for (const n of order) {
|
|
101
|
+
if (isGroupTask(n) || n.deps.length === 0) continue
|
|
102
|
+
const into = new Set<string>()
|
|
103
|
+
const seen = new Set<string>()
|
|
104
|
+
for (const d of n.deps) through(d, into, seen)
|
|
105
|
+
direct.set(n.id, [...into])
|
|
106
|
+
}
|
|
87
107
|
const before = (a: string, b: string): boolean => {
|
|
88
108
|
const d = (rank.get(a) ?? 0) - (rank.get(b) ?? 0)
|
|
89
109
|
return d !== 0 ? d > 0 : position.get(a)! < position.get(b)!
|
|
@@ -103,9 +123,9 @@ export function createForecast(
|
|
|
103
123
|
let chainEnd: string | undefined
|
|
104
124
|
let work = 0
|
|
105
125
|
let known = false
|
|
106
|
-
let overrun = false
|
|
107
126
|
// The best `workers` blocked tasks, kept sorted as the pass meets them.
|
|
108
127
|
const held: BlockedTask[] = []
|
|
128
|
+
const blockedBy = new Map<string, number>()
|
|
109
129
|
for (const n of order) {
|
|
110
130
|
if (finished.has(n.id)) continue
|
|
111
131
|
let start = 0
|
|
@@ -124,6 +144,8 @@ export function createForecast(
|
|
|
124
144
|
since === undefined ? (head.get(n.id) ?? n.deps.find((d) => end.has(d))) : undefined
|
|
125
145
|
if (by !== undefined && !isGroupTask(n)) {
|
|
126
146
|
waiting++
|
|
147
|
+
for (const on of direct.get(n.id) ?? [])
|
|
148
|
+
if (running.has(on)) blockedBy.set(on, (blockedBy.get(on) ?? 0) + 1)
|
|
127
149
|
let at = held.length
|
|
128
150
|
while (at > 0 && before(n.id, held[at - 1]!.id)) at--
|
|
129
151
|
if (at < workers) {
|
|
@@ -135,7 +157,6 @@ export function createForecast(
|
|
|
135
157
|
let own = 0
|
|
136
158
|
if (p !== undefined) {
|
|
137
159
|
known = true
|
|
138
|
-
if (since !== undefined && nowMs - since > p * OVERRUN) overrun = true
|
|
139
160
|
own = since === undefined ? p : Math.max(0, p - (nowMs - since))
|
|
140
161
|
}
|
|
141
162
|
const e = start + own
|
|
@@ -146,7 +167,7 @@ export function createForecast(
|
|
|
146
167
|
}
|
|
147
168
|
work += own
|
|
148
169
|
}
|
|
149
|
-
if (!known
|
|
170
|
+
if (!known) return { waiting, expected, blocked: held, blockedBy }
|
|
150
171
|
const critical = chainEnd === undefined ? undefined : head.get(chainEnd)
|
|
151
172
|
return {
|
|
152
173
|
etaMs: Math.max(chain, work / workers),
|
|
@@ -154,6 +175,7 @@ export function createForecast(
|
|
|
154
175
|
waiting,
|
|
155
176
|
expected,
|
|
156
177
|
blocked: held,
|
|
178
|
+
blockedBy,
|
|
157
179
|
}
|
|
158
180
|
}
|
|
159
181
|
}
|
|
@@ -32,22 +32,35 @@ import {
|
|
|
32
32
|
identityColor,
|
|
33
33
|
maskedCommand,
|
|
34
34
|
paint,
|
|
35
|
+
printable,
|
|
36
|
+
ruleWidth,
|
|
35
37
|
ui,
|
|
36
38
|
type ColorSupport,
|
|
37
39
|
} from '../util/index.js'
|
|
38
40
|
import { neverStarted } from './summary.js'
|
|
39
|
-
import { outcomeLabel, ranNoCache, skippedReason } from './events.js'
|
|
41
|
+
import { outcomeLabel, outcomeWord, ranNoCache, skippedReason } from './events.js'
|
|
40
42
|
import { fileMemo, linkPaths } from './path-links.js'
|
|
41
43
|
|
|
42
44
|
const NO_COLOR: ColorSupport = { enabled: false }
|
|
45
|
+
// Ends any colour a task's output left on, so vx's own lines below never
|
|
46
|
+
// take it (a task killed mid-write leaves one open).
|
|
47
|
+
export const RESET = '\x1b[0m'
|
|
48
|
+
|
|
49
|
+
/** Whether `text`'s last colour code sets a style rather than resetting it. */
|
|
50
|
+
export function leavesStyle(text: string): boolean {
|
|
51
|
+
const at = text.lastIndexOf('\x1b[')
|
|
52
|
+
if (at < 0) return false
|
|
53
|
+
const sgr = /^\[([\d;]*)m/.exec(text.slice(at + 1, at + 64))
|
|
54
|
+
// Not a colour code (a cursor move): look no further, as a reset costs nothing.
|
|
55
|
+
if (sgr === null) return true
|
|
56
|
+
return sgr[1] !== '' && sgr[1] !== '0'
|
|
57
|
+
}
|
|
43
58
|
|
|
44
59
|
const ACCENT = '#06b6d4' // cyan-500 — bullets, remote-hit hint
|
|
45
60
|
// Identity hues (`identityColor`) are outside the status palette (green /
|
|
46
61
|
// red / yellow / cyan) so a task id can never read as an outcome. Each
|
|
47
62
|
// half of an id hashes its own name: same project, same hue; same target,
|
|
48
63
|
// same hue, in every run, list and region row.
|
|
49
|
-
// Section rules + the summary rule share one frame width.
|
|
50
|
-
const FRAME_WIDTH = 60
|
|
51
64
|
|
|
52
65
|
const SUCCESS = '#22c55e' // green-500 — success / fresh
|
|
53
66
|
const WARN = '#eab308' // yellow-500 — skipped
|
|
@@ -99,8 +112,11 @@ export function formatTaskBlock(
|
|
|
99
112
|
|
|
100
113
|
const idPainted = paintTaskId(node, colors, { bold: true })
|
|
101
114
|
const corner = (s: string) => paint('', s, colors, { dim: true })
|
|
102
|
-
|
|
103
|
-
const
|
|
115
|
+
// A ready server's frame reads as the live one does: marked, and running.
|
|
116
|
+
const mark = persistentMark(node, colors)
|
|
117
|
+
const running = mark !== '' && outcome.status === 'success' && outcome.serverExit === undefined
|
|
118
|
+
const header = running ? paint(ACCENT, 'running', colors) : formatBlockHeader(outcome, colors)
|
|
119
|
+
const lines: string[] = [`${corner('┌─')} ${mark}${idPainted} ${corner('>')} ${header}`]
|
|
104
120
|
|
|
105
121
|
// The command section shows what actually ran — executed tasks
|
|
106
122
|
// (success and failed). Cache hits normally skip it (the stored
|
|
@@ -126,11 +142,18 @@ export function formatTaskBlock(
|
|
|
126
142
|
lines.push(...violationSection(outcome, colors))
|
|
127
143
|
|
|
128
144
|
lines.push(
|
|
129
|
-
`${corner('└─')} ${idPainted}${whereNote(outcome, colors)} ${corner('──')}${formatBlockFooter(outcome, colors)}`,
|
|
145
|
+
`${corner('└─')} ${mark}${idPainted}${whereNote(outcome, colors)} ${corner('──')}${running ? runningFooter(outcome, colors) : formatBlockFooter(outcome, colors)}`,
|
|
130
146
|
)
|
|
131
147
|
return lines.join('\n') + '\n'
|
|
132
148
|
}
|
|
133
149
|
|
|
150
|
+
/** What a bounded capture dropped from its head, so a capped log never reads as whole. */
|
|
151
|
+
function droppedNote(lost: number, colors: ColorSupport): string {
|
|
152
|
+
return paint('', `… ${lost} earlier character${lost === 1 ? '' : 's'} dropped`, colors, {
|
|
153
|
+
dim: true,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
134
157
|
/**
|
|
135
158
|
* One `├─ <LABEL>` section of captured output, with the head-dropped notice
|
|
136
159
|
* when the capture was bounded. Shared by the task frame and the persistent
|
|
@@ -147,9 +170,10 @@ function pushStreamSection(
|
|
|
147
170
|
if (text.trim().length === 0) return
|
|
148
171
|
lines.push(sectionLine(title, color, colors), '')
|
|
149
172
|
if (lost > 0) {
|
|
150
|
-
lines.push(
|
|
173
|
+
lines.push(droppedNote(lost, colors))
|
|
151
174
|
}
|
|
152
175
|
pushBodyLines(lines, text)
|
|
176
|
+
if (leavesStyle(text)) lines.push(`${lines.pop()}${RESET}`)
|
|
153
177
|
lines.push('')
|
|
154
178
|
}
|
|
155
179
|
|
|
@@ -171,7 +195,7 @@ function pushOutputSection(
|
|
|
171
195
|
if (output.every((c) => c.text.trim().length === 0)) return
|
|
172
196
|
lines.push(sectionLine('OUTPUT', '', colors), '')
|
|
173
197
|
if (lost > 0) {
|
|
174
|
-
lines.push(
|
|
198
|
+
lines.push(droppedNote(lost, colors))
|
|
175
199
|
}
|
|
176
200
|
const body: Array<{ text: string; err: boolean }> = []
|
|
177
201
|
let open: { parts: string[]; err: boolean } | null = null
|
|
@@ -194,6 +218,8 @@ function pushOutputSection(
|
|
|
194
218
|
if (!l.err || !colors.enabled) lines.push(text)
|
|
195
219
|
else lines.push(l.text.includes('\x1b') ? text : paint(ERROR, text, colors))
|
|
196
220
|
}
|
|
221
|
+
const styled = output.findLast((c) => c.text.includes('\x1b['))
|
|
222
|
+
if (styled !== undefined && leavesStyle(styled.text)) lines.push(`${lines.pop()}${RESET}`)
|
|
197
223
|
lines.push('')
|
|
198
224
|
}
|
|
199
225
|
|
|
@@ -207,8 +233,8 @@ function pushOutputSection(
|
|
|
207
233
|
function sectionLine(title: string, color: string, colors: ColorSupport): string {
|
|
208
234
|
const corner = (s: string) => paint('', s, colors, { dim: true })
|
|
209
235
|
const label = paint(color, title, colors, { bold: true })
|
|
210
|
-
|
|
211
|
-
const rule = corner('─'.repeat(Math.max(1,
|
|
236
|
+
// As wide as the footer's rule, so the frames and the footer line up.
|
|
237
|
+
const rule = corner('─'.repeat(Math.max(1, ruleWidth(colors) - 4 - title.length)))
|
|
212
238
|
return `${corner('├─')} ${label} ${rule}`
|
|
213
239
|
}
|
|
214
240
|
|
|
@@ -263,13 +289,18 @@ function paintTaskId(node: TaskNode, colors: ColorSupport, opts: { bold?: boolea
|
|
|
263
289
|
// the two axes out, each in its own color. Time is right-aligned in a
|
|
264
290
|
// fixed cell so durations line up and a ticking elapsed never shifts
|
|
265
291
|
// the row. All detail (exit code, output) lives in the framed block.
|
|
266
|
-
export const TIME_COL = 7 // "0ms" … "999.99s"
|
|
292
|
+
export const TIME_COL = 7 // "0ms" … "999.99s", then "16m 40s" … "99m 59s"
|
|
267
293
|
const STATUS_COL = 7 // "success" / "running" / "skipped" / "aborted" / "not run"
|
|
268
294
|
const CACHE_COL = 8 // "no-cache" / "remote" / "local" / "fresh" / "miss"
|
|
295
|
+
const BLOCKED_COL = 7 // the live list's count of tasks a running one holds back
|
|
296
|
+
/** The list header's colour: violet-400, the summary rule's first stop. */
|
|
297
|
+
const HEADER = '#a78bfa'
|
|
269
298
|
|
|
270
299
|
/** Right-align the duration in a TIME_COL cell (pad on the left). */
|
|
271
300
|
function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
272
|
-
|
|
301
|
+
let raw = ms === null ? '' : formatElapsed(ms)
|
|
302
|
+
// `1000.00s` is wider than the cell; minutes fit it up to `99m 59s`.
|
|
303
|
+
if (ms !== null && raw.length > TIME_COL) raw = formatElapsed(ms, true)
|
|
273
304
|
return ' '.repeat(Math.max(0, TIME_COL - raw.length)) + paint('', raw, colors, { dim: true })
|
|
274
305
|
}
|
|
275
306
|
|
|
@@ -358,20 +389,32 @@ export function formatTaskRow(
|
|
|
358
389
|
cacheColor: string,
|
|
359
390
|
paintedId: string,
|
|
360
391
|
colors: ColorSupport = NO_COLOR,
|
|
392
|
+
/** The live list's `blocked` cell, right-aligned before the id; absent off that list. */
|
|
393
|
+
blocked?: string,
|
|
361
394
|
): string {
|
|
362
395
|
const st = cell(status, statusColor, STATUS_COL, colors)
|
|
363
396
|
// `aborting` runs one into the cache cell, which is blank beside it.
|
|
364
397
|
const ca = cell(cache, cacheColor, CACHE_COL - Math.max(0, status.length - STATUS_COL), colors)
|
|
365
|
-
|
|
398
|
+
const bl =
|
|
399
|
+
blocked === undefined
|
|
400
|
+
? ''
|
|
401
|
+
: `${' '.repeat(Math.max(0, BLOCKED_COL - blocked.length))}${paint(WARN, blocked, colors)} `
|
|
402
|
+
return ` ${glyph} ${timeCell(ms, colors)} ${st} ${ca} ${bl}${paintedId}`
|
|
366
403
|
}
|
|
367
404
|
|
|
368
405
|
/**
|
|
369
|
-
* The
|
|
370
|
-
*
|
|
406
|
+
* The header over a task list, on the row grid: bold and coloured, so it
|
|
407
|
+
* reads apart from the rows (dim, it was lost among them). `worker` when an
|
|
408
|
+
* executor places tasks: the name after the id's dot. `blocked` on the live
|
|
409
|
+
* list, whose rows count the tasks each running one holds back.
|
|
371
410
|
*/
|
|
372
|
-
export function formatListHeader(
|
|
373
|
-
|
|
374
|
-
|
|
411
|
+
export function formatListHeader(
|
|
412
|
+
worker: boolean,
|
|
413
|
+
colors: ColorSupport = NO_COLOR,
|
|
414
|
+
blocked = false,
|
|
415
|
+
): string {
|
|
416
|
+
const head = ` ${'time'.padStart(TIME_COL)} ${'status'.padEnd(STATUS_COL)} ${'cache'.padEnd(CACHE_COL)} ${blocked ? `${'blocked'.padStart(BLOCKED_COL)} ` : ''}task${worker ? ' \u00b7 worker' : ''}`
|
|
417
|
+
return paint(HEADER, head, colors, { bold: true })
|
|
375
418
|
}
|
|
376
419
|
|
|
377
420
|
/** Compact one-liner for a cache hit with nothing to replay. */
|
|
@@ -384,6 +427,21 @@ export function formatTaskHitLine(
|
|
|
384
427
|
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath)
|
|
385
428
|
}
|
|
386
429
|
|
|
430
|
+
/**
|
|
431
|
+
* The `--output-logs hash-only` line: outcome word, task id, cache key —
|
|
432
|
+
* absent for a task that never computed one (skipped, uncacheable). The
|
|
433
|
+
* word in its outcome's colour, the key dim.
|
|
434
|
+
*/
|
|
435
|
+
export function formatTaskHashLine(
|
|
436
|
+
node: TaskNode,
|
|
437
|
+
o: TaskOutcome,
|
|
438
|
+
colors: ColorSupport = NO_COLOR,
|
|
439
|
+
): string {
|
|
440
|
+
const color = o.status === 'aborted' ? ABORTED : statusOf(o).color
|
|
441
|
+
const key = o.hash === undefined ? '' : ` ${paint('', o.hash, colors, { dim: true })}`
|
|
442
|
+
return `${paint(color, outcomeWord(o), colors)} ${paintTaskId(node, colors)}${key}`
|
|
443
|
+
}
|
|
444
|
+
|
|
387
445
|
/** Compact one-liner for an executed task in broad mode. */
|
|
388
446
|
export function formatTaskExecutedLine(
|
|
389
447
|
node: TaskNode,
|
|
@@ -423,17 +481,17 @@ function formatOutcomeRow(
|
|
|
423
481
|
|
|
424
482
|
/** `critical path`: the row's task held the run's end while others ran beside it. */
|
|
425
483
|
export function pathNote(colors: ColorSupport = NO_COLOR): string {
|
|
426
|
-
return ` ${paint(
|
|
484
|
+
return ` ${paint(ERROR, 'critical path', colors)}`
|
|
427
485
|
}
|
|
428
486
|
|
|
429
|
-
/** Where an executor ran the task, when not here, right after its id: `@small#2` (
|
|
487
|
+
/** Where an executor ran the task, when not here, right after its id: `@small#2` (2026-10-21). */
|
|
430
488
|
export function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
|
|
431
489
|
return o.where === undefined ? '' : placeNote(o.where, colors)
|
|
432
490
|
}
|
|
433
491
|
|
|
434
492
|
/**
|
|
435
493
|
* ` · <where>` after the id: the dot dim, the place in its own hue. Last
|
|
436
|
-
* on the row, as
|
|
494
|
+
* on the row, as a worker's name has no width to pad to (2026-10-21).
|
|
437
495
|
*/
|
|
438
496
|
export function placeNote(where: string, colors: ColorSupport = NO_COLOR): string {
|
|
439
497
|
return `${paint('', ' \u00b7', colors, { dim: true })} ${ui.inline(ui.place(where), colors)}`
|
|
@@ -465,9 +523,9 @@ export function flakyNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): stri
|
|
|
465
523
|
export function formatTaskSkippedLine(
|
|
466
524
|
node: TaskNode,
|
|
467
525
|
colors: ColorSupport = NO_COLOR,
|
|
468
|
-
|
|
526
|
+
skip?: Parameters<typeof skippedReason>[0],
|
|
469
527
|
): string {
|
|
470
|
-
const why = skippedReason(
|
|
528
|
+
const why = skippedReason(skip)
|
|
471
529
|
const suffix = why === '' ? '' : ` ${paint('', `• ${why}`, colors, { dim: true })}`
|
|
472
530
|
return (
|
|
473
531
|
formatTaskRow(
|
|
@@ -532,7 +590,7 @@ export function formatFrameOpen(
|
|
|
532
590
|
): string {
|
|
533
591
|
const corner = (t: string) => paint('', t, colors, { dim: true })
|
|
534
592
|
const cmd = shownCommand(node, forwardArgs)
|
|
535
|
-
const mark =
|
|
593
|
+
const mark = persistentMark(node, colors)
|
|
536
594
|
return `${corner('┌─')} ${mark}${paintTaskId(node, colors, { bold: true })} ${corner('>')} $ ${cmd}`
|
|
537
595
|
}
|
|
538
596
|
|
|
@@ -541,8 +599,8 @@ export function formatFrameOpen(
|
|
|
541
599
|
* sees them in context with the failing task, not as loose status output
|
|
542
600
|
* above the box. Shared by BOTH renderers: the focused/live frame used to
|
|
543
601
|
* omit it, so a single-task run — exactly how someone debugging one task
|
|
544
|
-
* invokes vx — showed a failure with no violations at all
|
|
545
|
-
* 2026-09-05).
|
|
602
|
+
* invokes vx — showed a failure with no violations at all
|
|
603
|
+
* (2026-09-05).
|
|
546
604
|
*/
|
|
547
605
|
function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[] {
|
|
548
606
|
const vlines = outcome.sandboxViolationLines
|
|
@@ -552,11 +610,13 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
552
610
|
// VERBATIM: a violation is evidence, and evidence that has been rewritten
|
|
553
611
|
// for display is not the thing the sandbox reported. The outcome keeps
|
|
554
612
|
// every record for telemetry.
|
|
555
|
-
|
|
613
|
+
// Verbatim but for control bytes: a denied path is anyone's bytes, and
|
|
614
|
+
// one named `ESC ]0;…` retitled the terminal (the `\xNN` is still exact).
|
|
615
|
+
const body = [...new Set(vlines)].map(printable)
|
|
556
616
|
// Red, not amber: a violation fails the task, so it reads as a failure.
|
|
557
617
|
// vx's note, after the evidence: the grants that would have allowed it.
|
|
558
618
|
// A read the output depends on is a key input too; the sandbox never
|
|
559
|
-
// adds one (
|
|
619
|
+
// adds one (2026-10-09), so the note names the reads no input
|
|
560
620
|
// glob matches and the user decides.
|
|
561
621
|
const fix = outcome.sandboxFix
|
|
562
622
|
const line = fix === undefined ? undefined : sandboxFixLine(fix)
|
|
@@ -569,12 +629,16 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
569
629
|
...(fix.inputs !== undefined
|
|
570
630
|
? [
|
|
571
631
|
dim(
|
|
572
|
-
`and, if the output depends on them, inputs.files: [${fix.inputs.map((p) => `'${p}'`).join(', ')}]`,
|
|
632
|
+
`and, if the output depends on them, inputs.files: [${fix.inputs.map((p) => `'${printable(p)}'`).join(', ')}]`,
|
|
573
633
|
),
|
|
574
634
|
]
|
|
575
635
|
: []),
|
|
576
636
|
...(fix.withheld !== undefined
|
|
577
|
-
? [
|
|
637
|
+
? [
|
|
638
|
+
dim(
|
|
639
|
+
`credential path, grant it only on purpose: ${fix.withheld.map(printable).join(', ')}`,
|
|
640
|
+
),
|
|
641
|
+
]
|
|
578
642
|
: []),
|
|
579
643
|
'',
|
|
580
644
|
]
|
|
@@ -595,14 +659,13 @@ export function formatFrameClose(
|
|
|
595
659
|
colors: ColorSupport = NO_COLOR,
|
|
596
660
|
): string {
|
|
597
661
|
const corner = (t: string) => paint('', t, colors, { dim: true })
|
|
598
|
-
const
|
|
599
|
-
const mark = persistent ? `${paint(ACCENT, '▸', colors)} ` : ''
|
|
662
|
+
const mark = persistentMark(node, colors)
|
|
600
663
|
// A persistent task's child keeps running past this "ready" outcome,
|
|
601
664
|
// so the close reads `running` (accent), not `success` — the ▸ marks
|
|
602
665
|
// both ends of the frame as a long-lived task.
|
|
603
666
|
const tail =
|
|
604
|
-
|
|
605
|
-
?
|
|
667
|
+
mark !== '' && outcome.status === 'success' && outcome.serverExit === undefined
|
|
668
|
+
? runningFooter(outcome, colors)
|
|
606
669
|
: formatBlockFooter(outcome, colors)
|
|
607
670
|
const close = `${corner('└─')} ${mark}${paintTaskId(node, colors, { bold: true })}${whereNote(outcome, colors)} ${corner('──')}${tail}`
|
|
608
671
|
const violations = violationSection(outcome, colors)
|
|
@@ -618,8 +681,12 @@ function shownCommand(node: TaskNode, forwardArgs: readonly string[]): string {
|
|
|
618
681
|
)
|
|
619
682
|
}
|
|
620
683
|
|
|
621
|
-
function
|
|
622
|
-
return node.config.exec?.persistent !== undefined
|
|
684
|
+
function persistentMark(node: TaskNode, colors: ColorSupport): string {
|
|
685
|
+
return node.config.exec?.persistent !== undefined ? `${paint(ACCENT, '▸', colors)} ` : ''
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function runningFooter(o: TaskOutcome, colors: ColorSupport): string {
|
|
689
|
+
return ` ${paint('', `(${formatElapsed(o.durationMs)})`, colors, { dim: true })} ${paint(ACCENT, 'running', colors)}`
|
|
623
690
|
}
|
|
624
691
|
|
|
625
692
|
/**
|
|
@@ -661,10 +728,37 @@ export function formatKeptLines(node: TaskNode, text: string, colors: ColorSuppo
|
|
|
661
728
|
const prefix = `${paintTaskId(node, colors)} ${paint('', '│', colors, { dim: true })} `
|
|
662
729
|
return text
|
|
663
730
|
.split('\n')
|
|
664
|
-
.map((line) =>
|
|
731
|
+
.map((line) => {
|
|
732
|
+
const shown = lastRedraw(line)
|
|
733
|
+
return `${prefix}${shown}${leavesStyle(shown) ? RESET : ''}\n`
|
|
734
|
+
})
|
|
665
735
|
.join('')
|
|
666
736
|
}
|
|
667
737
|
|
|
738
|
+
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
739
|
+
const LINE_RESET = /\r|\x1b\[[12]K|\x1b\[[01]?G/g
|
|
740
|
+
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
741
|
+
const TRAILING_RESETS = /(?:\r|\x1b\[[12]K|\x1b\[[01]?G)+$/
|
|
742
|
+
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
743
|
+
const SGR = /\x1b\[[0-9;]*m/g
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* What a line shows once its redraws have played: the text after its last
|
|
747
|
+
* `\r`, line erase (`CSI 1K`/`2K`) or move to column 1 (`CSI G`), with the
|
|
748
|
+
* colours set before it. Written after an id prefix, a dev server's redraw
|
|
749
|
+
* (`\x1b[2K\x1b[1Gbuilt in 30ms`, a `10%\r100%` progress line) erased or
|
|
750
|
+
* overwrote the prefix on a terminal. A reset that ends the line, as in
|
|
751
|
+
* CRLF, redraws nothing.
|
|
752
|
+
*/
|
|
753
|
+
function lastRedraw(line: string): string {
|
|
754
|
+
if (!line.includes('\r') && !line.includes('\x1b')) return line
|
|
755
|
+
const text = line.replace(TRAILING_RESETS, '')
|
|
756
|
+
let cut = -1
|
|
757
|
+
for (const m of text.matchAll(LINE_RESET)) cut = m.index + m[0].length
|
|
758
|
+
if (cut === -1) return text
|
|
759
|
+
return (text.slice(0, cut).match(SGR) ?? []).join('') + text.slice(cut)
|
|
760
|
+
}
|
|
761
|
+
|
|
668
762
|
/**
|
|
669
763
|
* Between the requested task's frame and the summary footer, list the
|
|
670
764
|
* persistent tasks (dev servers / watchers) the run is keeping alive in
|
|
@@ -698,6 +792,8 @@ function formatBlockHeader(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
698
792
|
return paint(ERROR, word, colors, { bold: true })
|
|
699
793
|
case 'skipped':
|
|
700
794
|
return paint(WARN, word, colors)
|
|
795
|
+
case 'aborted':
|
|
796
|
+
return paint(ABORTED, word, colors)
|
|
701
797
|
default:
|
|
702
798
|
return dim(word)
|
|
703
799
|
}
|
|
@@ -723,12 +819,10 @@ function formatStatusTag(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
723
819
|
return paint(ERROR, word, colors, { bold: true })
|
|
724
820
|
case 'skipped':
|
|
725
821
|
return paint(WARN, word, colors)
|
|
726
|
-
case '
|
|
727
|
-
|
|
728
|
-
case 'success':
|
|
729
|
-
return paint('', word, colors, { dim: true })
|
|
822
|
+
case 'aborted':
|
|
823
|
+
return paint(ABORTED, word, colors)
|
|
730
824
|
default:
|
|
731
|
-
return
|
|
825
|
+
return paint('', word, colors, { dim: true })
|
|
732
826
|
}
|
|
733
827
|
}
|
|
734
828
|
|
|
@@ -736,14 +830,11 @@ function formatStatusTag(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
736
830
|
* The fix as the config line it is: `exec.sandbox.allow: { read: ['a'] }`,
|
|
737
831
|
* or undefined when every denial was a withheld credential path.
|
|
738
832
|
*/
|
|
739
|
-
function sandboxFixLine({
|
|
740
|
-
withheld
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (Object.keys(fix).length === 0) return undefined
|
|
745
|
-
const parts = Object.entries(fix).map(
|
|
746
|
-
([k, list]: [string, string[]]) => `${k}: [${list.map((p) => `'${p}'`).join(', ')}]`,
|
|
833
|
+
function sandboxFixLine(f: NonNullable<TaskOutcome['sandboxFix']>): string | undefined {
|
|
834
|
+
const fix = Object.entries(f).filter(([k]) => k !== 'withheld' && k !== 'inputs')
|
|
835
|
+
if (fix.length === 0) return undefined
|
|
836
|
+
const parts = fix.map(
|
|
837
|
+
([k, list]: [string, string[]]) => `${k}: [${list.map((p) => `'${printable(p)}'`).join(', ')}]`,
|
|
747
838
|
)
|
|
748
839
|
return `exec.sandbox.allow: { ${parts.join(', ')} }`
|
|
749
840
|
}
|