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/doctor.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
2
3
|
|
|
3
4
|
import { resolveDevcontainerCli } from './devcontainer-cli.ts'
|
|
4
5
|
import type { WorkspaceContext } from './paths.ts'
|
|
@@ -12,6 +13,20 @@ export interface DoctorCheck {
|
|
|
12
13
|
message: string
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
export interface DoctorCommandResult {
|
|
17
|
+
code: number
|
|
18
|
+
stdout: string
|
|
19
|
+
stderr: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type DoctorCommandRunner = (command: string, args: string[]) => Promise<DoctorCommandResult>
|
|
23
|
+
|
|
24
|
+
export interface RunDoctorChecksOptions {
|
|
25
|
+
includeOptional?: boolean
|
|
26
|
+
includeDockerMountProbe?: boolean
|
|
27
|
+
runCommand?: DoctorCommandRunner
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
function nodeVersionPasses (version: string): boolean {
|
|
16
31
|
const major = Number.parseInt(version.split('.')[0] ?? '', 10)
|
|
17
32
|
return Number.isInteger(major) && major >= 24
|
|
@@ -25,26 +40,26 @@ function check (name: string, pass: boolean, okMessage: string, failMessage: str
|
|
|
25
40
|
}
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
async function
|
|
29
|
-
|
|
43
|
+
async function runDoctorCommand (command: string, args: string[]): Promise<DoctorCommandResult> {
|
|
44
|
+
return runBuffered(command, args, {
|
|
30
45
|
mirrorStdout: false,
|
|
31
46
|
mirrorStderr: false
|
|
32
47
|
})
|
|
48
|
+
}
|
|
33
49
|
|
|
50
|
+
async function commandWorks (runCommand: DoctorCommandRunner, command: string, args: string[]): Promise<boolean> {
|
|
51
|
+
const result = await runCommand(command, args)
|
|
34
52
|
return result.code === 0
|
|
35
53
|
}
|
|
36
54
|
|
|
37
|
-
async function commandExists (command: string, args: string[]): Promise<boolean> {
|
|
38
|
-
const result = await
|
|
39
|
-
mirrorStdout: false,
|
|
40
|
-
mirrorStderr: false
|
|
41
|
-
})
|
|
42
|
-
|
|
55
|
+
async function commandExists (runCommand: DoctorCommandRunner, command: string, args: string[]): Promise<boolean> {
|
|
56
|
+
const result = await runCommand(command, args)
|
|
43
57
|
return result.code !== 127
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
export async function runDoctorChecks (context: WorkspaceContext): Promise<DoctorCheck[]> {
|
|
60
|
+
export async function runDoctorChecks (context: WorkspaceContext, options: RunDoctorChecksOptions = {}): Promise<DoctorCheck[]> {
|
|
47
61
|
const checks: DoctorCheck[] = []
|
|
62
|
+
const runCommand = options.runCommand ?? runDoctorCommand
|
|
48
63
|
const nodeVersion = process.versions.node
|
|
49
64
|
|
|
50
65
|
checks.push(check(
|
|
@@ -54,37 +69,59 @@ export async function runDoctorChecks (context: WorkspaceContext): Promise<Docto
|
|
|
54
69
|
`Node ${nodeVersion}; expected >=24.0.0`
|
|
55
70
|
))
|
|
56
71
|
|
|
72
|
+
const sshAgent = await runCommand('ssh-add', ['-L'])
|
|
73
|
+
const identities = sshAgent.code === 0
|
|
74
|
+
? sshAgent.stdout.split(/\r?\n/).filter((line) => line.trim().startsWith('ssh-')).length
|
|
75
|
+
: 0
|
|
76
|
+
checks.push({
|
|
77
|
+
name: 'git-signing-agent',
|
|
78
|
+
level: sshAgent.code === 0 && identities === 1 ? 'ok' : 'warn',
|
|
79
|
+
message: sshAgent.code !== 0
|
|
80
|
+
? 'SSH agent is unavailable; Boxdown commits will remain unsigned'
|
|
81
|
+
: identities === 0
|
|
82
|
+
? 'SSH agent has no identities; Boxdown commits will remain unsigned'
|
|
83
|
+
: identities > 1
|
|
84
|
+
? 'SSH agent has multiple identities; Boxdown will not guess a signing key and commits will remain unsigned'
|
|
85
|
+
: 'One SSH agent identity is available for Boxdown commit signing'
|
|
86
|
+
})
|
|
87
|
+
|
|
57
88
|
checks.push(check(
|
|
58
89
|
'devcontainers-cli',
|
|
59
|
-
await packagedDevcontainerCliWorks(context),
|
|
90
|
+
await packagedDevcontainerCliWorks(context, runCommand),
|
|
60
91
|
'Packaged @devcontainers/cli is available',
|
|
61
92
|
'Packaged @devcontainers/cli is required but was not available'
|
|
62
93
|
))
|
|
63
94
|
|
|
95
|
+
const dockerCliWorks = await commandWorks(runCommand, 'docker', ['--version'])
|
|
64
96
|
checks.push(check(
|
|
65
97
|
'docker-cli',
|
|
66
|
-
|
|
98
|
+
dockerCliWorks,
|
|
67
99
|
'Docker CLI is available',
|
|
68
100
|
'Docker CLI is required but was not available'
|
|
69
101
|
))
|
|
70
102
|
|
|
103
|
+
const dockerDaemonWorks = await commandWorks(runCommand, 'docker', ['info'])
|
|
71
104
|
checks.push(check(
|
|
72
105
|
'docker-daemon',
|
|
73
|
-
|
|
106
|
+
dockerDaemonWorks,
|
|
74
107
|
'Docker daemon is reachable',
|
|
75
108
|
'Docker daemon is required but was not reachable'
|
|
76
109
|
))
|
|
77
110
|
|
|
78
111
|
checks.push(check(
|
|
79
112
|
'ssh',
|
|
80
|
-
await commandExists('ssh', ['-V']),
|
|
113
|
+
await commandExists(runCommand, 'ssh', ['-V']),
|
|
81
114
|
'ssh is available',
|
|
82
115
|
'ssh is required but was not available'
|
|
83
116
|
))
|
|
84
117
|
|
|
118
|
+
if (options.includeDockerMountProbe ?? true) {
|
|
119
|
+
checks.push(await checkDockerBindMounts(context, runCommand, dockerCliWorks && dockerDaemonWorks))
|
|
120
|
+
}
|
|
121
|
+
|
|
85
122
|
checks.push(check(
|
|
86
123
|
'ssh-keygen',
|
|
87
|
-
await commandExists('ssh-keygen', ['-?']),
|
|
124
|
+
await commandExists(runCommand, 'ssh-keygen', ['-?']),
|
|
88
125
|
'ssh-keygen is available',
|
|
89
126
|
'ssh-keygen is required but was not available'
|
|
90
127
|
))
|
|
@@ -96,28 +133,156 @@ export async function runDoctorChecks (context: WorkspaceContext): Promise<Docto
|
|
|
96
133
|
`Missing Boxdown devcontainer assets: ${context.assetsDevcontainerDir}`
|
|
97
134
|
))
|
|
98
135
|
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
136
|
+
if (options.includeOptional ?? true) {
|
|
137
|
+
if (await commandWorks(runCommand, 'gh', ['--version'])) {
|
|
138
|
+
const ghAuth = await commandWorks(runCommand, 'gh', ['auth', 'status', '--hostname', 'github.com'])
|
|
139
|
+
checks.push({
|
|
140
|
+
name: 'gh-auth',
|
|
141
|
+
level: ghAuth ? 'ok' : 'warn',
|
|
142
|
+
message: ghAuth ? 'GitHub CLI auth is available' : 'GitHub CLI is available but not authenticated'
|
|
143
|
+
})
|
|
144
|
+
if (ghAuth && identities === 1) {
|
|
145
|
+
const user = await runCommand('gh', ['api', 'user', '--jq', '.login'])
|
|
146
|
+
const signing = user.code === 0 && user.stdout.trim().length > 0
|
|
147
|
+
? await runCommand('gh', ['api', `users/${user.stdout.trim()}/ssh_signing_keys`, '--paginate', '--jq', '.[].key'])
|
|
148
|
+
: { code: 1, stdout: '', stderr: '' }
|
|
149
|
+
const identity = sshAgent.stdout.split(/\r?\n/).find((line) => line.trim().startsWith('ssh-'))?.trim()
|
|
150
|
+
checks.push({
|
|
151
|
+
name: 'git-signing-github',
|
|
152
|
+
level: signing.code === 0 && identity !== undefined && signing.stdout.includes(identity.split(/\s+/, 3).slice(0, 2).join(' ')) ? 'ok' : 'warn',
|
|
153
|
+
message: signing.code !== 0
|
|
154
|
+
? 'GitHub SSH signing-key registration could not be checked'
|
|
155
|
+
: signing.stdout.includes(identity?.split(/\s+/, 3).slice(0, 2).join(' ') ?? '')
|
|
156
|
+
? 'Selected SSH key is registered with GitHub for commit signing'
|
|
157
|
+
: 'Register the selected public key with GitHub as a signing key to receive Verified badges'
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
checks.push({
|
|
162
|
+
name: 'gh',
|
|
163
|
+
level: 'warn',
|
|
164
|
+
message: 'GitHub CLI is optional and was not available'
|
|
165
|
+
})
|
|
166
|
+
}
|
|
112
167
|
}
|
|
113
168
|
|
|
114
169
|
return checks
|
|
115
170
|
}
|
|
116
171
|
|
|
117
|
-
|
|
172
|
+
function dockerProbeImage (output: string): string | undefined {
|
|
173
|
+
return output
|
|
174
|
+
.split(/\r?\n/)
|
|
175
|
+
.map((line) => line.trim())
|
|
176
|
+
.find((image) => image.length > 0 && image !== '<none>:<none>')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function dockerMountError (output: string): boolean {
|
|
180
|
+
return /invalid mount config|bind source path does not exist|mount denied|file sharing|mounts denied|permission denied|operation not permitted/i.test(output)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function compactOutput (output: string): string {
|
|
184
|
+
return output.trim().replace(/\s+/g, ' ').slice(0, 300)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interface DockerMountSource {
|
|
188
|
+
label: string
|
|
189
|
+
path: string
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function checkDockerBindMounts (
|
|
193
|
+
context: WorkspaceContext,
|
|
194
|
+
runCommand: DoctorCommandRunner,
|
|
195
|
+
dockerReady: boolean
|
|
196
|
+
): Promise<DoctorCheck> {
|
|
197
|
+
if (!dockerReady) {
|
|
198
|
+
return {
|
|
199
|
+
name: 'docker-bind-mounts',
|
|
200
|
+
level: 'warn',
|
|
201
|
+
message: 'Docker bind-mount readiness was not checked because Docker is unavailable'
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const imageResult = await runCommand('docker', ['image', 'ls', '--format', '{{.Repository}}:{{.Tag}}'])
|
|
206
|
+
const image = imageResult.code === 0 ? dockerProbeImage(imageResult.stdout) : undefined
|
|
207
|
+
|
|
208
|
+
if (image === undefined) {
|
|
209
|
+
return {
|
|
210
|
+
name: 'docker-bind-mounts',
|
|
211
|
+
level: 'warn',
|
|
212
|
+
message: 'Docker bind-mount readiness was not checked because no local Docker image is available'
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
mkdirSync(context.workspaceDataDir, { recursive: true })
|
|
217
|
+
const runtimeProbeDir = mkdtempSync(join(context.workspaceDataDir, 'doctor-mount-probe-'))
|
|
218
|
+
const sources: DockerMountSource[] = [
|
|
219
|
+
{ label: 'workspace', path: context.workspaceFolder },
|
|
220
|
+
{ label: 'Boxdown devcontainer assets', path: context.assetsDevcontainerDir },
|
|
221
|
+
{ label: 'Boxdown runtime state', path: runtimeProbeDir }
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
for (const source of sources) {
|
|
226
|
+
const createResult = await runCommand('docker', [
|
|
227
|
+
'create',
|
|
228
|
+
'--pull=never',
|
|
229
|
+
'--entrypoint',
|
|
230
|
+
'/bin/true',
|
|
231
|
+
'--mount',
|
|
232
|
+
`type=bind,source=${source.path},target=/boxdown-preflight,readonly`,
|
|
233
|
+
image
|
|
234
|
+
])
|
|
235
|
+
const output = `${createResult.stderr}\n${createResult.stdout}`
|
|
236
|
+
|
|
237
|
+
if (createResult.code !== 0) {
|
|
238
|
+
if (dockerMountError(output)) {
|
|
239
|
+
return {
|
|
240
|
+
name: 'docker-bind-mounts',
|
|
241
|
+
level: 'fail',
|
|
242
|
+
message: `Docker cannot bind-mount the ${source.label} path (${source.path}). Check Docker Desktop file sharing and host-folder permissions.`
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
name: 'docker-bind-mounts',
|
|
248
|
+
level: 'warn',
|
|
249
|
+
message: `Docker bind-mount readiness could not be checked for ${source.label}: ${compactOutput(output) || 'Docker create failed'}`
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const containerId = createResult.stdout.trim().split(/\r?\n/)[0]
|
|
254
|
+
if (containerId === undefined || containerId.length === 0) {
|
|
255
|
+
return {
|
|
256
|
+
name: 'docker-bind-mounts',
|
|
257
|
+
level: 'warn',
|
|
258
|
+
message: `Docker bind-mount readiness could not be checked for ${source.label}: Docker did not return a container ID`
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const removeResult = await runCommand('docker', ['rm', '-f', containerId])
|
|
263
|
+
if (removeResult.code !== 0) {
|
|
264
|
+
return {
|
|
265
|
+
name: 'docker-bind-mounts',
|
|
266
|
+
level: 'warn',
|
|
267
|
+
message: `Docker bind-mount readiness was checked, but the disposable probe container could not be removed: ${compactOutput(removeResult.stderr) || 'docker rm failed'}`
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} finally {
|
|
272
|
+
rmSync(runtimeProbeDir, { recursive: true, force: true })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
name: 'docker-bind-mounts',
|
|
277
|
+
level: 'ok',
|
|
278
|
+
message: 'Docker can bind-mount Boxdown workspace, assets, and runtime-state paths'
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function packagedDevcontainerCliWorks (context: WorkspaceContext, runCommand: DoctorCommandRunner): Promise<boolean> {
|
|
118
283
|
try {
|
|
119
284
|
const cli = resolveDevcontainerCli(context)
|
|
120
|
-
return await commandWorks(cli.command, [...cli.argsPrefix, '--version'])
|
|
285
|
+
return await commandWorks(runCommand, cli.command, [...cli.argsPrefix, '--version'])
|
|
121
286
|
} catch {
|
|
122
287
|
return false
|
|
123
288
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { WorkspaceContext } from './paths.ts'
|
|
5
|
+
import { runBuffered } from './process.ts'
|
|
6
|
+
|
|
7
|
+
export type GitSigningReason = 'agent-unavailable' | 'no-identities' | 'ambiguous-identities' | 'configured-key-not-loaded' | 'agent-mount-unavailable'
|
|
8
|
+
|
|
9
|
+
export interface GitSigningPlan {
|
|
10
|
+
enabled: boolean
|
|
11
|
+
reason?: GitSigningReason
|
|
12
|
+
publicKey?: string
|
|
13
|
+
agentSocketSource?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseSshPublicKey (value: string): string | undefined {
|
|
17
|
+
const [algorithm, key] = value.trim().split(/\s+/, 3)
|
|
18
|
+
if (algorithm === undefined || key === undefined || !/^ssh-[A-Za-z0-9-]+$/.test(algorithm) || key.length === 0) {
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `${algorithm} ${key}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function selectGitSigningKey (identities: string[], configuredKey?: string, githubKeys?: string[]): { key?: string, reason?: GitSigningReason } {
|
|
26
|
+
const keys = identities.map(parseSshPublicKey).filter((key): key is string => key !== undefined)
|
|
27
|
+
if (keys.length === 0) return { reason: 'no-identities' }
|
|
28
|
+
|
|
29
|
+
const configured = configuredKey === undefined ? undefined : parseSshPublicKey(configuredKey)
|
|
30
|
+
if (configured !== undefined) {
|
|
31
|
+
return keys.includes(configured) ? { key: configured } : { reason: 'configured-key-not-loaded' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const github = new Set((githubKeys ?? []).map(parseSshPublicKey).filter((key): key is string => key !== undefined))
|
|
35
|
+
const matches = keys.filter((key) => github.has(key))
|
|
36
|
+
if (matches.length === 1) return { key: matches[0] }
|
|
37
|
+
if (keys.length === 1) return { key: keys[0] }
|
|
38
|
+
return { reason: 'ambiguous-identities' }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function writeGitSigningPublicKey (context: WorkspaceContext, key: string): void {
|
|
42
|
+
mkdirSync(context.gitSigningStateDir, { recursive: true, mode: 0o700 })
|
|
43
|
+
writeFileSync(join(context.gitSigningStateDir, 'signing-key.pub'), `${parseSshPublicKey(key) ?? key}\n`, { mode: 0o644 })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function dockerCanMountAgent (source: string): Promise<boolean> {
|
|
47
|
+
const images = await runBuffered('docker', ['image', 'ls', '--format', '{{.Repository}}:{{.Tag}}'], { mirrorStdout: false, mirrorStderr: false })
|
|
48
|
+
const image = images.stdout.split(/\r?\n/).map((value) => value.trim()).find((value) => value.length > 0 && value !== '<none>:<none>')
|
|
49
|
+
if (images.code !== 0 || image === undefined) return false
|
|
50
|
+
|
|
51
|
+
const created = await runBuffered('docker', ['create', '--pull=never', '--entrypoint', '/bin/true', '--mount', `type=bind,source=${source},target=/run/boxdown/ssh-agent.sock`, image], { mirrorStdout: false, mirrorStderr: false })
|
|
52
|
+
const containerId = created.stdout.trim().split(/\r?\n/)[0]
|
|
53
|
+
if (created.code !== 0 || containerId === undefined || containerId.length === 0) return false
|
|
54
|
+
await runBuffered('docker', ['rm', '-f', containerId], { mirrorStdout: false, mirrorStderr: false })
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function resolveGitSigningPlan (context: WorkspaceContext): Promise<GitSigningPlan> {
|
|
59
|
+
const result = await runBuffered('ssh-add', ['-L'], { mirrorStdout: false, mirrorStderr: false })
|
|
60
|
+
if (result.code !== 0) return { enabled: false, reason: 'agent-unavailable' }
|
|
61
|
+
|
|
62
|
+
const identities = result.stdout.split(/\r?\n/)
|
|
63
|
+
const format = await runBuffered('git', ['config', '--global', '--get', 'gpg.format'], { mirrorStdout: false, mirrorStderr: false })
|
|
64
|
+
const configuredKey = format.code === 0 && format.stdout.trim() === 'ssh'
|
|
65
|
+
? (await runBuffered('git', ['config', '--global', '--get', 'user.signingkey'], { mirrorStdout: false, mirrorStderr: false })).stdout.trim()
|
|
66
|
+
: undefined
|
|
67
|
+
let githubKeys: string[] | undefined
|
|
68
|
+
if (identities.filter((key) => parseSshPublicKey(key) !== undefined).length > 1) {
|
|
69
|
+
const user = await runBuffered('gh', ['api', 'user', '--jq', '.login'], { mirrorStdout: false, mirrorStderr: false })
|
|
70
|
+
const login = user.stdout.trim()
|
|
71
|
+
if (user.code === 0 && login.length > 0) {
|
|
72
|
+
const github = await runBuffered('gh', ['api', `users/${login}/keys`, '--paginate', '--jq', '.[].key'], { mirrorStdout: false, mirrorStderr: false })
|
|
73
|
+
if (github.code === 0) githubKeys = github.stdout.split(/\r?\n/)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const selected = selectGitSigningKey(identities, configuredKey, githubKeys)
|
|
77
|
+
if (selected.key === undefined) return { enabled: false, reason: selected.reason }
|
|
78
|
+
|
|
79
|
+
const agentSocketSource = process.platform === 'darwin'
|
|
80
|
+
? '/run/host-services/ssh-auth.sock'
|
|
81
|
+
: process.env.SSH_AUTH_SOCK
|
|
82
|
+
if (agentSocketSource === undefined || agentSocketSource.length === 0) return { enabled: false, reason: 'agent-mount-unavailable' }
|
|
83
|
+
if (!await dockerCanMountAgent(agentSocketSource)) return { enabled: false, reason: 'agent-mount-unavailable' }
|
|
84
|
+
|
|
85
|
+
writeGitSigningPublicKey(context, selected.key)
|
|
86
|
+
return { enabled: true, publicKey: selected.key, agentSocketSource }
|
|
87
|
+
}
|