@type-x/runtime
v0.4.7
Published
Run commands with the same runtime context as @type-x/cli
Downloads
1,659
Readme
@type-x/runtime
@type-x/runtime lets you build a normal npm CLI while still getting the same runtime context used by x.
Main API
Use initCli in your entrypoint:
import { initCli, type CommandContext } from "@type-x/runtime";
type Store = {
runs: number;
};
async function main(context: CommandContext<Store>): Promise<void> {
const runs = (await context.store.get("runs")) ?? 0;
await context.store.set("runs", runs + 1);
const gitStatus = await context.exec("git status --short", {
throwOnError: false,
silent: true,
});
context.log.info(`command: ${context.command.name}`);
context.log.info(`runs: ${runs + 1}`);
context.log.info(`git status exit code: ${gitStatus.exitCode}`);
}
initCli(main);Context
The injected context exposes:
commandrequeststoreloguiexecgitioenv
request.flags contains parsed CLI flags. A flag used once is exposed as a
single string or boolean; a repeated flag is exposed as an array:
my-cli --param a --param b --verbosecontext.request.flags.param; // ["a", "b"]
context.request.flags.verbose; // trueThis is the default runtime.repeatedFlags: "array" behavior. To keep only the
last value for repeated flags, configure the runtime:
initCli(main, {
runtime: {
repeatedFlags: "last",
},
});Exec
context.exec() runs a command and returns:
exitCodestdoutstderr
Pass a string to run through the shell:
await context.exec("git status --short");Pass an argv tuple to run without shell interpolation:
await context.exec(["git", "checkout", branchName]);Options include:
mode"capture"is the default and buffers stdout/stderr while optionally streaming them."inherit"attaches the child process directly to the current terminal so interactive commands likesudo, editors, or prompts behave normally. In this mode,stdoutandstderrare returned as empty strings.silentWhenfalse, stream command output to the current process stdout/stderr. This is the default.throwOnErrorWhenfalse, return non-zero exit codes instead of throwing. By default it is true.timeoutMsStop the command after this many milliseconds. The process receivesSIGTERMfirst and escalates toSIGKILLif it does not exit. Timed-out commands use exit code124.
When throwOnError is left on and the command exits non-zero, exec() throws a CommandExecError from @type-x/runtime. The error message is a stable summary, and the raw process output is available on the error instance through stdout, stderr, exitCode, command, cwd, and mode. @type-x/runtime also exports isCommandExecError(error) for structural narrowing when you do not want to rely on instanceof.
Example for an interactive command:
await context.exec("sudo npm install -g some-tool", {
mode: "inherit",
});Example for handling command failures:
import { CommandExecError, initCli, isCommandExecError } from "@type-x/runtime";
try {
await context.exec("git push");
} catch (error) {
if (error instanceof CommandExecError) {
console.error(error.exitCode);
console.error(error.stderr);
}
if (isCommandExecError(error)) {
console.error(error.command);
console.error(error.stderr);
}
throw error;
}Store
context.store persists JSON state for the command package. Keys can be top-level
or dot paths:
await context.store.set("providers.github", "hello");
await context.store.get("providers.github"); // "hello"This writes:
{
"providers": {
"github": "hello"
}
}Failures
Use context.fail() for expected user-facing failures:
context.fail("GitHub token is required. Run: x auth github");This prints the message to stderr and sets exit code 1. You can choose a
different code:
context.fail("Invalid config", {
exitCode: 2,
});Unexpected bugs can still be thrown as normal errors; initCli catches them and
sets exit code 1.
Default Store Location
If you do not override runtime.homeDir, the runtime uses:
~/.type-x/<sanitized-package-name>
You can override that explicitly:
initCli(main, {
runtime: {
homeDir: "/some/custom/path",
},
});