npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@magnaboy/cli-core

v0.0.7

Published

Reusable Node.js primitives for building robust repository scripts and CLIs.

Readme

@magnaboy/cli-core

Reusable, shell-free Node.js primitives for repository scripts and command-line tools.

Install

npm i @magnaboy/cli-core

Requires Node 25+ and ESM.

Run commands

import { NativeProcessRunner } from '@magnaboy/cli-core';

const runner = new NativeProcessRunner();
const result = await runner.run({
	command: process.execPath,
	args: ['--version'],
	output: 'tee',
	timeoutMs: 30_000
});

if (!result.ok) throw result.spawnError ?? new Error(`Command exited with ${result.exitCode}`);

Commands are executed without a shell. The process runner supports captured or teed output, combined log files, stdin, timeouts, cancellation, injected streams and environments, and descendant-process termination.

Input and bounded output

const result = await runner.run({
	command: 'build-tool',
	args: ['build'],
	stdinMode: 'inherit',
	output: 'tee',
	captureLimitBytes: 2 * 1024 * 1024,
	logPath: 'logs/build.log',
	logRotation: { maxBytes: 4 * 1024 * 1024, maxFiles: 3 }
});

stdin accepts literal strings, byte arrays, and Node readable streams. Streams are consumed by the command; do not reuse them afterward. Set stdinMode: 'inherit' to inherit the caller's stdin descriptor, or 'ignore' to provide no input. stdin and stdinMode are mutually exclusive. The string stdin: 'inherit' remains literal input. Inherited stdin does not enable a PTY or change stdout/stderr into terminal descriptors.

When output is retained, captureLimitBytes keeps only the final bytes of stdout, stderr, and their combined output. Each has an independent ring buffer of that size. The child continues running when a buffer fills; live output and log files are unaffected. The returned UTF-8 strings fit the limit and drop a character cut at the start of a tail. outputTruncated reports truncation separately for all three fields. Combined output follows observed stream delivery, decoding partial UTF-8 characters independently per stream. It cannot establish a total ordering between simultaneous stdout and stderr writes.

The limit must be a positive safe integer. It does not enable retention: use retainOutput: true with start if you need tails. Omitting the limit preserves the existing capture behavior and Execa buffer limit.

logStream selects 'stdout', 'stderr', or 'combined' (default). logRotation requires logPath. maxBytes and maxFiles are positive safe integers; maxFiles includes the active log and defaults to two. Logs rotate as build.log, build.log.1, through build.log.N; .1 is the newest backup. Large chunks are split to keep each newly written file within maxBytes. Rotation boundaries are bytes, not lines or UTF-8 characters. An append run rejects an active log already larger than the limit.

Log files must be regular files, and the caller must own the basename and configured backup names exclusively. Rotation replaces those backup files; it does not prune names outside the configured range. Changing a file count does not delete older backups outside that range. The runner applies backpressure and waits for file writes before resolving completion. File failures prevent a successful result.

await runner.run({
	command: 'analyzer',
	args: ['input.bin'],
	stdoutFile: 'reports/analysis.json',
	logPath: 'reports/diagnostics.log',
	logStream: 'stderr',
	retainOutput: false
});

stdoutFile opens with exclusive creation before spawning. An existing file or symlink causes rejection without running the command. It receives raw stdout bytes only, and retains partial output if the command fails. It can coexist with capture, tee output, and a separate log file. Output paths resolve against the command's cwd, their parent directories are created, and stdoutFile and logPath must differ.

Linux process groups

const gate = await runner.run({
	command: 'test-gate',
	args: ['--check'],
	cleanupProcessGroup: true,
	killSignal: 'SIGINT',
	killGraceMs: 3_000,
	timeoutMs: 600_000
});

killSignal selects the first signal for cancellation and timeouts (default SIGTERM). Windows uses Execa's process-tree termination and cannot provide POSIX signal semantics.

cleanupProcessGroup: true requires Linux with readable /proc; other platforms reject it before spawning. After the direct child exits, including a successful exit, the runner checks its owned process group for live members. It sends killSignal, then SIGTERM if different, then SIGKILL. Each graceful stage waits up to killGraceMs; after SIGKILL it allows five seconds to verify termination. Zombies are not running processes; the runner cannot reap grandchildren owned by another parent. Completion waits for this cleanup, even when all output pipes have closed. Failed verification sets cleanupError, ok: false, and a nonzero exit code. runChecked always rejects cleanup failures, regardless of allowedExitCodes.

This option does not contain descendants that create another process group/session. It also does not provide parent-death signaling after a hard kill of the runner, an OS advisory lock, or a security boundary. Those require native supervision. Without this option, existing cancellation and descendant handling remain.

Script entry points

import { isMainModule, NativeProcessRunner, runScript } from '@magnaboy/cli-core';

if (isMainModule(import.meta.url)) {
	await runScript(async signal => {
		const runner = new NativeProcessRunner({ signal });
		const result = await runner.run({ command: 'node', args: ['scripts/check.mjs'], output: 'tee' });
		return result.ok ? 0 : 1;
	});
}

The package also exports bounded parallel workflows, typed CLI errors, safe path helpers, timeout-aware fetch helpers, terminal prompting, and Windows/WSL host interoperability helpers. It contains no repository-specific root discovery, commands, or configuration.

Verified file downloads

import { downloadFile } from '@magnaboy/cli-core';

const file = await downloadFile(sourceUrl, 'downloads/tool.tar.gz', {
	sha256: expectedSha256,
	maxBytes: 100 * 1024 * 1024,
	timeoutMs: 60_000,
	signal
});
console.log(file.path, file.bytes, file.sha256);

downloadFile streams a response into a private temporary sibling directory and checks its SHA-256 before publishing it. sha256 is required and accepts either hex case. maxBytes is a required non-negative safe integer; zero allows an empty file. The helper checks both the declared Content-Length and the actual streamed bytes. The timeout defaults to 30 seconds and covers fetching and consuming the body; it checks cancellation again before publication. headers and an injectable fetch are supported. An injected fetch must honor the supplied signal, including while reading its response body. Failed HTTP statuses are rejected, not retried.

The destination is exclusive: it never replaces an existing path, even if another caller creates that path during the download. An atomic hard link publishes verified bytes without a partially written destination. The destination filesystem must support hard links; no non-atomic copy fallback is used. The parent directory is created as needed. Failures remove temporary output and preserve existing destinations. A hard process kill may leave a temporary sibling directory. The returned result contains the absolute path, byte count, and lowercase SHA-256. This provides atomic visibility, not a durability guarantee across host power loss.

Checked commands and background processes

import { commandOutput, NativeProcessRunner, runChecked } from '@magnaboy/cli-core';

const runner = new NativeProcessRunner({ cwd: process.cwd() });
const commit = await commandOutput(runner, { command: 'git', args: ['rev-parse', 'HEAD'] });
const diff = await runChecked(runner, {
	command: 'git', args: ['diff', '--quiet'], allowedExitCodes: [0, 1]
});

const worker = await runner.start({
	command: process.execPath,
	args: ['worker.mjs'],
	logPath: 'logs/worker.log',
	killGraceMs: 5_000
});
try {
	// Run the work that needs this process. worker.exited and worker.completion expose its state.
} finally {
	await worker.stop();
}

runChecked returns CommandResult or throws CommandError, which keeps the result on .result. Only normal exits can match allowedExitCodes (default [0]). Cancellation, timeouts, signals, and spawn errors always fail. Error messages omit arguments, environment values, and captured output; inspect the result explicitly when safe. commandOutput captures and trims stdout.

start returns a ManagedProcess before completion. stop() cancels the process tree and waits for exit, with forced termination after killGraceMs (default 30 seconds). Repeated stops share the completion result. Spawn failures resolve completion with spawnError; invalid launch options can reject start itself. This API supervises processes tied to the caller's lifetime, not detached GUI applications. start defaults to no retained output; run keeps its existing capture default. Both support log files and injected cancellation signals. Native results include cancelled; the optional field preserves compatibility with existing injected ProcessRunner implementations. ManagedProcessRunner adds the typed start method.

runScript also accepts an async cleanup callback and awaits it. Cleanup failures are reported, preserve an existing nonzero exit code, and otherwise set exit code 3. Signal handlers are removed even if cleanup fails.

Environment files and file hashes

import { readEnvFile, sha256File } from '@magnaboy/cli-core';

const environment = { ...readEnvFile('build.env'), ...process.env };
const digest = await sha256File('build/archive.zip');

readEnvFile is synchronous and uses Node's util.parseEnv format: comments, optional export, quoted values, and multiline values. It does not mutate process.env. Missing files return {}; other read errors propagate. It does not JSON-decode quoted values or expand variables. Callers migrating a custom parser must check those differences. Environment precedence is the caller's choice.

sha256File streams bytes with bounded memory and returns a lowercase hex digest. An optional { signal } cancels the read. Missing or unreadable files fail.

Temporary directories and ZIP archives

import { compressDirectory, createStagingDirectory } from '@magnaboy/cli-core';

const staging = createStagingDirectory('build-');
try {
	await compressDirectory('build/output', `${staging.path}/output.zip`, {
		onProgress: ({ completedFiles, totalFiles }) => console.log(`${completedFiles}/${totalFiles}`)
	});
} finally {
	staging.cleanup();
}

createStagingDirectory(prefix, options?) creates a unique directory under the OS temp directory or an existing parentDirectory. Prefixes cannot contain path separators. Cleanup removes only this owned directory, is idempotent, retries locked Windows files, and returns false rather than throwing on failure. A later cleanup or process exit retries failed removal. onCleanupError(error, directory) overrides the default warning.

A shared exit handler removes active directories. By default, shared SIGINT, SIGTERM, SIGHUP, and Windows SIGBREAK handlers clean up and exit with the signal's conventional code. Set cleanupOnSignals: false when runScript or another caller owns signal handling; perform cleanup in that caller's finally/cleanup callback. The synchronous process-exit backstop remains enabled. Hard kills cannot run cleanup.

compressDirectory(source, destination, options?) is async and uses yazl to stream one input file at a time. It fixes entry timestamps and modes, sorts paths without locale dependence, and supports ZIP64 automatically. compressionLevel is 0 through 9, default 1. Progress starts at 0 and advances as each file is read. Archive completion is the returned promise, not the last progress event. signal cancels archive creation.

The destination must be outside the source directory, including directory aliases. Symbolic links and special files in the source are rejected; empty directories are omitted. Inputs must remain unchanged during creation. Output is written to a temporary sibling directory and replaces the destination only after success. A failure or cancellation removes temporary output and preserves the previous archive. The helper creates destination parent directories as needed. Stable metadata yields repeatable archives for the same inputs and toolchain; compression-library updates may change bytes. Archives are not byte-compatible with other ZIP writers.

Retry, polling, and heartbeat

import { pollUntil, retry, retryTransfer, withHeartbeat } from '@magnaboy/cli-core';

const result = await retry(async (attempt, signal) => performOperation(signal), {
	attempts: 3,
	delayMs: attempt => attempt * 500,
	shouldRetry: error => isTransient(error)
});

await pollUntil(async signal => fetch('http://localhost:8080/healthz', { signal }).then(r => r.ok), {
	accept: healthy => healthy,
	timeoutMs: 30_000,
	intervalMs: 500
});

await withHeartbeat(async signal => performOperation(signal), {
	onHeartbeat: elapsedMs => console.log(`Working: ${Math.round(elapsedMs / 1000)}s`)
});

retry uses one-based attempts, three attempts by default, and exponential delays starting at 500 ms and capped at 30 seconds. The final error is preserved. Use shouldRetry to exclude permanent failures; cancellation and AbortError are never retried. All three helpers accept an external signal.

pollUntil polls values until accept succeeds. Probe errors propagate. Its total timeout covers pending probes and intervals and rejects with TimeoutError; timers and listeners are removed on completion. Actions must honor their signal to stop underlying work after cancellation.

stableForMs requires accept to hold continuously for that long before returning, and a single rejected probe restarts the window. Use it when readiness must be stable rather than momentary, such as a service that answers once and then restarts.

withHeartbeat reports immediately and every 10 seconds by default (intervalMs overrides this). It returns the action result and always clears its timer. A reporter failure rejects the operation and signals cancellation.

retryTransfer takes a fresh request factory (attempt, signal) => Promise<Response> plus RetryOptions. It retries HTTP 408, 429, 500, 502, 503, and 504, disposing each discarded response body. It returns the final response even if that response has an error status. Other HTTP statuses return immediately. shouldRetry controls thrown errors; retryable HTTP statuses use the fixed policy. Create a new stream body inside the factory for each upload attempt. Choose a per-request timeout and use this only for operations that can safely repeat.

File locks, generated files, and Git fingerprints

acquireFileLock(path, metadata?) creates an exclusive local filesystem lock and returns { path, owner, release() }. Use release() in a finally block. Release is idempotent and checks the ownership token. Existing locks throw FileLockError, including locks with stale or incomplete metadata. Recovery is explicit: verify no owner or contender is active before removing a leftover lock. There is no PID-based automatic recovery, signal handler, lease, or network filesystem guarantee. Do not remove or replace an active lock outside this API.

writeOrCheckFile(path, content, { check }) returns written, current, or stale. Check mode compares exact bytes and performs no writes or directory creation. Missing files are stale; other I/O errors propagate. Write mode creates parents and writes the supplied string or bytes. Callers own logging and exit codes. Writes are not transactional.

gitSourceFingerprint({ cwd, paths, marker, signal, runner }) hashes selected Git filenames and streamed content. Paths must be nonempty and are Git pathspecs. It includes tracked and untracked non-ignored files, skips deleted files, deduplicates and sorts names, and uses NUL-delimited Git output. The digest includes the marker, filename and binary SHA-256 of each file. Symlinks follow Node file-reading behavior; keep inputs stable while hashing. The helper does not infer artifact policy, toolchain markers or secret exclusions. Select input paths explicitly.

Command-line parsing

Argument parsing itself is Commander, which is an optional peer dependency: install it only if you import @magnaboy/cli-core/commander. Pair it with @commander-js/extra-typings for inferred option types.

import { Command, Option } from '@commander-js/extra-typings';
import { envFlag, integerParser, runCommand } from '@magnaboy/cli-core/commander';

const command = new Command('capture')
	.argument('[seconds]', 'measurement window', integerParser(1, 3_600), 30)
	.addOption(new Option('--freq <hz>').env('FREQ').default(4_000).argParser(integerParser(1, 100_000)))
	.addOption(envFlag('heap', 'HEAP', { defaultValue: true, description: 'java heap snapshot' }))
	.action(async (seconds, options) => capture(seconds, options));

await runScript(() => runCommand(command));

runCommand exists because Commander calls process.exit by default, which skips runScript's cleanup callback and leaves its signal handlers installed. It applies exitOverride across the whole command tree, returns 0 when Commander has already printed help or a version, and turns every other parse failure into a CliError. Commander's own error output is suppressed so the message is printed once, by runScript, together with any usageHint. exitOverride is inherited by subcommands but output configuration is not, so runCommand walks them.

envFlag(name, environmentVariable) is a boolean option that reads its environment variable correctly. Commander treats a plain boolean option's variable as set-by-presence, so HEAP=0 turns the flag on; this declares an optional-value option instead, where --heap alone is true and both --heap 0 and HEAP=0 are false. Precedence is command line, then environment, then default, and getOptionValueSource reports which applied.

integerParser, numberParser and listParser build argParser functions that report failures as Commander usage errors. listParser splits, trims, and rejects blanks and repeats.

Generated text, sections and tables

import { formatKeyValues, parseKeyValues, renderTable, splitSections, toLf } from '@magnaboy/cli-core';

const device = parseKeyValues(await readFile('device.txt', 'utf8'), { allowedKeys: ['serial', 'label'] });
const sections = splitSections(await readFile('before.txt', 'utf8'));
console.log(renderTable(threads, [
	{ header: 'thread', value: row => row.comm },
	{ header: 'cpu%', align: 'right', value: row => row.corePercent.toFixed(1) }
]));

toLf drops carriage returns so Windows tool output compares equal to the POSIX form.

splitSections reads ### name blocks into a Map in file order. Content before the first marker is rejected rather than dropped, because a tool that printed a warning ahead of the first section would otherwise lose it silently. Duplicate and unnamed sections are errors.

parseKeyValues reads key=value lines and returns a KeyValues with accessors that name the key that failed: require, number, integer, boolean, optionalNumber. Only the first = splits, so a value may contain one. allowedKeys rejects anything unexpected and duplicates are rejected unless allowDuplicates is set. formatKeyValues writes what parseKeyValues reads back, and rejects keys or values that would not survive the round trip. Failures throw TextParseError.

renderTable sizes columns from the widest cell, or to an explicit width that truncates. Cells are formatted by the caller, so numeric precision stays with the code that knows the units; null and undefined render as the column's empty placeholder. With no rows it returns emptyMessage rather than a bare header.

formatBytes, formatDuration, formatPercent and formatCount cover the usual report units. formatPercent takes a ratio in 0..1, not a percentage; formatCount groups digits without depending on the host locale.

Run directories

import { createRunDirectory, timestampSlug, writeNewFile } from '@magnaboy/cli-core';

const run = await createRunDirectory({ root: 'out', tag: 'full', utc: true });
writeNewFile(`${run.path}/summary.json`, JSON.stringify(summary));

timestampSlug returns a sortable 20260917-143005, or 20260917T143005Z with utc. createRunDirectory names a directory tag-stamp and is exclusive by default: reusing a directory silently mixes two runs' artifacts and the reader cannot tell afterwards which file came from which window. writeNewFile applies the same rule to a single file, creating parent directories and failing with EEXIST rather than replacing.