boxdown 1.0.0 → 1.2.1
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 +135 -12
- package/assets/devcontainer/README.md +65 -21
- package/assets/devcontainer/devcontainer.json +27 -15
- package/assets/devcontainer/hooks/initialize.sh +76 -22
- package/assets/devcontainer/hooks/post-create.sh +70 -12
- package/assets/devcontainer/hooks/post-start.sh +20 -13
- package/assets/devcontainer/ssh-config-install.sh +12 -3
- package/assets/devcontainer/start.sh +721 -44
- package/assets/devcontainer/utils/coding-agent-cli-update.sh +267 -3
- package/assets/devcontainer/utils/deps-install.sh +68 -0
- package/assets/devcontainer/utils/git-config-bootstrap.sh +128 -0
- package/assets/devcontainer/utils/git-signing-bootstrap.sh +109 -0
- package/assets/devcontainer/utils/python-bootstrap.sh +69 -0
- package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
- package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
- package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
- package/assets/devcontainer/utils/ssh-bootstrap.sh +9 -0
- package/dist/bin/cli.cjs +1 -1
- package/dist/bin/cli.mjs +1 -1
- package/dist/main-BDgyf2t5.cjs +5758 -0
- package/dist/main-J4_2Up3o.mjs +5718 -0
- package/dist/main-J4_2Up3o.mjs.map +1 -0
- package/dist/main.cjs +5 -2
- package/dist/main.d.cts +501 -4
- package/dist/main.d.cts.map +1 -1
- package/dist/main.d.mts +501 -4
- package/dist/main.d.mts.map +1 -1
- package/dist/main.mjs +2 -2
- package/docs/README.md +1 -0
- package/docs/architecture.md +32 -0
- package/docs/development.md +13 -6
- package/docs/features/README.md +2 -0
- package/docs/features/commit-signing.md +94 -0
- package/docs/features/generated-config-and-state.md +73 -5
- package/docs/features/github-auth-refresh.md +15 -2
- package/docs/features/lifecycle.md +103 -11
- package/docs/features/setup.md +66 -0
- package/docs/features/ssh-config-and-proxy.md +228 -7
- package/docs/features/start-and-shell.md +45 -5
- package/docs/superpowers/plans/2026-07-11-default-commit-signing.md +110 -0
- package/docs/superpowers/plans/2026-07-11-progress-ci-regression.md +125 -0
- package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
- package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
- package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +416 -0
- package/docs/superpowers/specs/2026-07-11-progress-ci-regression-design.md +43 -0
- package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
- package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
- package/docs/testing.md +35 -2
- package/docs/todo.md +2 -2
- package/package.json +1 -1
- package/src/claude-app-config.ts +304 -0
- package/src/cli-style.ts +43 -0
- package/src/codex-app-config.ts +656 -0
- package/src/config.ts +80 -10
- package/src/constants.ts +12 -0
- package/src/devcontainer.ts +511 -64
- package/src/doctor.ts +292 -30
- package/src/git-signing.ts +267 -0
- package/src/interactive-prompts.ts +692 -0
- package/src/list.ts +16 -2
- package/src/logging.ts +164 -0
- package/src/main.ts +1214 -63
- package/src/metadata.ts +52 -3
- package/src/package-info.ts +18 -0
- package/src/paths.ts +71 -11
- package/src/process.ts +80 -2
- package/src/progress.ts +593 -0
- package/src/purge.ts +216 -0
- package/src/shell.ts +25 -0
- package/src/ssh-config.ts +134 -16
- package/src/ssh-install-targets.ts +111 -0
- package/src/ssh-key.ts +53 -13
- package/src/status.ts +5 -0
- package/dist/main-BuEptwlL.cjs +0 -1707
- package/dist/main-ZFTrSVgt.mjs +0 -1685
- package/dist/main-ZFTrSVgt.mjs.map +0 -1
package/src/devcontainer.ts
CHANGED
|
@@ -5,8 +5,12 @@ 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 { reportGitSigningPlan, resolveGitSigningPlan } from './git-signing.ts'
|
|
9
|
+
import type { WorkspaceCommandLogger } from './logging.ts'
|
|
10
|
+
import { recordWorkspaceDockerImage } from './metadata.ts'
|
|
8
11
|
import type { WorkspaceContext } from './paths.ts'
|
|
9
12
|
import { runBuffered, runInteractive } from './process.ts'
|
|
13
|
+
import { assertProgressCommandSucceeded, type ProgressReporter, runProgressCommand } from './progress.ts'
|
|
10
14
|
import { interactiveCommandScript, interactiveShellEnvArgs, interactiveShellScript } from './shell.ts'
|
|
11
15
|
import { ensureHostSshKey } from './ssh-key.ts'
|
|
12
16
|
import { type ContainerSummary, parseDockerPsJsonLines } from './status.ts'
|
|
@@ -14,9 +18,32 @@ import { type ContainerSummary, parseDockerPsJsonLines } from './status.ts'
|
|
|
14
18
|
export interface StartOptions {
|
|
15
19
|
recreate?: boolean
|
|
16
20
|
proxyMode?: boolean
|
|
21
|
+
progress?: ProgressReporter
|
|
22
|
+
logger?: WorkspaceCommandLogger
|
|
17
23
|
reuseRunning?: boolean
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
export interface ContainerCommandOptions {
|
|
27
|
+
progress?: ProgressReporter
|
|
28
|
+
logger?: WorkspaceCommandLogger
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TunnelPortForward {
|
|
32
|
+
localPort: number
|
|
33
|
+
remotePort: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SshTunnelOptions {
|
|
37
|
+
bindAddress?: string
|
|
38
|
+
remoteHost?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DockerImageInfo {
|
|
42
|
+
id: string
|
|
43
|
+
name?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
20
47
|
function devcontainerWorkspaceArgs (context: WorkspaceContext): string[] {
|
|
21
48
|
return [
|
|
22
49
|
'--workspace-folder',
|
|
@@ -34,11 +61,23 @@ function log (message: string, proxyMode = false): void {
|
|
|
34
61
|
}
|
|
35
62
|
}
|
|
36
63
|
|
|
64
|
+
function containerProgressEnvArgs (progress?: ProgressReporter): string[] {
|
|
65
|
+
if (progress === undefined) {
|
|
66
|
+
return []
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
'env',
|
|
71
|
+
`BOXDOWN_VERBOSE=${progress.verbose ? '1' : '0'}`,
|
|
72
|
+
`BOXDOWN_PROGRESS=${progress.verbose ? '0' : '1'}`
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
|
|
37
76
|
export function parseContainerIdFromUpOutput (output: string): string | undefined {
|
|
38
77
|
return /"containerId"\s*:\s*"([^"]+)"/.exec(output)?.[1]
|
|
39
78
|
}
|
|
40
79
|
|
|
41
|
-
export async function findWorkspaceContainer (context: WorkspaceContext): Promise<ContainerSummary | undefined> {
|
|
80
|
+
export async function findWorkspaceContainer (context: WorkspaceContext, options: { logger?: WorkspaceCommandLogger } = {}): Promise<ContainerSummary | undefined> {
|
|
42
81
|
const result = await runBuffered('docker', [
|
|
43
82
|
'ps',
|
|
44
83
|
'-a',
|
|
@@ -47,6 +86,7 @@ export async function findWorkspaceContainer (context: WorkspaceContext): Promis
|
|
|
47
86
|
'--format',
|
|
48
87
|
'{{json .}}'
|
|
49
88
|
], {
|
|
89
|
+
logger: options.logger,
|
|
50
90
|
mirrorStdout: false,
|
|
51
91
|
mirrorStderr: false
|
|
52
92
|
})
|
|
@@ -78,7 +118,7 @@ export async function listWorkspaceContainers (): Promise<ContainerSummary[] | u
|
|
|
78
118
|
return parseDockerPsJsonLines(result.stdout)
|
|
79
119
|
}
|
|
80
120
|
|
|
81
|
-
export async function findRunningContainerId (context: WorkspaceContext): Promise<string | undefined> {
|
|
121
|
+
export async function findRunningContainerId (context: WorkspaceContext, options: { logger?: WorkspaceCommandLogger } = {}): Promise<string | undefined> {
|
|
82
122
|
const result = await runBuffered('docker', [
|
|
83
123
|
'ps',
|
|
84
124
|
'--filter',
|
|
@@ -86,6 +126,7 @@ export async function findRunningContainerId (context: WorkspaceContext): Promis
|
|
|
86
126
|
'--format',
|
|
87
127
|
'{{.ID}}'
|
|
88
128
|
], {
|
|
129
|
+
logger: options.logger,
|
|
89
130
|
mirrorStdout: false,
|
|
90
131
|
mirrorStderr: false
|
|
91
132
|
})
|
|
@@ -97,8 +138,64 @@ export async function findRunningContainerId (context: WorkspaceContext): Promis
|
|
|
97
138
|
return result.stdout.split(/\r?\n/).find((line) => line.length > 0)
|
|
98
139
|
}
|
|
99
140
|
|
|
100
|
-
export
|
|
101
|
-
const
|
|
141
|
+
export function parseDockerInspectImage (output: string, containerId: string): DockerImageInfo | undefined {
|
|
142
|
+
const [rawId, rawName] = output.trim().split('|')
|
|
143
|
+
|
|
144
|
+
if (rawId === undefined || rawId.length === 0) {
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let id: unknown
|
|
149
|
+
let name: unknown
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
id = JSON.parse(rawId)
|
|
153
|
+
name = rawName === undefined || rawName.length === 0 ? undefined : JSON.parse(rawName)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new Error(`Could not parse docker inspect output for ${containerId}`, { cause: error })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (typeof id !== 'string' || id.length === 0) return undefined
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
id,
|
|
162
|
+
...(typeof name === 'string' && name.length > 0 ? { name } : {})
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function inspectContainerImage (containerId: string, options: { logger?: WorkspaceCommandLogger } = {}): Promise<DockerImageInfo | undefined> {
|
|
167
|
+
const result = await runBuffered('docker', [
|
|
168
|
+
'inspect',
|
|
169
|
+
'--format',
|
|
170
|
+
'{{json .Image}}|{{json .Config.Image}}',
|
|
171
|
+
containerId
|
|
172
|
+
], {
|
|
173
|
+
logger: options.logger,
|
|
174
|
+
mirrorStdout: false,
|
|
175
|
+
mirrorStderr: false
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
if (result.code !== 0) {
|
|
179
|
+
throw new Error(`Could not inspect devcontainer image for ${containerId}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return parseDockerInspectImage(result.stdout, containerId)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function recordContainerImageIfPresent (context: WorkspaceContext, containerId: string, logger?: WorkspaceCommandLogger): Promise<void> {
|
|
186
|
+
try {
|
|
187
|
+
const image = await inspectContainerImage(containerId, { logger })
|
|
188
|
+
|
|
189
|
+
if (image !== undefined) {
|
|
190
|
+
recordWorkspaceDockerImage(context, image)
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
process.stderr.write(`Warning: could not record devcontainer image metadata for ${containerId}.\n`)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function stopWorkspaceContainer (context: WorkspaceContext, options: { logger?: WorkspaceCommandLogger } = {}): Promise<void> {
|
|
198
|
+
const container = await findWorkspaceContainer(context, { logger: options.logger })
|
|
102
199
|
|
|
103
200
|
if (container === undefined) {
|
|
104
201
|
process.stdout.write(`No devcontainer found for: ${context.workspaceFolder}\n`)
|
|
@@ -111,6 +208,7 @@ export async function stopWorkspaceContainer (context: WorkspaceContext): Promis
|
|
|
111
208
|
}
|
|
112
209
|
|
|
113
210
|
const result = await runBuffered('docker', ['stop', container.id], {
|
|
211
|
+
logger: options.logger,
|
|
114
212
|
mirrorStdout: false,
|
|
115
213
|
mirrorStderr: false
|
|
116
214
|
})
|
|
@@ -122,41 +220,139 @@ export async function stopWorkspaceContainer (context: WorkspaceContext): Promis
|
|
|
122
220
|
process.stdout.write(`Stopped devcontainer: ${container.id}\n`)
|
|
123
221
|
}
|
|
124
222
|
|
|
125
|
-
export async function removeWorkspaceContainer (context: WorkspaceContext): Promise<void> {
|
|
126
|
-
const container = await findWorkspaceContainer(context)
|
|
223
|
+
export async function removeWorkspaceContainer (context: WorkspaceContext, options: { logger?: WorkspaceCommandLogger } = {}): Promise<void> {
|
|
224
|
+
const container = await findWorkspaceContainer(context, { logger: options.logger })
|
|
127
225
|
|
|
128
226
|
if (container === undefined) {
|
|
129
227
|
process.stdout.write(`No devcontainer found for: ${context.workspaceFolder}\n`)
|
|
130
228
|
return
|
|
131
229
|
}
|
|
132
230
|
|
|
133
|
-
|
|
231
|
+
await removeContainerById(container.id, { logger: options.logger })
|
|
232
|
+
process.stdout.write(`Removed devcontainer: ${container.id}\n`)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function removeContainerById (containerId: string, options: { volumes?: boolean, logger?: WorkspaceCommandLogger } = {}): Promise<void> {
|
|
236
|
+
const result = await runBuffered('docker', [
|
|
237
|
+
'rm',
|
|
238
|
+
'-f',
|
|
239
|
+
...(options.volumes === true ? ['-v'] : []),
|
|
240
|
+
containerId
|
|
241
|
+
], {
|
|
242
|
+
logger: options.logger,
|
|
134
243
|
mirrorStdout: false,
|
|
135
244
|
mirrorStderr: false
|
|
136
245
|
})
|
|
137
246
|
|
|
138
247
|
if (result.code !== 0) {
|
|
139
|
-
throw new Error(`Could not remove devcontainer ${
|
|
248
|
+
throw new Error(`Could not remove devcontainer ${containerId}`)
|
|
140
249
|
}
|
|
250
|
+
}
|
|
141
251
|
|
|
142
|
-
|
|
252
|
+
function dockerImageMissing (stderr: string): boolean {
|
|
253
|
+
return /No such image/i.test(stderr) || /not found/i.test(stderr)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function removeDockerImage (imageId: string, options: { logger?: WorkspaceCommandLogger } = {}): Promise<boolean> {
|
|
257
|
+
const result = await runBuffered('docker', ['image', 'rm', '-f', imageId], {
|
|
258
|
+
logger: options.logger,
|
|
259
|
+
mirrorStdout: false,
|
|
260
|
+
mirrorStderr: false
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
if (result.code !== 0) {
|
|
264
|
+
if (dockerImageMissing(result.stderr)) {
|
|
265
|
+
process.stdout.write(`Docker image already absent: ${imageId}\n`)
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
throw new Error(`Could not remove Docker image ${imageId}`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
process.stdout.write(`Removed Docker image: ${imageId}\n`)
|
|
273
|
+
return true
|
|
143
274
|
}
|
|
144
275
|
|
|
145
276
|
export async function startDevcontainer (context: WorkspaceContext, options: StartOptions = {}): Promise<string> {
|
|
146
|
-
|
|
147
|
-
|
|
277
|
+
const progress = options.progress
|
|
278
|
+
const proxyMode = options.proxyMode ?? false
|
|
279
|
+
const hasSshIdentityStep = progress?.hasStep('ssh-identity') === true
|
|
280
|
+
const hasConfigStep = progress?.hasStep('devcontainer-config') === true
|
|
281
|
+
const hasStartStep = progress?.hasStep('devcontainer-start') === true
|
|
282
|
+
|
|
283
|
+
if (hasSshIdentityStep) {
|
|
284
|
+
progress?.startStep('ssh-identity')
|
|
285
|
+
} else {
|
|
286
|
+
progress?.item('Preparing SSH identity')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await ensureHostSshKey(context, {
|
|
291
|
+
quiet: proxyMode,
|
|
292
|
+
progress
|
|
293
|
+
})
|
|
294
|
+
if (hasSshIdentityStep) {
|
|
295
|
+
progress?.completeStep('ssh-identity')
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (hasSshIdentityStep) {
|
|
299
|
+
progress?.failStep('ssh-identity')
|
|
300
|
+
}
|
|
301
|
+
throw error
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (hasConfigStep) {
|
|
305
|
+
progress?.startStep('devcontainer-config')
|
|
306
|
+
} else if (progress !== undefined) {
|
|
307
|
+
progress.item('Writing generated devcontainer config')
|
|
308
|
+
progress.detail(context.generatedConfigPath)
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const signingPlan = await resolveGitSigningPlan(context)
|
|
312
|
+
reportGitSigningPlan(signingPlan, {
|
|
313
|
+
logger: options.logger,
|
|
314
|
+
quiet: proxyMode,
|
|
315
|
+
...(progress === undefined
|
|
316
|
+
? {}
|
|
317
|
+
: {
|
|
318
|
+
writeWarning: (message: string) => {
|
|
319
|
+
progress.warn(message.trimEnd())
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
writeGeneratedDevcontainerConfig(context, signingPlan)
|
|
324
|
+
if (hasConfigStep) {
|
|
325
|
+
progress?.completeStep('devcontainer-config')
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (hasConfigStep) {
|
|
329
|
+
progress?.failStep('devcontainer-config')
|
|
330
|
+
}
|
|
331
|
+
throw error
|
|
332
|
+
}
|
|
148
333
|
|
|
149
334
|
if (options.reuseRunning === true && options.recreate !== true) {
|
|
150
|
-
const runningContainerId = await findRunningContainerId(context)
|
|
335
|
+
const runningContainerId = await findRunningContainerId(context, { logger: options.logger })
|
|
151
336
|
|
|
152
337
|
if (runningContainerId !== undefined) {
|
|
153
|
-
|
|
338
|
+
if (progress === undefined) {
|
|
339
|
+
log(`Using running devcontainer for: ${context.workspaceFolder}`, proxyMode)
|
|
340
|
+
} else if (hasStartStep) {
|
|
341
|
+
progress.startStep('devcontainer-start')
|
|
342
|
+
progress.completeStep('devcontainer-start')
|
|
343
|
+
} else {
|
|
344
|
+
progress.item('Using running devcontainer')
|
|
345
|
+
progress.detail(runningContainerId)
|
|
346
|
+
}
|
|
347
|
+
await recordContainerImageIfPresent(context, runningContainerId, options.logger)
|
|
154
348
|
return runningContainerId
|
|
155
349
|
}
|
|
156
350
|
}
|
|
157
351
|
|
|
158
352
|
const cli = resolveDevcontainerCli(context)
|
|
159
|
-
|
|
353
|
+
if (progress === undefined) {
|
|
354
|
+
log(`Starting devcontainer for: ${context.workspaceFolder}`, proxyMode)
|
|
355
|
+
}
|
|
160
356
|
|
|
161
357
|
const args = [
|
|
162
358
|
'up',
|
|
@@ -165,28 +361,50 @@ export async function startDevcontainer (context: WorkspaceContext, options: Sta
|
|
|
165
361
|
|
|
166
362
|
if (options.recreate === true) {
|
|
167
363
|
args.push('--remove-existing-container')
|
|
168
|
-
|
|
364
|
+
if (progress === undefined) {
|
|
365
|
+
log('Removing existing dev container so create-time settings apply.', proxyMode)
|
|
366
|
+
} else if (!hasStartStep) {
|
|
367
|
+
progress.item('Removing existing devcontainer before start')
|
|
368
|
+
}
|
|
169
369
|
}
|
|
170
370
|
|
|
171
|
-
const result =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
371
|
+
const result = progress === undefined
|
|
372
|
+
? await runBuffered(cli.command, [...cli.argsPrefix, ...args], {
|
|
373
|
+
mirrorStdout: proxyMode ? 'stderr' : 'stdout',
|
|
374
|
+
mirrorStderr: 'stderr',
|
|
375
|
+
logger: options.logger
|
|
376
|
+
})
|
|
377
|
+
: await runProgressCommand('devcontainer up', cli.command, [...cli.argsPrefix, ...args], {
|
|
378
|
+
progress,
|
|
379
|
+
spinnerLabel: 'Starting devcontainer',
|
|
380
|
+
stepId: 'devcontainer-start',
|
|
381
|
+
verboseStdout: proxyMode ? 'stderr' : 'stdout',
|
|
382
|
+
verboseStderr: 'stderr',
|
|
383
|
+
logger: options.logger
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
if (progress === undefined && result.code !== 0) {
|
|
177
387
|
throw new Error(`devcontainer up failed for ${context.workspaceFolder}`)
|
|
178
388
|
}
|
|
179
389
|
|
|
180
|
-
|
|
390
|
+
if (progress !== undefined) {
|
|
391
|
+
assertProgressCommandSucceeded('devcontainer up', result, `devcontainer up failed for ${context.workspaceFolder}`)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const containerId = parseContainerIdFromUpOutput(`${result.stdout}\n${result.stderr}`) ?? await findRunningContainerId(context, { logger: options.logger })
|
|
181
395
|
|
|
182
396
|
if (containerId === undefined) {
|
|
397
|
+
if (hasStartStep) {
|
|
398
|
+
progress?.failStep('devcontainer-start')
|
|
399
|
+
}
|
|
183
400
|
throw new Error(`Could not resolve devcontainer ID for ${context.workspaceFolder}`)
|
|
184
401
|
}
|
|
185
402
|
|
|
403
|
+
await recordContainerImageIfPresent(context, containerId, options.logger)
|
|
186
404
|
return containerId
|
|
187
405
|
}
|
|
188
406
|
|
|
189
|
-
export async function printPortHint (context: WorkspaceContext, containerId: string): Promise<void> {
|
|
407
|
+
export async function printPortHint (context: WorkspaceContext, containerId: string, options: { logger?: WorkspaceCommandLogger } = {}): Promise<void> {
|
|
190
408
|
const config = buildGeneratedDevcontainerConfig(context)
|
|
191
409
|
const containerPort = publishContainerPortFromConfig(config)
|
|
192
410
|
|
|
@@ -196,6 +414,7 @@ export async function printPortHint (context: WorkspaceContext, containerId: str
|
|
|
196
414
|
}
|
|
197
415
|
|
|
198
416
|
const result = await runBuffered('docker', ['port', containerId, `${containerPort}/tcp`], {
|
|
417
|
+
logger: options.logger,
|
|
199
418
|
mirrorStdout: false,
|
|
200
419
|
mirrorStderr: false
|
|
201
420
|
})
|
|
@@ -209,7 +428,7 @@ export async function printPortHint (context: WorkspaceContext, containerId: str
|
|
|
209
428
|
}
|
|
210
429
|
}
|
|
211
430
|
|
|
212
|
-
export async function openShell (context: WorkspaceContext): Promise<number> {
|
|
431
|
+
export async function openShell (context: WorkspaceContext, options: { logger?: WorkspaceCommandLogger } = {}): Promise<number> {
|
|
213
432
|
const cli = resolveDevcontainerCli(context)
|
|
214
433
|
process.stdout.write('Dropping into container shell...\n')
|
|
215
434
|
|
|
@@ -223,7 +442,7 @@ export async function openShell (context: WorkspaceContext): Promise<number> {
|
|
|
223
442
|
'bash',
|
|
224
443
|
'-c',
|
|
225
444
|
interactiveShellScript()
|
|
226
|
-
])
|
|
445
|
+
], { logger: options.logger })
|
|
227
446
|
}
|
|
228
447
|
|
|
229
448
|
export function codingAgentDevcontainerExecArgs (context: WorkspaceContext, agent: CodingAgentCli, agentArgs: string[] = []): string[] {
|
|
@@ -242,58 +461,140 @@ export function codingAgentDevcontainerExecArgs (context: WorkspaceContext, agen
|
|
|
242
461
|
]
|
|
243
462
|
}
|
|
244
463
|
|
|
245
|
-
export async function openCodingAgentCli (context: WorkspaceContext, agent: CodingAgentCli, agentArgs: string[] = []): Promise<number> {
|
|
464
|
+
export async function openCodingAgentCli (context: WorkspaceContext, agent: CodingAgentCli, agentArgs: string[] = [], options: { logger?: WorkspaceCommandLogger } = {}): Promise<number> {
|
|
246
465
|
const cli = resolveDevcontainerCli(context)
|
|
247
466
|
process.stdout.write(`Dropping into ${codingAgentBinary(agent)} inside the devcontainer...\n`)
|
|
248
467
|
|
|
249
468
|
return runInteractive(cli.command, [
|
|
250
469
|
...cli.argsPrefix,
|
|
251
470
|
...codingAgentDevcontainerExecArgs(context, agent, agentArgs)
|
|
252
|
-
])
|
|
471
|
+
], { logger: options.logger })
|
|
253
472
|
}
|
|
254
473
|
|
|
255
|
-
export async function ensureContainerSshRuntime (context: WorkspaceContext): Promise<void> {
|
|
474
|
+
export async function ensureContainerSshRuntime (context: WorkspaceContext, options: ContainerCommandOptions = {}): Promise<void> {
|
|
256
475
|
const cli = resolveDevcontainerCli(context)
|
|
257
|
-
const
|
|
476
|
+
const args = [
|
|
258
477
|
...cli.argsPrefix,
|
|
259
478
|
'exec',
|
|
260
479
|
...devcontainerWorkspaceArgs(context),
|
|
261
480
|
'--',
|
|
481
|
+
...containerProgressEnvArgs(options.progress),
|
|
262
482
|
'bash',
|
|
263
483
|
`${BOXDOWN_CONTAINER_DEVCONTAINER_DIR}/utils/ssh-bootstrap.sh`,
|
|
264
484
|
'runtime'
|
|
265
|
-
]
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
485
|
+
]
|
|
486
|
+
const result = options.progress === undefined
|
|
487
|
+
? await runBuffered(cli.command, args, {
|
|
488
|
+
mirrorStdout: 'stderr',
|
|
489
|
+
mirrorStderr: 'stderr',
|
|
490
|
+
logger: options.logger
|
|
491
|
+
})
|
|
492
|
+
: await runProgressCommand('prepare SSH runtime', cli.command, args, {
|
|
493
|
+
progress: options.progress,
|
|
494
|
+
spinnerLabel: 'Preparing container SSH runtime',
|
|
495
|
+
stepId: 'ssh-runtime',
|
|
496
|
+
verboseStdout: 'stderr',
|
|
497
|
+
verboseStderr: 'stderr',
|
|
498
|
+
logger: options.logger
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
if (options.progress === undefined && result.code !== 0) {
|
|
271
502
|
throw new Error('Failed to prepare devcontainer SSH runtime')
|
|
272
503
|
}
|
|
504
|
+
|
|
505
|
+
if (options.progress !== undefined) {
|
|
506
|
+
assertProgressCommandSucceeded('prepare SSH runtime', result, 'Failed to prepare devcontainer SSH runtime')
|
|
507
|
+
}
|
|
273
508
|
}
|
|
274
509
|
|
|
275
|
-
export async function refreshContainerCodingAgentClis (
|
|
510
|
+
export async function refreshContainerCodingAgentClis (
|
|
511
|
+
context: WorkspaceContext,
|
|
512
|
+
proxyMode = false,
|
|
513
|
+
agents: CodingAgentCli[] = [],
|
|
514
|
+
options: ContainerCommandOptions = {}
|
|
515
|
+
): Promise<void> {
|
|
276
516
|
const cli = resolveDevcontainerCli(context)
|
|
277
|
-
const
|
|
517
|
+
const spinnerLabel = agents.length === 0
|
|
518
|
+
? 'Refreshing default coding-agent CLIs'
|
|
519
|
+
: `Refreshing coding-agent CLIs: ${agents.map(codingAgentBinary).join(', ')}`
|
|
520
|
+
const args = [
|
|
278
521
|
...cli.argsPrefix,
|
|
279
522
|
'exec',
|
|
280
523
|
...devcontainerWorkspaceArgs(context),
|
|
281
524
|
'--',
|
|
525
|
+
...containerProgressEnvArgs(options.progress),
|
|
282
526
|
'bash',
|
|
283
527
|
`${BOXDOWN_CONTAINER_DEVCONTAINER_DIR}/utils/coding-agent-cli-update.sh`,
|
|
284
528
|
'maybe-update',
|
|
285
529
|
...agents
|
|
286
|
-
]
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
530
|
+
]
|
|
531
|
+
const result = options.progress === undefined
|
|
532
|
+
? await runBuffered(cli.command, args, {
|
|
533
|
+
mirrorStdout: proxyMode ? 'stderr' : 'stdout',
|
|
534
|
+
mirrorStderr: 'stderr',
|
|
535
|
+
logger: options.logger
|
|
536
|
+
})
|
|
537
|
+
: await runProgressCommand('refresh coding-agent CLIs', cli.command, args, {
|
|
538
|
+
progress: options.progress,
|
|
539
|
+
spinnerLabel,
|
|
540
|
+
stepId: 'coding-agent-refresh',
|
|
541
|
+
verboseStdout: proxyMode ? 'stderr' : 'stdout',
|
|
542
|
+
verboseStderr: 'stderr',
|
|
543
|
+
logger: options.logger
|
|
544
|
+
})
|
|
290
545
|
|
|
291
546
|
if (result.code !== 0) {
|
|
292
|
-
|
|
547
|
+
if (options.progress === undefined) {
|
|
548
|
+
process.stderr.write('Warning: could not refresh one or more coding-agent CLIs inside the devcontainer.\n')
|
|
549
|
+
} else {
|
|
550
|
+
options.progress.warn('Could not refresh one or more coding-agent CLIs inside the devcontainer.')
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export async function ensureContainerCodingAgentCli (
|
|
556
|
+
context: WorkspaceContext,
|
|
557
|
+
agent: CodingAgentCli,
|
|
558
|
+
options: ContainerCommandOptions = {}
|
|
559
|
+
): Promise<void> {
|
|
560
|
+
const cli = resolveDevcontainerCli(context)
|
|
561
|
+
const spinnerLabel = `Preparing ${codingAgentBinary(agent)} inside the devcontainer`
|
|
562
|
+
const args = [
|
|
563
|
+
...cli.argsPrefix,
|
|
564
|
+
'exec',
|
|
565
|
+
...devcontainerWorkspaceArgs(context),
|
|
566
|
+
'--',
|
|
567
|
+
...containerProgressEnvArgs(options.progress),
|
|
568
|
+
'bash',
|
|
569
|
+
`${BOXDOWN_CONTAINER_DEVCONTAINER_DIR}/utils/coding-agent-cli-update.sh`,
|
|
570
|
+
'ensure',
|
|
571
|
+
agent
|
|
572
|
+
]
|
|
573
|
+
const result = options.progress === undefined
|
|
574
|
+
? await runBuffered(cli.command, args, {
|
|
575
|
+
mirrorStdout: 'stdout',
|
|
576
|
+
mirrorStderr: 'stderr',
|
|
577
|
+
logger: options.logger
|
|
578
|
+
})
|
|
579
|
+
: await runProgressCommand(`prepare ${codingAgentBinary(agent)}`, cli.command, args, {
|
|
580
|
+
progress: options.progress,
|
|
581
|
+
spinnerLabel,
|
|
582
|
+
stepId: 'agent-cli',
|
|
583
|
+
verboseStdout: 'stdout',
|
|
584
|
+
verboseStderr: 'stderr',
|
|
585
|
+
logger: options.logger
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
if (options.progress === undefined && result.code !== 0) {
|
|
589
|
+
throw new Error(`Could not install or refresh ${codingAgentBinary(agent)} inside the devcontainer`)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (options.progress !== undefined) {
|
|
593
|
+
assertProgressCommandSucceeded(`prepare ${codingAgentBinary(agent)}`, result, `Could not install or refresh ${codingAgentBinary(agent)} inside the devcontainer`)
|
|
293
594
|
}
|
|
294
595
|
}
|
|
295
596
|
|
|
296
|
-
export async function runSshdProxy (containerId: string): Promise<number> {
|
|
597
|
+
export async function runSshdProxy (containerId: string, options: { logger?: WorkspaceCommandLogger } = {}): Promise<number> {
|
|
297
598
|
return runInteractive('docker', [
|
|
298
599
|
'exec',
|
|
299
600
|
'-i',
|
|
@@ -314,7 +615,28 @@ export async function runSshdProxy (containerId: string): Promise<number> {
|
|
|
314
615
|
'AllowStreamLocalForwarding=yes',
|
|
315
616
|
'-o',
|
|
316
617
|
'PermitTTY=yes'
|
|
317
|
-
])
|
|
618
|
+
], { logger: options.logger })
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function sshTunnelArgs (alias: string, ports: TunnelPortForward[], options: SshTunnelOptions = {}): string[] {
|
|
622
|
+
const bindAddress = options.bindAddress ?? '127.0.0.1'
|
|
623
|
+
const remoteHost = options.remoteHost ?? 'localhost'
|
|
624
|
+
|
|
625
|
+
return [
|
|
626
|
+
'-N',
|
|
627
|
+
'-o',
|
|
628
|
+
'ExitOnForwardFailure=yes',
|
|
629
|
+
...ports.flatMap((port) => [
|
|
630
|
+
'-L',
|
|
631
|
+
`${bindAddress}:${port.localPort}:${remoteHost}:${port.remotePort}`
|
|
632
|
+
]),
|
|
633
|
+
alias
|
|
634
|
+
]
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export async function openSshTunnel (alias: string, ports: TunnelPortForward[], options: SshTunnelOptions & { logger?: WorkspaceCommandLogger } = {}): Promise<number> {
|
|
638
|
+
const { logger, ...tunnelOptions } = options
|
|
639
|
+
return runInteractive('ssh', sshTunnelArgs(alias, ports, tunnelOptions), { logger })
|
|
318
640
|
}
|
|
319
641
|
|
|
320
642
|
async function hostGhTokenOrEmpty (): Promise<string> {
|
|
@@ -330,18 +652,80 @@ async function hostGhTokenOrEmpty (): Promise<string> {
|
|
|
330
652
|
return result.stdout.trim()
|
|
331
653
|
}
|
|
332
654
|
|
|
333
|
-
export async function refreshContainerGhAuth (context: WorkspaceContext): Promise<void> {
|
|
334
|
-
|
|
335
|
-
|
|
655
|
+
export async function refreshContainerGhAuth (context: WorkspaceContext, options: ContainerCommandOptions = {}): Promise<void> {
|
|
656
|
+
const progress = options.progress
|
|
657
|
+
const hasConfigStep = progress?.hasStep('gh-auth-config') === true
|
|
658
|
+
const hasTokenStep = progress?.hasStep('gh-token-read') === true
|
|
659
|
+
const hasGitAuthStep = progress?.hasStep('gh-git-auth') === true
|
|
660
|
+
|
|
661
|
+
if (hasConfigStep) {
|
|
662
|
+
progress?.startStep('gh-auth-config')
|
|
663
|
+
} else {
|
|
664
|
+
progress?.item('Preparing generated config for GitHub auth refresh')
|
|
665
|
+
}
|
|
336
666
|
|
|
337
|
-
|
|
667
|
+
try {
|
|
668
|
+
await ensureHostSshKey(context, {
|
|
669
|
+
quiet: true,
|
|
670
|
+
progress
|
|
671
|
+
})
|
|
672
|
+
const signingPlan = await resolveGitSigningPlan(context)
|
|
673
|
+
reportGitSigningPlan(signingPlan, {
|
|
674
|
+
logger: options.logger,
|
|
675
|
+
...(progress === undefined
|
|
676
|
+
? {}
|
|
677
|
+
: {
|
|
678
|
+
writeWarning: (message: string) => {
|
|
679
|
+
progress.warn(message.trimEnd())
|
|
680
|
+
}
|
|
681
|
+
})
|
|
682
|
+
})
|
|
683
|
+
writeGeneratedDevcontainerConfig(context, signingPlan)
|
|
684
|
+
if (hasConfigStep) {
|
|
685
|
+
progress?.completeStep('gh-auth-config')
|
|
686
|
+
}
|
|
687
|
+
} catch (error) {
|
|
688
|
+
if (hasConfigStep) {
|
|
689
|
+
progress?.failStep('gh-auth-config')
|
|
690
|
+
}
|
|
691
|
+
throw error
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (hasTokenStep) {
|
|
695
|
+
progress?.startStep('gh-token-read')
|
|
696
|
+
} else {
|
|
697
|
+
progress?.item('Reading host GitHub CLI token')
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
let token: string
|
|
701
|
+
try {
|
|
702
|
+
token = await hostGhTokenOrEmpty()
|
|
703
|
+
if (hasTokenStep) {
|
|
704
|
+
progress?.completeStep('gh-token-read')
|
|
705
|
+
}
|
|
706
|
+
} catch (error) {
|
|
707
|
+
if (hasTokenStep) {
|
|
708
|
+
progress?.failStep('gh-token-read')
|
|
709
|
+
}
|
|
710
|
+
throw error
|
|
711
|
+
}
|
|
338
712
|
|
|
339
713
|
if (token.length === 0) {
|
|
714
|
+
progress?.skipStep('gh-auth-refresh')
|
|
715
|
+
progress?.skipStep('gh-git-auth')
|
|
716
|
+
progress?.skipStep('gh-auth-verify')
|
|
717
|
+
if (progress === undefined) {
|
|
718
|
+
process.stderr.write('Warning: Host GitHub CLI token unavailable; skipping container auth refresh.\n')
|
|
719
|
+
} else {
|
|
720
|
+
progress.warn('Host GitHub CLI token unavailable; skipping container auth refresh.')
|
|
721
|
+
}
|
|
340
722
|
return
|
|
341
723
|
}
|
|
342
724
|
|
|
725
|
+
options.logger?.addRedaction(token)
|
|
726
|
+
|
|
343
727
|
const cli = resolveDevcontainerCli(context)
|
|
344
|
-
const
|
|
728
|
+
const loginArgs = [
|
|
345
729
|
...cli.argsPrefix,
|
|
346
730
|
'exec',
|
|
347
731
|
...devcontainerWorkspaceArgs(context),
|
|
@@ -355,23 +739,67 @@ export async function refreshContainerGhAuth (context: WorkspaceContext): Promis
|
|
|
355
739
|
'https',
|
|
356
740
|
'--with-token',
|
|
357
741
|
'--insecure-storage'
|
|
358
|
-
]
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
742
|
+
]
|
|
743
|
+
const login = progress === undefined
|
|
744
|
+
? await runBuffered(cli.command, loginArgs, {
|
|
745
|
+
input: `${token}\n`,
|
|
746
|
+
logger: options.logger,
|
|
747
|
+
mirrorStdout: false,
|
|
748
|
+
mirrorStderr: false
|
|
749
|
+
})
|
|
750
|
+
: await runProgressCommand('refresh GitHub CLI auth', cli.command, loginArgs, {
|
|
751
|
+
input: `${token}\n`,
|
|
752
|
+
progress,
|
|
753
|
+
logger: options.logger,
|
|
754
|
+
spinnerLabel: 'Refreshing GitHub CLI auth inside the devcontainer',
|
|
755
|
+
stepId: 'gh-auth-refresh',
|
|
756
|
+
verboseStdout: false,
|
|
757
|
+
verboseStderr: false
|
|
758
|
+
})
|
|
363
759
|
|
|
364
760
|
if (login.code !== 0) {
|
|
365
|
-
|
|
761
|
+
progress?.skipStep('gh-git-auth')
|
|
762
|
+
progress?.skipStep('gh-auth-verify')
|
|
763
|
+
if (progress === undefined) {
|
|
764
|
+
process.stderr.write('Warning: could not refresh GitHub CLI auth inside the devcontainer.\n')
|
|
765
|
+
} else {
|
|
766
|
+
progress.warn('Could not refresh GitHub CLI auth inside the devcontainer.')
|
|
767
|
+
}
|
|
366
768
|
return
|
|
367
769
|
}
|
|
368
770
|
|
|
369
|
-
|
|
771
|
+
if (hasGitAuthStep) {
|
|
772
|
+
progress?.startStep('gh-git-auth')
|
|
773
|
+
} else {
|
|
774
|
+
progress?.item('Configuring workspace GitHub Git auth')
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
let gitAuthConfigured: boolean
|
|
778
|
+
try {
|
|
779
|
+
gitAuthConfigured = await configureWorkspaceGithubGitAuth(context.workspaceFolder)
|
|
780
|
+
if (hasGitAuthStep) {
|
|
781
|
+
if (gitAuthConfigured) {
|
|
782
|
+
progress?.completeStep('gh-git-auth')
|
|
783
|
+
} else {
|
|
784
|
+
progress?.failStep('gh-git-auth')
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
} catch (error) {
|
|
788
|
+
if (hasGitAuthStep) {
|
|
789
|
+
progress?.failStep('gh-git-auth')
|
|
790
|
+
}
|
|
791
|
+
throw error
|
|
792
|
+
}
|
|
793
|
+
|
|
370
794
|
if (!gitAuthConfigured) {
|
|
371
|
-
|
|
795
|
+
if (progress === undefined) {
|
|
796
|
+
process.stderr.write('Warning: GitHub CLI auth refreshed, but GitHub Git auth was not configured for this workspace.\n')
|
|
797
|
+
} else {
|
|
798
|
+
progress.warn('GitHub CLI auth refreshed, but GitHub Git auth was not configured for this workspace.')
|
|
799
|
+
}
|
|
372
800
|
}
|
|
373
801
|
|
|
374
|
-
const
|
|
802
|
+
const verifyArgs = [
|
|
375
803
|
...cli.argsPrefix,
|
|
376
804
|
'exec',
|
|
377
805
|
...devcontainerWorkspaceArgs(context),
|
|
@@ -381,14 +809,33 @@ export async function refreshContainerGhAuth (context: WorkspaceContext): Promis
|
|
|
381
809
|
'status',
|
|
382
810
|
'--hostname',
|
|
383
811
|
'github.com'
|
|
384
|
-
]
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
812
|
+
]
|
|
813
|
+
const verify = progress === undefined
|
|
814
|
+
? await runBuffered(cli.command, verifyArgs, {
|
|
815
|
+
mirrorStdout: false,
|
|
816
|
+
mirrorStderr: false,
|
|
817
|
+
logger: options.logger
|
|
818
|
+
})
|
|
819
|
+
: await runProgressCommand('verify GitHub CLI auth', cli.command, verifyArgs, {
|
|
820
|
+
progress,
|
|
821
|
+
logger: options.logger,
|
|
822
|
+
spinnerLabel: 'Verifying GitHub CLI auth inside the devcontainer',
|
|
823
|
+
stepId: 'gh-auth-verify',
|
|
824
|
+
verboseStdout: false,
|
|
825
|
+
verboseStderr: false
|
|
826
|
+
})
|
|
388
827
|
|
|
389
828
|
if (verify.code === 0) {
|
|
390
|
-
|
|
829
|
+
if (progress === undefined) {
|
|
830
|
+
process.stdout.write('GitHub CLI auth refreshed inside the devcontainer.\n')
|
|
831
|
+
} else if (!progress.hasStep('gh-auth-verify')) {
|
|
832
|
+
progress.item('GitHub CLI auth refreshed inside the devcontainer')
|
|
833
|
+
}
|
|
391
834
|
} else {
|
|
392
|
-
|
|
835
|
+
if (progress === undefined) {
|
|
836
|
+
process.stderr.write('Warning: GitHub CLI auth refresh completed, but verification failed.\n')
|
|
837
|
+
} else {
|
|
838
|
+
progress.warn('GitHub CLI auth refresh completed, but verification failed.')
|
|
839
|
+
}
|
|
393
840
|
}
|
|
394
841
|
}
|