refactor(cli): inline source launch script

This commit is contained in:
Turtle
2026-08-11 11:58:47 +08:00
parent 6ad289809b
commit 67ef355800
9 changed files with 16 additions and 139 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md
2026-08-10-source-run-without-managed-installer.md: 85b98cabe18f038908a00d789c48197c36b80a3b 2026-08-10-source-run-without-managed-installer.md: ac506b72acab0dd6c92ce6111487d091b2bf4a73
2026-08-10-source-run-without-managed-installer.zh.md: 4a322a48c23352cecfeccef3a93d3b782b8d3f80 2026-08-10-source-run-without-managed-installer.zh.md: 86e44a90a9e7b34ad37b6a4bfd3ad14de40868f5
@@ -12,7 +12,7 @@ That lifecycle is not required to run or develop DeepSeek Harness from a source
## Decision ## Decision
The repository supports source execution through its root `pnpm` scripts. The `pnpm dsh <args...>` launcher runs the complete repository build before launching the source CLI. It discards successful build output so CLI stdout remains machine-readable, reports failed build diagnostics on stderr, and sets `NODE_USE_ENV_PROXY=1` for the CLI process. Users select Web with `pnpm dsh web` and headless execution with `pnpm dsh --profile headless "task"`. The independent ACP example remains available through `pnpm run demo:acp`. The repository supports source execution through its root `pnpm` scripts. The `dsh` entry in `package.json` runs `pnpm run build`, then launches `apps/cli/src/bin.ts` through `node --import tsx/esm`; build output remains visible before the CLI output. The package script forwards arguments and inherits the caller's environment, including `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. Users select Web with `pnpm dsh web` and headless execution with `pnpm dsh --profile headless "task"`. The independent ACP example remains available through `pnpm run demo:acp`.
The repository does not distribute a source installer, an installer test suite, or skills that assume a managed `current` symlink and timestamped staging worktrees. Users own source checkout placement, Git updates, and any launcher they create outside the repository. The repository does not distribute a source installer, an installer test suite, or skills that assume a managed `current` symlink and timestamped staging worktrees. Users own source checkout placement, Git updates, and any launcher they create outside the repository.
@@ -28,4 +28,4 @@ The repository does not distribute a source installer, an installer test suite,
Source users invoke repository scripts rather than an installed `dsh` command. The repository provides no atomic upgrade cutover or preserved staging rollback checkout, and it does not automate the integration or upstream publication of personal source modifications. A future distribution mechanism must justify its ownership of installation and upgrade state, define recovery behavior, and add tests and user documentation without making the source-run path depend on it. Any future publication workflow must isolate one approved feature and obtain explicit approval before its first push and draft PR. Source users invoke repository scripts rather than an installed `dsh` command. The repository provides no atomic upgrade cutover or preserved staging rollback checkout, and it does not automate the integration or upstream publication of personal source modifications. A future distribution mechanism must justify its ownership of installation and upgrade state, define recovery behavior, and add tests and user documentation without making the source-run path depend on it. Any future publication workflow must isolate one approved feature and obtain explicit approval before its first push and draft PR.
Verification covers repository-wide references to the removed entry points, documentation links, generated third-party-notice freshness, and source CLI smokes that verify the build-first launch and absence of build logs on stdout through `pnpm dsh`. Verification covers repository-wide references to the removed entry points, documentation links, generated third-party-notice freshness, the build-first `package.json` command, and a source CLI smoke through the exact `node --import tsx/esm` runtime vector.
@@ -12,7 +12,7 @@ Status: implemented
## 决策 ## 决策
仓库通过根目录的 `pnpm` 脚本支持从源码运行。`pnpm dsh <args...>` 启动器会先完成整个仓库的构建,再启动源码 CLI(命令行界面)。它会丢弃成功构建的输出,使 CLI stdout 保持机器可读;在 stderr 中报告构建失败的诊断信息;并为 CLI 进程设置 `NODE_USE_ENV_PROXY=1`。用户使用 `pnpm dsh web` 选择 Web,使用 `pnpm dsh --profile headless "task"` 选择无头执行。独立的 ACPAgent Client Protocol)示例仍可通过 `pnpm run demo:acp` 运行。 仓库通过根目录的 `pnpm` 脚本支持从源码运行。`package.json` 中的 `dsh` 项先执行 `pnpm run build`,再通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`;构建输出会显示在 CLI(命令行界面)输出之前。该包脚本会转发参数并继承调用方环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY``HTTPS_PROXY` 时,调用方可设置 `NODE_USE_ENV_PROXY=1`。用户使用 `pnpm dsh web` 选择 Web,使用 `pnpm dsh --profile headless "task"` 选择无头执行。独立的 ACPAgent Client Protocol)示例仍可通过 `pnpm run demo:acp` 运行。
仓库不分发源码安装器、安装器测试套件,也不分发依赖受管理的 `current` 符号链接和带时间戳 staging worktree 的 skill。源码检出的存放位置、Git 更新,以及用户在仓库外创建的任何启动器均由用户负责。 仓库不分发源码安装器、安装器测试套件,也不分发依赖受管理的 `current` 符号链接和带时间戳 staging worktree 的 skill。源码检出的存放位置、Git 更新,以及用户在仓库外创建的任何启动器均由用户负责。
@@ -28,4 +28,4 @@ Status: implemented
源码用户通过仓库脚本运行程序,而非使用已安装的 `dsh` 命令。仓库不提供原子升级切换,也不保留 staging 回滚检出;仓库同样不会自动集成个人源码修改或将其发布到上游。未来的分发机制必须说明为何应由其管理安装和升级状态,定义恢复行为,并补充测试与用户文档,同时不得让源码运行路径依赖该机制。未来任何发布工作流都必须隔离出一项获批功能,并在首次推送和创建草稿 PR(Pull Request)前取得明确批准。 源码用户通过仓库脚本运行程序,而非使用已安装的 `dsh` 命令。仓库不提供原子升级切换,也不保留 staging 回滚检出;仓库同样不会自动集成个人源码修改或将其发布到上游。未来的分发机制必须说明为何应由其管理安装和升级状态,定义恢复行为,并补充测试与用户文档,同时不得让源码运行路径依赖该机制。未来任何发布工作流都必须隔离出一项获批功能,并在首次推送和创建草稿 PR(Pull Request)前取得明确批准。
验证范围包括仓库内对已移除入口点的所有引用、文档链接、生成的第三方声明文件的新鲜度,以及通过 `pnpm dsh` 对源码 CLI 进行冒烟测试,验证先构建后启动且 stdout 不含构建日志 验证范围包括仓库内对已移除入口点的所有引用、文档链接、生成的第三方声明文件的新鲜度`package.json` 中的先构建后启动命令,以及通过准确的 `node --import tsx/esm` 运行方式对源码 CLI 进行冒烟测试。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md # pnpm run verify-translation-pairing --write apps/cli/reference/README.md
README.md: 48c8e8fd902db4de5ceb5532a67d1559453392c4 README.md: cef687dc392f97886ea18b162e8f668a44ce2284
README.zh.md: d51b4256b421ecfb71d294c5b3c25bfcf6ed2c8a README.zh.md: f6721ec256d404e2b602fb2ed921b41c03633a82
+1 -1
View File
@@ -81,4 +81,4 @@ Install external plugin bundles through `dsh plugin --profile <name> add <packag
## Source execution ## Source execution
From the repository root, use `pnpm dsh <args...>`. The script runs the complete repository build, launches `apps/cli/src/bin.ts` with `node --import tsx/esm`, and forwards every argument. Successful build output stays out of CLI stdout, while build failures report diagnostics on stderr. The CLI process receives `NODE_USE_ENV_PROXY=1` so Node versions with environment-proxy fetch support honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository. 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.
+1 -1
View File
@@ -81,4 +81,4 @@ dsh web --help
## 源码执行 ## 源码执行
请从仓库根目录使用 `pnpm dsh <args...>`脚本会完成整个仓库的构建,通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。构建成功时,其输出不会进入 CLI stdout;构建失败时,诊断信息会写入 stderr。CLI 进程会接收 `NODE_USE_ENV_PROXY=1`,从而让支持环境代理的 Node 版本中的 fetch 遵循 `HTTP_PROXY``HTTPS_PROXY`。安装形式会直接启动构建后的 `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`,不会重新构建仓库。
+5 -80
View File
@@ -1,7 +1,4 @@
import { existsSync } from 'node:fs' import { readFile } from 'node:fs/promises'
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { delimiter, join } from 'node:path'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { execa } from 'execa' import { execa } from 'execa'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
@@ -19,86 +16,14 @@ import { describe, expect, it } from 'vitest'
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)) const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const dshSourceBin = 'apps/cli/src/bin.ts' const dshSourceBin = 'apps/cli/src/bin.ts'
function resolvePnpmExecutable(): string {
const pathKey = Object.keys(process.env).find(key => key.toUpperCase() === 'PATH') ?? 'PATH'
const executableNames = process.platform === 'win32' ? ['pnpm.cmd', 'pnpm.exe', 'pnpm'] : ['pnpm']
for (const directory of (process.env[pathKey] ?? '').split(delimiter)) {
for (const executableName of executableNames) {
const candidate = join(directory, executableName)
if (existsSync(candidate)) return candidate
}
}
throw new Error('pnpm executable is absent from PATH')
}
async function createPnpmStub(emitBuildOutput = false): Promise<{
readonly directory: string
readonly env: NodeJS.ProcessEnv
readonly invocationLog: string
}> {
const directory = await mkdtemp(join(tmpdir(), 'dsh-source-launch-'))
const invocationLog = join(directory, 'pnpm-args.json')
const simulatedOutput = emitBuildOutput
? "process.stdout.write('tsdown simulated build\\n')\nprocess.stderr.write('build:lib simulated build\\n')\n"
: ''
const stubBody = `const { appendFileSync } = require('node:fs')\nappendFileSync(process.env.DSH_TEST_PNPM_LOG, JSON.stringify(process.argv.slice(2)))\n${simulatedOutput}`
await writeFile(join(directory, 'pnpm'), `#!/usr/bin/env node\n${stubBody}`)
await chmod(join(directory, 'pnpm'), 0o755)
await writeFile(join(directory, 'pnpm-stub.cjs'), stubBody)
await writeFile(join(directory, 'pnpm.cmd'), '@echo off\r\nnode "%~dp0pnpm-stub.cjs" %*\r\n')
const pathKey = Object.keys(process.env).find(key => key.toUpperCase() === 'PATH') ?? 'PATH'
return {
directory,
invocationLog,
env: {
...process.env,
[pathKey]: `${directory}${delimiter}${process.env[pathKey] ?? ''}`,
DSH_TEST_PNPM_LOG: invocationLog,
},
}
}
describe('dsh SOURCE launcher (node --import tsx/esm)', () => { describe('dsh SOURCE launcher (node --import tsx/esm)', () => {
it('runs the repository build before launching the source CLI', async () => { it('builds before launching the source CLI', async () => {
const stub = await createPnpmStub() const rootPackage = JSON.parse(await readFile(new URL('../../../package.json', import.meta.url), 'utf8')) as {
try { readonly scripts?: Record<string, string>
const result = await execa(process.execPath, ['--import', 'tsx/esm', 'scripts/run-source-dsh.ts', '--help'], {
cwd: repoRoot,
env: stub.env,
reject: false,
})
expect(result.exitCode).toBe(0)
expect(result.stdout).toMatch(/^Usage: dsh /)
await expect(readFile(stub.invocationLog, 'utf8')).resolves.toBe('["run","build"]')
} finally {
await rm(stub.directory, { recursive: true, force: true })
} }
expect(rootPackage.scripts?.dsh).toBe('pnpm run build && node --import tsx/esm apps/cli/src/bin.ts')
}) })
it('builds without mixing build logs into CLI stdout', async () => {
const pnpmExecutable = resolvePnpmExecutable()
const stub = await createPnpmStub(true)
try {
const result = await execa(pnpmExecutable, ['dsh', '--help'], {
cwd: repoRoot,
env: stub.env,
timeout: 25_000,
killSignal: 'SIGKILL',
reject: false,
})
if (result.timedOut) {
throw new Error(`pnpm dsh --help did not exit within 25s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
}
expect(result.exitCode).toBe(0)
expect(result.stdout).toMatch(/^Usage: dsh /)
expect(result.stdout).not.toContain('tsdown')
expect(result.stdout).not.toContain('build:lib')
await expect(readFile(stub.invocationLog, 'utf8')).resolves.toBe('["run","build"]')
} finally {
await rm(stub.directory, { recursive: true, force: true })
}
}, 30_000)
it('boots the source entry and requires a profile', async () => { it('boots the source entry and requires a profile', async () => {
const result = await execa(process.execPath, ['--import', 'tsx/esm', dshSourceBin], { const result = await execa(process.execPath, ['--import', 'tsx/esm', dshSourceBin], {
cwd: repoRoot, cwd: repoRoot,
+1 -1
View File
@@ -127,7 +127,7 @@
"release:pack": "tsx scripts/release/pack.ts", "release:pack": "tsx scripts/release/pack.ts",
"release:verify-packed-install": "tsx scripts/release/verify-packed-install.ts", "release:verify-packed-install": "tsx scripts/release/verify-packed-install.ts",
"release:publish": "tsx scripts/release/publish.ts", "release:publish": "tsx scripts/release/publish.ts",
"dsh": "tsx scripts/run-source-dsh.ts", "dsh": "pnpm run build && node --import tsx/esm apps/cli/src/bin.ts",
"demo:code-mode": "node scripts/demo-code-mode.mjs", "demo:code-mode": "node scripts/demo-code-mode.mjs",
"demo:cordis": "node scripts/demo-cordis.mjs", "demo:cordis": "node scripts/demo-cordis.mjs",
"demo:acp": "node --import tsx packages/examples/acp-demo/src/bin.ts --config examples/acp-agent/cordis.yml", "demo:acp": "node --import tsx packages/examples/acp-demo/src/bin.ts --config examples/acp-agent/cordis.yml",
-48
View File
@@ -1,48 +0,0 @@
/**
* Source-checkout launcher. Successful build output stays out of CLI stdout;
* build failures report their captured diagnostics on stderr before exiting.
*/
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { execa } from 'execa'
const repoRoot = fileURLToPath(new URL('..', import.meta.url))
const sourceBin = fileURLToPath(new URL('../apps/cli/src/bin.ts', import.meta.url))
function completeFrom(result: { readonly exitCode?: number; readonly signal?: string }): void {
if (result.signal !== undefined) {
process.kill(process.pid, result.signal)
return
}
process.exitCode = result.exitCode ?? 1
}
function reportBuildFailure(result: {
readonly all: string | undefined
readonly shortMessage: string | undefined
}): void {
const diagnostic = result.all === undefined || result.all.length === 0
? result.shortMessage ?? 'Source build failed without diagnostics.'
: result.all
process.stderr.write(diagnostic.endsWith('\n') ? diagnostic : `${diagnostic}\n`)
}
const build = await execa('pnpm', ['run', 'build'], {
all: true,
cwd: repoRoot,
reject: false,
stripFinalNewline: false,
})
if (build.failed) {
reportBuildFailure(build)
completeFrom(build)
} else {
const cli = await execa(process.execPath, ['--import', 'tsx/esm', sourceBin, ...process.argv.slice(2)], {
cwd: repoRoot,
env: { ...process.env, NODE_USE_ENV_PROXY: '1' },
reject: false,
stdio: 'inherit',
})
completeFrom(cli)
}