Skip to content

fix(cli): cold-start agent findings — serve+web, get nodeCount, error URLs#53

Merged
naorsabag merged 3 commits into
masterfrom
fix/cold-start-bugs
Apr 27, 2026
Merged

fix(cli): cold-start agent findings — serve+web, get nodeCount, error URLs#53
naorsabag merged 3 commits into
masterfrom
fix/cold-start-bugs

Conversation

@naorsabag
Copy link
Copy Markdown
Owner

@naorsabag naorsabag commented Apr 27, 2026

Summary

A fresh-context agent ran the full OpenHop flow end-to-end (validate → push → get → patch → get → remove) — every command worked first try. It caught three rough edges, all fixed here.

Three fixes

1. `openhop serve` now starts both API + web UI

  • Was: only `:8787` (Fastify). The URL printed by `push` points at `:8788` so users hit a 404 unless they manually started `packages/web`'s vite dev server.
  • Now: `serve` spawns both. `--no-web` opts out for headless CI.
  • Both children torn down on SIGINT/SIGTERM and on API exit.

2. `get --json` includes `nodeCount`

  • Was: `get` returned the full storedFlow without a flat `nodeCount`. `push` had it. Inconsistent.
  • Now: `get --json` includes `nodeCount` so agents can read it without drilling into `.flow.nodes.length`.

3. Network-error JSON includes failing URL

  • Was: `{ok:false, error:"network", message:"fetch failed"}` — useless for diagnosis.
  • Now: every server-touching command (push, list, get, patch, remove) includes `url` in its error JSON. Human mode also says `Connection failed (http://...)`.

Test plan

  • Contract suite locks the new `url` field for every network error
  • 83/83 tests in @openhop/cli (was 82)
  • `prettier --check` clean
  • `eslint` clean
  • CI green
  • CodeRabbit review

Caveat

`serve` only works in a from-source checkout — the published `npm i -g openhop` doesn't ship server/ or web/. Tracked for v0.2 with a separate @openhop/server package or a docker-based prod path.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • serve now launches API and web servers together (use --no-web for API-only), coordinates shutdown across them, and preserves stdout for data-only CLI output.
    • push success output distinguishes the API base URL from the returned web UI flow URL.
  • Bug Fixes

    • Network error outputs (JSON and human) include the attempted request URL.
    • list, get, remove, push consistently report network errors with the same exit status and include the attempted URL.
    • get JSON now includes a node count; human output prefers flow metadata title when present.

… URLs

Three bugs surfaced by a fresh-context agent that ran the full OpenHop
flow end-to-end (validate → push → get → patch → get → remove). All
commands worked first try, but it caught three rough edges:

1. `openhop serve` only started the API on :8787, not the web UI on
   :8788. The URL printed by `push` points at :8788, so users clicked
   into a 404. Now serves both: spawns the Vite dev server alongside
   Fastify. `--no-web` opts out for headless / CI use. Both children
   are torn down on SIGINT/SIGTERM and on API exit.

2. `get --json` returned the full storedFlow shape but no flat
   `nodeCount`, while `push --json` did. Inconsistent. Add `nodeCount`
   to `get --json` so agents can read it without drilling into
   `.flow.nodes.length`.

3. Network-error JSON used to be {ok:false, error:"network",
   message:"fetch failed"}. Useless for diagnosis — no host or port.
   Now includes the URL it tried for every command (push, list, get,
   patch, remove). Human-mode error message also says
   `Connection failed (http://...)` instead of just `Connection failed`.

Contract suite extended: every server-touching command's network-error
JSON must include `url` matching the failing endpoint. New test
iterates list/get/remove/push and locks the contract.

Tests: 83/83 in @openhop/cli (was 82). Bundle 47.1 kb (was 45.8 — the
extended serve action adds ~1.3 kb).

Caveat noted in serve's comment: this command only works in a
from-source checkout; the published `npm i -g openhop` package doesn't
ship server/ or web/. Tracked for v0.2 with a separate @openhop/server
package or a docker-based prod path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 38a80ba0-8fd3-445b-b07f-3664ae6802c7

📥 Commits

Reviewing files that changed from the base of the PR and between 4cfc3bb and 0df5e87.

📒 Files selected for processing (1)
  • packages/cli/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/index.ts

Walkthrough

Extends the network-error JSON contract to include a url field, standardizes request URL construction and error reporting across client commands, updates get to include endpoint URLs and node counts in outputs, and enhances serve to spawn and manage both the Fastify API and Vite dev server with coordinated shutdown.

Changes

Cohort / File(s) Summary
Contract & Tests
packages/cli/__tests__/contract.test.ts
Extended network-failure JSON contract to require a url field; updated list/get assertions to check exact endpoint URLs; added test enforcing uniform network error responses (exit code 6, error: 'network', url starts with configured base) across list, get, remove, and push.
Client: get command
packages/cli/src/get.ts
Constructs a single request url used for fetch and all error outputs; includes url in JSON and stderr on non-OK responses and exceptions; computes nodeCount from optional flow.flow.nodes and outputs { ...flow, nodeCount } in --json; uses flow.meta.title (when string) for human Title.
CLI index / serve & client commands
packages/cli/src/index.ts
serve now spawns Fastify API and (optionally) Vite dev server, pipes child stderr to parent, and implements coordinated shutdown on SIGINT/SIGTERM or child error/exit; standardized url construction for push, list, patch, remove and embeds failing url into JSON and non-JSON error outputs; push success distinguishes API base url from returned web flowUrl.

Sequence Diagram(s)

sequenceDiagram
    participant Dev as Developer / CLI
    participant CLI as CLI (parent)
    participant API as Fastify API (child)
    participant Web as Vite Dev Server (child)
    Dev->>CLI: run `cli serve` (optionally `--no-web`)
    CLI->>API: spawn API process (npx tsx ...)
    CLI->>Web: spawn Web process (npm run dev) / skipped if --no-web
    API-->>CLI: error / exit
    CLI->>Web: kill (if running)
    CLI->>API: ensure shutdown
    Dev->>CLI: SIGINT / SIGTERM
    CLI->>API: forward shutdown
    CLI->>Web: kill (if running)
    CLI-->>Dev: exit with propagated code
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly summarizes the three main changes: serve+web startup, nodeCount in get command, and error URLs in network failures.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cold-start-bugs

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/cli/__tests__/contract.test.ts (1)

255-265: Include patch in the cross-command URL contract matrix.

The new matrix claims to lock all server-touching commands, but patch is still missing from cases, so its url field can regress without failing this test.

♻️ Suggested diff
   it('every network-error JSON includes the URL that failed', () => {
@@
-    const cases = [
+    const validPatch =
+      'operations:\n  - op: rename-nodes\n    nodes:\n      - id: a\n        label: A\n'
+    const cases: Array<{ args: string[]; input?: string }> = [
       { args: ['list', '--json', '--server', 'http://127.0.0.1:1'] },
       { args: ['get', 'x', '--json', '--server', 'http://127.0.0.1:1'] },
       { args: ['remove', 'x', '--json', '--server', 'http://127.0.0.1:1'] },
       {
         args: ['push', EXAMPLE_GOOD, '--json', '--server', 'http://127.0.0.1:1'],
       },
+      {
+        args: ['patch', 'x', '-', '--json', '--server', 'http://127.0.0.1:1'],
+        input: validPatch,
+      },
     ]
     for (const c of cases) {
-      const r = run(c.args)
+      const r = run(c.args, c.input)

Also applies to: 266-274

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/__tests__/contract.test.ts` around lines 255 - 265, The test
matrix variable cases in contract.test.ts is missing the 'patch' command so its
URL contract can regress; add a new case entry in the cases array for the
'patch' command (similar to the existing 'push' entry) using the same fixture
symbol EXAMPLE_GOOD and flags (e.g. '--json', '--server', 'http://127.0.0.1:1')
so the patch command's URL is validated alongside list/get/remove/push.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/get.ts`:
- Around line 24-27: Remove the unused variable bindings named fullUrl that
cause the ESLint failure: locate the occurrences of "const fullUrl =
`${opts.server}/api/flows/${flowId}`" (and the similar binding around lines
62-66) in packages/cli/src/get.ts and either delete those const declarations or
replace usages so the value is actually used (preferably delete the unused
consts since fetch already builds the URL); ensure no other references to
fullUrl remain.

In `@packages/cli/src/index.ts`:
- Around line 79-91: The child processes (api/web) are currently started with
stdio: 'inherit', which sends child stdout into the CLI stdout and can mix logs
with data; change both spawn calls that create api (spawn('npx', ...,
serverEntry)) and web (spawn('npm', ['run','dev'], { cwd: webDir })) to use a
stdio tuple that prevents inheriting stdout (e.g., stdio:
['ignore','pipe','inherit'] or ['inherit','pipe','inherit']) so child stdout is
piped (not inherited) while stderr remains inherited; also ensure any created
child.stdout streams (from the api/web child) are either consumed, forwarded to
a logger, or explicitly ignored to avoid unhandled stream warnings.

---

Nitpick comments:
In `@packages/cli/__tests__/contract.test.ts`:
- Around line 255-265: The test matrix variable cases in contract.test.ts is
missing the 'patch' command so its URL contract can regress; add a new case
entry in the cases array for the 'patch' command (similar to the existing 'push'
entry) using the same fixture symbol EXAMPLE_GOOD and flags (e.g. '--json',
'--server', 'http://127.0.0.1:1') so the patch command's URL is validated
alongside list/get/remove/push.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f7e04ff2-60ab-4ecc-8029-c8ad891e8160

📥 Commits

Reviewing files that changed from the base of the PR and between 335cc0e and 2cbe458.

📒 Files selected for processing (3)
  • packages/cli/__tests__/contract.test.ts
  • packages/cli/src/get.ts
  • packages/cli/src/index.ts

Comment thread packages/cli/src/get.ts Outdated
Comment thread packages/cli/src/index.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/cli/src/get.ts (1)

47-47: Optional: simplify nodeCount expression and drop non-null assertions

This is equivalent but easier to read and avoids ! assertions.

♻️ Suggested refactor
-          const nodeCount = Array.isArray(flow.flow?.nodes) ? flow.flow!.nodes!.length : 0
+          const nodes = flow.flow?.nodes
+          const nodeCount = Array.isArray(nodes) ? nodes.length : 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/get.ts` at line 47, The nodeCount expression uses
Array.isArray and non-null assertions; replace it with a simpler
optional-chaining/nullish-coalescing expression to avoid `!` assertions and
improve readability: update the declaration of nodeCount (the variable currently
computed from flow.flow?.nodes) to use something like optional chaining on flow
and nodes and `?? 0` to default when missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/cli/src/get.ts`:
- Line 47: The nodeCount expression uses Array.isArray and non-null assertions;
replace it with a simpler optional-chaining/nullish-coalescing expression to
avoid `!` assertions and improve readability: update the declaration of
nodeCount (the variable currently computed from flow.flow?.nodes) to use
something like optional chaining on flow and nodes and `?? 0` to default when
missing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fcd654a5-9f62-4783-b712-2c9f04b41446

📥 Commits

Reviewing files that changed from the base of the PR and between 2cbe458 and 4cfc3bb.

📒 Files selected for processing (1)
  • packages/cli/src/get.ts

CodeRabbit on PR #53 flagged that `stdio: 'inherit'` on the API + web
children leaks Fastify's pino logs and Vite's dev banner onto the
CLI's stdout. CLI contract is stdout=data, stderr=logs. Pipe both
children's stdout and stderr through the parent's stderr so jq-style
post-processing of `serve` output stays clean.

Tests still 83/83.
@naorsabag
Copy link
Copy Markdown
Owner Author

@coderabbitai addressed in 0df5e87 — both children now use stdio: [ignore, pipe, pipe] and pipe stdout+stderr to parent stderr. Stdout discipline preserved.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Tip

For best results, initiate chat on the files or code changes.

@naorsabag: Thanks for the update! Using stdio: [ignore, pipe, pipe] with piping to parent stderr is a clean approach — it keeps stdout reserved for --json output while still surfacing child process logs for debugging. ✅

@naorsabag naorsabag merged commit 1e2bae3 into master Apr 27, 2026
7 checks passed
@naorsabag naorsabag deleted the fix/cold-start-bugs branch April 27, 2026 16:07
naorsabag added a commit that referenced this pull request Apr 27, 2026
## 1. add-steps: pure 0-based `index`, no sentinels

Original `after: -1` / `after: "end"` design needed two sentinels and
a paragraph of explanation. Refactored to match Array.prototype.splice
exactly: `index` is the 0-based position the new steps will occupy
after insertion. Range [0, steps.length] inclusive. Omit `index` to
append (the most common case, no need to know steps.length first).

  add-steps    | index?: N, steps: [...]  | Insert at 0-based index;
                                            omit to append.

Out-of-range error is short and self-explanatory:
  index 99 is out of range (valid: 0..4; omit to append)

This is a breaking change vs the previous patch shape, but we haven't
shipped v0.1 yet and there are no users. Cleaner mental model now,
no doc burden, fewer special cases to test.

## 2. openhop serve: machine-readable readiness signal

Three sub-issues from the cold-start report:
  (a) Long silence between "Starting OpenHop API..." and the child's
      "Server listening at..." line (10-15s on slow disks)
  (b) The server's stdout banner is a freeform string, not parseable
  (c) No --wait-ready flag for `serve & openhop push ...` patterns

Fix: parent process polls `:<port>/health` after spawning children.
On 200, prints exactly one line to stdout:

  openhop: ready api=http://localhost:8787 web=http://localhost:8788 elapsed=12.4s

Stable prefix `^openhop: ready ` so callers can `grep -m1`. Default-on;
opt out with --no-wait-ready. Configurable via --ready-timeout
<seconds> (default 60).

Stdout from `serve` is now reserved for that one line; child logs go
to parent stderr per #53. Stays consistent with stdout=data discipline.

## Tests

- 32/32 in @openhop/shared (was 30) — new add-steps cases:
    - inserts at index 0 (prepend)
    - inserts at middle index, existing step pushed right
    - omitted index appends
    - explicit `index: steps.length` also appends
    - out-of-range error mentions valid range and the "omit to append" hint
- 83/83 in @openhop/cli unchanged
- prettier --check + eslint clean

CLI bundle 47.2 → 48.6 kb.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
naorsabag added a commit that referenced this pull request Apr 27, 2026
## 1. add-steps: pure 0-based `index`, no sentinels

Original `after: -1` / `after: "end"` design needed two sentinels and
a paragraph of explanation. Refactored to match Array.prototype.splice
exactly: `index` is the 0-based position the new steps will occupy
after insertion. Range [0, steps.length] inclusive. Omit `index` to
append (the most common case, no need to know steps.length first).

  add-steps    | index?: N, steps: [...]  | Insert at 0-based index;
                                            omit to append.

Out-of-range error is short and self-explanatory:
  index 99 is out of range (valid: 0..4; omit to append)

This is a breaking change vs the previous patch shape, but we haven't
shipped v0.1 yet and there are no users. Cleaner mental model now,
no doc burden, fewer special cases to test.

## 2. openhop serve: machine-readable readiness signal

Three sub-issues from the cold-start report:
  (a) Long silence between "Starting OpenHop API..." and the child's
      "Server listening at..." line (10-15s on slow disks)
  (b) The server's stdout banner is a freeform string, not parseable
  (c) No --wait-ready flag for `serve & openhop push ...` patterns

Fix: parent process polls `:<port>/health` after spawning children.
On 200, prints exactly one line to stdout:

  openhop: ready api=http://localhost:8787 web=http://localhost:8788 elapsed=12.4s

Stable prefix `^openhop: ready ` so callers can `grep -m1`. Default-on;
opt out with --no-wait-ready. Configurable via --ready-timeout
<seconds> (default 60).

Stdout from `serve` is now reserved for that one line; child logs go
to parent stderr per #53. Stays consistent with stdout=data discipline.

## Tests

- 32/32 in @openhop/shared (was 30) — new add-steps cases:
    - inserts at index 0 (prepend)
    - inserts at middle index, existing step pushed right
    - omitted index appends
    - explicit `index: steps.length` also appends
    - out-of-range error mentions valid range and the "omit to append" hint
- 83/83 in @openhop/cli unchanged
- prettier --check + eslint clean

CLI bundle 47.2 → 48.6 kb.

Co-authored-by: Naor Sabag <naorsabag@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant