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/metadata.ts
CHANGED
|
@@ -13,6 +13,14 @@ export interface WorkspaceMetadata {
|
|
|
13
13
|
sshAlias: string
|
|
14
14
|
firstSeenAt: string
|
|
15
15
|
lastSeenAt: string
|
|
16
|
+
dockerImageId?: string
|
|
17
|
+
dockerImageName?: string
|
|
18
|
+
dockerImageLastSeenAt?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorkspaceDockerImageMetadata {
|
|
22
|
+
id: string
|
|
23
|
+
name?: string
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
function isWorkspaceMetadata (value: unknown): value is WorkspaceMetadata {
|
|
@@ -28,7 +36,10 @@ function isWorkspaceMetadata (value: unknown): value is WorkspaceMetadata {
|
|
|
28
36
|
typeof candidate.workspaceBasename === 'string' &&
|
|
29
37
|
typeof candidate.sshAlias === 'string' &&
|
|
30
38
|
typeof candidate.firstSeenAt === 'string' &&
|
|
31
|
-
typeof candidate.lastSeenAt === 'string'
|
|
39
|
+
typeof candidate.lastSeenAt === 'string' &&
|
|
40
|
+
(candidate.dockerImageId === undefined || typeof candidate.dockerImageId === 'string') &&
|
|
41
|
+
(candidate.dockerImageName === undefined || typeof candidate.dockerImageName === 'string') &&
|
|
42
|
+
(candidate.dockerImageLastSeenAt === undefined || typeof candidate.dockerImageLastSeenAt === 'string')
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
export function workspaceMetadataPath (context: WorkspaceContext): string {
|
|
@@ -45,13 +56,25 @@ export function readWorkspaceMetadataFile (path: string): WorkspaceMetadata {
|
|
|
45
56
|
return parsed
|
|
46
57
|
}
|
|
47
58
|
|
|
59
|
+
export function readWorkspaceMetadata (context: WorkspaceContext): WorkspaceMetadata | undefined {
|
|
60
|
+
const metadataPath = workspaceMetadataPath(context)
|
|
61
|
+
|
|
62
|
+
if (!existsSync(metadataPath)) {
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return readWorkspaceMetadataFile(metadataPath)
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
export function writeWorkspaceMetadata (context: WorkspaceContext, sshAlias: string, now = new Date()): WorkspaceMetadata {
|
|
49
70
|
const metadataPath = workspaceMetadataPath(context)
|
|
50
71
|
const timestamp = now.toISOString()
|
|
51
72
|
let firstSeenAt = timestamp
|
|
73
|
+
let existingMetadata: WorkspaceMetadata | undefined
|
|
52
74
|
|
|
53
75
|
if (existsSync(metadataPath)) {
|
|
54
|
-
|
|
76
|
+
existingMetadata = readWorkspaceMetadataFile(metadataPath)
|
|
77
|
+
firstSeenAt = existingMetadata.firstSeenAt
|
|
55
78
|
}
|
|
56
79
|
|
|
57
80
|
const metadata: WorkspaceMetadata = {
|
|
@@ -61,7 +84,10 @@ export function writeWorkspaceMetadata (context: WorkspaceContext, sshAlias: str
|
|
|
61
84
|
workspaceBasename: context.workspaceBasename,
|
|
62
85
|
sshAlias,
|
|
63
86
|
firstSeenAt,
|
|
64
|
-
lastSeenAt: timestamp
|
|
87
|
+
lastSeenAt: timestamp,
|
|
88
|
+
...(existingMetadata?.dockerImageId === undefined ? {} : { dockerImageId: existingMetadata.dockerImageId }),
|
|
89
|
+
...(existingMetadata?.dockerImageName === undefined ? {} : { dockerImageName: existingMetadata.dockerImageName }),
|
|
90
|
+
...(existingMetadata?.dockerImageLastSeenAt === undefined ? {} : { dockerImageLastSeenAt: existingMetadata.dockerImageLastSeenAt })
|
|
65
91
|
}
|
|
66
92
|
|
|
67
93
|
mkdirSync(context.workspaceDataDir, { recursive: true })
|
|
@@ -69,6 +95,29 @@ export function writeWorkspaceMetadata (context: WorkspaceContext, sshAlias: str
|
|
|
69
95
|
return metadata
|
|
70
96
|
}
|
|
71
97
|
|
|
98
|
+
export function recordWorkspaceDockerImage (
|
|
99
|
+
context: WorkspaceContext,
|
|
100
|
+
image: WorkspaceDockerImageMetadata,
|
|
101
|
+
now = new Date()
|
|
102
|
+
): WorkspaceMetadata | undefined {
|
|
103
|
+
const metadata = readWorkspaceMetadata(context)
|
|
104
|
+
|
|
105
|
+
if (metadata === undefined) {
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const nextMetadata: WorkspaceMetadata = {
|
|
110
|
+
...metadata,
|
|
111
|
+
dockerImageId: image.id,
|
|
112
|
+
...(image.name === undefined ? {} : { dockerImageName: image.name }),
|
|
113
|
+
dockerImageLastSeenAt: now.toISOString()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
mkdirSync(context.workspaceDataDir, { recursive: true })
|
|
117
|
+
writeFileSync(workspaceMetadataPath(context), `${JSON.stringify(nextMetadata, null, 2)}\n`)
|
|
118
|
+
return nextMetadata
|
|
119
|
+
}
|
|
120
|
+
|
|
72
121
|
export function listWorkspaceMetadata (dataRoot: string): WorkspaceMetadata[] {
|
|
73
122
|
const workspacesDir = join(dataRoot, 'workspaces')
|
|
74
123
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { packageRootFromImportMeta } from './paths.ts'
|
|
5
|
+
|
|
6
|
+
interface PackageJson {
|
|
7
|
+
version?: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function readPackageVersion (packageRoot = packageRootFromImportMeta(import.meta.url)): string {
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')) as PackageJson
|
|
12
|
+
|
|
13
|
+
if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
|
|
14
|
+
throw new Error('Boxdown package.json is missing a valid version.')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return packageJson.version
|
|
18
|
+
}
|
package/src/paths.ts
CHANGED
|
@@ -26,11 +26,24 @@ export interface WorkspaceContext {
|
|
|
26
26
|
workspaceDataDir: string
|
|
27
27
|
generatedConfigPath: string
|
|
28
28
|
hostAgentsDir: string
|
|
29
|
+
hostCodexAuthPath: string
|
|
30
|
+
hostGitconfigPath: string
|
|
31
|
+
hostGitconfigSnapshotDir: string
|
|
32
|
+
hostGitconfigSnapshotPath: string
|
|
33
|
+
gitSigningStateDir: string
|
|
34
|
+
gitSigningPublicKeyPath: string
|
|
29
35
|
sshKeyDir: string
|
|
30
36
|
sshKeyPath: string
|
|
31
37
|
sshPublicKeyPath: string
|
|
32
38
|
sshPublicKeyRuntimeDir: string
|
|
33
39
|
sshPublicKeyRuntimePath: string
|
|
40
|
+
workspaceLogPath: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WorkspaceContextIdentity {
|
|
44
|
+
workspaceFolder: string
|
|
45
|
+
workspaceBasename: string
|
|
46
|
+
workspaceId: string
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
export function packageRootFromImportMeta (importMetaUrl = import.meta.url): string {
|
|
@@ -83,23 +96,33 @@ export function defaultHostAgentsDir (env: NodeJS.ProcessEnv = process.env): str
|
|
|
83
96
|
return join(env.HOME ?? homedir(), '.agents')
|
|
84
97
|
}
|
|
85
98
|
|
|
86
|
-
export function
|
|
99
|
+
export function defaultHostCodexAuthPath (env: NodeJS.ProcessEnv = process.env): string {
|
|
100
|
+
return join(env.HOME ?? homedir(), '.codex', 'auth.json')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function defaultHostGitconfigPath (env: NodeJS.ProcessEnv = process.env): string {
|
|
104
|
+
return join(env.HOME ?? homedir(), '.gitconfig')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createWorkspaceContextFromIdentity (
|
|
108
|
+
identity: WorkspaceContextIdentity,
|
|
109
|
+
options: Omit<WorkspaceContextOptions, 'workspace' | 'cwd'> = {}
|
|
110
|
+
): WorkspaceContext {
|
|
87
111
|
const env = options.env ?? process.env
|
|
88
|
-
const workspaceFolder = resolveWorkspaceFolder(options.workspace, options.cwd)
|
|
89
|
-
const workspaceBasename = basename(workspaceFolder)
|
|
90
|
-
const workspaceId = workspaceIdFor(workspaceFolder)
|
|
91
112
|
const packageRoot = options.packageRoot ?? packageRootFromImportMeta()
|
|
92
113
|
const assetsDevcontainerDir = options.assetsDevcontainerDir ?? env.BOXDOWN_DEVCONTAINER_ASSETS_DIR ?? join(packageRoot, 'assets', 'devcontainer')
|
|
93
114
|
const cacheRoot = defaultCacheRoot(env)
|
|
94
115
|
const dataRoot = defaultDataRoot(env)
|
|
95
116
|
const hostAgentsDir = defaultHostAgentsDir(env)
|
|
96
|
-
const
|
|
97
|
-
const
|
|
117
|
+
const hostCodexAuthPath = defaultHostCodexAuthPath(env)
|
|
118
|
+
const workspaceCacheDir = join(cacheRoot, 'workspaces', identity.workspaceId)
|
|
119
|
+
const workspaceDataDir = join(dataRoot, 'workspaces', identity.workspaceId)
|
|
120
|
+
const hostGitconfigSnapshotDir = join(workspaceDataDir, 'gitconfig')
|
|
98
121
|
|
|
99
122
|
return {
|
|
100
|
-
workspaceFolder,
|
|
101
|
-
workspaceBasename,
|
|
102
|
-
workspaceId,
|
|
123
|
+
workspaceFolder: identity.workspaceFolder,
|
|
124
|
+
workspaceBasename: identity.workspaceBasename,
|
|
125
|
+
workspaceId: identity.workspaceId,
|
|
103
126
|
packageRoot,
|
|
104
127
|
assetsDevcontainerDir,
|
|
105
128
|
cacheRoot,
|
|
@@ -108,10 +131,27 @@ export function createWorkspaceContext (options: WorkspaceContextOptions = {}):
|
|
|
108
131
|
workspaceDataDir,
|
|
109
132
|
generatedConfigPath: join(workspaceCacheDir, 'devcontainer.json'),
|
|
110
133
|
hostAgentsDir,
|
|
134
|
+
hostCodexAuthPath,
|
|
135
|
+
hostGitconfigPath: defaultHostGitconfigPath(env),
|
|
136
|
+
hostGitconfigSnapshotDir,
|
|
137
|
+
hostGitconfigSnapshotPath: join(hostGitconfigSnapshotDir, '.gitconfig'),
|
|
138
|
+
gitSigningStateDir: join(workspaceDataDir, 'git-signing'),
|
|
139
|
+
gitSigningPublicKeyPath: join(workspaceDataDir, 'git-signing', 'signing-key.pub'),
|
|
111
140
|
sshKeyDir: join(workspaceDataDir, 'ssh'),
|
|
112
141
|
sshKeyPath: join(workspaceDataDir, 'ssh', 'id_ed25519'),
|
|
113
142
|
sshPublicKeyPath: join(workspaceDataDir, 'ssh', 'id_ed25519.pub'),
|
|
114
143
|
sshPublicKeyRuntimeDir: join(workspaceDataDir, 'ssh-public'),
|
|
115
|
-
sshPublicKeyRuntimePath: join(workspaceDataDir, 'ssh-public', 'id_ed25519.pub')
|
|
144
|
+
sshPublicKeyRuntimePath: join(workspaceDataDir, 'ssh-public', 'id_ed25519.pub'),
|
|
145
|
+
workspaceLogPath: join(workspaceDataDir, 'boxdown.log')
|
|
116
146
|
}
|
|
117
147
|
}
|
|
148
|
+
|
|
149
|
+
export function createWorkspaceContext (options: WorkspaceContextOptions = {}): WorkspaceContext {
|
|
150
|
+
const workspaceFolder = resolveWorkspaceFolder(options.workspace, options.cwd)
|
|
151
|
+
|
|
152
|
+
return createWorkspaceContextFromIdentity({
|
|
153
|
+
workspaceFolder,
|
|
154
|
+
workspaceBasename: basename(workspaceFolder),
|
|
155
|
+
workspaceId: workspaceIdFor(workspaceFolder)
|
|
156
|
+
}, options)
|
|
157
|
+
}
|
package/src/process.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
+
import { delimiter } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { WorkspaceCommandLogger } from './logging.ts'
|
|
2
5
|
|
|
3
6
|
export interface BufferedCommandOptions {
|
|
4
7
|
cwd?: string
|
|
@@ -6,6 +9,9 @@ export interface BufferedCommandOptions {
|
|
|
6
9
|
input?: string
|
|
7
10
|
mirrorStdout?: 'stdout' | 'stderr' | false
|
|
8
11
|
mirrorStderr?: 'stdout' | 'stderr' | false
|
|
12
|
+
logger?: WorkspaceCommandLogger
|
|
13
|
+
onStdout?: (chunk: Buffer) => void
|
|
14
|
+
onStderr?: (chunk: Buffer) => void
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
export interface CommandResult {
|
|
@@ -15,10 +21,58 @@ export interface CommandResult {
|
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
function mergedEnv (env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
18
|
-
|
|
24
|
+
const baseEnv = {
|
|
19
25
|
...process.env,
|
|
20
26
|
...(env ?? {})
|
|
21
27
|
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
...baseEnv,
|
|
31
|
+
PATH: buildHostToolPath(baseEnv)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function uniquePathEntries (entries: readonly string[]): string[] {
|
|
36
|
+
const seen = new Set<string>()
|
|
37
|
+
const result: string[] = []
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (entry.length === 0 || seen.has(entry)) {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
seen.add(entry)
|
|
45
|
+
result.push(entry)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildHostToolPath (env: NodeJS.ProcessEnv = process.env): string {
|
|
52
|
+
const home = env.HOME
|
|
53
|
+
const existingPath = env.PATH ?? ''
|
|
54
|
+
const configuredPrefix = env.BOXDOWN_HOST_PATH_PREFIX?.split(delimiter) ?? []
|
|
55
|
+
const guiFriendlyPrefix = [
|
|
56
|
+
...(home === undefined ? [] : [
|
|
57
|
+
`${home}/.local/bin`,
|
|
58
|
+
`${home}/.docker/bin`
|
|
59
|
+
]),
|
|
60
|
+
'/opt/homebrew/bin',
|
|
61
|
+
'/opt/homebrew/sbin',
|
|
62
|
+
'/usr/local/bin',
|
|
63
|
+
'/usr/local/sbin',
|
|
64
|
+
'/Applications/Docker.app/Contents/Resources/bin',
|
|
65
|
+
'/usr/bin',
|
|
66
|
+
'/bin',
|
|
67
|
+
'/usr/sbin',
|
|
68
|
+
'/sbin'
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
return uniquePathEntries([
|
|
72
|
+
...configuredPrefix,
|
|
73
|
+
...existingPath.split(delimiter),
|
|
74
|
+
...guiFriendlyPrefix
|
|
75
|
+
]).join(delimiter)
|
|
22
76
|
}
|
|
23
77
|
|
|
24
78
|
function writeChunk (target: 'stdout' | 'stderr' | false, chunk: Buffer): void {
|
|
@@ -31,6 +85,7 @@ function writeChunk (target: 'stdout' | 'stderr' | false, chunk: Buffer): void {
|
|
|
31
85
|
|
|
32
86
|
export function runBuffered (command: string, args: string[], options: BufferedCommandOptions = {}): Promise<CommandResult> {
|
|
33
87
|
return new Promise((resolve) => {
|
|
88
|
+
const loggedCommand = options.logger?.startCommand(command, args, { cwd: options.cwd })
|
|
34
89
|
const child = spawn(command, args, {
|
|
35
90
|
cwd: options.cwd,
|
|
36
91
|
env: mergedEnv(options.env),
|
|
@@ -42,15 +97,28 @@ export function runBuffered (command: string, args: string[], options: BufferedC
|
|
|
42
97
|
|
|
43
98
|
child.stdout.on('data', (chunk: Buffer) => {
|
|
44
99
|
stdoutChunks.push(chunk)
|
|
100
|
+
loggedCommand?.stream('stdout', chunk)
|
|
101
|
+
options.onStdout?.(chunk)
|
|
45
102
|
writeChunk(options.mirrorStdout ?? 'stdout', chunk)
|
|
46
103
|
})
|
|
47
104
|
|
|
48
105
|
child.stderr.on('data', (chunk: Buffer) => {
|
|
49
106
|
stderrChunks.push(chunk)
|
|
107
|
+
loggedCommand?.stream('stderr', chunk)
|
|
108
|
+
options.onStderr?.(chunk)
|
|
50
109
|
writeChunk(options.mirrorStderr ?? 'stderr', chunk)
|
|
51
110
|
})
|
|
52
111
|
|
|
112
|
+
let resolved = false
|
|
113
|
+
|
|
53
114
|
child.on('error', (error) => {
|
|
115
|
+
if (resolved) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
resolved = true
|
|
120
|
+
loggedCommand?.error(error)
|
|
121
|
+
loggedCommand?.finish(127)
|
|
54
122
|
resolve({
|
|
55
123
|
code: 127,
|
|
56
124
|
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
@@ -59,6 +127,12 @@ export function runBuffered (command: string, args: string[], options: BufferedC
|
|
|
59
127
|
})
|
|
60
128
|
|
|
61
129
|
child.on('close', (code) => {
|
|
130
|
+
if (resolved) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
resolved = true
|
|
135
|
+
loggedCommand?.finish(code ?? 1)
|
|
62
136
|
resolve({
|
|
63
137
|
code: code ?? 1,
|
|
64
138
|
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
@@ -74,8 +148,9 @@ export function runBuffered (command: string, args: string[], options: BufferedC
|
|
|
74
148
|
})
|
|
75
149
|
}
|
|
76
150
|
|
|
77
|
-
export function runInteractive (command: string, args: string[], options: Pick<BufferedCommandOptions, 'cwd' | 'env'> = {}): Promise<number> {
|
|
151
|
+
export function runInteractive (command: string, args: string[], options: Pick<BufferedCommandOptions, 'cwd' | 'env' | 'logger'> = {}): Promise<number> {
|
|
78
152
|
return new Promise((resolve) => {
|
|
153
|
+
const loggedCommand = options.logger?.startCommand(command, args, { cwd: options.cwd })
|
|
79
154
|
const child = spawn(command, args, {
|
|
80
155
|
cwd: options.cwd,
|
|
81
156
|
env: mergedEnv(options.env),
|
|
@@ -83,11 +158,14 @@ export function runInteractive (command: string, args: string[], options: Pick<B
|
|
|
83
158
|
})
|
|
84
159
|
|
|
85
160
|
child.on('error', (error) => {
|
|
161
|
+
loggedCommand?.error(error)
|
|
162
|
+
loggedCommand?.finish(127)
|
|
86
163
|
process.stderr.write(`${error.message}\n`)
|
|
87
164
|
resolve(127)
|
|
88
165
|
})
|
|
89
166
|
|
|
90
167
|
child.on('close', (code) => {
|
|
168
|
+
loggedCommand?.finish(code ?? 1)
|
|
91
169
|
resolve(code ?? 1)
|
|
92
170
|
})
|
|
93
171
|
})
|