refactor(packages): dissolve ui/ and rename sdk/ to scaffold/

git mv per the regrouping RFC: the five human-collaboration seams and
tui join packages/interaction/, app-boot becomes packages/boot/, and
jsonrpc joins the renamed scaffold/ (formerly sdk/) as its server half
beside client/protocol/create-sdk/helper/scripts/telemetry, whose
folders drop the legacy sdk- prefix. Three new group README triplets
replace the ui/ and sdk/ ones; tsconfig references/paths/globs,
knip keys, vitest globs, gate scripts, catalogs, docs, and the
lockfile follow. Adds the four settled FIXME rename markers
(dsh-sdk-server, dsh-sdk-telemetry, dsh-sdk-helper, dsh-sdk-scripts).

The scaffold folders diverge from their npm names until those renames
land, so tsconfig.base.json maps the three affected names explicitly
beside the group wildcard. Also repairs two pre-existing stale-path
classes the strengthened sweep surfaced: docs/web-styling.md's retired
web-ui host package and type-model spec fixture-literal joins.

app-boot's three Loader-composition specs time out at the default 5s
under full-suite parallel load on this filesystem (pre-existing;
pass isolated with --testTimeout=30000); interaction/scaffold/boot
suites otherwise green (687 passed).
This commit is contained in:
Tianyi Cui
2026-07-30 03:13:49 +08:00
parent 7e445c3a67
commit 3fc35c91ff
351 changed files with 368 additions and 311 deletions

View File

@@ -0,0 +1,96 @@
/**
* Commander adapter for the create-sdk command surface.
*
* @module @deepseek-ai/create-sdk/args
*/
import { Command, Option } from 'commander'
import type { PackageManagerName, RunInterface } from '@deepseek-ai/dsh-helper'
/** Parsed create command flags before interactive resolution. */
export interface CreateArgs {
directory?: string
description?: string
provider?: 'deepseek-official' | 'custom'
baseURL?: string
apiKey?: string
model?: string
runInterface?: RunInterface
packageManager?: PackageManagerName
install?: boolean
linkWorkspace?: boolean
config?: string
configJson?: string
json?: boolean
help: boolean
}
interface CommanderCreateOptions {
description?: string
provider?: 'deepseek-official' | 'custom'
baseUrl?: string
apiKey?: string
model?: string
interface?: RunInterface
pm?: PackageManagerName
install?: boolean
linkWorkspace?: boolean
config?: string
configJson?: string
json?: boolean
help?: boolean
}
function createProgram(): Command {
return new Command()
.name('create-sdk')
.description('Create a DeepSeek Harness SDK project')
.helpOption(false)
.showHelpAfterError(false)
.exitOverride()
.configureOutput({
/* v8 ignore next -- the command wrapper renders the package-owned usage template */
writeOut: () => {},
/* v8 ignore next -- Commander output is deliberately suppressed; errors are returned to the bin wrapper */
writeErr: () => {},
})
.argument('[directory]')
.option('-h, --help')
.option('--description <text>')
.addOption(new Option('--provider <name>').choices(['deepseek-official', 'custom']))
.option('--base-url <url>')
.option('--api-key <key>')
.option('--model <name>')
.addOption(new Option('--interface <name>').choices(['acp', 'embed']))
.addOption(new Option('--pm <name>').choices(['npm', 'pnpm', 'yarn']))
.addOption(new Option('--install').default(undefined))
.addOption(new Option('--no-install').default(undefined))
.option('--link-workspace')
.option('--config <path>')
.option('--config-json <json>')
.addOption(new Option('--json').default(undefined))
}
/** Parse create-sdk positionals/options through Commander into a domain-neutral value. */
export function parseCreateArgs(argv: readonly string[]): CreateArgs {
const program = createProgram()
program.parse([...argv], { from: 'user' })
const options = program.opts<CommanderCreateOptions>()
const directory = program.processedArgs[0] as string | undefined
return {
...directory === undefined ? {} : { directory },
...options.description === undefined ? {} : { description: options.description },
...options.provider === undefined ? {} : { provider: options.provider },
...options.baseUrl === undefined ? {} : { baseURL: options.baseUrl },
...options.apiKey === undefined ? {} : { apiKey: options.apiKey },
...options.model === undefined ? {} : { model: options.model },
...options.interface === undefined ? {} : { runInterface: options.interface },
...options.pm === undefined ? {} : { packageManager: options.pm },
...options.install === undefined ? {} : { install: options.install },
...options.linkWorkspace ? { linkWorkspace: true } : {},
...options.config === undefined ? {} : { config: options.config },
...options.configJson === undefined ? {} : { configJson: options.configJson },
...options.json === undefined ? {} : { json: options.json },
help: options.help ?? false,
}
}

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env node
/**
* Self-executing create-sdk command.
*
* @module @deepseek-ai/create-sdk/bin
*/
import { runCreateCommand } from './command.ts'
process.exitCode = await runCreateCommand()

View File

@@ -0,0 +1,144 @@
/**
* Internal create-sdk command composition used by the package bin.
*
* @module @deepseek-ai/create-sdk/command
*/
import { readFile } from 'node:fs/promises'
import {
ClackPromptPort,
HeadlessPromptError,
HeadlessPromptPort,
NodeCommandRunner,
PromptCancelledError,
type PackageManagerVersionProbe,
type PromptPort,
} from '@deepseek-ai/dsh-helper'
import { parseCreateArgs, type CreateArgs } from './args.ts'
import { CreateWizard, type ResolvedCreateRequest } from './create-wizard.ts'
import { resolveHeadless } from './headless.ts'
import { scaffoldProject, type ScaffoldResult } from './project-scaffolder.ts'
import { CREATE_TEMPLATES, packageManagerTemplateModel } from './templates/create-templates.ts'
/** Process and terminal slice used by the initializer. */
export interface CreateCommandContext {
cwd: string
stdin: NodeJS.ReadStream
stdout: NodeJS.WriteStream
stderr: NodeJS.WriteStream
releaseVersion?: string
versionProbe?: PackageManagerVersionProbe
port?: PromptPort
setup?: (request: ResolvedCreateRequest) => Promise<void>
}
/** Read this initializer package's release version in source and built layouts. */
export async function readCreateSdkVersion(): Promise<string> {
const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')) as { version?: unknown }
/* v8 ignore next -- this package's checked-in manifest always carries its version */
if (typeof manifest.version !== 'string') throw new Error('create-sdk package version is missing')
return manifest.version
}
/** Resolve, write, optionally install, and build one new project. */
export async function createProject(
argv: readonly string[],
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
}
const headless = await resolveHeadless(args)
if (!headless && !context.port && (!context.stdin.isTTY || !context.stdout.isTTY)) {
throw new Error('create-sdk requires an interactive TTY, --config <file>, or --config-json <json>')
}
const wizard = new CreateWizard({
args: headless ? headless.args : args,
/* v8 ignore next -- production TTY wiring is exercised by the built-bin smoke */
port: context.port ?? (headless ? new HeadlessPromptPort() : new ClackPromptPort(context.stdin, context.stdout)),
cwd: context.cwd,
releaseVersion: context.releaseVersion ?? await readCreateSdkVersion(),
...context.versionProbe ? { versionProbe: context.versionProbe } : {},
...headless?.features ? { features: headless.features } : {},
})
const resolved = await wizard.run()
const result = await scaffoldProject(resolved.directory, resolved.request)
progress.write(CREATE_TEMPLATES.created.render({
name: resolved.request.name,
directory: resolved.directory,
}))
if (resolved.install) {
try {
if (context.setup) await context.setup(resolved)
else {
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({
directory: resolved.directory,
error: String(error),
...packageManagerTemplateModel(resolved.request.packageManager),
}))
throw error
}
}
progress.write(CREATE_TEMPLATES.nextSteps.render({
directory: resolved.directory,
setupRequired: !resolved.install,
...packageManagerTemplateModel(resolved.request.packageManager),
}))
return result
}
/** Whether NDJSON lifecycle events were requested, tolerating unparseable argv. */
function wantsJsonEvents(argv: readonly string[]): boolean {
let parsed: CreateArgs
try {
parsed = parseCreateArgs(argv)
} catch {
return false
}
return parsed.json === true
}
/** Run the create command with process defaults and convert cancellation to a clean exit. */
export async function runCreateCommand(
argv: readonly string[] = process.argv.slice(2),
context: CreateCommandContext = {
cwd: process.cwd(),
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
},
): Promise<number> {
const json = wantsJsonEvents(argv)
const emit = (event: Record<string, unknown>): void => {
context.stdout.write(`${JSON.stringify(event)}\n`)
}
try {
await createProject(argv, context)
if (json) emit({ type: 'done' })
return 0
} catch (error) {
if (error instanceof PromptCancelledError) {
if (json) emit({ type: 'error', reason: 'cancelled' })
else context.stderr.write('create-sdk: cancelled\n')
return 1
}
if (json && error instanceof HeadlessPromptError) {
emit({ type: 'action-required', prompt: error.prompt })
return 1
}
const message = error instanceof Error ? error.message : String(error)
if (json) emit({ type: 'error', message })
else context.stderr.write(`create-sdk: ${message}\n`)
return 1
}
}

View File

@@ -0,0 +1,204 @@
/**
* Static create-sdk question sequence; dynamic feature/plugin loops remain
* in the wizard orchestrator.
*
* @module @deepseek-ai/create-sdk/create-questions
*/
import { existsSync } from 'node:fs'
import { basename, resolve } from 'node:path'
import {
ConfirmQuestion,
SecretQuestion,
SelectQuestion,
TextQuestion,
requireAnswer,
type PromptPort,
type Question,
type RunInterface,
} from '@deepseek-ai/dsh-helper'
import type { CreateArgs } from './args.ts'
/** Answers that establish project identity and feature applicability. */
export interface ProjectAnswers {
directory: string
name: string
description: string
provider: 'deepseek-official' | 'custom'
baseURL: string
apiKey: string
model: string
runInterface: RunInterface
}
interface ProjectAnswerState extends Partial<ProjectAnswers> {
readonly args: CreateArgs
readonly cwd: string
}
interface WizardStep<TState> {
run(port: PromptPort, state: TState): Promise<void>
}
function questionStep<TState, TValue>(options: {
question: (state: TState) => Question<TValue>
when?: (state: TState) => boolean
prefilled?: (state: TState) => TValue | undefined
apply: (state: TState, value: TValue) => void
}): WizardStep<TState> {
return {
async run(port, state) {
if (options.when && !options.when(state)) return
const value = requireAnswer(await options.question(state).resolve(port, options.prefilled?.(state)))
options.apply(state, value)
},
}
}
/** Validate one required text answer. */
function nonEmpty(value: string): string | undefined {
return value.trim().length === 0 ? 'A value is required' : undefined
}
function packageName(value: string): string | undefined {
if (!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(value)) {
return 'Use a lowercase npm package name'
}
return undefined
}
function projectDirectory(value: string, cwd: string): string | undefined {
const empty = nonEmpty(value)
if (empty) return empty
return existsSync(resolve(cwd, value)) ? 'Target already exists' : undefined
}
const API_KEY_STEP: WizardStep<ProjectAnswerState> = {
async run(port, state) {
let prefilled = state.args.apiKey
while (true) {
const apiKey = requireAnswer(await new SecretQuestion({
id: 'apiKey',
message: state.provider === 'custom' ? 'Custom provider API key' : 'DeepSeek API key',
}).resolve(port, prefilled))
if (apiKey.length > 0) {
state.apiKey = apiKey
return
}
const keepEmpty = requireAnswer(await new ConfirmQuestion({
id: 'apiKey.empty',
message: 'Keep the API key empty and fill .env later?',
initialValue: false,
tone: 'warning',
}).resolve(port))
if (keepEmpty) {
state.apiKey = ''
return
}
prefilled = undefined
}
},
}
const PROJECT_QUESTION_STEPS: readonly WizardStep<ProjectAnswerState>[] = [
questionStep({
question: state => new TextQuestion({
id: 'directory',
message: 'Where should the project be created?',
placeholder: 'my-agent',
defaultValue: 'my-agent',
validate: value => projectDirectory(value, state.cwd),
}),
prefilled: state => state.args.directory,
apply: (state, value) => { state.directory = resolve(state.cwd, value) },
}),
questionStep({
question: (state) => {
/* v8 ignore next -- the preceding directory step always populates this state */
if (!state.directory) throw new Error('directory must resolve before package name')
return new TextQuestion({
id: 'name',
message: 'Package name',
placeholder: basename(state.directory),
defaultValue: basename(state.directory),
validate: packageName,
})
},
apply: (state, value) => { state.name = value },
}),
questionStep({
question: (state) => {
/* v8 ignore next -- the preceding package-name step always populates this state */
if (!state.name) throw new Error('package name must resolve before description')
return new TextQuestion({
id: 'description',
message: 'Project description',
placeholder: `A DeepSeek Harness agent named ${state.name}`,
defaultValue: `A DeepSeek Harness agent named ${state.name}`,
validate: nonEmpty,
})
},
prefilled: state => state.args.description,
apply: (state, value) => { state.description = value },
}),
questionStep({
question: () => new SelectQuestion<'deepseek-official' | 'custom'>({
id: 'provider',
message: 'Model provider',
options: [
{ value: 'deepseek-official', label: 'DeepSeek' },
{ value: 'custom', label: 'Custom endpoint (pi-ai)' },
],
initialValue: 'deepseek-official',
}),
prefilled: state => state.args.provider,
apply: (state, value) => { state.provider = value },
}),
questionStep({
question: () => new TextQuestion({
id: 'baseURL', message: 'Custom provider base URL', validate: nonEmpty,
}),
when: state => state.provider === 'custom' || state.args.baseURL !== undefined,
prefilled: state => state.args.baseURL,
apply: (state, value) => { state.baseURL = value },
}),
API_KEY_STEP,
questionStep({
question: () => new SelectQuestion<RunInterface>({
id: 'interface',
message: 'Run interface',
options: [
{ value: 'acp', label: 'ACP automation server' },
{ value: 'embed', label: 'Embedded context' },
],
initialValue: 'acp',
}),
prefilled: state => state.args.runInterface,
apply: (state, value) => { state.runInterface = value },
}),
]
function completeAnswers(state: ProjectAnswerState): ProjectAnswers {
const keys = ['directory', 'name', 'description', 'provider', 'baseURL', 'apiKey', 'model', 'runInterface'] as const
for (const key of keys) {
/* v8 ignore next -- the fixed step list above populates every key or throws/cancels first */
if (state[key] === undefined) throw new Error(`create question did not resolve ${key}`)
}
return state as ProjectAnswerState & ProjectAnswers
}
/** Run the fixed project-context sequence in declaration order. */
export async function collectProjectAnswers(
port: PromptPort,
args: CreateArgs,
cwd: string,
): Promise<ProjectAnswers> {
const state: ProjectAnswerState = {
args,
cwd,
baseURL: args.baseURL ?? '',
model: args.model ?? 'deepseek-v4-flash',
}
for (const step of PROJECT_QUESTION_STEPS) await step.run(port, state)
return completeAnswers(state)
}

View File

@@ -0,0 +1,233 @@
/**
* Declarative create questions with dynamic feature and plugin orchestration.
*
* @module @deepseek-ai/create-sdk/create-wizard
*/
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import {
FeatureConfigurator,
ConfirmQuestion,
LocalPluginBlueprint,
NpmPackageManager,
SelectQuestion,
featureId,
createBuiltinRegistry,
createPackageManager,
inferPackageManagerName,
probePackageManagerVersion,
requireAnswer,
type FeatureRegistry,
type FeatureSelection,
type LocalPluginKind,
type PackageManager,
type PackageManagerName,
type PackageManagerVersionProbe,
type ProjectCreationRequest,
type ProjectProfile,
type PromptPort,
} from '@deepseek-ai/dsh-helper'
import type { CreateArgs } from './args.ts'
import { collectProjectAnswers, type ProjectAnswers } from './create-questions.ts'
import { CREATE_TEMPLATES, packageManagerTemplateModel } from './templates/create-templates.ts'
/** Fully resolved initializer request and post-create choice. */
export interface ResolvedCreateRequest {
directory: string
request: ProjectCreationRequest
install: boolean
}
/** Create-specific orchestration around declarative questions and dynamic selections. */
export class CreateWizard {
private readonly args: CreateArgs
private readonly port: PromptPort
private readonly cwd: string
private readonly releaseVersion: string
private readonly versionProbe: PackageManagerVersionProbe
private readonly userAgent: string
private readonly linkWorkspaceRoot: string | undefined
private readonly featurePlan: readonly FeatureSelection[] | undefined
/** Bind parsed args and infrastructure to one wizard run. */
constructor(options: {
args: CreateArgs
port: PromptPort
cwd?: string
releaseVersion: string
versionProbe?: PackageManagerVersionProbe
userAgent?: string
features?: readonly FeatureSelection[]
}) {
this.args = options.args
this.port = options.port
this.cwd = resolve(options.cwd ?? process.cwd())
this.releaseVersion = options.releaseVersion
this.versionProbe = options.versionProbe ?? probePackageManagerVersion
/* v8 ignore next -- pnpm supplies npm_config_user_agent while direct invocations may omit it */
this.userAgent = options.userAgent ?? process.env.npm_config_user_agent ?? ''
this.linkWorkspaceRoot = options.args.linkWorkspace
? fileURLToPath(new URL('../../../../', import.meta.url))
: undefined
this.featurePlan = options.features
}
/** Collect all answers before constructing any project files. */
async run(): Promise<ResolvedCreateRequest> {
const answers = await this.collectProjectAnswers()
const profile = this.provisionalProfile(answers)
const registry = createBuiltinRegistry(profile)
const features = await this.collectFeatures(profile, registry, answers)
const localPlugins = await this.collectPlugins()
const { manager, install } = await this.collectPackageManager()
return {
directory: answers.directory,
install,
request: {
name: answers.name,
description: answers.description,
runtime: { model: answers.model },
packageManager: manager,
releaseVersion: this.releaseVersion,
...this.linkWorkspaceRoot ? { linkWorkspaceRoot: this.linkWorkspaceRoot } : {},
features,
localPlugins,
},
}
}
private async collectProjectAnswers(): Promise<ProjectAnswers> {
return collectProjectAnswers(this.port, this.args, this.cwd)
}
private provisionalProfile(answers: ProjectAnswers): ProjectProfile {
return {
name: answers.name,
description: answers.description,
runtime: { model: answers.model },
runInterface: answers.runInterface,
packageManager: new NpmPackageManager('10.0.0'),
releaseVersion: this.releaseVersion,
...this.linkWorkspaceRoot ? { linkWorkspaceRoot: this.linkWorkspaceRoot } : {},
}
}
private async collectFeatures(
profile: ProjectProfile,
registry: FeatureRegistry,
answers: ProjectAnswers,
): Promise<FeatureSelection[]> {
const configurator = new FeatureConfigurator(this.port)
const selections: FeatureSelection[] = [
{
id: featureId('provider'),
options: [answers.provider],
...answers.baseURL ? { values: { baseURL: answers.baseURL } } : {},
secrets: { apiKey: answers.apiKey },
},
{ id: featureId('spine'), options: ['default'] },
{ id: featureId('app'), options: [answers.runInterface] },
]
const configurable = registry.all().filter(feature => feature.id === 'bash'
|| feature.id === 'persistence'
|| (!feature.required && feature.isApplicable(profile)))
const selected = this.featurePlan
? this.featurePlan.map(feature => ({ value: feature.id, choices: feature.options }))
: [...requireAnswer(await this.port.nestedMultiselect({
message: 'Select features',
options: configurable.map((feature) => {
const nested = feature.mode !== 'single'
const defaults = new Set(feature.defaultOptions(profile))
return {
value: feature.id,
label: feature.summary,
required: feature.required,
default: feature.required || feature.id === 'hmr' || feature.id === 'fs' || feature.id === 'todo'
|| feature.id === 'skill',
...nested ? {
choiceMode: feature.mode === 'multiple' ? 'multiple' as const : 'exclusive' as const,
choices: feature.options.map(option => ({
value: option.id,
label: option.label,
default: defaults.has(option.id),
})),
} : {},
}
}),
}))]
if (!this.featurePlan) {
for (const { value: id } of [...selected]) {
const feature = registry.get(id)
for (const suggestedId of feature.suggests) {
if (selected.some(item => item.value === suggestedId)) continue
const suggested = registry.get(suggestedId)
const add = requireAnswer(await new ConfirmQuestion({
id: `${feature.id}.${suggested.id}`,
message: `Add the recommended ${suggested.summary.toLowerCase()} for ${feature.summary.toLowerCase()}?`,
initialValue: true,
}).resolve(this.port))
if (add) selected.push({ value: suggested.id, choices: suggested.defaultOptions(profile) })
}
}
}
const fixed = new Set(selections.map(selection => selection.id))
const choices = new Map<FeatureSelection['id'], readonly string[] | undefined>()
for (const feature of registry.all()) {
if (feature.required && feature.isApplicable(profile) && !fixed.has(feature.id)) {
choices.set(feature.id, feature.defaultOptions(profile))
}
}
for (const choice of selected) {
choices.set(choice.value, choice.choices.length > 0 ? choice.choices : undefined)
}
const plannedById = new Map((this.featurePlan ?? []).map(feature => [feature.id, feature]))
for (const [id, options] of choices) {
const planned = plannedById.get(id)
selections.push(await configurator.configure(
registry.get(id),
profile,
undefined,
options,
planned?.secrets ?? {},
planned?.values ?? {},
))
}
return selections
}
private async collectPlugins(): Promise<LocalPluginBlueprint[]> {
const kind = requireAnswer(await new SelectQuestion<LocalPluginKind | 'none'>({
id: 'plugins.kind',
message: 'Local plugin',
options: [
{ value: 'none', label: 'No local plugin' },
{ value: 'plugin', label: 'Cordis plugin' },
{ value: 'tool', label: 'Model-facing tool' },
],
initialValue: 'none',
}).resolve(this.port))
return kind === 'none' ? [] : [new LocalPluginBlueprint(kind, kind)]
}
private async collectPackageManager(): Promise<{ manager: PackageManager; install: boolean }> {
const inferred = inferPackageManagerName(this.args.packageManager, this.userAgent)
const name = requireAnswer(await new SelectQuestion<PackageManagerName>({
id: 'packageManager',
message: 'Package manager',
options: [
{ value: 'npm', label: 'npm' },
{ value: 'pnpm', label: 'pnpm' },
{ value: 'yarn', label: 'Yarn' },
],
initialValue: inferred ?? 'npm',
}).resolve(this.port, inferred))
const manager = createPackageManager(name, await this.versionProbe(name, this.cwd))
const install = requireAnswer(await new ConfirmQuestion({
id: 'install',
message: CREATE_TEMPLATES.installQuestion.render(packageManagerTemplateModel(manager)).trimEnd(),
initialValue: true,
}).resolve(this.port, this.args.install))
return { manager, install }
}
}

View File

@@ -0,0 +1,98 @@
/**
* 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<string, unknown> {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${source}: expected a JSON object`)
}
return value as Record<string, unknown>
}
/** 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<string> = path => readFile(path, 'utf8'),
): Promise<ResolvedHeadless | undefined> {
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 }
}

View File

@@ -0,0 +1,7 @@
/**
* The create-sdk package is a CLI initializer; its library entry exports no symbols.
*
* @module @deepseek-ai/create-sdk
*/
export {}

View File

@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/create-sdk`.
* @module @deepseek-ai/create-sdk/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/create-sdk'
/** Cordis companion plugin name. */
export const name = 'create-sdk-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: this SDK build-time package owns no live event stream or mutable data;
* generated output and consumer tests cover its contract.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */

View File

@@ -0,0 +1,41 @@
/**
* Project creation use case over the shared SDK aggregate and edit session.
*
* @module @deepseek-ai/create-sdk/project-scaffolder
*/
import { stat } from 'node:fs/promises'
import {
SdkProject,
createBuiltinRegistry,
type ChangeSet,
type ProjectCreationRequest,
} from '@deepseek-ai/dsh-helper'
/** Result of writing one new SDK project. */
export interface ScaffoldResult {
project: SdkProject
changes: ChangeSet
}
/** Create a project entirely in memory, then validate and commit it once. */
export async function scaffoldProject(root: string, request: ProjectCreationRequest): Promise<ScaffoldResult> {
let targetExists = true
try {
await stat(root)
} catch (error) {
/* v8 ignore else -- the other arm requires a filesystem permission/IO fault from stat */
if ((error as NodeJS.ErrnoException).code === 'ENOENT') targetExists = false
/* v8 ignore next -- paired with the ignored defensive stat-error arm above */
else throw error
}
if (targetExists) throw new Error(`target already exists: ${root}`)
const project = SdkProject.create(root, request)
const registry = createBuiltinRegistry(project.profile)
const edit = project.edit(registry)
for (const selection of request.features) {
edit.installFeature(registry.get(selection.id), selection)
}
for (const plugin of request.localPlugins) edit.addPlugin(plugin)
return edit.commit()
}

View File

@@ -0,0 +1 @@
Created {{name}} in {{directory}}

View File

@@ -0,0 +1 @@
Run {{packageManager}} {{installArgs}} and then build the project?

View File

@@ -0,0 +1,5 @@
{{#if setupRequired}}
Next: cd {{directory}} && {{packageManager}} {{installArgs}} && {{packageManager}} {{buildArgs}} && {{packageManager}} start
{{else}}
Next: cd {{directory}} && {{packageManager}} start
{{/if}}

View File

@@ -0,0 +1,2 @@
Project files are ready, but setup failed: {{error}}
Retry: cd {{directory}} && {{packageManager}} {{installArgs}} && {{packageManager}} {{buildArgs}}

View File

@@ -0,0 +1,14 @@
Usage: create-sdk [directory] [options]
Options:
--description <text>
--provider <deepseek-official|custom>
--base-url <url>
--api-key <key>
--model <name>
--interface <acp|embed>
--pm <npm|pnpm|yarn>
--install / --no-install
--config <path>
--config-json <json>
--json

View File

@@ -0,0 +1,59 @@
/**
* Package-owned terminal templates for create-sdk.
*
* @module @deepseek-ai/create-sdk/templates/create-templates
*/
import {
TextTemplate,
type PackageManager,
type PackageManagerName,
} from '@deepseek-ai/dsh-helper'
interface CreatedTemplateModel {
name: string
directory: string
}
interface NextStepsTemplateModel extends PackageManagerTemplateModel {
directory: string
setupRequired: boolean
}
interface SetupFailureTemplateModel extends PackageManagerTemplateModel {
directory: string
error: string
}
/** Package-manager execution data consumed by create-sdk templates. */
export interface PackageManagerTemplateModel {
packageManager: PackageManagerName
installArgs: string
buildArgs: string
}
/**
* Map package-manager execution data into terminal-template fields.
* @param manager - selected package-manager strategy.
* @returns executable name and operation arguments.
*/
export function packageManagerTemplateModel(manager: PackageManager): PackageManagerTemplateModel {
return {
packageManager: manager.name,
installArgs: manager.installCommand().join(' '),
buildArgs: manager.buildCommand().join(' '),
}
}
/** Compiled create-sdk terminal templates. */
export const CREATE_TEMPLATES = {
usage: TextTemplate.fromFile<Record<string, never>>(new URL('./assets/usage.txt.tpl', import.meta.url)),
created: TextTemplate.fromFile<CreatedTemplateModel>(new URL('./assets/created.txt.tpl', import.meta.url)),
nextSteps: TextTemplate.fromFile<NextStepsTemplateModel>(new URL('./assets/next-steps.txt.tpl', import.meta.url)),
setupFailure: TextTemplate.fromFile<SetupFailureTemplateModel>(
new URL('./assets/setup-failure.txt.tpl', import.meta.url),
),
installQuestion: TextTemplate.fromFile<PackageManagerTemplateModel>(
new URL('./assets/install-question.txt.tpl', import.meta.url),
),
} as const