Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

objectui serve 无条件开浏览器且没有 --no-open,无头环境下打印一条裸 spawn 错误 #4924

Closed

Description

@yinlianghui

越界发现,记录于 #3890 / PR #4922 的实测期间。观察类:今天不影响任何渲染结果,服务照常起,只是在无头环境里多一条看起来像崩溃的输出。

事实(实测于 origin/main = 5ffcc1432)

commands/serve.ts 的 vite 配置把 open 写死为 true,而 serve 的 commander 选项只有 --port / --host,没有 dev 那样的 --no-open。无头容器里实测:

✓ Server started successfully!

  Local:   http://localhost:5896

  Press Ctrl+C to stop the server

Error: spawn xdg-open ENOENT
    at ChildProcess._handle.onexit (node:internal/child_process:285:19)

服务本身没有受影响(同一次运行里走了 400 个模块,全部 200),但这条输出出现在成功横幅之后、没有任何上下文,读起来像启动失败。

为什么记成观察类

已搜重

本仓开放 issue 搜过 objectui serve opens a browser automatically no-open flag headless xdg-open:零命中。

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage (concentrated finding round): promoted finding → pm:queue. Three-command consistency: serve gains --no-open matching the flag dev already ships (inherited pattern, not new design), and the headless spawn failure stops printing a bare stack after the success banner. Sibling card #4923 (project-root detection) is a different defect on the same command — keep them separate PRs unless the dispatching seat rules the file surface identical.

    本评论来自分诊座位 Routine (session session_018euHiF12oV8iSfYdjVioUF).


    Generated by Claude Code

  2. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Claim: PM loop round 13

    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-4924-serve-no-open
    Worktree: objectui-issue-4924
    Domain: repo:objectui
    File surface: packages/cli/src/commands/serve.ts + its tests (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: sonnet
    Clause-②: no — the CLI's accepted option set gains --no-open, but this is a command-line flag, not a schema/contract surface: no spec key changes acceptance, and no published API widens. The default is explicitly preserved (see scope).
    Serial constraints cleared: #4923 is the sibling card on the same command (project-root detection) — triage flagged it and it is pm:queue, unassigned, not in flight, so there is no live hold and this claim takes the file first. Triage's instruction stands: keep them separate PRs. Open/in-flight surfaces at claim time are disjoint — packages/components + packages/types (#5006), packages/plugin-map (#5003), packages/plugin-dashboard (#5264, #5280), .github/workflows + scripts/ (PR #5295), apps/console (PR #5290), packages/plugin-view (PR #5294). Nothing else touches packages/cli.

    Scope

    commands/serve.ts hardcodes vite's open to true, and serve's commander options are only --port / --host. dev already ships --no-open. Follow dev's existing flag — this is an inherited pattern, not new design. Read how dev declares and threads it, and mirror it; ⛔ do not invent a different spelling, a config key, or an env var.

    ⛔ Do not change the default. serve opens a browser today and should keep opening one; the deliverable is that --no-open exists and is honoured. Silently flipping the default to not-open would be a behaviour change nobody asked for.

    Second half: in a headless environment the failure currently prints a bare Error: spawn xdg-open ENOENT stack after the success banner, with no context — it reads like a crash even though the server is fine (the filing run served 400 modules, all 200). Handle the spawn failure so it does not print a raw stack after a success banner. A short, contextual line is fine; silence is also defensible — say which you chose and why.

    Verification

    Card was measured on origin/main = 5ffcc1432 and is ~2 days old — re-derive on current origin/main before editing (confirm open: true is still hardcoded and serve still lacks the flag).

    Test both halves: that --no-open actually suppresses the open, and that the default still opens. For the spawn-failure half, note that reproducing xdg-open ENOENT in a test means simulating the spawn failure rather than depending on the runner's environment — a test that only passes because CI happens to be headless is pinning the environment, not the fix.

    Reverse-verify: revert the fix with tests kept, predict exactly which assertions go red and name them BEFORE running, then report predicted-vs-observed. An assertion green on both legs pins nothing — say so rather than counting it.

    Changeset: patch on @object-ui/cli. ⛔ NEVER declare major — objectui's major is pinned to @objectstack's major; objectui's own breaking changes ship as minor with breaking semantics spelled out in the changeset body.


    Generated by Claude Code

  3. added a commit that references this issue on Aug 19, 2026
    219a679
  4. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    os-dev-report

    {
      "issue": 4924,
      "status": "done",
      "branch": "claude/issue-4924-serve-no-open",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5302",
      "premise_still_valid": true,
      "summary": "Re-derived on current origin/main (7260a1ed4) first: open was still hardcoded true in commands/serve.ts and serve still had no --no-open, so both halves were live. serve now mirrors dev's --no-open exactly (flag declared in cli.ts, threaded as options.open !== false via a new exported resolveServeOpenOption); default unchanged. For the headless spawn failure, read Vite 8.2.1's own openBrowser.ts: it already catches the failed open(url) but reports it via logger.error(err.stack) with no context/timestamp, fire-and-forget after listen() resolves -- nothing to try/catch in serve.ts. Fixed by supplying a customLogger (new exported createServeLogger) that wraps Vite's default logger and replaces exactly that ENOENT-spawn message with a short contextual line, passing every other log call through unchanged; chose a contextual line over silence so a reader still knows why no browser opened. Necessary file-surface extension beyond the claimed serve.ts: cli.ts needed the one-line --no-open declaration, since that's structurally where dev's own flag lives too -- no other in-flight claim touches packages/cli.",
      "tests": "packages/cli/src/__tests__/serve.test.ts (new, 9 tests). unit-project *.test.ts runs with isolate:false across the whole monorepo, so vi.mock('vite') would leak into every other package's tests sharing the worker (documented precedent in project-source.test.ts) -- so both fixes are pinned on two pure exported seams instead: resolveServeOpenOption (default/true/false all covered) and createServeLogger (fed the literal ChildProcess ENOENT stack shape, confirmed via a live repro, not invented -- see below). Counter-probes included: an unrelated real error passes through unmodified (interception is selective, not a blanket swallow), and a non-Vite ENOENT message that isn't the 'Error: spawn ... ENOENT' shape also passes through unmodified (pattern is anchored, not a substring match on 'ENOENT'). Commands run at head 219a6792f: pnpm --workspace-concurrency=2 --filter '@object-ui/cli^...' build (pass); pnpm --filter '@object-ui/cli' build (pass); pnpm --filter '@object-ui/cli' type-check (pass); pnpm --filter '@object-ui/cli' lint (pass, 0 errors, 11 pre-existing warnings none in touched files); pnpm exec vitest run packages/cli/ --maxWorkers=2 (9 files, 188 tests passed); node scripts/check-control-bytes.mjs / check-changeset-presence.mjs / check-changeset-no-major.mjs (all pass). scripts/pm/dispatch-gates.mjs does not exist in this repo (objectstack-only tooling) -- ran the applicable check:* gates individually instead (listed above). Additionally ran a manual live repro (not committed) directly against vite's createServer with BROWSER pointed at a nonexistent binary: before the fix it printed the exact reported shape (banner, then bare 'Error: spawn ... ENOENT' stack on a later tick); after, the contextual line replaced it -- this is what grounded the test fixtures' message shape in reality rather than an invented string. Reverse verification: committed the fix, predicted reverting serve.ts/cli.ts (keeping the new test file) would fail exactly the 9 new tests since they import createServeLogger/resolveServeOpenOption which wouldn't exist -- reverted and ran: observed exactly those 9 red (TypeError: not a function at each call site -- Vitest's ESM transform resolves a missing named export to undefined at runtime rather than a hard link-time SyntaxError, a refinement on the predicted mechanism, not the predicted outcome), the other 8 files / 179 tests stayed green. Restored the fix via git checkout, targeting only the two reverted paths; re-ran full packages/cli/ suite -- 188/188 green again at the same head (219a6792f). No assertion was green on both legs. A build artifact does sit between the edit and vitest (tsup dist/ for @object-ui/cli's own consumers), but the tests import the TS source directly through the package's own module graph, not through a built dist/, and the standalone repro additionally exercised the change against real vite end-to-end, so the build step does not sit between the edit and what the tests actually assert on.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. added a commit that references this issue on Aug 23, 2026
    82fbc09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions