headless.ts 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. /**
  2. * Headless create input: a structured project spec supplied by an agent or CI
  3. * instead of interactive prompts.
  4. *
  5. * @module @deepseek-ai/create-sdk/headless
  6. */
  7. import { readFile } from 'node:fs/promises'
  8. import type { FeatureSelection, PackageManagerName, RunInterface } from '@deepseek-ai/dsh-helper'
  9. import type { CreateArgs } from './args.ts'
  10. /**
  11. * Structured, non-interactive create input. Scalar fields mirror {@link CreateArgs}
  12. * project answers; `features` is the headless feature plan handed to `CreateWizard`
  13. * (the interactive tree/suggests prompts are skipped). Absent required answers make
  14. * the run fail loud through `HeadlessPromptPort` rather than blocking.
  15. */
  16. interface HeadlessCreateSpec {
  17. directory?: string
  18. description?: string
  19. provider?: 'deepseek' | 'custom'
  20. baseURL?: string
  21. apiKey?: string
  22. model?: string
  23. interface?: RunInterface
  24. pm?: PackageManagerName
  25. install?: boolean
  26. linkWorkspace?: boolean
  27. features?: readonly FeatureSelection[]
  28. }
  29. /** Resolved headless input: the args the wizard reads plus the feature plan. */
  30. export interface ResolvedHeadless {
  31. args: CreateArgs
  32. features: readonly FeatureSelection[] | undefined
  33. }
  34. function asRecord(value: unknown, source: string): Record<string, unknown> {
  35. if (value === null || typeof value !== 'object' || Array.isArray(value)) {
  36. throw new Error(`${source}: expected a JSON object`)
  37. }
  38. return value as Record<string, unknown>
  39. }
  40. /** Parse and shallow-validate a headless spec from JSON text. */
  41. function parseHeadlessSpec(text: string, source: string): HeadlessCreateSpec {
  42. let parsed: unknown
  43. try {
  44. parsed = JSON.parse(text)
  45. } catch (error) {
  46. /* v8 ignore next -- JSON.parse only throws Error instances; the String() branch is defensive */
  47. throw new Error(`${source}: invalid JSON (${error instanceof Error ? error.message : String(error)})`)
  48. }
  49. const record = asRecord(parsed, source)
  50. if (record.features !== undefined && !Array.isArray(record.features)) {
  51. throw new Error(`${source}: "features" must be an array`)
  52. }
  53. return record
  54. }
  55. /**
  56. * Load a headless spec from `--config-json` (inline) or `--config` (a JSON file),
  57. * returning `undefined` when neither is supplied.
  58. * @param args - parsed create args.
  59. * @param readFileText - file reader seam for tests.
  60. * @returns the resolved args + feature plan, or `undefined` for interactive runs.
  61. */
  62. export async function resolveHeadless(
  63. args: CreateArgs,
  64. readFileText: (path: string) => Promise<string> = path => readFile(path, 'utf8'),
  65. ): Promise<ResolvedHeadless | undefined> {
  66. let text: string
  67. let source: string
  68. if (args.configJson !== undefined) {
  69. text = args.configJson
  70. source = '--config-json'
  71. } else if (args.config !== undefined) {
  72. source = args.config
  73. text = await readFileText(args.config)
  74. } else {
  75. return undefined
  76. }
  77. const spec = parseHeadlessSpec(text, source)
  78. const resolvedArgs: CreateArgs = {
  79. ...spec.directory === undefined ? {} : { directory: spec.directory },
  80. ...spec.description === undefined ? {} : { description: spec.description },
  81. ...spec.provider === undefined ? {} : { provider: spec.provider },
  82. ...spec.baseURL === undefined ? {} : { baseURL: spec.baseURL },
  83. ...spec.apiKey === undefined ? {} : { apiKey: spec.apiKey },
  84. ...spec.model === undefined ? {} : { model: spec.model },
  85. ...spec.interface === undefined ? {} : { runInterface: spec.interface },
  86. ...spec.pm === undefined ? {} : { packageManager: spec.pm },
  87. ...spec.install === undefined ? {} : { install: spec.install },
  88. ...spec.linkWorkspace ? { linkWorkspace: true } : {},
  89. help: false,
  90. }
  91. return { args: resolvedArgs, features: spec.features }
  92. }