Everything that you need to create CLI easy! TypeScript or JavaScript → single native binary. No Node.js, no node_modules, no build step.
curl -fsSL https://raw.githubusercontent.com/snatvb/imp/main/scripts/install.sh | bashThis will:
- Auto-detect your OS and architecture
- Download the latest release to
~/.local/bin/imp - Add
~/.local/binto your PATH if needed
curl -fsSL https://raw.githubusercontent.com/snatvb/imp/main/scripts/install.sh | bash -s -- v0.1.0brew tap snatvb/brew git@github.com:snatvb/brew.git
brew install snatvb/brew/impscoop bucket add imp https://github.com/snatvb/scoop-imp
scoop install impcargo install imp-cliDownload the latest binary from GitHub Releases.
You write a CLI tool in TypeScript. Today you ship a 200MB node_modules
folder and hope the user has the right Node version. With imp you ship
one ~11MB binary that just runs.
| Node.js | ImpJS | |
|---|---|---|
| Distribution | needs Node + deps | one binary (.exe or no ext) |
| Cold start | ~80ms | <5ms |
| TypeScript | needs build | native (oxc at parse) |
| Bin size | ~100MB | ~11MB |
.tsruns directly, no transpilerimp:fs—readFile,writeFile,walk,glob,FileHandle,WriteHandleimp:parsers— JSON, YAML, TOML, XML, RON, CSV, MessagePack in one moduleimp:crypto—randomBytes,randomHex,randomUUID,randomInt,hmac,aesEncrypt,aesDecrypt,timingSafeEqualimp:encoding—base64,hex,utf8,uriencode/decodeimp:hash—md5,sha1,sha256,sha512,blake3imp:env—parseIni,parseDotenv,expand,merge,loadFileimp:inq—confirm,select,prompt,multiSelect,password,editorimp:subprocess—run(cmd, args, options)withcwd/env/input/timeout/signal/encodingimp:signal— OS signal handlers (SIGINT, SIGTERM, SIGHUP, SIGQUIT, SIGBREAK)imp:time—Duration,ImpDate,ImpTime,ImpDateTime,ImpLocalDateTimeimp:clap— CLI argument parsing withParserimp:sys—inputSimulate,stdinfor system-level I/ODuration,ByteBuffer,RsString— Rust-backed primitives
# scaffold TypeScript types (optional — for IDE support)
imp init
# run a script
imp run hello.ts
imp run path/to/script.ts arg1 arg2// hello.ts
import { readFile, writeFile } from "imp:fs"
import { json, yaml } from "imp:parsers"
const raw = await readFile("config.json", "utf8")
const data = json.parse(raw)
await writeFile("config.yaml", yaml.stringify(data))
console.log("Converted JSON → YAML")The whole point. One file, no runtime, no installer on the target.
# Windows
imp compile hello.ts hello.exe
.\hello.exe
# macOS / Linux
imp compile hello.ts hello
./helloOn Windows you can omit .exe and it's added automatically. On macOS / Linux
the output keeps the exact name you pass.
The result is a single ~11MB binary containing the QuickJS runtime and
your bundled script. No Node, no node_modules, no shared libs. Copy it
to another machine, run it.
import { readFile, writeFile, walk, glob, open, openWrite } from "imp:fs"
const text = await readFile("data.txt", "utf8")
await writeFile("out.txt", "result")
for await (const entry of walk("./src")) {
console.log(entry.path)
}
const files = await glob("./src", "**/*.ts")
for (const file of files) {
console.log(file)
}
// Streaming with FileHandle — using auto-closes via Symbol.dispose
using fh = await open("large.bin", 4096)
const chunk = await fh.read()
// WriteHandle
using wh = await openWrite("output.txt", "w")
await wh.write("hello")import { json, yaml, toml, csv, xml } from "imp:parsers"
const obj = json.parse('{"a": 1}')
const str = yaml.stringify({ key: "value" })
const cfg = toml.parse("[server]\nport = 8080")
const rows = csv.parse("name,age\nAlice,30")import { randomBytes, randomUUID, hmac, aesEncrypt, aesDecrypt } from "imp:crypto"
const bytes = randomBytes(32)
const id = randomUUID()
const sig = hmac("sha256", "secret", "message")
const key = randomBytes(32)
const iv = randomBytes(12)
const encrypted = aesEncrypt("aes-256-gcm", key, plaintext)
const decrypted = aesDecrypt("aes-256-gcm", key, encrypted)import { base64, hex, utf8, uri } from "imp:encoding"
const encoded = base64.encode("hello")
const decoded = base64.decode(encoded)
const hexStr = hex.encode(bytes)
const buf = utf8.encode("text")
const safe = uri.encode("path/to/file")import { sha256, blake3 } from "imp:hash"
const hash = sha256("hello world")
const h3 = blake3("data", "hex")import { parseIni, parseDotenv, expand, loadFile } from "imp:env"
const ini = parseIni("[db]\nhost = localhost\nport = 5432")
const env = parseDotenv("FOO=bar\nBAZ=qux")
const expanded = expand("HOME=$HOME/user")
const config = await loadFile(".env")import { prompt, select, confirm, multiSelect } from "imp:inq"
const name = await prompt("What is your name?")
const choice = await select("Pick one", ["a", "b", "c"])
const ok = await confirm("Continue?", true)
const picks = await multiSelect("Pick many", ["x", "y", "z"])import { run } from "imp:subprocess"
const result = await run("git", ["status"])
console.log(result.stdout, result.code)
const r2 = await run("echo", ["hello"], { timeout: 5000 })import { signal } from "imp:signal"
const dispose = signal.on("SIGINT", () => {
console.log("Interrupted!")
process.exit(0)
})
signal.once("SIGTERM", () => cleanup())import { Duration, ImpDate, ImpDateTime } from "imp:time"
const d = Duration.seconds(30)
const today = ImpDate.today()
const now = ImpDateTime.now()
const fmt = now.format("%Y-%m-%d %H:%M:%S")No imports needed — available everywhere:
| Global | Description |
|---|---|
console |
log, error, warn, info, assert, trace |
process |
cwd(), exit(), env, argv, pid, platform |
path |
resolve, join, basename, dirname, extname, relative, normalize |
fetch |
HTTP client (Web standard fetch, Request, Response, Headers) |
URL, URLSearchParams |
URL parsing and manipulation |
AbortController, AbortSignal |
Request cancellation |
setTimeout, setInterval, setImmediate |
Async timers |
TextEncoder, TextDecoder |
UTF-8 encode/decode |
Buffer |
Byte buffer (Node.js compatible subset) |
import clap from "imp:clap"
const parser = new clap.Parser()
.name("hello")
.arg({ name: "name", long: "name", action: "set", help: "who to greet" })
.arg({ name: "count", short: "c", long: "count", action: "set", help: "how many times" })
const result = parser.parse(clap.args)
if (String(result.type) === "result") {
const name = String(result.name ?? "world")
const count = Number(result.count ?? 1)
for (let i = 0; i < count; i++) {
console.log(`Hello, ${name}!`)
}
}$ imp run hello.ts --name Alice --count 3
Hello, Alice!
Hello, Alice!
Hello, Alice!
$ imp run hello.ts --help
Usage: hello [OPTIONS] [NAME]imp <file> # run a script
imp run <file> [args...] # same, explicit
imp compile <file> <output> # bundle to native binary
imp init [path] # scaffold imp.d.ts + tsconfig.jsonSee examples/ — 7 real-world scripts: HTTP client,
config format converter, parallel CSV stats, markdown renderer, interactive
scaffolder, tail -f clone, and a concurrent file sorter.
Cross-compile Windows and Linux binaries from macOS:
# Install cross-compilation toolchains
brew install mingw-w64 # Windows x64 linker
brew install messense/macos-cross-toolchains # Linux x86_64 linker
cargo install cargo-zigbuild && brew install zig # Linux ARM64
# Build all targets
cargo build --release -p cli # macOS arm64
cargo build --release --target x86_64-pc-windows-gnu -p cli # Windows x64
cargo build --release --target x86_64-unknown-linux-gnu -p cli # Linux x86_64
cargo zigbuild --release --target aarch64-unknown-linux-gnu -p cli # Linux ARM64Output binaries:
| Target | Path | Size |
|---|---|---|
| macOS arm64 | target/release/imp |
~11 MB |
| Windows x64 | target/x86_64-pc-windows-gnu/release/imp.exe |
~9.6 MB |
| Linux x86_64 | target/x86_64-unknown-linux-gnu/release/imp |
~10 MB |
| Linux ARM64 | target/aarch64-unknown-linux-gnu/release/imp |
~8.7 MB |
All binaries are self-contained — no system OpenSSL/libssl required. Only glibc and ca-certificates needed on target Linux machines.