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:
@@ -9,6 +9,7 @@ import {
|
||||
ClackPromptPort,
|
||||
HeadlessPromptError,
|
||||
HeadlessPromptPort,
|
||||
NodeCommandRunner,
|
||||
PromptCancelledError,
|
||||
type PackageManagerVersionProbe,
|
||||
type PromptPort,
|
||||
@@ -45,6 +46,9 @@ export async function createProject(
|
||||
context: CreateCommandContext,
|
||||
): Promise<ScaffoldResult | undefined> {
|
||||
const args = parseCreateArgs(argv)
|
||||
// Under --json, stdout carries only NDJSON events: human-readable progress
|
||||
// and package-manager child output move to stderr.
|
||||
const progress = args.json === true ? context.stderr : context.stdout
|
||||
if (args.help) {
|
||||
context.stdout.write(CREATE_TEMPLATES.usage.render({}))
|
||||
return undefined
|
||||
@@ -64,7 +68,7 @@ export async function createProject(
|
||||
})
|
||||
const resolved = await wizard.run()
|
||||
const result = await scaffoldProject(resolved.directory, resolved.request)
|
||||
context.stdout.write(CREATE_TEMPLATES.created.render({
|
||||
progress.write(CREATE_TEMPLATES.created.render({
|
||||
name: resolved.request.name,
|
||||
directory: resolved.directory,
|
||||
}))
|
||||
@@ -72,8 +76,9 @@ export async function createProject(
|
||||
try {
|
||||
if (context.setup) await context.setup(resolved)
|
||||
else {
|
||||
await resolved.request.packageManager.install(resolved.directory)
|
||||
await resolved.request.packageManager.build(resolved.directory)
|
||||
const runner = args.json === true ? new NodeCommandRunner(context.stderr) : new NodeCommandRunner()
|
||||
await resolved.request.packageManager.install(resolved.directory, runner)
|
||||
await resolved.request.packageManager.build(resolved.directory, runner)
|
||||
}
|
||||
} catch (error) {
|
||||
context.stderr.write(CREATE_TEMPLATES.setupFailure.render({
|
||||
@@ -84,7 +89,7 @@ export async function createProject(
|
||||
throw error
|
||||
}
|
||||
}
|
||||
context.stdout.write(CREATE_TEMPLATES.nextSteps.render({
|
||||
progress.write(CREATE_TEMPLATES.nextSteps.render({
|
||||
directory: resolved.directory,
|
||||
setupRequired: !resolved.install,
|
||||
...packageManagerTemplateModel(resolved.request.packageManager),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
HeadlessPromptPort,
|
||||
LocalPluginBlueprint,
|
||||
featureId,
|
||||
NodeCommandRunner,
|
||||
NpmPackageManager,
|
||||
type FeatureSelection,
|
||||
type NestedMultiSelectValue,
|
||||
@@ -511,6 +512,12 @@ describe('create command composition', () => {
|
||||
const okSpec = JSON.stringify({ ...base, directory: 'done-agent', provider: 'deepseek', apiKey: 'key', features: [] })
|
||||
await expect(runCreateCommand(['--config-json', okSpec, '--json'], ok)).resolves.toBe(0)
|
||||
expect(ok.readStdout()).toContain('{"type":"done"}')
|
||||
// stdout stays pure NDJSON: every line parses, human progress goes to stderr
|
||||
for (const line of ok.readStdout().split('\n').filter(line => line.length > 0)) {
|
||||
expect(() => { JSON.parse(line) }).not.toThrow()
|
||||
}
|
||||
expect(ok.readStderr()).toContain('Created done-agent')
|
||||
expect(ok.readStderr()).toContain('Next: cd')
|
||||
|
||||
const missing = commandContext(root)
|
||||
missing.stdin.isTTY = false
|
||||
@@ -564,6 +571,17 @@ describe('create command composition', () => {
|
||||
await createProject(argv('agent', true), context)
|
||||
expect(install).toHaveBeenCalledOnce()
|
||||
expect(build).toHaveBeenCalledOnce()
|
||||
const spec = JSON.stringify({
|
||||
directory: 'json-agent', description: 'test', provider: 'deepseek', apiKey: 'key',
|
||||
model: 'deepseek-v4-flash', interface: 'embed', pm: 'npm', install: true, features: [],
|
||||
})
|
||||
const json = commandContext(root)
|
||||
json.stdin.isTTY = false
|
||||
json.stdout.isTTY = false
|
||||
await createProject(['--config-json', spec, '--json'], json)
|
||||
// json mode hands install/build a runner that redirects child output to stderr
|
||||
expect(install).toHaveBeenCalledTimes(2)
|
||||
expect(install.mock.calls[1]?.[1]).toBeInstanceOf(NodeCommandRunner)
|
||||
install.mockRestore()
|
||||
build.mockRestore()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user