boxdown 1.2.1 → 1.3.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 +10 -0
- package/assets/devcontainer/README.md +12 -1
- package/assets/devcontainer/devcontainer.json +1 -1
- package/assets/devcontainer/hooks/post-create.sh +12 -1
- package/dist/bin/cli.cjs +1 -1
- package/dist/bin/cli.mjs +1 -1
- package/dist/{main-BDgyf2t5.cjs → main-CT2n9qcb.cjs} +448 -67
- package/dist/{main-J4_2Up3o.mjs → main-O__JaiXe.mjs} +431 -68
- package/dist/main-O__JaiXe.mjs.map +1 -0
- package/dist/main.cjs +4 -1
- package/dist/main.d.cts +92 -26
- package/dist/main.d.cts.map +1 -1
- package/dist/main.d.mts +92 -26
- package/dist/main.d.mts.map +1 -1
- package/dist/main.mjs +2 -2
- package/docs/features/lifecycle.md +13 -0
- package/docs/features/setup.md +5 -0
- package/docs/features/start-and-shell.md +5 -0
- package/docs/superpowers/plans/2026-07-18-architecture-aware-1password-installer.md +163 -0
- package/docs/superpowers/plans/2026-07-18-devcontainer-node-image-digest.md +341 -0
- package/docs/superpowers/plans/2026-07-19-container-runtime-readiness.md +1536 -0
- package/docs/superpowers/specs/2026-07-18-architecture-aware-1password-installer-design.md +63 -0
- package/docs/superpowers/specs/2026-07-18-devcontainer-node-image-digest-design.md +108 -0
- package/docs/superpowers/specs/2026-07-19-container-runtime-readiness-design.md +353 -0
- package/package.json +1 -1
- package/src/container-runtime.ts +295 -0
- package/src/devcontainer.ts +20 -4
- package/src/doctor.ts +48 -22
- package/src/main.ts +95 -11
- package/src/process.ts +50 -10
- package/src/progress.ts +63 -8
- package/dist/main-J4_2Up3o.mjs.map +0 -1
package/src/process.ts
CHANGED
|
@@ -12,12 +12,25 @@ export interface BufferedCommandOptions {
|
|
|
12
12
|
logger?: WorkspaceCommandLogger
|
|
13
13
|
onStdout?: (chunk: Buffer) => void
|
|
14
14
|
onStderr?: (chunk: Buffer) => void
|
|
15
|
+
timeoutMs?: number
|
|
16
|
+
timeoutControl?: BufferedTimeoutControl
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface CommandResult {
|
|
18
20
|
code: number
|
|
19
21
|
stdout: string
|
|
20
22
|
stderr: string
|
|
23
|
+
timedOut?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BufferedTimeoutControl {
|
|
27
|
+
schedule: (callback: () => void, milliseconds: number) => unknown
|
|
28
|
+
cancel: (handle: unknown) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const realTimeoutControl: BufferedTimeoutControl = {
|
|
32
|
+
schedule: (callback, milliseconds) => setTimeout(callback, milliseconds),
|
|
33
|
+
cancel: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>)
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
function mergedEnv (env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
@@ -84,6 +97,10 @@ function writeChunk (target: 'stdout' | 'stderr' | false, chunk: Buffer): void {
|
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
export function runBuffered (command: string, args: string[], options: BufferedCommandOptions = {}): Promise<CommandResult> {
|
|
100
|
+
if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0)) {
|
|
101
|
+
throw new Error('timeoutMs must be a finite non-negative number')
|
|
102
|
+
}
|
|
103
|
+
|
|
87
104
|
return new Promise((resolve) => {
|
|
88
105
|
const loggedCommand = options.logger?.startCommand(command, args, { cwd: options.cwd })
|
|
89
106
|
const child = spawn(command, args, {
|
|
@@ -94,8 +111,13 @@ export function runBuffered (command: string, args: string[], options: BufferedC
|
|
|
94
111
|
|
|
95
112
|
const stdoutChunks: Buffer[] = []
|
|
96
113
|
const stderrChunks: Buffer[] = []
|
|
114
|
+
let settled = false
|
|
115
|
+
let timeoutHandle: unknown
|
|
116
|
+
let timeoutScheduled = false
|
|
117
|
+
const timeoutControl = options.timeoutControl ?? realTimeoutControl
|
|
97
118
|
|
|
98
119
|
child.stdout.on('data', (chunk: Buffer) => {
|
|
120
|
+
if (settled) return
|
|
99
121
|
stdoutChunks.push(chunk)
|
|
100
122
|
loggedCommand?.stream('stdout', chunk)
|
|
101
123
|
options.onStdout?.(chunk)
|
|
@@ -103,20 +125,18 @@ export function runBuffered (command: string, args: string[], options: BufferedC
|
|
|
103
125
|
})
|
|
104
126
|
|
|
105
127
|
child.stderr.on('data', (chunk: Buffer) => {
|
|
128
|
+
if (settled) return
|
|
106
129
|
stderrChunks.push(chunk)
|
|
107
130
|
loggedCommand?.stream('stderr', chunk)
|
|
108
131
|
options.onStderr?.(chunk)
|
|
109
132
|
writeChunk(options.mirrorStderr ?? 'stderr', chunk)
|
|
110
133
|
})
|
|
111
134
|
|
|
112
|
-
let resolved = false
|
|
113
|
-
|
|
114
135
|
child.on('error', (error) => {
|
|
115
|
-
if (
|
|
116
|
-
return
|
|
117
|
-
}
|
|
136
|
+
if (settled) return
|
|
118
137
|
|
|
119
|
-
|
|
138
|
+
settled = true
|
|
139
|
+
if (timeoutScheduled) timeoutControl.cancel(timeoutHandle)
|
|
120
140
|
loggedCommand?.error(error)
|
|
121
141
|
loggedCommand?.finish(127)
|
|
122
142
|
resolve({
|
|
@@ -127,11 +147,10 @@ export function runBuffered (command: string, args: string[], options: BufferedC
|
|
|
127
147
|
})
|
|
128
148
|
|
|
129
149
|
child.on('close', (code) => {
|
|
130
|
-
if (
|
|
131
|
-
return
|
|
132
|
-
}
|
|
150
|
+
if (settled) return
|
|
133
151
|
|
|
134
|
-
|
|
152
|
+
settled = true
|
|
153
|
+
if (timeoutScheduled) timeoutControl.cancel(timeoutHandle)
|
|
135
154
|
loggedCommand?.finish(code ?? 1)
|
|
136
155
|
resolve({
|
|
137
156
|
code: code ?? 1,
|
|
@@ -140,6 +159,27 @@ export function runBuffered (command: string, args: string[], options: BufferedC
|
|
|
140
159
|
})
|
|
141
160
|
})
|
|
142
161
|
|
|
162
|
+
if (options.timeoutMs !== undefined) {
|
|
163
|
+
timeoutHandle = timeoutControl.schedule(() => {
|
|
164
|
+
if (settled) return
|
|
165
|
+
|
|
166
|
+
settled = true
|
|
167
|
+
const message = `Command timed out after ${options.timeoutMs as number} milliseconds.`
|
|
168
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8')
|
|
169
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8')
|
|
170
|
+
loggedCommand?.error(new Error(message))
|
|
171
|
+
loggedCommand?.finish(124)
|
|
172
|
+
child.kill('SIGKILL')
|
|
173
|
+
resolve({
|
|
174
|
+
code: 124,
|
|
175
|
+
stdout,
|
|
176
|
+
stderr: `${stderr}${stderr.length > 0 && !stderr.endsWith('\n') ? '\n' : ''}${message}\n`,
|
|
177
|
+
timedOut: true
|
|
178
|
+
})
|
|
179
|
+
}, options.timeoutMs)
|
|
180
|
+
timeoutScheduled = true
|
|
181
|
+
}
|
|
182
|
+
|
|
143
183
|
if (options.input !== undefined) {
|
|
144
184
|
child.stdin.end(options.input)
|
|
145
185
|
} else {
|
package/src/progress.ts
CHANGED
|
@@ -178,6 +178,16 @@ export class ProgressReporter {
|
|
|
178
178
|
this.#writeLine(`${promptRail()} ${color(message, 'dim')}`)
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
status (message: string): void {
|
|
182
|
+
if (this.mode === 'none') return
|
|
183
|
+
if (this.mode === 'verbose') {
|
|
184
|
+
this.#write(this.target, message)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.#writeInteractiveLine(`${promptRail()} ${color(message, 'dim')}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
181
191
|
warn (message: string): void {
|
|
182
192
|
if (this.mode === 'none') {
|
|
183
193
|
return
|
|
@@ -188,7 +198,7 @@ export class ProgressReporter {
|
|
|
188
198
|
return
|
|
189
199
|
}
|
|
190
200
|
|
|
191
|
-
this.#
|
|
201
|
+
this.#writeInteractiveLine(`${promptRail()} ${color('!', 'dim')} ${message}`)
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
marker (message: string): void {
|
|
@@ -329,6 +339,18 @@ export class ProgressReporter {
|
|
|
329
339
|
this.#write(this.target, message)
|
|
330
340
|
}
|
|
331
341
|
|
|
342
|
+
#writeInteractiveLine (message: string): void {
|
|
343
|
+
if (this.#isTTY && this.#renderedStepLineCount > 0) {
|
|
344
|
+
this.#writeRaw(this.target, `\u001B[${this.#renderedStepLineCount}A\r\u001B[2K`)
|
|
345
|
+
this.#write(this.target, message)
|
|
346
|
+
this.#renderedStepLineCount = 0
|
|
347
|
+
this.#renderChecklist()
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this.#writeLine(message)
|
|
352
|
+
}
|
|
353
|
+
|
|
332
354
|
#renderSpinner (): void {
|
|
333
355
|
const spinner = this.#spinner
|
|
334
356
|
if (spinner === undefined) {
|
|
@@ -505,18 +527,38 @@ function outputWithoutProgressMarkers (output: string): string {
|
|
|
505
527
|
.join('\n')
|
|
506
528
|
}
|
|
507
529
|
|
|
508
|
-
|
|
530
|
+
export interface CommandFailureOptions {
|
|
531
|
+
tailLines?: number
|
|
532
|
+
logPath?: string
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function outputLines (output: string): string[] {
|
|
509
536
|
return output
|
|
510
537
|
.split(/\r?\n/u)
|
|
511
538
|
.map((line) => line.trimEnd())
|
|
512
539
|
.filter((line) => line.trim().length > 0)
|
|
513
|
-
.slice(-maxLines)
|
|
514
540
|
}
|
|
515
541
|
|
|
516
|
-
|
|
542
|
+
function isDevcontainerErrorEnvelope (line: string): boolean {
|
|
543
|
+
try {
|
|
544
|
+
const value = JSON.parse(line) as { outcome?: unknown, message?: unknown }
|
|
545
|
+
return value !== null && typeof value === 'object' &&
|
|
546
|
+
value.outcome === 'error' && typeof value.message === 'string'
|
|
547
|
+
} catch {
|
|
548
|
+
return false
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function formatCommandFailure (label: string, result: CommandResult, options: CommandFailureOptions = {}): string {
|
|
517
553
|
const maxLines = options.tailLines ?? DEFAULT_FAILURE_TAIL_LINES
|
|
518
|
-
const
|
|
519
|
-
const
|
|
554
|
+
const stderrLines = outputLines(outputWithoutProgressMarkers(result.stderr))
|
|
555
|
+
const stderrTail = maxLines <= 0 ? [] : stderrLines.slice(-maxLines)
|
|
556
|
+
const stdoutLines = outputLines(outputWithoutProgressMarkers(result.stdout))
|
|
557
|
+
const wrapperPresent = stdoutLines.some(isDevcontainerErrorEnvelope)
|
|
558
|
+
const specificStdout = stdoutLines.filter((line) => !isDevcontainerErrorEnvelope(line))
|
|
559
|
+
const hasSpecificDiagnostic = stderrLines.length > 0 || specificStdout.length > 0
|
|
560
|
+
const stdoutBudget = Math.max(0, maxLines - stderrTail.length)
|
|
561
|
+
const stdoutTail = stdoutBudget === 0 ? [] : specificStdout.slice(-stdoutBudget)
|
|
520
562
|
const lines = [
|
|
521
563
|
`${label} failed with exit code ${result.code}.`,
|
|
522
564
|
'Rerun with --verbose to see full command output.'
|
|
@@ -530,6 +572,14 @@ export function formatCommandFailure (label: string, result: CommandResult, opti
|
|
|
530
572
|
lines.push('', 'stdout tail:', ...stdoutTail.map((line) => ` ${line}`))
|
|
531
573
|
}
|
|
532
574
|
|
|
575
|
+
if (wrapperPresent && !hasSpecificDiagnostic) {
|
|
576
|
+
lines.push('', 'The Dev Containers CLI reported a nested command failure without diagnostic output.')
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (options.logPath !== undefined) {
|
|
580
|
+
lines.push('', `Command log: ${options.logPath}`)
|
|
581
|
+
}
|
|
582
|
+
|
|
533
583
|
return lines.join('\n')
|
|
534
584
|
}
|
|
535
585
|
|
|
@@ -586,8 +636,13 @@ export async function runProgressCommand (
|
|
|
586
636
|
}
|
|
587
637
|
}
|
|
588
638
|
|
|
589
|
-
export function assertProgressCommandSucceeded (
|
|
639
|
+
export function assertProgressCommandSucceeded (
|
|
640
|
+
label: string,
|
|
641
|
+
result: CommandResult,
|
|
642
|
+
message: string,
|
|
643
|
+
options: CommandFailureOptions = {}
|
|
644
|
+
): void {
|
|
590
645
|
if (result.code !== 0) {
|
|
591
|
-
throw new Error(`${message}\n${formatCommandFailure(label, result)}`)
|
|
646
|
+
throw new Error(`${message}\n${formatCommandFailure(label, result, options)}`)
|
|
592
647
|
}
|
|
593
648
|
}
|