@repo-toolkit/release-artifact
Assemble, verify, and distribute a self-contained CLI release artifact (tarball) from a monorepo.
release-artifact discovers packages under packages/*, generates bash wrappers
for each bin entry, copies the workspace node_modules (optional), writes an
artifact-manifest.json, and packages everything into a <toolName>-<version>.tar.gz.
Verification re-extracts the tarball and checks required files, symlink safety,
and that each wrapper boots (<wrapper> --help).
The asdf plugin in this repo consumes the resulting tarball directly, but the package is generic: any monorepo that wants to ship a bundled CLI artifact can use it.
Install
- npm
- Yarn
- pnpm
- Bun
npm install --save-dev @repo-toolkit/release-artifact
yarn add --dev @repo-toolkit/release-artifact
pnpm add --save-dev @repo-toolkit/release-artifact
bun add --dev @repo-toolkit/release-artifact
CLI
Build
repo-toolkit-build-artifact --version v1.2.3
Verify
repo-toolkit-verify-artifact --version v1.2.3
Flags
| Flag | Description | Default |
| -------------------------------- | ----------------------------------------------------------------------------------------- | ------------------ | --- |
| --config <path> | Config file (JSON, .mjs, or .cjs default export). CLI flags override config. | — |
| --cwd <path> | Workspace root directory | process.cwd() |
| --version <version> | Target version. A leading v is stripped. | — |
| --tag <version> | Alias for --version | — |
| --tool-name <name> | Tool name used in artifact filenames | repo-toolkit |
| --packages-dir <path> | Directory under workspace root holding packages (build only) | packages |
| --dist-dir <path> | Directory under workspace root where the tarball is written / located | dist |
| --version-files <f>[,<f>] | Root file(s) copied into artifact root, preserving subpath (build only) | ['VERSION'] |
| --root-files <f>[,<f>] | Additional root files copied into artifact root, preserving subpath (build only) | — |
| --node-modules <mode> | Resolved node-modules mode: production, copy, or none (build only) | production |
| --skip-node-modules | Compatibility alias for --node-modules none (build only) | — |
| --production-node-modules | Compatibility alias for --node-modules production (build only) | — |
| --no-production-node-modules | Compatibility alias for --node-modules copy | none (build only) | — |
| --node-command <name> | Node interpreter used in bash wrappers (build only) | node |
| --exclude <glob>[,<glob>] | Glob patterns excluded from each copied package (replaces defaults; build only) | see source |
| --run-timeout-ms <ms> | Per-process timeout for external commands | 60000 |
| --max-archive-member-count <n> | Maximum number of archive members before validation rejects the artifact (verify/install) | 20000 |
| --artifact-path <path> | Explicit tarball path; overrides cwd/tool-name/dist-dir (verify only) | — |
| --help-flag <flag> | Flag passed to each wrapper to confirm it boots (verify only) | --help |
| -h, --help | Show help | — |
JavaScript API
Build
import { buildReleaseArtifact } from '@repo-toolkit/release-artifact';
const plan = buildReleaseArtifact({
version: '1.2.3',
cwd: '/path/to/monorepo',
toolName: 'repo-toolkit',
nodeModulesMode: 'production',
rootFiles: ['LICENSE'],
});
console.log(plan.artifactPath);
Verify
import { verifyReleaseArtifact } from '@repo-toolkit/release-artifact';
verifyReleaseArtifact({
version: '1.2.3',
cwd: '/path/to/monorepo',
toolName: 'repo-toolkit',
});
Exports
buildReleaseArtifact(options)— assemble the artifact and write the tarball; returns the resolved plan.verifyReleaseArtifact(options)— extract the tarball and validate manifest, required files, symlink safety, and each wrapper's--help.resolveBuildArtifactPlan(options)— resolve the build plan without writing.resolveArtifactPath(options)— resolve the expected tarball path for a version.buildWrapperScript(targetPath, nodeCommand?)— generate a bash wrapper thatexecs the node interpreter.toBinEntries(binField, packageName)— normalize apackage.json#binfield into[name, entry]pairs.collectCommands(packagesRoot, packageDirNames)— readbinentries across packages.collectCommandPackageClosure(packagesRoot, packageDirNames, commands)— compute the transitive closure of command-owning packages.mergeClosureDependencies(packagesRoot, closurePackageDirs)— merge production deps of closure packages, rejecting incompatible range conflicts.intersectSemverRanges(ranges)— conservative npm range intersection (rejects when in doubt).resolveNodeModulesMode(mode, includeNodeModules, productionNodeModules)— resolve the legacy booleans + explicit mode to a singleNodeModulesMode.resolveRootFileDestination(value, label)— validate a root/version file's relative POSIX destination.buildRequiredFiles(commands, versionFiles)— compute the manifest'srequiredFileslist.createArtifactManifest(version, commands, requiredFiles)— assemble the manifest (commands sorted).verifySymlinks(rootPath, currentPath?)— throw on any absolute symlink.validateArtifactRunner(runner)— assert a value is a validArtifactRunner.defaultArtifactRunner— injectable runner that boundstar/pnpm/bash/wrapper execution with timeout and max-output limits.resolveRunTimeoutMs(value)— validate the per-process timeout override.resolveMaxArchiveMemberCount(value)— validate the max archive member count override.
Options
BuildArtifactOptions
version(string, required) Target version. A leadingvis stripped.cwd(string) Workspace root directory. Defaults toprocess.cwd().toolName(string) Tool name used in artifact directory and tarball filenames (default:repo-toolkit).versionFiles(string[]) Root file(s) copied into artifact root, preserving the configured subpath (default:['VERSION']). Missing files fail the build.rootFiles(string[]) Additional root files copied into artifact root, preserving the configured subpath. Missing files fail the build.packagesDir(string) Directory under workspace root holding packages (default:packages).distDir(string) Directory under workspace root where the tarball is written (default:dist).nodeModulesMode('production' | 'copy' | 'none') Resolved node-modules mode (default:production). ReplacesincludeNodeModules/productionNodeModules; passing both with conflicting values is rejected.includeNodeModules(boolean, deprecated) UsenodeModulesMode: 'copy'(true) ornodeModulesMode: 'none'(false).productionNodeModules(boolean, deprecated) UsenodeModulesMode: 'production'(true) ornodeModulesMode: 'copy'|'none'(false).nodeCommand(string) Node interpreter used in generated bash wrappers (default:node).excludes(string[]) Glob patterns excluded from each copied package directory. Replaces the defaults.runner(ArtifactRunner) Injectable subprocess runner (default:defaultArtifactRunner).runTimeoutMs(number) Per-process timeout for external commands (default: 60000).
VerifyArtifactOptions
version(string, required unlessartifactPathis set) Target version used to locate the tarball.cwd(string) Workspace root directory. Defaults toprocess.cwd().toolName(string) Tool name used to locate the tarball (default:repo-toolkit).distDir(string) Directory under workspace root holding the tarball (default:dist).artifactPath(string) Explicit tarball path; overridescwd/toolName/distDirresolution.helpFlag(string) Flag passed to each wrapper to confirm the command boots (default:--help).skipExec(boolean) Skip executing wrappers; only check manifest, required files, symlink safety, x_OK, andbash -n.runner(ArtifactRunner) Injectable subprocess runner (default:defaultArtifactRunner).runTimeoutMs(number) Per-process timeout for external commands (default: 60000).maxArchiveMemberCount(number) Maximum number of archive members before validation rejects the artifact (default: 20000). Must be a positive finite integer.
Injectable subprocess runner
buildReleaseArtifact, verifyReleaseArtifact, verifyExtractedArtifact, and
installReleaseArtifact accept an injectable ArtifactRunner so tests can
assert exact invocations without contacting a real toolchain. The default
runner (defaultArtifactRunner) spawns via execFileSync with:
timeoutMs(default 60s) — kills the child after the configured window viaSIGTERM.maxOutputBytes(default 8 MiB) — bounds the captured output ofrunner.capture().
run() surfaces a nonzero exit or timeout as a thrown Error; capture()
additionally rejects output larger than maxOutputBytes. Tests inject a fake
runner to assert exact tar/pnpm/bash/wrapper invocations offline.
Security note
verifyReleaseArtifact executes the artifact's bash wrappers, which in turn
exec the node interpreter against the artifact's own entry files. Only verify
artifacts you trust — verification is an integrity check, not a sandbox.