boxdown 1.2.0 → 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 (53) hide show
  1. package/README.md +10 -0
  2. package/assets/devcontainer/README.md +29 -6
  3. package/assets/devcontainer/devcontainer.json +4 -9
  4. package/assets/devcontainer/hooks/initialize.sh +49 -27
  5. package/assets/devcontainer/hooks/post-create.sh +23 -1
  6. package/assets/devcontainer/hooks/post-start.sh +0 -10
  7. package/assets/devcontainer/utils/git-signing-bootstrap.sh +58 -5
  8. package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
  9. package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
  10. package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
  11. package/dist/bin/cli.cjs +1 -1
  12. package/dist/bin/cli.mjs +1 -1
  13. package/dist/{main-Df4E8ARj.cjs → main-CT2n9qcb.cjs} +907 -266
  14. package/dist/{main-XMBsKjIK.mjs → main-O__JaiXe.mjs} +891 -268
  15. package/dist/main-O__JaiXe.mjs.map +1 -0
  16. package/dist/main.cjs +4 -1
  17. package/dist/main.d.cts +98 -26
  18. package/dist/main.d.cts.map +1 -1
  19. package/dist/main.d.mts +98 -26
  20. package/dist/main.d.mts.map +1 -1
  21. package/dist/main.mjs +2 -2
  22. package/docs/features/commit-signing.md +71 -0
  23. package/docs/features/generated-config-and-state.md +19 -4
  24. package/docs/features/github-auth-refresh.md +3 -2
  25. package/docs/features/lifecycle.md +19 -0
  26. package/docs/features/setup.md +5 -0
  27. package/docs/features/start-and-shell.md +10 -0
  28. package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
  29. package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
  30. package/docs/superpowers/plans/2026-07-18-architecture-aware-1password-installer.md +163 -0
  31. package/docs/superpowers/plans/2026-07-18-devcontainer-node-image-digest.md +341 -0
  32. package/docs/superpowers/plans/2026-07-19-container-runtime-readiness.md +1536 -0
  33. package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +20 -0
  34. package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
  35. package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
  36. package/docs/superpowers/specs/2026-07-18-architecture-aware-1password-installer-design.md +63 -0
  37. package/docs/superpowers/specs/2026-07-18-devcontainer-node-image-digest-design.md +108 -0
  38. package/docs/superpowers/specs/2026-07-19-container-runtime-readiness-design.md +353 -0
  39. package/package.json +1 -1
  40. package/src/config.ts +14 -2
  41. package/src/constants.ts +7 -0
  42. package/src/container-runtime.ts +295 -0
  43. package/src/devcontainer.ts +57 -30
  44. package/src/doctor.ts +169 -46
  45. package/src/git-signing.ts +205 -25
  46. package/src/logging.ts +11 -1
  47. package/src/main.ts +97 -12
  48. package/src/paths.ts +21 -1
  49. package/src/process.ts +50 -10
  50. package/src/progress.ts +63 -8
  51. package/src/purge.ts +8 -0
  52. package/src/shell.ts +6 -0
  53. package/dist/main-XMBsKjIK.mjs.map +0 -1
@@ -0,0 +1,295 @@
1
+ import { performance } from 'node:perf_hooks'
2
+
3
+ import { runBuffered, type CommandResult } from './process.ts'
4
+
5
+ export type ContainerRuntimeReason =
6
+ | 'docker-cli-unavailable'
7
+ | 'docker-daemon-unavailable'
8
+ | 'buildx-builder-unavailable'
9
+
10
+ export type ContainerRuntimeMode = 'buildx' | 'fallback'
11
+ export type ContainerRuntimeCommandResult = CommandResult
12
+ export type ContainerRuntimeCommandRunner = (
13
+ command: string,
14
+ args: string[],
15
+ timeoutMs?: number
16
+ ) => Promise<ContainerRuntimeCommandResult>
17
+
18
+ export interface ContainerRuntimeFailure {
19
+ reason: ContainerRuntimeReason
20
+ command: string[]
21
+ detail: string
22
+ timedOut?: true
23
+ }
24
+
25
+ export type ContainerRuntimeProbe =
26
+ | { state: 'ready', mode: ContainerRuntimeMode, warnings: string[] }
27
+ | { state: 'waiting', failure: ContainerRuntimeFailure }
28
+ | { state: 'failed', failure: ContainerRuntimeFailure }
29
+
30
+ const BUILDX_FALLBACK_WARNING = 'Docker Buildx is unavailable; the Dev Containers CLI will use its classic-build fallback.'
31
+
32
+ async function runContainerRuntimeCommand (
33
+ command: string,
34
+ args: string[],
35
+ timeoutMs?: number
36
+ ): Promise<ContainerRuntimeCommandResult> {
37
+ return runBuffered(command, args, {
38
+ mirrorStdout: false,
39
+ mirrorStderr: false,
40
+ timeoutMs
41
+ })
42
+ }
43
+
44
+ function compactCommandOutput (result: ContainerRuntimeCommandResult): string {
45
+ const output = result.stderr.trim().length > 0 ? result.stderr : result.stdout
46
+ const compact = output.trim().replace(/\s+/gu, ' ').slice(0, 500)
47
+ return compact.length > 0 ? compact : `Command exited with code ${result.code}`
48
+ }
49
+
50
+ function failure (
51
+ reason: ContainerRuntimeReason,
52
+ command: string[],
53
+ result: ContainerRuntimeCommandResult
54
+ ): ContainerRuntimeFailure {
55
+ return {
56
+ reason,
57
+ command,
58
+ detail: compactCommandOutput(result),
59
+ ...(result.timedOut === true ? { timedOut: true as const } : {})
60
+ }
61
+ }
62
+
63
+ export async function probeContainerRuntime (
64
+ runCommand: ContainerRuntimeCommandRunner = runContainerRuntimeCommand
65
+ ): Promise<ContainerRuntimeProbe> {
66
+ const dockerVersionCommand = ['docker', '--version']
67
+ const dockerVersion = await runCommand(dockerVersionCommand[0] as string, dockerVersionCommand.slice(1))
68
+ if (dockerVersion.code !== 0) {
69
+ return {
70
+ state: 'failed',
71
+ failure: failure('docker-cli-unavailable', dockerVersionCommand, dockerVersion)
72
+ }
73
+ }
74
+
75
+ const dockerInfoCommand = ['docker', 'info']
76
+ const dockerInfo = await runCommand(dockerInfoCommand[0] as string, dockerInfoCommand.slice(1))
77
+ if (dockerInfo.code !== 0) {
78
+ return {
79
+ state: 'waiting',
80
+ failure: failure('docker-daemon-unavailable', dockerInfoCommand, dockerInfo)
81
+ }
82
+ }
83
+
84
+ const buildxVersionCommand = ['docker', 'buildx', 'version']
85
+ const buildxVersion = await runCommand(buildxVersionCommand[0] as string, buildxVersionCommand.slice(1))
86
+ if (buildxVersion.timedOut === true) {
87
+ return {
88
+ state: 'waiting',
89
+ failure: failure('buildx-builder-unavailable', buildxVersionCommand, buildxVersion)
90
+ }
91
+ }
92
+ if (buildxVersion.code !== 0) {
93
+ return {
94
+ state: 'ready',
95
+ mode: 'fallback',
96
+ warnings: [BUILDX_FALLBACK_WARNING]
97
+ }
98
+ }
99
+
100
+ const buildxInspectCommand = ['docker', 'buildx', 'inspect', '--bootstrap']
101
+ const buildxInspect = await runCommand(buildxInspectCommand[0] as string, buildxInspectCommand.slice(1))
102
+ if (buildxInspect.code !== 0) {
103
+ return {
104
+ state: 'waiting',
105
+ failure: failure('buildx-builder-unavailable', buildxInspectCommand, buildxInspect)
106
+ }
107
+ }
108
+
109
+ return { state: 'ready', mode: 'buildx', warnings: [] }
110
+ }
111
+
112
+ export type ContainerRuntimeWaitResult =
113
+ | { state: 'ready', mode: ContainerRuntimeMode, warnings: string[] }
114
+ | {
115
+ state: 'failed'
116
+ failure: ContainerRuntimeFailure
117
+ timedOut: boolean
118
+ timeoutMs: number
119
+ }
120
+
121
+ export interface WaitForContainerRuntimeOptions {
122
+ runCommand?: ContainerRuntimeCommandRunner
123
+ onTransition?: (probe: ContainerRuntimeProbe) => void
124
+ }
125
+
126
+ /** @internal */
127
+ export interface InternalContainerRuntimeWaitOptions extends WaitForContainerRuntimeOptions {
128
+ timeoutMs?: number
129
+ pollIntervalMs?: number
130
+ now?: () => number
131
+ sleep?: (milliseconds: number) => Promise<void>
132
+ }
133
+
134
+ function defaultSleep (milliseconds: number): Promise<void> {
135
+ return new Promise((resolve) => setTimeout(resolve, milliseconds))
136
+ }
137
+
138
+ function transitionKey (probe: ContainerRuntimeProbe): string {
139
+ return probe.state === 'ready' ? 'ready' : `${probe.state}:${probe.failure.reason}`
140
+ }
141
+
142
+ class ContainerRuntimeDeadlineError extends Error {
143
+ readonly command: string[]
144
+
145
+ constructor (command: string[]) {
146
+ super(`Container runtime deadline expired before ${command.join(' ')}`)
147
+ this.command = command
148
+ }
149
+ }
150
+
151
+ function deadlineFailure (command: string[]): ContainerRuntimeFailure {
152
+ const reason: ContainerRuntimeReason = command[1] === 'info'
153
+ ? 'docker-daemon-unavailable'
154
+ : command[1] === 'buildx'
155
+ ? 'buildx-builder-unavailable'
156
+ : 'docker-cli-unavailable'
157
+
158
+ return {
159
+ reason,
160
+ command,
161
+ detail: `Readiness deadline expired before ${command.join(' ')} could start.`,
162
+ timedOut: true
163
+ }
164
+ }
165
+
166
+ /** @internal */
167
+ export async function waitForContainerRuntimeInternal (
168
+ options: InternalContainerRuntimeWaitOptions = {}
169
+ ): Promise<ContainerRuntimeWaitResult> {
170
+ const timeoutMs = options.timeoutMs ?? 60_000
171
+ const pollIntervalMs = options.pollIntervalMs ?? 1_000
172
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 60_000) {
173
+ throw new Error('timeoutMs must be a finite number between 0 and 60000 milliseconds')
174
+ }
175
+ if (pollIntervalMs !== 1_000) {
176
+ throw new Error('pollIntervalMs must be exactly 1000 milliseconds')
177
+ }
178
+ const now = options.now ?? Date.now
179
+ const sleep = options.sleep ?? defaultSleep
180
+ const deadline = now() + timeoutMs
181
+ let lastTransition: string | undefined
182
+ let lastWaitingFailure: ContainerRuntimeFailure | undefined
183
+
184
+ while (true) {
185
+ if (lastWaitingFailure !== undefined && deadline - now() <= 0) {
186
+ return { state: 'failed', failure: lastWaitingFailure, timedOut: true, timeoutMs }
187
+ }
188
+
189
+ const runCommand = options.runCommand ?? runContainerRuntimeCommand
190
+ let lastStartedCommand: string[] | undefined
191
+ let probe: ContainerRuntimeProbe
192
+ try {
193
+ probe = await probeContainerRuntime(async (command, args) => {
194
+ const commandArgs = [command, ...args]
195
+ const remainingMs = Math.floor(deadline - now())
196
+ if (remainingMs <= 0) throw new ContainerRuntimeDeadlineError(commandArgs)
197
+ lastStartedCommand = commandArgs
198
+ return runCommand(command, args, remainingMs)
199
+ })
200
+ } catch (error) {
201
+ if (!(error instanceof ContainerRuntimeDeadlineError)) throw error
202
+ return {
203
+ state: 'failed',
204
+ failure: lastWaitingFailure ?? deadlineFailure(error.command),
205
+ timedOut: true,
206
+ timeoutMs
207
+ }
208
+ }
209
+
210
+ if (deadline - now() <= 0) {
211
+ const currentFailure = probe.state === 'ready'
212
+ ? deadlineFailure(lastStartedCommand ?? ['docker', '--version'])
213
+ : probe.failure
214
+ return {
215
+ state: 'failed',
216
+ failure: lastWaitingFailure ?? currentFailure,
217
+ timedOut: true,
218
+ timeoutMs
219
+ }
220
+ }
221
+
222
+ if (probe.state === 'ready') return probe
223
+
224
+ const currentTransition = transitionKey(probe)
225
+ if (currentTransition !== lastTransition) {
226
+ options.onTransition?.(probe)
227
+ lastTransition = currentTransition
228
+ }
229
+
230
+ if (probe.state === 'failed') {
231
+ return {
232
+ state: 'failed',
233
+ failure: probe.failure,
234
+ timedOut: probe.failure.timedOut === true,
235
+ timeoutMs
236
+ }
237
+ }
238
+
239
+ lastWaitingFailure = probe.failure
240
+
241
+ const remainingMs = deadline - now()
242
+ if (remainingMs <= 0) {
243
+ return { state: 'failed', failure: probe.failure, timedOut: true, timeoutMs }
244
+ }
245
+
246
+ await sleep(Math.min(pollIntervalMs, remainingMs))
247
+ }
248
+ }
249
+
250
+ export async function waitForContainerRuntime (
251
+ options: WaitForContainerRuntimeOptions = {}
252
+ ): Promise<ContainerRuntimeWaitResult> {
253
+ return waitForContainerRuntimeInternal({
254
+ runCommand: options.runCommand,
255
+ onTransition: options.onTransition,
256
+ timeoutMs: 60_000,
257
+ pollIntervalMs: 1_000,
258
+ now: () => performance.now(),
259
+ sleep: defaultSleep
260
+ })
261
+ }
262
+
263
+ function commandText (command: readonly string[]): string {
264
+ return command.join(' ')
265
+ }
266
+
267
+ export function formatContainerRuntimeFailure (
268
+ result: Extract<ContainerRuntimeWaitResult, { state: 'failed' }>,
269
+ options: { logPath?: string } = {}
270
+ ): string {
271
+ const seconds = result.timeoutMs / 1_000
272
+ const descriptions: Record<ContainerRuntimeReason, string> = {
273
+ 'docker-cli-unavailable': 'Docker CLI is required but was not available.',
274
+ 'docker-daemon-unavailable': result.timedOut
275
+ ? `Docker daemon did not become ready within ${seconds} seconds.`
276
+ : 'Docker daemon is required but was not reachable.',
277
+ 'buildx-builder-unavailable': result.timedOut
278
+ ? `Docker Buildx builder did not become ready within ${seconds} seconds.`
279
+ : 'Docker Buildx builder was not operational.'
280
+ }
281
+ const manualCheck = result.failure.reason === 'buildx-builder-unavailable'
282
+ ? 'docker buildx inspect'
283
+ : result.failure.reason === 'docker-daemon-unavailable'
284
+ ? 'docker info'
285
+ : 'docker --version'
286
+ const lines = [
287
+ descriptions[result.failure.reason],
288
+ `Last check: ${commandText(result.failure.command)}`,
289
+ `Detail: ${result.failure.detail}`,
290
+ `Check ${result.failure.reason === 'buildx-builder-unavailable' ? 'Buildx' : 'Docker'} with: ${manualCheck}`
291
+ ]
292
+
293
+ if (options.logPath !== undefined) lines.push(`Command log: ${options.logPath}`)
294
+ return lines.join('\n')
295
+ }
@@ -5,7 +5,7 @@ import { buildGeneratedDevcontainerConfig, publishContainerPortFromConfig, write
5
5
  import { codingAgentBinary, type CodingAgentCli } from './coding-agents.ts'
6
6
  import { resolveDevcontainerCli } from './devcontainer-cli.ts'
7
7
  import { configureWorkspaceGithubGitAuth } from './github-git-auth.ts'
8
- import { resolveGitSigningPlan } from './git-signing.ts'
8
+ import { reportGitSigningPlan, resolveGitSigningPlan } from './git-signing.ts'
9
9
  import type { WorkspaceCommandLogger } from './logging.ts'
10
10
  import { recordWorkspaceDockerImage } from './metadata.ts'
11
11
  import type { WorkspaceContext } from './paths.ts'
@@ -21,6 +21,7 @@ export interface StartOptions {
21
21
  progress?: ProgressReporter
22
22
  logger?: WorkspaceCommandLogger
23
23
  reuseRunning?: boolean
24
+ runDevcontainerUp?: typeof runProgressCommand
24
25
  }
25
26
 
26
27
  export interface ContainerCommandOptions {
@@ -43,10 +44,6 @@ export interface DockerImageInfo {
43
44
  name?: string
44
45
  }
45
46
 
46
- interface DockerInspectContainer {
47
- Image?: unknown
48
- Config?: unknown
49
- }
50
47
 
51
48
  function devcontainerWorkspaceArgs (context: WorkspaceContext): string[] {
52
49
  return [
@@ -142,36 +139,28 @@ export async function findRunningContainerId (context: WorkspaceContext, options
142
139
  return result.stdout.split(/\r?\n/).find((line) => line.length > 0)
143
140
  }
144
141
 
145
- function isRecord (value: unknown): value is Record<string, unknown> {
146
- return value !== null && typeof value === 'object' && !Array.isArray(value)
147
- }
148
-
149
- function parseDockerInspectImage (output: string, containerId: string): DockerImageInfo | undefined {
150
- const trimmed = output.trim()
142
+ export function parseDockerInspectImage (output: string, containerId: string): DockerImageInfo | undefined {
143
+ const [rawId, rawName] = output.trim().split('|')
151
144
 
152
- if (trimmed.length === 0) {
145
+ if (rawId === undefined || rawId.length === 0) {
153
146
  return undefined
154
147
  }
155
148
 
156
- let parsed: DockerInspectContainer
149
+ let id: unknown
150
+ let name: unknown
157
151
 
158
152
  try {
159
- parsed = JSON.parse(trimmed) as DockerInspectContainer
153
+ id = JSON.parse(rawId)
154
+ name = rawName === undefined || rawName.length === 0 ? undefined : JSON.parse(rawName)
160
155
  } catch (error) {
161
156
  throw new Error(`Could not parse docker inspect output for ${containerId}`, { cause: error })
162
157
  }
163
158
 
164
- if (typeof parsed.Image !== 'string' || parsed.Image.length === 0) {
165
- return undefined
166
- }
167
-
168
- const configImage = isRecord(parsed.Config) && typeof parsed.Config.Image === 'string' && parsed.Config.Image.length > 0
169
- ? parsed.Config.Image
170
- : undefined
159
+ if (typeof id !== 'string' || id.length === 0) return undefined
171
160
 
172
161
  return {
173
- id: parsed.Image,
174
- ...(configImage === undefined ? {} : { name: configImage })
162
+ id,
163
+ ...(typeof name === 'string' && name.length > 0 ? { name } : {})
175
164
  }
176
165
  }
177
166
 
@@ -179,7 +168,7 @@ export async function inspectContainerImage (containerId: string, options: { log
179
168
  const result = await runBuffered('docker', [
180
169
  'inspect',
181
170
  '--format',
182
- '{{json .}}',
171
+ '{{json .Image}}|{{json .Config.Image}}',
183
172
  containerId
184
173
  ], {
185
174
  logger: options.logger,
@@ -320,7 +309,19 @@ export async function startDevcontainer (context: WorkspaceContext, options: Sta
320
309
  progress.detail(context.generatedConfigPath)
321
310
  }
322
311
  try {
323
- writeGeneratedDevcontainerConfig(context, await resolveGitSigningPlan(context))
312
+ const signingPlan = await resolveGitSigningPlan(context)
313
+ reportGitSigningPlan(signingPlan, {
314
+ logger: options.logger,
315
+ quiet: proxyMode,
316
+ ...(progress === undefined
317
+ ? {}
318
+ : {
319
+ writeWarning: (message: string) => {
320
+ progress.warn(message.trimEnd())
321
+ }
322
+ })
323
+ })
324
+ writeGeneratedDevcontainerConfig(context, signingPlan)
324
325
  if (hasConfigStep) {
325
326
  progress?.completeStep('devcontainer-config')
326
327
  }
@@ -374,7 +375,7 @@ export async function startDevcontainer (context: WorkspaceContext, options: Sta
374
375
  mirrorStderr: 'stderr',
375
376
  logger: options.logger
376
377
  })
377
- : await runProgressCommand('devcontainer up', cli.command, [...cli.argsPrefix, ...args], {
378
+ : await (options.runDevcontainerUp ?? runProgressCommand)('devcontainer up', cli.command, [...cli.argsPrefix, ...args], {
378
379
  progress,
379
380
  spinnerLabel: 'Starting devcontainer',
380
381
  stepId: 'devcontainer-start',
@@ -388,7 +389,12 @@ export async function startDevcontainer (context: WorkspaceContext, options: Sta
388
389
  }
389
390
 
390
391
  if (progress !== undefined) {
391
- assertProgressCommandSucceeded('devcontainer up', result, `devcontainer up failed for ${context.workspaceFolder}`)
392
+ assertProgressCommandSucceeded(
393
+ 'devcontainer up',
394
+ result,
395
+ `devcontainer up failed for ${context.workspaceFolder}`,
396
+ { logPath: options.logger?.logPath }
397
+ )
392
398
  }
393
399
 
394
400
  const containerId = parseContainerIdFromUpOutput(`${result.stdout}\n${result.stderr}`) ?? await findRunningContainerId(context, { logger: options.logger })
@@ -503,7 +509,12 @@ export async function ensureContainerSshRuntime (context: WorkspaceContext, opti
503
509
  }
504
510
 
505
511
  if (options.progress !== undefined) {
506
- assertProgressCommandSucceeded('prepare SSH runtime', result, 'Failed to prepare devcontainer SSH runtime')
512
+ assertProgressCommandSucceeded(
513
+ 'prepare SSH runtime',
514
+ result,
515
+ 'Failed to prepare devcontainer SSH runtime',
516
+ { logPath: options.logger?.logPath }
517
+ )
507
518
  }
508
519
  }
509
520
 
@@ -590,7 +601,12 @@ export async function ensureContainerCodingAgentCli (
590
601
  }
591
602
 
592
603
  if (options.progress !== undefined) {
593
- assertProgressCommandSucceeded(`prepare ${codingAgentBinary(agent)}`, result, `Could not install or refresh ${codingAgentBinary(agent)} inside the devcontainer`)
604
+ assertProgressCommandSucceeded(
605
+ `prepare ${codingAgentBinary(agent)}`,
606
+ result,
607
+ `Could not install or refresh ${codingAgentBinary(agent)} inside the devcontainer`,
608
+ { logPath: options.logger?.logPath }
609
+ )
594
610
  }
595
611
  }
596
612
 
@@ -669,7 +685,18 @@ export async function refreshContainerGhAuth (context: WorkspaceContext, options
669
685
  quiet: true,
670
686
  progress
671
687
  })
672
- writeGeneratedDevcontainerConfig(context, await resolveGitSigningPlan(context))
688
+ const signingPlan = await resolveGitSigningPlan(context)
689
+ reportGitSigningPlan(signingPlan, {
690
+ logger: options.logger,
691
+ ...(progress === undefined
692
+ ? {}
693
+ : {
694
+ writeWarning: (message: string) => {
695
+ progress.warn(message.trimEnd())
696
+ }
697
+ })
698
+ })
699
+ writeGeneratedDevcontainerConfig(context, signingPlan)
673
700
  if (hasConfigStep) {
674
701
  progress?.completeStep('gh-auth-config')
675
702
  }