boxdown 1.0.0 → 1.2.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 +135 -12
- package/assets/devcontainer/README.md +49 -17
- package/assets/devcontainer/devcontainer.json +24 -7
- package/assets/devcontainer/hooks/initialize.sh +32 -0
- package/assets/devcontainer/hooks/post-create.sh +59 -12
- package/assets/devcontainer/hooks/post-start.sh +21 -4
- 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 +56 -0
- package/assets/devcontainer/utils/python-bootstrap.sh +69 -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-Df4E8ARj.cjs +5498 -0
- package/dist/main-XMBsKjIK.mjs +5458 -0
- package/dist/main-XMBsKjIK.mjs.map +1 -0
- package/dist/main.cjs +5 -2
- package/dist/main.d.cts +495 -4
- package/dist/main.d.cts.map +1 -1
- package/dist/main.d.mts +495 -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 +23 -0
- package/docs/features/generated-config-and-state.md +57 -4
- package/docs/features/github-auth-refresh.md +12 -0
- package/docs/features/lifecycle.md +97 -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 +40 -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/specs/2026-07-11-default-commit-signing-design.md +396 -0
- package/docs/superpowers/specs/2026-07-11-progress-ci-regression-design.md +43 -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 +68 -10
- package/src/constants.ts +5 -0
- package/src/devcontainer.ts +500 -64
- package/src/doctor.ts +195 -30
- package/src/git-signing.ts +87 -0
- package/src/interactive-prompts.ts +692 -0
- package/src/list.ts +16 -2
- package/src/logging.ts +154 -0
- package/src/main.ts +1213 -63
- package/src/metadata.ts +52 -3
- package/src/package-info.ts +18 -0
- package/src/paths.ts +50 -10
- package/src/process.ts +80 -2
- package/src/progress.ts +593 -0
- package/src/purge.ts +208 -0
- package/src/shell.ts +19 -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 { 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,36 @@ 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
|
+
interface DockerInspectContainer {
|
|
47
|
+
Image?: unknown
|
|
48
|
+
Config?: unknown
|
|
49
|
+
}
|
|
50
|
+
|
|
20
51
|
function devcontainerWorkspaceArgs (context: WorkspaceContext): string[] {
|
|
21
52
|
return [
|
|
22
53
|
'--workspace-folder',
|
|
@@ -34,11 +65,23 @@ function log (message: string, proxyMode = false): void {
|
|
|
34
65
|
}
|
|
35
66
|
}
|
|
36
67
|
|
|
68
|
+
function containerProgressEnvArgs (progress?: ProgressReporter): string[] {
|
|
69
|
+
if (progress === undefined) {
|
|
70
|
+
return []
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [
|
|
74
|
+
'env',
|
|
75
|
+
`BOXDOWN_VERBOSE=${progress.verbose ? '1' : '0'}`,
|
|
76
|
+
`BOXDOWN_PROGRESS=${progress.verbose ? '0' : '1'}`
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
|
|
37
80
|
export function parseContainerIdFromUpOutput (output: string): string | undefined {
|
|
38
81
|
return /"containerId"\s*:\s*"([^"]+)"/.exec(output)?.[1]
|
|
39
82
|
}
|
|
40
83
|
|
|
41
|
-
export async function findWorkspaceContainer (context: WorkspaceContext): Promise<ContainerSummary | undefined> {
|
|
84
|
+
export async function findWorkspaceContainer (context: WorkspaceContext, options: { logger?: WorkspaceCommandLogger } = {}): Promise<ContainerSummary | undefined> {
|
|
42
85
|
const result = await runBuffered('docker', [
|
|
43
86
|
'ps',
|
|
44
87
|
'-a',
|
|
@@ -47,6 +90,7 @@ export async function findWorkspaceContainer (context: WorkspaceContext): Promis
|
|
|
47
90
|
'--format',
|
|
48
91
|
'{{json .}}'
|
|
49
92
|
], {
|
|
93
|
+
logger: options.logger,
|
|
50
94
|
mirrorStdout: false,
|
|
51
95
|
mirrorStderr: false
|
|
52
96
|
})
|
|
@@ -78,7 +122,7 @@ export async function listWorkspaceContainers (): Promise<ContainerSummary[] | u
|
|
|
78
122
|
return parseDockerPsJsonLines(result.stdout)
|
|
79
123
|
}
|
|
80
124
|
|
|
81
|
-
export async function findRunningContainerId (context: WorkspaceContext): Promise<string | undefined> {
|
|
125
|
+
export async function findRunningContainerId (context: WorkspaceContext, options: { logger?: WorkspaceCommandLogger } = {}): Promise<string | undefined> {
|
|
82
126
|
const result = await runBuffered('docker', [
|
|
83
127
|
'ps',
|
|
84
128
|
'--filter',
|
|
@@ -86,6 +130,7 @@ export async function findRunningContainerId (context: WorkspaceContext): Promis
|
|
|
86
130
|
'--format',
|
|
87
131
|
'{{.ID}}'
|
|
88
132
|
], {
|
|
133
|
+
logger: options.logger,
|
|
89
134
|
mirrorStdout: false,
|
|
90
135
|
mirrorStderr: false
|
|
91
136
|
})
|
|
@@ -97,8 +142,72 @@ export async function findRunningContainerId (context: WorkspaceContext): Promis
|
|
|
97
142
|
return result.stdout.split(/\r?\n/).find((line) => line.length > 0)
|
|
98
143
|
}
|
|
99
144
|
|
|
100
|
-
|
|
101
|
-
|
|
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()
|
|
151
|
+
|
|
152
|
+
if (trimmed.length === 0) {
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let parsed: DockerInspectContainer
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(trimmed) as DockerInspectContainer
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new Error(`Could not parse docker inspect output for ${containerId}`, { cause: error })
|
|
162
|
+
}
|
|
163
|
+
|
|
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
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
id: parsed.Image,
|
|
174
|
+
...(configImage === undefined ? {} : { name: configImage })
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function inspectContainerImage (containerId: string, options: { logger?: WorkspaceCommandLogger } = {}): Promise<DockerImageInfo | undefined> {
|
|
179
|
+
const result = await runBuffered('docker', [
|
|
180
|
+
'inspect',
|
|
181
|
+
'--format',
|
|
182
|
+
'{{json .}}',
|
|
183
|
+
containerId
|
|
184
|
+
], {
|
|
185
|
+
logger: options.logger,
|
|
186
|
+
mirrorStdout: false,
|
|
187
|
+
mirrorStderr: false
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
if (result.code !== 0) {
|
|
191
|
+
throw new Error(`Could not inspect devcontainer image for ${containerId}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return parseDockerInspectImage(result.stdout, containerId)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function recordContainerImageIfPresent (context: WorkspaceContext, containerId: string, logger?: WorkspaceCommandLogger): Promise<void> {
|
|
198
|
+
try {
|
|
199
|
+
const image = await inspectContainerImage(containerId, { logger })
|
|
200
|
+
|
|
201
|
+
if (image !== undefined) {
|
|
202
|
+
recordWorkspaceDockerImage(context, image)
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
process.stderr.write(`Warning: could not record devcontainer image metadata for ${containerId}.\n`)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function stopWorkspaceContainer (context: WorkspaceContext, options: { logger?: WorkspaceCommandLogger } = {}): Promise<void> {
|
|
210
|
+
const container = await findWorkspaceContainer(context, { logger: options.logger })
|
|
102
211
|
|
|
103
212
|
if (container === undefined) {
|
|
104
213
|
process.stdout.write(`No devcontainer found for: ${context.workspaceFolder}\n`)
|
|
@@ -111,6 +220,7 @@ export async function stopWorkspaceContainer (context: WorkspaceContext): Promis
|
|
|
111
220
|
}
|
|
112
221
|
|
|
113
222
|
const result = await runBuffered('docker', ['stop', container.id], {
|
|
223
|
+
logger: options.logger,
|
|
114
224
|
mirrorStdout: false,
|
|
115
225
|
mirrorStderr: false
|
|
116
226
|
})
|
|
@@ -122,41 +232,127 @@ export async function stopWorkspaceContainer (context: WorkspaceContext): Promis
|
|
|
122
232
|
process.stdout.write(`Stopped devcontainer: ${container.id}\n`)
|
|
123
233
|
}
|
|
124
234
|
|
|
125
|
-
export async function removeWorkspaceContainer (context: WorkspaceContext): Promise<void> {
|
|
126
|
-
const container = await findWorkspaceContainer(context)
|
|
235
|
+
export async function removeWorkspaceContainer (context: WorkspaceContext, options: { logger?: WorkspaceCommandLogger } = {}): Promise<void> {
|
|
236
|
+
const container = await findWorkspaceContainer(context, { logger: options.logger })
|
|
127
237
|
|
|
128
238
|
if (container === undefined) {
|
|
129
239
|
process.stdout.write(`No devcontainer found for: ${context.workspaceFolder}\n`)
|
|
130
240
|
return
|
|
131
241
|
}
|
|
132
242
|
|
|
133
|
-
|
|
243
|
+
await removeContainerById(container.id, { logger: options.logger })
|
|
244
|
+
process.stdout.write(`Removed devcontainer: ${container.id}\n`)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function removeContainerById (containerId: string, options: { volumes?: boolean, logger?: WorkspaceCommandLogger } = {}): Promise<void> {
|
|
248
|
+
const result = await runBuffered('docker', [
|
|
249
|
+
'rm',
|
|
250
|
+
'-f',
|
|
251
|
+
...(options.volumes === true ? ['-v'] : []),
|
|
252
|
+
containerId
|
|
253
|
+
], {
|
|
254
|
+
logger: options.logger,
|
|
134
255
|
mirrorStdout: false,
|
|
135
256
|
mirrorStderr: false
|
|
136
257
|
})
|
|
137
258
|
|
|
138
259
|
if (result.code !== 0) {
|
|
139
|
-
throw new Error(`Could not remove devcontainer ${
|
|
260
|
+
throw new Error(`Could not remove devcontainer ${containerId}`)
|
|
140
261
|
}
|
|
262
|
+
}
|
|
141
263
|
|
|
142
|
-
|
|
264
|
+
function dockerImageMissing (stderr: string): boolean {
|
|
265
|
+
return /No such image/i.test(stderr) || /not found/i.test(stderr)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function removeDockerImage (imageId: string, options: { logger?: WorkspaceCommandLogger } = {}): Promise<boolean> {
|
|
269
|
+
const result = await runBuffered('docker', ['image', 'rm', '-f', imageId], {
|
|
270
|
+
logger: options.logger,
|
|
271
|
+
mirrorStdout: false,
|
|
272
|
+
mirrorStderr: false
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
if (result.code !== 0) {
|
|
276
|
+
if (dockerImageMissing(result.stderr)) {
|
|
277
|
+
process.stdout.write(`Docker image already absent: ${imageId}\n`)
|
|
278
|
+
return false
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
throw new Error(`Could not remove Docker image ${imageId}`)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
process.stdout.write(`Removed Docker image: ${imageId}\n`)
|
|
285
|
+
return true
|
|
143
286
|
}
|
|
144
287
|
|
|
145
288
|
export async function startDevcontainer (context: WorkspaceContext, options: StartOptions = {}): Promise<string> {
|
|
146
|
-
|
|
147
|
-
|
|
289
|
+
const progress = options.progress
|
|
290
|
+
const proxyMode = options.proxyMode ?? false
|
|
291
|
+
const hasSshIdentityStep = progress?.hasStep('ssh-identity') === true
|
|
292
|
+
const hasConfigStep = progress?.hasStep('devcontainer-config') === true
|
|
293
|
+
const hasStartStep = progress?.hasStep('devcontainer-start') === true
|
|
294
|
+
|
|
295
|
+
if (hasSshIdentityStep) {
|
|
296
|
+
progress?.startStep('ssh-identity')
|
|
297
|
+
} else {
|
|
298
|
+
progress?.item('Preparing SSH identity')
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
await ensureHostSshKey(context, {
|
|
303
|
+
quiet: proxyMode,
|
|
304
|
+
progress
|
|
305
|
+
})
|
|
306
|
+
if (hasSshIdentityStep) {
|
|
307
|
+
progress?.completeStep('ssh-identity')
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (hasSshIdentityStep) {
|
|
311
|
+
progress?.failStep('ssh-identity')
|
|
312
|
+
}
|
|
313
|
+
throw error
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (hasConfigStep) {
|
|
317
|
+
progress?.startStep('devcontainer-config')
|
|
318
|
+
} else if (progress !== undefined) {
|
|
319
|
+
progress.item('Writing generated devcontainer config')
|
|
320
|
+
progress.detail(context.generatedConfigPath)
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
writeGeneratedDevcontainerConfig(context, await resolveGitSigningPlan(context))
|
|
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,69 @@ 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
|
+
}
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
await ensureHostSshKey(context, {
|
|
669
|
+
quiet: true,
|
|
670
|
+
progress
|
|
671
|
+
})
|
|
672
|
+
writeGeneratedDevcontainerConfig(context, await resolveGitSigningPlan(context))
|
|
673
|
+
if (hasConfigStep) {
|
|
674
|
+
progress?.completeStep('gh-auth-config')
|
|
675
|
+
}
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (hasConfigStep) {
|
|
678
|
+
progress?.failStep('gh-auth-config')
|
|
679
|
+
}
|
|
680
|
+
throw error
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (hasTokenStep) {
|
|
684
|
+
progress?.startStep('gh-token-read')
|
|
685
|
+
} else {
|
|
686
|
+
progress?.item('Reading host GitHub CLI token')
|
|
687
|
+
}
|
|
336
688
|
|
|
337
|
-
|
|
689
|
+
let token: string
|
|
690
|
+
try {
|
|
691
|
+
token = await hostGhTokenOrEmpty()
|
|
692
|
+
if (hasTokenStep) {
|
|
693
|
+
progress?.completeStep('gh-token-read')
|
|
694
|
+
}
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (hasTokenStep) {
|
|
697
|
+
progress?.failStep('gh-token-read')
|
|
698
|
+
}
|
|
699
|
+
throw error
|
|
700
|
+
}
|
|
338
701
|
|
|
339
702
|
if (token.length === 0) {
|
|
703
|
+
progress?.skipStep('gh-auth-refresh')
|
|
704
|
+
progress?.skipStep('gh-git-auth')
|
|
705
|
+
progress?.skipStep('gh-auth-verify')
|
|
706
|
+
if (progress === undefined) {
|
|
707
|
+
process.stderr.write('Warning: Host GitHub CLI token unavailable; skipping container auth refresh.\n')
|
|
708
|
+
} else {
|
|
709
|
+
progress.warn('Host GitHub CLI token unavailable; skipping container auth refresh.')
|
|
710
|
+
}
|
|
340
711
|
return
|
|
341
712
|
}
|
|
342
713
|
|
|
714
|
+
options.logger?.addRedaction(token)
|
|
715
|
+
|
|
343
716
|
const cli = resolveDevcontainerCli(context)
|
|
344
|
-
const
|
|
717
|
+
const loginArgs = [
|
|
345
718
|
...cli.argsPrefix,
|
|
346
719
|
'exec',
|
|
347
720
|
...devcontainerWorkspaceArgs(context),
|
|
@@ -355,23 +728,67 @@ export async function refreshContainerGhAuth (context: WorkspaceContext): Promis
|
|
|
355
728
|
'https',
|
|
356
729
|
'--with-token',
|
|
357
730
|
'--insecure-storage'
|
|
358
|
-
]
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
731
|
+
]
|
|
732
|
+
const login = progress === undefined
|
|
733
|
+
? await runBuffered(cli.command, loginArgs, {
|
|
734
|
+
input: `${token}\n`,
|
|
735
|
+
logger: options.logger,
|
|
736
|
+
mirrorStdout: false,
|
|
737
|
+
mirrorStderr: false
|
|
738
|
+
})
|
|
739
|
+
: await runProgressCommand('refresh GitHub CLI auth', cli.command, loginArgs, {
|
|
740
|
+
input: `${token}\n`,
|
|
741
|
+
progress,
|
|
742
|
+
logger: options.logger,
|
|
743
|
+
spinnerLabel: 'Refreshing GitHub CLI auth inside the devcontainer',
|
|
744
|
+
stepId: 'gh-auth-refresh',
|
|
745
|
+
verboseStdout: false,
|
|
746
|
+
verboseStderr: false
|
|
747
|
+
})
|
|
363
748
|
|
|
364
749
|
if (login.code !== 0) {
|
|
365
|
-
|
|
750
|
+
progress?.skipStep('gh-git-auth')
|
|
751
|
+
progress?.skipStep('gh-auth-verify')
|
|
752
|
+
if (progress === undefined) {
|
|
753
|
+
process.stderr.write('Warning: could not refresh GitHub CLI auth inside the devcontainer.\n')
|
|
754
|
+
} else {
|
|
755
|
+
progress.warn('Could not refresh GitHub CLI auth inside the devcontainer.')
|
|
756
|
+
}
|
|
366
757
|
return
|
|
367
758
|
}
|
|
368
759
|
|
|
369
|
-
|
|
760
|
+
if (hasGitAuthStep) {
|
|
761
|
+
progress?.startStep('gh-git-auth')
|
|
762
|
+
} else {
|
|
763
|
+
progress?.item('Configuring workspace GitHub Git auth')
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
let gitAuthConfigured: boolean
|
|
767
|
+
try {
|
|
768
|
+
gitAuthConfigured = await configureWorkspaceGithubGitAuth(context.workspaceFolder)
|
|
769
|
+
if (hasGitAuthStep) {
|
|
770
|
+
if (gitAuthConfigured) {
|
|
771
|
+
progress?.completeStep('gh-git-auth')
|
|
772
|
+
} else {
|
|
773
|
+
progress?.failStep('gh-git-auth')
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
} catch (error) {
|
|
777
|
+
if (hasGitAuthStep) {
|
|
778
|
+
progress?.failStep('gh-git-auth')
|
|
779
|
+
}
|
|
780
|
+
throw error
|
|
781
|
+
}
|
|
782
|
+
|
|
370
783
|
if (!gitAuthConfigured) {
|
|
371
|
-
|
|
784
|
+
if (progress === undefined) {
|
|
785
|
+
process.stderr.write('Warning: GitHub CLI auth refreshed, but GitHub Git auth was not configured for this workspace.\n')
|
|
786
|
+
} else {
|
|
787
|
+
progress.warn('GitHub CLI auth refreshed, but GitHub Git auth was not configured for this workspace.')
|
|
788
|
+
}
|
|
372
789
|
}
|
|
373
790
|
|
|
374
|
-
const
|
|
791
|
+
const verifyArgs = [
|
|
375
792
|
...cli.argsPrefix,
|
|
376
793
|
'exec',
|
|
377
794
|
...devcontainerWorkspaceArgs(context),
|
|
@@ -381,14 +798,33 @@ export async function refreshContainerGhAuth (context: WorkspaceContext): Promis
|
|
|
381
798
|
'status',
|
|
382
799
|
'--hostname',
|
|
383
800
|
'github.com'
|
|
384
|
-
]
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
801
|
+
]
|
|
802
|
+
const verify = progress === undefined
|
|
803
|
+
? await runBuffered(cli.command, verifyArgs, {
|
|
804
|
+
mirrorStdout: false,
|
|
805
|
+
mirrorStderr: false,
|
|
806
|
+
logger: options.logger
|
|
807
|
+
})
|
|
808
|
+
: await runProgressCommand('verify GitHub CLI auth', cli.command, verifyArgs, {
|
|
809
|
+
progress,
|
|
810
|
+
logger: options.logger,
|
|
811
|
+
spinnerLabel: 'Verifying GitHub CLI auth inside the devcontainer',
|
|
812
|
+
stepId: 'gh-auth-verify',
|
|
813
|
+
verboseStdout: false,
|
|
814
|
+
verboseStderr: false
|
|
815
|
+
})
|
|
388
816
|
|
|
389
817
|
if (verify.code === 0) {
|
|
390
|
-
|
|
818
|
+
if (progress === undefined) {
|
|
819
|
+
process.stdout.write('GitHub CLI auth refreshed inside the devcontainer.\n')
|
|
820
|
+
} else if (!progress.hasStep('gh-auth-verify')) {
|
|
821
|
+
progress.item('GitHub CLI auth refreshed inside the devcontainer')
|
|
822
|
+
}
|
|
391
823
|
} else {
|
|
392
|
-
|
|
824
|
+
if (progress === undefined) {
|
|
825
|
+
process.stderr.write('Warning: GitHub CLI auth refresh completed, but verification failed.\n')
|
|
826
|
+
} else {
|
|
827
|
+
progress.warn('GitHub CLI auth refresh completed, but verification failed.')
|
|
828
|
+
}
|
|
393
829
|
}
|
|
394
830
|
}
|