Skip to content

Fix Windows libuv/TTY crashes in create, branch create, config export/apply - #312

Open
STiFLeR7 wants to merge 2 commits into
InsForge:mainfrom
STiFLeR7:fix/windows-libuv-tty-crashes
Open

STiFLeR7 wants to merge 2 commits into
InsForge:mainfrom
STiFLeR7:fix/windows-libuv-tty-crashes

Conversation

@STiFLeR7

@STiFLeR7 STiFLeR7 commented Oct 22, 2026 •

Copy link
Copy Markdown

Summary

Investigated whether #306, #300, #286, #270 (all Windows-only libuv/TTY crash reports) share one root cause. They trace to two distinct mechanisms, not one:

1. TTY-capability bypass → fixes #270, #286

src/commands/config/export.ts and src/commands/config/apply.ts call @clack/prompts' confirm() directly instead of the isInteractive-guarded wrapper already defined in src/lib/prompts.ts. Clack's raw confirm() always tries to init a TTY on stdin regardless of whether stdout/stdin is actually a console handle, which is exactly what produces #270's Error: TTY initialization failed: uv_tty_init returned EINVAL.

src/commands/branch/create.ts's spinner has the same category of gap — it's guarded by !json only, not !json && isInteractive, unlike the identical pattern already established in ai/setup.ts, posthog/setup.ts, and webscraper/apify/connect.ts. That mismatch matches #286 (spinner writes into a non-interactive Windows terminal and eventually EPIPEs).

Fix: route export.ts/apply.ts's confirmation prompt through lib/prompts.js, and add the same isInteractive guard to branch/create.ts's spinner — both changes adopt a pattern that already exists and is tested elsewhere in this codebase.

2. Forced process.exit() racing a still-open handle → fixes #306, #300

  • create.ts calls process.exit(0) immediately after a cancelled prompt. In the non-interactive fallback (lib/prompts.ts's nonTtyText/LineReader), a cancel means stdin just hit EOF and the underlying readline.Interface's own 'close' event only just fired. Calling process.exit() synchronously the instant after that is the documented class of Windows libuv race in src/win/async.c — forcing an immediate handle teardown while one is already mid-close.
  • Every command's handleError() (the single shared error path) runs through PostHog analytics first. src/lib/analytics.ts constructs the PostHog client without enableLocalEvaluation: false. I pulled posthog-node@5.28.9's source to confirm: with local evaluation enabled (the default), the FeatureFlagsPoller constructor calls loadFeatureFlags() unconditionally and unawaited, immediately firing a flag-definitions fetch and scheduling a recurring setTimeout poll — entirely pointless here, since nothing in src/ ever calls getFeatureFlag/isFeatureEnabled. shutdownAnalytics() → client.shutdown() → stopPoller() only does clearTimeout(this.poller), which cancels the next scheduled poll — it does not cancel an already in-flight fetch. handleError()'s process.exit() fires right after that shutdown() resolves, so an in-flight PostHog socket can still be open when the process is forced to exit. This matches Migration name validation triggers Windows libuv assertion #300's Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 94 and the issue's own triage note about "the catch-path PostHog flush."

Fix: disable PostHog's local evaluation (one line, in the single shared client constructor — removes the dead poller/fetch entirely), and replace create.ts's cancel-time process.exit(0) with process.exitCode = 0; return; so Node drains naturally instead of forcing a hard stop (Node's own docs recommend exitCode over exit() for exactly this class of issue).

Confidence

  • High on fix (1): source-verified, exact error-message match (uv_tty_init returned EINVAL), minimal diff adopting an existing in-repo pattern.
  • Medium-high on fix (2): I don't have a Windows box to reproduce the libuv assertion directly, but both changes are independently correct on their own merits (the PostHog poller is genuinely dead weight; process.exitCode over process.exit() is the documented-safe pattern for this exact race), and they directly target the mechanisms each issue's own repro describes.

Test plan

  • npx tsc --noEmit — no new errors vs. the pre-fix baseline (diffed explicitly)
  • npx eslint src/ — clean
  • npx vitest run — 835 passed, 5 pre-existing failures (Windows path-separator issues in flyctl.test.ts, migrations.test.ts, local/checkout.test.ts — none of these files were touched by this PR)
  • Added regression tests for config export/config apply's overwrite/apply confirmation prompt — previously uncovered, since every existing test passes --force/--yes/--json and skips the prompt entirely. New tests assert the command calls lib/prompts.js's confirm() (not raw @clack/prompts) and that declining leaves state untouched.
  • Updated branch/create.test.ts to mock isInteractive: true, since those tests specifically assert on the spinner-driven interactive path.

🤖 Generated with Claude Code


Summary by cubic

Fixes four Windows-only crashes in create, branch create, config export, and config apply (#306, #300, #286, #270). They came from two sources: prompts and spinners that bypassed the CLI's isInteractive TTY guard, and process.exit() racing a still-open libuv handle.

Bug Fixes

  • Route config export and config apply's confirmation prompts through lib/prompts.js's guarded confirm() instead of raw @clack/prompts, which tried to init a TTY on stdin even without a console handle and crashed with uv_tty_init returned EINVAL.
  • Guard branch create's spinner with isInteractive (previously only !json), matching the pattern used elsewhere in the CLI.
  • Disable PostHog local evaluation (enableLocalEvaluation: false) so the unused feature-flag poller never starts, leaving no in-flight socket for handleError()'s process.exit() to race.
  • Replace cancel-time process.exit(0) in create.ts with process.exitCode = 0; return; so Node drains naturally instead of forcing a hard stop.
  • Add regression tests for the export/apply confirmation routing (previously uncovered, since existing tests always passed --force/--yes/--json) and mock isInteractive: true in branch/create.test.ts for its spinner path.

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

View guided diff

…xport/apply

Four Windows-only crash reports (InsForge#306, InsForge#300, InsForge#286, InsForge#270) trace to two
distinct mechanisms, not one shared bug:

1. TTY-capability bypass (InsForge#270, InsForge#286): config export/apply call
   @clack/prompts' confirm() directly instead of the isInteractive-guarded
   wrapper in lib/prompts.ts, so they try to init a TTY on stdin even when
   stdout/stdin isn't a real console handle, crashing with "TTY
   initialization failed: uv_tty_init returned EINVAL". branch create
   has the same gap for its spinner (`!json` guard only, no isInteractive
   check), unlike the identical pattern already used in ai/setup.ts,
   posthog/setup.ts, and webscraper/apify/connect.ts.

   Fix: route export.ts/apply.ts's confirm through lib/prompts.ts, and
   add the same isInteractive guard to branch/create.ts's spinner.

2. Forced process.exit() racing a still-active handle (InsForge#306, InsForge#300):
   - create.ts calls process.exit(0) right after a cancelled prompt;
     in the non-interactive fallback, that fires the instant after the
     underlying readline interface's own 'close' event (stdin EOF) —
     exiting synchronously while that handle is still unwinding is the
     documented Windows libuv race in src/win/async.c.
   - Every command's handleError() runs through PostHog analytics first.
     analytics.ts constructs PostHog without enableLocalEvaluation: false,
     which (confirmed by reading posthog-node's source) unconditionally
     starts a background feature-flag poller and fires an *unawaited*
     flag-definitions fetch on construction — one the CLI never needs,
     since nothing in src/ calls getFeatureFlag/isFeatureEnabled.
     shutdownAnalytics()'s stopPoller() only clears the *next* scheduled
     poll via clearTimeout; it doesn't cancel that in-flight request. The
     immediately-following process.exit() can race its still-open socket.

   Fix: disable PostHog's local evaluation (one-line, single shared
   constructor) so the pointless poller/fetch never starts, and replace
   create.ts's cancel-time process.exit(0) with process.exitCode + return
   so Node drains naturally instead of forcing a hard stop.

Confidence: high on (1), source-verified exact error-message match.
Medium-high on (2): no Windows box to reproduce the assertion directly,
but both fixes are independently correct (dead poller removed; Node's own
docs recommend exitCode over exit() for exactly this class of race) and
directly address the mechanisms described in each issue's own repro.

Tests: added regression coverage for the export/apply confirm-routing
fix (previously uncovered — existing tests always pass --force/--yes/
--json, which skip the prompt entirely). branch/create.test.ts updated
to mock isInteractive: true, since it asserts on the spinner-driven path.

Fixes InsForge#306, Fixes InsForge#300, Fixes InsForge#286, Fixes InsForge#270
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-22T05:39:22.786603Z
We couldn’t complete this review. Comment @codex review to try again.
c3b298d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@agent-zhang-beihai

Copy link
Copy Markdown
Contributor

Thanks for the PR, @STiFLeR7! This links #270, #306, but those issues aren't assigned to anyone yet. Our workflow is claim the issue first, then submit the PR. It'll still be reviewed — to keep ownership clear, comment on the issue that you'd like it assigned to you.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@agent-zhang-beihai agent-zhang-beihai Bot added the needs-claim PR work started without being assigned the issue — claim the issue first label Oct 22, 2026
@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 22 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 511375b7-e70d-47bc-a467-8b5421c89ad0
📥 Commits

Reviewing files that changed from the base of the PR and between e68f9c1 and bd92545.

📒 Files selected for processing: 8
  • src/commands/branch/create.test.ts
  • src/commands/branch/create.ts
  • src/commands/config/apply.test.ts
  • src/commands/config/apply.ts
  • src/commands/config/export.test.ts
  • src/commands/config/export.ts
  • src/commands/create.ts
  • src/lib/analytics.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

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

Copy link
Copy Markdown
Contributor

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 8 files

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

View guided diff | Re-trigger cubic

Comment thread src/commands/config/export.test.ts Outdated
@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with the previously reported missing success message remaining a non-blocking issue.

Findings

  1. P2 Branch creation succeeds silently ▶

Summary

The PR guards Windows terminal use and avoids forced exits when project creation is cancelled.

  • Config export and apply ask for confirmation through the shared prompt path.
  • Branch creation shows a spinner only in an interactive terminal.
  • Cancelled create prompts let Node finish closing input handles.
  • The PostHog client no longer starts local feature-flag checks.

Reviews (2) · Last reviewed commit: "fix(test): correct stale TTY_TEST refere..." · Reviewed by Greptile

// before an error). JSON mode skips the spinner — `outputJson({ branch:
// ready })` below remains the sole authoritative output.
const spinner = !json ? clack.spinner() : null;
const spinner = !json && isInteractive ? clack.spinner() : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Branch creation succeeds silently

When stdout is redirected, isInteractive is false and the spinner is now absent. A successful branch create <name> --no-switch then prints nothing: its only success message is in spinner?.stop(), and the later output block has no message for this case.

Keep the spinner guard, but print a plain success message when !json && !isInteractive. Add a test with isInteractive: false so this path stays covered.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

cubic-dev-ai follow-up on InsForge#312: the comment pointed at a nonexistent
TTY_TEST. Point it at the actual regression test name instead.
@STiFLeR7

Copy link
Copy Markdown
Author

Fixed in bd92545 — the comment now points at the actual test name (routes the overwrite prompt through the TTY-safe wrapper) instead of a nonexistent TTY_TEST.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants