fix(create-sdk): keep --json stdout pure NDJSON
The SKILL.md contract says every stdout line is one JSON event, but createProject wrote the Created/Next-steps templates to stdout and the default install/build path inherited the launcher's stdio, so package- manager child output interleaved with the event stream. Under --json, route human progress to stderr and run install/build through a NodeCommandRunner that redirects child stdout+stderr to stderr.
This commit is contained in:
@@ -58,17 +58,38 @@ export function scrubEnvironment(environment: NodeJS.ProcessEnv = process.env):
|
||||
|
||||
/** Node child-process command runner with inherited stdio and quiescent completion. */
|
||||
export class NodeCommandRunner implements CommandRunner {
|
||||
/** Spawn one child and settle only after its exit. */
|
||||
private readonly output: NodeJS.WritableStream | undefined
|
||||
|
||||
/**
|
||||
* @param output - redirect target for child stdout+stderr; the child inherits
|
||||
* this process's stdio when absent. Callers whose own stdout carries a machine
|
||||
* protocol (create-sdk --json NDJSON) redirect child output to keep the
|
||||
* protocol stream pure.
|
||||
*/
|
||||
constructor(output?: NodeJS.WritableStream) {
|
||||
this.output = output
|
||||
}
|
||||
|
||||
/** Spawn one child and settle only after exit, with redirected stdio drained. */
|
||||
run(command: string, args: readonly string[], cwd: string): Promise<CommandResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const output = this.output
|
||||
if (output === undefined) {
|
||||
const child = spawn(command, [...args], { cwd, env: scrubEnvironment(), stdio: 'inherit', shell: false })
|
||||
child.once('error', reject)
|
||||
child.once('exit', (exitCode, signal) => { resolve({ exitCode, signal }) })
|
||||
return
|
||||
}
|
||||
const child = spawn(command, [...args], {
|
||||
cwd,
|
||||
env: scrubEnvironment(),
|
||||
stdio: 'inherit',
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
shell: false,
|
||||
})
|
||||
child.stdout.pipe(output, { end: false })
|
||||
child.stderr.pipe(output, { end: false })
|
||||
child.once('error', reject)
|
||||
child.once('exit', (exitCode, signal) => { resolve({ exitCode, signal }) })
|
||||
child.once('close', (exitCode, signal) => { resolve({ exitCode, signal }) })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { chmod, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Writable } from 'node:stream'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { CordisYamlFile, JsExpression } from '../src/documents/cordis-yaml-file.ts'
|
||||
import { EnvFile } from '../src/documents/env-file.ts'
|
||||
@@ -325,6 +326,19 @@ describe('package manager strategies', () => {
|
||||
const runner = new NodeCommandRunner()
|
||||
await expect(runner.run(process.execPath, ['-e', ''], root)).resolves.toEqual({ exitCode: 0, signal: null })
|
||||
await expect(runner.run('missing-dsh-command', [], root)).rejects.toThrow()
|
||||
let redirected = ''
|
||||
const output = new Writable({
|
||||
write(chunk, _encoding, callback) { redirected += String(chunk); callback() },
|
||||
})
|
||||
const redirecting = new NodeCommandRunner(output)
|
||||
await expect(redirecting.run(
|
||||
process.execPath,
|
||||
['-e', 'process.stdout.write("child-out"); process.stderr.write("child-err")'],
|
||||
root,
|
||||
)).resolves.toEqual({ exitCode: 0, signal: null })
|
||||
expect(redirected).toContain('child-out')
|
||||
expect(redirected).toContain('child-err')
|
||||
await expect(redirecting.run('missing-dsh-command', [], root)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('discovers and rewrites a repository-local NPM dependency closure', async () => {
|
||||
|
||||
Reference in New Issue
Block a user