Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions bin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/usr/bin/env node
import { satisfies } from 'semver';
import { Agent, setGlobalDispatcher } from 'undici';
import { ErrorCodes } from './src/lib/errors/codes.js';
import { emitWizardError } from './src/lib/errors/emit.js';

Expand All @@ -10,21 +9,6 @@ import { emitWizardError } from './src/lib/errors/emit.js';
// TypeError on Node < 22.10).
const NODE_VERSION_RANGE = '>=22.22.0';

/*
* TODO(#1198): remove when fetch over HTTP/2 is safe on Node 26. Remove when all
* of these are true:
* - nodejs/node no longer creates an orphan ClientHttp2Stream when a client
* session gets HEADERS for a stream id it already reset. Repro: abort a fetch
* before its response headers, then idle 4s on Node 26. Fixed when the
* process survives.
* - modelcontextprotocol/typescript-sdk#2526 is closed.
* - pi-coding-agent's CLI drops `allowH2: false` from its http-dispatcher.
* Same workaround as pi's CLI and typescript-sdk#2526: HTTP/1.1 only.
*/
setGlobalDispatcher(new Agent({ allowH2: false }));

// Have to run this above the other imports because they are importing clack that
// has the problematic imports.
if (!satisfies(process.version, NODE_VERSION_RANGE)) {
// eslint-disable-next-line no-console
console.log(
Expand All @@ -49,6 +33,23 @@ if (!satisfies(process.version, NODE_VERSION_RANGE)) {
process.exit(1);
}

/*
* Imported here, not at the top. A static import loads undici before the
* preflight above runs, and that load crashes on the runtimes it rejects.
*
* TODO(#1198): remove when fetch over HTTP/2 is safe on Node 26. Remove when all
* of these are true:
* - nodejs/node no longer creates an orphan ClientHttp2Stream when a client
* session gets HEADERS for a stream id it already reset. Repro: abort a fetch
* before its response headers, then idle 4s on Node 26. Fixed when the
* process survives.
* - modelcontextprotocol/typescript-sdk#2526 is closed.
* - pi-coding-agent's CLI drops `allowH2: false` from its http-dispatcher.
* Same workaround as pi's CLI and typescript-sdk#2526: HTTP/1.1 only.
*/
const { Agent, setGlobalDispatcher } = await import('undici');
setGlobalDispatcher(new Agent({ allowH2: false }));

// Test mock server — only loaded when NODE_ENV is 'test'.
// In production builds, tsdown replaces process.env.NODE_ENV with 'production',
// making this block dead code.
Expand Down
19 changes: 15 additions & 4 deletions scripts/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
# with the tailored "CI mode is not currently supported" error and a
# non-zero exit. Guards against a future change that re-enables --ci in
# published builds without anyone noticing.
# 3. In production builds, the experimental headless flag IS accepted (the
# 3. undici stays out of the static import graph, so the Node version
# preflight still runs on a runtime undici cannot load on.
# 4. In production builds, the experimental headless flag IS accepted (the
# non-interactive published-build path) — it must not be rejected as an
# unknown argument. It is intentionally undocumented; this check only keeps
# the published binary from silently dropping the flag the cloud runs need.
Expand All @@ -23,7 +25,16 @@ node --input-type=module -e "import '$DIST_BIN'" 2>&1 | head -5 | grep -q 'PostH
exit 1
}

# ── 2. CI flag overrides physically absent from production builds ───────────
# ── 2. undici loads lazily, after the Node version preflight ─────────────────
# undici 8.5.0 throws `webidl.util.markAsUncloneable is not a function` on load
# below Node 22.10. A static import runs before any statement in the entry, so
# it would crash the binary before the preflight can print the upgrade message.
if grep -qE '^import[^(]*undici' "$DIST_BIN"; then
echo 'Smoke test failed: undici is statically imported, so it loads before the Node version preflight' >&2
exit 1
fi

# ── 3. CI flag overrides physically absent from production builds ───────────
# The override path (src/utils/ci-flag-overrides.ts) is dead code in published
# builds and tsdown strips it; its env var name appearing in dist/*.js means
# dead-code elimination regressed and a prod surface leaked. Sourcemaps keep
Expand Down Expand Up @@ -58,7 +69,7 @@ else
done
fi

# ── 3. --ci rejected in production builds ────────────────────────────────────
# ── 4. --ci rejected in production builds ────────────────────────────────────
# build:ci sets WIZARD_BUILD_NODE_ENV=ci → --ci stays enabled → skip the check.
if [ "${WIZARD_BUILD_NODE_ENV:-production}" = "ci" ]; then
exit 0
Expand All @@ -81,7 +92,7 @@ if ! echo "$output" | grep -qi 'CI mode is not currently supported'; then
exit 1
fi

# ── 4. Experimental headless flag accepted in production builds ──────────────
# ── 5. Experimental headless flag accepted in production builds ──────────────
# The non-interactive path for published builds (cloud / CI runs). yargs must
# not reject the flag, and it must not fall through to the --ci rejection. With
# no api-key the run exits fast on "Headless mode requires --api-key" — all this
Expand Down
Loading