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,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/scaffold/scripts/README.md
README.md: 9a696bf5a4de9a80f0741f07a7e753733bc2f998
README.zh.md: cf9fc2cbbe467eb86e89d9eb77e9bc34860b5a3a

View File

@@ -0,0 +1,37 @@
# `@deepseek-ai/dsh-scripts`
English | [中文](README.zh.md)
The `dsh-sdk` launcher owns SDK project startup and configuration.
| Command | Behavior |
|---|---|
| `dsh-sdk start [target] [-- args…]` | Import a module target and invoke `main(bootContext)`, or boot `cordis.yml` when omitted; arguments after `--` are forwarded |
| `dsh-sdk dev [target] [-- args…]` | Register TypeScript and local-workspace source resolution, then use the start path |
| `dsh-sdk build [args…]` | Invoke the project's installed tsdown with the project arguments |
| `dsh-sdk config` | Open one interactive edit session, review accumulated changes, commit once, and install once when NPM dependencies changed |
| `dsh-sdk create <source>` | Add an external Cordis plugin from a native package-manager source (`pkg@version` or `github:owner/repo#ref`): confirm, `<pm> add <source>`, then mount the resolved dependency in `cordis.yml`. No giget/pacote; the package manager resolves and pins the source (github deps build via their own `prepare` under the manager's policy) |
`ProjectBuild(tsdownConfig)` and `PluginBuild(tsdownConfig)` are exported only from `@deepseek-ai/dsh-scripts/dev/tsdown-config`. Development and production read the same `cordis.yml`.
Generated project scripts invoke `dsh-sdk` for dev, build, start, and config; typecheck runs `tsc -b` directly. HMR remains an explicit `cordis.yml` feature loaded by both dev and start.
The runtime library exports `startSDK(source)` to load `.env` and `cordis.yml` and return the live context, and `runSDK(target)` to import a project module and invoke its `main(bootContext)` (`runSDK()` without a target delegates to `startSDK('./cordis.yml')`). `SdkBootContext` carries the raw forwarded `argv`, generic `args`, the absolute launcher `cwd`, and the `start`/`dev` mode. The launcher declares no project options: Node `parseArgs()` runs with zero schema, so valued flags use `--key=value`, bare flags become booleans, `--no-cache` becomes `args.cache = false`, and option names retain Node's spelling (`--max-depth=3``args['max-depth']`).
`start` never builds. `dev` registers the project-installed tsx transform plus an exact package-name map from `plugins/*/package.json` to each `src/index.ts`, then follows the same start path. `build` invokes the project-installed tsdown and forwards its arguments; an absent tsdown config is a successful no-op.
`config` requires a TTY. One feature tree selects the desired enabled set; changed rows are highlighted, Right changes finite feature options, required rows cannot be deselected, inconsistent rows show diagnostics, and custom/manual Cordis config entries support enable/disable. The workflow reconciles that target into one edit session. Review & Apply commits once, then NPM dependency changes trigger one package-manager install. A failed install does not undo committed files.
The root library exports `startSDK`, `runSDK`, and the `SdkBootArgs`/`SdkBootContext` types; command composition remains private to the bin. No `src/*`, bin, or package-manifest subpath is exported.
## Model Experience
Indirectly, through the project `cordis.yml` tree loaded by `start` or `dev`.
#### KV Cache effect
No direct invalidation; the named consumer owns any request-prefix changes.
## Known Limitations and Deferred Work
- **Launcher arguments are schema-free** — `start` and `dev` preserve Node `parseArgs()` output rather than validating project-specific flags.

View File

@@ -0,0 +1,37 @@
# `@deepseek-ai/dsh-scripts`
[English](README.md) | 中文
`dsh-sdk` 启动器负责 SDK 项目启动与配置。
| 命令 | 行为 |
|---|---|
| `dsh-sdk start [target] [-- args…]` | 导入模块目标并调用 `main(bootContext)`;省略目标时启动 `cordis.yml``--` 后的参数原样转发 |
| `dsh-sdk dev [target] [-- args…]` | 注册 TypeScript 与本地工作区源代码解析,然后进入 start 路径 |
| `dsh-sdk build [args…]` | 使用项目参数调用项目已安装的 tsdown |
| `dsh-sdk config` | 打开一个交互式编辑会话审阅累计变更统一提交一次NPM 依赖变化时只安装一次 |
| `dsh-sdk create <source>` | 从包管理器原生支持的来源(`pkg@version``github:owner/repo#ref`)添加外部 Cordis 插件:确认后执行 `<pm> add <source>`,再将解析出的依赖挂载到 `cordis.yml`。不使用 gigetpacote由包管理器解析并固定来源GitHub 依赖会在管理器策略下通过自身 `prepare` 构建) |
`ProjectBuild(tsdownConfig)``PluginBuild(tsdownConfig)` 只从 `@deepseek-ai/dsh-scripts/dev/tsdown-config` 导出。开发环境与生产环境读取同一个 `cordis.yml`
生成项目的脚本通过 `dsh-sdk` 执行 dev、build、start 和 config类型检查直接运行 `tsc -b`。HMR热模块替换始终是显式的 `cordis.yml` 功能,并由 dev 与 start 同时加载。
运行时库导出 `startSDK(source)`,用于加载 `.env``cordis.yml` 并返回活跃上下文;还导出 `runSDK(target)`,用于导入项目模块并调用其 `main(bootContext)`(不带目标的 `runSDK()` 会委派给 `startSDK('./cordis.yml')`)。`SdkBootContext` 携带原样转发的 `argv`、通用 `args`、启动器的绝对 `cwd`,以及 `start``dev` 模式。启动器不声明项目选项Node `parseArgs()` 使用空 schema 运行,因此带值的标志写作 `--key=value`,裸标志变为布尔值,`--no-cache` 变为 `args.cache = false`,选项名称保留 Node 的拼写(`--max-depth=3``args['max-depth']`)。
`start` 绝不构建。`dev` 注册项目已安装的 tsx 转换,并建立从 `plugins/*/package.json` 中的精确包名到各自 `src/index.ts` 的映射,然后沿用相同的 start 路径。`build` 调用项目已安装的 tsdown 并转发其参数;缺少 tsdown 配置时视为成功且不执行操作。
`config` 要求 TTY。一个功能树用于选择期望的启用集合变更行会高亮Right 用于修改取值有限的功能选项,必需行无法取消选择,不一致行会显示诊断,自定义/手动 Cordis 配置项支持启用禁用。工作流会在一个编辑会话中将配置协调至该目标状态。Review & Apply 只提交一次;之后,如果 NPM 依赖有变更,则触发一次包管理器安装。安装失败不会撤销已提交文件。
根库导出 `startSDK``runSDK` 以及 `SdkBootArgs``SdkBootContext` 类型;命令组合仍是 bin 的私有实现。不导出 `src/*`、bin 或 package-manifest 子路径。
## 模型体验
通过项目 `cordis.yml` 树间接提供;该树由 `start``dev` 加载。
#### KV Cache 影响
不会直接导致 KV Cache 失效;由具名消费方负责请求前缀变更。
## 已知限制与暂缓事项
- **启动器参数没有 schema**`start``dev` 会保留 Node `parseArgs()` 输出,而不会验证项目专用标志。

View File

@@ -0,0 +1,64 @@
{
"name": "@deepseek-ai/dsh-scripts",
"description": "DeepSeek Harness SDK launcher for start, dev, build, and project configuration",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"bin": {
"dsh-sdk": "lib/bin.js"
},
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./dev/tsdown-config": {
"types": "./lib/types/dev/tsdown-config.d.ts",
"default": "./lib/dev/tsdown-config.js"
}
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/bin.js",
"lib/dev/tsdown-config.js",
"lib/local-plugin-loader-hooks.js",
"lib/assets",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"dependencies": {
"@deepseek-ai/dsh-helper": "workspace:^",
"@deepseek-ai/dsh-telemetry": "workspace:^",
"commander": "^15.0.0",
"node-addon-require-builtin": "^0.1.4"
},
"peerDependencies": {
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7",
"tsdown": "^0.22.2",
"tsx": "^4.22.4"
},
"peerDependenciesMeta": {
"tsdown": {
"optional": true
},
"tsx": {
"optional": true
}
},
"devDependencies": {
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"cordis": "^4.0.0-rc.7",
"tsdown": "^0.22.2",
"tsx": "^4.22.4"
}
}

View File

@@ -0,0 +1,74 @@
/**
* Commander adapter for the dsh-sdk subcommand surface.
*
* @module @deepseek-ai/dsh-scripts/args
*/
import { parseArgs as parseNodeArgs } from 'node:util'
import { Command } from 'commander'
/** Commands implemented by the dsh-sdk launcher. */
type DshSdkCommand = 'start' | 'dev' | 'build' | 'config' | 'create'
/** Parsed dsh-sdk invocation. */
export interface DshSdkArgs {
command?: DshSdkCommand
target?: string
source?: string
forwarded: readonly string[]
help: boolean
}
/** Parse arbitrary project flags through Node's zero-schema argument parser. */
export function parseSdkBootArgs(argv: readonly string[]): Record<string, string | boolean | undefined> {
return parseNodeArgs({
args: [...argv],
strict: false,
allowPositionals: true,
allowNegative: true,
}).values
}
/** Parse one launcher invocation through real Commander subcommands. */
export function parseDshSdkArgs(argv: readonly string[]): DshSdkArgs {
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
return { forwarded: [], help: true }
}
const separator = argv.indexOf('--')
const launcherArgv = separator === -1 ? argv : argv.slice(0, separator)
const passthrough = separator === -1 ? [] : argv.slice(separator + 1)
let parsed: DshSdkArgs | undefined
const program = new Command()
.name('dsh-sdk')
.helpOption(false)
.showHelpAfterError(false)
.exitOverride()
.configureOutput({
/* v8 ignore next -- the command wrapper renders the package-owned usage template */
writeOut: () => {},
/* v8 ignore next -- Commander errors are returned to the command wrapper */
writeErr: () => {},
})
program.command('start [target]').helpOption(false).action((target?: string) => {
parsed = { command: 'start', ...target ? { target } : {}, forwarded: [], help: false }
})
program.command('dev [target]').helpOption(false).action((target?: string) => {
parsed = { command: 'dev', ...target ? { target } : {}, forwarded: [], help: false }
})
program.command('build [args...]').helpOption(false).allowUnknownOption(true).action((args: string[] = []) => {
parsed = { command: 'build', forwarded: args, help: false }
})
program.command('config').helpOption(false).action(() => {
parsed = { command: 'config', forwarded: [], help: false }
})
program.command('create <source>').helpOption(false).action((source: string) => {
parsed = { command: 'create', source, forwarded: [], help: false }
})
program.parse([...launcherArgv], { from: 'user' })
/* v8 ignore next -- every registered Commander action above assigns parsed or Commander throws */
if (!parsed) throw new Error('dsh-sdk command did not resolve')
if (parsed.command === 'config' && passthrough.length > 0) {
throw new Error('dsh-sdk config does not accept forwarded arguments')
}
return { ...parsed, forwarded: [...parsed.forwarded, ...passthrough] }
}

View File

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

View File

@@ -0,0 +1,94 @@
/**
* User-owned tsdown configuration wrappers and child-process invocation.
*
* @module @deepseek-ai/dsh-scripts/build
*/
import { createRequire } from 'node:module'
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import type { UserConfig } from 'tsdown'
import { NodeCommandRunner, type CommandRunner } from '@deepseek-ai/dsh-helper'
function hasLocalPluginPackages(root: string): boolean {
const directory = resolve(root, 'plugins')
return existsSync(directory) && readdirSync(directory, { withFileTypes: true }).some(
item => item.isDirectory() && existsSync(resolve(directory, item.name, 'package.json')),
)
}
function hasTsdownConfig(root: string): boolean {
const hasConfigFile = [
'tsdown.config.ts', 'tsdown.config.mts', 'tsdown.config.cts',
'tsdown.config.js', 'tsdown.config.mjs', 'tsdown.config.cjs',
'tsdown.config.json',
]
.some(name => existsSync(resolve(root, name)))
if (hasConfigFile) return true
let manifestText: string
try {
manifestText = readFileSync(resolve(root, 'package.json'), 'utf8')
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
throw error
}
const manifest: unknown = JSON.parse(manifestText)
return manifest !== null && !Array.isArray(manifest) && typeof manifest === 'object'
&& Object.hasOwn(manifest, 'tsdown')
}
/**
* Preserve the developer's root config and append a separate workspace pass
* when generated local plugin packages exist.
* @param tsdownConfig - developer-owned root tsdown config.
* @returns root tsdown config and optional local-plugin workspace pass.
*/
export function ProjectBuild(tsdownConfig: UserConfig): UserConfig[] {
if (tsdownConfig.workspace !== undefined) {
throw new Error('ProjectBuild owns workspace discovery; remove config.workspace')
}
const root = resolve(tsdownConfig.cwd ?? process.cwd())
return hasLocalPluginPackages(root)
? [{ ...tsdownConfig }, { workspace: { include: ['plugins/*'] } }]
: [{ ...tsdownConfig }]
}
/**
* Preserve a local plugin package's developer-owned tsdown config.
* @param tsdownConfig - developer-owned plugin tsdown config.
* @returns validated tsdown config copy.
*/
export function PluginBuild(tsdownConfig: UserConfig): UserConfig {
if (tsdownConfig.workspace !== undefined) throw new Error('PluginBuild does not accept nested workspace config')
return { ...tsdownConfig }
}
function resolveTsdownBin(cwd: string): string {
const require = createRequire(resolve(cwd, 'package.json'))
let manifestPath: string
try {
manifestPath = require.resolve('tsdown/package.json')
} catch (error) {
throw new Error(`dsh-sdk build requires tsdown in this project: ${String(error)}`)
}
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { bin?: unknown }
const bin = typeof manifest.bin === 'string'
? manifest.bin
: manifest.bin && typeof manifest.bin === 'object'
? (manifest.bin as Record<string, unknown>).tsdown
: undefined
if (typeof bin !== 'string') throw new Error('installed tsdown package has no executable')
return resolve(dirname(manifestPath), bin)
}
/** Invoke the project's installed tsdown, forwarding all build arguments. */
export async function runProjectBuild(
args: readonly string[],
cwd: string = process.cwd(),
runner: CommandRunner = new NodeCommandRunner(),
): Promise<void> {
if (!hasTsdownConfig(cwd)) return
const result = await runner.run(process.execPath, [resolveTsdownBin(cwd), ...args], resolve(cwd))
if (result.signal) throw new Error(`tsdown was killed by ${result.signal}`)
if (result.exitCode !== 0) throw new Error(`tsdown exited with code ${String(result.exitCode)}`)
}

View File

@@ -0,0 +1,76 @@
/**
* Internal dsh-sdk command composition used by the package bin.
*
* @module @deepseek-ai/dsh-scripts/command
*/
import { parseDshSdkArgs } from './args.ts'
import { runProjectBuild } from './build.ts'
import { runConfigCommand, type ConfigCommandContext } from './config.ts'
import { runCreatePluginCommand } from './create-plugin.ts'
import { runSDK } from './runtime.ts'
import { reportCommandTelemetry, type CommandTelemetryEvent } from './telemetry.ts'
import { DSH_SDK_TEMPLATES } from './templates/dsh-sdk-templates.ts'
/** Injectable process and command boundaries used by the dsh-sdk bin. */
export interface DshSdkCommandContext extends ConfigCommandContext {
cwd: string
stdin: NodeJS.ReadStream
stdout: NodeJS.WriteStream
stderr: NodeJS.WriteStream
run?: typeof runSDK
build?: typeof runProjectBuild
config?: typeof runConfigCommand
createPlugin?: typeof runCreatePluginCommand
telemetry?: (event: CommandTelemetryEvent) => Promise<void>
}
/** Run one parsed dsh-sdk command and return its process exit code. */
export async function runDshSdkCommand(
argv: readonly string[] = process.argv.slice(2),
context: DshSdkCommandContext = {
cwd: process.cwd(),
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
},
): Promise<number> {
const startedAt = Date.now()
let command: string | undefined
let success = true
try {
const args = parseDshSdkArgs(argv)
if (args.help || !args.command) {
context.stdout.write(DSH_SDK_TEMPLATES.usage.render({}))
return 0
}
command = args.command
const run = context.run ?? runSDK
const build = context.build ?? runProjectBuild
const config = context.config ?? runConfigCommand
const createPlugin = context.createPlugin ?? runCreatePluginCommand
switch (args.command) {
case 'start': await run(args.target, { cwd: context.cwd, argv: args.forwarded }); break
case 'dev': await run(args.target, { cwd: context.cwd, dev: true, argv: args.forwarded }); break
case 'build': await build(args.forwarded, context.cwd); break
case 'config': {
const result = await config(context)
if (result.installError) { success = false; return 1 }
break
}
/* v8 ignore next -- Commander requires <source>, so create never dispatches without it */
case 'create': await createPlugin(args.source ?? '', context); break
}
return 0
} catch (error) {
success = false
context.stderr.write(`dsh-sdk: ${error instanceof Error ? error.message : String(error)}\n`)
return 1
} finally {
if (command !== undefined) {
/* v8 ignore next -- production telemetry wiring is exercised by the built-bin smoke */
const telemetry = context.telemetry ?? reportCommandTelemetry
await telemetry({ command, cwd: context.cwd, durationMs: Date.now() - startedAt, success })
}
}
}

View File

@@ -0,0 +1,37 @@
/**
* dsh-sdk config command composition.
*
* @module @deepseek-ai/dsh-scripts/config
*/
import {
ClackPromptPort,
SdkProject,
createBuiltinRegistry,
type PromptPort,
} from '@deepseek-ai/dsh-helper'
import { ConfigWorkflow, type ConfigWorkflowResult } from './config/config-workflow.ts'
/** Process stream slice required by dsh-sdk config. */
export interface ConfigCommandContext {
cwd: string
stdin: NodeJS.ReadStream
stdout: NodeJS.WriteStream
port?: PromptPort
install?: (project: SdkProject) => Promise<void>
}
/** Open and interactively edit one existing SDK project. */
export async function runConfigCommand(context: ConfigCommandContext): Promise<ConfigWorkflowResult> {
if (!context.port && (!context.stdin.isTTY || !context.stdout.isTTY)) {
throw new Error('dsh-sdk config requires an interactive TTY')
}
const project = await SdkProject.open(context.cwd)
const registry = createBuiltinRegistry(project.profile)
return new ConfigWorkflow(
/* v8 ignore next -- production TTY wiring is exercised by the built-bin smoke */
context.port ?? new ClackPromptPort(context.stdin, context.stdout),
context.stdout,
context.install,
).run(project, registry)
}

View File

@@ -0,0 +1,241 @@
/**
* Tree-shaped existing-project feature workflow and single Apply boundary.
*
* @module @deepseek-ai/dsh-scripts/config/config-workflow
*/
import type { Writable } from 'node:stream'
import {
FeatureConfigurator,
ConfirmQuestion,
requireAnswer,
type Feature,
type FeatureInstallation,
type FeatureRegistry,
type FeatureSelection,
type ChangeSet,
type NestedMultiSelectValue,
type ProjectCommitResult,
type PromptPort,
type RunInterface,
type SdkProject,
} from '@deepseek-ai/dsh-helper'
import { DSH_SDK_TEMPLATES } from '../templates/dsh-sdk-templates.ts'
/** Config result, including an install failure that happened after commit. */
export interface ConfigWorkflowResult {
commit?: ProjectCommitResult<SdkProject>
installError?: Error
}
/**
* Non-interactive desired end-state for a config run: the complete set of enabled
* features, with options and any secrets/values a newly installed feature needs.
* Features not listed are reconciled to disabled, exactly as an interactive tree
* selection would be. Custom (non-feature) cordis plugins keep their current state;
* toggling them headlessly is not yet supported.
*/
export interface ConfigPlan {
features: readonly FeatureSelection[]
}
function featureTarget(feature: Feature): string {
return `feature:${feature.id}`
}
function pluginTarget(id: string): string {
return `plugin:${id}`
}
function sameOptions(left: readonly string[], right: readonly string[]): boolean {
return [...left].sort().join('\0') === [...right].sort().join('\0')
}
function targetRunInterface(
current: RunInterface,
desired: ReadonlyMap<string, NestedMultiSelectValue<string, string>>,
): RunInterface {
const selected = desired.get('feature:app')?.choices[0]
return selected === 'acp' || selected === 'embed' ? selected : current
}
/** Reconcile one tree selection into domain commands, then review and commit once. */
export class ConfigWorkflow {
private readonly port: PromptPort
private readonly output: Writable
private readonly install: (project: SdkProject) => Promise<void>
/** Bind terminal prompts and descriptive output. */
constructor(
port: PromptPort,
output: Writable = process.stdout,
install: (project: SdkProject) => Promise<void> = project => project.profile.packageManager.install(project.root),
) {
this.port = port
this.output = output
this.install = install
}
/** Select desired state, reconcile the working copy, review, and apply. */
async run(project: SdkProject, registry: FeatureRegistry, plan?: ConfigPlan): Promise<ConfigWorkflowResult> {
const edit = project.edit(registry)
const configurator = new FeatureConfigurator(this.port)
const features = registry.all().filter(feature => feature.isApplicable(project.profile))
const inspections = new Map(edit.inspections().map(item => [item.id, item]))
const custom = edit.cordisConfigEntries().filter(entry => !registry.ownerOfPackage(entry.name, project.profile))
const desired = plan
? [
...plan.features.map(selection => ({
value: featureTarget(registry.get(selection.id)),
choices: selection.options,
})),
...custom
.filter(entry => !entry.disabled)
.map(entry => ({ value: pluginTarget(entry.id), choices: [] as readonly string[] })),
]
: requireAnswer(await this.port.nestedMultiselect<string, string>({
message: 'Configure the project',
showChanges: true,
options: [
...features.map((feature) => {
const installation = inspections.get(feature.id)
/* v8 ignore next -- inspections() is built from this exact feature registry */
if (!installation) throw new Error(`feature inspection is missing: ${feature.id}`)
const inconsistent = installation.state === 'inconsistent'
const selectedOptions = new Set(installation.options.length > 0
? installation.options
: feature.defaultOptions(project.profile))
return {
value: featureTarget(feature),
label: feature.summary,
required: feature.required,
default: feature.required || installation.state === 'enabled' || inconsistent,
disabled: inconsistent,
...inconsistent ? { warning: installation.diagnostics.join('; ') } : {},
...feature.mode === 'single' ? {} : {
choiceMode: feature.mode,
choices: feature.options.map(option => ({
value: option.id,
label: option.label,
default: selectedOptions.has(option.id),
})),
},
}
}),
...custom.map(entry => ({
value: pluginTarget(entry.id),
label: `${entry.name} [custom]`,
default: !entry.disabled,
})),
],
}))
const desiredByTarget = new Map(desired.map(item => [item.value, item]))
const targetProfile = {
...project.profile,
runInterface: targetRunInterface(project.profile.runInterface, desiredByTarget),
}
for (const feature of features) {
/* v8 ignore next -- no current built-in feature is interface-specific */
if (!feature.isApplicable(targetProfile)) desiredByTarget.delete(featureTarget(feature))
}
const plannedById = new Map<FeatureSelection['id'], FeatureSelection>(
(plan?.features ?? []).map(selection => [selection.id, selection]),
)
for (const feature of features) {
const installation = inspections.get(feature.id)
/* v8 ignore next -- inspections() is built from this exact feature registry */
if (!installation) throw new Error(`feature inspection is missing: ${feature.id}`)
if (installation.state === 'inconsistent') continue
const choice = desiredByTarget.get(featureTarget(feature))
if (!choice && !feature.required) continue
await this.enableOrConfigure(feature, installation, choice, project, edit, configurator, plannedById.get(feature.id))
}
for (const feature of [...features].reverse()) {
const installation = inspections.get(feature.id)
/* v8 ignore next -- inspections() is built from this exact feature registry */
if (!installation) throw new Error(`feature inspection is missing: ${feature.id}`)
if (feature.required || installation.state !== 'enabled'
|| desiredByTarget.has(featureTarget(feature))) continue
edit.disableFeature(feature)
}
for (const entry of custom) {
const enabled = desiredByTarget.has(pluginTarget(entry.id))
if (enabled === !entry.disabled) continue
edit.setCustomPluginDisabled(entry.id, !enabled)
}
const changes = edit.changes()
if (changes.changedFiles.length === 0) {
this.output.write('No changes.\n')
return {}
}
this.renderReview(changes)
const apply = requireAnswer(await new ConfirmQuestion({
id: 'config.apply', message: 'Apply these changes?', initialValue: true,
}).resolve(this.port))
if (!apply) return {}
const commit = await edit.commit()
if (!commit.changes.npmDependenciesChanged) return { commit }
try {
await this.install(project)
return { commit }
} catch (error) {
const installError = error instanceof Error ? error : new Error(String(error))
const manager = project.profile.packageManager
this.output.write(DSH_SDK_TEMPLATES.configInstallFailure.render({
error: installError.message,
packageManager: manager.name,
installArgs: manager.installCommand().join(' '),
}))
return { commit, installError }
}
}
private async enableOrConfigure(
feature: Feature,
installation: FeatureInstallation,
choice: NestedMultiSelectValue<string, string> | undefined,
project: SdkProject,
edit: ReturnType<SdkProject['edit']>,
configurator: FeatureConfigurator,
planned?: FeatureSelection,
): Promise<void> {
const options = choice?.choices.length
? choice.choices
: installation.options.length > 0
? installation.options
: feature.defaultOptions(project.profile)
if (installation.state === 'absent') {
const selection = await configurator.configure(
feature, project.profile, undefined, options, planned?.secrets ?? {}, planned?.values ?? {},
)
edit.installFeature(feature, selection)
return
}
/* v8 ignore next -- non-absent/non-inconsistent inspections always carry their normalized selection */
if (!installation.selection) throw new Error(`feature ${feature.id} has no readable selection`)
if (!sameOptions(installation.options, options)) {
const selection: FeatureSelection = await configurator.configure(
feature, project.profile, installation.selection, options, planned?.secrets ?? {}, planned?.values ?? {},
)
edit.configureFeature(feature, selection)
}
if (installation.state === 'disabled') edit.enableFeature(feature)
}
private renderReview(changes: ChangeSet): void {
const lines = [
...changes.addedFeatures.map(id => `Install feature: ${id}`),
...changes.enabledFeatures.map(id => `Enable feature: ${id}`),
...changes.disabledFeatures.map(id => `Disable feature: ${id}`),
...changes.configuredFeatures.map(id => `Configure feature: ${id}`),
...changes.enabledPlugins.map(id => `Enable custom plugin: ${id}`),
...changes.disabledPlugins.map(id => `Disable custom plugin: ${id}`),
...changes.changedFiles.map(path => `Change file: ${path}`),
]
this.output.write(`${lines.join('\n')}\n`)
}
}

View File

@@ -0,0 +1,91 @@
/**
* dsh-sdk create command: add an external Cordis plugin (github or npm) as a
* native package-manager dependency and mount it in cordis.yml.
*
* @module @deepseek-ai/dsh-scripts/create-plugin
*/
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import {
ClackPromptPort,
ConfirmQuestion,
SdkProject,
createBuiltinRegistry,
requireAnswer,
type PackageManager,
type ProjectCommitResult,
type PromptPort,
} from '@deepseek-ai/dsh-helper'
/** Process and interaction slice required by dsh-sdk create. */
export interface CreatePluginContext {
cwd: string
stdin: NodeJS.ReadStream
stdout: NodeJS.WriteStream
port?: PromptPort
add?: (manager: PackageManager, spec: string, cwd: string) => Promise<void>
}
/** Result of a create run; `undefined` when the confirmation was declined. */
export type CreatePluginResult = ProjectCommitResult<SdkProject> | undefined
/** Derive a stable cordis entry id from a package name's last path segment. */
function pluginId(packageName: string): string {
const base = packageName.startsWith('@') ? packageName.slice(packageName.indexOf('/') + 1) : packageName
const id = base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
/* v8 ignore next -- a valid npm package name always yields a non-empty id */
if (!id) throw new Error(`cannot derive a plugin id from package name: ${packageName}`)
return id
}
/** Read the direct dependency names declared in a project's package.json. */
async function dependencyNames(cwd: string): Promise<Set<string>> {
const manifest = JSON.parse(await readFile(join(cwd, 'package.json'), 'utf8')) as {
dependencies?: Record<string, unknown>
}
/* v8 ignore next -- generated projects always declare a dependencies map */
return new Set(Object.keys(manifest.dependencies ?? {}))
}
/**
* Add one external plugin dependency to the current project and mount it.
* @param source - a package-manager-native source (`pkg@version` or `github:owner/repo#ref`).
* @param context - process, interaction, and dependency-add boundaries.
* @returns the commit result, or `undefined` when the confirmation was declined.
*/
export async function runCreatePluginCommand(
source: string,
context: CreatePluginContext,
): Promise<CreatePluginResult> {
const spec = source.trim()
if (!spec) throw new Error('dsh-sdk create requires a plugin source (pkg@version or github:owner/repo#ref)')
if (!context.port && (!context.stdin.isTTY || !context.stdout.isTTY)) {
throw new Error('dsh-sdk create requires an interactive TTY')
}
const project = await SdkProject.open(context.cwd)
/* v8 ignore next -- production TTY wiring is exercised by the built-bin smoke */
const port = context.port ?? new ClackPromptPort(context.stdin, context.stdout)
const confirmed = requireAnswer(await new ConfirmQuestion({
id: 'create.confirm',
message: `Add plugin '${spec}' as a dependency and mount it in cordis.yml?`,
initialValue: true,
}).resolve(port))
if (!confirmed) return undefined
const before = await dependencyNames(context.cwd)
/* v8 ignore next -- production package-manager wiring is exercised by the built-bin smoke */
const add = context.add ?? ((manager, source, cwd) => manager.add(source, cwd))
await add(project.profile.packageManager, spec, context.cwd)
const after = await dependencyNames(context.cwd)
const added = [...after].filter(name => !before.has(name))
if (added.length === 0) throw new Error(`dsh-sdk create: '${spec}' added no new dependency`)
const reopened = await SdkProject.open(context.cwd)
const registry = createBuiltinRegistry(reopened.profile)
const edit = reopened.edit(registry)
for (const packageName of added) edit.addExternalPlugin(pluginId(packageName), packageName)
const commit = await edit.commit()
context.stdout.write(`Mounted ${added.join(', ')} in cordis.yml.\n`)
return commit
}

View File

@@ -0,0 +1,7 @@
/**
* Generated-project tsdown config wrappers.
*
* @module @deepseek-ai/dsh-scripts/dev/tsdown-config
*/
export { PluginBuild, ProjectBuild } from '../build.ts'

View File

@@ -0,0 +1,11 @@
/**
* Public DeepSeek Harness SDK runtime entry points.
*
* FIXME: rename to `@deepseek-ai/dsh-sdk-scripts` before the first tagged release —
* the current name is indefensibly generic as a published name
* ([regrouping Agent Note](../../../../.agents/notes/proposed/architecture/2026-07-29-package-regrouping.md)).
*
* @module @deepseek-ai/dsh-scripts
*/
export { runSDK, startSDK, type SdkBootContext } from './runtime.ts'

View File

@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-scripts`.
* @module @deepseek-ai/dsh-scripts/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-scripts'
/** Cordis companion plugin name. */
export const name = 'scripts-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,27 @@
/**
* Node module customization hook for project-local plugin package names.
*
* @module @deepseek-ai/dsh-scripts/local-plugin-loader-hooks
*/
import type { ResolveHookContext, ResolveFnOutput } from 'node:module'
interface HookData {
mappings: Readonly<Record<string, string>>
}
let mappings: Readonly<Record<string, string>> = {}
/** Receive the package-name to source-URL map from the launcher thread. */
export function initialize(data: HookData): void {
mappings = { ...data.mappings }
}
/** Resolve exact local workspace package names to their TypeScript entry source. */
export async function resolve(
specifier: string,
context: ResolveHookContext,
nextResolve: (specifier: string, context: ResolveHookContext) => Promise<ResolveFnOutput>,
): Promise<ResolveFnOutput> {
return nextResolve(mappings[specifier] ?? specifier, context)
}

View File

@@ -0,0 +1,137 @@
/**
* Shared start/dev runtime and project-local module resolution.
*
* @module @deepseek-ai/dsh-scripts/runtime
*/
import { register as registerHook } from 'node:module'
import { access, readFile, readdir } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import type { Context } from 'cordis'
import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import { parseSdkBootArgs } from './args.ts'
/** Options that distinguish dev boot from production boot. */
interface BootProjectOptions {
cwd?: string
dev?: boolean
argv?: readonly string[]
}
/** Startup context passed to a generated project's exported `main()`. */
export interface SdkBootContext {
/** Developer arguments forwarded after the launcher's `--` separator. */
readonly argv: readonly string[]
/** SDK-recognized structured arguments parsed from {@link argv}. */
readonly args: Record<string, string | boolean | undefined>
/** Absolute project working directory selected by the launcher. */
readonly cwd: string
/** Whether the launcher is running the built or TypeScript development entry. */
readonly mode: 'start' | 'dev'
}
async function localPluginMappings(cwd: string): Promise<Record<string, string>> {
const mappings: Record<string, string> = {}
let directories
try {
directories = await readdir(resolve(cwd, 'plugins'), { withFileTypes: true })
} catch (error) {
/* v8 ignore else -- the other arm requires a filesystem permission/IO fault from readdir */
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return mappings
/* v8 ignore next -- paired with the ignored defensive readdir-error arm above */
throw error
}
for (const directory of directories) {
if (!directory.isDirectory()) continue
const root = resolve(cwd, 'plugins', directory.name)
let manifest: { name?: unknown }
try {
manifest = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8')) as { name?: unknown }
await access(resolve(root, 'src/index.ts'))
} catch (error) {
throw new Error(`cannot load local plugin metadata from ${root}: ${String(error)}`)
}
if (typeof manifest.name !== 'string' || manifest.name.length === 0) {
throw new Error(`local plugin package has no name: ${root}`)
}
if (mappings[manifest.name]) throw new Error(`duplicate local plugin package name: ${manifest.name}`)
mappings[manifest.name] = pathToFileURL(resolve(root, 'src/index.ts')).href
}
return mappings
}
/** Register tsx and exact local-plugin source mappings for the current process. */
async function registerDevRuntime(cwd: string = process.cwd()): Promise<void> {
let registerTsx: typeof import('tsx/esm/api')['register']
try {
({ register: registerTsx } = await import('tsx/esm/api'))
} catch (error) {
/* v8 ignore next -- tsx is a declared project NPM dependency; missing-package behavior is defensive */
throw new Error(`dsh-sdk dev requires the project's tsx NPM dependency: ${String(error)}`)
}
registerTsx()
const mappings = await localPluginMappings(resolve(cwd))
const hook = new URL(
/* v8 ignore next -- the .js arm is exercised by the built-bin smoke rather than source coverage */
import.meta.url.endsWith('.ts')
? './local-plugin-loader-hooks.ts'
: './local-plugin-loader-hooks.js', import.meta.url)
registerHook(hook, { data: { mappings } })
}
/**
* Boot one cordis.yml after loading its sibling .env.
* @param source - file path or file URL to cordis.yml.
* @param options - working directory and development-runtime options.
* @returns live Cordis context.
*/
export async function startSDK(
source: string | URL = './cordis.yml',
options: BootProjectOptions = {},
): Promise<Context> {
const cwd = resolve(options.cwd ?? process.cwd())
if (options.dev) await registerDevRuntime(cwd)
if (source instanceof URL && source.protocol !== 'file:') {
throw new Error(`cordis.yml URL must use file:, got ${source.protocol}`)
}
const requested = source instanceof URL ? fileURLToPath(source) : source
const absolute = resolveConfigPath(requested, undefined, cwd)
loadEnv('dsh-sdk', dirname(absolute))
installFailLoud('dsh-sdk')
return boot('dsh-sdk', absolute)
}
/**
* Import and invoke a module target's main(), or directly boot cordis.yml.
* @param target - module path relative to the project, or absent for cordis.yml.
* @param options - working directory and development-runtime options.
* @returns target main result or live Cordis context.
*/
export async function runSDK(
target?: string,
options: BootProjectOptions = {},
): Promise<unknown> {
/* v8 ignore next -- the bin always supplies cwd; direct consumers normally accept process.cwd() */
const cwd = resolve(options.cwd ?? process.cwd())
if (options.dev) await registerDevRuntime(cwd)
if (!target) return startSDK('./cordis.yml', { cwd })
const absolute = resolve(cwd, target)
try {
await access(absolute)
} catch (error) {
const hint = options.dev ? '' : ' Run dsh-sdk build first if this is a TypeScript project.'
throw new Error(`cannot start missing target ${target}.${hint} ${String(error)}`)
}
const module = await import(pathToFileURL(absolute).href) as { main?: (context: SdkBootContext) => unknown }
if (typeof module.main !== 'function') {
throw new Error(`dsh-sdk target ${target} must export function main()`)
}
const argv = [...options.argv ?? []]
return module.main({
argv,
args: parseSdkBootArgs(argv),
cwd,
mode: options.dev ? 'dev' : 'start',
})
}

View File

@@ -0,0 +1,63 @@
/**
* Launcher-side telemetry wiring: resolve consent and send one fire-and-forget
* event around each dsh-sdk command. Best-effort — never affects the command's
* outcome or exit code.
*
* @module @deepseek-ai/dsh-scripts/telemetry
*/
import {
ConsentResolver,
TelemetryReporter,
buildTelemetryPayload,
type ConsentDecision,
} from '@deepseek-ai/dsh-telemetry'
/** One command's telemetry lifecycle facts. */
export interface CommandTelemetryEvent {
/** The dsh-sdk command that ran. */
command: string
/** Project directory whose consent, `cordis.yml`, and `package.json` are read. */
cwd: string
/** Wall-clock duration in milliseconds. */
durationMs: number
/** Whether the command completed without error. */
success: boolean
}
/** Injectable consent and delivery seams for tests. */
export interface CommandTelemetryDeps {
resolve?: (cwd: string) => Promise<ConsentDecision>
reporter?: Pick<TelemetryReporter, 'report' | 'flush'>
}
/**
* Resolve consent for the project and, when allowed, assemble and send one
* telemetry event, draining in-flight sends before returning. Swallows every
* error so telemetry can never change a command's result.
* @param event - the command lifecycle facts.
* @param deps - consent and delivery seams; defaults hit the real endpoint.
*/
export async function reportCommandTelemetry(
event: CommandTelemetryEvent,
deps: CommandTelemetryDeps = {},
): Promise<void> {
try {
/* v8 ignore next -- the production ConsentResolver is exercised by the built-bin smoke */
const resolve = deps.resolve ?? (cwd => new ConsentResolver().resolve(cwd))
const consent = await resolve(event.cwd)
if (!consent.allowed) return
const payload = await buildTelemetryPayload({
command: event.command,
durationMs: event.durationMs,
success: event.success,
projectDir: event.cwd,
})
/* v8 ignore next -- the production TelemetryReporter is exercised by the built-bin smoke */
const reporter = deps.reporter ?? new TelemetryReporter()
reporter.report(payload, consent)
await reporter.flush()
} catch {
// Telemetry is best-effort; a consent, payload, or delivery fault never reaches the command.
}
}

View File

@@ -0,0 +1,2 @@
Changes were committed, but install failed: {{error}}
Retry: {{packageManager}} {{installArgs}}

View File

@@ -0,0 +1,8 @@
Usage: dsh-sdk <command> [options]
Commands:
start [target] [-- args...] Import a built module, or boot cordis.yml
dev [target] [-- args...] Start with TypeScript and local-plugin source resolution
build [args...] Run the project's installed tsdown
config Interactively edit project features
create <source> Add an external plugin dependency (pkg@version or github:owner/repo#ref) and mount it in cordis.yml

View File

@@ -0,0 +1,21 @@
/**
* Package-owned terminal templates for the dsh-sdk launcher.
*
* @module @deepseek-ai/dsh-scripts/templates/dsh-sdk-templates
*/
import { TextTemplate, type PackageManagerName } from '@deepseek-ai/dsh-helper'
interface ConfigInstallFailureTemplateModel {
error: string
packageManager: PackageManagerName
installArgs: string
}
/** Compiled dsh-sdk terminal templates. */
export const DSH_SDK_TEMPLATES = {
usage: TextTemplate.fromFile<Record<string, never>>(new URL('./assets/usage.txt.tpl', import.meta.url)),
configInstallFailure: TextTemplate.fromFile<ConfigInstallFailureTemplateModel>(
new URL('./assets/config-install-failure.txt.tpl', import.meta.url),
),
} as const

View File

@@ -0,0 +1,288 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`dsh-sdk config terminal contract > pins the feature tree and Review & Apply output 1`] = `
{
"committed": {
"addedFeatures": [
"todo",
],
"addedPlugins": [],
"changedFiles": [
"cordis.yml",
"package.json",
],
"configuredFeatures": [],
"disabledFeatures": [],
"disabledPlugins": [],
"enabledFeatures": [],
"enabledPlugins": [],
"npmDependenciesChanged": true,
},
"installs": 1,
"review": "Install feature: todo
Change file: cordis.yml
Change file: package.json
",
"transcript": [
{
"kind": "nested-multiselect",
"message": "Configure the project",
"options": [
{
"choiceMode": "exclusive",
"choices": [
{
"default": true,
"label": "DeepSeek",
"value": "deepseek-official",
},
{
"default": false,
"label": "Custom endpoint (pi-ai)",
"value": "custom",
},
],
"default": true,
"disabled": false,
"label": "Model provider",
"required": true,
"value": "feature:provider",
"warning": undefined,
},
{
"choiceMode": undefined,
"choices": undefined,
"default": true,
"disabled": false,
"label": "Agent runtime spine",
"required": true,
"value": "feature:spine",
"warning": undefined,
},
{
"choiceMode": "exclusive",
"choices": [
{
"default": true,
"label": "Local executor",
"value": "local",
},
{
"default": false,
"label": "Sandboxed executor",
"value": "sandbox",
},
],
"default": true,
"disabled": false,
"label": "Command execution",
"required": true,
"value": "feature:bash",
"warning": undefined,
},
{
"choiceMode": "exclusive",
"choices": [
{
"default": true,
"label": "ACP automation server",
"value": "acp",
},
{
"default": false,
"label": "Embedded context",
"value": "embed",
},
],
"default": true,
"disabled": false,
"label": "Run interface",
"required": true,
"value": "feature:app",
"warning": undefined,
},
{
"choiceMode": "exclusive",
"choices": [
{
"default": true,
"label": "JSONL files",
"value": "jsonl",
},
{
"default": false,
"label": "SQLite database",
"value": "sqlite",
},
],
"default": true,
"disabled": false,
"label": "Durable session storage",
"required": true,
"value": "feature:persistence",
"warning": undefined,
},
{
"choiceMode": undefined,
"choices": undefined,
"default": false,
"disabled": false,
"label": "Hot-module reload",
"required": false,
"value": "feature:hmr",
"warning": undefined,
},
{
"choiceMode": undefined,
"choices": undefined,
"default": false,
"disabled": false,
"label": "Read, write, and edit local files",
"required": false,
"value": "feature:fs",
"warning": undefined,
},
{
"choiceMode": undefined,
"choices": undefined,
"default": false,
"disabled": false,
"label": "Model-facing task tracking",
"required": false,
"value": "feature:todo",
"warning": undefined,
},
{
"choiceMode": undefined,
"choices": undefined,
"default": false,
"disabled": false,
"label": "Local skill discovery",
"required": false,
"value": "feature:skill",
"warning": undefined,
},
{
"choiceMode": "exclusive",
"choices": [
{
"default": true,
"label": "DeepSeek search",
"value": "deepseek-official",
},
{
"default": false,
"label": "Exa search",
"value": "exa",
},
{
"default": false,
"label": "Perplexity search",
"value": "perplexity",
},
{
"default": false,
"label": "Fetch only",
"value": "fetch-only",
},
],
"default": false,
"disabled": false,
"label": "Web search and fetch tools",
"required": false,
"value": "feature:web",
"warning": undefined,
},
{
"choiceMode": "multiple",
"choices": [
{
"default": true,
"label": "Fresh child agent",
"value": "spawn",
},
{
"default": false,
"label": "Fork parent history",
"value": "fork",
},
],
"default": false,
"disabled": false,
"label": "Delegate work to child agents",
"required": false,
"value": "feature:subagent",
"warning": undefined,
},
{
"choiceMode": undefined,
"choices": undefined,
"default": false,
"disabled": false,
"label": "Scripted multi-agent workflows",
"required": false,
"value": "feature:workflow",
"warning": undefined,
},
{
"choiceMode": undefined,
"choices": undefined,
"default": false,
"disabled": false,
"label": "Automatic context compaction",
"required": false,
"value": "feature:compact",
"warning": undefined,
},
{
"choiceMode": "multiple",
"choices": [
{
"default": true,
"label": "Claude Code hooks",
"value": "claude",
},
{
"default": false,
"label": "Codex hooks",
"value": "codex",
},
],
"default": false,
"disabled": false,
"label": "Run Claude Code or Codex hooks",
"required": false,
"value": "feature:hooks",
"warning": undefined,
},
{
"choiceMode": undefined,
"choices": undefined,
"default": false,
"disabled": false,
"label": "Loop-hygiene reminders",
"required": false,
"value": "feature:guard",
"warning": undefined,
},
{
"choiceMode": undefined,
"choices": undefined,
"default": false,
"disabled": false,
"label": "Tool timeout policy",
"required": false,
"value": "feature:timeout-policy",
"warning": undefined,
},
],
"showChanges": true,
},
{
"initialValue": true,
"kind": "confirm",
"message": "Apply these changes?",
},
],
}
`;

View File

@@ -0,0 +1,128 @@
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Writable } from 'node:stream'
import { afterEach, describe, expect, it } from 'vitest'
import {
NpmPackageManager,
SdkProject,
featureId,
createBuiltinRegistry,
type NestedMultiSelectValue,
type PromptPort,
} from '@deepseek-ai/dsh-helper'
import type {
ConfirmPromptRequest,
MultiSelectPromptRequest,
NestedMultiSelectRequest,
PromptOutcome,
SecretPromptRequest,
SelectPromptRequest,
TextPromptRequest,
} from '../../helper/src/questions/prompt-port.ts'
import { ConfigWorkflow } from '../src/config/config-workflow.ts'
class RecordingPort implements PromptPort {
readonly transcript: unknown[] = []
readonly #answers: unknown[]
constructor(answers: unknown[]) { this.#answers = [...answers] }
answer<T>(record: unknown): Promise<PromptOutcome<T>> {
this.transcript.push(record)
return Promise.resolve({ status: 'answered', value: this.#answers.shift() as T })
}
text(request: TextPromptRequest): Promise<PromptOutcome<string>> {
return this.answer({ kind: 'text', message: request.message })
}
secret(request: SecretPromptRequest): Promise<PromptOutcome<string>> {
return this.answer({ kind: 'secret', message: request.message })
}
select<T>(request: SelectPromptRequest<T>): Promise<PromptOutcome<T>> {
return this.answer({
kind: 'select', message: request.message,
options: request.options.map(option => ({ value: option.value, label: option.label })),
})
}
multiselect<T>(request: MultiSelectPromptRequest<T>): Promise<PromptOutcome<readonly T[]>> {
return this.answer({ kind: 'multiselect', message: request.message })
}
confirm(request: ConfirmPromptRequest): Promise<PromptOutcome<boolean>> {
return this.answer({ kind: 'confirm', message: request.message, initialValue: request.initialValue })
}
nestedMultiselect<TValue, TChoice>(
request: NestedMultiSelectRequest<TValue, TChoice>,
): Promise<PromptOutcome<readonly NestedMultiSelectValue<TValue, TChoice>[]>> {
return this.answer({
kind: 'nested-multiselect',
message: request.message,
showChanges: request.showChanges,
options: request.options.map(option => ({
value: option.value,
label: option.label,
required: option.required,
default: option.default,
disabled: option.disabled,
warning: option.warning,
choiceMode: option.choiceMode,
choices: option.choices?.map(choice => ({
value: choice.value,
label: choice.label,
default: choice.default,
})),
})),
})
}
}
const temporary: string[] = []
afterEach(async () => {
await Promise.all(temporary.splice(0).map(path => rm(path, { recursive: true, force: true })))
})
async function baseProject(): Promise<SdkProject> {
const root = await mkdtemp(join(tmpdir(), 'dsh-config-snapshot-'))
temporary.push(root)
const request = {
name: 'snapshot-agent',
description: 'snapshot',
runtime: { model: 'deepseek-v4-flash' },
packageManager: new NpmPackageManager('10.0.0'),
releaseVersion: '0.0.1',
features: [
{ id: featureId('provider'), options: ['deepseek-official'], secrets: { apiKey: 'key' } },
{ id: featureId('bash'), options: ['local'] },
{ id: featureId('app'), options: ['acp'] },
{ id: featureId('persistence'), options: ['jsonl'] },
],
localPlugins: [],
}
const project = SdkProject.create(root, request)
const registry = createBuiltinRegistry(project.profile)
const edit = project.edit(registry)
for (const item of request.features) edit.installFeature(registry.get(item.id), item)
return (await edit.commit()).project
}
describe('dsh-sdk config terminal contract', () => {
it('pins the feature tree and Review & Apply output', async () => {
const project = await baseProject()
const registry = createBuiltinRegistry(project.profile)
const port = new RecordingPort([
[{ value: 'feature:todo', choices: [] }],
true,
])
let output = ''
const stream = new Writable({ write(chunk, _encoding, callback) { output += String(chunk); callback() } })
let installs = 0
const result = await new ConfigWorkflow(port, stream, async () => { installs += 1 }).run(project, registry)
expect({
transcript: port.transcript,
review: output,
installs,
committed: result.commit?.changes,
}).toMatchSnapshot()
})
})

View File

@@ -0,0 +1,650 @@
import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { PassThrough, Writable } from 'node:stream'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
import {
HeadlessPromptPort,
LocalPluginBlueprint,
NpmPackageManager,
SdkProject,
featureId,
createBuiltinRegistry,
type CommandRunner,
type NestedMultiSelectValue,
type ProjectCreationRequest,
type PromptPort,
} from '@deepseek-ai/dsh-helper'
import type {
ConfirmPromptRequest,
MultiSelectPromptRequest,
NestedMultiSelectRequest,
PromptOutcome,
SecretPromptRequest,
SelectPromptRequest,
TextPromptRequest,
} from '../../helper/src/questions/prompt-port.ts'
import { runSDK, startSDK } from '@deepseek-ai/dsh-scripts'
import { parseDshSdkArgs, parseSdkBootArgs } from '../src/args.ts'
import { PluginBuild, ProjectBuild, runProjectBuild } from '../src/build.ts'
import { runDshSdkCommand, type DshSdkCommandContext } from '../src/command.ts'
import { runConfigCommand } from '../src/config.ts'
import { ConfigWorkflow, type ConfigPlan } from '../src/config/config-workflow.ts'
import { runCreatePluginCommand } from '../src/create-plugin.ts'
import { reportCommandTelemetry, type CommandTelemetryEvent } from '../src/telemetry.ts'
import { initialize, resolve as resolveLocalPlugin } from '../src/local-plugin-loader-hooks.ts'
const temporary: string[] = []
afterEach(async () => {
await Promise.all(temporary.splice(0).map(path => rm(path, { recursive: true, force: true })))
})
class QueuePort implements PromptPort {
readonly #answers: unknown[]
constructor(answers: unknown[]) { this.#answers = [...answers] }
next<T>(): Promise<PromptOutcome<T>> {
return Promise.resolve({ status: 'answered', value: this.#answers.shift() as T })
}
text(_request: TextPromptRequest): Promise<PromptOutcome<string>> { return this.next() }
secret(_request: SecretPromptRequest): Promise<PromptOutcome<string>> { return this.next() }
select<T>(_request: SelectPromptRequest<T>): Promise<PromptOutcome<T>> { return this.next() }
multiselect<T>(_request: MultiSelectPromptRequest<T>): Promise<PromptOutcome<readonly T[]>> { return this.next() }
confirm(_request: ConfirmPromptRequest): Promise<PromptOutcome<boolean>> { return this.next() }
nestedMultiselect<TValue, TChoice>(
_request: NestedMultiSelectRequest<TValue, TChoice>,
): Promise<PromptOutcome<readonly NestedMultiSelectValue<TValue, TChoice>[]>> { return this.next() }
}
function outputBuffer(): { stream: Writable; read: () => string } {
let text = ''
return {
stream: new Writable({ write(chunk, _encoding, callback) { text += String(chunk); callback() } }),
read: () => text,
}
}
function commandContext(cwd: string): DshSdkCommandContext & { readStdout: () => string; readStderr: () => string } {
let stdout = ''
let stderr = ''
const stdin = Object.assign(new PassThrough(), { isTTY: true }) as unknown as NodeJS.ReadStream
const output = Object.assign(new Writable({
write(chunk, _encoding, callback) { stdout += String(chunk); callback() },
}), { isTTY: true }) as unknown as NodeJS.WriteStream
const error = new Writable({
write(chunk, _encoding, callback) { stderr += String(chunk); callback() },
}) as unknown as NodeJS.WriteStream
return {
cwd, stdin, stdout: output, stderr: error,
readStdout: () => stdout,
readStderr: () => stderr,
}
}
function creation(
extra: ProjectCreationRequest['features'] = [],
localPlugins: readonly LocalPluginBlueprint[] = [],
app: 'acp' | 'embed' = 'embed',
): ProjectCreationRequest {
return {
name: 'config-agent',
description: 'config test',
runtime: { model: 'deepseek-v4-flash' },
packageManager: new NpmPackageManager('10.0.0'),
releaseVersion: '0.0.1',
features: [
{ id: featureId('provider'), options: ['deepseek-official'], secrets: { apiKey: 'key' } },
{ id: featureId('bash'), options: ['local'] },
{ id: featureId('app'), options: [app] },
{ id: featureId('persistence'), options: ['jsonl'] },
...extra,
],
localPlugins,
}
}
async function committedProject(
extra: ProjectCreationRequest['features'] = [],
localPlugins: readonly LocalPluginBlueprint[] = [],
app: 'acp' | 'embed' = 'embed',
): Promise<SdkProject> {
const root = await mkdtemp(join(tmpdir(), 'dsh-config-workflow-'))
temporary.push(root)
const request = creation(extra, localPlugins, app)
const project = SdkProject.create(root, request)
const registry = createBuiltinRegistry(project.profile)
const edit = project.edit(registry)
for (const item of request.features) edit.installFeature(registry.get(item.id), item)
for (const plugin of localPlugins) edit.addPlugin(plugin)
return (await edit.commit()).project
}
describe('Commander launcher arguments', () => {
it('parses real subcommands and forwards arbitrary build options', () => {
expect(parseDshSdkArgs([])).toMatchObject({ help: true })
expect(parseDshSdkArgs(['start', 'index.js'])).toMatchObject({ command: 'start', target: 'index.js' })
expect(parseDshSdkArgs(['dev'])).toEqual({ command: 'dev', forwarded: [], help: false })
expect(parseDshSdkArgs(['build', '--watch', '--minify'])).toMatchObject({
command: 'build', forwarded: ['--watch', '--minify'],
})
expect(parseDshSdkArgs(['start', 'index.js', '--', '--resume', 'session-1'])).toMatchObject({
command: 'start', target: 'index.js', forwarded: ['--resume', 'session-1'],
})
expect(parseDshSdkArgs(['config'])).toMatchObject({ command: 'config' })
expect(parseDshSdkArgs(['start'])).toEqual({ command: 'start', forwarded: [], help: false })
expect(parseDshSdkArgs(['dev', 'index.ts'])).toMatchObject({ command: 'dev', target: 'index.ts' })
expect(parseDshSdkArgs(['-h'])).toMatchObject({ help: true })
expect(parseDshSdkArgs(['--help'])).toMatchObject({ help: true })
expect(() => parseDshSdkArgs(['unknown'])).toThrow()
expect(() => parseDshSdkArgs(['config', 'extra'])).toThrow()
expect(() => parseDshSdkArgs(['config', '--', 'extra'])).toThrow('does not accept forwarded')
expect(parseSdkBootArgs([
'--model=mock', '--resume=session-1', '--custom=value', '--verbose', '--no-cache', '--max-depth=-1',
])).toEqual({
model: 'mock', resume: 'session-1', custom: 'value', verbose: true, cache: false, 'max-depth': '-1',
})
})
it('dispatches every command and maps failures to exit codes', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-command-'))
temporary.push(root)
const context = commandContext(root)
const calls: unknown[] = []
context.run = async (target, options) => { calls.push(['run', target, options]); return undefined }
context.build = async (args, cwd) => { calls.push(['build', args, cwd]) }
context.config = async () => { calls.push(['config']); return {} }
await expect(runDshSdkCommand(['start', 'index.js', '--', '--resume', 'session-1'], context)).resolves.toBe(0)
await expect(runDshSdkCommand(['dev', 'index.ts'], context)).resolves.toBe(0)
await expect(runDshSdkCommand(['build', '--watch'], context)).resolves.toBe(0)
await expect(runDshSdkCommand(['config'], context)).resolves.toBe(0)
expect(calls).toHaveLength(4)
expect(calls[0]).toEqual(['run', 'index.js', { cwd: root, argv: ['--resume', 'session-1'] }])
expect(calls[1]).toEqual(['run', 'index.ts', { cwd: root, dev: true, argv: [] }])
context.config = async () => ({ installError: new Error('offline') })
await expect(runDshSdkCommand(['config'], context)).resolves.toBe(1)
context.config = async () => { throw 'broken' }
await expect(runDshSdkCommand(['config'], context)).resolves.toBe(1)
expect(context.readStderr()).toContain('broken')
await expect(runDshSdkCommand(['unknown'], context)).resolves.toBe(1)
await expect(runDshSdkCommand([], context)).resolves.toBe(0)
expect(context.readStdout()).toContain('Usage: dsh-sdk')
expect(context.readStdout()).toContain('create <source>')
const defaults = commandContext(root)
await writeFile(join(root, 'main.mjs'), 'export function main() { return "ok" }\n')
await expect(runDshSdkCommand(['start', 'main.mjs'], defaults)).resolves.toBe(0)
await expect(runDshSdkCommand(['build'], defaults)).resolves.toBe(0)
defaults.port = new QueuePort([[]])
await expect(runDshSdkCommand(['config'], defaults)).resolves.toBe(1)
})
})
describe('build profiles and invocation', () => {
it('discovers root and plugin targets and creates independent profiles', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-build-profile-'))
temporary.push(root)
await mkdir(join(root, 'plugins', 'one', 'src'), { recursive: true })
await writeFile(join(root, 'index.ts'), 'export {}\n')
await writeFile(join(root, 'plugins', 'one', 'package.json'), '{"name":"one"}\n')
await writeFile(join(root, 'plugins', 'one', 'src', 'index.ts'), 'export {}\n')
expect(ProjectBuild({ cwd: root, entry: ['index.ts'] })).toEqual([
{ cwd: root, entry: ['index.ts'] },
{ workspace: { include: ['plugins/*'] } },
])
expect(PluginBuild({ entry: ['src/index.ts'], dts: true })).toEqual({ entry: ['src/index.ts'], dts: true })
expect(() => ProjectBuild({ workspace: true })).toThrow('owns workspace discovery')
expect(() => PluginBuild({ workspace: true })).toThrow('does not accept nested workspace')
expect(ProjectBuild({ cwd: join(root, 'empty'), entry: ['index.ts'] })).toEqual([
{ cwd: join(root, 'empty'), entry: ['index.ts'] },
])
expect(ProjectBuild({ entry: ['index.ts'] })[0]).toMatchObject({ entry: ['index.ts'] })
})
it('runs the project-installed tsdown and reports child failure', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-build-run-'))
temporary.push(root)
await writeFile(join(root, 'package.json'), '{"type":"module"}\n')
await writeFile(join(root, 'index.ts'), 'export {}\n')
await writeFile(join(root, 'tsdown.config.ts'), 'export default {}\n')
await mkdir(join(root, 'node_modules'), { recursive: true })
const manifest = fileURLToPath(import.meta.resolve('tsdown/package.json'))
await symlink(dirname(manifest), join(root, 'node_modules', 'tsdown'))
const calls: string[][] = []
const runner: CommandRunner = {
run: async (command, args) => {
calls.push([command, ...args])
return { exitCode: 0, signal: null }
},
}
await runProjectBuild(['--watch'], root, runner)
expect(calls[0]?.[0]).toBe(process.execPath)
expect(calls[0]?.at(-1)).toBe('--watch')
const failed: CommandRunner = { run: async () => ({ exitCode: 2, signal: null }) }
await expect(runProjectBuild([], root, failed)).rejects.toThrow('exited with code 2')
const killed: CommandRunner = { run: async () => ({ exitCode: null, signal: 'SIGTERM' }) }
await expect(runProjectBuild([], root, killed)).rejects.toThrow('killed by SIGTERM')
})
it('recognizes every tsdown config source', async () => {
const manifest = fileURLToPath(import.meta.resolve('tsdown/package.json'))
for (const extension of ['cts', 'cjs', 'json']) {
const root = await mkdtemp(join(tmpdir(), `dsh-build-${extension}-`))
temporary.push(root)
await writeFile(join(root, 'package.json'), '{"type":"module"}\n')
await writeFile(join(root, `tsdown.config.${extension}`), '{}\n')
await mkdir(join(root, 'node_modules'), { recursive: true })
await symlink(dirname(manifest), join(root, 'node_modules', 'tsdown'))
let called = false
await runProjectBuild([], root, {
run: async () => { called = true; return { exitCode: 0, signal: null } },
})
expect(called).toBe(true)
}
const root = await mkdtemp(join(tmpdir(), 'dsh-build-package-json-'))
temporary.push(root)
await writeFile(join(root, 'package.json'), '{"type":"module","tsdown":{}}\n')
await mkdir(join(root, 'node_modules'), { recursive: true })
await symlink(dirname(manifest), join(root, 'node_modules', 'tsdown'))
let called = false
await runProjectBuild([], root, {
run: async () => { called = true; return { exitCode: 0, signal: null } },
})
expect(called).toBe(true)
})
it('reports missing and malformed project tsdown executables', async () => {
const missing = await mkdtemp(join(tmpdir(), 'dsh-build-missing-'))
temporary.push(missing)
await writeFile(join(missing, 'package.json'), '{"type":"module"}')
await writeFile(join(missing, 'tsdown.config.ts'), 'export default {}\n')
await expect(runProjectBuild([], missing)).rejects.toThrow('requires tsdown')
const malformed = await mkdtemp(join(tmpdir(), 'dsh-build-malformed-'))
temporary.push(malformed)
await writeFile(join(malformed, 'package.json'), '{"type":"module"}')
await writeFile(join(malformed, 'tsdown.config.ts'), 'export default {}\n')
await mkdir(join(malformed, 'node_modules', 'tsdown'), { recursive: true })
await writeFile(join(malformed, 'node_modules', 'tsdown', 'package.json'), JSON.stringify({
name: 'tsdown', version: '0.0.0', exports: { './package.json': './package.json' }, bin: {},
}))
await expect(runProjectBuild([], malformed)).rejects.toThrow('has no executable')
await writeFile(join(malformed, 'node_modules', 'tsdown', 'package.json'), JSON.stringify({
name: 'tsdown', version: '0.0.0', exports: { './package.json': './package.json' },
}))
await expect(runProjectBuild([], malformed)).rejects.toThrow('has no executable')
const stringBin = await mkdtemp(join(tmpdir(), 'dsh-build-string-bin-'))
temporary.push(stringBin)
await writeFile(join(stringBin, 'package.json'), '{"type":"module"}')
await writeFile(join(stringBin, 'tsdown.config.js'), 'export default {}\n')
await mkdir(join(stringBin, 'node_modules', 'tsdown'), { recursive: true })
await writeFile(join(stringBin, 'node_modules', 'tsdown', 'package.json'), JSON.stringify({
name: 'tsdown', version: '0.0.0', exports: { './package.json': './package.json' }, bin: 'cli.js',
}))
await writeFile(join(stringBin, 'node_modules', 'tsdown', 'cli.js'), '')
let command = ''
await runProjectBuild([], stringBin, {
run: async (_node, args) => { command = args[0] ?? ''; return { exitCode: 0, signal: null } },
})
expect(command).toContain('cli.js')
})
it('returns a no-op for a project with no build targets and hints on a missing start target', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-no-build-'))
temporary.push(root)
let called = false
await runProjectBuild([], root, { run: async () => { called = true; return { exitCode: 0, signal: null } } })
expect(called).toBe(false)
const unreadableManifest = await mkdtemp(join(tmpdir(), 'dsh-build-unreadable-manifest-'))
temporary.push(unreadableManifest)
await mkdir(join(unreadableManifest, 'package.json'))
await expect(runProjectBuild([], unreadableManifest)).rejects.toThrow()
await expect(runSDK('index.js', { cwd: root })).rejects.toThrow('Run dsh-sdk build first')
})
it('invokes the target module main export and rejects passive modules', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-module-main-'))
temporary.push(root)
await writeFile(join(root, 'main.mjs'), 'export function main(context) { return context }\n')
await writeFile(join(root, 'passive.mjs'), 'export const value = 1\n')
await expect(runSDK('main.mjs', {
cwd: root,
argv: ['--model=mock', '--resume=session-1', 'custom'],
})).resolves.toEqual({
argv: ['--model=mock', '--resume=session-1', 'custom'],
args: { model: 'mock', resume: 'session-1' }, cwd: root, mode: 'start',
})
await expect(runSDK('passive.mjs', { cwd: root })).rejects.toThrow('must export function main()')
})
it('boots empty Cordis configs and delegates targetless runs', async () => {
expectTypeOf(runSDK).toBeCallableWith()
const root = await mkdtemp(join(tmpdir(), 'dsh-start-sdk-'))
temporary.push(root)
await writeFile(join(root, 'cordis.yml'), '[]\n')
const byUrl = await startSDK(pathToFileURL(join(root, 'cordis.yml')))
await byUrl.fiber.dispose()
const byRun = await runSDK(undefined, { cwd: root }) as import('cordis').Context
await byRun.fiber.dispose()
const dev = await startSDK('./cordis.yml', { cwd: root, dev: true })
await dev.fiber.dispose()
await expect(startSDK(new URL('https://example.invalid/cordis.yml'), { cwd: root })).rejects.toThrow()
})
it('validates local plugin metadata in dev mode', async () => {
const malformed = await mkdtemp(join(tmpdir(), 'dsh-dev-malformed-'))
temporary.push(malformed)
await mkdir(join(malformed, 'plugins', 'bad'), { recursive: true })
await expect(runSDK('missing.ts', { cwd: malformed, dev: true })).rejects.toThrow('cannot load local plugin metadata')
const absent = await mkdtemp(join(tmpdir(), 'dsh-dev-absent-'))
temporary.push(absent)
await expect(runSDK('missing.ts', { cwd: absent, dev: true })).rejects.toThrow('cannot start missing target')
const unnamed = await mkdtemp(join(tmpdir(), 'dsh-dev-unnamed-'))
temporary.push(unnamed)
await mkdir(join(unnamed, 'plugins', 'bad', 'src'), { recursive: true })
await writeFile(join(unnamed, 'plugins', 'bad', 'package.json'), '{}')
await writeFile(join(unnamed, 'plugins', 'bad', 'src/index.ts'), 'export {}\n')
await expect(runSDK('missing.ts', { cwd: unnamed, dev: true })).rejects.toThrow('has no name')
const duplicate = await mkdtemp(join(tmpdir(), 'dsh-dev-duplicate-'))
temporary.push(duplicate)
for (const name of ['one', 'two']) {
await mkdir(join(duplicate, 'plugins', name, 'src'), { recursive: true })
await writeFile(join(duplicate, 'plugins', name, 'package.json'), '{"name":"same"}')
await writeFile(join(duplicate, 'plugins', name, 'src/index.ts'), 'export {}\n')
}
await expect(runSDK('missing.ts', { cwd: duplicate, dev: true })).rejects.toThrow('duplicate local plugin')
const valid = await mkdtemp(join(tmpdir(), 'dsh-dev-valid-'))
temporary.push(valid)
await mkdir(join(valid, 'plugins', 'one', 'src'), { recursive: true })
await writeFile(join(valid, 'plugins', 'README.md'), 'skip\n')
await writeFile(join(valid, 'plugins', 'one', 'package.json'), '{"name":"local"}')
await writeFile(join(valid, 'plugins', 'one', 'src/index.ts'), 'export {}\n')
await writeFile(join(valid, 'main.ts'), 'export function main() { return "dev" }\n')
await expect(runSDK('main.ts', { cwd: valid, dev: true })).resolves.toBe('dev')
await expect(runSDK('missing.ts', { cwd: valid, dev: true })).rejects.toThrow('cannot start missing target')
})
it('maps only exact local package names through the loader hook', async () => {
initialize({ mappings: { local: 'file:///tmp/local.ts' } })
const next = async (specifier: string) => ({ url: specifier, format: 'module' as const })
const context: import('node:module').ResolveHookContext = {
conditions: [], importAttributes: {}, parentURL: undefined,
}
await expect(resolveLocalPlugin('local', context, next)).resolves.toMatchObject({ url: 'file:///tmp/local.ts' })
await expect(resolveLocalPlugin('other', context, next)).resolves.toMatchObject({ url: 'other' })
})
})
describe('ConfigWorkflow', () => {
it('opens a project through the config command prompt seam', async () => {
const project = await committedProject()
const context = commandContext(project.root)
context.port = new QueuePort([[]])
context.install = async () => { throw new Error('install should not run') }
await expect(runConfigCommand(context)).resolves.toEqual({})
delete context.port
delete context.install
context.stdin.isTTY = false
await expect(runConfigCommand(context)).rejects.toThrow('interactive TTY')
context.stdin.isTTY = true
context.stdout.isTTY = false
await expect(runConfigCommand(context)).rejects.toThrow('interactive TTY')
})
it('accumulates a disable and commits only after Review & Apply', async () => {
const project = await committedProject([{ id: featureId('todo'), options: ['default'] }])
const registry = createBuiltinRegistry(project.profile)
const output = outputBuffer()
const workflow = new ConfigWorkflow(new QueuePort([
[], true,
]), output.stream, async () => { throw new Error('install should not run') })
const result = await workflow.run(project, registry)
expect(result.commit?.project.cordis.entry('tool-todo')?.disabled).toBe(true)
expect(output.read()).toContain('Disable feature: todo')
})
it('reconciles a headless plan without prompting and preserves custom plugins', async () => {
const project = await committedProject([], [new LocalPluginBlueprint('plugin', 'plugin')])
const registry = createBuiltinRegistry(project.profile)
const output = outputBuffer()
let installs = 0
const plan: ConfigPlan = {
features: [
{ id: featureId('bash'), options: ['local'] },
{ id: featureId('persistence'), options: ['jsonl'] },
{ id: featureId('todo'), options: ['default'] },
{ id: featureId('web'), options: ['exa'], secrets: { apiKey: 'exa-key' } },
],
}
const result = await new ConfigWorkflow(
new HeadlessPromptPort(), output.stream, async () => { installs += 1 },
).run(project, registry, plan)
expect(result.commit?.project.cordis.entry('tool-todo')).toBeDefined()
// the unlisted custom local plugin keeps its enabled state (not nuked by the plan)
expect(result.commit?.project.cordis.entry('plugin')?.disabled).toBeFalsy()
expect(installs).toBe(1)
})
it('installs once after NPM dependency changes and keeps committed files on install failure', async () => {
const project = await committedProject()
const registry = createBuiltinRegistry(project.profile)
const output = outputBuffer()
let installs = 0
const workflow = new ConfigWorkflow(new QueuePort([
[{ value: 'feature:todo', choices: [] }], true,
]), output.stream, async () => {
installs += 1
throw new Error('offline')
})
const result = await workflow.run(project, registry)
expect(installs).toBe(1)
expect(result.installError?.message).toBe('offline')
expect(result.commit?.project.cordis.entry('tool-todo')).toBeDefined()
expect(output.read()).toContain('Changes were committed, but install failed')
})
it('cancels apply and enables a disabled feature without reinstalling', async () => {
const project = await committedProject([{ id: featureId('todo'), options: ['default'] }])
const registry = createBuiltinRegistry(project.profile)
const cancelled = await new ConfigWorkflow(new QueuePort([[], false]), outputBuffer().stream).run(project, registry)
expect(cancelled).toEqual({})
const disable = project.edit(registry)
disable.disableFeature(registry.get(featureId('todo')))
const disabled = (await disable.commit()).project
let installs = 0
const enabled = await new ConfigWorkflow(new QueuePort([
[{ value: 'feature:todo', choices: [] }], true,
]), outputBuffer().stream, async () => { installs += 1 }).run(disabled, createBuiltinRegistry(disabled.profile))
expect(enabled.commit?.project.cordis.entry('tool-todo')?.disabled).toBeUndefined()
expect(installs).toBe(0)
})
it('toggles custom Cordis config entries without changing NPM dependencies', async () => {
const project = await committedProject([], [new LocalPluginBlueprint('sample', 'plugin')])
await expect(new ConfigWorkflow(new QueuePort([
[{ value: 'plugin:sample', choices: [] }],
]), outputBuffer().stream).run(project, createBuiltinRegistry(project.profile))).resolves.toEqual({})
const output = outputBuffer()
const disabled = await new ConfigWorkflow(new QueuePort([[], true]), output.stream).run(
project, createBuiltinRegistry(project.profile),
)
expect(disabled.commit?.project.cordis.entry('sample')?.disabled).toBe(true)
expect(output.read()).toContain('Disable custom plugin: sample')
const next = disabled.commit?.project
if (!next) throw new Error('custom toggle did not commit')
const enabled = await new ConfigWorkflow(new QueuePort([
[{ value: 'plugin:sample', choices: [] }], true,
]), outputBuffer().stream).run(next, createBuiltinRegistry(next.profile))
expect(enabled.commit?.project.cordis.entry('sample')?.disabled).toBeUndefined()
})
it('shows inconsistent features as diagnostic-only rows', async () => {
const complete = await committedProject()
await writeFile(join(complete.root, 'cordis.yml'), `${await readFile(join(complete.root, 'cordis.yml'), 'utf8')}- id: web-search-exa
name: '@deepseek-ai/dsh-web-search-exa'
`)
const project = await SdkProject.open(complete.root)
const port = new QueuePort([[]])
await expect(new ConfigWorkflow(port, outputBuffer().stream).run(project, createBuiltinRegistry(project.profile)))
.resolves.toEqual({})
})
it('uses the default installer and normalizes non-Error install failures', async () => {
const project = await committedProject()
const install = vi.spyOn(NpmPackageManager.prototype, 'install').mockResolvedValue()
await new ConfigWorkflow(new QueuePort([
[{ value: 'feature:todo', choices: [] }], true,
])).run(project, createBuiltinRegistry(project.profile))
expect(install).toHaveBeenCalledOnce()
install.mockRestore()
const next = await committedProject()
const failed = await new ConfigWorkflow(new QueuePort([
[{ value: 'feature:todo', choices: [] }], true,
]), outputBuffer().stream, async () => { throw 'offline-string' }).run(next, createBuiltinRegistry(next.profile))
expect(failed.installError?.message).toBe('offline-string')
})
it('reconciles a child option selected in the feature tree', async () => {
const project = await committedProject()
const registry = createBuiltinRegistry(project.profile)
let installs = 0
const workflow = new ConfigWorkflow(new QueuePort([
[{ value: 'feature:persistence', choices: ['sqlite'] }], true,
]), outputBuffer().stream, async () => { installs += 1 })
const result = await workflow.run(project, registry)
expect(result.commit?.project.cordis.entry('session-persistence')).toMatchObject({
name: '@deepseek-ai/dsh-session-persistence-sqlite',
config: { path: './.sessions/sessions.sqlite' },
})
expect(installs).toBe(1)
})
it('switches required provider and interface options', async () => {
const project = await committedProject()
const registry = createBuiltinRegistry(project.profile)
const workflow = new ConfigWorkflow(new QueuePort([
[
{ value: 'feature:provider', choices: ['custom'] },
{ value: 'feature:app', choices: ['acp'] },
{ value: 'feature:persistence', choices: ['jsonl'] },
],
'https://provider.example/v1',
'custom-key',
true,
]), outputBuffer().stream, async () => {})
const result = await workflow.run(project, registry)
const provider = result.commit?.project.cordis.entry('llm-pi-ai')
expect(provider?.config).not.toHaveProperty('apiKey')
expect(provider?.config?.baseURL).toBe('https://provider.example/v1')
expect(result.commit?.project.cordis.entry('acp')).toBeDefined()
expect(result.commit?.project.cordis.entry('agent-loop')).toBeDefined()
expect(result.commit?.project.cordis.entry('agent-core')).toBeUndefined()
})
})
describe('dsh-sdk create', () => {
const writeDependency = (name: string) => async (_m: unknown, spec: string, cwd: string): Promise<void> => {
const path = join(cwd, 'package.json')
const manifest = JSON.parse(await readFile(path, 'utf8')) as { dependencies?: Record<string, string> }
manifest.dependencies = { ...manifest.dependencies, [name]: spec }
await writeFile(path, JSON.stringify(manifest, null, 2))
}
it('adds a dependency and mounts it after confirmation', async () => {
const project = await committedProject()
const context = { ...commandContext(project.root), port: new QueuePort([true]), add: writeDependency('my-ext-plugin') }
const result = await runCreatePluginCommand('github:o/r#sha', context)
expect(result?.project.cordis.entry('my-ext-plugin')?.name).toBe('my-ext-plugin')
expect(context.readStdout()).toContain('Mounted my-ext-plugin')
})
it('derives the cordis id from a scoped package name', async () => {
const project = await committedProject()
const context = { ...commandContext(project.root), port: new QueuePort([true]), add: writeDependency('@acme/cool-plugin') }
const result = await runCreatePluginCommand('@acme/cool-plugin@1.0.0', context)
expect(result?.project.cordis.entry('cool-plugin')?.name).toBe('@acme/cool-plugin')
})
it('returns undefined and adds nothing when declined', async () => {
const project = await committedProject()
let added = false
const context = {
...commandContext(project.root),
port: new QueuePort([false]),
add: async () => { added = true },
}
await expect(runCreatePluginCommand('pkg@1.0.0', context)).resolves.toBeUndefined()
expect(added).toBe(false)
})
it('rejects an empty source, a non-TTY session, and a no-op add', async () => {
const project = await committedProject()
await expect(runCreatePluginCommand(' ', { ...commandContext(project.root), port: new QueuePort([]) }))
.rejects.toThrow('requires a plugin source')
const noTty = commandContext(project.root)
noTty.stdin.isTTY = false
noTty.stdout.isTTY = false
await expect(runCreatePluginCommand('pkg@1.0.0', noTty)).rejects.toThrow('interactive TTY')
const noOutTty = commandContext(project.root)
noOutTty.stdout.isTTY = false
await expect(runCreatePluginCommand('pkg@1.0.0', noOutTty)).rejects.toThrow('interactive TTY')
await expect(runCreatePluginCommand('pkg@1.0.0', {
...commandContext(project.root), port: new QueuePort([true]), add: async () => {},
})).rejects.toThrow('added no new dependency')
})
it('dispatches create through the launcher', async () => {
const project = await committedProject()
const context = commandContext(project.root)
context.createPlugin = async () => undefined
await expect(runDshSdkCommand(['create', 'pkg@1.0.0'], context)).resolves.toBe(0)
})
})
describe('command telemetry', () => {
it('reports when consent allows and skips when denied or faulting', async () => {
const dir = await mkdtemp(join(tmpdir(), 'dsh-telemetry-'))
temporary.push(dir)
const sent: unknown[] = []
const reporter = { report: () => { sent.push(1) }, flush: async () => {} }
await reportCommandTelemetry(
{ command: 'build', cwd: dir, durationMs: 5, success: true },
{ resolve: async () => ({ allowed: true, reason: 'absent' }), reporter },
)
expect(sent).toHaveLength(1)
await reportCommandTelemetry(
{ command: 'build', cwd: dir, durationMs: 5, success: true },
{ resolve: async () => ({ allowed: false, reason: 'disabled' }), reporter },
)
expect(sent).toHaveLength(1)
await expect(reportCommandTelemetry(
{ command: 'build', cwd: dir, durationMs: 5, success: true },
{ resolve: async () => { throw new Error('boom') }, reporter },
)).resolves.toBeUndefined()
expect(sent).toHaveLength(1)
})
it('emits a telemetry event carrying each command outcome', async () => {
const project = await committedProject()
const events: CommandTelemetryEvent[] = []
const context = commandContext(project.root)
context.telemetry = async (event) => { events.push(event) }
context.build = async () => {}
await expect(runDshSdkCommand(['build'], context)).resolves.toBe(0)
expect(events).toHaveLength(1)
expect(events[0]).toMatchObject({ command: 'build', cwd: project.root, success: true })
await runDshSdkCommand([], context)
expect(events).toHaveLength(1)
context.build = async () => { throw new Error('boom') }
await expect(runDshSdkCommand(['build'], context)).resolves.toBe(1)
expect(events[1]).toMatchObject({ command: 'build', success: false })
context.config = async () => ({ installError: new Error('offline') })
await expect(runDshSdkCommand(['config'], context)).resolves.toBe(1)
expect(events.at(-1)).toMatchObject({ command: 'config', success: false })
})
})

View File

@@ -0,0 +1,15 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"],
"references": [
{ "path": "../helper" },
{ "path": "../telemetry" },
{ "path": "../../boot/app-boot" },
{ "path": "../../../vendor/cordis" },
{ "path": "../../support/invariants" }
]
}

View File

@@ -0,0 +1,26 @@
import { defineConfig } from 'tsdown'
/** Bundle each public or runtime entry and mirror package-owned terminal templates. */
export default defineConfig([
{
entry: ['lib/types/index.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024',
fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false,
copy: [{ from: 'src/templates/assets/*', to: 'lib/assets' }],
},
{
entry: ['lib/types/invariant.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024',
fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false,
},
{
entry: ['lib/types/bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024',
fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false,
},
{
entry: ['lib/types/dev/tsdown-config.js'], outDir: 'lib/dev', format: ['esm'], platform: 'node',
target: 'es2024', fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false,
},
{
entry: ['lib/types/local-plugin-loader-hooks.js'], outDir: 'lib', format: ['esm'], platform: 'node',
target: 'es2024', fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false,
},
])