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.
Files changed (32) hide show
  1. package/README.md +10 -0
  2. package/assets/devcontainer/README.md +12 -1
  3. package/assets/devcontainer/devcontainer.json +1 -1
  4. package/assets/devcontainer/hooks/post-create.sh +12 -1
  5. package/dist/bin/cli.cjs +1 -1
  6. package/dist/bin/cli.mjs +1 -1
  7. package/dist/{main-BDgyf2t5.cjs → main-CT2n9qcb.cjs} +448 -67
  8. package/dist/{main-J4_2Up3o.mjs → main-O__JaiXe.mjs} +431 -68
  9. package/dist/main-O__JaiXe.mjs.map +1 -0
  10. package/dist/main.cjs +4 -1
  11. package/dist/main.d.cts +92 -26
  12. package/dist/main.d.cts.map +1 -1
  13. package/dist/main.d.mts +92 -26
  14. package/dist/main.d.mts.map +1 -1
  15. package/dist/main.mjs +2 -2
  16. package/docs/features/lifecycle.md +13 -0
  17. package/docs/features/setup.md +5 -0
  18. package/docs/features/start-and-shell.md +5 -0
  19. package/docs/superpowers/plans/2026-07-18-architecture-aware-1password-installer.md +163 -0
  20. package/docs/superpowers/plans/2026-07-18-devcontainer-node-image-digest.md +341 -0
  21. package/docs/superpowers/plans/2026-07-19-container-runtime-readiness.md +1536 -0
  22. package/docs/superpowers/specs/2026-07-18-architecture-aware-1password-installer-design.md +63 -0
  23. package/docs/superpowers/specs/2026-07-18-devcontainer-node-image-digest-design.md +108 -0
  24. package/docs/superpowers/specs/2026-07-19-container-runtime-readiness-design.md +353 -0
  25. package/package.json +1 -1
  26. package/src/container-runtime.ts +295 -0
  27. package/src/devcontainer.ts +20 -4
  28. package/src/doctor.ts +48 -22
  29. package/src/main.ts +95 -11
  30. package/src/process.ts +50 -10
  31. package/src/progress.ts +63 -8
  32. 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 (resolved) {
116
- return
117
- }
136
+ if (settled) return
118
137
 
119
- resolved = true
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 (resolved) {
131
- return
132
- }
150
+ if (settled) return
133
151
 
134
- resolved = true
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.#writeLine(`${promptRail()} ${color('!', 'dim')} ${message}`)
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
- function tailLines (output: string, maxLines: number): string[] {
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
- export function formatCommandFailure (label: string, result: CommandResult, options: { tailLines?: number } = {}): string {
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 stderrTail = tailLines(outputWithoutProgressMarkers(result.stderr), maxLines)
519
- const stdoutTail = tailLines(outputWithoutProgressMarkers(result.stdout), Math.max(0, maxLines - stderrTail.length))
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 (label: string, result: CommandResult, message: string): void {
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
  }