command.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. /**
  2. * Internal create-sdk command composition used by the package bin.
  3. *
  4. * @module @deepseek-ai/create-sdk/command
  5. */
  6. import { readFile } from 'node:fs/promises'
  7. import {
  8. ClackPromptPort,
  9. HeadlessPromptError,
  10. HeadlessPromptPort,
  11. NodeCommandRunner,
  12. PromptCancelledError,
  13. type PackageManagerVersionProbe,
  14. type PromptPort,
  15. } from '@deepseek-ai/dsh-helper'
  16. import { parseCreateArgs, type CreateArgs } from './args.ts'
  17. import { CreateWizard, type ResolvedCreateRequest } from './create-wizard.ts'
  18. import { resolveHeadless } from './headless.ts'
  19. import { scaffoldProject, type ScaffoldResult } from './project-scaffolder.ts'
  20. import { CREATE_TEMPLATES, packageManagerTemplateModel } from './templates/create-templates.ts'
  21. /** Process and terminal slice used by the initializer. */
  22. export interface CreateCommandContext {
  23. cwd: string
  24. stdin: NodeJS.ReadStream
  25. stdout: NodeJS.WriteStream
  26. stderr: NodeJS.WriteStream
  27. releaseVersion?: string
  28. versionProbe?: PackageManagerVersionProbe
  29. port?: PromptPort
  30. setup?: (request: ResolvedCreateRequest) => Promise<void>
  31. }
  32. /** Read this initializer package's release version in source and built layouts. */
  33. export async function readCreateSdkVersion(): Promise<string> {
  34. const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')) as { version?: unknown }
  35. /* v8 ignore next -- this package's checked-in manifest always carries its version */
  36. if (typeof manifest.version !== 'string') throw new Error('create-sdk package version is missing')
  37. return manifest.version
  38. }
  39. /** Resolve, write, optionally install, and build one new project. */
  40. export async function createProject(
  41. argv: readonly string[],
  42. context: CreateCommandContext,
  43. ): Promise<ScaffoldResult | undefined> {
  44. const args = parseCreateArgs(argv)
  45. // Under --json, stdout carries only NDJSON events: human-readable progress
  46. // and package-manager child output move to stderr.
  47. const progress = args.json === true ? context.stderr : context.stdout
  48. if (args.help) {
  49. context.stdout.write(CREATE_TEMPLATES.usage.render({}))
  50. return undefined
  51. }
  52. const headless = await resolveHeadless(args)
  53. if (!headless && !context.port && (!context.stdin.isTTY || !context.stdout.isTTY)) {
  54. throw new Error('create-sdk requires an interactive TTY, --config <file>, or --config-json <json>')
  55. }
  56. const wizard = new CreateWizard({
  57. args: headless ? headless.args : args,
  58. /* v8 ignore next -- production TTY wiring is exercised by the built-bin smoke */
  59. port: context.port ?? (headless ? new HeadlessPromptPort() : new ClackPromptPort(context.stdin, context.stdout)),
  60. cwd: context.cwd,
  61. releaseVersion: context.releaseVersion ?? await readCreateSdkVersion(),
  62. ...context.versionProbe ? { versionProbe: context.versionProbe } : {},
  63. ...headless?.features ? { features: headless.features } : {},
  64. })
  65. const resolved = await wizard.run()
  66. const result = await scaffoldProject(resolved.directory, resolved.request)
  67. progress.write(CREATE_TEMPLATES.created.render({
  68. name: resolved.request.name,
  69. directory: resolved.directory,
  70. }))
  71. if (resolved.install) {
  72. try {
  73. if (context.setup) await context.setup(resolved)
  74. else {
  75. const runner = args.json === true ? new NodeCommandRunner(context.stderr) : new NodeCommandRunner()
  76. await resolved.request.packageManager.install(resolved.directory, runner)
  77. await resolved.request.packageManager.build(resolved.directory, runner)
  78. }
  79. } catch (error) {
  80. context.stderr.write(CREATE_TEMPLATES.setupFailure.render({
  81. directory: resolved.directory,
  82. error: String(error),
  83. ...packageManagerTemplateModel(resolved.request.packageManager),
  84. }))
  85. throw error
  86. }
  87. }
  88. progress.write(CREATE_TEMPLATES.nextSteps.render({
  89. directory: resolved.directory,
  90. setupRequired: !resolved.install,
  91. ...packageManagerTemplateModel(resolved.request.packageManager),
  92. }))
  93. return result
  94. }
  95. /** Whether NDJSON lifecycle events were requested, tolerating unparseable argv. */
  96. function wantsJsonEvents(argv: readonly string[]): boolean {
  97. let parsed: CreateArgs
  98. try {
  99. parsed = parseCreateArgs(argv)
  100. } catch {
  101. return false
  102. }
  103. return parsed.json === true
  104. }
  105. /** Run the create command with process defaults and convert cancellation to a clean exit. */
  106. export async function runCreateCommand(
  107. argv: readonly string[] = process.argv.slice(2),
  108. context: CreateCommandContext = {
  109. cwd: process.cwd(),
  110. stdin: process.stdin,
  111. stdout: process.stdout,
  112. stderr: process.stderr,
  113. },
  114. ): Promise<number> {
  115. const json = wantsJsonEvents(argv)
  116. const emit = (event: Record<string, unknown>): void => {
  117. context.stdout.write(`${JSON.stringify(event)}\n`)
  118. }
  119. try {
  120. await createProject(argv, context)
  121. if (json) emit({ type: 'done' })
  122. return 0
  123. } catch (error) {
  124. if (error instanceof PromptCancelledError) {
  125. if (json) emit({ type: 'error', reason: 'cancelled' })
  126. else context.stderr.write('create-sdk: cancelled\n')
  127. return 1
  128. }
  129. if (json && error instanceof HeadlessPromptError) {
  130. emit({ type: 'action-required', prompt: error.prompt })
  131. return 1
  132. }
  133. const message = error instanceof Error ? error.message : String(error)
  134. if (json) emit({ type: 'error', message })
  135. else context.stderr.write(`create-sdk: ${message}\n`)
  136. return 1
  137. }
  138. }