/** * Headless create input: a structured project spec supplied by an agent or CI * instead of interactive prompts. * * @module @deepseek-ai/create-sdk/headless */ import { readFile } from 'node:fs/promises' import type { FeatureSelection, PackageManagerName, RunInterface } from '@deepseek-ai/dsh-helper' import type { CreateArgs } from './args.ts' /** * Structured, non-interactive create input. Scalar fields mirror {@link CreateArgs} * project answers; `features` is the headless feature plan handed to `CreateWizard` * (the interactive tree/suggests prompts are skipped). Absent required answers make * the run fail loud through `HeadlessPromptPort` rather than blocking. */ interface HeadlessCreateSpec { directory?: string description?: string provider?: 'deepseek-official' | 'custom' baseURL?: string apiKey?: string model?: string interface?: RunInterface pm?: PackageManagerName install?: boolean linkWorkspace?: boolean features?: readonly FeatureSelection[] } /** Resolved headless input: the args the wizard reads plus the feature plan. */ export interface ResolvedHeadless { args: CreateArgs features: readonly FeatureSelection[] | undefined } function asRecord(value: unknown, source: string): Record { if (value === null || typeof value !== 'object' || Array.isArray(value)) { throw new Error(`${source}: expected a JSON object`) } return value as Record } /** Parse and shallow-validate a headless spec from JSON text. */ function parseHeadlessSpec(text: string, source: string): HeadlessCreateSpec { let parsed: unknown try { parsed = JSON.parse(text) } catch (error) { /* v8 ignore next -- JSON.parse only throws Error instances; the String() branch is defensive */ throw new Error(`${source}: invalid JSON (${error instanceof Error ? error.message : String(error)})`) } const record = asRecord(parsed, source) if (record.features !== undefined && !Array.isArray(record.features)) { throw new Error(`${source}: "features" must be an array`) } return record } /** * Load a headless spec from `--config-json` (inline) or `--config` (a JSON file), * returning `undefined` when neither is supplied. * @param args - parsed create args. * @param readFileText - file reader seam for tests. * @returns the resolved args + feature plan, or `undefined` for interactive runs. */ export async function resolveHeadless( args: CreateArgs, readFileText: (path: string) => Promise = path => readFile(path, 'utf8'), ): Promise { let text: string let source: string if (args.configJson !== undefined) { text = args.configJson source = '--config-json' } else if (args.config !== undefined) { source = args.config text = await readFileText(args.config) } else { return undefined } const spec = parseHeadlessSpec(text, source) const resolvedArgs: CreateArgs = { ...spec.directory === undefined ? {} : { directory: spec.directory }, ...spec.description === undefined ? {} : { description: spec.description }, ...spec.provider === undefined ? {} : { provider: spec.provider }, ...spec.baseURL === undefined ? {} : { baseURL: spec.baseURL }, ...spec.apiKey === undefined ? {} : { apiKey: spec.apiKey }, ...spec.model === undefined ? {} : { model: spec.model }, ...spec.interface === undefined ? {} : { runInterface: spec.interface }, ...spec.pm === undefined ? {} : { packageManager: spec.pm }, ...spec.install === undefined ? {} : { install: spec.install }, ...spec.linkWorkspace ? { linkWorkspace: true } : {}, help: false, } return { args: resolvedArgs, features: spec.features } }