Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml
#	docs/core-data-structures/core.i18n.yaml
#	docs/module-graph.md
#	packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
#	packages/client/ui-conversation/src/client/index.ts
#	packages/compact/compact-basic/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-08-08 17:56:53 +08:00
841 changed files with 13085 additions and 5978 deletions

View File

@@ -2,5 +2,5 @@
# 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 apps/cli/README.md
README.md: f50f26ec5de54e094e17221f7cd355483483754e
README.zh.md: 242e64a0c42064b9f0b7621e665e85fe44523fe5
README.md: 12108fcbff4e649d0bcb3e01e688fa334ab91b14
README.zh.md: 9518feed1d5d40c3e5ec2d346b929118ccc08810

View File

@@ -9,11 +9,11 @@ The `dsh` command is the product launcher for profiles: ordered stacks of plugin
| Command | Purpose |
|---|---|
| `dsh --profile <name>` | Boot the named profile under `$DSH_HOME/profiles/<name>`. |
| `dsh --profile headless "task"` | Run one fresh persisted session, print the final answer, and exit. |
| `dsh run [--profile <name>] [--patch <path>...] "task"` | Run one fresh persisted session, print the final answer, and exit; the profile defaults to `headless`. |
| `dsh web` | Alias of `--profile web` with the Web flag family (`--host`, `--port`, `--dev`, ...). |
| `dsh plugin --profile <name> <pnpm args>` | Manage a profile's plugins by forwarding to pnpm in the profile directory. |
The invoking directory is the default workspace root. The `web` and `headless` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`.
The invoking directory is the default workspace root. `dsh run` requires non-blank task text and the selected profile must mount the `headless-runner` row; `--profile` preserves custom one-shot profiles. The `web` and `headless` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`.
## Profiles

View File

@@ -9,11 +9,11 @@
| 命令 | 用途 |
|---|---|
| `dsh --profile <name>` | 启动位于 `$DSH_HOME/profiles/<name>` 的指定 profile。 |
| `dsh --profile headless "task"` | 运行一个新的持久化会话,打印最终答案并退出。 |
| `dsh run [--profile <name>] [--patch <path>...] "task"` | 运行一个新的持久化会话,打印最终答案并退出profile 默认为 `headless`。 |
| `dsh web` | `--profile web` 的别名,附带 Web flag 系列(`--host``--port``--dev` 等)。 |
| `dsh plugin --profile <name> <pnpm args>` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 |
调用目录是默认 workspace 根目录。`web``headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。
调用目录是默认 workspace 根目录。`dsh run` 要求任务文本非空白,且所选 profile 必须挂载 `headless-runner` 行;`--profile` 保留对自定义一次性 profile 的支持。`web``headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。
## Profile

View File

@@ -82,6 +82,8 @@ flowchart LR
cfg --> plugin_dsh_base_skill
plugin_dsh_base_skill_local["skill-local<br/>@deepseek-ai/dsh-skill-local"]
cfg --> plugin_dsh_base_skill_local
plugin_dsh_base_skill_badge["skill-badge<br/>@deepseek-ai/dsh-skill-badge"]
cfg --> plugin_dsh_base_skill_badge
plugin_dsh_base_tool_skill["tool-skill<br/>@deepseek-ai/dsh-tool-skill"]
cfg --> plugin_dsh_base_tool_skill
plugin_dsh_base_commands["commands<br/>@deepseek-ai/dsh-commands"]
@@ -199,6 +201,7 @@ flowchart LR
| `workspace-context` | `@deepseek-ai/dsh-workspace-context` |
| `skill` | `@deepseek-ai/dsh-skill` |
| `skill-local` | `@deepseek-ai/dsh-skill-local` |
| `skill-badge` | `@deepseek-ai/dsh-skill-badge` |
| `tool-skill` | `@deepseek-ai/dsh-tool-skill` |
| `commands` | `@deepseek-ai/dsh-commands` |
| `command-feedback` | `@deepseek-ai/dsh-command-feedback` |

View File

@@ -36,11 +36,14 @@
"node-addon-require-builtin": "^0.1.4"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-frontend-static": "workspace:^",
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-llm-mock-server": "workspace:^",
"@deepseek-ai/dsh-loader-smoke": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@types/js-yaml": "^4.0.9",

View File

@@ -2,5 +2,5 @@
# 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 apps/cli/reference/README.md
README.md: c7c7b2aa231d4c9f4b3fbf31663237c8457eb051
README.zh.md: 5439aa78b74415c8e6264d21f5c52e5cee5b38ee
README.md: 496cecdb64e3254a2a77690f55f760b4cd90b521
README.zh.md: 4673bf764347307a9b91e2a5474a8439cf67b481

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
This reference defines the profile, web-alias, plugin-management, and config-dump command modes. Argv is parsed once through [`src/args.ts`](../src/args.ts), and [`src/bin.ts`](../src/bin.ts) dynamically imports only the selected runner.
This reference defines the profile, one-shot run, web-alias, plugin-management, and config-dump command modes. Argv is parsed once through [`src/args.ts`](../src/args.ts), and [`src/bin.ts`](../src/bin.ts) dynamically imports only the selected runner.
## Profile boot
@@ -12,7 +12,7 @@ Bundle names resolve from the dsh installation first, then from the profile dire
The `web` and `headless` profiles auto-initialize from shipped templates on first use (`web`: base + web-app; `headless`: base + web-app + headless). Any other missing profile fails loud with a hint to run `dsh plugin --profile <name> add <package>`.
A positional task (`dsh --profile headless "run the tests"`) requires the composition to mount the one-shot runner row (`headless-runner`); the launcher patches the task text into that row, the runner drives one fresh persisted session through the in-process API carrier, prints the final assistant text on stdout, and exits 0 on a completed turn, else 1. The session's Web host runs on an OS-assigned port and is announced on stderr, so the run is observable in a browser.
Profile boot accepts no positional task. A profile that mounts the one-shot runner row (`headless-runner`) therefore fails loud with the canonical `dsh run --profile <name> "<task>"` command instead of reaching the row's raw required-field error.
Inspect the composed tree without booting it:
@@ -23,6 +23,12 @@ dsh --profile web --patch ./extra.yml --dump-config
`--dump-default-config` prints only the bundle layers; `--dump-config` adds the profile's `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml`, and `--patch` overlays. Both print provenance comments per layer; `!!js` expressions remain unevaluated, and unmatched patch targets are reported on stderr.
## One-shot run
`dsh run [--profile <name>] [--patch <path>...] <task...>` joins the task arguments with spaces, rejects a missing or blank task, and defaults `--profile` to `headless`. Repeatable `--patch` overlays occupy the same layer position as profile-boot overlays. A custom selected profile must mount `headless-runner`; otherwise launch fails before boot with a diagnostic naming that missing row.
The launcher patches the task text into the runner row, which drives one fresh persisted session through the in-process API carrier, prints the final assistant text on stdout, and exits 0 on a completed turn, else 1. At the idle boundary, the runner waits until its mux consumer has observed the session's final event sequence before deriving that output and exit reason. The session's Web host runs on an OS-assigned port and is announced on stderr, so the run is observable in a browser.
## Plugin management
`dsh plugin --profile <name> <args...>` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `<args...>` to `pnpm` with the profile directory as working directory — `add`, `remove`, `why`, `update`, and every other pnpm verb work unchanged; pnpm must be on PATH. Relative path specs (`.`, `../plugin`, and their `file:`/`link:` forms) are anchored to the invoking directory first, so `add .` from a plugin checkout installs that checkout, not the profile. After every successful run, `dsh.profile.bundles` is reconciled against the installed state: each dependency resolving to a package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` joins the layer stack (so an `update` that gains the declaration activates it), a bundle-less dependency stays plain with a one-time warning, and a removed dependency leaves the stack.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
本参考定义 profile、web 别名、插件管理和配置 dump 命令模式。参数由 [`src/args.ts`](../src/args.ts) 统一解析,[`src/bin.ts`](../src/bin.ts) 只动态导入选中的运行器。
本参考定义 profile、一次性运行、web 别名、插件管理和配置 dump 命令模式。参数由 [`src/args.ts`](../src/args.ts) 统一解析,[`src/bin.ts`](../src/bin.ts) 只动态导入选中的运行器。
## Profile 启动
@@ -12,7 +12,7 @@
`web``headless` profile 首次使用时会从随附模板自动初始化(`web`base + web-app`headless`base + web-app + headless。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>`
位置参数任务(`dsh --profile headless "run the tests"`)要求组合挂载一次性运行器行(`headless-runner`;启动器把任务文本 patch 进该行,运行器通过进程内 API 载体驱动一个全新的持久化会话,在 stdout 打印最终 assistant 文本,并在轮次完成时以 0 退出,否则以 1 退出。会话的 Web 宿主运行在 OS 分配的端口上并公布到 stderr因此该次运行可在浏览器中观察
Profile 启动不接受位置参数任务。因此,挂载一次性运行器行(`headless-runner`的 profile 会显式报错,并提示规范命令 `dsh run --profile <name> "<task>"`,而不会触发该行原始的必填字段错误
可在不启动的情况下检查组合出的配置树:
@@ -23,6 +23,12 @@ dsh --profile web --patch ./extra.yml --dump-config
`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml``--patch` overlay。两者都会按层打印来源注释`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。
## 一次性运行
`dsh run [--profile <name>] [--patch <path>...] <task...>` 会用空格拼接任务参数,拒绝缺失或空白任务,并让 `--profile` 默认为 `headless`。可重复使用的 `--patch` overlay 与 profile 启动的 overlay 位于同一层。所选的自定义 profile 必须挂载 `headless-runner`;否则启动器会在启动前失败,并在诊断中指明缺少该行。
启动器把任务文本 patch 进运行器行,运行器再通过进程内 API 载体驱动一个全新的持久化会话,在 stdout 打印最终 assistant 文本,并在轮次完成时以 0 退出,否则以 1 退出。到达 idle 边界时,运行器会等到 mux 消费方观察到会话的最终事件序号,再生成输出与退出原因。会话的 Web 宿主运行在 OS 分配的端口上并公布到 stderr因此该次运行可在浏览器中观察。
## 插件管理
`dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `pnpm``add``remove``why``update` 及其他所有 pnpm 子命令都照常可用pnpm 必须在 PATH 上。相对路径 spec`.``../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout而不是 profile。每次成功运行后`dsh.profile.bundles` 都会与已安装状态对齐:每个解析到 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的包的依赖加入层栈(因此让包获得该声明的 `update` 会将其激活),没有组合包声明的依赖保持为普通依赖并给出一次性警告,已移除的依赖则退出层栈。

View File

@@ -1,10 +1,10 @@
/**
* Commander adapter for the `dsh` command-line entry. The default command
* boots a named profile (`--profile <name>`), optionally with extra `--patch`
* overlays and a positional task (one-shot mode for profiles mounting the
* headless runner). `web` is a hardcoded alias for `--profile web` that adds
* the Web flag family; `plugin` manages a profile's plugin dependencies by
* forwarding to pnpm. Commander owns help, version, and parse errors.
* overlays. `run` owns one-shot task execution, defaulting to the headless
* profile; `web` is a hardcoded alias for `--profile web` that adds the Web
* flag family; `plugin` manages a profile's plugin dependencies by forwarding
* to pnpm. Commander owns help, version, and parse errors.
* @module @deepseek-ai/dsh/args
*/
@@ -16,8 +16,16 @@ interface ProfileInvocation {
profile: string
/** Extra patch-list overlays applied after the profile's own layer, in argv order. */
patches: string[]
/** Positional task text joined by spaces; non-empty only for one-shot runs. */
task?: string
}
/** Run one task through a profile mounting the headless runner. */
interface RunInvocation {
mode: 'run'
profile: string
/** Extra patch-list overlays applied after the profile's own layer, in argv order. */
patches: string[]
/** Non-blank task text joined from the variadic positional arguments. */
task: string
}
/** Print a composed profile tree and exit without booting. */
@@ -54,7 +62,7 @@ interface PluginInvocation {
}
/** The resolved `dsh` invocation. Help, version, and errors exit inside {@link parseDshArgs}. */
export type DshInvocation = ProfileInvocation | DumpConfigInvocation | WebInvocation | PluginInvocation
export type DshInvocation = ProfileInvocation | RunInvocation | DumpConfigInvocation | WebInvocation | PluginInvocation
/** Raw web-subcommand options straight from Commander. */
interface WebOptions {
@@ -68,6 +76,12 @@ interface WebOptions {
dumpDefaultConfig?: boolean
}
/** Raw run-subcommand options straight from Commander. */
interface RunOptions {
profile: string
patch?: string[]
}
/**
* Repeatable single-value collector: `--patch a.yml --patch b.yml`. Never
* variadic — a variadic `--patch` would swallow a following positional task.
@@ -90,19 +104,19 @@ export function parseDshArgs(argv: readonly string[], version: string): DshInvoc
.addHelpText('after', `
Examples:
dsh --profile web boot the web profile (same as: dsh web)
dsh --profile headless "run the tests" answer one task, print the result, and exit
dsh run "run the tests" answer one task, print the result, and exit
dsh run --profile custom "run the tests" run one task through a custom one-shot profile
dsh --profile tui --patch ./extra.yml boot a custom profile with one extra overlay
dsh plugin --profile tui add <package> install a plugin into the tui profile
dsh web --port 8080 the web alias with its flag family
`)
.exitOverride()
.enablePositionalOptions()
.argument('[task...]', 'one-shot task text for profiles mounting the headless runner')
.option('--profile <name>', 'the profile under $DSH_HOME/profiles to boot')
.option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)
.option('--dump-config', 'print the composed profile tree and exit')
.option('--dump-default-config', 'print the profile tree without its user layer or --patch overlays and exit')
.action((task: string[], options: {
.action((options: {
profile?: string
patch?: string[]
dumpConfig?: boolean
@@ -116,7 +130,6 @@ Examples:
if (options.dumpConfig === true && options.dumpDefaultConfig === true) {
program.error('error: --dump-config and --dump-default-config are mutually exclusive')
}
if (task.length > 0) program.error('error: --dump-config/--dump-default-config take no task')
const defaultOnly = options.dumpDefaultConfig === true
if (defaultOnly && patches.length > 0) {
program.error('error: --dump-default-config prints the bundle layers and takes no --patch')
@@ -124,12 +137,7 @@ Examples:
resolved = { mode: 'dump-config', profile, defaultOnly, patches }
return
}
resolved = {
mode: 'profile',
profile,
patches,
...task.length > 0 ? { task: task.join(' ') } : {},
}
resolved = { mode: 'profile', profile, patches }
})
/** Reject parent options that crossed a subcommand boundary. */
@@ -146,6 +154,22 @@ Examples:
}
}
const run = program.command('run').description('run one task through a profile mounting the headless runner')
run
.option('--profile <name>', 'one-shot profile under $DSH_HOME/profiles', 'headless')
.option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)
.argument('<task...>', 'task text')
.action((task: string[], options: RunOptions) => {
rejectParentOptions('run')
const profile = options.profile
if (profile === '') program.error('error: --profile needs a name')
const patches = options.patch ?? []
if (patches.includes('')) program.error('error: --patch needs a path')
const joined = task.join(' ')
if (joined.trim() === '') program.error('error: run needs a non-blank task')
resolved = { mode: 'run', profile, patches, task: joined }
})
const web = program.command('web').description('serve the browser UI (alias of --profile web) on the configured host and port')
web
.option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)

View File

@@ -33,7 +33,16 @@ switch (invocation.mode) {
environment: loadLayeredEnv('dsh'),
profile: invocation.profile,
patchFiles: invocation.patches,
...invocation.task !== undefined && { task: invocation.task },
})
break
}
case 'run': {
const { runProfile } = await import('./profile-boot.ts')
await runProfile({
environment: loadLayeredEnv('dsh'),
profile: invocation.profile,
patchFiles: invocation.patches,
task: invocation.task,
})
break
}

View File

@@ -47,7 +47,7 @@ export const INSTALL_ANCHOR = fileURLToPath(new URL('../package.json', import.me
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets. */
const TELEMETRY_ROW_ID = 'telemetry-otel'
/** The one-shot runner row a positional task requires and configures. */
/** The one-shot runner row a `dsh run` task requires and configures. */
const HEADLESS_ROW_ID = 'headless-runner'
/** The empty root entry list every profile tree patches over. */
@@ -160,7 +160,7 @@ export interface RunProfileOptions {
patchFiles: readonly string[]
/** Launcher hook turning the pre-flag composed rows into flag patches (the web alias's flag family). */
deriveFlagPatches?: (rows: ProfileRows) => PatchOptions[]
/** One-shot task text; requires the composition to mount the headless runner row. */
/** `dsh run` task text; requires the composition to mount the headless runner row. */
task?: string
/** Surface setup registered after Loader installation and before any config-tree entry mounts. */
prepare?: (ctx: Context, rows: ProfileRows) => Promise<void> | void
@@ -190,7 +190,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
// error naming no fix.
throw new Error(
`dsh: profile ${JSON.stringify(options.profile)} mounts the one-shot runner and needs a task: `
+ `dsh --profile ${options.profile} "<task>"`,
+ `dsh run --profile ${options.profile} "<task>"`,
)
}

View File

@@ -21,12 +21,16 @@ function exitCode(argv: string[]): number {
afterEach(() => { vi.restoreAllMocks() })
describe('parseDshArgs', () => {
it('routes profile boots, one-shot tasks, and the web alias', () => {
it('routes profile boots, one-shot runs, and the web alias', () => {
expect(parse(['--profile', 'tui'])).toEqual({ mode: 'profile', profile: 'tui', patches: [] })
expect(parse(['--profile', 'headless', 'run', 'the', 'tests']))
.toEqual({ mode: 'profile', profile: 'headless', patches: [], task: 'run the tests' })
expect(parse(['--profile', 'tui', '--patch', 'a.yml', '--patch', 'b.yml']))
.toEqual({ mode: 'profile', profile: 'tui', patches: ['a.yml', 'b.yml'] })
expect(parse(['run', 'run', 'the', 'tests']))
.toEqual({ mode: 'run', profile: 'headless', patches: [], task: 'run the tests' })
expect(parse(['run', '--profile', 'custom', '--patch', 'a.yml', '--patch', 'b.yml', 'run', 'the', 'tests']))
.toEqual({ mode: 'run', profile: 'custom', patches: ['a.yml', 'b.yml'], task: 'run the tests' })
expect(parse(['run', '--', '--profile', 'is', 'task', 'text']))
.toEqual({ mode: 'run', profile: 'headless', patches: [], task: '--profile is task text' })
expect(parse(['web'])).toEqual({ mode: 'web', dev: false, patches: [] })
expect(parse(['web', '--patch', 'web.yml'])).toEqual({ mode: 'web', dev: false, patches: ['web.yml'] })
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w']))
@@ -65,6 +69,13 @@ describe('parseDshArgs', () => {
expect(exitCode(['tui'])).toBe(1) // a bare word is a task without --profile
expect(exitCode(['--config', 'c.yml'])).toBe(1) // removed
expect(exitCode(['-p', 'task'])).toBe(1) // removed
expect(exitCode(['--profile', 'headless', 'task'])).toBe(1) // tasks belong to `run`
expect(exitCode(['run'])).toBe(1)
expect(exitCode(['run', ''])).toBe(1)
expect(exitCode(['run', '--profile', '', 'task'])).toBe(1)
expect(exitCode(['run', '--patch=', 'task'])).toBe(1)
expect(exitCode(['--profile', 'headless', 'run', 'task'])).toBe(1)
expect(exitCode(['--patch', 'parent.yml', 'run', 'task'])).toBe(1)
expect(exitCode(['--profile', ''])).toBe(1)
expect(exitCode(['--profile', 'x', '--patch='])).toBe(1)
expect(exitCode(['--dump-config'])).toBe(1)
@@ -90,6 +101,7 @@ describe('parseDshArgs', () => {
it('exits 0 for help and version', () => {
expect(exitCode(['--help'])).toBe(0)
expect(exitCode(['run', '--help'])).toBe(0)
expect(exitCode(['--version'])).toBe(0)
})
})

View File

@@ -182,14 +182,56 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
const help = await runBuiltBin(['--help'])
expect(help.code).toBe(0)
expect(help.stdout).toContain('dsh --profile web')
expect(help.stdout).toContain('dsh run "run the tests"')
expect(help.stdout).toContain('dsh plugin --profile')
expect(help.stdout).not.toMatch(/^\s+(?:tui|meta|upgrade)\b/mu)
for (const removed of [['tui'], ['--config', 'x.yml'], ['-p', 'task']]) {
for (const removed of [['tui'], ['--config', 'x.yml'], ['-p', 'task'], ['--profile', 'headless', 'task']]) {
const result = await runBuiltBin(removed)
expect(result.code).toBe(1)
}
}, 30_000)
it('prints run help without initializing the selected profile', async () => {
const parent = mkdtempSync(join(tmpdir(), 'dsh-run-help-'))
const home = join(parent, 'not-created')
try {
const result = await runBuiltBin(['run', '--help'], { DSH_HOME: home })
expect(result.code).toBe(0)
expect(result.stderr).toBe('')
expect(result.stdout).toContain('Usage: dsh run [options] <task...>')
expect(existsSync(home)).toBe(false)
} finally {
rmSync(parent, { recursive: true, force: true })
}
})
it('runs the default headless profile through the published run command', async () => {
const apiKey = 'built-dsh-run-key'
const server = await startMockLlmServer({
sequence: ['success'],
apiKey,
successText: 'published dsh run reached the mock',
})
const home = mkdtempSync(join(tmpdir(), 'dsh-built-run-'))
try {
const result = await runBuiltBin(['run', 'answer', 'from', 'the', 'published', 'entry'], {
DSH_HOME: home,
DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: apiKey,
DEEPSEEK_BASE_URL: server.baseURL,
})
expect(result.code, result.stderr).toBe(0)
expect(result.stdout).toBe('published dsh run reached the mock')
expect(result.stderr).toMatch(/^dsh: observing at http:\/\/127\.0\.0\.1:\d+$/u)
expect(server.requests.length).toBeGreaterThan(0)
expect(server.requests.every(request => request.path === '/chat/completions')).toBe(true)
expect(JSON.stringify(server.requests.map(request => request.body))).toContain('answer from the published entry')
} finally {
await server.close()
rmSync(home, { recursive: true, force: true })
}
}, 30_000)
it('does not load a project environment for --version', async () => {
const project = mkdtempSync(join(tmpdir(), 'dsh-version-project-'))
writeFileSync(join(project, '.env'), 'PATH=/project-only-path\n')

View File

@@ -0,0 +1,176 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
const binScript = fileURLToPath(new URL('./fixtures/dsh-badge/snapshot.ts', import.meta.url))
const configPath = fileURLToPath(new URL('./fixtures/dsh-badge/cordis.yml', import.meta.url))
const defaultConfigPath = fileURLToPath(new URL('./fixtures/dsh-badge/default.cordis.yml', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const badgeAssetsPath = fileURLToPath(new URL('../../../packages/skill/skill-badge/assets/', import.meta.url))
describe('dsh badge assembled snapshot', () => {
it('advertises and loads the opt-in bundled skill through the shipped app', async () => {
const disabled = await runLoaderSmoke({
label: 'disabled dsh badge skill snapshot',
tempDirPrefix: 'headless-snapshot-dsh-badge-disabled-',
binScript,
libBinScript: binScript,
configPath: defaultConfigPath,
tsconfigPath,
})
const enabled = await runLoaderSmoke({
label: 'dsh badge skill snapshot',
tempDirPrefix: 'headless-snapshot-dsh-badge-',
binScript,
libBinScript: binScript,
configPath,
tsconfigPath,
})
const disabledSnapshot = JSON.parse(disabled.stdout) as unknown
const enabledSnapshot = JSON.parse(
enabled.stdout.replaceAll(badgeAssetsPath, '{{badgeAssetsPath}}'),
) as unknown
expect(disabled.stderr).toBe('')
expect(enabled.stderr).toBe('')
expect(disabledSnapshot).toMatchInlineSnapshot(`
{
"catalog": null,
"result": {
"content": [
{
"text": "Error: skill "dsh-badge" is unknown or no longer available",
"type": "text",
},
],
"error": {
"message": "skill "dsh-badge" is unknown or no longer available",
},
"isError": true,
},
"summary": null,
}
`)
expect(enabledSnapshot).toMatchInlineSnapshot(`
{
"catalog": [
{
"text": "<system-reminder>
A skill is a reusable set of task-specific instructions. The following skills are available in this session:
<available_skills>
- \`dsh-badge\`: Add the official “powered by dsh” badge to documents, pull requests, merge requests, and other content produced with DeepSeek Harness. Use whenever creating a pull request or merge request. Also use when the user asks for a dsh badge, powered-by-dsh attribution, or a reusable dsh badge asset or snippet.
</available_skills>
If the user names a skill, or the task clearly matches a skill's description, call the \`skill\` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.
A user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the \`skill\` tool again for that skill.
</system-reminder>",
"type": "text",
},
],
"result": {
"content": [
{
"text": "<skill_content name="dsh-badge">
<skill_resources>
Base directory for this skill: {{badgeAssetsPath}}
Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.
</skill_resources>
<skill_instructions>
# dsh Badge
Add the official “powered by dsh” badge without recreating or restyling it.
## Assets
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
## Markdown
Use this linked badge in Markdown:
\`\`\`markdown
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
\`\`\`
If attribution should not be linked, use:
\`\`\`markdown
![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)
\`\`\`
## Usage rules
- For GitHub or GitLab Markdown, use the Shields.io URL and link it to the project URL unless the user asks for an unlinked image.
- For Feishu and other systems that import remote images unreliably, upload \`dsh-badge.png\` from this skill directory instead of generating another badge.
- Preserve the badge's 121×20 dimensions and aspect ratio.
- Place the badge at the end of the attributed document or section unless the user specifies another position.
- Do not substitute another color, logo, label, or project URL.
</skill_instructions>
</skill_content>",
"type": "text",
},
],
"isError": false,
"value": {
"content": "# dsh Badge
Add the official “powered by dsh” badge without recreating or restyling it.
## Assets
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
## Markdown
Use this linked badge in Markdown:
\`\`\`markdown
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
\`\`\`
If attribution should not be linked, use:
\`\`\`markdown
![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)
\`\`\`
## Usage rules
- For GitHub or GitLab Markdown, use the Shields.io URL and link it to the project URL unless the user asks for an unlinked image.
- For Feishu and other systems that import remote images unreliably, upload \`dsh-badge.png\` from this skill directory instead of generating another badge.
- Preserve the badge's 121×20 dimensions and aspect ratio.
- Place the badge at the end of the attributed document or section unless the user specifies another position.
- Do not substitute another color, logo, label, or project URL.
",
"name": "dsh-badge",
"provider": "dsh-badge",
"resourceBase": {
"kind": "directory",
"path": "{{badgeAssetsPath}}",
},
},
},
"summary": {
"description": "Add the official “powered by dsh” badge to documents, pull requests, merge requests, and other content produced with DeepSeek Harness. Use whenever creating a pull request or merge request. Also use when the user asks for a dsh badge, powered-by-dsh attribution, or a reusable dsh badge asset or snippet.",
"invocation": {
"modelInvocable": true,
"userInvocable": true,
},
"name": "dsh-badge",
"provider": "dsh-badge",
"resourceBase": {
"kind": "directory",
"path": "{{badgeAssetsPath}}",
},
"source": "bundled",
},
}
`)
}, LOADER_SMOKE_TEST_TIMEOUT_MS * 2)
})

View File

@@ -0,0 +1,9 @@
- id: skill-badge
disabled: false
- id: skill-local
config:
watch: false
- id: telemetry-otel
disabled: true

View File

@@ -0,0 +1,6 @@
- id: skill-local
config:
watch: false
- id: telemetry-otel
disabled: true

View File

@@ -0,0 +1,56 @@
import { fileURLToPath } from 'node:url'
import { Context } from 'cordis'
import { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
import { CallId } from '@deepseek-ai/dsh-llm'
import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import { SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-skill'
import type {} from '@deepseek-ai/dsh-tools'
const overlayPath = process.argv[2]
if (overlayPath === undefined) throw new Error('dsh-badge snapshot requires an overlay path')
const rootConfigPath = fileURLToPath(new URL('../../../../../packages/bundle/base/tests/fixtures/root.cordis.yml', import.meta.url))
const basePatchPath = fileURLToPath(new URL('../../../../../packages/bundle/base/cordis.patch.yml', import.meta.url))
const ctx = await boot('dsh-badge-snapshot', rootConfigPath, [
...loadOverlayPatches('dsh-badge-snapshot', basePatchPath),
...loadOverlayPatches('dsh-badge-snapshot', overlayPath),
])
try {
const agentId = SessionId('dsh-badge-snapshot')
const session = ctx.sessions.create(agentId, { meta: { cwd: process.cwd() } })
const agent: Agent = {
ctx: new Context(),
id: agentId,
options: {},
session,
inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
status: 'idle',
send: () => {},
followup: () => {},
steer: () => {},
inject: () => { throw new Error('dsh-badge snapshot must receive the catalog at the step boundary') },
cancel: () => {},
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
const decision = await agentEvents(ctx, agent).waterfall(
'agent/pre-step',
{ messages: [], turn: 1, step: 1, signal: new AbortController().signal },
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
)
const catalog = decision.kind === 'enter'
? decision.messages.find(message => message.role === 'user'
&& message.source.kind === 'skill-catalog')?.content
: undefined
const summary = (await ctx.skills.list()).find(skill => skill.name === 'dsh-badge')
const result = await ctx.tools.execute({
callId: CallId('dsh-badge-snapshot'),
name: 'skill',
arguments: { name: 'dsh-badge' },
signal: new AbortController().signal,
})
process.stdout.write(`${JSON.stringify({ catalog: catalog ?? null, summary: summary ?? null, result })}\n`)
} finally {
await ctx.fiber.dispose()
}

View File

@@ -66,7 +66,7 @@ async function runHeadlessPtySmoke(): Promise<string> {
try {
const home = join(cwd, '.dsh')
// Pre-initialize the headless profile with the never-dispose row in its
// user patch layer (the same file `dsh --profile headless` hot-reloads).
// user patch layer (the same file a long-lived profile boot hot-reloads).
const profileDir = join(home, 'profiles', 'headless')
await mkdir(profileDir, { recursive: true })
await writeFile(join(profileDir, 'package.json'), JSON.stringify({
@@ -83,7 +83,7 @@ async function runHeadlessPtySmoke(): Promise<string> {
].join('\n'))
const launch = resolveExampleLaunch({
srcBin: dshBinScript,
configArgs: ['--profile', 'headless', 'never complete'],
configArgs: ['run', 'never complete'],
tsconfigPath,
env: {
DSH_HOME: home,