diff --git a/apps/desktop-host/src/index.ts b/apps/desktop-host/src/index.ts index 3f40918149..e9e4e13b29 100644 --- a/apps/desktop-host/src/index.ts +++ b/apps/desktop-host/src/index.ts @@ -26,7 +26,7 @@ async function main(): Promise { profile: 'desktop', resolvedProfile: { profile, installAnchor }, patchFiles: [], - args: ['--no-open', '--port', process.env.DSH_DESKTOP_WEB_PORT ?? '19387'], + args: ['--no-open', '--port', '19387'], ...(process.argv[5] === undefined ? {} : { packageManager: { command: process.execPath, diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 28c136006e..039d4701b1 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -21,8 +21,8 @@ en: 0d11af8639977a6d zh: 86b086b12d2210d3 /deepseek-harness-desktop/develop: - en: 283cda577d788f8c - zh: 43011655030a5784 + en: 9b9b362a9865e763 + zh: c748901168e2db8c /deepseek-harness-desktop/develop/startup-onboarding: en: 5e2d62207a603d81 zh: 5fb863d4906f4a67 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index b7f411d63e..0f579c249b 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -120,11 +120,7 @@ The development application menu offers Reload Page (Cmd+R on macOS, Ctrl+R else pnpm run dev:desktop ``` -On macOS, double-click [`start-desktop.command`](../../start-desktop.command) in the repository root to run the same build-and-launch path in Terminal. The launcher loads the user's zsh startup files and uses the existing `DSH_HOME` or `~/.dsh`, so Desktop reuses the user's sessions, settings, and credentials. It uses the workspace pnpm when installed, keeps Renderer DevTools closed by default, and waits for Enter after a failure so the diagnostic remains visible. - -To run a second Desktop from a different checkout, double-click [`启动Desktop.command`](../../启动Desktop.command). It selects free ports after the default Desktop ports, sets `DSH_DESKTOP_WEB_PORT`, uses an isolated `$DSH_HOME` under `~/.dsh-desktop--`, and does not reuse the other checkout’s state or ports. Each checkout has one development bundle and Electron user-data directory; concurrent launches from the same checkout are unsupported. When its primary runtime is missing, it probes local proxy ports 7890, 7897, 1087, and 8080 for the preparation process, with loopback excluded from proxying. Both launchers clear inherited proxy variables before starting Desktop. - -Development Harness state defaults to `apps/desktop/.desktop-build/development/home`, the disposable npm project lives at `apps/desktop/.desktop-build/development/project`, and Electron browser data lives at `apps/desktop/.desktop-build/development/electron-user-data`. Sessions, settings, credentials, package links, and browser data therefore stay out of the user's normal Harness home. An explicit `DSH_HOME` replaces only the development Harness home. The Desktop Web Host listens on port 19387 by default; `DSH_DESKTOP_WEB_PORT` replaces that port. Development launchers require an integer from 1 through 65535 and preserve it in the macOS bundle for Launch Services cold starts. Renderer DevTools opens automatically; Main, Renderer, and dsh Host debugging listen on ports 9229, 9222, and 9230. `DSH_DESKTOP_MAIN_INSPECT_PORT`, `DSH_DESKTOP_RENDERER_DEBUG_PORT`, and `DSH_DESKTOP_HOST_INSPECT_PORT` replace those ports, while `DSH_DESKTOP_OPEN_DEVTOOLS=0` keeps the detached Renderer tools closed. +Development Harness state defaults to `apps/desktop/.desktop-build/development/home`, the disposable npm project lives at `apps/desktop/.desktop-build/development/project`, and Electron browser data lives at `apps/desktop/.desktop-build/development/electron-user-data`. Sessions, settings, credentials, package links, and browser data therefore stay out of the user's normal Harness home. An explicit `DSH_HOME` replaces only the development Harness home. Renderer DevTools opens automatically; Main, Renderer, and dsh Host debugging listen on ports 9229, 9222, and 9230. `DSH_DESKTOP_MAIN_INSPECT_PORT`, `DSH_DESKTOP_RENDERER_DEBUG_PORT`, and `DSH_DESKTOP_HOST_INSPECT_PORT` replace those ports, while `DSH_DESKTOP_OPEN_DEVTOOLS=0` keeps the detached Renderer tools closed. After an explicit build, `start:desktop` reconstructs the disposable project and launches the existing artifacts without building again: diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index e5c4707f1e..62e34fad48 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -122,11 +122,7 @@ macOS 上自定义菜单保留 Electron 的标准 Window 菜单及应用隐藏 pnpm run dev:desktop ``` -在 macOS 上,双击仓库根目录中的 [`start-desktop.command`](../../start-desktop.command),即可在终端中执行相同的构建和启动流程。该启动器会加载用户的 zsh 启动文件,并使用已有的 `DSH_HOME` 或 `~/.dsh`,因此 Desktop 会复用用户的会话、设置和凭据。依赖已安装时,启动器使用工作区 pnpm;它默认不打开 Renderer DevTools,并在失败后等待按下回车键,以便保留诊断信息。 - -要从不同 checkout 启动第二个 Desktop 实例,请双击 [`启动Desktop.command`](../../启动Desktop.command)。该脚本从默认 Desktop 端口之后选择空闲端口,设置 `DSH_DESKTOP_WEB_PORT`,并使用 `~/.dsh-desktop-<仓库>-<端口>` 下的独立 `$DSH_HOME`,不会复用另一 checkout 的状态和端口。每个 checkout 共用一个开发应用包和 Electron 用户数据目录,不支持从同一 checkout 并发启动。主运行时缺失时,它探测本地代理端口 7890、7897、1087 和 8080,仅供准备进程使用,并将本地回环地址排除在代理之外。两个启动脚本都会在启动 Desktop 前清除继承的代理变量。 - -开发 Harness 状态默认写入 `apps/desktop/.desktop-build/development/home`,一次性 npm 项目位于 `apps/desktop/.desktop-build/development/project`,Electron 浏览器数据则位于 `apps/desktop/.desktop-build/development/electron-user-data`。因此,会话、设置、凭据、包链接和浏览器数据都不会进入用户正常使用的 Harness home;显式 `DSH_HOME` 只会替换开发 Harness home。Desktop Web Host 默认监听 19387 端口;`DSH_DESKTOP_WEB_PORT` 可以替换该端口。开发启动器要求端口为 1 至 65535 的整数,并将其保存在 macOS 应用包中,供 Launch Services 冷启动使用。Renderer DevTools 默认自动打开,Main、Renderer 和 dsh Host 调试端口依次为 9229、9222 和 9230。`DSH_DESKTOP_MAIN_INSPECT_PORT`、`DSH_DESKTOP_RENDERER_DEBUG_PORT` 与 `DSH_DESKTOP_HOST_INSPECT_PORT` 可以替换这些端口,`DSH_DESKTOP_OPEN_DEVTOOLS=0` 则保持 Renderer 调试窗口关闭。 +开发 Harness 状态默认写入 `apps/desktop/.desktop-build/development/home`,一次性 npm 项目位于 `apps/desktop/.desktop-build/development/project`,Electron 浏览器数据则位于 `apps/desktop/.desktop-build/development/electron-user-data`。因此,会话、设置、凭据、包链接和浏览器数据都不会进入用户正常使用的 Harness home;显式 `DSH_HOME` 只会替换开发 Harness home。Renderer DevTools 默认自动打开,Main、Renderer 和 dsh Host 调试端口依次为 9229、9222 和 9230。`DSH_DESKTOP_MAIN_INSPECT_PORT`、`DSH_DESKTOP_RENDERER_DEBUG_PORT` 与 `DSH_DESKTOP_HOST_INSPECT_PORT` 可以替换这些端口,`DSH_DESKTOP_OPEN_DEVTOOLS=0` 则保持 Renderer 调试窗口关闭。 显式构建完成后,`start:desktop` 会重新生成一次性项目,并跳过构建直接启动已有产物: diff --git a/apps/desktop/scripts/dev.ts b/apps/desktop/scripts/dev.ts index 4f6f4338ad..1ae2f3e952 100644 --- a/apps/desktop/scripts/dev.ts +++ b/apps/desktop/scripts/dev.ts @@ -27,7 +27,7 @@ function packageVersion(path: string, subject: string): string { return manifest.version } -function developmentPort(name: string, fallback: number): number { +function debugPort(name: string, fallback: number): number { const value = process.env[name] if (value === undefined || value === '') return fallback const port = Number(value) @@ -56,13 +56,13 @@ async function runPackageScript(script: string, cwd: string): Promise { await run(process.execPath, [packageManager, 'run', script], cwd) } -async function launchElectron(webPort: number): Promise { +async function launchElectron(): Promise { const require = createRequire(import.meta.url) const electron: unknown = require('electron') if (typeof electron !== 'string') throw new Error('desktop development: electron executable is unavailable') - const mainPort = developmentPort('DSH_DESKTOP_MAIN_INSPECT_PORT', 9229) - const rendererPort = developmentPort('DSH_DESKTOP_RENDERER_DEBUG_PORT', 9222) - const hostPort = developmentPort('DSH_DESKTOP_HOST_INSPECT_PORT', 9230) + const mainPort = debugPort('DSH_DESKTOP_MAIN_INSPECT_PORT', 9229) + const rendererPort = debugPort('DSH_DESKTOP_RENDERER_DEBUG_PORT', 9222) + const hostPort = debugPort('DSH_DESKTOP_HOST_INSPECT_PORT', 9230) const home = resolve(process.env.DSH_HOME ?? join(DEVELOPMENT_ROOT, 'home')) const userData = resolve(process.env.DSH_DESKTOP_USER_DATA_DIR ?? join(DEVELOPMENT_ROOT, 'electron-user-data')) const environment: NodeJS.ProcessEnv = { @@ -70,7 +70,6 @@ async function launchElectron(webPort: number): Promise { DSH_HOME: home, DSH_DESKTOP_PRIMARY_RUNTIME_DIR: process.env.DSH_DESKTOP_PRIMARY_RUNTIME_DIR ?? developmentRuntimeDirectory(), DSH_DESKTOP_HOST_INSPECT_PORT: String(hostPort), - DSH_DESKTOP_WEB_PORT: String(webPort), DSH_DESKTOP_OPEN_DEVTOOLS: process.env.DSH_DESKTOP_OPEN_DEVTOOLS ?? '1', ELECTRON_ENABLE_LOGGING: process.env.ELECTRON_ENABLE_LOGGING ?? '1', } @@ -79,7 +78,7 @@ async function launchElectron(webPort: number): Promise { console.log(`desktop development: inspectors main=${String(mainPort)}, renderer=${String(rendererPort)}, host=${String(hostPort)}`) if (process.platform === 'darwin') { const executable = prepareDevelopmentApp({ electron, appRoot: APP_ROOT, directory: DEVELOPMENT_ROOT, home, userData, - mainPort, rendererPort, hostPort, webPort, openDevtools: environment.DSH_DESKTOP_OPEN_DEVTOOLS! }) + mainPort, rendererPort, hostPort, openDevtools: environment.DSH_DESKTOP_OPEN_DEVTOOLS! }) await run(executable, [], APP_ROOT, environment) return } @@ -93,7 +92,6 @@ async function launchElectron(webPort: number): Promise { async function main(): Promise { const { values } = parseArgs({ options: { 'skip-build': { type: 'boolean', default: false } } }) - const webPort = developmentPort('DSH_DESKTOP_WEB_PORT', 19387) if (!values['skip-build']) { await runPackageScript('build', REPOSITORY_ROOT) await runPackageScript('build', APP_ROOT) @@ -123,7 +121,7 @@ async function main(): Promise { target: resolveDesktopBuildTarget(), }) await preparePrimaryRuntime() - await launchElectron(webPort) + await launchElectron() } await main().catch((error: unknown) => { diff --git a/apps/desktop/scripts/development-app.ts b/apps/desktop/scripts/development-app.ts index 1361715c26..ff09b8b783 100644 --- a/apps/desktop/scripts/development-app.ts +++ b/apps/desktop/scripts/development-app.ts @@ -14,7 +14,6 @@ export interface DevelopmentAppOptions { readonly mainPort: number readonly rendererPort: number readonly hostPort: number - readonly webPort: number readonly openDevtools: string } @@ -65,7 +64,6 @@ export function developmentLauncher(options: DevelopmentAppOptions, bundle: stri DSH_HOME: options.home, DSH_DESKTOP_DEV_APP: '1', DSH_DESKTOP_HOST_INSPECT_PORT: String(options.hostPort), - DSH_DESKTOP_WEB_PORT: String(options.webPort), DSH_DESKTOP_OPEN_DEVTOOLS: options.openDevtools, ELECTRON_ENABLE_LOGGING: '1', } diff --git a/apps/desktop/tests/development-app.spec.ts b/apps/desktop/tests/development-app.spec.ts index 092a3d17f0..2f718b7bc5 100644 --- a/apps/desktop/tests/development-app.spec.ts +++ b/apps/desktop/tests/development-app.spec.ts @@ -1,38 +1,23 @@ /** Cold-start launcher preserves workspace paths without evaluating shell syntax. */ -import { execFile, execFileSync } from 'node:child_process' +import { execFileSync } from 'node:child_process' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { promisify } from 'node:util' import { expect, it, onTestFinished } from 'vitest' import { developmentLauncher } from '../scripts/development-app.ts' -it.each(['abc', '1.5', '0', '65536'])('rejects Web port %s before starting a build', async (port) => { - const launch = promisify(execFile)(process.execPath, - ['--import', 'tsx', fileURLToPath(new URL('../scripts/dev.ts', import.meta.url))], - { env: { ...process.env, DSH_DESKTOP_WEB_PORT: port, npm_execpath: '' } }, - ) - await expect(launch).rejects.toMatchObject({ - code: 1, - signal: null, - killed: false, - }) - await expect(launch).rejects.toThrow('DSH_DESKTOP_WEB_PORT must be an integer from 1 through 65535') -}) - it.skipIf(process.platform === 'win32')('passes literal workspace paths and cold-start settings to Electron', () => { const root = mkdtempSync(join(tmpdir(), 'dsh-development-app-')) onTestFinished(() => { rmSync(root, { recursive: true, force: true }) }) const bundle = join(root, "Harness ' $(false).app") const binary = join(bundle, 'Contents', 'MacOS', 'Electron') mkdirSync(join(bundle, 'Contents', 'MacOS'), { recursive: true }) - writeFileSync(binary, '#!/bin/sh\nprintf "%s\\n" "$DSH_HOME" "$DSH_DESKTOP_DEV_APP" "$DSH_DESKTOP_OPEN_DEVTOOLS" "$DSH_DESKTOP_WEB_PORT" "$@"\n', { mode: 0o755 }) + writeFileSync(binary, '#!/bin/sh\nprintf "%s\\n" "$DSH_HOME" "$DSH_DESKTOP_DEV_APP" "$DSH_DESKTOP_OPEN_DEVTOOLS" "$@"\n', { mode: 0o755 }) const launcher = join(root, 'launcher') const home = join(root, "home ' $(false)") writeFileSync(launcher, developmentLauncher({ electron: binary, appRoot: root, directory: root, - home, userData: join(root, 'browser data'), mainPort: 9229, rendererPort: 9222, hostPort: 9230, webPort: 19401, openDevtools: '0' }, bundle)) + home, userData: join(root, 'browser data'), mainPort: 9229, rendererPort: 9222, hostPort: 9230, openDevtools: '0' }, bundle)) const result = execFileSync('/bin/sh', [launcher, '--test-launch-argument'], { encoding: 'utf8' }) - expect(result.trimEnd().split('\n')).toEqual([home, '1', '0', '19401', '--inspect=127.0.0.1:9229', + expect(result.trimEnd().split('\n')).toEqual([home, '1', '0', '--inspect=127.0.0.1:9229', '--remote-debugging-port=9222', `--user-data-dir=${join(root, 'browser data')}`, root, '--test-launch-argument']) }) diff --git a/start-desktop.command b/start-desktop.command deleted file mode 100755 index 30b5612a74..0000000000 --- a/start-desktop.command +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/zsh - -readonly launcher_path="${0:A}" -readonly repository_root="${launcher_path:h}" - -exec /bin/zsh -ilc ' - set -u - - readonly repository_root="$1" - typeset -a pnpm_command - typeset -i exit_code=0 - - if ! cd -- "$repository_root"; then - exit_code=1 - else - readonly local_pnpm="$repository_root/apps/desktop/node_modules/pnpm/bin/pnpm.mjs" - readonly node_path="${commands[node]-}" - readonly pnpm_path="${commands[pnpm]-}" - readonly corepack_path="${commands[corepack]-}" - - if [[ -n "$node_path" && -f "$local_pnpm" ]]; then - pnpm_command=("$node_path" "$local_pnpm") - elif [[ -n "$pnpm_path" ]]; then - pnpm_command=("$pnpm_path") - elif [[ -n "$corepack_path" ]]; then - pnpm_command=("$corepack_path" pnpm) - else - print -u2 "未找到 Node.js/pnpm。请先安装仓库要求的 Node.js,再运行 pnpm install。" - exit_code=127 - fi - - if (( exit_code == 0 )); then - export DSH_HOME="${DSH_HOME:-$HOME/.dsh}" - export DSH_DESKTOP_OPEN_DEVTOOLS="${DSH_DESKTOP_OPEN_DEVTOOLS:-0}" - unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY NODE_USE_ENV_PROXY - "${pnpm_command[@]}" run dev:desktop - exit_code=$? - fi - fi - - if (( exit_code != 0 )); then - print -u2 - print -u2 "桌面端启动失败(退出码 $exit_code)。" - read -r "?按回车键关闭窗口。" - fi - - exit "$exit_code" -' start-desktop "$repository_root" diff --git a/启动Desktop.command b/启动Desktop.command deleted file mode 100755 index f5e71a0018..0000000000 --- a/启动Desktop.command +++ /dev/null @@ -1,166 +0,0 @@ -#!/bin/zsh -# 启动当前仓库的第二个 Desktop 开发实例。 -# -# 本脚本为 Web Host、Electron 调试接口和 Harness home 选择独立值,因此可以 -# 与另一个仓库中已经运行的 Desktop 同时启动。运行期间请保持终端窗口打开; -# 按 Ctrl-C 结束本次开发运行。 - -set -u - -readonly launcher_path="${0:A}" -readonly repository_root="${launcher_path:h}" - -if [[ ! -f "$repository_root/package.json" ]] || ! grep -q '"dev:desktop"' "$repository_root/package.json"; then - print -u2 "找不到 Desktop 仓库:$repository_root" - print -u2 '请把本文件放在仓库根目录后重试。按回车键关闭窗口。' - read -r - exit 1 -fi -if ! cd -- "$repository_root"; then - print -u2 "无法进入仓库目录:$repository_root" - read -r - exit 1 -fi - -# Finder 启动的是登录 shell;这里仍然显式补上常见工具链路径。 -for directory in /opt/homebrew/bin /usr/local/bin "$HOME/.local/share/pnpm"; do - case ":$PATH:" in - *":$directory:"*) ;; - *) [[ -d "$directory" ]] && PATH="$directory:$PATH" ;; - esac -done -export PATH - -typeset -a pnpm_command -readonly local_pnpm="$repository_root/apps/desktop/node_modules/pnpm/bin/pnpm.mjs" -readonly node_path="${commands[node]-}" -readonly pnpm_path="${commands[pnpm]-}" -readonly corepack_path="${commands[corepack]-}" -if [[ -n "$node_path" && -f "$local_pnpm" ]]; then - pnpm_command=("$node_path" "$local_pnpm") -elif [[ -n "$pnpm_path" ]]; then - pnpm_command=("$pnpm_path") -elif [[ -n "$corepack_path" ]]; then - pnpm_command=("$corepack_path" pnpm) -else - print -u2 '未找到 Node.js/pnpm。请先安装仓库要求的 Node.js,再运行 pnpm install。按回车键关闭窗口。' - read -r - exit 127 -fi - -if [[ ! -d node_modules/.pnpm ]]; then - print '依赖尚未安装,先执行 pnpm install ...' - if ! "${pnpm_command[@]}" install; then - print - print -u2 'pnpm install 失败。按回车键关闭窗口。' - read -r - exit 1 - fi -fi - -if (( ! $+commands[lsof] )); then - print -u2 '找不到 lsof,无法检查 Desktop 端口冲突。按回车键关闭窗口。' - read -r - exit 1 -fi - -typeset -a selected_ports -selected_ports=() -port_already_selected() { - local port="$1" - local selected - for selected in "${selected_ports[@]}"; do - [[ "$selected" == "$port" ]] && return 0 - done - return 1 -} -port_is_busy() { - lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1 -} -next_free_port() { - integer candidate="$1" - while port_is_busy "$candidate" || port_already_selected "$candidate"; do - (( candidate += 1 )) - done - print -r -- "$candidate" -} - -# The first Desktop normally uses 19387, 9229, 9222 and 9230. Start searching -# after those values so this instance also avoids another already-isolated run. -readonly web_port="$(next_free_port 19389)" -selected_ports+=("$web_port") -readonly main_inspect_port="$(next_free_port 9329)" -selected_ports+=("$main_inspect_port") -readonly renderer_debug_port="$(next_free_port 9322)" -selected_ports+=("$renderer_debug_port") -readonly host_inspect_port="$(next_free_port 9330)" - -readonly repository_name="${repository_root:t}" -readonly default_home="$HOME/.dsh-desktop-${repository_name}-${web_port}" -export DSH_HOME="${DSH_DESKTOP_SECOND_HOME:-$default_home}" -export DSH_DESKTOP_WEB_PORT="$web_port" -export DSH_DESKTOP_MAIN_INSPECT_PORT="$main_inspect_port" -export DSH_DESKTOP_RENDERER_DEBUG_PORT="$renderer_debug_port" -export DSH_DESKTOP_HOST_INSPECT_PORT="$host_inspect_port" -export DSH_DESKTOP_OPEN_DEVTOOLS="${DSH_DESKTOP_OPEN_DEVTOOLS:-0}" - -# The preparation process alone may use a verified local proxy. -typeset primary_manifest -if ! primary_manifest="$("$node_path" --input-type=module -e ' - import { resolveDesktopTargetBuildPaths } from "./apps/desktop/scripts/desktop-build-paths.mjs"; - console.log(resolveDesktopTargetBuildPaths().runtime + "/primary-runtime/runtime.json"); -')"; then - print -u2 '无法确定 Desktop 运行时目录。' - exit 1 -fi -if [[ ! -f "$primary_manifest" ]]; then - ( - unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY NODE_USE_ENV_PROXY - export no_proxy=127.0.0.1,localhost,::1 - export NO_PROXY="$no_proxy" - if (( $+commands[nc] )); then - for proxy_port in 7890 7897 1087 8080; do - if nc -G 2 -z 127.0.0.1 "$proxy_port" >/dev/null 2>&1; then - export http_proxy="http://127.0.0.1:$proxy_port" - export https_proxy="$http_proxy" - export NODE_USE_ENV_PROXY=1 - break - fi - done - fi - "${pnpm_command[@]}" --dir apps/desktop run prepare:primary-runtime - ) - if (( $? != 0 )); then - print -u2 'Desktop 运行时准备失败。按回车键关闭窗口。' - read -r - exit 1 - fi -fi -unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY NODE_USE_ENV_PROXY - -# 分支切换可能留下指向已删除 workspace 包的悬空链接;它们会阻塞 Desktop 项目投影。 -if [[ -d node_modules/.pnpm/node_modules ]]; then - while IFS= read -r stale_link; do - [[ -n "$stale_link" ]] || continue - print "清理悬空 workspace 链接:$stale_link -> $(readlink "$stale_link")" - rm -- "$stale_link" - done < <(find node_modules/.pnpm/node_modules -type l ! -exec test -e {} \; -print 2>/dev/null) -fi - -printf '\033]0;DSH Desktop 开发模式(端口 %s)\007' "$web_port" -print 'DSH Desktop 开发模式(隔离实例)' -print "仓库:$repository_root" -print "Harness home:$DSH_HOME" -print "Web Host:127.0.0.1:$web_port" -print "调试端口:Main=$main_inspect_port Renderer=$renderer_debug_port Host=$host_inspect_port" -print - -"${pnpm_command[@]}" run dev:desktop -readonly exit_code=$? -if (( exit_code != 0 )); then - print - print -u2 "dev:desktop 以状态码 $exit_code 退出,保留窗口以便查看日志。" - print -u2 '按回车键关闭窗口。' - read -r -fi -exit "$exit_code"