Repository navigation
feat(cli): REPL 打磨、doctor --probe、lisa upgrade - #372
Conversation
03145cf to
b2680eb
Compare
965d64d to
faf6347
Compare
b2680eb to
bb01dc4
Compare
faf6347 to
9791297
Compare
bb01dc4 to
1ebdc58
Compare
…(UX-9) Every command — `doctor`, `status`, a one-line prompt — opened with "[proxy] outbound HTTP routed through …". That line is debug detail on a one-shot command but the one useful line in serve.log, so: - proxy-bootstrap.ts: the success banner is now opt-in (`verbose`); failures still always log because they are actionable. New proxyStatusLine() returns the same text so serve startup / doctor can print it deliberately, and there is one wording for both. - cli.ts: both bootstrap call sites pass verbose = --verbose || LISA_DEBUG || subcommand is serve. The first one runs at module load, before parseArgs, so cli-args.ts gains a raw-argv isVerboseArgv() helper and ParsedArgs.verbose (--verbose) for later use by the renderer. - New src/cli/display-path.ts: displayPath() collapses $HOME to ~ for display only. Applied to the missing-key error (which was one 200-character line and is now four readable ones), the help Data/ Plugins/MCP/Heartbeat/Config lines, `lisa channels`, and every path doctor prints. Tests: display-path (child/self/sibling-prefix/outside/trailing-sep), proxy-bootstrap (quiet default, status line, idempotence, verbose announces once — via a fresh module instance), cli-args (--verbose, LISA_DEBUG spellings). npm test 1658 pass / 0 fail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit ef74576c685e66bd18c24e9f8ddb0e5c448f996c)
…r (UX-9) The review scored the CLI 6/10: `doctor` is the best screen in the product, but the conversation itself was "plain text, no history, tool calls dumped to stderr". Five changes, all inside src/cli/: - `history.ts` + readline wiring: prompts persist in ~/.lisa/history, newest 1000, mode 0600 (prompts routinely carry pasted secrets), blanks and consecutive duplicates dropped. Written atomically on close, and only for a real terminal so `lisa < script.txt` never lands in the user's history. - `ansi.ts`: one decision function for "may this stream carry colour?" — --no-color beats everything, then NO_COLOR (no-color.org semantics), then TERM=dumb, then "is a TTY". colors.ts now delegates to it instead of carrying a second copy of the rule, so doctor/status/monitor pick up --no-color for free. - `render.ts`: one compact stderr line per tool call — "⚙ bash npm test" while running, rewritten in place to "✓ bash (1.2s)" or "✗ bash (0.3s): <first line>". Full results stay hidden unless --verbose. A TTY-only spinner covers the wait between sending a prompt and the first token, a dim "thinking…" marker says reasoning is happening without ever printing it, and system_prompt_rebuilt reads as "[soul updated]". - stdout still carries Lisa's words and nothing else — the `Lisa> ` label moved to stderr — so `lisa "…" > answer.md` and `| pbcopy` keep working. The renderer tracks the terminal cursor and the stdout stream separately for exactly that reason. - repl.ts takes its streams as options (testable without a TTY) and queues line handlers, which also fixes an ERR_USE_AFTER_CLOSE crash when stdin ended while a turn was still running. `--no-color` is parsed in cli-args.ts and recorded once, process-wide. No new dependencies: the escapes, the spinner and the history file are ~200 lines of hand-rolled code. Tests: render.test.ts drives the renderer through a fake non-TTY writer with an injected clock and injected timers (no real interval can leak on CI), plus ansi/history/repl unit tests. 1700 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit e8f28b99091e9e39251e832f827673d570fc6163)
The tech review's reliability finding was that the daily-driver instance
stalled for 5+ seconds at a time and nothing could see it: launchd's KeepAlive
only restarts a process that exited, and /health answered {ok:true}, which a
wedged event loop keeps saying happily. The server stream is extending /health
with version, uptime, event-loop-lag percentiles, heap and tenant counts; this
is the client that reads them.
lisa doctor --probe # http://127.0.0.1:5757
lisa doctor --probe 8080 # bare port
lisa doctor --probe https://host # any instance
Compatibility is the whole point of a probe, so every telemetry field is
optional and printed only when present — an older server that says {ok:true}
reports as healthy with a "no telemetry" note rather than as broken. /health is
tried first and /healthz second, since which one a deployment exposes has
changed over time.
Event loop lag p99 above 1000ms gets a ⚠ (up but degraded, exit 0); an
unreachable or non-2xx instance exits 1, so `lisa doctor --probe || launchctl
kickstart …` is a usable watchdog line.
`doctor` and `upgrade` join heartbeat/autostart in RAW_SUBCOMMANDS so the
global parser collects their own flags instead of rejecting --probe as unknown.
Tests: probe.test.ts stands up a real http server on port 0 for the rich,
legacy-{ok:true}, /healthz-only, 503, non-JSON and connection-refused cases,
and closes every socket so no handle outlives the test on Node 22 CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit e45b36cb82f51381f57e7a7cb4dffd560cf5ff1b)
…art the daemon Tech review item 17: "add `lisa upgrade` (npm global upgrade + launchctl kickstart) so the author's own machine always runs the newest release candidate". Upgrading today means remembering how Lisa got installed and then remembering that the login LaunchAgent keeps executing the old code until something kicks it — both mechanical, both now handled. lisa upgrade detect → upgrade → restart the daemon lisa upgrade --check compare the installed and published versions only lisa upgrade --dry-run print the exact commands, run nothing Detection reads process.argv[1], its realpath, `brew --prefix` and `npm prefix -g`. Order matters and is the interesting part: on Apple silicon a Homebrew shim and an npm global bin are both `/opt/homebrew/bin/lisa`, and the brew formula wraps the npm package, so the resolved path contains BOTH `/Cellar/` and `/node_modules/@oratis/lisa/`. `/Cellar/` is conclusive, so it is checked first; the package path second; the npm global prefix third; a git checkout last. Decisions worth recording: - A source checkout is never upgraded automatically. `git pull` on a working tree that may hold uncommitted work is a merge, not an upgrade — so it prints the three commands and exits 0. - `--check` exits 0 whether or not an update exists. It is a report; a cron line that mails its output shouldn't look like a failure. - The kickstart only runs on macOS, only when `autostart status` says the agent is actually loaded, and a kickstart failure is a warning — the upgrade itself already succeeded. - AUTOSTART_LABEL is copied from src/autostart/install.ts (that file owns it and isn't mine to edit); a test reads the installer and fails on drift. Every decision is a pure function (detectInstall / upgradeCommands / compareVersions / kickstartCommand) and every effect is injected, so the 26 tests cover the whole surface with no network and no real install. `upgrade` joins the subcommand union and RAW_SUBCOMMANDS so --check/--dry-run reach the handler instead of being rejected as unknown global flags. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit d982c98142a14cd793cc6be636f8cf983094b089)
The two new commands existed but were invisible: `lisa --help` didn't mention them and tab-completion didn't offer them. Both lists keep their existing structure — `doctor --probe` sits under its `doctor` entry in INSPECTION, `upgrade` opens LIFECYCLE, next to the other "keep this machine running" commands. While in completions/*, the three files had also fallen behind the CLI: pair, mail, kb, login, logout and billing shipped without ever being added, and --verbose / --no-color / --host were missing from the flag lists. Added, since a completion list that is missing a third of the subcommands teaches people the wrong command set. Verified with `bash -n` and `zsh -n`; fish isn't installed on this machine, so lisa.fish was edited to match the file's existing `complete -c lisa` patterns and read back rather than parsed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit e89acbb13c52634c66d2a30ce3962292efe43d9b)
AbortSignal.timeout() leaves its timer armed for the full duration even after the fetch resolves. Harmless in production, but Node 22's test runner — which CI uses and this machine does not — counts still-pending async activity from a finished test against it. An own AbortController with an unref'd timer cleared in a `finally` costs three lines and removes the whole class of problem. Also adds the case that timer exists for: a server that accepts the connection and then never answers, which is exactly the wedged-event-loop symptom the probe was written to diagnose. It now reports "timed out after 150ms" instead of hanging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 581c2c692be810b61770dedbb5c01779278836ce)
`lisa upgrade` printed npm's first line and stopped. EACCES on a global install is the single most common way this command fails — it is the whole reason `sudo npm install -g` is folklore — and #371 had already taught the Mac wizard the no-sudo prefix fix. The same user got a dead end from the CLI and a working fix from the app. classifyUpgradeFailure() mirrors classifyInstallFailure() in packaging/mac-client/Sources/LisaSetup/BackendSetup.swift, on the same needles, so the two surfaces cannot drift into contradicting each other about one shared `npm install -g`. The permissions branch prints that wizard's commands verbatim. Classification reads the whole error message, not its first line: runCmd rejects with `npm exited 1: <stderr>` and the needle is almost never on line one — the reason the old first-line-only output was uninformative. Tests cover each class, the multi-line real-world shape, that no branch hands out a bare `sudo npm install` (that is what creates the root-owned folder in the first place), and that MIN_NODE_MAJOR tracks package.json `engines.node`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9791297 to
32bc272
Compare
评审 — 承诺逐条实跑;补上失败升级的出路已 rebase 到 main,base 改为 承诺实测(不是读代码,是真跑)
结构性检查命令执行面干净。
"thinking 内容从不打印"是结构性保证,不是约定。 spinner 定时器 已修:升级失败后没有出路失败路径原本是打印 npm 报错的第一行然后停止: 问题有两层:
新增 一个实现细节值得说:分类读整条消息而不是第一行。 测试覆盖四种分类、真实多行报错形状、任何分支都不会给出裸 验证
🤖 Generated with Claude Code |
This PR raised engines.node to >=22.19.0 for a real reason (undici calls
worker_threads APIs added in 22.10) and its final commit aligned three
places that still promised Node 20 — both READMEs, both install pages and
`lisa doctor`. Two more were left saying 20:
- **The Mac install wizard.** BackendSetup.minimumNodeMajor was 20 and the
check compared the major only, so it reported Node 20 — or Node 22.5 —
as a perfectly good environment and then ran an `npm install -g` that npm
refuses with EBADENGINE. That is precisely the failure this commit's own
message set out to remove ("the tool told them their environment was fine
minutes before it broke"), on a surface it did not check. The floor is now
major.minor via nodeVersionParts + meetsNodeFloor, and both user-facing
strings say 22.19.
- **`lisa upgrade`'s node-too-old advice**, whose MIN_NODE_MAJOR was 20.
The drift guard added in #372 is what caught this: `MIN_NODE_MAJOR tracks
package.json engines.node` failed with `20 !== 22` on the integrated tree,
which is the only place the two halves meet.
New Swift tests pin the pair that a major-only check waves through — 22.0
and 22.18 are below the floor, 22.19 is exactly at it — and two existing
tests that hardcoded Node 20 as "at the floor" move with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
第 4 / 8 条,实现 UX-9 与 T-3、T-11 的 CLI 部分。基于 #371。
做了什么
日志噪音与路径(UX-9)。
[proxy] outbound HTTP routed through …之前每条命令都打印,现在只在--verbose或LISA_DEBUG=1下出现(serve启动时仍保留一行)。所有面向用户的路径统一用~缩写——缺密钥的报错之前会打出 200 多字符的绝对路径。REPL。 持久化历史(
~/.lisa/history,0600,上限 1000 行,跳过空行与连续重复);ANSI 颜色助手,遵守NO_COLOR、--no-color和非 TTY;工具调用压缩成单行——运行中⚙ bash npm test,结束时✓ bash (1.2s)或✗ bash (0.3s): <首行错误>,完整结果只在--verbose下展开;发送提示词到首个响应之间显示 spinner(仅 TTY,走 stderr);thinking 事件显示为暗色标记但从不打印思考内容。stdout 仍然只承载 Lisa 的正文,管道用法不受影响。lisa doctor --probe [url]。 探测运行中后端的/health,打印版本、运行时长、事件循环延迟、堆内存、租户数;p99 超过 1000ms 标警告;不可达时退出码非零。同时兼容只返回{ok:true}的旧服务端。lisa upgrade。 识别安装方式(Homebrew / npm 全局 / 源码检出),执行对应升级,打印升级前后版本;若 LaunchAgent 已加载则顺带launchctl kickstart让守护进程用上新代码——这正是此前需要手工执行 6 条命令的场景。--check只比对版本,--dry-run只打印将要执行的命令。验证
新增逻辑均有测试(渲染器用假的非 TTY writer,探测用端口 0 的本地服务,升级只测纯检测与命令拼装,不联网不真装)。无新增运行时依赖。
🤖 Generated with Claude Code