mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/master' into feat/loader-entry-disabled-interpolation
This commit is contained in:
@@ -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: 4fae5338a89ce12c2620e123530acf883ae9efff
|
||||
README.zh.md: a2d086b8ff12fb07f2446fc4162de09739bcdeab
|
||||
README.md: 98a856261bc632c97f350db8fc7bb0b10c22235d
|
||||
README.zh.md: 283e54138e24202ed6b88d1d309538c58cd66b3e
|
||||
|
||||
@@ -31,8 +31,8 @@ dsh --help # the launcher's own help
|
||||
|
||||
A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml` (the user's own patch layer, hot-reloaded on long-lived surfaces). The tree composes over an empty root: each bundle's patch in `dsh.profile.bundles` order, then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`, then `--patch` overlays. Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it.
|
||||
|
||||
The [CLI behavior reference](reference/README.md) owns exact layer precedence, flags, shutdown behavior, deployment defaults, and the source launcher.
|
||||
The [CLI behavior reference](reference/README.md) owns exact layer precedence, flags, shutdown behavior, deployment defaults, and source execution.
|
||||
|
||||
## Development
|
||||
|
||||
Production runs require built package and frontend artifacts. From a checkout, `pnpm run dsh` runs the TypeScript entry and forwards arguments; the [source-launcher reference](reference/README.md#source-launcher) describes the PATH symlink and module-resolution contract.
|
||||
Production runs require built package and frontend artifacts. From the repository root, `pnpm dsh <args...>` builds those artifacts, runs the TypeScript entry, and forwards every argument; the [source-execution reference](reference/README.md#source-execution) owns the module-resolution contract.
|
||||
|
||||
@@ -31,8 +31,8 @@ dsh --help # the launcher's own help
|
||||
|
||||
profile 目录包含一个 `package.json`(树外插件依赖,加上 profile manifest(元数据清单)`dsh.profile` 及其有序的 `bundles` 列表)和一个 `cordis.patch.yml`(用户自己的 patch 层,在长期运行的 surface 上热重载)。配置树在空根之上组合:先按 `dsh.profile.bundles` 顺序应用各组合包的 patch,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`,然后是 `--patch` overlay。`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自己的 `node_modules` 解析;pnpm 把树外插件安装在后者。使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。
|
||||
|
||||
[CLI(命令行界面)行为参考](reference/README.md)负责确切的层优先级、flag、关闭行为、部署默认值和源码启动器。
|
||||
[CLI(命令行界面)行为参考](reference/README.md)负责确切的层优先级、flag、关闭行为、部署默认值和源码执行。
|
||||
|
||||
## 开发
|
||||
|
||||
生产运行需要已构建的包与前端产物。在 checkout 中,`pnpm run dsh` 会运行 TypeScript 入口并转发参数;[源码启动器参考](reference/README.md#source-launcher)说明 PATH 符号链接和模块解析契约。
|
||||
生产运行需要已构建的包与前端产物。从仓库根目录运行 `pnpm dsh <args...>` 会先构建这些产物,再运行 TypeScript 入口并转发所有参数;模块解析约定由[源码执行参考](reference/README.md#source-execution)负责。
|
||||
|
||||
@@ -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: fd0647347312051a1814a5e3464b34032ae70dfc
|
||||
README.zh.md: afe4b9ba5651e962288ffebbe7c095ad20bcc617
|
||||
README.md: cef687dc392f97886ea18b162e8f668a44ce2284
|
||||
README.zh.md: f6721ec256d404e2b602fb2ed921b41c03633a82
|
||||
|
||||
@@ -79,12 +79,6 @@ Session events stream as OTLP/HTTP logs by default. `DSH_TELEMETRY_OTLP_URL` sel
|
||||
|
||||
Install external plugin bundles through `dsh plugin --profile <name> add <package-or-git-spec>`. The installed package owns its dependencies and contributes its declared `cordis.patch.yml` layer. The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.
|
||||
|
||||
## Source launcher
|
||||
## Source execution
|
||||
|
||||
Link the source-running launcher onto PATH:
|
||||
|
||||
```sh
|
||||
ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh
|
||||
```
|
||||
|
||||
It resolves the checkout through its real path and launches `apps/cli/src/bin.ts` with `node --import tsx/esm`. `TSX_TSCONFIG_PATH` is pinned to the checkout root, so workspace package resolution is independent of the invoking directory. `pnpm run dsh` uses the same entry and forwards arguments. The built form is `apps/cli/lib/bin.js` after `pnpm run build`.
|
||||
From the repository root, use `pnpm dsh <args...>`. The `package.json` script runs the complete repository build, launches `apps/cli/src/bin.ts` with `node --import tsx/esm`, and forwards every argument. Build output appears before CLI output. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.
|
||||
|
||||
@@ -79,12 +79,6 @@ dsh web --help
|
||||
|
||||
通过 `dsh plugin --profile <name> add <package-or-git-spec>` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent(智能体)沙箱之外的受信任可执行代码。
|
||||
|
||||
## 源码启动器
|
||||
## 源码执行
|
||||
|
||||
把源码运行启动器链接到 PATH:
|
||||
|
||||
```sh
|
||||
ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh
|
||||
```
|
||||
|
||||
它通过 real path 解析 checkout,并使用 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`。`TSX_TSCONFIG_PATH` 固定到 checkout 根目录,因此 workspace 包解析不依赖调用目录。`pnpm run dsh` 使用同一入口并转发参数。运行 `pnpm run build` 后,构建形式为 `apps/cli/lib/bin.js`。
|
||||
请从仓库根目录使用 `pnpm dsh <args...>`。`package.json` 中的脚本会完成整个仓库的构建,通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。构建输出会显示在 CLI 输出之前。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY` 和 `HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
|
||||
|
||||
@@ -85,14 +85,14 @@ describe('dsh badge assembled snapshot', () => {
|
||||
|
||||
- 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\`
|
||||
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness\`
|
||||
|
||||
## Markdown
|
||||
|
||||
Use this linked badge in Markdown:
|
||||
|
||||
\`\`\`markdown
|
||||
[](https://github.com/deepseek-ai/deepseek-harness-sdk)
|
||||
[](https://github.com/deepseek-ai/deepseek-harness)
|
||||
\`\`\`
|
||||
|
||||
If attribution should not be linked, use:
|
||||
@@ -124,14 +124,14 @@ describe('dsh badge assembled snapshot', () => {
|
||||
|
||||
- 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\`
|
||||
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness\`
|
||||
|
||||
## Markdown
|
||||
|
||||
Use this linked badge in Markdown:
|
||||
|
||||
\`\`\`markdown
|
||||
[](https://github.com/deepseek-ai/deepseek-harness-sdk)
|
||||
[](https://github.com/deepseek-ai/deepseek-harness)
|
||||
\`\`\`
|
||||
|
||||
If attribution should not be linked, use:
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import { chmodSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { execa } from 'execa'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
const installer = fileURLToPath(new URL('../../../scripts/install.sh', import.meta.url))
|
||||
const fixtures: string[] = []
|
||||
|
||||
const PTY_DRIVER = String.raw`
|
||||
import errno, json, os, pty, select, signal, sys, time
|
||||
script, cwd, env_json, actions_json = sys.argv[1:]
|
||||
env = os.environ.copy()
|
||||
env.update(json.loads(env_json))
|
||||
actions = json.loads(actions_json)
|
||||
pid, fd = pty.fork()
|
||||
if pid == 0:
|
||||
os.chdir(cwd)
|
||||
os.execvpe("sh", ["sh", script], env)
|
||||
|
||||
output = bytearray()
|
||||
action_index = 0
|
||||
deadline = time.monotonic() + 15
|
||||
status = None
|
||||
while time.monotonic() < deadline:
|
||||
ready, _, _ = select.select([fd], [], [], 0.05)
|
||||
if ready:
|
||||
try:
|
||||
chunk = os.read(fd, 65536)
|
||||
except OSError as error:
|
||||
if error.errno != errno.EIO:
|
||||
raise
|
||||
chunk = b""
|
||||
output.extend(chunk)
|
||||
while action_index < len(actions) and actions[action_index]["waitFor"].encode() in output:
|
||||
os.write(fd, actions[action_index]["send"].encode())
|
||||
action_index += 1
|
||||
waited, candidate = os.waitpid(pid, os.WNOHANG)
|
||||
if waited == pid:
|
||||
status = candidate
|
||||
break
|
||||
|
||||
if status is None:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
_, status = os.waitpid(pid, 0)
|
||||
sys.stdout.buffer.write(output)
|
||||
if action_index != len(actions):
|
||||
sys.stderr.write(f"completed {action_index}/{len(actions)} PTY actions\n")
|
||||
sys.exit(124)
|
||||
sys.exit(os.waitstatus_to_exitcode(status))
|
||||
`
|
||||
|
||||
interface Action {
|
||||
readonly waitFor: string
|
||||
readonly send: string
|
||||
}
|
||||
|
||||
interface Fixture {
|
||||
readonly binDirectory: string
|
||||
readonly launchLog: string
|
||||
readonly pnpmLog: string
|
||||
readonly root: string
|
||||
readonly script: string
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(fixtures.splice(0).map(async (fixture) => { await rm(fixture, { force: true, recursive: true }) }))
|
||||
})
|
||||
|
||||
function executable(path: string, content: string): void {
|
||||
writeFileSync(path, content)
|
||||
chmodSync(path, 0o755)
|
||||
}
|
||||
|
||||
async function createFixture(): Promise<Fixture> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-install-'))
|
||||
fixtures.push(root)
|
||||
const checkoutDirectory = join(root, 'checkout')
|
||||
const scriptsDirectory = join(checkoutDirectory, 'scripts')
|
||||
const sourceBinDirectory = join(checkoutDirectory, 'bin')
|
||||
const fakeBinDirectory = join(root, 'fake-bin')
|
||||
const binDirectory = join(root, 'path-bin')
|
||||
for (const directory of [scriptsDirectory, sourceBinDirectory, fakeBinDirectory, binDirectory, join(root, 'home/.dsh')]) {
|
||||
mkdirSync(directory, { recursive: true })
|
||||
}
|
||||
const script = join(scriptsDirectory, 'install.sh')
|
||||
copyFileSync(installer, script)
|
||||
const launchLog = join(root, 'launch.log')
|
||||
const pnpmLog = join(root, 'pnpm.log')
|
||||
executable(join(sourceBinDirectory, 'dsh'), '#!/bin/sh\nprintf \'%s\\n\' "$*" >"$DSH_TEST_LAUNCH_LOG"\n')
|
||||
executable(join(fakeBinDirectory, 'pnpm'), `#!/bin/sh
|
||||
if [ "\${1:-}" = --version ]; then printf '11.7.0\\n'; exit 0; fi
|
||||
printf '%s\\n' "$*" >>"$DSH_TEST_PNPM_LOG"
|
||||
`)
|
||||
await execa('git', ['init', '-q'], { cwd: checkoutDirectory })
|
||||
await execa('git', ['add', 'bin/dsh', 'scripts/install.sh'], { cwd: checkoutDirectory })
|
||||
await execa('git', [
|
||||
'-c', 'user.name=dsh-test',
|
||||
'-c', 'user.email=dsh-test@example.invalid',
|
||||
'commit', '-qm', 'fixture',
|
||||
], { cwd: checkoutDirectory })
|
||||
writeFileSync(join(root, 'home/.dsh/.env'), 'DEEPSEEK_API_KEY=test\n')
|
||||
return { binDirectory, launchLog, pnpmLog, root, script }
|
||||
}
|
||||
|
||||
async function runInstaller(fixture: Fixture, actions: readonly Action[]): Promise<string> {
|
||||
const result = await execa('python3', [
|
||||
'-c',
|
||||
PTY_DRIVER,
|
||||
fixture.script,
|
||||
fixture.root,
|
||||
JSON.stringify({
|
||||
DSH_BIN_DIR: fixture.binDirectory,
|
||||
DSH_HOME: join(fixture.root, 'home/.dsh'),
|
||||
DSH_TEST_LAUNCH_LOG: fixture.launchLog,
|
||||
DSH_TEST_PNPM_LOG: fixture.pnpmLog,
|
||||
HOME: join(fixture.root, 'home'),
|
||||
PATH: `${join(fixture.root, 'fake-bin')}:${fixture.binDirectory}:${process.env.PATH ?? ''}`,
|
||||
}),
|
||||
JSON.stringify(actions),
|
||||
], { reject: false, stripFinalNewline: false, timeout: 20_000 })
|
||||
expect(result.exitCode, result.stderr).toBe(0)
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
describe.runIf(process.platform !== 'win32')('one-line installer launch', { timeout: 25_000 }, () => {
|
||||
it('builds and launches the Web UI', async () => {
|
||||
const fixture = await createFixture()
|
||||
|
||||
const output = await runInstaller(fixture, [
|
||||
{ waitFor: 'Replace it?', send: '\n' },
|
||||
])
|
||||
|
||||
expect(output).toContain('launching Web UI')
|
||||
expect(readFileSync(fixture.pnpmLog, 'utf8')).toBe('install\nrun build\n')
|
||||
expect(readFileSync(fixture.launchLog, 'utf8')).toBe('web\n')
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,12 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { execa } from 'execa'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
/**
|
||||
* Keyless smoke for the SOURCE `dsh` launcher: run `apps/cli/src/bin.ts`
|
||||
* with the exact production launch vector (`node --import tsx/esm`, the same
|
||||
* executable and arguments as `bin/dsh` and the root `dsh`/`demo:web` scripts) and assert the
|
||||
* Keyless smoke for SOURCE `dsh` execution: run `apps/cli/src/bin.ts`
|
||||
* with the exact production runtime vector (`node --import tsx/esm`, the
|
||||
* vector the root `dsh` script invokes after building) and assert the
|
||||
* required-config diagnostic. The Node compatibility matrix runs this
|
||||
* WHOLE file, so a Node release changing module hooks or TypeScript handling
|
||||
* breaks this gate instead of every developer's `pnpm dsh`; the built-bin
|
||||
@@ -16,6 +17,13 @@ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
const dshSourceBin = 'apps/cli/src/bin.ts'
|
||||
|
||||
describe('dsh SOURCE launcher (node --import tsx/esm)', () => {
|
||||
it('builds before launching the source CLI', async () => {
|
||||
const rootPackage = JSON.parse(await readFile(new URL('../../../package.json', import.meta.url), 'utf8')) as {
|
||||
readonly scripts?: Record<string, string>
|
||||
}
|
||||
expect(rootPackage.scripts?.dsh).toBe('pnpm run build && node --import tsx/esm apps/cli/src/bin.ts')
|
||||
})
|
||||
|
||||
it('boots the source entry and requires a profile', async () => {
|
||||
const result = await execa(process.execPath, ['--import', 'tsx/esm', dshSourceBin], {
|
||||
cwd: repoRoot,
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('the shipped Web composition', () => {
|
||||
// depend on ripgrep being present on the machine.
|
||||
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
|
||||
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
||||
'get_goal', 'interrupt_agent', 'list_agents', 'ralph', 'read', 'send_message', 'skill',
|
||||
'get_goal', 'interrupt_agent', 'list_agents', 'ralph', 'read', 'read_image', 'send_message', 'skill',
|
||||
'subagent', 'subagent_fork', 'task_kill',
|
||||
'task_list', 'task_output', 'todo_write', 'update_goal', 'web_search',
|
||||
'workflow', 'write',
|
||||
|
||||
Reference in New Issue
Block a user