symlx
v0.1.13
Published
Temporary local CLI bin linker
Downloads
737
Maintainers
Readme
tl:dr
In a CLI project with:
{
"name": "awesome-cli",
"bin": {
"awesome-cli": "./dist/cli.js"
}
}run:
symlx serveThen use your CLI normally:
awesome-cli --helpUse symlx link when you want one-shot linking without keeping a live file-watcher session open.
symlx
Temporary command linker for local CLI development.
symlx serve links command names from your project into a runnable bin directory for the lifetime of the process.
When symlx stops, those links are cleaned up.
symlx link creates the same links once and exits immediately.
Why symlx
During CLI development, running node dist/cli.js repeatedly is noisy.
npm link has generally been buggy and slow to pick recent code changes.
symlx gives you the real command experience (my-cli --help) without a global publish/install cycle.
Core guarantees:
- Links are session-scoped and cleaned on exit.
- Collision behavior is explicit (
prompt,skip,fail,overwrite). - Option resolution is deterministic.
- Target execution is hybrid by default:
- shebang present -> direct link
- no shebang -> launcher inference by target type
- PATH setup for
~/.symlx/binis automated on install (with opt-out).
Install
npx symlx serve
# or
npm i -g symlxAlias
symlx can be clackful for power users, hence its alias: cx.
Equivalent commands:
symlx serve
symlx link
cx serve
cx linkCommand Reference
symlx serve
Links commands from resolved bin mappings and keeps the process alive until interrupted.
Options
| Option | Type | Default | Description |
| -------------------------------------- | ------------------------------------- | -------------- | --------------------------------------------------------------------- |
| --bin-dir <dir> | string | ~/.symlx/bin | Target directory where command links are created. |
| --collision <policy> | prompt \| skip \| fail \| overwrite | prompt | What to do when a command name already exists in bin dir. |
| --bin-resolution-strategy <strategy> | replace \| merge | replace | How to resolve bin across package.json, config, and inline flags. |
| --non-interactive | boolean | false | Disable prompts and force non-interactive behavior. |
| --bin <name=path> (repeatable) | string[] | [] | Inline bin mapping (for quick overrides/ad-hoc runs). |
Examples:
symlx serve --collision overwrite
symlx serve --bin admin=dist/admin.js --bin worker=dist/worker.js
symlx serve --bin-resolution-strategy mergesymlx link
Links commands from resolved bin mappings and exits immediately.
It uses the exact same options and resolution behavior as symlx serve, but it does not keep a live session.
Examples:
symlx link
symlx link --collision overwrite
symlx link --bin admin=dist/admin.jsBin Resolution Model
symlx resolves options from three user sources plus defaults:
package.jsonsymlx.config.json- inline CLI flags
Scalar fields (collision, binDir, nonInteractive, binResolutionStrategy) follow normal override order:
defaults -> package.json-derived -> config -> inline
bin uses strategy mode:
replace(default): first non-empty wins by priorityinline > config > package.json > defaultmerge: combines allpackage.json + config + inline(right-most source overrides key collisions)
Supported Bin Sources
package.json
bin supports both npm-compatible linking:
{
"name": "my-cli",
"bin": "./dist/cli.js"
}{
"bin": {
"my-cli": "./dist/cli.js",
"my-admin": "./dist/admin.js"
}
}If bin is a string, name is required so command name can be inferred.
symlx.config.json
{
"binDir": "~/.symlx/bin",
"collision": "prompt",
"nonInteractive": false,
"binResolutionStrategy": "replace",
"bin": {
"my-cli": "./dist/cli.js"
}
}Notes:
- In case of invalid non-critical config values,
symlxfalls back to defaults (with warnings). binDiris treated as critical and must pass validation.
Inline Flags
symlx serve --bin my-cli=dist/cli.js
# multiple inline bins
symlx serve \
--bin xin-ping=./cli.js \
--bin admin=./scripts/admin.jsname rules:
- lowercase letters, digits,
- - no spaces
path rules:
- must be relative (for example
dist/cli.jsor./dist/cli.js) - absolute paths are rejected
Target Execution Model (Hybrid by Default)
For each resolved target file:
- if target has a shebang, symlx links it directly
- if target has no shebang, symlx infers launcher by file type
Current launcher inference:
.js,.mjs,.cjs-> Node launcher.ts,.tsx,.mts,.cts->tsxlauncher- if a TypeScript target declares
#!/usr/bin/env node, symlx fails early and tells you to usetsxshebang or remove shebang for launcher inference
TypeScript runtime resolution order is:
- project-local
node_modules/.bin/tsx tsxonPATH
If target has no shebang and launcher support is unavailable, symlx fails with a clear message that this is not supported yet without shebang and asks you to manually add shebang.
Collision Policies
prompt: ask per conflict (interactive TTY only)skip: keep existing command, skip linkfail: stop on first conflictoverwrite: replace existing entry
If prompt is requested in non-interactive mode, symlx falls back to skip and warns.
Install-Time PATH Setup
On install, symlx updates shell profile PATH block.
Managed path:
$HOME/.symlx/binOpt out:
SYMLX_SKIP_PATH_SETUP=1 npm i -g symlxTo set a custom bin directory:
symlx serve --bin-dir ~/.symlx/binRuntime Safety Checks
Before linking, symlx prepares each resolved bin target:
- file exists
- target is not a directory
- shebang path: direct link + executable permission repair when possible
- no-shebang path: launcher inference + runtime availability checks
Missing targets, directories, unsupported no-shebang target types, missing launcher runtimes, and permission-update failures fail early with actionable messages.
Exit Behavior
Ctrl+C(SIGINT), SIGTERM, SIGHUP, uncaught exception, and unhandled rejection trigger cleanup.- Session metadata is stored under
~/.symlx/sessions. - Stale sessions leftover due to hard crashes are cleaned on startup.
Troubleshooting
"not supported yet without shebang"
- add a shebang to the target file to declare its runner explicitly
"no bin entries found"
Add a bin mapping in at least one place:
package.json -> binsymlx.config.json -> bin--bin name=path
"command conflicts at ..."
Use a collision mode:
symlx serve --collision overwrite
# or
symlx serve --collision fail"tsx runtime could not be resolved for target"
Install tsx in the project or make tsx available on PATH.
"typescript target uses node shebang and is not directly runnable"
- replace shebang with
#!/usr/bin/env tsx - or remove shebang and let symlx infer launcher by file type
"package.json not found"
Run in your project root, or pass bins inline/config.
Development
pnpm install
pnpm run check
pnpm run build
pnpm run testExtending Commands (Contributor Contract)
To add a new command while preserving set conventions:
- Define command surface in
src/cli.ts. - Keep orchestration in
src/commands/*. - Reuse
resolveOptions()for deterministic source handling. - Validate user-facing options with zod schemas in
src/lib/schema.ts. - Add behavior coverage in
test/*.test.ts. - Update this README command reference and examples.
The goal is consistent behavior across all current and future commands.
