2026-07-12 07:44:21 +08:00
#!/usr/bin/env node
2026-07-15 14:44:04 +00:00
import { access } from "node:fs/promises" ;
2026-10-24 11:50:01 +08:00
import { dirname , resolve } from "node:path" ;
2026-07-15 14:44:04 +00:00
import { fileURLToPath } from "node:url" ;
2026-10-24 11:50:01 +08:00
import { prepareCliProcessEnvironment } from "./lib/cli-process-environment.mjs" ;
2026-07-12 08:45:35 +08:00
// Route production MCP before importing Incur. Incur owns registration helpers
2026-10-19 08:58:51 -07:00
// such as `mcp add`, while the MCP runtime serves the tool catalog over stdio
// or, with `--http`, over streamable HTTP.
2026-07-12 07:44:21 +08:00
const args = process . argv . slice ( 2 );
2026-07-12 23:42:20 +00:00
const { default : packageJson } = await import ( "../package.json" , {
with : { type : "json" },
});
2026-10-24 11:50:01 +08:00
prepareCliProcessEnvironment (
process . env ,
process . platform ,
packageJson . version ,
);
2026-10-24 02:30:51 +08:00
const packageRoot = resolve ( dirname ( fileURLToPath ( import . meta . url )), ".." );
const builtinArgumentRuntime = "dist/cli-runtime/cliBuiltinArguments.js" ;
const builtinArguments = ( await compiledRuntimeExists ([ builtinArgumentRuntime ]))
? await import ( "../dist/cli-runtime/cliBuiltinArguments.js" )
: undefined ;
2026-10-24 04:23:25 +08:00
const isShellCompletion =
builtinArguments ? . cliCompletionShell ? .( process . env ) !== undefined ;
const doctorArguments = isShellCompletion
? undefined
: builtinArguments ? . productionMcpDoctorArguments ( args );
const isMcpEntry =
! isShellCompletion && ( args [ 0 ] === "--mcp" || args [ 0 ] === "mcp" );
2026-10-19 08:58:51 -07:00
const isMcpMode = isMcpEntry && args . length === 1 ;
2026-10-24 02:30:51 +08:00
const isMcpDoctorMode = doctorArguments !== undefined ;
2026-10-19 08:58:51 -07:00
// Only flags may follow the entry: `mcp add --http` stays Incur's `mcp add`,
// while `mcp -h --http` reaches the HTTP entry's own help.
const isMcpHttpMode =
isMcpEntry &&
( args [ 1 ] ? . startsWith ( "-" ) ?? false ) &&
args . slice ( 1 ). includes ( "--http" );
2026-10-14 18:20:19 -07:00
/** Exit status of a writer terminated by SIGPIPE (128 + 13). */
const SIGPIPE_EXIT_STATUS = 141 ;
// Set once the reader of CLI output has closed the pipe.
let stdoutClosed = false ;
2026-10-24 02:30:51 +08:00
const runtimeFiles = [
builtinArgumentRuntime ,
...( isMcpMode || isMcpHttpMode
2026-10-19 08:58:51 -07:00
? [ "dist/mcp/main.js" ]
: isMcpDoctorMode
? [ "dist/mcp/main.js" , "dist/cli-runtime/mcpDoctor.js" ]
: [
"dist/cli-runtime/cli.js" ,
"dist/cli-runtime/cliOutput.js" ,
"dist/cli-runtime/cliResultOutput.js" ,
2026-10-24 02:30:51 +08:00
]),
];
2026-07-12 07:44:21 +08:00
2026-08-10 02:59:52 +08:00
if ( ! ( await compiledRuntimeExists ( runtimeFiles ))) {
2026-07-15 14:44:04 +00:00
process . stderr . write (
2026-10-05 22:19:35 -07:00
`REA's compiled runtime is missing. Run \`npm ci && npm run build:cached\` in ${ packageRoot } to install dependencies and build REA, then restart it. If this is an installed package, reinstall rea-agents.\n` ,
2026-07-15 14:44:04 +00:00
);
process . exitCode = 1 ;
} else if ( isMcpMode ) {
2026-10-11 21:01:01 +08:00
const { runEntrypoint } = await import ( "../dist/mcp/main.js" );
2026-07-13 19:09:27 +00:00
await runEntrypoint ();
2026-10-19 08:58:51 -07:00
} else if ( isMcpHttpMode ) {
const { runMcpHttpEntrypoint } = await import ( "../dist/mcp/main.js" );
await runMcpHttpEntrypoint ( args . slice ( 1 ));
2026-07-22 20:38:26 +08:00
} else if ( isMcpDoctorMode ) {
2026-10-14 18:20:19 -07:00
guardClosedStdout ();
2026-10-27 20:53:29 -07:00
// Doctor runs outside Incur, so handle its global timing flag here. Keep a
// token consumed by --format as a value, even when it looks like a flag.
const outputValues = new Set (
builtinArguments
. splitIncurBuiltinArguments ( doctorArguments )
. outputValues . map (({ index }) => index ),
);
const doctorCliArguments = doctorArguments . filter (
( argument , index ) => argument !== "--timings" || outputValues . has ( index ),
);
const timingRequested = doctorCliArguments . length !== doctorArguments . length ;
const started = performance . now ();
try {
const { runProductionMcpDoctorCli } =
await import ( "../dist/cli-runtime/mcpDoctor.js" );
const result = await runProductionMcpDoctorCli ( doctorCliArguments , {
dispatcherPath : fileURLToPath ( import . meta . url ),
packageRoot ,
stdoutIsTerminal : process . stdout . isTTY === true ,
});
writeStdout ( result . output );
process . exitCode = result . exitCode ;
} finally {
if ( timingRequested )
process . stderr . write (
` ${ JSON . stringify ({ rea_timings : { version : 1 , command_ms : Math . round ( performance . now () - started ) } })}\n` ,
);
}
2026-07-12 07:44:21 +08:00
} else {
2026-07-22 20:38:26 +08:00
const {
2026-10-23 21:06:03 +05:00
cliConfigurationError ,
2026-07-22 20:38:26 +08:00
renderCliOutputArgumentError ,
2026-10-07 02:51:10 -07:00
renderEmptyFilteredCliOutput ,
2026-10-23 21:06:03 +05:00
renderVersionReport ,
2026-10-24 00:30:07 +08:00
parseCliOutputArguments ,
2026-10-12 09:25:27 +08:00
} = await import ( "../dist/cli-runtime/cliOutput.js" );
2026-10-14 18:20:19 -07:00
guardClosedStdout ();
2026-10-27 09:29:20 +08:00
const cliArgs = isShellCompletion
? args
: builtinArguments . normalizeCliArguments ( args );
2026-10-24 04:23:25 +08:00
// Completion words are transport data, including partial output flags.
const outputOptions = parseCliOutputArguments (
isShellCompletion ? [] : cliArgs ,
);
2026-10-24 00:30:07 +08:00
const outputArguments = outputOptions . validation ;
2026-10-23 21:06:03 +05:00
// `--version` with an explicit format reports the runtime as a document;
// plain `--version` stays Incur's bare version string.
const versionOutput = renderVersionReport (
2026-10-24 00:30:07 +08:00
outputOptions ,
2026-10-23 21:06:03 +05:00
{
packageName : packageJson . name ,
packageVersion : packageJson . version ,
versions : process . versions ,
platform : process . platform ,
architecture : process . arch ,
},
process . stdout . isTTY === true ,
);
2026-10-12 06:07:37 -07:00
const configurationError = cliConfigurationError ( process . env );
2026-10-21 19:01:20 +08:00
let dispatchFailure ;
2026-10-23 21:06:03 +05:00
let closeCliJavaScriptResources ;
2026-10-21 19:01:20 +08:00
// Incur can exit from the callback below before this function returns.
// That callback owns the status, including a closed-pipe exit of 141.
let exiting = false ;
try {
// Doctor is the readiness report for this failure. Other commands still stop
// here so they do not run with an unusable log level.
2026-10-24 08:54:24 +08:00
if (
configurationError !== undefined &&
( isShellCompletion ||
builtinArguments . splitIncurBuiltinArguments ( cliArgs ). command [ 0 ] !==
"doctor" )
) {
2026-10-21 19:01:20 +08:00
process . stderr . write ( ` ${ configurationError } \n` );
process . exitCode = 1 ;
} else if ( ! outputArguments . ok ) {
writeStdout ( renderCliOutputArgumentError ( outputArguments ));
process . exitCode = 1 ;
2026-10-23 21:06:03 +05:00
} else if ( versionOutput !== undefined ) {
writeStdout ( versionOutput );
2026-10-21 19:01:20 +08:00
} else {
2026-10-23 21:06:03 +05:00
const cliRuntime = await import ( "../dist/cli-runtime/cli.js" );
closeCliJavaScriptResources = cliRuntime . closeCliJavaScriptResources ;
const { createCli } = cliRuntime ;
const { createCliResultOutput } =
await import ( "../dist/cli-runtime/cliResultOutput.js" );
2026-10-21 19:01:20 +08:00
let wroteOutput = false ;
2026-10-24 00:30:07 +08:00
const resultOutput = createCliResultOutput ( outputOptions , process . stdout );
2026-10-21 19:01:20 +08:00
try {
await createCli ( process . env , resultOutput ). serve ( cliArgs , {
stdout : ( output ) => {
if ( resultOutput ? . handled ) {
if ( resultOutput . failed && ! stdoutClosed )
process . stderr . write ( output );
return ;
}
if ( output . length > 0 ) wroteOutput = true ;
2026-10-23 23:43:28 +05:30
if ( resultOutput ? . captureSdkOutput ( output )) return ;
2026-10-21 19:01:20 +08:00
writeStdout ( output );
},
// A command failure caused by the closed pipe keeps the pipe's status.
// Worker shutdown is shared with the dispatch finally below. A failed
// shutdown is reported here because this callback is what exits.
exit : ( code ) => {
exiting = true ;
2026-10-23 23:43:28 +05:30
void ( async () => {
let status = code ;
try {
await closeCliJavaScriptResources ();
} catch ( cleanup ) {
process . stderr . write (
`Failed to close the JavaScript analysis worker: ${ cleanupFailureText ( cleanup ) } \n` ,
);
if ( status === 0 ) status = 1 ;
}
try {
await resultOutput ? . finishSdkOutput ( code );
} catch ( cause ) {
if ( ! stdoutClosed )
process . stderr . write ( ` ${ cleanupFailureText ( cause ) } \n` );
if ( status === 0 ) status = 1 ;
}
process . exit ( stdoutClosed ? SIGPIPE_EXIT_STATUS : status );
})();
2026-10-21 19:01:20 +08:00
},
2026-10-18 03:55:35 +08:00
});
2026-10-21 19:01:20 +08:00
} catch ( cause ) {
if ( wroteOutput ) throw cause ;
const recovered = await resultOutput
? . recoverFormattingFailure ( cause )
. catch (( writeFailure ) => {
// Recovery writes outside Incur's command boundary. A reader that
// declined this document keeps the same quiet SIGPIPE outcome.
if ( ! stdoutClosed ) throw writeFailure ;
return true ;
});
if ( ! recovered ) throw cause ;
process . exitCode = stdoutClosed ? SIGPIPE_EXIT_STATUS : 1 ;
}
if ( ! wroteOutput && ! resultOutput ? . handled )
2026-10-24 00:30:07 +08:00
writeStdout ( renderEmptyFilteredCliOutput ( outputOptions ) ?? "" );
2026-10-23 23:43:28 +05:30
// An SDK failure exits through the callback, which owns its pending write.
if ( ! exiting )
await resultOutput ? . finishSdkOutput (
typeof process . exitCode === "number" ? process . exitCode : 0 ,
);
2026-10-21 19:01:20 +08:00
}
} catch ( cause ) {
2026-10-24 07:32:40 +08:00
if ( cause ? . code === "EPIPE" ) {
stdoutClosed = true ;
process . exitCode = SIGPIPE_EXIT_STATUS ;
}
2026-10-21 19:01:20 +08:00
dispatchFailure = cause ;
} finally {
// Serve and output rendering can throw after a worker is parked. Stop it
// on that path too, and keep the dispatch failure when both fail.
try {
2026-10-23 21:06:03 +05:00
await closeCliJavaScriptResources ? .();
2026-10-21 19:01:20 +08:00
} catch ( cleanup ) {
// The exit callback reports this same shared failure and then exits.
if ( ! exiting ) {
dispatchFailure =
dispatchFailure === undefined
? cleanup
: new AggregateError (
[ dispatchFailure , cleanup ],
"CLI dispatch failed and the JavaScript analysis worker remained owned" ,
);
}
2026-10-18 03:55:35 +08:00
}
2026-07-22 20:38:26 +08:00
}
2026-10-24 07:32:40 +08:00
if (
! exiting &&
dispatchFailure !== undefined &&
! ( stdoutClosed && dispatchFailure ? . code === "EPIPE" )
)
throw dispatchFailure ;
2026-07-12 07:44:21 +08:00
}
2026-07-15 14:44:04 +00:00
2026-10-14 18:20:19 -07:00
/**
* A reader that closes the pipe early, as `rea ... | head` does, has declined
* the rest of the output. Stop writing without reporting a failure, and exit
* with the status a SIGPIPE-terminated writer reports. MCP stdio keeps its own
* client lifecycle.
*/
2026-10-21 19:01:20 +08:00
function cleanupFailureText ( cleanup ) {
if ( cleanup instanceof Error ) return cleanup . stack ?? cleanup . message ;
return String ( cleanup );
}
2026-10-14 18:20:19 -07:00
function guardClosedStdout () {
process . stdout . on ( "error" , ( cause ) => {
if ( cause ? . code === "EPIPE" ) {
stdoutClosed = true ;
process . exitCode = SIGPIPE_EXIT_STATUS ;
return ;
}
// Other failures stay unhandled unless a pending JSON write reports them.
if ( process . stdout . listenerCount ( "error" ) === 1 ) throw cause ;
});
}
function writeStdout ( output ) {
if ( ! stdoutClosed ) process . stdout . write ( output );
}
2026-07-15 14:44:04 +00:00
async function compiledRuntimeExists ( paths ) {
for ( const path of paths ) {
try {
await access ( resolve ( packageRoot , path ));
} catch ( cause ) {
if ( isMissing ( cause )) return false ;
throw cause ;
}
}
return true ;
}
function isMissing ( cause ) {
return (
typeof cause === "object" &&
cause !== null &&
"code" in cause &&
cause . code === "ENOENT"
);
}