feat(install): choose web or tui after setup
This commit is contained in:
+2
-2
@@ -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: b447c9634189353854e8be9d0bf597a8b0c7e371
|
||||
README.zh.md: f8bbbc36bc670403c0b9a40977f32f598e77ee46
|
||||
README.md: f5367578570a7b4ec7bad13be3b801ac7870de6c
|
||||
README.zh.md: b9f593857d2a65b5df61983a640022be605ee93b
|
||||
@@ -22,7 +22,7 @@ Install `dsh` with one command:
|
||||
curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | 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 its frontend 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.
|
||||
|
||||
@@ -30,14 +30,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 frontend and run:
|
||||
|
||||
```sh
|
||||
(cd ~/.dsh/source/current && pnpm run build)
|
||||
(cd ~/.dsh/source/current && pnpm --filter @deepseek-ai/dsh-frontend 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
|
||||
|
||||
|
||||
+4
-4
@@ -22,7 +22,7 @@ DeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源
|
||||
curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | 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)。
|
||||
|
||||
@@ -30,14 +30,14 @@ curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/m
|
||||
|
||||
### Web UI
|
||||
|
||||
推荐在本地使用 Web UI。安装完成后以及每次更新后,请先构建当前生效的检出,再启动 Web UI:
|
||||
推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建前端并运行:
|
||||
|
||||
```sh
|
||||
(cd ~/.dsh/source/current && pnpm run build)
|
||||
(cd ~/.dsh/source/current && pnpm --filter @deepseek-ai/dsh-frontend 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
|
||||
|
||||
|
||||
@@ -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\n--filter @deepseek-ai/dsh-frontend run 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
@@ -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 frontend
|
||||
# artifact 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 Web UI"
|
||||
( cd "$DSH_STAGING" && pnpm --filter @deepseek-ai/dsh-frontend 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 --filter @deepseek-ai/dsh-frontend 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
|
||||
Reference in New Issue
Block a user