feat(sdk): add developer project tooling
docs(rfc): propose SDK developer project tooling feat: rename / docs ci: fix windows gates docs: revert
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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' | 'custom'
|
||||
baseURL?: string
|
||||
apiKey?: string
|
||||
model?: string
|
||||
runInterface?: RunInterface
|
||||
packageManager?: PackageManagerName
|
||||
install?: boolean
|
||||
linkWorkspace?: boolean
|
||||
help: boolean
|
||||
}
|
||||
|
||||
interface CommanderCreateOptions {
|
||||
description?: string
|
||||
provider?: 'deepseek' | 'custom'
|
||||
baseUrl?: string
|
||||
apiKey?: string
|
||||
model?: string
|
||||
interface?: RunInterface
|
||||
pm?: PackageManagerName
|
||||
install?: boolean
|
||||
linkWorkspace?: 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', 'custom']))
|
||||
.option('--base-url <url>')
|
||||
.option('--api-key <key>')
|
||||
.option('--model <name>')
|
||||
.addOption(new Option('--interface <name>').choices(['acp', 'stdio', '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')
|
||||
}
|
||||
|
||||
/** 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 } : {},
|
||||
help: options.help ?? false,
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Internal create-sdk command composition used by the package bin.
|
||||
*
|
||||
* @module @deepseek-ai/create-sdk/command
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import {
|
||||
ClackPromptPort,
|
||||
PromptCancelledError,
|
||||
type PackageManagerVersionProbe,
|
||||
type PromptPort,
|
||||
} from '@deepseek-ai/dsh-helper'
|
||||
import { parseCreateArgs } from './args.ts'
|
||||
import { CreateWizard, type ResolvedCreateRequest } from './create-wizard.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)
|
||||
if (args.help) {
|
||||
context.stdout.write(CREATE_TEMPLATES.usage.render({}))
|
||||
return undefined
|
||||
}
|
||||
if (!context.port && (!context.stdin.isTTY || !context.stdout.isTTY)) {
|
||||
throw new Error('create-sdk requires an interactive TTY')
|
||||
}
|
||||
const wizard = new CreateWizard({
|
||||
args,
|
||||
/* v8 ignore next -- production TTY wiring is exercised by the built-bin smoke */
|
||||
port: context.port ?? new ClackPromptPort(context.stdin, context.stdout),
|
||||
cwd: context.cwd,
|
||||
releaseVersion: context.releaseVersion ?? await readCreateSdkVersion(),
|
||||
...context.versionProbe ? { versionProbe: context.versionProbe } : {},
|
||||
})
|
||||
const resolved = await wizard.run()
|
||||
const result = await scaffoldProject(resolved.directory, resolved.request)
|
||||
context.stdout.write(CREATE_TEMPLATES.created.render({
|
||||
name: resolved.request.name,
|
||||
directory: resolved.directory,
|
||||
}))
|
||||
if (resolved.install) {
|
||||
try {
|
||||
if (context.setup) await context.setup(resolved)
|
||||
else {
|
||||
await resolved.request.packageManager.install(resolved.directory)
|
||||
await resolved.request.packageManager.build(resolved.directory)
|
||||
}
|
||||
} catch (error) {
|
||||
context.stderr.write(CREATE_TEMPLATES.setupFailure.render({
|
||||
directory: resolved.directory,
|
||||
error: String(error),
|
||||
...packageManagerTemplateModel(resolved.request.packageManager),
|
||||
}))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
context.stdout.write(CREATE_TEMPLATES.nextSteps.render({
|
||||
directory: resolved.directory,
|
||||
packageManager: resolved.request.packageManager.name,
|
||||
}))
|
||||
return result
|
||||
}
|
||||
|
||||
/** 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> {
|
||||
try {
|
||||
await createProject(argv, context)
|
||||
return 0
|
||||
} catch (error) {
|
||||
if (error instanceof PromptCancelledError) {
|
||||
context.stderr.write('create-sdk: cancelled\n')
|
||||
return 1
|
||||
}
|
||||
context.stderr.write(`create-sdk: ${error instanceof Error ? error.message : String(error)}\n`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* 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' | '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' | 'custom'>({
|
||||
id: 'provider',
|
||||
message: 'Model provider',
|
||||
options: [
|
||||
{ value: 'deepseek', label: 'DeepSeek' },
|
||||
{ value: 'custom', label: 'Custom endpoint (pi-ai)' },
|
||||
],
|
||||
initialValue: 'deepseek',
|
||||
}),
|
||||
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 server' },
|
||||
{ value: 'stdio', label: 'Terminal REPL' },
|
||||
{ value: 'embed', label: 'Embedded context' },
|
||||
],
|
||||
initialValue: 'stdio',
|
||||
}),
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
/** Bind parsed args and infrastructure to one wizard run. */
|
||||
constructor(options: {
|
||||
args: CreateArgs
|
||||
port: PromptPort
|
||||
cwd?: string
|
||||
releaseVersion: string
|
||||
versionProbe?: PackageManagerVersionProbe
|
||||
userAgent?: string
|
||||
}) {
|
||||
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
|
||||
}
|
||||
|
||||
/** 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 = [...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),
|
||||
})),
|
||||
} : {},
|
||||
}
|
||||
}),
|
||||
}))]
|
||||
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)
|
||||
}
|
||||
for (const [id, options] of choices) {
|
||||
selections.push(await configurator.configure(
|
||||
registry.get(id),
|
||||
profile,
|
||||
undefined,
|
||||
options,
|
||||
))
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* The create-sdk package is a CLI initializer; its library entry exports no symbols.
|
||||
*
|
||||
* @module @deepseek-ai/create-sdk
|
||||
*/
|
||||
|
||||
export {}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Created {{name}} in {{directory}}
|
||||
@@ -0,0 +1 @@
|
||||
Run {{packageManager}} {{installArgs}} and then build the project?
|
||||
@@ -0,0 +1 @@
|
||||
Next: cd {{directory}} && {{packageManager}} start
|
||||
@@ -0,0 +1,2 @@
|
||||
Project files are ready, but setup failed: {{error}}
|
||||
Retry: cd {{directory}} && {{packageManager}} {{installArgs}} && {{packageManager}} {{buildArgs}}
|
||||
@@ -0,0 +1,11 @@
|
||||
Usage: create-sdk [directory] [options]
|
||||
|
||||
Options:
|
||||
--description <text>
|
||||
--provider <deepseek|custom>
|
||||
--base-url <url>
|
||||
--api-key <key>
|
||||
--model <name>
|
||||
--interface <acp|stdio|embed>
|
||||
--pm <npm|pnpm|yarn>
|
||||
--install / --no-install
|
||||
@@ -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 {
|
||||
directory: string
|
||||
packageManager: PackageManagerName
|
||||
}
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user