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/progress.ts
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { runBuffered, type BufferedCommandOptions, type CommandResult } from './process.ts'
|
|
2
|
+
import { color, formatPromptEnd, formatPromptTitle, promptRail, selectedMark } from './cli-style.ts'
|
|
3
|
+
|
|
4
|
+
export type ProgressOutputTarget = 'stdout' | 'stderr'
|
|
5
|
+
export type ProgressMode = 'interactive' | 'verbose' | 'none'
|
|
6
|
+
export type ProgressWriter = (target: ProgressOutputTarget, message: string) => void
|
|
7
|
+
export type ProgressRawWriter = (target: ProgressOutputTarget, message: string) => void
|
|
8
|
+
|
|
9
|
+
export interface ProgressReporterOptions {
|
|
10
|
+
mode?: ProgressMode
|
|
11
|
+
verbose?: boolean
|
|
12
|
+
target?: ProgressOutputTarget
|
|
13
|
+
write?: ProgressWriter
|
|
14
|
+
writeRaw?: ProgressRawWriter
|
|
15
|
+
isTTY?: boolean
|
|
16
|
+
spinnerFrames?: readonly string[]
|
|
17
|
+
spinnerIntervalMs?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ProgressCommandOptions extends Pick<BufferedCommandOptions, 'cwd' | 'env' | 'input'> {
|
|
21
|
+
logger?: BufferedCommandOptions['logger']
|
|
22
|
+
progress?: ProgressReporter
|
|
23
|
+
verboseStdout?: ProgressOutputTarget | false
|
|
24
|
+
verboseStderr?: ProgressOutputTarget | false
|
|
25
|
+
spinnerLabel?: string
|
|
26
|
+
stepId?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ResolveProgressModeOptions {
|
|
30
|
+
verbose?: boolean
|
|
31
|
+
json?: boolean
|
|
32
|
+
target?: ProgressOutputTarget
|
|
33
|
+
env?: NodeJS.ProcessEnv
|
|
34
|
+
isTTY?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ProgressStepState = 'pending' | 'running' | 'complete' | 'failed' | 'skipped'
|
|
38
|
+
|
|
39
|
+
export interface ProgressStepDefinition {
|
|
40
|
+
id: string
|
|
41
|
+
label: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PROGRESS_MARKER_PREFIX = 'BOXDOWN_PROGRESS:'
|
|
45
|
+
const DEFAULT_FAILURE_TAIL_LINES = 20
|
|
46
|
+
const DEFAULT_SPINNER_FRAMES = ['◒', '◐', '◓', '◑'] as const
|
|
47
|
+
const DEFAULT_SPINNER_INTERVAL_MS = 120
|
|
48
|
+
|
|
49
|
+
interface ActiveSpinner {
|
|
50
|
+
message: string
|
|
51
|
+
frameIndex: number
|
|
52
|
+
timer?: ReturnType<typeof setInterval>
|
|
53
|
+
tty: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ProgressStep extends ProgressStepDefinition {
|
|
57
|
+
state: ProgressStepState
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeLine (target: ProgressOutputTarget, message: string): void {
|
|
61
|
+
const stream = target === 'stderr' ? process.stderr : process.stdout
|
|
62
|
+
stream.write(`${message}\n`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeRaw (target: ProgressOutputTarget, message: string): void {
|
|
66
|
+
const stream = target === 'stderr' ? process.stderr : process.stdout
|
|
67
|
+
stream.write(message)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function targetIsTTY (target: ProgressOutputTarget): boolean {
|
|
71
|
+
const stream = target === 'stderr' ? process.stderr : process.stdout
|
|
72
|
+
return stream.isTTY === true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isCiEnvironment (env: NodeJS.ProcessEnv): boolean {
|
|
76
|
+
const ci = env.CI
|
|
77
|
+
return ci !== undefined && ci !== '' && ci !== '0' && ci !== 'false'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeMessage (message: string): string {
|
|
81
|
+
return message.trim().replace(/\s+/g, ' ')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function resolveProgressMode (options: ResolveProgressModeOptions = {}): ProgressMode {
|
|
85
|
+
if (options.json === true) {
|
|
86
|
+
return 'none'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (options.verbose === true) {
|
|
90
|
+
return 'verbose'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isCiEnvironment(options.env ?? process.env)) {
|
|
94
|
+
return 'verbose'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const target = options.target ?? 'stdout'
|
|
98
|
+
const isTTY = options.isTTY ?? targetIsTTY(target)
|
|
99
|
+
|
|
100
|
+
return isTTY ? 'interactive' : 'verbose'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class ProgressReporter {
|
|
104
|
+
readonly mode: ProgressMode
|
|
105
|
+
readonly verbose: boolean
|
|
106
|
+
readonly target: ProgressOutputTarget
|
|
107
|
+
readonly #write: ProgressWriter
|
|
108
|
+
readonly #writeRaw: ProgressRawWriter
|
|
109
|
+
readonly #isTTY: boolean
|
|
110
|
+
readonly #spinnerFrames: readonly string[]
|
|
111
|
+
readonly #spinnerIntervalMs: number
|
|
112
|
+
#sectionPrinted = false
|
|
113
|
+
#sectionOpen = false
|
|
114
|
+
#spinner: ActiveSpinner | undefined
|
|
115
|
+
#steps: ProgressStep[] = []
|
|
116
|
+
#renderedStepLineCount = 0
|
|
117
|
+
#stepFrameIndex = 0
|
|
118
|
+
#stepTimer: ReturnType<typeof setInterval> | undefined
|
|
119
|
+
|
|
120
|
+
constructor (options: ProgressReporterOptions = {}) {
|
|
121
|
+
this.mode = options.mode ?? (options.verbose === true ? 'verbose' : 'interactive')
|
|
122
|
+
this.verbose = this.mode === 'verbose'
|
|
123
|
+
this.target = options.target ?? 'stdout'
|
|
124
|
+
this.#write = options.write ?? writeLine
|
|
125
|
+
this.#writeRaw = options.writeRaw ?? writeRaw
|
|
126
|
+
this.#isTTY = options.isTTY ?? targetIsTTY(this.target)
|
|
127
|
+
this.#spinnerFrames = options.spinnerFrames ?? DEFAULT_SPINNER_FRAMES
|
|
128
|
+
this.#spinnerIntervalMs = options.spinnerIntervalMs ?? DEFAULT_SPINNER_INTERVAL_MS
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
section (title: string): void {
|
|
132
|
+
if (this.mode !== 'interactive') {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (this.#sectionOpen) {
|
|
137
|
+
this.end()
|
|
138
|
+
} else if (this.#sectionPrinted) {
|
|
139
|
+
this.#write(this.target, '')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.#write(this.target, formatPromptTitle(title))
|
|
143
|
+
this.#sectionPrinted = true
|
|
144
|
+
this.#sectionOpen = true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
end (): void {
|
|
148
|
+
this.stopSpinner()
|
|
149
|
+
this.#stopStepTimer()
|
|
150
|
+
this.#steps = []
|
|
151
|
+
this.#renderedStepLineCount = 0
|
|
152
|
+
|
|
153
|
+
if (this.mode !== 'interactive') {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!this.#sectionOpen) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.#write(this.target, formatPromptEnd())
|
|
162
|
+
this.#sectionOpen = false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
item (message: string): void {
|
|
166
|
+
if (this.mode !== 'interactive') {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.#writeLine(`${promptRail()} ${selectedMark()} ${message}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
detail (message: string): void {
|
|
174
|
+
if (this.mode !== 'interactive') {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.#writeLine(`${promptRail()} ${color(message, 'dim')}`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
warn (message: string): void {
|
|
182
|
+
if (this.mode === 'none') {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (this.mode === 'verbose') {
|
|
187
|
+
this.#write(this.target, `Warning: ${message}`)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.#writeLine(`${promptRail()} ${color('!', 'dim')} ${message}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
marker (message: string): void {
|
|
195
|
+
if (this.#steps.length > 0) {
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const normalized = normalizeMessage(message)
|
|
200
|
+
|
|
201
|
+
if (normalized.length > 0) {
|
|
202
|
+
this.item(normalized)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setSteps (steps: readonly ProgressStepDefinition[]): void {
|
|
207
|
+
this.#stopStepTimer()
|
|
208
|
+
this.#steps = steps.map((step) => ({
|
|
209
|
+
...step,
|
|
210
|
+
state: 'pending'
|
|
211
|
+
}))
|
|
212
|
+
this.#stepFrameIndex = 0
|
|
213
|
+
this.#renderedStepLineCount = 0
|
|
214
|
+
this.#renderChecklist()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
isChecklistActive (): boolean {
|
|
218
|
+
return this.#steps.length > 0
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
startStep (id: string): void {
|
|
222
|
+
this.#updateStep(id, 'running')
|
|
223
|
+
this.#startStepTimer()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
completeStep (id: string): void {
|
|
227
|
+
this.#updateStep(id, 'complete')
|
|
228
|
+
this.#stopStepTimerIfIdle()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
failStep (id: string): void {
|
|
232
|
+
this.#updateStep(id, 'failed')
|
|
233
|
+
this.#stopStepTimerIfIdle()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
skipStep (id: string): void {
|
|
237
|
+
this.#updateStep(id, 'skipped')
|
|
238
|
+
this.#stopStepTimerIfIdle()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
hasStep (id: string): boolean {
|
|
242
|
+
return this.#steps.some((step) => step.id === id)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
startSpinner (message: string): void {
|
|
246
|
+
if (this.mode !== 'interactive') {
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const normalized = normalizeMessage(message)
|
|
251
|
+
if (normalized.length === 0) {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.stopSpinner()
|
|
256
|
+
this.#spinner = {
|
|
257
|
+
message: normalized,
|
|
258
|
+
frameIndex: 0,
|
|
259
|
+
tty: this.#isTTY
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (this.#spinner.tty) {
|
|
263
|
+
this.#renderSpinner()
|
|
264
|
+
this.#spinner.timer = setInterval(() => {
|
|
265
|
+
this.tickSpinner()
|
|
266
|
+
}, this.#spinnerIntervalMs)
|
|
267
|
+
this.#spinner.timer.unref?.()
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.#writeLine(`${promptRail()} ${color(this.#spinnerFrames[0] ?? '◒', 'cyan')} ${normalized}`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
tickSpinner (): void {
|
|
275
|
+
if (this.#steps.some((step) => step.state === 'running')) {
|
|
276
|
+
this.#stepFrameIndex += 1
|
|
277
|
+
this.#renderChecklist()
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const spinner = this.#spinner
|
|
282
|
+
if (spinner === undefined || !spinner.tty) {
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
spinner.frameIndex += 1
|
|
287
|
+
this.#renderSpinner()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
stopSpinner (status: 'complete' | 'clear' = 'clear'): void {
|
|
291
|
+
const spinner = this.#spinner
|
|
292
|
+
if (spinner === undefined) {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (spinner.timer !== undefined) {
|
|
297
|
+
clearInterval(spinner.timer)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (spinner.tty) {
|
|
301
|
+
this.#clearSpinnerLine()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.#spinner = undefined
|
|
305
|
+
|
|
306
|
+
if (status === 'complete') {
|
|
307
|
+
this.item(spinner.message)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
commandEnv (env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
312
|
+
return {
|
|
313
|
+
...(env ?? {}),
|
|
314
|
+
BOXDOWN_VERBOSE: this.verbose ? '1' : '0',
|
|
315
|
+
BOXDOWN_PROGRESS: this.mode === 'interactive' ? '1' : '0'
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#writeLine (message: string): void {
|
|
320
|
+
const spinner = this.#spinner
|
|
321
|
+
|
|
322
|
+
if (spinner !== undefined && spinner.tty) {
|
|
323
|
+
this.#clearSpinnerLine()
|
|
324
|
+
this.#write(this.target, message)
|
|
325
|
+
this.#renderSpinner()
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this.#write(this.target, message)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#renderSpinner (): void {
|
|
333
|
+
const spinner = this.#spinner
|
|
334
|
+
if (spinner === undefined) {
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const frame = this.#spinnerFrames[spinner.frameIndex % this.#spinnerFrames.length] ?? '◒'
|
|
339
|
+
this.#writeRaw(this.target, `\r\u001B[2K${promptRail()} ${color(frame, 'cyan')} ${spinner.message}`)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#clearSpinnerLine (): void {
|
|
343
|
+
this.#writeRaw(this.target, '\r\u001B[2K')
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#updateStep (id: string, state: ProgressStepState): void {
|
|
347
|
+
const index = this.#steps.findIndex((step) => step.id === id)
|
|
348
|
+
|
|
349
|
+
if (index === -1) {
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const step = this.#steps[index]
|
|
354
|
+
if (step === undefined) {
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this.#steps[index] = {
|
|
359
|
+
...step,
|
|
360
|
+
state
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.#renderChecklist()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#renderChecklist (): void {
|
|
367
|
+
if (this.mode !== 'interactive' || this.#steps.length === 0) {
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const lines = this.#steps.map((step) => this.#formatStep(step))
|
|
372
|
+
|
|
373
|
+
if (this.#isTTY) {
|
|
374
|
+
if (this.#renderedStepLineCount > 0) {
|
|
375
|
+
this.#writeRaw(this.target, `\u001B[${this.#renderedStepLineCount}A`)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const line of lines) {
|
|
379
|
+
this.#writeRaw(this.target, `\u001B[2K\r${line}\n`)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
this.#renderedStepLineCount = lines.length
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (this.#renderedStepLineCount === 0) {
|
|
387
|
+
for (const line of lines) {
|
|
388
|
+
this.#write(this.target, line)
|
|
389
|
+
}
|
|
390
|
+
this.#renderedStepLineCount = lines.length
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#formatStep (step: ProgressStep): string {
|
|
395
|
+
const label = step.state === 'skipped' ? color(step.label, 'dim') : step.label
|
|
396
|
+
return `${promptRail()} ${this.#stepMark(step.state)} ${label}`
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#stepMark (state: ProgressStepState): string {
|
|
400
|
+
if (state === 'running') {
|
|
401
|
+
const frame = this.#spinnerFrames[this.#stepFrameIndex % this.#spinnerFrames.length] ?? '◐'
|
|
402
|
+
return color(frame, 'cyan')
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (state === 'complete') {
|
|
406
|
+
return color('✔', 'green')
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (state === 'failed') {
|
|
410
|
+
return color('!', 'dim')
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return color('□', 'dim')
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#startStepTimer (): void {
|
|
417
|
+
if (this.mode !== 'interactive' || !this.#isTTY || this.#stepTimer !== undefined) {
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
this.#stepTimer = setInterval(() => {
|
|
422
|
+
this.tickSpinner()
|
|
423
|
+
}, this.#spinnerIntervalMs)
|
|
424
|
+
this.#stepTimer.unref?.()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#stopStepTimerIfIdle (): void {
|
|
428
|
+
if (this.#steps.some((step) => step.state === 'running')) {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
this.#stopStepTimer()
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
#stopStepTimer (): void {
|
|
436
|
+
if (this.#stepTimer === undefined) {
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
clearInterval(this.#stepTimer)
|
|
441
|
+
this.#stepTimer = undefined
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function createProgress (options: ProgressReporterOptions = {}): ProgressReporter {
|
|
446
|
+
return new ProgressReporter(options)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function progressMarkerLine (message: string): string {
|
|
450
|
+
return `${PROGRESS_MARKER_PREFIX} ${message}`
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function isProgressMarkerLine (line: string): boolean {
|
|
454
|
+
return line.trimStart().startsWith(PROGRESS_MARKER_PREFIX)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function progressMarkerMessage (line: string): string | undefined {
|
|
458
|
+
const trimmed = line.trimStart()
|
|
459
|
+
|
|
460
|
+
if (!trimmed.startsWith(PROGRESS_MARKER_PREFIX)) {
|
|
461
|
+
return undefined
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return trimmed.slice(PROGRESS_MARKER_PREFIX.length).trim()
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function createMarkerSink (progress: ProgressReporter): {
|
|
468
|
+
write: (chunk: Buffer) => void
|
|
469
|
+
flush: () => void
|
|
470
|
+
} {
|
|
471
|
+
let pending = ''
|
|
472
|
+
|
|
473
|
+
function processLine (line: string): void {
|
|
474
|
+
const message = progressMarkerMessage(line)
|
|
475
|
+
|
|
476
|
+
if (message !== undefined) {
|
|
477
|
+
progress.marker(message)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
write: (chunk: Buffer) => {
|
|
483
|
+
pending += chunk.toString('utf8')
|
|
484
|
+
|
|
485
|
+
const lines = pending.split(/\r?\n/u)
|
|
486
|
+
pending = lines.pop() ?? ''
|
|
487
|
+
|
|
488
|
+
for (const line of lines) {
|
|
489
|
+
processLine(line)
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
flush: () => {
|
|
493
|
+
if (pending.length > 0) {
|
|
494
|
+
processLine(pending)
|
|
495
|
+
pending = ''
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function outputWithoutProgressMarkers (output: string): string {
|
|
502
|
+
return output
|
|
503
|
+
.split(/\r?\n/u)
|
|
504
|
+
.filter((line) => !isProgressMarkerLine(line))
|
|
505
|
+
.join('\n')
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function tailLines (output: string, maxLines: number): string[] {
|
|
509
|
+
return output
|
|
510
|
+
.split(/\r?\n/u)
|
|
511
|
+
.map((line) => line.trimEnd())
|
|
512
|
+
.filter((line) => line.trim().length > 0)
|
|
513
|
+
.slice(-maxLines)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function formatCommandFailure (label: string, result: CommandResult, options: { tailLines?: number } = {}): string {
|
|
517
|
+
const maxLines = options.tailLines ?? DEFAULT_FAILURE_TAIL_LINES
|
|
518
|
+
const stderrTail = tailLines(outputWithoutProgressMarkers(result.stderr), maxLines)
|
|
519
|
+
const stdoutTail = tailLines(outputWithoutProgressMarkers(result.stdout), Math.max(0, maxLines - stderrTail.length))
|
|
520
|
+
const lines = [
|
|
521
|
+
`${label} failed with exit code ${result.code}.`,
|
|
522
|
+
'Rerun with --verbose to see full command output.'
|
|
523
|
+
]
|
|
524
|
+
|
|
525
|
+
if (stderrTail.length > 0) {
|
|
526
|
+
lines.push('', 'stderr tail:', ...stderrTail.map((line) => ` ${line}`))
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (stdoutTail.length > 0) {
|
|
530
|
+
lines.push('', 'stdout tail:', ...stdoutTail.map((line) => ` ${line}`))
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return lines.join('\n')
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function runProgressCommand (
|
|
537
|
+
label: string,
|
|
538
|
+
command: string,
|
|
539
|
+
args: string[],
|
|
540
|
+
options: ProgressCommandOptions = {}
|
|
541
|
+
): Promise<CommandResult> {
|
|
542
|
+
const progress = options.progress
|
|
543
|
+
const verbose = progress?.verbose ?? true
|
|
544
|
+
const markerSink = progress !== undefined && !verbose ? createMarkerSink(progress) : undefined
|
|
545
|
+
const checklistStepId = progress !== undefined && options.stepId !== undefined && progress.hasStep(options.stepId)
|
|
546
|
+
? options.stepId
|
|
547
|
+
: undefined
|
|
548
|
+
|
|
549
|
+
if (progress !== undefined && !verbose && checklistStepId !== undefined) {
|
|
550
|
+
progress.startStep(checklistStepId)
|
|
551
|
+
} else if (progress !== undefined && !verbose && options.spinnerLabel !== undefined) {
|
|
552
|
+
progress.startSpinner(options.spinnerLabel)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const result = await runBuffered(command, args, {
|
|
557
|
+
cwd: options.cwd,
|
|
558
|
+
env: progress?.commandEnv(options.env) ?? options.env,
|
|
559
|
+
input: options.input,
|
|
560
|
+
logger: options.logger,
|
|
561
|
+
mirrorStdout: verbose ? (options.verboseStdout ?? 'stdout') : false,
|
|
562
|
+
mirrorStderr: verbose ? (options.verboseStderr ?? 'stderr') : false,
|
|
563
|
+
onStdout: markerSink?.write,
|
|
564
|
+
onStderr: markerSink?.write
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
markerSink?.flush()
|
|
568
|
+
if (checklistStepId !== undefined) {
|
|
569
|
+
if (result.code === 0) {
|
|
570
|
+
progress?.completeStep(checklistStepId)
|
|
571
|
+
} else {
|
|
572
|
+
progress?.failStep(checklistStepId)
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
progress?.stopSpinner(result.code === 0 ? 'complete' : 'clear')
|
|
576
|
+
}
|
|
577
|
+
return result
|
|
578
|
+
} catch (error) {
|
|
579
|
+
markerSink?.flush()
|
|
580
|
+
if (checklistStepId !== undefined) {
|
|
581
|
+
progress?.failStep(checklistStepId)
|
|
582
|
+
} else {
|
|
583
|
+
progress?.stopSpinner()
|
|
584
|
+
}
|
|
585
|
+
throw error
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function assertProgressCommandSucceeded (label: string, result: CommandResult, message: string): void {
|
|
590
|
+
if (result.code !== 0) {
|
|
591
|
+
throw new Error(`${message}\n${formatCommandFailure(label, result)}`)
|
|
592
|
+
}
|
|
593
|
+
}
|