mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-06-01 15:27:41 +00:00
* feat(sdk): add SDK foundation — type declarations, errors, and utilities Adds standalone SDK building blocks with no SDK source dependencies: - sdk.d.ts: ambient type declarations for SDK bundle - coreSchemas.ts + coreTypes.generated.ts: Zod schemas and generated types - errors.ts: SDK-specific error classes - validation.ts: input validation utilities - messageFilters.ts: extracted message filter logic - handlePromptSubmit.ts: imports from messageFilters - 16 generated-types tests * fix(sdk): narrow assertFunction type from broad Function to callable signature Code review finding: assertFunction used `asserts value is Function` which accepts any function-like value without narrowing. Changed to `(...args: any[]) => any` for better type safety. * fix(sdk): update sdk.d.ts header — manually maintained, not generated Reviewer noted the header said "Generated from index.ts" but no generator produces this file. Updated to "Manually maintained — keep in sync with index.ts". Drift detection added in validate-externals.ts (PR 3). * fix(sdk): align sdk.d.ts types with canonical coreTypes.generated.ts Tighten SDK public type contract to resolve reviewer blockers: - PermissionResult: unknown[] → precise 6-shape discriminated union (addRules/replaceRules/removeRules/setMode/addDirectories/removeDirectories) - SDKSessionInfo: snake_case → camelCase (sessionId, lastModified, etc.) - ForkSessionResult: session_id → sessionId - SDKPermissionRequestMessage: uuid + session_id now required - SDKPermissionTimeoutMessage: added uuid + session_id - SessionMessage: parent_uuid → parentUuid - SDKMessage/SDKUserMessage/SDKResultMessage: replaced loose inline definitions with re-exports from coreTypes.generated.ts * feat(sdk): wire existing code modules + SDK shared utilities Modifies core modules for SDK integration: - QueryEngine, tools, state, commands: SDK type hooks - SDK shared utilities (shared.ts, permissions.ts) - 21 SDK tests (shared-utils, permissions) Stack: main ← pr1-foundation ← pr2-sdk-core * feat(sdk): add snake_case ↔ camelCase key mapping utilities casing.ts provides recursive key transformation for the SDK boundary layer. Internal runtime uses snake_case; public API exposes camelCase. Will be used by shared.ts, sessions.ts, query.ts at export boundaries. * test(sdk): add tests for snake_case ↔ camelCase mapping utilities Covers snakeToCamel, camelToSnake, mapKeysToCamel, mapKeysToSnake including nested objects, arrays, null/undefined, and round-trips. * feat(sdk): add SDK runtime — query engine, sessions, build pipeline Completes the SDK implementation: - SDK build target (dist/sdk.mjs) with TUI dependency stubbing - External dependency lists (scripts/externals.ts) - SDK type generation from Zod schemas (scripts/generate-sdk-types.ts) - External validation (scripts/validate-externals.ts) - SDK source: index, query, v2, sessions modules - agentSdkTypes: re-exports SDK functions (query, createSession, etc.) - 136 SDK tests + 7 build scanner tests Stack: main ← pr1-foundation ← pr2-sdk-core ← pr3-sdk-runtime * fix(sdk): align internal SDK types with camelCase public contract shared.ts: SDKSessionInfo, ForkSessionResult, SessionMessage fields now use camelCase matching sdk.d.ts. SDKPermissionRequestMessage and SDKPermissionTimeoutMessage gain required uuid + session_id fields. permissions.ts: onPermissionRequest/onTimeout callbacks now include uuid and session_id in emitted messages. * fix(sdk): update runtime modules to use camelCase field names sessions.ts: toSDKSessionInfo outputs camelCase keys, entryToSessionMessage uses parentUuid, forkSession returns sessionId. query.ts: reads sessionId from listSessions/forkSession results instead of snake_case session_id. * fix(test): update session tests to use camelCase field names session_id → sessionId in forkSession result assertions and getSessionMessages calls. * fix(sdk): prevent permission timeout race condition with once-only resolve wrapper Add createOnceOnlyResolve utility to prevent double-resolution of promises when timeout and host response happen simultaneously. This ensures deterministic behavior in the permission handling flow. * fix(sdk): improve race condition test robustness * fix(sdk): handle consecutive underscores in snakeToCamel conversion Changes: - Use _+([a-z]) regex to match multiple consecutive underscores before letters - Add lookahead (?=. ) to preserve underscore-letter pairs at string end - Handle dunder names (__proto__, __typename) by stripping wrapper and capitalizing - Add tests for consecutive underscores and trailing underscore preservation * fix(sdk): include original error message in permission callback denial When a canUseTool callback throws an error, the catch block now includes the original error message in the denial message, making debugging easier for SDK consumers. * feat(sdk): add optional timeout to env mutex for deadlock prevention Add timeout parameter to acquireEnvMutex() to prevent infinite waits in deadlock scenarios. The timeout is optional and defaults to no timeout (wait forever) for backward compatibility. Returns a MutexAcquireResult object with acquired status and optional timeout reason for failed acquisitions. * fix(sdk): remove timed-out callback from mutex queue to prevent deadlock * test(sdk): add missing error path and timeout scenario tests Add tests for timeout scenarios when host doesn't respond to permission requests, fallback behavior when no onPermissionRequest callback, and MCP connection edge cases for undefined/empty config. * fix(sdk): address code review issues - race conditions, validation, error handling - Add createPermissionTarget() factory that applies onceOnlyResolve at registration time, fixing race condition where timeout and host response could both try to resolve the same promise - Add try-catch to releaseEnvMutex() to prevent permanent lock if callback throws - Extract DEFAULT_PERMISSION_TIMEOUT_MS constant (30 seconds) - Add MCP config validation rejecting null, non-objects, and arrays - Preserve error stack traces in MCP connection failures - Add runtime validation to mapMessageToSDK for null/non-object/invalid type - Update tests to use createPermissionTarget and add validation tests * fix(sdk): syntax fixes and MCP connection error handling - Remove extra closing parenthesis in permissions.ts - Remove extra closing braces in shared.ts type definitions - Wrap MCP connection in try/catch to continue without MCP tools on failure * fix(sdk): syntax fixes, MCP error handling, and logic clarity - Remove extra closing parenthesis in permissions.ts - Remove extra closing braces in shared.ts type definitions - Wrap MCP connection in try/catch to continue without MCP tools on failure - Clarify thinkingConfig logic: use ?? true instead of !== false - Add explanatory comment about thinkingEnabled default behavior - Apply createOnceOnlyResolve wrapper in QueryImpl.registerPendingPermission * fix(sdk): comprehensive error handling and resource cleanup - Add try-catch around injectAgents() to gracefully handle plugin agent tool validation failures (prevents test crashes from unknown 'LS' tool) - Add console.warn logging to agent loading/injection catch blocks for debugging visibility (matches v2.ts pattern) - Add pendingPermissionPrompts.clear() to close() and interrupt() methods in both query.ts and v2.ts to prevent memory accumulation - Add close() method to SDKSession interface and SDKSessionImpl - Wrap MCP connection in query.ts with try-catch (matches v2.ts behavior) - Add timeoutQueue cleanup in finally blocks (query.ts + v2.ts) - Remove error.stack from MCP error messages to prevent internal path leak All 208 SDK tests pass. TypeScript errors are pre-existing. * fix(sdk): address code review non-blocking issues - Add SDKAgentLoadFailureMessage type for agent load failure events - Emit agent definition/injection failures to SDK message stream - Add tool name to permission timeout denial message - Replace 'as any' casts with proper typed state access - Fix supportedCommands to use correct mcp.commands/plugins.commands paths - Update test for correct AppState structure * fix(sdk): address code review blocking and non-blocking issues Blocking Issues Fixed: - MCP cleanup missing on session/query close - now disconnects MCP clients to prevent resource leaks in long-running processes with multiple sessions - Engine reference not cleared on close - now sets _engine = null to prevent memory leaks - Added MCP cleanup tests (9 new tests covering cleanup scenarios) Non-Blocking Issues Fixed: - Removed redundant catch block that just rethrew errors (query.ts) - Fixed inconsistent timeout denial message format (permissions.ts) - Fixed hardcoded tool name 'Bash' in test (permissions.test.ts) - Exported PermissionResolveDecision type for SDK consumers (index.ts) All 217 SDK tests pass. * fix(sdk): address code review type consistency issues - Add close() method to SDKSession interface (documented but missing from type) - Fix SDKSessionInfo, ForkSessionResult, SessionMessage field naming: snake_case → camelCase to match sdk.d.ts public contract and implementation - Add uuid and session_id to SDKPermissionTimeoutMessage for correlation - Fix JSDoc comment in forkSession to use sessionId (not session_id) These changes align internal types (shared.ts) with the public SDK contract (sdk.d.ts) and actual implementation output. The merge from origin/main introduced snake_case types that mismatched camelCase implementation and tests. * fix: restore openclaude.json comment in REPL.tsx Merge0f3aa7aincorrectly took main's side for this comment, reverting PR2 fixc725c48. Project has migrated to ~/.openclaude.json, not ~/.claude.json. This is the only PR2 fix lost during merge - all other PR2 fixes (permissions.ts race conditions, state.ts parentSessionId, etc.) are preserved in PR3 via subsequent fix commits. * fix(sdk): add missing type declarations to sdk.d.ts Add SDKAgentLoadFailureMessage and PermissionResolveDecision to sdk.d.ts to resolve type declaration drift detected by build validation. - SDKAgentLoadFailureMessage: Agent loading failure notification (stage: definitions/injection, error_message) - PermissionResolveDecision: SDK-specific permission resolution result (allow with updatedInput, deny with message + decisionReason) Build validation now passes: 56 exports match between index.ts and sdk.d.ts. * fix(sdk): resource leak and null safety in close/interrupt paths - unstable_v2_prompt: wrap session in try/finally to guarantee session.close() on both success and error paths, preventing MCP connection and engine resource leaks - QueryImpl.interrupt(): add null guard on _engine so calling interrupt() after close() is a safe no-op instead of throwing - SDKSessionImpl.interrupt(): add matching null guard for v2 sessions, consistent with the Query fix - QueryImpl.close(): call this.interrupt() before cleanup to properly stop in-flight engine operations, matching v2's close() pattern and ensuring engine.interrupt() runs before nulling * fix(sdk): abort AbortController in SDKSessionImpl.close() to prevent resource leak SDKSessionImpl.close() was not aborting the AbortController, unlike QueryImpl.close() which does. This meant in-flight HTTP requests and async operations could continue running after session closure. - Store AbortController reference via _abortController field + late-bind setter - Abort and null the controller in close(), mirroring QueryImpl pattern - Also null _appStateStore in close() to release state snapshots - Wire abortController through createEngineFromOptions return value * fix(sdk): index ALL entries in byUuid for compact preserved segment The byUuid map must index system compact_boundary entries, not just user/assistant. When anchorUuid === boundary.uuid, the relink walk needs to find the boundary in byUuid. Changes: - query.ts: Index ALL non-sidechain entries (user, assistant, system) - v2.ts: Same fix — index ALL entries, leaf selection user/assistant only - Add regression test: boundary.uuid as anchorUuid scenario Test verifies preserved messages kept, stale pre-compact dropped, post-boundary chain intact when anchorUuid points to boundary itself. * fix(sdk): complete preserved segment handling for compact resumes Multiple fixes for compact-aware transcript loading: 1. Index ALL entries in byUuid (including system compact_boundary) - Needed when anchorUuid === boundary.uuid 2. Keep anchorUuid when pruning preserved segment entries - The anchor is the parent of preserved head after relink - Deleting it breaks the conversation chain 3. Filter system entries from final messages - compact_boundary is metadata, shouldn't pass to engine 4. Fix test timestamp format (ISO 8601 requires 2-digit hours) - '2025-01-04T0:00:00Z' → '2025-01-04T00:00:00Z' 5. Update test expectations for anchor inclusion - When anchor is a stale entry, it appears in messages - preserved(4) + anchor(1) + post(4) = 9 max All 224 SDK tests pass. * fix(sdk): MCP type:sdk tools properly convert SdkMcpToolDefinition to Tool - Import MCPTool base from tools/MCPTool/MCPTool.js - Spread MCPTool properties for proper Tool interface compliance - Add tools field to SdkMcpSdkConfig type declaration - Add regression tests for type:sdk tools wiring Fix ensures in-process SDK tools match Tool interface expected by QueryEngine and permission handlers. * test(sdk): strengthen preserved segment and MCP tools tests Preserved segment test improvements: - Fix content extraction (access message.content, not message) - Add exact count assert: messages.length === 6 - Add exact content asserts: preserved turn 1/2, post-boundary present - Assert no stale, no system entries in final messages MCP tools test additions: - Direct test of connectSdkMcpServers() function - Assert clients.length === 0 (in-process, no MCP connections) - Assert tools.length === 1 with proper name/description - Verify handler works via direct call (not via Tool.call which needs context) * fix(sdk): published types complete, init errors fatal, permission session IDs Three fixes for SDK production readiness: 1. HIGH: Published SDK types incomplete - Add coreTypes.generated.d.ts to package.json "files" array - sdk.d.ts re-exports from ./sdk/coreTypes.generated.js which was missing - TypeScript consumers would get module resolution errors 2. MEDIUM: query() swallows real init() failures - Add _engineWasInjected field to track pre-injected vs fresh engine - Check _engineWasInjected, not _engine !== null (always true after setEngine) - Auth/config/init errors now properly fatal for normal query() calls 3. MEDIUM: SDK permission events lose real session id - Pass sessionId to createExternalCanUseTool() in both query.ts and v2.ts - Permission_request/timeout messages now have correct session_id - Hosts can correlate permission callbacks to sessions Test result: 225 pass, 0 fail * fix(sdk): complete package types + dynamic permission session_id Two fixes for SDK production readiness: 1. Published SDK types now include actual definitions - Replace 215-byte wrapper with 63KB coreTypes.generated.ts - TypeScript consumers get full type definitions (SDKMessage, etc.) - npm pack now includes real generated types 2. Permission event session_id dynamic for all query() paths - createExternalCanUseTool accepts string | (() => string | undefined) - query.ts passes () => queryImpl.sessionId getter - Fresh/fork/continue queries emit correct session_id at event time - V2 passes static sessionId (stable at creation/resume) - Add 4 tests: static sessionId, getter resolution, undefined fallback, timeout Test result: 229 pass, 0 fail * fix(sdk): fix sdk.d.ts for real TypeScript consumer compilation Two issues prevented external consumers from compiling against packed SDK types: 1. SDKRateLimitError used constructor parameter properties (readonly resetsAt, readonly rateLimitType) which are invalid in .d.ts declarations — moved to class properties with separate constructor signature. 2. Re-exported SDKMessage/SDKUserMessage/SDKResultMessage were not imported into local scope — added import type alongside export type so TypeScript can resolve them for use in other declarations within the same file. Added package-consumer-types.test.ts that compiles a real temp project against the SDK types with skipLibCheck:false, catching both regressions. * fix(sdk): eliminate React/Ink imports from SDK bundle SDK bundle leaked React/Ink imports via tool UI modules, keybindings, react-compiler-runtime, and spawnMultiAgent's static React import. Changes: - Stub root ink.js barrel, tool UI.js, keybindings/, react-compiler-runtime, It2SetupPrompt, and React hook files in SDK build - Add local no-op stub for react/jsx-dev-runtime (jsxDEV returns null) - Convert spawnMultiAgent's static React/It2SetupPrompt imports to dynamic await import() — spawnTeammate logic stays fully intact - Add post-build leakage validation (fails on from "react"/"ink"/jsx-dev-runtime) - Remove react/jsx-dev-runtime from SDK externals (now handled by build plugin) * fix(sdk): wire disallowedTools through permission context QueryOptions.disallowedTools was declared but never used. buildPermissionContext() now passes it to alwaysDenyRules.cliArg so getTools() filters denied tools from the model-visible list. Also added to V2 SDKSessionOptions for API consistency. * fix(sdk): defer permission warning to execution time createDefaultCanUseTool() warned at construction time even when the caller provided canUseTool/onPermissionRequest. Move warning to first actual default denial so valid SDK consumers never see false warnings. Add tests for disallowedTools filtering, tool exclusion, and warning timing. * refactor(sdk): extract transcript helpers + fix permission typing - Extract shared transcript utilities to transcript.ts (parseJsonlEntries, findLastCompactBoundary, applyPreservedSegmentRelinks, buildConversationChain, stripExtraFields) deduplicating query.ts and v2.ts - Add PermissionTarget interface to hide internal pendingPermissionPrompts map from createExternalCanUseTool, with deletePendingPermission and denyPendingPermission methods on QueryImpl and SDKSessionImpl - Fix sessionId stability: preserve constructor UUID for fresh queries when continue:true finds no existing sessions, and when explicit sessionId does not resolve to a valid transcript file - Add getMcpClients/setMcpClients to QueryEngine for SDK cleanup access * fix(sdk): resolve remaining TypeScript errors in SDK modules - Fix PermissionDecision type compatibility: import from types/permissions and cast PermissionResolveDecision to PermissionDecision properly - Fix AsyncIterator/AsyncGenerator: async generators must return AsyncGenerator (which implements AsyncIterable), not AsyncIterator - Fix Map method callable errors: cast additionalWorkingDirectories to Map<string, unknown> before calling .set() and .keys() - Fix ApiKeySource type: map internal ApiKeySource to SDK's narrower type using conversion function, spread info before apiKeySource to avoid override - Fix MCP config scope type: cast 'session' scope to ScopedMcpServerConfig for connectToServer compatibility - Add PermissionMode import and cast for decisionReason.mode - Deny pending permissions in interrupt(): resolve all pending promises with deny before clearing the map (both query.ts and v2.ts) * fix(sdk): correct init skip logic and test mocks - query.ts: skip init() entirely for injected engines (mocks, SDK host overrides) instead of calling init() and swallowing errors. Pass { injected: false } from query() factory to distinguish real engine from test mocks. - mock-engine.ts: add getMcpClients() and setMcpClients() methods to match QueryEngine API added in this PR. - permissions.test.ts: use filterToolsByDenyRules instead of getTools for disallowedTools tests, with proper base tool fixtures. * fix: address code review feedback for exports and build script package.json exports (Breaking Change Mitigation): - Add "./package.json": "./package.json" for tool compatibility - Add "./dist/cli.mjs": "./dist/cli.mjs" for CLI bundle access - Keep ./sdk as sole library entrypoint - Root import intentionally blocked (CLI-first package, no main field) build.ts (Bug Fix): - Add | undefined to result/sdkResult type declarations - Add optional chaining: result?.success, sdkResult?.success - Prevents TypeError masking actual build errors when Bun.build throws tests/sdk/package-consumer-types.test.ts: - Update simulated exports to match real package.json - Add tests verifying exports map structure and file existence --------- Co-authored-by: Ali Alakbarli <ali.alakbarli@users.noreply.github.com>
935 lines
40 KiB
TypeScript
935 lines
40 KiB
TypeScript
/**
|
|
* OpenClaude build script — bundles the TypeScript source into a single
|
|
* distributable JS file using Bun's bundler.
|
|
*
|
|
* Handles:
|
|
* - bun:bundle feature() flags for the open build
|
|
* - MACRO.* globals → inlined version/build-time constants
|
|
* - src/ path aliases
|
|
*/
|
|
|
|
import { readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
import { join } from 'path'
|
|
import { noTelemetryPlugin } from './no-telemetry-plugin'
|
|
import { CLI_EXTERNALS, SDK_EXTERNALS } from './externals.js'
|
|
|
|
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
|
|
const version = pkg.version
|
|
|
|
// Feature flags for the open build.
|
|
// Most Anthropic-internal features stay off; open-build features can be
|
|
// selectively enabled here when their full source exists in the mirror.
|
|
const featureFlags: Record<string, boolean> = {
|
|
// ── Disabled: require Anthropic infrastructure or missing source ─────
|
|
VOICE_MODE: false, // Push-to-talk STT via claude.ai OAuth endpoint
|
|
PROACTIVE: false, // Autonomous agent mode (missing proactive/ module)
|
|
KAIROS: false, // Persistent assistant/session mode (cloud backend)
|
|
BRIDGE_MODE: false, // Remote desktop bridge via CCR infrastructure
|
|
DAEMON: false, // Background daemon process (stubbed in open build)
|
|
AGENT_TRIGGERS: false, // Scheduled remote agent triggers
|
|
ABLATION_BASELINE: false, // A/B testing harness for eval experiments
|
|
CONTEXT_COLLAPSE: false, // Context collapsing optimization (stubbed)
|
|
COMMIT_ATTRIBUTION: false, // Co-Authored-By metadata in git commits
|
|
UDS_INBOX: false, // Unix Domain Socket inter-session messaging
|
|
BG_SESSIONS: false, // Background sessions via tmux (stubbed)
|
|
WEB_BROWSER_TOOL: false, // Built-in browser automation (source not mirrored)
|
|
CHICAGO_MCP: false, // Computer-use MCP (native Swift modules stubbed)
|
|
COWORKER_TYPE_TELEMETRY: false, // Telemetry for agent/coworker type classification
|
|
MCP_SKILLS: false, // Dynamic MCP skill discovery (src/skills/mcpSkills.ts not mirrored; enabling this causes "fetchMcpSkillsForClient is not a function" when MCP servers with resources connect — see #856)
|
|
|
|
// ── Enabled: upstream defaults ──────────────────────────────────────
|
|
COORDINATOR_MODE: true, // Multi-agent coordinator with worker delegation
|
|
BUILTIN_EXPLORE_PLAN_AGENTS: true, // Built-in Explore/Plan specialized subagents
|
|
BUDDY: true, // Buddy mode for paired programming
|
|
MONITOR_TOOL: true, // MCP server monitoring/streaming tool
|
|
TEAMMEM: true, // Team memory management
|
|
MESSAGE_ACTIONS: true, // Message action buttons in the UI
|
|
|
|
// ── Enabled: new activations ────────────────────────────────────────
|
|
DUMP_SYSTEM_PROMPT: true, // --dump-system-prompt CLI flag for debugging
|
|
CACHED_MICROCOMPACT: true, // Cache-aware tool result truncation optimization
|
|
AWAY_SUMMARY: true, // "While you were away" recap after 5min blur
|
|
TRANSCRIPT_CLASSIFIER: true, // Auto-approval classifier for safe tool uses
|
|
ULTRATHINK: true, // Deep thinking mode — type "ultrathink" to boost reasoning
|
|
TOKEN_BUDGET: true, // Token budget tracking with usage warnings
|
|
HISTORY_PICKER: true, // Enhanced interactive prompt history picker
|
|
QUICK_SEARCH: true, // Ctrl+G quick search across prompts
|
|
SHOT_STATS: true, // Shot distribution stats in session summary
|
|
EXTRACT_MEMORIES: true, // Auto-extract durable memories from conversations
|
|
FORK_SUBAGENT: true, // Implicit context-forking when omitting subagent_type
|
|
VERIFICATION_AGENT: true, // Built-in read-only agent for test/verification
|
|
PROMPT_CACHE_BREAK_DETECTION: true, // Detect & log unexpected prompt cache invalidations
|
|
HOOK_PROMPTS: true, // Allow tools to request interactive user prompts
|
|
}
|
|
|
|
// ── Pre-process: replace feature() calls with boolean literals ──────
|
|
// Bun v1.3.9+ resolves `import { feature } from 'bun:bundle'` natively
|
|
// before plugins can intercept it via onResolve. The bun: namespace is
|
|
// handled by Bun's C++ resolver which runs before the JS plugin phase,
|
|
// so the previous onResolve/onLoad shim was silently ineffective — ALL
|
|
// feature() calls evaluated to false regardless of the featureFlags map.
|
|
//
|
|
// Fix: pre-process source files to strip the bun:bundle import and
|
|
// replace feature('FLAG') calls with their boolean literal. Files are
|
|
// modified in-place before Bun.build() and restored in a finally block.
|
|
|
|
// Match feature('FLAG') calls, including multi-line: feature(\n 'FLAG',\n)
|
|
const featureCallRe = /\bfeature\(\s*['"](\w+)['"][,\s]*\)/gs
|
|
const featureImportRe = /import\s*\{[^}]*\bfeature\b[^}]*\}\s*from\s*['"]bun:bundle['"];?\s*\n?/g
|
|
const modifiedFiles = new Map<string, string>() // path → original content
|
|
|
|
function preProcessFeatureFlags(dir: string) {
|
|
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
const full = join(dir, ent.name)
|
|
if (ent.isDirectory()) { preProcessFeatureFlags(full); continue }
|
|
if (!/\.(ts|tsx)$/.test(ent.name)) continue
|
|
|
|
const raw = readFileSync(full, 'utf-8')
|
|
if (!raw.includes('feature(')) continue
|
|
|
|
let contents = raw
|
|
contents = contents.replace(featureImportRe, '')
|
|
contents = contents.replace(featureCallRe, (_match, name) =>
|
|
String((featureFlags as Record<string, boolean>)[name] ?? false),
|
|
)
|
|
|
|
if (contents !== raw) {
|
|
modifiedFiles.set(full, raw)
|
|
writeFileSync(full, contents)
|
|
}
|
|
}
|
|
}
|
|
|
|
function restoreModifiedFiles() {
|
|
for (const [path, original] of modifiedFiles) {
|
|
writeFileSync(path, original)
|
|
}
|
|
modifiedFiles.clear()
|
|
}
|
|
|
|
preProcessFeatureFlags(join(import.meta.dir, '..', 'src'))
|
|
const numModified = modifiedFiles.size
|
|
|
|
// Restore source files on abrupt termination (Ctrl+C, kill, etc.)
|
|
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
|
process.on(signal, () => {
|
|
restoreModifiedFiles()
|
|
process.exit(signal === 'SIGINT' ? 130 : 143)
|
|
})
|
|
}
|
|
|
|
let result: Awaited<ReturnType<typeof Bun.build>> | undefined
|
|
let sdkResult: Awaited<ReturnType<typeof Bun.build>> | undefined
|
|
|
|
try {
|
|
|
|
result = await Bun.build({
|
|
entrypoints: ['./src/entrypoints/cli.tsx'],
|
|
outdir: './dist',
|
|
target: 'node',
|
|
format: 'esm',
|
|
splitting: false,
|
|
sourcemap: 'external',
|
|
minify: false,
|
|
naming: 'cli.mjs',
|
|
define: {
|
|
// MACRO.* build-time constants
|
|
// Keep the internal compatibility version high enough to pass
|
|
// first-party minimum-version guards, but expose the real package
|
|
// version separately in Open Claude branding.
|
|
'MACRO.VERSION': JSON.stringify('99.0.0'),
|
|
'MACRO.DISPLAY_VERSION': JSON.stringify(version),
|
|
'MACRO.BUILD_TIME': JSON.stringify(new Date().toISOString()),
|
|
'MACRO.ISSUES_EXPLAINER':
|
|
JSON.stringify('report the issue at https://github.com/Gitlawb/openclaude/issues'),
|
|
'MACRO.FEEDBACK_CHANNEL':
|
|
JSON.stringify('https://github.com/Gitlawb/openclaude/issues'),
|
|
'MACRO.PACKAGE_URL': JSON.stringify('@gitlawb/openclaude'),
|
|
'MACRO.NATIVE_PACKAGE_URL': 'undefined',
|
|
},
|
|
plugins: [
|
|
noTelemetryPlugin,
|
|
{
|
|
name: 'bun-bundle-shim',
|
|
setup(build) {
|
|
const internalFeatureStubModules = new Map([
|
|
[
|
|
'../daemon/workerRegistry.js',
|
|
'export async function runDaemonWorker() { throw new Error("Daemon worker is unavailable in the open build."); }',
|
|
],
|
|
[
|
|
'../daemon/main.js',
|
|
'export async function daemonMain() { throw new Error("Daemon mode is unavailable in the open build."); }',
|
|
],
|
|
[
|
|
'../cli/bg.js',
|
|
`
|
|
export async function psHandler() { throw new Error("Background sessions are unavailable in the open build."); }
|
|
export async function logsHandler() { throw new Error("Background sessions are unavailable in the open build."); }
|
|
export async function attachHandler() { throw new Error("Background sessions are unavailable in the open build."); }
|
|
export async function killHandler() { throw new Error("Background sessions are unavailable in the open build."); }
|
|
export async function handleBgFlag() { throw new Error("Background sessions are unavailable in the open build."); }
|
|
`,
|
|
],
|
|
[
|
|
'../cli/handlers/templateJobs.js',
|
|
'export async function templatesMain() { throw new Error("Template jobs are unavailable in the open build."); }',
|
|
],
|
|
[
|
|
'../environment-runner/main.js',
|
|
'export async function environmentRunnerMain() { throw new Error("Environment runner is unavailable in the open build."); }',
|
|
],
|
|
[
|
|
'../self-hosted-runner/main.js',
|
|
'export async function selfHostedRunnerMain() { throw new Error("Self-hosted runner is unavailable in the open build."); }',
|
|
],
|
|
] as const)
|
|
|
|
// bun:bundle feature() replacement is handled by the source
|
|
// pre-processing step above (see preProcessFeatureFlags).
|
|
// The previous onResolve/onLoad shim was ineffective in Bun
|
|
// v1.3.9+ because the bun: namespace is resolved natively
|
|
// before the JS plugin phase runs.
|
|
|
|
build.onResolve(
|
|
{ filter: /^\.\.\/(daemon\/workerRegistry|daemon\/main|cli\/bg|cli\/handlers\/templateJobs|environment-runner\/main|self-hosted-runner\/main)\.js$/ },
|
|
args => {
|
|
if (!internalFeatureStubModules.has(args.path)) return null
|
|
return {
|
|
path: args.path,
|
|
namespace: 'internal-feature-stub',
|
|
}
|
|
},
|
|
)
|
|
build.onLoad(
|
|
{ filter: /.*/, namespace: 'internal-feature-stub' },
|
|
args => ({
|
|
contents:
|
|
internalFeatureStubModules.get(args.path) ??
|
|
'export {}',
|
|
loader: 'js',
|
|
}),
|
|
)
|
|
|
|
// Resolve react/compiler-runtime to the standalone package
|
|
build.onResolve({ filter: /^react\/compiler-runtime$/ }, () => ({
|
|
path: 'react/compiler-runtime',
|
|
namespace: 'react-compiler-shim',
|
|
}))
|
|
build.onLoad(
|
|
{ filter: /.*/, namespace: 'react-compiler-shim' },
|
|
() => ({
|
|
contents: `export function c(size) { return new Array(size).fill(Symbol.for('react.memo_cache_sentinel')); }`,
|
|
loader: 'js',
|
|
}),
|
|
)
|
|
|
|
// NOTE: @opentelemetry/* kept as external deps (too many named exports to stub)
|
|
|
|
// Resolve native addon and missing snapshot imports to stubs
|
|
for (const mod of [
|
|
'audio-capture-napi',
|
|
'audio-capture.node',
|
|
'image-processor-napi',
|
|
'modifiers-napi',
|
|
'url-handler-napi',
|
|
'color-diff-napi',
|
|
'@anthropic-ai/mcpb',
|
|
'@ant/claude-for-chrome-mcp',
|
|
'@anthropic-ai/sandbox-runtime',
|
|
'asciichart',
|
|
'plist',
|
|
'cacache',
|
|
'fuse',
|
|
'code-excerpt',
|
|
'stack-utils',
|
|
]) {
|
|
build.onResolve({ filter: new RegExp(`^${mod}$`) }, () => ({
|
|
path: mod,
|
|
namespace: 'native-stub',
|
|
}))
|
|
}
|
|
build.onLoad(
|
|
{ filter: /.*/, namespace: 'native-stub' },
|
|
() => ({
|
|
// Comprehensive stub that handles any named export via Proxy
|
|
contents: `
|
|
const noop = () => null;
|
|
const noopClass = class {};
|
|
const handler = {
|
|
get(_, prop) {
|
|
if (prop === '__esModule') return true;
|
|
if (prop === 'default') return new Proxy({}, handler);
|
|
if (prop === 'ExportResultCode') return { SUCCESS: 0, FAILED: 1 };
|
|
if (prop === 'resourceFromAttributes') return () => ({});
|
|
if (prop === 'SandboxRuntimeConfigSchema') return { parse: () => ({}) };
|
|
return noop;
|
|
}
|
|
};
|
|
const stub = new Proxy(noop, handler);
|
|
export default stub;
|
|
export const __stub = true;
|
|
// Named exports for all known imports
|
|
export const SandboxViolationStore = null;
|
|
export const SandboxManager = new Proxy({}, { get: () => noop });
|
|
export const SandboxRuntimeConfigSchema = { parse: () => ({}) };
|
|
export const BROWSER_TOOLS = [];
|
|
export const getMcpConfigForManifest = noop;
|
|
export const ColorDiff = null;
|
|
export const ColorFile = null;
|
|
export const getSyntaxTheme = noop;
|
|
export const plot = noop;
|
|
export const createClaudeForChromeMcpServer = noop;
|
|
// OpenTelemetry exports
|
|
export const ExportResultCode = { SUCCESS: 0, FAILED: 1 };
|
|
export const resourceFromAttributes = noop;
|
|
export const Resource = noopClass;
|
|
export const SimpleSpanProcessor = noopClass;
|
|
export const BatchSpanProcessor = noopClass;
|
|
export const NodeTracerProvider = noopClass;
|
|
export const BasicTracerProvider = noopClass;
|
|
export const OTLPTraceExporter = noopClass;
|
|
export const OTLPLogExporter = noopClass;
|
|
export const OTLPMetricExporter = noopClass;
|
|
export const PrometheusExporter = noopClass;
|
|
export const LoggerProvider = noopClass;
|
|
export const SimpleLogRecordProcessor = noopClass;
|
|
export const BatchLogRecordProcessor = noopClass;
|
|
export const MeterProvider = noopClass;
|
|
export const PeriodicExportingMetricReader = noopClass;
|
|
export const trace = { getTracer: () => ({ startSpan: () => ({ end: noop, setAttribute: noop, setStatus: noop, recordException: noop }) }) };
|
|
export const context = { active: noop, with: (_, fn) => fn() };
|
|
export const SpanStatusCode = { OK: 0, ERROR: 1, UNSET: 2 };
|
|
export const ATTR_SERVICE_NAME = 'service.name';
|
|
export const ATTR_SERVICE_VERSION = 'service.version';
|
|
export const SEMRESATTRS_SERVICE_NAME = 'service.name';
|
|
export const SEMRESATTRS_SERVICE_VERSION = 'service.version';
|
|
export const AggregationTemporality = { CUMULATIVE: 0, DELTA: 1 };
|
|
export const DataPointType = { HISTOGRAM: 0, SUM: 1, GAUGE: 2 };
|
|
export const InstrumentType = { COUNTER: 0, HISTOGRAM: 1, UP_DOWN_COUNTER: 2 };
|
|
export const PushMetricExporter = noopClass;
|
|
export const SeverityNumber = {};
|
|
`,
|
|
loader: 'js',
|
|
}),
|
|
)
|
|
|
|
// Resolve .md and .txt file imports to empty string stubs
|
|
build.onResolve({ filter: /\.(md|txt)$/ }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'text-stub',
|
|
}))
|
|
build.onLoad(
|
|
{ filter: /.*/, namespace: 'text-stub' },
|
|
() => ({
|
|
contents: `export default '';`,
|
|
loader: 'js',
|
|
}),
|
|
)
|
|
|
|
// Pre-scan: find all missing modules that need stubbing
|
|
// (Bun's onResolve corrupts module graph even when returning null,
|
|
// so we use exact-match resolvers instead of catch-all patterns)
|
|
const fs = require('fs')
|
|
const pathMod = require('path')
|
|
const srcDir = pathMod.resolve(__dirname, '..', 'src')
|
|
const missingModules = new Set<string>()
|
|
const missingModuleExports = new Map<string, Set<string>>()
|
|
|
|
// Known missing external packages
|
|
for (const pkg of [
|
|
'@ant/computer-use-mcp',
|
|
'@ant/computer-use-mcp/sentinelApps',
|
|
'@ant/computer-use-mcp/types',
|
|
'@ant/computer-use-swift',
|
|
'@ant/computer-use-input',
|
|
]) {
|
|
missingModules.add(pkg)
|
|
}
|
|
|
|
// Scan source to find imports that can't resolve
|
|
function scanForMissingImports() {
|
|
function checkAndRegister(specifier: string, fileDir: string, namedPart: string) {
|
|
const names = namedPart.split(',')
|
|
.map((s: string) => s.trim().replace(/^type\s+/, ''))
|
|
.filter((s: string) => s && !s.startsWith('type '))
|
|
|
|
// Check src/tasks/ non-relative imports
|
|
if (specifier.startsWith('src/tasks/')) {
|
|
const resolved = pathMod.resolve(__dirname, '..', specifier)
|
|
const candidates = [
|
|
resolved,
|
|
`${resolved}.ts`, `${resolved}.tsx`,
|
|
resolved.replace(/\.js$/, '.ts'), resolved.replace(/\.js$/, '.tsx'),
|
|
pathMod.join(resolved, 'index.ts'), pathMod.join(resolved, 'index.tsx'),
|
|
]
|
|
if (!candidates.some((c: string) => fs.existsSync(c))) {
|
|
missingModules.add(specifier)
|
|
}
|
|
}
|
|
// Check relative .js imports
|
|
else if (specifier.endsWith('.js') && (specifier.startsWith('./') || specifier.startsWith('../'))) {
|
|
const resolved = pathMod.resolve(fileDir, specifier)
|
|
const tsVariant = resolved.replace(/\.js$/, '.ts')
|
|
const tsxVariant = resolved.replace(/\.js$/, '.tsx')
|
|
if (!fs.existsSync(resolved) && !fs.existsSync(tsVariant) && !fs.existsSync(tsxVariant)) {
|
|
missingModules.add(specifier)
|
|
}
|
|
}
|
|
|
|
// Track named exports for missing modules
|
|
if (names.length > 0) {
|
|
if (!missingModuleExports.has(specifier)) missingModuleExports.set(specifier, new Set())
|
|
for (const n of names) missingModuleExports.get(specifier)!.add(n)
|
|
}
|
|
}
|
|
|
|
function walk(dir: string) {
|
|
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = pathMod.join(dir, ent.name)
|
|
if (ent.isDirectory()) { walk(full); continue }
|
|
if (!/\.(ts|tsx)$/.test(ent.name)) continue
|
|
const rawCode: string = fs.readFileSync(full, 'utf-8')
|
|
const fileDir = pathMod.dirname(full)
|
|
|
|
// Strip comments before scanning for imports/requires.
|
|
// The regex scanner matches require()/import() patterns
|
|
// inside JSDoc comments, causing false-positive missing
|
|
// module detection that breaks the build with noop stubs.
|
|
const code = rawCode
|
|
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
|
|
.replace(/\/\/.*$/gm, '') // line comments
|
|
|
|
// Collect static imports: import { X } from '...'
|
|
for (const m of code.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))?\s*(?:,\s*\{([^}]*)\})?\s*from\s+['"](.*?)['"]/g)) {
|
|
checkAndRegister(m[4], fileDir, m[1] || m[3] || '')
|
|
}
|
|
|
|
// Collect dynamic requires: require('...') — these are used
|
|
// behind feature() gates and become live when flags are enabled.
|
|
for (const m of code.matchAll(/require\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)/g)) {
|
|
checkAndRegister(m[1], fileDir, '')
|
|
}
|
|
|
|
// Collect dynamic imports: import('...')
|
|
for (const m of code.matchAll(/import\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)/g)) {
|
|
checkAndRegister(m[1], fileDir, '')
|
|
}
|
|
}
|
|
}
|
|
walk(srcDir)
|
|
}
|
|
scanForMissingImports()
|
|
|
|
// Register exact-match resolvers for each missing module
|
|
for (const mod of missingModules) {
|
|
const escaped = mod.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
build.onResolve({ filter: new RegExp(`^${escaped}$`) }, () => ({
|
|
path: mod,
|
|
namespace: 'missing-module-stub',
|
|
}))
|
|
}
|
|
|
|
build.onLoad(
|
|
{ filter: /.*/, namespace: 'missing-module-stub' },
|
|
(args) => {
|
|
const names = missingModuleExports.get(args.path) ?? new Set()
|
|
const exports = [...names].map(n => `export const ${n} = noop;`).join('\n')
|
|
return {
|
|
contents: `
|
|
const noop = () => null;
|
|
export default noop;
|
|
${exports}
|
|
`,
|
|
loader: 'js',
|
|
}
|
|
},
|
|
)
|
|
},
|
|
},
|
|
],
|
|
external: CLI_EXTERNALS,
|
|
})
|
|
|
|
if (!result.success) {
|
|
console.error('Build failed:')
|
|
for (const log of result.logs) {
|
|
console.error(log)
|
|
}
|
|
process.exitCode = 1
|
|
} else {
|
|
console.log(`✓ Built openclaude v${version} → dist/cli.mjs`)
|
|
}
|
|
|
|
// ── SDK Bundle Build ──────────────────────────────────────────────────────
|
|
// SDK is a separate bundle for npm consumption - must NOT bundle React/Ink
|
|
console.log('Building SDK bundle...')
|
|
|
|
sdkResult = await Bun.build({
|
|
entrypoints: ['./src/entrypoints/sdk/index.ts'],
|
|
outdir: './dist',
|
|
target: 'node',
|
|
format: 'esm',
|
|
splitting: false,
|
|
sourcemap: 'external',
|
|
minify: false,
|
|
naming: 'sdk.mjs',
|
|
define: {
|
|
'MACRO.VERSION': JSON.stringify(version),
|
|
'MACRO.DISPLAY_VERSION': JSON.stringify(version),
|
|
'MACRO.BUILD_TIME': JSON.stringify(new Date().toISOString()),
|
|
'MACRO.ISSUES_EXPLAINER':
|
|
JSON.stringify('report the issue at https://github.com/Gitlawb/openclaude/issues'),
|
|
'MACRO.FEEDBACK_CHANNEL':
|
|
JSON.stringify('https://github.com/Gitlawb/openclaude/issues'),
|
|
'MACRO.PACKAGE_URL': JSON.stringify('@gitlawb/openclaude'),
|
|
'MACRO.NATIVE_PACKAGE_URL': 'undefined',
|
|
},
|
|
// External: everything TUI-related + native modules
|
|
external: SDK_EXTERNALS,
|
|
plugins: [
|
|
noTelemetryPlugin,
|
|
// Stub missing internal/optional modules (same pattern as CLI build)
|
|
{
|
|
name: 'sdk-missing-stub',
|
|
setup(build) {
|
|
const missingModules = [
|
|
'@anthropic-ai/mcpb',
|
|
'@ant/claude-for-chrome-mcp',
|
|
'@ant/computer-use-mcp',
|
|
'@ant/computer-use-swift',
|
|
'@ant/computer-use-input',
|
|
'@anthropic-ai/sandbox-runtime',
|
|
'audio-capture-napi', 'audio-capture.node',
|
|
'image-processor-napi', 'modifiers-napi', 'url-handler-napi', 'color-diff-napi',
|
|
'asciichart', 'plist', 'cacache', 'fuse', 'code-excerpt', 'stack-utils',
|
|
]
|
|
for (const mod of missingModules) {
|
|
const escaped = mod.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
build.onResolve({ filter: new RegExp(`^${escaped}$`) }, () => ({
|
|
path: mod,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
}
|
|
// Stub relative imports to TUI directories
|
|
// Use (\.\.?\/)+ to match multiple ../ prefixes like ../../components/
|
|
build.onResolve({ filter: /^(\.\.?\/)+components\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
build.onResolve({ filter: /^(\.\.?\/)+ink\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
build.onResolve({ filter: /^(\.\.?\/)+commands\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
build.onResolve({ filter: /^(\.\.?\/)+cli\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
// Stub relative imports to state/ directory EXCEPT for store.js and AppStateStore.js
|
|
// which are React-free utilities needed by the SDK for state management.
|
|
build.onResolve({ filter: /^(\.\.?\/)+state\// }, (args) => {
|
|
// Exclude React-free state utilities from stubbing
|
|
const isReactFreeStateModule =
|
|
args.path.endsWith('store.js') ||
|
|
args.path.endsWith('AppStateStore.js') ||
|
|
args.path.endsWith('store.ts') ||
|
|
args.path.endsWith('AppStateStore.ts')
|
|
if (isReactFreeStateModule) {
|
|
return null // Let Bun resolve normally
|
|
}
|
|
return {
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}
|
|
})
|
|
build.onResolve({ filter: /^(\.\.?\/)+context\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
// Stub root ink.js barrel imports (../ink.js, ../../ink.js, ./ink.js)
|
|
// These are TUI entry points that import React directly.
|
|
build.onResolve({ filter: /^(\.\.?\/)+ink\.js$/ }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
// Also stub ./ paths used by re-exports in src/ink.ts, src/components/, etc.
|
|
build.onResolve({ filter: /^\.\/components\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
build.onResolve({ filter: /^\.\/ink\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
build.onResolve({ filter: /^\.\/commands\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
build.onResolve({ filter: /^\.\/cli\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
// Stub tool UI.js imports from within src/tools/ subdirectories.
|
|
// Tool UI modules render React/TUI components that are not needed
|
|
// in the SDK (headless) bundle. Only stub when the importer is
|
|
// inside src/tools/ to avoid blind-matching other UI.js files.
|
|
build.onResolve({ filter: /(?:^|\/)UI\.js$/ }, (args) => {
|
|
// Normalize path separators for cross-platform matching
|
|
const importer = (args.importer || '').replace(/\\/g, '/')
|
|
if (importer.includes('src/tools/')) {
|
|
return {
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}
|
|
}
|
|
return null
|
|
})
|
|
|
|
// Stub src/ alias imports that resolve to TUI directories
|
|
// These are used by require('src/components/...') style imports
|
|
build.onResolve({ filter: /^src\/components\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
build.onResolve({ filter: /^src\/ink\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
// Stub src/ink.js root barrel import (used by some files via 'src/ink.js')
|
|
build.onResolve({ filter: /^src\/ink\.js$/ }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
build.onResolve({ filter: /^src\/commands\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
build.onResolve({ filter: /^src\/cli\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
// src/state/ contains AppState.tsx with React hooks, but store.ts and AppStateStore.ts
|
|
// are React-free utilities needed by the SDK - exclude them from stubbing.
|
|
build.onResolve({ filter: /^src\/state\// }, (args) => {
|
|
// Exclude React-free state utilities from stubbing
|
|
const isReactFreeStateModule =
|
|
args.path.endsWith('store.js') ||
|
|
args.path.endsWith('AppStateStore.js') ||
|
|
args.path.endsWith('store.ts') ||
|
|
args.path.endsWith('AppStateStore.ts')
|
|
if (isReactFreeStateModule) {
|
|
return null // Let Bun resolve normally
|
|
}
|
|
return {
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}
|
|
})
|
|
build.onResolve({ filter: /^src\/context\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
// Stub src/keybindings/ — React-dependent keybinding system not needed in SDK
|
|
build.onResolve({ filter: /^src\/keybindings\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
build.onResolve({ filter: /^(\.\.?\/)+keybindings\// }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
// Stub react-compiler-runtime — not needed in SDK bundle
|
|
build.onResolve({ filter: /^react-compiler-runtime$/ }, () => ({
|
|
path: 'react-compiler-runtime',
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
// Stub TUI-only React hook files that leak into SDK via tool imports.
|
|
// These are imported transitively through spawnMultiAgent → It2SetupPrompt
|
|
// and through keybinding hooks. The SDK doesn't use TUI features.
|
|
for (const hookPath of [
|
|
'useDoublePress.js', 'useExitOnCtrlCD.js', 'useExitOnCtrlCDWithKeybindings.js',
|
|
'useTerminalSize.js', 'useShortcutDisplay.js',
|
|
]) {
|
|
const escaped = hookPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
build.onResolve({ filter: new RegExp(`(^|/)${escaped}$`) }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
}
|
|
// Stub It2SetupPrompt.tsx — TUI component pulled in by spawnMultiAgent
|
|
build.onResolve({ filter: /It2SetupPrompt\.js$/ }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
|
|
// Stub react/jsx-dev-runtime with local no-op — tool .tsx files compile
|
|
// to jsxDEV() calls that are never rendered in SDK headless mode.
|
|
// This eliminates the external react/jsx-dev-runtime import entirely.
|
|
build.onResolve({ filter: /^react\/jsx-dev-runtime$/ }, () => ({
|
|
path: 'react/jsx-dev-runtime',
|
|
namespace: 'sdk-jsx-stub',
|
|
}))
|
|
build.onLoad({ filter: /.*/, namespace: 'sdk-jsx-stub' }, () => ({
|
|
contents: `
|
|
// No-op jsxDEV: returns null (SDK never renders JSX)
|
|
export function jsxDEV(type, props, key, isStaticChildren, source, self) {
|
|
return null;
|
|
}
|
|
// No-op Fragment: returns null (never used in SDK rendering)
|
|
export const Fragment = null;
|
|
`,
|
|
loader: 'js',
|
|
}))
|
|
|
|
// Resolve .md and .txt file imports (used by yolo-classifier etc.) to empty string stubs
|
|
build.onResolve({ filter: /\.(md|txt)$/, namespace: 'file' }, (args) => ({
|
|
path: args.path,
|
|
namespace: 'sdk-text-stub',
|
|
}))
|
|
build.onLoad(
|
|
{ filter: /.*/, namespace: 'sdk-text-stub' },
|
|
() => ({
|
|
contents: `export default '';`,
|
|
loader: 'js',
|
|
}),
|
|
)
|
|
|
|
// Stub require() calls to modules that don't exist on disk.
|
|
// These are feature-gated lazy imports (e.g. cachedMCConfig, VerifyPlanExecutionTool,
|
|
// mcpSkills) that only resolve when the feature flag is enabled at build time.
|
|
// Pre-scan source files for require('...') to non-existent .js paths.
|
|
const sdkRequireScanDir = require('path').resolve(__dirname, '..', 'src')
|
|
const sdkMissingRequires = new Set<string>()
|
|
const sdkPathMod = require('path')
|
|
const sdkFs = require('fs')
|
|
function scanSdkRequireImports() {
|
|
function walkRequireScan(dir: string) {
|
|
for (const ent of sdkFs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = sdkPathMod.join(dir, ent.name)
|
|
if (ent.isDirectory()) { walkRequireScan(full); continue }
|
|
if (!/\.(ts|tsx)$/.test(ent.name)) continue
|
|
const fileDir = sdkPathMod.dirname(full)
|
|
const rawCode: string = sdkFs.readFileSync(full, 'utf-8')
|
|
// Strip comments
|
|
const code = rawCode
|
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
.replace(/\/\/.*$/gm, '')
|
|
// Collect require('...') calls for relative .js paths
|
|
for (const m of code.matchAll(/require\(\s*['"](\.\.?\/[^'"]+\.js)['"]\s*\)/g)) {
|
|
const specifier = m[1]
|
|
const resolved = sdkPathMod.resolve(fileDir, specifier)
|
|
const tsVariant = resolved.replace(/\.js$/, '.ts')
|
|
if (!sdkFs.existsSync(resolved) && !sdkFs.existsSync(tsVariant)) {
|
|
sdkMissingRequires.add(specifier)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
walkRequireScan(sdkRequireScanDir)
|
|
}
|
|
scanSdkRequireImports()
|
|
for (const mod of sdkMissingRequires) {
|
|
const escaped = mod.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
build.onResolve({ filter: new RegExp(`^${escaped}$`) }, () => ({
|
|
path: mod,
|
|
namespace: 'sdk-missing-stub',
|
|
}))
|
|
}
|
|
|
|
// Pre-scan: find all named imports for each stubbed module so we can
|
|
// generate matching exports dynamically (avoids the whack-a-mole of
|
|
// static export lists that break whenever a new import is added).
|
|
const fs = require('fs')
|
|
const pathMod = require('path')
|
|
const srcDir = pathMod.resolve(__dirname, '..', 'src')
|
|
const sdkStubExports = new Map<string, Set<string>>() // module path → set of imported names
|
|
|
|
function scanSdkStubImports() {
|
|
function register(specifier: string, namedPart: string) {
|
|
const rawNames = namedPart.split(',')
|
|
.map((s: string) => s.trim().replace(/^type\s+/, ''))
|
|
.filter((s: string) => s && !s.startsWith('type '))
|
|
if (rawNames.length === 0) return
|
|
if (!sdkStubExports.has(specifier)) sdkStubExports.set(specifier, new Set())
|
|
const names = sdkStubExports.get(specifier)!
|
|
for (const s of rawNames) {
|
|
// Handle "originalName as localName" — export BOTH names
|
|
// because Bun validates the original export name exists
|
|
const asMatch = s.match(/^(\w+)\s+as\s+(\w+)$/)
|
|
if (asMatch) {
|
|
names.add(asMatch[1]) // original name
|
|
names.add(asMatch[2]) // aliased name
|
|
} else {
|
|
names.add(s)
|
|
}
|
|
}
|
|
}
|
|
const isStubbedSpecifier = (s: string) =>
|
|
missingModules.includes(s) ||
|
|
/^(\.\.?\/)+(components|ink|commands|cli|context|state|keybindings)\//.test(s) ||
|
|
/^(\.\.?\/)+ink\.js$/.test(s) ||
|
|
/^src\/(components|ink|commands|cli|state|context|keybindings)\//.test(s) ||
|
|
/^src\/ink\.js$/.test(s) ||
|
|
/(?:^|\/)UI\.js$/.test(s) ||
|
|
s === 'react-compiler-runtime' ||
|
|
/(?:^|\/)It2SetupPrompt\.js$/.test(s) ||
|
|
/(?:^|\/)(useDoublePress|useExitOnCtrlCD|useExitOnCtrlCDWithKeybindings|useTerminalSize|useShortcutDisplay)\.js$/.test(s)
|
|
function walk(dir: string) {
|
|
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = pathMod.join(dir, ent.name)
|
|
if (ent.isDirectory()) { walk(full); continue }
|
|
if (!/\.(ts|tsx)$/.test(ent.name)) continue
|
|
const fileDir = pathMod.dirname(full)
|
|
const rawCode: string = fs.readFileSync(full, 'utf-8')
|
|
// Strip comments
|
|
const code = rawCode
|
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
.replace(/\/\/.*$/gm, '')
|
|
// Collect static imports: import { X } from '...'
|
|
for (const m of code.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))?\s*(?:,\s*\{([^}]*)\})?\s*from\s+['"](.*?)['"]/g)) {
|
|
const specifier = m[4]
|
|
if (isStubbedSpecifier(specifier)) {
|
|
register(specifier, m[1] || m[3] || '')
|
|
}
|
|
}
|
|
// Collect re-exports: export { X, Y } from '...'
|
|
for (const m of code.matchAll(/export\s+\{([^}]*)\}\s*from\s+['"](.*?)['"]/g)) {
|
|
const specifier = m[2]
|
|
if (isStubbedSpecifier(specifier)) {
|
|
register(specifier, m[1])
|
|
}
|
|
}
|
|
// Collect star re-exports: export * from '...'
|
|
// These re-export all named exports from the source module.
|
|
// For stubbed modules, we need to scan the re-exported module
|
|
// to find its exports and register them under the stubbed specifier.
|
|
for (const m of code.matchAll(/export\s+\*\s+from\s+['"](.*?)['"]/g)) {
|
|
const specifier = m[1]
|
|
if (isStubbedSpecifier(specifier)) {
|
|
// The re-exported module might itself be stubbed, so we need
|
|
// to find its exports. Parse the relative path and scan it.
|
|
const reexportPath = pathMod.resolve(fileDir, specifier)
|
|
const reexportBase = reexportPath.replace(/\.js$/, '')
|
|
const candidates = [
|
|
`${reexportBase}.ts`,
|
|
`${reexportBase}.tsx`,
|
|
reexportPath,
|
|
`${reexportPath}.ts`,
|
|
`${reexportPath}.tsx`,
|
|
]
|
|
for (const candidate of candidates) {
|
|
if (fs.existsSync(candidate)) {
|
|
const reexportCode = fs.readFileSync(candidate, 'utf-8')
|
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
.replace(/\/\/.*$/gm, '')
|
|
// Collect exports from the re-exported module
|
|
for (const exp of reexportCode.matchAll(/export\s+(?:const|let|var|function|class|type|interface)\s+(\w+)/g)) {
|
|
register(specifier, exp[1])
|
|
}
|
|
for (const exp of reexportCode.matchAll(/export\s+\{([^}]*)\}/g)) {
|
|
register(specifier, exp[1])
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
walk(srcDir)
|
|
}
|
|
scanSdkStubImports()
|
|
|
|
// Special default exports for known modules
|
|
const defaultExportOverrides: Record<string, string> = {
|
|
'stringWidth': '(s) => s?.length || 0',
|
|
'wrapAnsi': '(s) => s',
|
|
'instances': 'new Map()',
|
|
'selectableUserMessagesFilter': '() => true',
|
|
'messagesAfterAreOnlySynthetic': '() => false',
|
|
'SandboxManager': 'class { static isSupportedPlatform = () => false; static create = noop; static Version = \'\'; }',
|
|
'SandboxRuntimeConfigSchema': '{ parse: noop }',
|
|
'SandboxViolationStore': 'null',
|
|
'BaseSandboxManager': 'class { static isSupportedPlatform = () => false; }',
|
|
'ExportResultCode': '{ SUCCESS: 0, FAILED: 1 }',
|
|
'linkifyUrlsInText': '(s) => s',
|
|
}
|
|
|
|
build.onLoad({ filter: /.*/, namespace: 'sdk-missing-stub' }, (args) => {
|
|
const names = sdkStubExports.get(args.path) ?? new Set()
|
|
const parts: string[] = []
|
|
for (const n of names) {
|
|
if (n === 'default') continue // handled by `export default noop` below
|
|
const val = defaultExportOverrides[n] ?? 'noop'
|
|
parts.push(`export const ${n} = ${val};`)
|
|
}
|
|
return {
|
|
contents: `
|
|
const noop = () => null;
|
|
export default noop;
|
|
export const __stub = true;
|
|
${parts.join('\n')}
|
|
`,
|
|
loader: 'js',
|
|
}
|
|
})
|
|
},
|
|
},
|
|
],
|
|
})
|
|
|
|
if (!sdkResult.success) {
|
|
console.error('SDK build failed:')
|
|
for (const log of sdkResult.logs) {
|
|
console.error(log)
|
|
}
|
|
process.exitCode = 1
|
|
} else {
|
|
console.log(`✓ Built SDK bundle → dist/sdk.mjs`)
|
|
}
|
|
|
|
} finally {
|
|
// Always restore source files, even if Bun.build() throws
|
|
restoreModifiedFiles()
|
|
console.log(` 🔄 feature-flags: pre-processed ${numModified} files (restored)`)
|
|
}
|
|
|
|
// ── Validate SDK bundle for React/Ink leakage ──────────────────────────────
|
|
if (sdkResult?.success) {
|
|
const sdkBundle = readFileSync('./dist/sdk.mjs', 'utf-8')
|
|
// Patterns that indicate React/Ink code leaked into the SDK bundle.
|
|
const reactInkPatterns = [
|
|
/from\s+["']react["']/, // direct react import
|
|
/from\s+["']ink["']/, // direct ink import
|
|
/from\s+["']react\/jsx-dev-runtime["']/, // JSX runtime (must be stubbed, not external)
|
|
]
|
|
const leaks: string[] = []
|
|
for (const pattern of reactInkPatterns) {
|
|
const match = sdkBundle.match(pattern)
|
|
if (match) leaks.push(match[0])
|
|
}
|
|
if (leaks.length > 0) {
|
|
console.error(`\n❌ SDK bundle contains React/Ink imports (must be stubbed):`)
|
|
for (const leak of leaks) console.error(` - ${leak}`)
|
|
process.exitCode = 1
|
|
} else {
|
|
console.log(`✓ SDK bundle: no React/Ink leakage detected`)
|
|
}
|
|
}
|
|
|
|
// ── Validate external lists ──────────────────────────────────────────────
|
|
if (result?.success && sdkResult?.success) {
|
|
console.log('\nValidating external lists...')
|
|
const validation = Bun.spawnSync(['bun', 'run', 'scripts/validate-externals.ts'], {
|
|
stdout: 'inherit',
|
|
stderr: 'inherit',
|
|
})
|
|
if (validation.exitCode !== 0) {
|
|
process.exitCode = 1
|
|
}
|
|
}
|