Skip to content

feat: run this repo and vercel-labs emulators together via a single-process launcher#24

Merged
amondnet merged 6 commits into
mainfrom
amondnet/vercel-labs
Jul 14, 2026
Merged

feat: run this repo and vercel-labs emulators together via a single-process launcher#24
amondnet merged 6 commits into
mainfrom
amondnet/vercel-labs

Conversation

@amondnet

@amondnet amondnet commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Lets users run this repo's emulators (Kakao, Naver, Toss, Firebase, etc.) alongside vercel-labs' upstream @emulators/* emulators (github, google, slack, stripe, aws, …) in one process, with one command.

Changes

  • build: align @emulators/core version ^0.6.0^0.9.0 across all 14 packages. The 0.6.0 and 0.9.0 cores are byte-identical, so this is a pure version-alignment change (no code diff) that lets this repo's @emulators/* service packages dedup onto the same shared core vercel-labs' emulators depend on.
  • feat: add scripts/emulate-all.ts (bun run emulate:all), a single-process launcher that hosts this repo's emulators and the upstream vercel-labs @emulators/* emulators together on the shared core.
  • Export SERVICE_NAMES from the @pleaseai/emulate public API.
  • Add root devDependencies + emulate:all script needed by the launcher.

Behavior

  • bun run emulate:all starts all requested emulators in a single process with stable per-service ports in separate ranges: this-repo services at 4000+, vercel-labs services at 4100+ (stable regardless of selection order).
  • Shared emulate.config.* seeding across both emulator sets.
  • Guards against port-range overlap.
  • Graceful shutdown on SIGINT/SIGTERM.
  • Flags: --service, --port, --vercel-port, --config.

Test Plan

  • Lint passes
  • Type-check passes (27 packages)
  • Build passes (14 packages)
  • Test passes (27 packages)
  • Launcher smoke-tested end-to-end: single PID hosting kakao + github + stripe together, config seeding verified, stable ports confirmed across different selection orders, graceful shutdown verified

Note for Reviewers

The launcher and its vercel @emulators/* dependencies are root devDependencies only — the published @pleaseai/emulate package's dependency footprint is unchanged.


Summary by cubic

Run this repo’s emulators and vercel-labs @emulators/* together in one process with a new single-command launcher. Aligns all packages to @emulators/core@^0.9.0 so both sets share the same core.

  • New Features

    • New command: bun run emulate:all to start selected services from both repos in one process.
    • Stable ports: this repo at 4000+, vercel-labs at 4100+; configure with --port and --vercel-port.
    • Shared seeding via emulate.config.{yaml,yml,json}; supports --service and --config.
    • Prevents port range overlap; graceful shutdown on SIGINT/SIGTERM.
    • Expose SERVICE_NAMES from @pleaseai/emulate.
  • Bug Fixes

    • Handle occupied ports by awaiting “listening” and rejecting on “error”; clearer startup errors with service names and stacks.
    • CLI hardening: validate numeric --port/--vercel-port; reject missing or empty --service and empty lists; dedupe names; warn when config has no known services; reject base port 0 and ranges that exceed 65535.
    • Seed webhooks for vercel services by threading WebhookDispatcher into seedFromConfig (Stripe webhooks now register).
    • Harden shutdown: server.close rejects with an Error; log per-service close failures; guard missing default exports.

Written for commit 1c102f9. Summary will update on new commits.

amondnet added 2 commits July 14, 2026 23:17
Bump @emulators/core from ^0.6.0 to ^0.9.0 across all service
packages so this repo's emulators dedup onto the same shared core
as vercel-labs' @emulators/* packages (the two core versions are
byte-identical, so no code changes were required). This is a
prerequisite for running both sets of emulators in a single process.
Add scripts/emulate-all.ts, a single-process launcher that hosts this
repo's services (via @pleaseai/emulate's createEmulator) alongside
vercel-labs' @emulators/* services (via the shared core's
createServer/serve) in one process. Services get stable per-service
ports in separate ranges (this-repo 4000+, vercel-labs 4100+), share
emulate.config.* seeding, guard against port-range overlap, and shut
down gracefully.

Export SERVICE_NAMES from @pleaseai/emulate's public API as the
single source of truth for the service list, consumed by the
launcher. Add the launcher's root devDependencies (bun's isolated
linker requires them to be direct root deps): @emulators/core plus
12 vercel service packages, @pleaseai/emulate (workspace:*),
picocolors, yaml, and the upstream emulate CLI for reference. Wire
up the emulate:all script.
@codacy-production

codacy-production Bot commented Jul 14, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 critical · 9 high

Alerts:
⚠ 10 issues (≤ 0 issues of at least minor severity)

Results:
10 new issues

Category Results
Security 1 critical
9 high

View in Codacy

🟢 Metrics 89 complexity · 1 duplication

Metric Results
Complexity 89
Duplication 1

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

amondnet added 2 commits July 14, 2026 23:48
- thread the WebhookDispatcher into vercel seedFromConfig so stripe
  webhook seed config is actually registered (was silently dropped)
- error on non-numeric --port/--vercel-port instead of silently
  falling back to the default
- warn (not silently start everything) when a config has no recognized
  service keys
- dedupe --service names to avoid a same-port bind crash
- name the failing service and keep the stack on startup errors
- guard a missing default export; log per-service close failures
Address the two genuine Codacy findings in the emulate-all launcher's
server close handler (L217):
- reject with a real Error when server.close reports one, instead of
  forwarding a possibly-undefined value (prefer-promise-reject-errors)
- give the close callback a block body so the arrow no longer returns a
  void expression (no-confusing-void-expression)

The remaining Codacy warnings are eslint-plugin-security false positives
for a trusted dev CLI launcher; the repo's own ESLint config intentionally
does not enforce those rules, and `eslint .` stays green.
@amondnet amondnet marked this pull request as ready for review July 14, 2026 18:18

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 18 files

Architecture diagram
sequenceDiagram
    participant Dev as Developer
    participant Launcher as emulate-all.ts
    participant Core as @emulators/core
    participant Please as @pleaseai/emulate
    participant VercelPkg as @emulators/stripe (example)
    participant ConfigFile as emulate.config.yaml

    Dev->>Launcher: bun run emulate:all [--service=kakao,stripe] [--port=5000]

    Note over Launcher: Parse args, compute stable base ports

    Launcher->>Launcher: parseArgs(process.argv)
    alt --port provided
        Launcher->>Launcher: use given port for this-repo base
    else
        Launcher->>Launcher: check EMULATE_PORT, PORT env, default 4000
    end
    alt --vercel-port provided
        Launcher->>Launcher: use given port for vercel-labs base
    else
        Launcher->>Launcher: vercelBase = pleaseBase + 100
    end

    Note over Launcher: Ensure range overlap guard
    alt pleaseBase + len(PLEASE_ORDER) >= vercelBase
        Launcher-->>Dev: ERROR: Port ranges overlap
        Launcher->>Launcher: process.exit(1)
    end

    Note over Launcher: Load config file
    alt --config provided
        Launcher->>ConfigFile: load explicit path
    else
        Launcher->>ConfigFile: probe emulate.config.{yaml,yml,json}
    end
    ConfigFile-->>Launcher: parsed SeedConfig

    Note over Launcher: Resolve service list
    alt --service given
        Launcher->>Launcher: split and dedupe services
    else config present with known keys
        Launcher->>Launcher: infer from config keys
    else fallback
        Launcher->>Launcher: include all services from both repos
    end

    Note over Launcher: Start services with stable ports
    loop For each service
        alt Service in PLEASE_SET (this-repo)
            Launcher->>Please: createEmulator({service, port, seed})
            Note right of Please: Uses @emulators/core internally
            Please-->>Launcher: Emulator instance (url, close)
        else Service in VERCEL_SET (vercel-labs)
            Launcher->>VercelPkg: import default export (ServicePlugin)
            Launcher->>Core: createServer(plugin, {port, baseUrl, tokens})
            Core-->>Launcher: {app, store, webhooks}
            alt config provides seed for this service
                Launcher->>VercelPkg: seedFromConfig(store, baseUrl, svcConfig, webhooks)
                Note right of VercelPkg: e.g. Stripe webhook registration
            end
            Launcher->>Core: serve({fetch: app.fetch, port})
            Core-->>Launcher: server handle
            Launcher->>Launcher: wrap close() to reject with Error
        end
        Launcher->>Launcher: store Running instance
    end

    Note over Launcher: Log started services with URLs
    Launcher-->>Dev: All services ready (e.g. kakao:4000, stripe:4102)

    Note over Launcher,Dev: Runtime operations… (services handle incoming requests)

    Dev->>Launcher: SIGINT/SIGTERM
    Launcher->>Launcher: gracefulShutdown()
    loop For each Running service
        Launcher->>Launcher: close() with timeout
        alt close succeeds
            Launcher->>Launcher: log success
        else close fails
            Launcher->>Launcher: log error
        end
    end
    Launcher->>Launcher: exit(0)
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread scripts/emulate-all.ts
Comment thread scripts/emulate-all.ts Outdated
@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a single-process launcher (scripts/emulate-all.ts, bun run emulate:all) that starts both this repo's emulators and the upstream vercel-labs @emulators/* emulators together, using stable per-service port ranges (4000+ for this-repo, 4100+ for vercel-labs). It also aligns all 14 packages from @emulators/core@^0.6.0 to ^0.9.0 so both sets share the same hoisted core module.

  • Launcher (scripts/emulate-all.ts): robust CLI parsing with numeric validation, deduplication, config auto-detection, port-range overlap guard, and graceful shutdown on SIGINT/SIGTERM; startVercel correctly awaits 'listening' before resolving, but startPlease delegates to createEmulator which calls serve() without the same guard, leaving port-occupied errors for "please" services unhandled.
  • @pleaseai/emulate API: exports SERVICE_NAMES from the public surface so the launcher can import the canonical service list.
  • Version alignment: all 14 per-service package.json files bump @emulators/core to ^0.9.0; the root devDependencies add the vercel-labs packages and an unscoped emulate@0.9.0 entry that does not appear to be used by the launcher script.

Confidence Score: 4/5

Safe to merge for the vercel-labs path, but the "please" service startup path has a real error-handling gap that leaves already-started services orphaned on port conflict.

The interaction between startPlease and createEmulator needs attention: createEmulator calls serve() and returns without awaiting the listening event, so a port-in-use error for any this-repo service fires as an unhandled async rejection, the try/catch in main() never sees it, and graceful shutdown never runs.

scripts/emulate-all.ts (startPlease) and packages/emulate/src/api.ts (createEmulator) — createEmulator should await the listening event before resolving, mirroring the guard already present in startVercel.

Important Files Changed

Filename Overview
scripts/emulate-all.ts New single-process launcher; startVercel correctly awaits port binding but startPlease doesn't, leaving a gap where port-occupied errors for "please" services bypass the try/catch and graceful shutdown.
packages/emulate/src/api.ts Adds SERVICE_NAMES re-export; createEmulator itself is unchanged but lacks the listening-event await that the new launcher's startVercel path relies on.
package.json Adds vercel-labs @emulators/* devDependencies and emulate:all script; includes an unscoped emulate@0.9.0 devDependency not visibly used by the launcher.
packages/asana/package.json Version bump @emulators/core from ^0.6.0 to ^0.9.0 for deduplication; no logic changes.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant Launcher as emulate-all.ts (main)
    participant startPlease
    participant createEmulator
    participant startVercel
    participant Core as @emulators/core (serve)

    User->>Launcher: bun run emulate:all --service kakao,github
    Launcher->>Launcher: parseArgs / basePortFrom / vercelBaseFrom
    Launcher->>Launcher: loadConfig (auto-detect emulate.config.yaml)
    Launcher->>Launcher: resolveServices
    Launcher->>Launcher: validate service names + port ranges

    Launcher->>startPlease: startPlease(kakao, 4000, config)
    startPlease->>createEmulator: "createEmulator({ service, port, seed })"
    createEmulator->>Core: "serve({ fetch, port: 4000 })"
    Note over createEmulator,Core: No listening await - port error bypasses try/catch
    createEmulator-->>startPlease: Emulator (url, reset, close)
    startPlease-->>Launcher: "Running { name, port, close }"

    Launcher->>startVercel: startVercel(github, 4101, config)
    startVercel->>Core: "createServer + serve({ fetch, port: 4101 })"
    startVercel->>Core: await listening event
    Core-->>startVercel: listening
    startVercel-->>Launcher: "Running { name, port, close }"

    Launcher->>User: Print service table
    User->>Launcher: Ctrl+C (SIGINT)
    Launcher->>Launcher: shutdown() - Promise.allSettled(running.map(r.close))
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant Launcher as emulate-all.ts (main)
    participant startPlease
    participant createEmulator
    participant startVercel
    participant Core as @emulators/core (serve)

    User->>Launcher: bun run emulate:all --service kakao,github
    Launcher->>Launcher: parseArgs / basePortFrom / vercelBaseFrom
    Launcher->>Launcher: loadConfig (auto-detect emulate.config.yaml)
    Launcher->>Launcher: resolveServices
    Launcher->>Launcher: validate service names + port ranges

    Launcher->>startPlease: startPlease(kakao, 4000, config)
    startPlease->>createEmulator: "createEmulator({ service, port, seed })"
    createEmulator->>Core: "serve({ fetch, port: 4000 })"
    Note over createEmulator,Core: No listening await - port error bypasses try/catch
    createEmulator-->>startPlease: Emulator (url, reset, close)
    startPlease-->>Launcher: "Running { name, port, close }"

    Launcher->>startVercel: startVercel(github, 4101, config)
    startVercel->>Core: "createServer + serve({ fetch, port: 4101 })"
    startVercel->>Core: await listening event
    Core-->>startVercel: listening
    startVercel-->>Launcher: "Running { name, port, close }"

    Launcher->>User: Print service table
    User->>Launcher: Ctrl+C (SIGINT)
    Launcher->>Launcher: shutdown() - Promise.allSettled(running.map(r.close))
Loading

Reviews (3): Last reviewed commit: "fix: reject empty --service and out-of-r..." | Re-trigger Greptile

Comment thread scripts/emulate-all.ts Outdated
Comment thread scripts/emulate-all.ts Outdated
Comment thread scripts/emulate-all.ts Outdated
Comment thread scripts/emulate-all.ts Outdated
- startVercel: await 'listening'/reject on 'error' so an occupied Vercel
  port rejects into main()'s try/catch (closing already-started services)
  instead of crashing via an async listen error (cubic P2)
- parseArgs: reject `--service` with no value instead of silently starting
  every emulator (cubic P3)
- basePortFrom/vercelBaseFrom: use `!== undefined` so an explicit port 0 is
  honored rather than falling through as falsy (greptile P2)
- startPlease: narrow with `name as ServiceName` instead of `name as never`,
  restoring type-checking at the call site (greptile P2)
- resolveServices: route the "no known service keys" notice through
  console.warn (stderr → warn) so it doesn't read as an error (greptile P2)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread scripts/emulate-all.ts Outdated
Comment thread scripts/emulate-all.ts
Follow-up to the review fixes, addressing cubic's re-review of the launcher:

- parseArgs: reject `--service ""` (empty string is falsy, so it slipped past
  the `=== undefined` guard and started every emulator); the `!value` check
  also drops the "unnecessary conditional" warning since argv indexing is typed
  as string
- resolveServices: reject a value that resolves to no names (e.g. `--service ,`)
  instead of starting nothing and idling
- main: reject a base port of 0 or any range that spills past 65535 for the
  range in use. Honoring an explicit `--port 0` (from the earlier falsy-guard
  fix) otherwise produced bogus localhost:0 URLs; erroring loudly keeps that
  fix's "no silent override" intent while covering the CLI and env sources
@sonarqubecloud

Copy link
Copy Markdown

@amondnet amondnet merged commit 22cd1a8 into main Jul 14, 2026
8 of 9 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 14, 2026
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