Prechádzať zdrojové kódy

Merge pull request #1085 from deepseek-harness/worktree/install-interface-choice

feat(install): choose Web UI or TUI after setup
Tianyi Cui 1 mesiac pred
rodič
commit
c3463ffd62

+ 2 - 2
README.i18n.yaml

@@ -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 README.md
-README.md: fb956dce51838438fb508db7ea9ebdf9e0b3a50b
-README.zh.md: aa80b744465d7d253a54fffeead7262a1fdf69eb
+README.md: 8ecd0928ee630eeca1cb8ce8b9c59d19f6984969
+README.zh.md: 9ffb3b3086415550df4a0f776c7b91c94122dd97

+ 3 - 3
README.md

@@ -24,7 +24,7 @@ cd deepseek-harness
 scripts/install.sh
 ```
 
-The installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, and prompts for a DeepSeek API key.
+The installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, prompts for a DeepSeek API key, then lets you launch the Web UI or TUI. Choosing Web UI builds the required repository artifacts first.
 
 The installer keeps every checkout under `~/.dsh/source`: the master clone at `~/.dsh/source/master` and each install's staging checkout as a git worktree `~/.dsh/source/staging-<timestamp>`. The stable symlink `~/.dsh/source/current` points at the active staging worktree, and `dsh` in `~/.local/bin` links to `current/bin/dsh`, so an upgrade repoints one symlink and the `dsh` on PATH never moves. Re-running the command adds a fresh staging worktree from an updated master and repoints `current` at it. See [`scripts/install.sh`](scripts/install.sh) for alternate install locations and other options.
 
@@ -32,14 +32,14 @@ The installer keeps every checkout under `~/.dsh/source`: the master clone at `~
 
 ### Web UI
 
-For the recommended local interface, build the active checkout after installation and after each update, then start the Web UI:
+For the recommended local interface, choose Web UI when the installer finishes. To start it later, or after updating the active checkout, build the repository and run:
 
 ```sh
 (cd ~/.dsh/source/current && pnpm run build)
 dsh web
 ```
 
-The full build produces the library and client bundles plus the frontend dist. The path above is the installer's default. If you set `DSH_SOURCE` or `DSH_CURRENT`, or reused an existing checkout, replace `~/.dsh/source/current` with that checkout path; see [`scripts/install.sh`](scripts/install.sh) for details. The Web UI is served at `http://127.0.0.1:3080` by default.
+The path above is the installer's default. If you set `DSH_SOURCE` or `DSH_CURRENT`, or reused an existing checkout, replace `~/.dsh/source/current` with that checkout path; see [`scripts/install.sh`](scripts/install.sh) for details. The Web UI is served at `http://127.0.0.1:3080` by default.
 
 ### TUI
 

+ 3 - 3
README.zh.md

@@ -24,7 +24,7 @@ cd deepseek-harness
 scripts/install.sh
 ```
 
-安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥。
+安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥,随后让你选择启动 Web UI 或 TUI。选择 Web UI 时,安装器会先构建所需的仓库产物。
 
 安装器会把所有检出都放在 `~/.dsh/source` 下:master 克隆位于 `~/.dsh/source/master`,每次安装的 staging 检出是一个 git worktree `~/.dsh/source/staging-<时间戳>`。稳定符号链接 `~/.dsh/source/current` 指向当前生效的 staging worktree,`~/.local/bin` 中的 `dsh` 链接到 `current/bin/dsh`,因此升级只需重指一个符号链接,PATH 上的 `dsh` 从不移动。再次运行该命令会基于更新后的 master 新增一个 staging worktree,并把 `current` 重指到它。其他安装位置和选项见 [`scripts/install.sh`](scripts/install.sh)。
 
@@ -32,14 +32,14 @@ scripts/install.sh
 
 ### Web UI
 
-推荐在本地使用 Web UI。安装完成后以及每次更新后,请先构建当前生效的检出,再启动 Web UI:
+推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建仓库并运行:
 
 ```sh
 (cd ~/.dsh/source/current && pnpm run build)
 dsh web
 ```
 
-完整构建会生成库与客户端 bundle,以及前端 dist。上述路径是安装器的默认位置。如果你设置过 `DSH_SOURCE` 或 `DSH_CURRENT`,或者复用了已有检出,请把 `~/.dsh/source/current` 换成该检出路径;详情见 [`scripts/install.sh`](scripts/install.sh)。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。
+上述路径是安装器的默认位置。如果你设置过 `DSH_SOURCE` 或 `DSH_CURRENT`,或者复用了已有检出,请把 `~/.dsh/source/current` 换成该检出路径;详情见 [`scripts/install.sh`](scripts/install.sh)。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。
 
 ### TUI
 

+ 147 - 0
apps/cli/tests/install-script.spec.ts

@@ -0,0 +1,147 @@
+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 scriptsDirectory = join(root, 'scripts')
+  const sourceBinDirectory = join(root, '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"
+`)
+  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 interface choice', { timeout: 25_000 }, () => {
+  it('builds and launches the Web UI when the default choice is accepted', async () => {
+    const fixture = await createFixture()
+
+    const output = await runInstaller(fixture, [
+      { waitFor: 'Replace it?', send: '\n' },
+      { waitFor: 'Choose an interface [1/2]:', 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')
+  })
+
+  it('rejects an unknown choice, then launches the TUI without building', async () => {
+    const fixture = await createFixture()
+
+    const output = await runInstaller(fixture, [
+      { waitFor: 'Replace it?', send: '\n' },
+      { waitFor: 'Choose an interface [1/2]:', send: 'terminal\n' },
+      { waitFor: 'choose 1 for Web UI or 2 for TUI', send: '2\n' },
+    ])
+
+    expect(output).toContain('launching TUI')
+    expect(readFileSync(fixture.pnpmLog, 'utf8')).toBe('install\n')
+    expect(readFileSync(fixture.launchLog, 'utf8')).toBe('\n')
+  })
+})

+ 33 - 11
scripts/install.sh

@@ -7,15 +7,16 @@
 # ~/.dsh/source/master), adds a per-install staging worktree at
 # ~/.dsh/source/staging-<timestamp> on branch dsh-staging/<timestamp>, checks
 # host dependencies (git, Node, pnpm) and offers to install a missing pnpm, runs
-# `pnpm install` (no build — the `bin/dsh` launcher runs the TypeScript source
-# through the repo's own tsx), points the stable `~/.dsh/source/current` symlink
+# `pnpm install`, points the stable `~/.dsh/source/current` symlink
 # at that staging worktree and symlinks `dsh` onto PATH at `current/bin/dsh`,
 # records your API credentials in the Harness home (`~/.dsh`) dsh reads at boot,
-# and drops you into `dsh`. Keeping every checkout under ~/.dsh/source keeps
-# successive upgrades in one place instead of scattered sibling clones, and lets
-# staging worktrees share the master clone's object store. The PATH symlink
-# resolves through `current`, so an upgrade repoints one stable symlink instead
-# of relinking PATH: the `dsh` on PATH never moves and can never dangle.
+# and lets you launch the Web UI or TUI. The Web choice builds the repository
+# artifacts first; the TUI runs directly from TypeScript source through the
+# repo's own tsx. Keeping every checkout under ~/.dsh/source keeps successive
+# upgrades in one place instead of scattered sibling clones, and lets staging
+# worktrees share the master clone's object store. The PATH symlink resolves through
+# `current`, so an upgrade repoints one stable symlink instead of relinking PATH:
+# the `dsh` on PATH never moves and can never dangle.
 #
 # When run from inside an existing checkout (e.g. `sh scripts/install.sh` rather
 # than `curl ... | sh`) it reuses that checkout in place and skips the
@@ -343,12 +344,33 @@ if [ "${SKIP_CREDS:-0}" != 1 ]; then
   fi
 fi
 
-# --- 6. launch -----------------------------------------------------------------
+# --- 6. choose and launch an interface -----------------------------------------
 step "Done"
 if [ "$HAS_TTY" = 1 ]; then
-  info "launching dsh — run 'dsh' anytime to start again"
-  exec "$DSH_BIN_DIR/dsh" </dev/tty
+  printf '    1) Web UI (recommended)\n'
+  printf '    2) TUI\n'
+  while :; do
+    LAUNCH_INTERFACE=$(ask "Choose an interface [1/2]:" 1)
+    case "$LAUNCH_INTERFACE" in
+      1|web|Web|WEB)
+        step "Building DeepSeek Harness for Web UI"
+        ( cd "$DSH_STAGING" && pnpm run build )
+        info "launching Web UI — run 'dsh web' anytime to start again"
+        exec "$DSH_BIN_DIR/dsh" web </dev/tty
+        ;;
+      2|tui|Tui|TUI)
+        info "launching TUI — run 'dsh' anytime to start again"
+        exec "$DSH_BIN_DIR/dsh" </dev/tty
+        ;;
+      *)
+        warn "choose 1 for Web UI or 2 for TUI"
+        ;;
+    esac
+  done
 else
-  info "install complete. Start it with:"
+  info "install complete. Build and start the Web UI with:"
+  printf '    (cd %s && pnpm run build)\n' "$DSH_STAGING"
+  printf '    %s web\n' "$DSH_BIN_DIR/dsh"
+  info "or start the TUI with:"
   printf '    %s\n' "$DSH_BIN_DIR/dsh"
 fi

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
scripts/snapshots/translation-prompt-v4/request-response.expected.json


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov