Skip to content

feat: bundle server+web into npm package; add openhop demo (closes B3, B9, W6)#66

Merged
naorsabag merged 2 commits into
masterfrom
feat/bundle-server-web-and-demo
May 4, 2026
Merged

feat: bundle server+web into npm package; add openhop demo (closes B3, B9, W6)#66
naorsabag merged 2 commits into
masterfrom
feat/bundle-server-web-and-demo

Conversation

@naorsabag
Copy link
Copy Markdown
Owner

@naorsabag naorsabag commented May 4, 2026

Summary

  • Closes the load-bearing v0.1 launch blockers from openhop-launch/READINESS-AUDIT.md: B3 (openhop demo command) and B9 (server+web were monorepo-only) plus all 6 of B9's "don't miss" sub-tasks (B9.1–B9.6) and W6 (closed #18–#37 README link).
  • After this lands, npx openhop is a runnable tool, not a config-shipper. The published tarball (or, more precisely, the published openhop + @openhop/server + @openhop/web set) is everything a fresh machine needs to render a flow — no clone, no docker.

Design choices

Followed the Storybook / Remotion shape (Option A from the audit): three independent npm packages with openhop as the user-facing one, depending on @openhop/server and @openhop/web.

  • @openhop/server — exports buildApp() and startServer({ port, host, logger }) returning a RunningServer handle. Still auto-starts when invoked as a script (node dist/server.js or npx tsx src/index.ts) via an import.meta.url === fileURLToPath(process.argv[1]) guard, so the dev experience is unchanged.
  • @openhop/web — ships the prebuilt Vite output as a static-assets-only package; build-time deps (react, elkjs, tailwind, vite) all moved to devDependencies so consumer installs don't pay for them.
  • openhop CLI — replaces the old spawn('npx tsx ../../server/src/index.ts') + spawn('npm run dev') pair with an in-process startServer() call plus a tiny static server (@fastify/static over @openhop/web/dist/ with SPA fallback for /flow/<id>).
  • openhop demo — new subcommand: pick ports, boot both servers in-process, POST a bundled starter flow, open the browser via xdg-open/open/start (no runtime dep on the open package). Closes the abort criterion in 01-repo-readiness.md:37.

Verification

Check Result
npm test --workspaces shared 93/93 · server 19/19 · CLI 83/83 — all pass
npm run lint exit 0 (12 pre-existing warnings in useFlowPolling.ts, none from this PR)
npm run format:check clean
npm pack per workspace server 9.8 KB · web 880 KB · CLI 20 KB
Install all three tarballs in a clean /tmp dir + ./node_modules/.bin/openhop demo --no-open --port 18787 --web-port 18788 openhop: ready api=… web=… flow=… on stdout, /health returns {"status":"ok"}, SPA fallback /flow/<id> returns index.html, SIGTERM shuts down cleanly

This is the v0.1 acceptance gate the audit asks for — proves what npm publish would deliver, without actually publishing.

Test plan

  • CI green on Node 20 + 22 (lint, format:check, typecheck, build, test, coverage, audit, gitleaks)
  • Reviewer runs npm pack for all three packages, installs in a clean dir, runs npx openhop demo, confirms the browser opens at the printed /flow/<id> URL
  • Reviewer confirms npx openhop serve (the unchanged-from-user-POV command) still works the same way it did before this PR
  • Reviewer eyeballs the README "Try it in 60 seconds" rewrite and the new install table
  • Reviewer confirms skills/openhop/SKILL.md no longer tells agents to git clone

Out of scope (deliberately deferred)

  • npm publish of any of the three packages — the audit's B4 blocker, gated on this landing first
  • git tag v0.1.0 + GitHub Release notes (B5)
  • B1 homepage clear, B2 social preview, B7 hero GIF, B8 PVR toggle — repo-settings work, separate PR(s)
  • W11 README contact email, W12 Sentry, W13 name squatting — separate procurement track

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added npx openhop demo (starts API + web UI, opens browser by default) and improved serve CLI with in-process API/web startup, configurable ports, and options like --no-open and --no-web.
  • Documentation

    • Updated README quickstart/install and SKILL.md to recommend the new demo/serve workflows and simplified startup scenarios.
    • Added explanatory notes in docker-compose for demo/serve usage.

…op demo

Closes READINESS-AUDIT B3, B9 (and the 6 sub-tasks B9.1, B9.4, B9.5, B9.6) and W6.

@openhop/server (publishable):
- drop private:true, version 0.1.0, files:[dist], main:dist/server.js,
  exports map (types: src, default: dist), esbuild build script + prepack
- factor named exports buildApp/startServer + RunningServer type, with
  auto-start guard so `node dist/server.js` and `npx tsx src/index.ts`
  both still work
- @openhop/shared moves to devDependencies (bundled at build time)

@openhop/web (publishable):
- drop private:true, version 0.1.0, files:[dist] — published tarball
  ships only the prebuilt static assets; build deps are devDependencies
  so consumers don't pull react/elkjs/etc

openhop CLI:
- depend on @openhop/server, @openhop/web, fastify, @fastify/static
- replace spawn-based serve with in-process startServer + new
  startWebServer (fastify-static of @openhop/web/dist/, SPA fallback)
- new `openhop demo` subcommand: bootstraps API+web, posts a bundled
  starter flow, opens the browser via xdg-open/open/start (no runtime
  dep on `open` package)
- esbuild externals updated for the new deps

Documentation cleanup:
- skills/openhop/SKILL.md: drop the "git clone the repo" fallback;
  agents run `npx openhop demo` or `npx openhop serve` instead (B9.4)
- docker-compose.yml: header comment marks it contributor-only (B9.5)
- README "Try it in 60 seconds": lead with `npx openhop demo`;
  install table rewritten with 4 scenarios (B9.6)
- README: drop the `#18-#37` blockquote (W6) — those issues are all
  closed and the install paths they tracked are now real

Verified end-to-end: `npm pack` of all three workspaces, install in
a clean dir, `npx openhop demo` boots both servers, /health responds,
the starter flow gets POSTed, the SPA fallback works, SIGTERM shuts
down cleanly. Test suites: shared 93/93, server 19/19, CLI 83/83.

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

coderabbitai Bot commented May 4, 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: 9d67567b-5351-4dc0-a038-24955921b569

📥 Commits

Reviewing files that changed from the base of the PR and between dd5c5f3 and 9baf956.

📒 Files selected for processing (3)
  • README.md
  • packages/server/package.json
  • packages/server/tsconfig.build.json
✅ Files skipped from review due to trivial changes (2)
  • packages/server/tsconfig.build.json
  • README.md

Walkthrough

Added an in-process demo CLI that starts API and web servers, posts a bundled starter flow, and optionally opens the browser; refactored server startup into exported buildApp/startServer functions; added an in-CLI static web server module; and updated docs and package metadata to support local npx openhop demo usage.

Changes

Demo Command & Server Refactoring

Layer / File(s) Summary
Type Definitions
packages/server/src/index.ts, packages/cli/src/web-server.ts
Added StartServerOptions, RunningServer, StartWebOptions, and RunningWeb interfaces to formalize startup options and returned handles.
Core Server Refactor
packages/server/src/index.ts
Extracted Fastify app construction into buildApp() and moved listening into startServer(); preserved CLI entrypoint to call startServer().
Static Web Server
packages/cli/src/web-server.ts
New Fastify-based static SPA server serving @openhop/web/dist/ with SPA fallback and a startWebServer() export.
Demo CLI Implementation
packages/cli/src/demo.ts
Added registerDemo(program) which starts API and web, posts STARTER_FLOW_YAML to /api/flows, prints machine-parseable readiness, optionally opens the browser, and handles graceful shutdown and error exit codes.
CLI Serve Integration
packages/cli/src/index.ts, packages/cli/src/help-json.ts
Rewrote serve to call startServer() and optional startWebServer() in-process (removed spawn/polling); registered demo command; updated serve examples.
Build / Packaging Changes
packages/cli/package.json, packages/server/package.json, packages/web/package.json, packages/server/tsconfig.build.json
Updated esbuild build flags and externals for CLI; made server/web packages publish-ready (version bump, exports, files, prepack/build scripts); moved some deps to devDependencies; added tsconfig.build.json to exclude tests.
Docs & Compose
README.md, skills/openhop/SKILL.md, docker-compose.yml
Quickstart and install sections updated to recommend npx openhop demo / npx openhop serve; added explanatory comment to docker-compose; removed previous clone-based dev instructions.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as CLI (npx openhop demo)
    participant APIServer as API Server (startServer)
    participant WebServer as Web Server (startWebServer)
    participant Flows as Flows API

    User ->> CLI: Run npx openhop demo
    CLI ->> APIServer: startServer() on port 8787
    APIServer -->> CLI: Running (url)
    CLI ->> WebServer: startWebServer() on port 8788
    WebServer -->> CLI: Running (url)
    CLI ->> Flows: POST /api/flows (STARTER_FLOW_YAML)
    Flows -->> CLI: Returns flowId
    CLI ->> User: Print readiness & flow URL
    CLI ->> User: Open browser (if enabled)
    User ->> WebServer: GET /flow/{flowId}
    WebServer -->> User: Serve SPA (index.html)
    User ->> CLI: SIGINT (Ctrl-C)
    CLI ->> APIServer: close()
    CLI ->> WebServer: close()
    CLI -->> User: Exit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: packaging server+web as npm packages and adding the openhop demo command, which aligns with the substantial refactoring across multiple package.json files, new CLI demo functionality, and documentation updates.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bundle-server-web-and-demo

Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

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/server/src/index.ts (1)

77-93: 💤 Low value

Consider displaying a user-friendly URL when binding to 0.0.0.0.

When host is 0.0.0.0 (e.g., from HOST=0.0.0.0 in Docker), the returned url will be http://0.0.0.0:8787, which isn't directly usable in a browser. Consider mapping 0.0.0.0 to 127.0.0.1 or localhost for display purposes while keeping the actual bind address unchanged.

Suggested change
   await app.listen({ port, host })
-  const url = `http://${host}:${port}`
+  const displayHost = host === '0.0.0.0' ? '127.0.0.1' : host
+  const url = `http://${displayHost}:${port}`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/index.ts` around lines 77 - 93, The returned URL uses the
actual bind host (so startServer currently returns "http://0.0.0.0:port" when
host is "0.0.0.0"); change startServer so it keeps binding to the real host for
app.listen but computes a displayHost (e.g., if host === "0.0.0.0" set
displayHost to "127.0.0.1" or "localhost") and build the returned url from
displayHost instead of host; update the returned object (url field) accordingly
while leaving app.listen({ port, host }) and close(): () => app.close()
unchanged.
🤖 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/server/package.json`:
- Around line 17-27: The package.json currently points "types" and the exports
"types" to ./src/index.ts while the "files" array only includes "dist", so
published packages will lack the source types; fix by either (A) add "src" to
the "files" array so ./src/index.ts is published (update the "files" array to
include "src"), or (B) change your build to emit declaration files (.d.ts) into
dist (enable "declaration": true / "declarationDir" in tsconfig and update your
build script) and then update "types" and the exports "types" entry to point to
the generated .d.ts in dist (e.g., dist/index.d.ts).

In `@README.md`:
- Line 66: Update the install table entry that currently shows "`npm install -g
openhop && openhop serve`" as the primary command so it leads with "`npx openhop
serve`" as the default (zero‑setup) invocation and list the global install "`npm
install -g openhop && openhop serve`" as an optional alternative; edit the table
row containing the text "`Long-lived local server`" to swap the order and
wording accordingly so the first example shown is "`npx openhop serve`" and the
second mentions the global install as an alternative.

---

Nitpick comments:
In `@packages/server/src/index.ts`:
- Around line 77-93: The returned URL uses the actual bind host (so startServer
currently returns "http://0.0.0.0:port" when host is "0.0.0.0"); change
startServer so it keeps binding to the real host for app.listen but computes a
displayHost (e.g., if host === "0.0.0.0" set displayHost to "127.0.0.1" or
"localhost") and build the returned url from displayHost instead of host; update
the returned object (url field) accordingly while leaving app.listen({ port,
host }) and close(): () => app.close() unchanged.
🪄 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: 10233868-5b3f-4259-821c-f11362a90ec7

📥 Commits

Reviewing files that changed from the base of the PR and between 99f927f and dd5c5f3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json
📒 Files selected for processing (11)
  • README.md
  • docker-compose.yml
  • packages/cli/package.json
  • packages/cli/src/demo.ts
  • packages/cli/src/help-json.ts
  • packages/cli/src/index.ts
  • packages/cli/src/web-server.ts
  • packages/server/package.json
  • packages/server/src/index.ts
  • packages/web/package.json
  • skills/openhop/SKILL.md

Comment thread packages/server/package.json
Comment thread README.md Outdated
Address CodeRabbit feedback on PR #66.

@openhop/server (Major):
- types field pointed at src/index.ts but files:[dist] excluded src/, so
  TS consumers of the published package would fail to resolve types.
- Switch to emitting declarations: tsconfig.build.json excludes test files,
  build now runs `tsc -p tsconfig.build.json --emitDeclarationOnly` before
  esbuild, types/exports.types point to dist/index.d.ts.
- Add `prepare: npm run build` so workspace installs build server before
  the CLI's own prepare runs (CLI's tsc resolves @openhop/server types
  via dist/index.d.ts after this).

README (Minor):
- Install table's "Long-lived local server" row now leads with
  `npx openhop serve`; global install kept as parenthetical alternative.
  Matches the new zero-setup positioning of the rest of the table.

Verified end-to-end: rebuilt all three workspaces, re-packed, installed
in a clean dir, `npx openhop demo` boots cleanly. Server tarball now
ships 10 files (server.js + 4 d.ts + 4 d.ts.map) instead of 2; web and
CLI tarball file counts unchanged. All workspace typechecks pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@naorsabag naorsabag merged commit c8392c7 into master May 4, 2026
7 checks passed
naorsabag added a commit that referenced this pull request May 4, 2026
…od CVE)

@fastify/static v8 has two moderate CVEs that #66 dragged into the
production tree when it added the package as a direct dep on the CLI:
  - GHSA-pr96-94w5-mx2h (path traversal in directory listing)
  - GHSA-x428-ghpx-8j92 (route-guard bypass via encoded separators)

Verified:
  - npm audit --omit=dev --audit-level=moderate → 0 vulnerabilities
  - All workspaces' tests pass (shared 93/93, server 19/19, cli 83/83,
    web 22/22 = 217 total)
  - All workspaces' coverage thresholds pass
  - Live serve smoke: /health ok, web UI + assets serve, SPA fallback
    intact, path-traversal probes return safe index.html (CVE patch
    confirmed in the running app).

The dev-only `esbuild`-via-`vitest@1` advisories that the original W7
finding called out are intentionally NOT addressed here. Per `01:38`
they're not a launch blocker, and the new audit gate in the follow-up
PR (#69) only fails dev-tree at high+. A vitest 4 bump would force
lowering coverage thresholds because @vitest/coverage-v8@4 instruments
arrow functions / inline callbacks more aggressively; not worth the
trade for non-blocking dev moderates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
naorsabag added a commit that referenced this pull request May 4, 2026
…od CVE)

@fastify/static v8 has two moderate CVEs that #66 dragged into the
production tree when it added the package as a direct dep on the CLI:
  - GHSA-pr96-94w5-mx2h (path traversal in directory listing)
  - GHSA-x428-ghpx-8j92 (route-guard bypass via encoded separators)

Verified:
  - npm audit --omit=dev --audit-level=moderate → 0 vulnerabilities
  - All workspaces' tests pass (shared 93/93, server 19/19, cli 83/83,
    web 22/22 = 217 total)
  - All workspaces' coverage thresholds pass
  - Live serve smoke: /health ok, web UI + assets serve, SPA fallback
    intact, path-traversal probes return safe index.html (CVE patch
    confirmed in the running app).

The dev-only `esbuild`-via-`vitest@1` advisories that the original W7
finding called out are intentionally NOT addressed here. Per `01:38`
they're not a launch blocker, and the new audit gate in the follow-up
PR (#69) only fails dev-tree at high+. A vitest 4 bump would force
lowering coverage thresholds because @vitest/coverage-v8@4 instruments
arrow functions / inline callbacks more aggressively; not worth the
trade for non-blocking dev moderates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
naorsabag added a commit that referenced this pull request May 4, 2026
…od CVE) (#68)

@fastify/static v8 has two moderate CVEs that #66 dragged into the
production tree when it added the package as a direct dep on the CLI:
  - GHSA-pr96-94w5-mx2h (path traversal in directory listing)
  - GHSA-x428-ghpx-8j92 (route-guard bypass via encoded separators)

Verified:
  - npm audit --omit=dev --audit-level=moderate → 0 vulnerabilities
  - All workspaces' tests pass (shared 93/93, server 19/19, cli 83/83,
    web 22/22 = 217 total)
  - All workspaces' coverage thresholds pass
  - Live serve smoke: /health ok, web UI + assets serve, SPA fallback
    intact, path-traversal probes return safe index.html (CVE patch
    confirmed in the running app).

The dev-only `esbuild`-via-`vitest@1` advisories that the original W7
finding called out are intentionally NOT addressed here. Per `01:38`
they're not a launch blocker, and the new audit gate in the follow-up
PR (#69) only fails dev-tree at high+. A vitest 4 bump would force
lowering coverage thresholds because @vitest/coverage-v8@4 instruments
arrow functions / inline callbacks more aggressively; not worth the
trade for non-blocking dev moderates.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
naorsabag added a commit that referenced this pull request May 4, 2026
The previous single `npm audit --audit-level=high` gate let W7's
moderate `@fastify/static` CVE land on master via #66 without flagging
it. Splitting the audit step:

  - prod tree (`--omit=dev --audit-level=moderate`) — anything moderate+
    that ships to end users blocks the merge.
  - full tree (`--audit-level=high`) — dev-only advisories don't reach
    users, so block on high+critical only to avoid noise.

Verified locally on master @ c8392c7 (pre-W7-fix tree): the new
prod-tree gate exits 1 (catches the @fastify/static GHSA), while the
full-tree gate stays at exit 0. Once #68 merges, both gates pass.

Also enabled Dependabot vulnerability alerts + automated security
fixes via the GitHub API on naorsabag/OpenHop. With those on, security
updates produce PRs that bypass dependabot.yml's major-version ignore
filter — closing the gap that let GHSA-pr96-94w5-mx2h linger silently
between #66 and #68.

Annotated dependabot.yml's ignore block to call out the security-update
bypass so future tightening doesn't accidentally close the gap again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
naorsabag added a commit that referenced this pull request May 4, 2026
The previous single `npm audit --audit-level=high` gate let W7's
moderate `@fastify/static` CVE land on master via #66 without flagging
it. Splitting the audit step:

  - prod tree (`--omit=dev --audit-level=moderate`) — anything moderate+
    that ships to end users blocks the merge.
  - full tree (`--audit-level=high`) — dev-only advisories don't reach
    users, so block on high+critical only to avoid noise.

Verified locally on master @ c8392c7 (pre-W7-fix tree): the new
prod-tree gate exits 1 (catches the @fastify/static GHSA), while the
full-tree gate stays at exit 0. Once #68 merges, both gates pass.

Also enabled Dependabot vulnerability alerts + automated security
fixes via the GitHub API on naorsabag/OpenHop. With those on, security
updates produce PRs that bypass dependabot.yml's major-version ignore
filter — closing the gap that let GHSA-pr96-94w5-mx2h linger silently
between #66 and #68.

Annotated dependabot.yml's ignore block to call out the security-update
bypass so future tightening doesn't accidentally close the gap again.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@naorsabag naorsabag deleted the feat/bundle-server-web-and-demo branch May 8, 2026 06:30
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