Skip to content

docs: add Astro Starlight documentation site deployed to Cloudflare Pages#20

Merged
amondnet merged 15 commits into
mainfrom
amondnet/docs-2
Jul 5, 2026
Merged

docs: add Astro Starlight documentation site deployed to Cloudflare Pages#20
amondnet merged 15 commits into
mainfrom
amondnet/docs-2

Conversation

@amondnet

@amondnet amondnet commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a documentation website under website/, built with Astro Starlight and deployed to Cloudflare Pages. The information architecture mirrors the original vercel-labs/emulate docs app (apps/web), adapted to this fork's 13 services.

  • Landing page — splash hero with the "stateful, production-fidelity, not mocks" pitch
  • Guides — Getting Started (CLI, port table, options), Configuration (emulate.config.yaml, tokens, seeding rules), Programmatic API (createEmulator/reset/close), Authentication (OAuth login page, ?user_id= CI auto-approve, per-service schemes)
  • Services — 13 pages generated from skills/*/SKILL.md via website/scripts/sync-services.ts (bun run sync:services); skills stay the single source of truth
  • Reference — Architecture and Emulator Conventions (from docs/EMULATOR-CONVENTIONS.md)

Deployment

  • website/wrangler.jsonc targets a Cloudflare Pages project emulate-docs (pages_build_output_dir: dist)
  • .github/workflows/docs.yml builds on PRs and deploys on pushes to main touching website/, skills/, or docs/
  • Requires CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID repository secrets before the deploy step can run

Test plan

  • bun run build — 21 pages built, Pagefind search index generated
  • astro preview smoke test — /, guides, service pages, reference all return 200; unknown path returns 404
  • No repo-relative links leaked into generated service pages

Summary by cubic

Adds a docs site under website/ using @astrojs/starlight, deployed to Cloudflare Pages with PR preview comments. Ports --portless HTTPS and improved base URL handling to the CLI; adds base-port env fallback and resolves lint/Codacy nits.

  • New Features

    • Docs: Starlight site (landing, Guides, Reference) with 13 Services pages generated by website/scripts/sync-services.ts; CI builds on every run, deploys main to production, PRs get preview URLs with a sticky comment; stale generated pages fail the build; workflow uses a concurrency group, pins actions by SHA, and skips deploy when Cloudflare secrets are missing. README links the live docs at https://emulate.pleaseai.dev.
    • CLI: --portless auto-registers <service>.emulate aliases (prompts install; requires portless proxy start; cleans up on exit) and advertises HTTPS URLs; base URL precedence is seed baseUrl--base-url (supports {service}) → EMULATE_BASE_URLPORTLESS_URLhttp://localhost:<port>; default base port falls back to EMULATE_PORT or PORT; options validation rejects --portless with --base-url and requires {service} for multi-service --base-url; tests cover interpolation, env fallbacks, and alias registration/rollback.
  • Migration

    • Cloudflare Pages: create project emulate-docs (output dist) and add repo secrets CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID.
    • Local docs dev: cd website && bun install && bun run dev. Optional HTTPS: npm i -g portless and portless proxy start before using --portless.

Written for commit b645fdb. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Added portless support for trusted HTTPS emulator URLs with per-service aliases.
    • Improved base URL handling, including {service} interpolation and clearer precedence across seed/config/env values.
  • Documentation
    • Published a new documentation site with getting-started, configuration, authentication, portless guide, architecture/conventions references, and service-specific pages (including auto-regenerated “Services” docs).
    • Updated the main README with HTTPS instructions via --portless.
  • Tests
    • Added coverage for base URL resolution and portless behavior.
  • Chores
    • Added a CI workflow to build, validate, and preview/deploy the docs site.

…ages

Modeled on the upstream vercel-labs/emulate docs app (apps/web):
guides (getting started, configuration, programmatic API, authentication),
reference (architecture, emulator conventions), and 13 service pages
generated from skills/*/SKILL.md via website/scripts/sync-services.ts.
Deploys to the emulate-docs Cloudflare Pages project from CI.
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@amondnet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 52af1b5c-d8d2-46e6-91fc-ec16568a01f7

📥 Commits

Reviewing files that changed from the base of the PR and between e115254 and b645fdb.

📒 Files selected for processing (2)
  • packages/emulate/src/commands/start.ts
  • packages/emulate/src/index.ts
📝 Walkthrough

Walkthrough

This PR adds an Astro Starlight docs site under website/, generates service documentation from skills/*/SKILL.md, and adds portless HTTPS support to emulator base URL handling, CLI startup, and alias management. It also updates docs content, tests, and deployment config.

Changes

Documentation website

Layer / File(s) Summary
Site scaffolding and deploy config
.github/workflows/docs.yml, website/package.json, website/astro.config.mjs, website/src/content.config.ts, website/tsconfig.json, website/wrangler.jsonc, website/.gitignore, website/README.md, README.md
Adds the docs workflow, website package and build config, Astro/Starlight setup, content collection schema, Wrangler Pages config, ignore rules, and README instructions, including the portless getting-started step.
Service doc generation
website/scripts/sync-services.ts
Adds the generator that reads skills/<service>/SKILL.md, derives page metadata, and writes Starlight service docs.
Guides and homepage
website/src/content/docs/guides/*, website/src/content/docs/index.mdx
Adds the getting-started, configuration, authentication, programmatic API, and portless guides, plus the homepage MDX content.
Reference documentation
website/src/content/docs/reference/*, docs/EMULATOR-CONVENTIONS.md
Adds architecture and conventions reference pages covering monorepo structure, plugin contracts, auth and webhook behavior, and testing/style conventions.
Service documentation pages
website/src/content/docs/services/*, skills/*/SKILL.md
Adds the service reference pages for Asana, Autumn, Firebase, GitLab, Kakao, Linear, Naver, PostHog, Spotify, Supabase, Toss Payments, WorkOS, and X, plus matching skill doc updates.

Portless HTTPS support

Layer / File(s) Summary
Base URL resolution
packages/emulate/src/base-url.ts, packages/emulate/src/__tests__/base-url.test.ts
Updates base URL resolution to normalize interpolated service URLs, add environment-variable fallbacks, and verify the new precedence in tests.
Portless helper runtime
packages/emulate/src/portless.ts, packages/emulate/src/__tests__/portless.test.ts
Adds the portless helper module for availability checks, installation prompts, alias registration and removal, HTTPS base URL construction, and coverage for those flows.
Start command wiring
packages/emulate/src/commands/start.ts, packages/emulate/src/index.ts, sonar-project.properties, eslint.config.ts
Adds the portless start option, enforces base-URL validation rules, wires alias lifecycle management, updates CLI option text, and adjusts lint/coverage configuration for the new command paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • pleaseai/emulate#14: Adds the skills/*/SKILL.md files that the new service-doc generator reads to produce the website’s service pages.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a Starlight docs site and deploying it to Cloudflare Pages.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch amondnet/docs-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@socket-security

socket-security Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedastro@​5.18.288768898100
Addedstarlight-theme-black@​0.6.0761007980100
Added@​astrojs/​starlight@​0.36.3991008597100
Addedwrangler@​4.107.0981009296100
Addedsharp@​0.34.59210010096100

View full report

@socket-security

socket-security Bot commented Jul 4, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @astrojs/compiler is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/astro@5.18.2npm/@astrojs/compiler@2.13.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@astrojs/compiler@2.13.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm astro is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: website/package.jsonnpm/astro@5.18.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/astro@5.18.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm css-tree is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/astro@5.18.2npm/css-tree@3.2.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/css-tree@3.2.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm node-fetch-native is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/astro@5.18.2npm/node-fetch-native@1.6.7

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/node-fetch-native@1.6.7. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@codacy-production

codacy-production Bot commented Jul 4, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 101 complexity · 2 duplication

Metric Results
Complexity 101
Duplication 2

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.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an Astro Starlight documentation site under website/ deployed to Cloudflare Pages, and extends the CLI with --portless HTTPS alias support and a reworked resolveBaseUrl priority chain. The workflow is well-structured — all actions are SHA-pinned, a staleness guard fails the build when generated service pages are out of sync with SKILL.md sources, and fork PRs safely skip deployment when secrets are absent.

  • Docs site (website/): Starlight site with 13 service pages auto-generated from skills/*/SKILL.md by sync-services.ts; CI builds on every run and deploys main to emulate.pleaseai.dev, with sticky preview comments on PRs.
  • Portless feature (portless.ts, start.ts): --portless auto-registers <service>.emulate HTTPS aliases via the portless CLI; ensurePortless handles install prompts and proxy checks cleanly using process.exit, but registerAliases throws a raw Error on failure which is not caught in setupPortless, surfacing as a stack trace instead of a clean message.
  • Base URL refactor (base-url.ts, start-options.ts): New priority chain (seed → flag → EMULATE_BASE_URLPORTLESS_URL → localhost) with {service} interpolation; extracted into a testable validateBaseUrlOptions helper with good coverage.

Confidence Score: 4/5

Safe to merge with one fix needed in the portless alias registration error path.

The docs site and CI workflow are solid. The portless feature introduces a gap: registerAliases throws a raw Error on failure, but setupPortless has no try/catch, so a registration failure during --portless startup prints a stack trace rather than the clean user-facing message the rest of the CLI produces. Everything else — SHA-pinned actions, staleness guard, base-URL priority chain, and test coverage — looks correct.

packages/emulate/src/commands/start.ts and packages/emulate/src/portless.ts — the setupPortless/registerAliases error handling path.

Important Files Changed

Filename Overview
packages/emulate/src/portless.ts New module for portless HTTPS alias management; registerAliases throws on failure while the rest of the CLI uses process.exit, creating inconsistent error handling when alias registration fails
packages/emulate/src/commands/start.ts Refactored to support portless and {service} interpolation; setupPortless does not catch the throw from registerAliases, causing a raw stack trace on registration failures
packages/emulate/src/base-url.ts Priority order changed so seedBaseUrl wins over explicit baseUrl/portless URL; adds EMULATE_BASE_URL and PORTLESS_URL env-var fallbacks and {service} interpolation throughout
packages/emulate/src/start-options.ts New module extracting defaultBasePort (env-var fallback chain) and validateBaseUrlOptions (mutually exclusive flags); clean and well-tested
.github/workflows/docs.yml Well-structured docs CI: all actions SHA-pinned, staleness guard for generated service pages, graceful secret detection for fork PRs, sticky PR preview comment
website/scripts/sync-services.ts Generates services/.md from skills//SKILL.md at build time; correctly uses JSON.stringify for description, drops the leading H1, and emits Starlight-compatible frontmatter
website/astro.config.mjs Standard Starlight configuration with sidebar, social links, and edit URL; uses autogenerate for services directory
packages/emulate/src/tests/portless.test.ts Comprehensive tests for portless module covering happy path, rollback on failure, proxy check, and interactive/non-interactive install prompts
packages/emulate/src/tests/base-url.test.ts Good coverage of resolveBaseUrl priority chain, {service} interpolation, and validateBaseUrlOptions; correctly resets env vars before each test

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[startCommand] --> B[defaultBasePort]
    A --> C[loadSeedConfig]
    A --> D[resolveServices]
    A --> E[validateBaseUrlOptions]
    E -->|error| F[process.exit 1]
    E -->|ok| G{portless?}
    G -->|yes| H[setupPortless]
    H --> I[ensurePortless]
    I -->|not installed, non-TTY| F
    I -->|not installed, TTY| J[prompt install]
    J -->|declined| F
    J -->|accepted| K[npm i -g portless]
    K -->|fail| F
    I -->|installed| L{proxy running?}
    L -->|no| F
    L -->|yes| M[buildAliases]
    M --> N[registerAliases]
    N -->|spawnSync fails| O[throws Error - no catch in setupPortless]
    N -->|ok| P[process.on exit cleanup]
    G -->|no| Q[createEmulator loop]
    P --> Q
    Q --> R[resolveBaseUrl per service]
    R -->|1 seedBaseUrl| S[use seed URL]
    R -->|2 baseUrl flag| T[use flag URL]
    R -->|3 EMULATE_BASE_URL| U[use env URL]
    R -->|4 PORTLESS_URL| V[use portless env]
    R -->|5 fallback| W[http://localhost:port]
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"}}}%%
flowchart TD
    A[startCommand] --> B[defaultBasePort]
    A --> C[loadSeedConfig]
    A --> D[resolveServices]
    A --> E[validateBaseUrlOptions]
    E -->|error| F[process.exit 1]
    E -->|ok| G{portless?}
    G -->|yes| H[setupPortless]
    H --> I[ensurePortless]
    I -->|not installed, non-TTY| F
    I -->|not installed, TTY| J[prompt install]
    J -->|declined| F
    J -->|accepted| K[npm i -g portless]
    K -->|fail| F
    I -->|installed| L{proxy running?}
    L -->|no| F
    L -->|yes| M[buildAliases]
    M --> N[registerAliases]
    N -->|spawnSync fails| O[throws Error - no catch in setupPortless]
    N -->|ok| P[process.on exit cleanup]
    G -->|no| Q[createEmulator loop]
    P --> Q
    Q --> R[resolveBaseUrl per service]
    R -->|1 seedBaseUrl| S[use seed URL]
    R -->|2 baseUrl flag| T[use flag URL]
    R -->|3 EMULATE_BASE_URL| U[use env URL]
    R -->|4 PORTLESS_URL| V[use portless env]
    R -->|5 fallback| W[http://localhost:port]
Loading

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/emulate/src/commands/start.ts:103-108
**Unhandled throw from `registerAliases` surfaces as a raw exception**

`registerAliases` in `portless.ts` throws `new Error('Failed to register portless alias: …')` when any alias fails. Since `setupPortless` doesn't wrap it in try/catch, the error propagates as a rejected promise through `startCommand`. Commander.js will print the error, but the output is a raw `Error:` stack trace rather than the clean `console.error + process.exit(1)` pattern used everywhere else in the CLI (including `ensurePortless` itself). A user who hits a registration failure on their first `--portless` run will see a confusing stack trace instead of an actionable message.

Reviews (9): Last reviewed commit: "chore: move EMULATE_PORT/PORT fallback i..." | Re-trigger Greptile

Comment thread .github/workflows/docs.yml Outdated
Comment thread website/scripts/sync-services.ts
Comment thread website/scripts/sync-services.ts
Ignore website content markdown (like docs/**) and fix curly/regexp
lint errors in sync-services.ts.

@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.

4 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="website/src/content/docs/guides/configuration.md">

<violation number="1" location="website/src/content/docs/guides/configuration.md:87">
P2: The `EMULATE_PORT` and `PORT` environment variables are claimed as an alternative for setting the base port, but no code in the repository reads these env vars. The CLI only accepts `--port` (default 4000). Either remove this section or implement the env-var fallback in the CLI.</violation>
</file>

<file name=".github/workflows/docs.yml">

<violation number="1" location=".github/workflows/docs.yml:21">
P2: Missing concurrency group to cancel in-progress runs on duplicate PR pushes. All other workflows in this repo (`ci.yml`, `release.yml`) use a concurrency group with `cancel-in-progress: true` for PRs — without it, rapid pushes to a PR will queue instead of cancelling the stale run, wasting CI minutes.</violation>
</file>

<file name="website/src/content/docs/services/linear.md">

<violation number="1" location="website/src/content/docs/services/linear.md:35">
P2: The Start section is missing the "(linear's slot is \`4006\`)" parenthetical that the Asana and Firebase docs include. Without it, users reading this page in isolation don't know which port Linear occupies in a multi-service setup. The `-p 4012` example is also misleading since 4012 belongs to X (the 13th service). Either add the slot mention as the other services do, or change the example to `-p 4012` only if the intent is to demonstrate choosing a completely unrelated port — otherwise prefer `-p 4006` or omit the specific number. Both nits trace back to the SKILL.md source in skills/linear/SKILL.md and can be fixed there before re-syncing.</violation>
</file>

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

Re-trigger cubic

Comment thread website/src/content/docs/services/linear.md
Comment thread website/src/content/docs/guides/configuration.md
Comment thread .github/workflows/docs.yml Outdated
Comment thread .github/workflows/docs.yml Outdated
- pin bun via packageManager in website/package.json (unambiguous bun-version-file)
- extract duplicated toss-payments package-name mapping into a variable
- fail CI when generated services/*.md drift from skills/

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

🧹 Nitpick comments (2)
.github/workflows/docs.yml (2)

22-24: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Bun setup: caching enabled by default and unpinned tag reference.

Two separate static-analysis findings on this step:

  • oven-sh/setup-bun's no-cache input defaults to false, so the downloaded Bun executable is cached by default — flagged by zizmor as a cache-poisoning risk since a poisoned cache entry could persist across runs.
  • The action is referenced by mutable tag (@v2) rather than a pinned full-length commit SHA, so its behavior can change without review (also flagged for cloudflare/wrangler-action@v3 at line 30).
🔒 Proposed fix (pin + disable cache)
-      - uses: oven-sh/setup-bun@v2
+      - uses: oven-sh/setup-bun@<pinned-sha> # v2.x.x
         with:
           bun-version-file: package.json
+          no-cache: true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docs.yml around lines 22 - 24, The `setup-bun` workflow
step is using a mutable action tag and leaving Bun caching enabled by default.
Update the `oven-sh/setup-bun` reference to a pinned full-length commit SHA, and
set its `no-cache` input to disable executable caching. Apply the same pinning
approach to any other action references in this workflow that still use version
tags, such as the `wrangler` step.

Source: Linters/SAST tools


30-30: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Pin cloudflare/wrangler-action to a commit SHA.

Same third-party-action-pinning concern as the setup-bun step: tag references (@v3) are mutable and not immutable releases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docs.yml at line 30, The workflow step using
cloudflare/wrangler-action is pinned to a mutable tag, so update the uses
reference to a specific commit SHA instead of `@v3`. Locate the wrangler-action
step in the docs workflow and replace the tag with an immutable commit hash,
matching the pinning approach used for other third-party actions in this file.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/docs.yml:
- Line 21: The docs workflow checkout step is leaving the default GITHUB_TOKEN
persisted in git config, which exposes credentials to later steps like bun
install and postinstall scripts. Update the actions/checkout usage in the docs
workflow to disable credential persistence by setting persist-credentials to
false, and keep the deploy step relying on its own Cloudflare token. Locate the
change at the checkout step in the docs workflow.
- Around line 28-35: The Cloudflare Pages deploy step in the docs workflow only
excludes pull requests, so manual runs can still deploy from non-main branches.
Update the deploy job’s guard on the Deploy to Cloudflare Pages step to also
require the main branch for workflow_dispatch, using the existing workflow
context around github.event_name and github.ref so only main can trigger
production deployment.

In `@website/package.json`:
- Line 11: The website package’s deploy workflow is missing the service-doc sync
step, so `deploy` can publish stale generated docs. Update the `deploy` script
in `package.json` to run the same `sync-services` step used by CI before `astro
build`, so `bun run deploy` always rebuilds from fresh `skills/*/SKILL.md`
content. Keep the existing `wrangler pages deploy dist` step after the build and
preserve the current script naming/entrypoint.

In `@website/src/content/docs/guides/configuration.md`:
- Around line 34-56: Normalize the YAML examples in the configuration docs so
they match the repo’s YAML style: update the tokens example and the supabase
mapping to use the preferred flow/block formatting consistently. Keep the
examples under the tokens, kakao, and supabase keys readable and stylistically
aligned with the rest of the documentation, avoiding mixed inline/flow styles
where the repo expects block style.

In `@website/src/content/docs/reference/conventions.md`:
- Around line 148-170: Normalize the example in the docs snippet to match the
repository style by updating the test setup around makeApp and the token test to
use single quotes, remove semicolons, and delete the unused beforeEach import
from bun:test. Keep the rest of the example intact while ensuring the import
list and string literals in the shown code follow the same markdown conventions
as nearby examples.

In `@website/src/content/docs/services/asana.md`:
- Around line 53-60: The token seeding docs in the Asana page are overstating
how login matching works. Update the `tokens` example and surrounding text to
describe only the actual `tokens[token] -> { login, scopes }` contract, and
remove any claim that `login` can match by email, gid, or name unless that
behavior is confirmed elsewhere. Keep the guidance aligned with the existing
`tokens` config shape in the docs content.

In `@website/src/content/docs/services/naver.md`:
- Around line 40-41: The JSON example in the Naver docs is malformed because the
error payload in the text uses comma-separated keys instead of key/value pairs.
Update the example in the Naver service documentation to show a valid JSON
object for the OAuth/token error response, keeping the same meaning but fixing
the punctuation so the payload shape is syntactically correct.

---

Nitpick comments:
In @.github/workflows/docs.yml:
- Around line 22-24: The `setup-bun` workflow step is using a mutable action tag
and leaving Bun caching enabled by default. Update the `oven-sh/setup-bun`
reference to a pinned full-length commit SHA, and set its `no-cache` input to
disable executable caching. Apply the same pinning approach to any other action
references in this workflow that still use version tags, such as the `wrangler`
step.
- Line 30: The workflow step using cloudflare/wrangler-action is pinned to a
mutable tag, so update the uses reference to a specific commit SHA instead of
`@v3`. Locate the wrangler-action step in the docs workflow and replace the tag
with an immutable commit hash, matching the pinning approach used for other
third-party actions in this file.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8daed626-e16a-481f-a2e0-220cab79d38a

📥 Commits

Reviewing files that changed from the base of the PR and between 0ea1419 and 7eadcf7.

⛔ Files ignored due to path filters (2)
  • website/bun.lock is excluded by !**/*.lock
  • website/public/favicon.svg is excluded by !**/*.svg
📒 Files selected for processing (29)
  • .github/workflows/docs.yml
  • website/.gitignore
  • website/README.md
  • website/astro.config.mjs
  • website/package.json
  • website/scripts/sync-services.ts
  • website/src/content.config.ts
  • website/src/content/docs/guides/authentication.md
  • website/src/content/docs/guides/configuration.md
  • website/src/content/docs/guides/getting-started.md
  • website/src/content/docs/guides/programmatic-api.md
  • website/src/content/docs/index.mdx
  • website/src/content/docs/reference/architecture.md
  • website/src/content/docs/reference/conventions.md
  • website/src/content/docs/services/asana.md
  • website/src/content/docs/services/autumn.md
  • website/src/content/docs/services/firebase.md
  • website/src/content/docs/services/gitlab.md
  • website/src/content/docs/services/kakao.md
  • website/src/content/docs/services/linear.md
  • website/src/content/docs/services/naver.md
  • website/src/content/docs/services/posthog.md
  • website/src/content/docs/services/spotify.md
  • website/src/content/docs/services/supabase.md
  • website/src/content/docs/services/tosspayments.md
  • website/src/content/docs/services/workos.md
  • website/src/content/docs/services/x.md
  • website/tsconfig.json
  • website/wrangler.jsonc

Comment thread .github/workflows/docs.yml Outdated
Comment thread .github/workflows/docs.yml Outdated
Comment thread website/package.json Outdated
Comment thread website/src/content/docs/guides/configuration.md
Comment thread website/src/content/docs/reference/conventions.md
Comment thread website/src/content/docs/services/asana.md
Comment thread website/src/content/docs/services/naver.md Outdated
Comment thread .github/workflows/docs.yml Outdated
- pin docs workflow actions to full commit SHAs (repo convention),
  using mise-action to provision bun like ci.yml
- iterate the service map instead of readdir in sync-services.ts

@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.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/docs.yml">

<violation number="1" location=".github/workflows/docs.yml:27">
P2: The staleness check only validates tracked file diffs, so untracked generated service pages can slip through CI. `git diff --exit-code` reports changes for tracked files only; it exits 0 when only untracked files are present. If `bun scripts/sync-services.ts` creates a new `.md` file for a newly added skill that hasn't been committed yet, this step passes silently and the generated docs remain incomplete on `main`. Consider expanding the check to also detect untracked files, for example by using `git status --short -- src/content/docs/services`.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread .github/workflows/docs.yml Outdated
amondnet added 2 commits July 4, 2026 19:01
Covers using vercel-labs/portless as a local HTTPS proxy in front of the
emulators via --base-url / per-service seed baseUrl, and notes that the
upstream --portless flag is not implemented in this fork.
Ports vercel-labs/emulate portless integration: --portless auto-registers
<service>.emulate aliases (removed on shutdown, prompts install when
missing), and resolveBaseUrl gains the upstream fallback chain — seed
baseUrl > --base-url > EMULATE_BASE_URL > PORTLESS_URL > localhost — with
{service} interpolation. --base-url now works with multiple services when
it contains a {service} placeholder.

Docs: portless guide now documents the flag; README and getting-started
options updated.
@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.97980% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/emulate/src/portless.ts 97.05% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread packages/emulate/src/portless.ts Fixed
Comment thread packages/emulate/src/portless.ts Fixed
Comment thread packages/emulate/src/portless.ts Fixed
Comment thread packages/emulate/src/portless.ts Fixed
Comment thread packages/emulate/src/portless.ts Fixed

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/emulate/src/__tests__/base-url.test.ts`:
- Around line 8-22: The resolveBaseUrl tests are only cleaning EMULATE_BASE_URL
and PORTLESS_URL in afterEach, so the first case can still read ambient
environment values before it runs. Update the base-url.test.ts setup around
describe('resolveBaseUrl') to clear both env vars before each test, while
keeping the existing restore logic in afterEach, so the defaults behavior is
isolated for the first test and any later cases.

In `@packages/emulate/src/commands/start.ts`:
- Line 112: The cleanup arrow in start.ts currently uses a shorthand body that
returns the void result of removeAliases, which triggers the void-expression
lint rule. Update the cleanup symbol to use a block body and call
removeAliases(aliases) inside it so the function explicitly returns nothing
while preserving the same cleanup behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b0e99ba1-ee6c-4fc7-96d0-ffc53ecf79de

📥 Commits

Reviewing files that changed from the base of the PR and between 7eadcf7 and 405734f.

📒 Files selected for processing (13)
  • .github/workflows/docs.yml
  • README.md
  • eslint.config.ts
  • packages/emulate/src/__tests__/base-url.test.ts
  • packages/emulate/src/base-url.ts
  • packages/emulate/src/commands/start.ts
  • packages/emulate/src/index.ts
  • packages/emulate/src/portless.ts
  • website/astro.config.mjs
  • website/package.json
  • website/scripts/sync-services.ts
  • website/src/content/docs/guides/getting-started.md
  • website/src/content/docs/guides/portless.md
✅ Files skipped from review due to trivial changes (2)
  • website/package.json
  • README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • website/astro.config.mjs
  • website/scripts/sync-services.ts
  • website/src/content/docs/guides/getting-started.md

Comment thread packages/emulate/src/__tests__/base-url.test.ts Outdated
Comment thread packages/emulate/src/commands/start.ts Outdated

@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.

2 issues found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/emulate/src/portless.ts">

<violation number="1" location="packages/emulate/src/portless.ts:71">
P2: `registerAliases` uses `portless alias … --force`, which overwrites any existing alias with the same name. If a later alias in the same batch fails, the rollback loop calls `removeAliases(registered)` and deletes every successfully processed alias name outright—including ones that existed before the call—instead of restoring their prior mappings. This breaks atomicity and can silently destroy pre-existing local portless configuration.

A concrete failure chain: alias `foo` already maps to port 9000; `registerAliases` overwrites it to port 3000 successfully; the next alias fails; rollback removes `foo` entirely rather than restoring `foo -> 9000`.

Consider checking whether an alias already exists before forcing the overwrite, and restoring the original mapping (or at least skipping removal of overwritten aliases) during rollback.</violation>
</file>

<file name="packages/emulate/src/__tests__/base-url.test.ts">

<violation number="1" location="packages/emulate/src/__tests__/base-url.test.ts:8">
P2: The `afterEach` block restores env vars after each test, but there's no `beforeEach` to clear them before the first test runs. If `EMULATE_BASE_URL` or `PORTLESS_URL` happen to be set in the ambient environment (e.g., in CI or a developer's shell), the first test case (`'defaults to localhost with the port'`) will resolve to the env value instead of the expected default, causing a flaky failure.

Adding a `beforeEach` that deletes both variables ensures consistent isolation from the first test onward.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/emulate/src/portless.ts Outdated
Comment thread packages/emulate/src/__tests__/base-url.test.ts Outdated
amondnet added 3 commits July 5, 2026 12:16
Addresses the SonarCloud quality gate: extracts resolveServices /
validateBaseUrlOptions / setupPortless from startCommand (cognitive
complexity), moves alias construction into buildAliases, adds tests for
alias registration/rollback/removal with a mocked child_process, and
marks the intentional PATH-based portless invocations with NOSONAR.
Comment thread packages/emulate/src/commands/start.ts
… gate fixes

- docs workflow now deploys every run: main goes to production, PR
  branches get a Cloudflare Pages preview (fork PRs skipped), following
  the wrangler-action recommended setup with gitHubToken deployment
  tracking; a sticky PR comment posts the preview + branch alias URLs
- cover ensurePortless fully (mocked child_process/readline/process.exit)
  and fix the misplaced NOSONAR on the multi-line spawnSync call
- exclude CLI wiring (index.ts, commands/) from Sonar coverage like the
  existing openapi.ts exclusion; add braces to a void-returning arrow
  (Codacy)

@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.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/docs.yml">

<violation number="1" location=".github/workflows/docs.yml:21">
P2: Missing concurrency group to cancel in-progress runs on duplicate PR pushes. All other workflows in this repo (`ci.yml`, `release.yml`) use a concurrency group with `cancel-in-progress: true` for PRs — without it, rapid pushes to a PR will queue instead of cancelling the stale run, wasting CI minutes.</violation>
</file>

<file name="packages/emulate/src/portless.ts">

<violation number="1" location="packages/emulate/src/portless.ts:71">
P2: `registerAliases` uses `portless alias … --force`, which overwrites any existing alias with the same name. If a later alias in the same batch fails, the rollback loop calls `removeAliases(registered)` and deletes every successfully processed alias name outright—including ones that existed before the call—instead of restoring their prior mappings. This breaks atomicity and can silently destroy pre-existing local portless configuration.

A concrete failure chain: alias `foo` already maps to port 9000; `registerAliases` overwrites it to port 3000 successfully; the next alias fails; rollback removes `foo` entirely rather than restoring `foo -> 9000`.

Consider checking whether an alias already exists before forcing the overwrite, and restoring the original mapping (or at least skipping removal of overwritten aliases) during rollback.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread sonar-project.properties
amondnet added 2 commits July 5, 2026 12:53
- linear/naver skill docs: add port-slot note, fix invalid JSON example
  (regenerated website service pages)
- implement EMULATE_PORT/PORT env fallback for the base port so the
  documented behavior is real (start-options.ts, tested)
- move validateBaseUrlOptions out of commands/ so it stays under
  coverage gates
- base-url.test.ts: clear env in beforeEach so ambient vars can't
  pollute the first test
- website deploy script: sync service pages before building
- docs.yml: staleness check now catches untracked generated pages;
  deploy step skips (instead of failing) when Cloudflare secrets are
  absent
- normalize the conventions test example to repo code style

@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.

1 issue found across 12 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread skills/linear/SKILL.md Outdated
- docs.yml: add concurrency group (repo convention) and
  persist-credentials: false on checkout
- clarify linear port-slot wording (positional assignment)
- avoid dynamically computed env keys (Codacy object-injection)
amondnet added 2 commits July 5, 2026 13:16
Add braces to a void-returning arrow in the readline mock and hoist the
defaultBasePort() call to a const (Codacy misreads the inline call as an
HTML-context sink).
Codacy misflags any variable near the '<port>' help text as an HTML
sink; resolving the default inside startCommand keeps index.ts free of
the pattern and the behavior identical (flag > env > 4000).
@amondnet amondnet merged commit 80c91db into main Jul 5, 2026
6 of 8 checks passed
@sonarqubecloud

sonarqubecloud Bot commented Jul 5, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
.github/workflows/docs.yml (1)

33-33: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

mise-action caching flagged for cache-poisoning by static analysis.

jdx/mise-action caches tool installs by default (cache: true). Since this job also deploys to production (Cloudflare Pages) on pushes to main, a poisoned cache entry could theoretically be restored in a privileged run. Risk is low-confidence per zizmor (this isn't a tag/release-triggered publish workflow), but consider pinning a scoped cache_key or disabling caching if it's not needed for a docs-only build.

Optional: disable or scope caching
       - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
+        with:
+          cache: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docs.yml at line 33, The docs workflow uses mise-action
with default caching, which can be risky in a production-deploying job. Update
the mise-action step in the docs workflow to either disable caching or provide a
scoped cache_key so cached tool installs cannot be reused across privileged
runs. Keep the change focused on the jdx/mise-action step and preserve the
existing docs deploy behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/emulate/src/__tests__/base-url.test.ts`:
- Around line 6-28: The cleanup in `afterAll` only restores `savedEnv` entries
that were originally defined, so any env var that started unset (like
`EMULATE_PORT` or `PORT`) can remain polluted after the tests. Update the
`afterAll` restore logic in `base-url.test.ts` to explicitly delete
`process.env` keys whose saved value is `undefined`, and restore only the
defined ones, using the existing `savedEnv` object and env var names already
captured at the top of the test file.

In `@packages/emulate/src/commands/start.ts`:
- Around line 92-119: The CLI entrypoint is still using a synchronous parse, so
async command failures from startCommand can surface as unhandled rejections.
Update the program.parse() call in the index entrypoint to use
program.parseAsync(process.argv), and add rejection handling there so errors are
reported cleanly to the user instead of bubbling uncaught.

---

Nitpick comments:
In @.github/workflows/docs.yml:
- Line 33: The docs workflow uses mise-action with default caching, which can be
risky in a production-deploying job. Update the mise-action step in the docs
workflow to either disable caching or provide a scoped cache_key so cached tool
installs cannot be reused across privileged runs. Keep the change focused on the
jdx/mise-action step and preserve the existing docs deploy behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fa11c019-ac26-40cd-a097-747498125866

📥 Commits

Reviewing files that changed from the base of the PR and between a432f1b and e115254.

📒 Files selected for processing (17)
  • .github/workflows/docs.yml
  • README.md
  • docs/EMULATOR-CONVENTIONS.md
  • packages/emulate/src/__tests__/base-url.test.ts
  • packages/emulate/src/__tests__/portless.test.ts
  • packages/emulate/src/commands/start.ts
  • packages/emulate/src/index.ts
  • packages/emulate/src/portless.ts
  • packages/emulate/src/start-options.ts
  • skills/linear/SKILL.md
  • skills/naver/SKILL.md
  • sonar-project.properties
  • website/README.md
  • website/package.json
  • website/src/content/docs/reference/conventions.md
  • website/src/content/docs/services/linear.md
  • website/src/content/docs/services/naver.md
✅ Files skipped from review due to trivial changes (6)
  • sonar-project.properties
  • docs/EMULATOR-CONVENTIONS.md
  • website/src/content/docs/services/linear.md
  • website/src/content/docs/reference/conventions.md
  • website/README.md
  • website/src/content/docs/services/naver.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • README.md
  • website/package.json

Comment on lines +6 to +28
const savedEnv = {
EMULATE_BASE_URL: process.env.EMULATE_BASE_URL,
PORTLESS_URL: process.env.PORTLESS_URL,
EMULATE_PORT: process.env.EMULATE_PORT,
PORT: process.env.PORT,
}

// Clear before each test (not just after) so ambient shell/CI env vars
// cannot leak into the first test case.
beforeEach(() => {
delete process.env.EMULATE_BASE_URL
delete process.env.PORTLESS_URL
delete process.env.EMULATE_PORT
delete process.env.PORT
})

afterAll(() => {
for (const [name, value] of Object.entries(savedEnv)) {
if (value !== undefined) {
Object.assign(process.env, { [name]: value })
}
}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

afterAll doesn't clear env vars that were originally unset.

Only vars with an original defined value get restored; vars that started undefined (e.g. EMULATE_PORT/PORT in most shells) are left with whatever the last test set them to, since nothing calls delete for the undefined case. This can leak stray PORT/EMULATE_PORT values into other test files sharing the same worker process.

💚 Proposed fix
 afterAll(() => {
   for (const [name, value] of Object.entries(savedEnv)) {
     if (value !== undefined) {
       Object.assign(process.env, { [name]: value })
     }
+    else {
+      delete process.env[name]
+    }
   }
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const savedEnv = {
EMULATE_BASE_URL: process.env.EMULATE_BASE_URL,
PORTLESS_URL: process.env.PORTLESS_URL,
EMULATE_PORT: process.env.EMULATE_PORT,
PORT: process.env.PORT,
}
// Clear before each test (not just after) so ambient shell/CI env vars
// cannot leak into the first test case.
beforeEach(() => {
delete process.env.EMULATE_BASE_URL
delete process.env.PORTLESS_URL
delete process.env.EMULATE_PORT
delete process.env.PORT
})
afterAll(() => {
for (const [name, value] of Object.entries(savedEnv)) {
if (value !== undefined) {
Object.assign(process.env, { [name]: value })
}
}
})
const savedEnv = {
EMULATE_BASE_URL: process.env.EMULATE_BASE_URL,
PORTLESS_URL: process.env.PORTLESS_URL,
EMULATE_PORT: process.env.EMULATE_PORT,
PORT: process.env.PORT,
}
// Clear before each test (not just after) so ambient shell/CI env vars
// cannot leak into the first test case.
beforeEach(() => {
delete process.env.EMULATE_BASE_URL
delete process.env.PORTLESS_URL
delete process.env.EMULATE_PORT
delete process.env.PORT
})
afterAll(() => {
for (const [name, value] of Object.entries(savedEnv)) {
if (value !== undefined) {
Object.assign(process.env, { [name]: value })
} else {
delete process.env[name]
}
}
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/emulate/src/__tests__/base-url.test.ts` around lines 6 - 28, The
cleanup in `afterAll` only restores `savedEnv` entries that were originally
defined, so any env var that started unset (like `EMULATE_PORT` or `PORT`) can
remain polluted after the tests. Update the `afterAll` restore logic in
`base-url.test.ts` to explicitly delete `process.env` keys whose saved value is
`undefined`, and restore only the defined ones, using the existing `savedEnv`
object and env var names already captured at the top of the test file.

Comment on lines +92 to +119
async function setupPortless(services: ServiceName[], basePort: number): Promise<void> {
await ensurePortless()
const aliases: PortlessAlias[] = buildAliases(services, basePort)
registerAliases(aliases)
process.on('exit', () => {
removeAliases(aliases)
})
}

if (options.baseUrl && services.length > 1) {
console.error('--base-url can only be used with a single service (--service).')
export async function startCommand(options: StartOptions): Promise<void> {
const { port: basePort } = options

const loaded = loadSeedConfig(options.seed)
const seedConfig = loaded?.config ?? null
const configSource = loaded?.source ?? null

const services = resolveServices(options, seedConfig)

const optionError = validateBaseUrlOptions(options, services.length)
if (optionError) {
console.error(optionError)
process.exit(1)
}

if (options.portless) {
await setupPortless(services, basePort)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how startCommand() is invoked and whether rejections are caught
rg -n -B5 -A10 'startCommand\(' packages/emulate/src/index.ts

Repository: pleaseai/emulate

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files 'packages/emulate/src/**' | sed -n '1,200p'

printf '\n== startCommand references ==\n'
rg -n --hidden --glob '!**/node_modules/**' 'startCommand\s*\(' packages/emulate/src || true

printf '\n== portless alias implementation ==\n'
rg -n --hidden --glob '!**/node_modules/**' 'registerAliases|removeAliases|ensurePortless|buildAliases' packages/emulate/src || true

Repository: pleaseai/emulate

Length of output: 3267


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== locate likely entrypoints ==\n'
fd -a 'index.ts' packages/emulate/src packages/emulate || true
fd -a 'main.ts' packages/emulate/src packages/emulate || true
fd -a 'cli.ts' packages/emulate/src packages/emulate || true

printf '\n== call sites for startCommand ==\n'
rg -n --hidden --glob '!**/node_modules/**' 'startCommand' packages/emulate || true

printf '\n== start.ts context ==\n'
sed -n '1,220p' packages/emulate/src/commands/start.ts

Repository: pleaseai/emulate

Length of output: 5435


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== all references to startCommand ==\n'
rg -n --hidden --glob '!**/node_modules/**' 'startCommand' .

printf '\n== portless helpers ==\n'
rg -n --hidden --glob '!**/node_modules/**' 'function (setupPortless|registerAliases|removeAliases|ensurePortless|buildAliases)|const (setupPortless|registerAliases|removeAliases|ensurePortless|buildAliases)|export (async )?function (setupPortless|registerAliases|removeAliases|ensurePortless|buildAliases)' packages/emulate/src

printf '\n== package files ==\n'
git ls-files 'packages/emulate/**' | sed -n '1,300p'

Repository: pleaseai/emulate

Length of output: 1599


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== find likely CLI entrypoints ==\n'
fd -a -I 'index.ts' . || true
fd -a -I 'cli.ts' . || true
fd -a -I 'main.ts' . || true

printf '\n== search for emulator start wiring ==\n'
rg -n --hidden --glob '!**/node_modules/**' 'startCommand|setupPortless|validateBaseUrlOptions|resolveServices' .

printf '\n== package.json bin/scripts for emulate ==\n'
rg -n --hidden --glob '!**/node_modules/**' '"bin"|emulate|startCommand' packages/emulate package.json .github . || true

Repository: pleaseai/emulate

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --hidden --glob '!**/node_modules/**' 'startCommand\(.*\)\.catch|catch\(\s*(err|error)|await startCommand|startCommand\(' packages/emulate .

Repository: pleaseai/emulate

Length of output: 392


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,120p' packages/emulate/src/index.ts
printf '\n== package version ==\n'
sed -n '1,120p' packages/emulate/package.json

Repository: pleaseai/emulate

Length of output: 3713


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,220p' packages/emulate/src/portless.ts
printf '\n== portless tests ==\n'
sed -n '55,95p' packages/emulate/src/__tests__/portless.test.ts

Repository: pleaseai/emulate

Length of output: 4697


🌐 Web query:

Commander.js async action handler rejection parse() parseAsync error handling

💡 Result:

When working with asynchronous action handlers in Commander.js, you must use.parseAsync instead of.parse to ensure that the command's promise-based lifecycle is managed correctly [1][2]. Error Handling and parseAsync Because.parseAsync returns a promise, errors occurring within async action handlers (or during parsing) will result in a promise rejection [3]. If these rejections are not handled, they can lead to unhandled promise rejections, which may not consistently terminate the process with a non-zero exit code depending on your Node.js environment [4]. To implement robust error handling: 1. Always use await or.catch with.parseAsync: Wrap your.parseAsync call in an async function and use try/catch or promise chaining to handle errors [5][3]. async function main { try { await program.parseAsync(process.argv); } catch (err) { console.error('Error:', err.message); process.exit(1); } } main; 2. Use.exitOverride: By default, Commander calls process.exit when it encounters errors (like missing arguments). To prevent this and allow your own error-handling logic to catch these as exceptions instead, call.exitOverride before parsing [1][2]. program.exitOverride.parseAsync(process.argv).catch(err => { // Handle Commander-specific errors (e.g., help, version, validation errors) if (err.code === 'commander.help') process.exit(0); console.error('CLI Error:', err.message); process.exit(1); }); Important Considerations - If any action handler or hook is asynchronous, you should use.parseAsync [1][6][3]. While.parse might sometimes appear to work with async code, it is not supported for this use case and can lead to race conditions or silent failures [7]. - CommanderError: When.exitOverride is active, Commander throws a CommanderError for standard CLI events (like help or version display) and validation failures. You can check the error code or exitCode property to distinguish between these cases [2]. - Async Limitations: Note that the callback provided to.exitOverride is currently synchronous. If you need to perform asynchronous cleanup before exiting, you should rely on catching the error from the.parseAsync promise chain rather than the.exitOverride callback [8].

Citations:


Use parseAsync() for the CLI entrypoint
packages/emulate/src/index.ts:23 calls program.parse(), so rejections from async actions like startCommand() can escape as an unhandled promise rejection. Switch this to program.parseAsync(process.argv) and handle the rejection to print a clean CLI error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/emulate/src/commands/start.ts` around lines 92 - 119, The CLI
entrypoint is still using a synchronous parse, so async command failures from
startCommand can surface as unhandled rejections. Update the program.parse()
call in the index entrypoint to use program.parseAsync(process.argv), and add
rejection handling there so errors are reported cleanly to the user instead of
bubbling uncaught.

Comment on lines +103 to +108

const loaded = loadSeedConfig(options.seed)
const seedConfig = loaded?.config ?? null
const configSource = loaded?.source ?? null

const services = resolveServices(options, seedConfig)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Unhandled throw from registerAliases surfaces as a raw exception

registerAliases in portless.ts throws new Error('Failed to register portless alias: …') when any alias fails. Since setupPortless doesn't wrap it in try/catch, the error propagates as a rejected promise through startCommand. Commander.js will print the error, but the output is a raw Error: stack trace rather than the clean console.error + process.exit(1) pattern used everywhere else in the CLI (including ensurePortless itself). A user who hits a registration failure on their first --portless run will see a confusing stack trace instead of an actionable message.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/emulate/src/commands/start.ts
Line: 103-108

Comment:
**Unhandled throw from `registerAliases` surfaces as a raw exception**

`registerAliases` in `portless.ts` throws `new Error('Failed to register portless alias: …')` when any alias fails. Since `setupPortless` doesn't wrap it in try/catch, the error propagates as a rejected promise through `startCommand`. Commander.js will print the error, but the output is a raw `Error:` stack trace rather than the clean `console.error + process.exit(1)` pattern used everywhere else in the CLI (including `ensurePortless` itself). A user who hits a registration failure on their first `--portless` run will see a confusing stack trace instead of an actionable message.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

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.

2 participants