Command line interface
The edge CLI runs on macOS, Linux, and WSL. It embeds the engine the JS host loads, a compiler.wasm built from the same source for speed and precompiled for the host machine, executed under wasmtime, so run, repl, test, and actor start in milliseconds with no browser and no server behind them. The system calls run in SpiderMonkey inside the binary, so a program answers the same on both hosts.
edge run app.py # run a script, a .edge, or stdin (--web)
edge build # pack a portable .edge (--app, --web)
edge actor actor.yml # run a pool of cooperative actors
edge serve # dev server with live reload
edge repl # interactive shell
edge test # run *_test.py files
edge init my-app # scaffold a project
edge publish app.edge # send a packed .edge to the registry
edge add json # add a package to edge.json
edge remove json # remove a package from edge.json
edge lock # resolve edge.json into edge.lock
edge uninstall # remove the binary and the PATH entryInstall
# Prebuilt binary (recommended)
curl -fsSL https://cdn.edgepython.com/cli/install.sh | sh
# Or from source (any platform with Rust and Cargo)
cargo install --path cliinstall.sh drops the binary at ~/.local/bin/edge and appends that directory to your ~/.bashrc or ~/.zshrc unless already present. Open a new shell and edge --version should work. Re-run the same line to upgrade. The Linux binaries are static, so any distribution works.
Pass a release to install that one instead of the latest, curl -fsSL https://cdn.edgepython.com/cli/install.sh | sh -s v1.0.0. Every release from 1.0 keeps a frozen copy of itself on the CDN, which is what a version reads and what a downgrade needs.
Building from source embeds compiler.wasm, so run cargo wasm first, or point EDGE_COMPILER_WASM at a copy. The build fetches nothing.
edge run: run a script
Runs a script and streams its output to the terminal. Bare imports resolve through edge.json, so declare each one with edge add and resolve it with edge lock first. Relative imports resolve against the importing file. Uncaught errors print a traceback to stderr and exit 1.
$ edge run broken.py
before
error: ZeroDivisionError: division by zero
--> broken.py:2:1
|
2 | x = 1 / 0
| ^raise SystemExit(code) with no argument or an integer exits cleanly with that code and no traceback. A string argument surfaces as a regular error and exits 1.
With no path, edge run reads the script from piped stdin (cat app.py | edge run) and errors when stdin is a terminal. -c <code> runs inline code instead (edge run -c 'print(1)'). With a path or -c, piped stdin instead feeds input() one line per call. A packed .edge or an app binary also runs here, edge run app.edge unpacks and runs it exactly as ./app would.
Flags: --events, --save-state, --restore-state, --preempt, --memory and --ops, see run flags.
--web runs the script on the browser host instead of the native engine, in headless Chrome. The JS host and the engine come out of the binary and are served on a loopback port, so only the modules the manifest declares by URL leave the machine. It drives a Chrome the system already has, or offers to download one the first time into ~/.local/share/edge/chromium, which edge uninstall offers to remove. EDGE_CHROME_PATH names a browser directly. --events, --save-state, --restore-state and --preempt belong to the native engine and are refused alongside it rather than ignored, while --memory and --ops apply to both.
edge serve: local dev server
Serves the current directory for browser apps and reloads the page on any file change, via a small polling client injected into served HTML.
$ edge serve
http://localhost:5173
watching .Flags: --port <n> (default 5173), --host <addr> (default 127.0.0.1), --open (open a browser). With --host 0.0.0.0 the banner adds your LAN URL, so you can open the app from a phone on the same network.
edge repl: interactive shell
$ edge add math
+ math 0.1.0
updated edge.json, run edge lock to resolve it
$ edge lock
+ math https://cdn.edgepython.com/pkg/math/0.1.0/app.edge
wrote edge.lock
$ edge repl
Edge Python 0.7.0 · .reset to start fresh · .exit, Ctrl+C or Ctrl+D to quit
>>> from math import sqrt
>>> print(sqrt(2))
1.4142135623730951One interpreter stays alive across prompts: imports, definitions, and mutations persist, and an input that raises keeps the effects it made before the error. Each line is one input, so compound statements go on a single line (def double(n): return n * 2). Expression results are not auto-printed, use print(). Arrow-key history works within the session and is not saved to disk. .reset wipes the session state, .exit quits, and Ctrl+C or Ctrl+D also quits the shell.
edge test: test runner
Discovers *_test.py files recursively (skipping dist/ and hidden directories), runs each in a fresh interpreter, and prints a verdict per file. A directory argument narrows discovery to that subtree, a file argument runs just that file.
$ edge test
pass. adds
1 passed, 0 failed
(successful) main_test.py
pass. parses
1 passed, 0 failed
(successful) lib/parse_test.py
2/2 files passed · 0.0sTest files declare tests with the test package and do not need to call run(). The runner drives it after the file loads and reads the verdict from the file’s SystemExit code, never from parsed output. A file that registers no tests fails. State never leaks between files. The project must declare test in edge.json, otherwise the runner stops with declare test in edge.json (edge add test).
Exit codes: 0 when every file passed, 1 when a file failed or no *_test.py was found, 2 when the engine session could not start.
--web runs the same files on the browser host in headless Chrome, so a package can prove it works where the system calls run in a real browser. One browser serves the whole suite with a fresh page per file, since launching one costs seconds. Everything else holds, the discovery, the test declaration and the exit codes, and the engine comes out of the binary the same way edge run --web gets it.
edge init: scaffold a project
Creates a ready-to-serve project. With no argument it scaffolds the current directory.
$ edge init my-app
created my-app/
├─ index.html
├─ main.py
└─ edge.json
cd my-app && edge serve--bare skips index.html for script-only projects. The manifest starts empty, nothing resolves until edge add fills it.
edge add / edge remove: package management
Edits edge.json by name, so you never paste URLs. Each line prints the name and what it wrote.
$ edge add math
+ math 0.1.0
updated edge.json, run edge lock to resolve itA name is looked up in the registry at its newest version, or at the one json@0.1.0 names, and the entry it writes is that version alone. An unknown name or version aborts the whole command before any write. Point a name at a custom URL with edge add foo=https://example.com/foo.wasm. It goes to imports too, whatever the URL, because each host tells a code module and a native module apart by the artifact, and a .js or .mjs URL is refused before anything is written. edge add keeps extends and any other key already in the manifest. edge remove deletes entries the same way, and neither touches edge.lock.
edge lock: resolve the manifest
Turns each version edge.json declares into the URL and digest of that release, and writes them to edge.lock beside it. Commit the lock, since it is what every other command reads.
$ edge lock
+ json https://cdn.edgepython.com/pkg/json/0.1.0/app.edge
wrote edge.lockThis is the only command that asks a registry where a name points. It rebuilds the whole lock each time, so running it again after editing edge.json is the way to bring the two back in step, and it also records a digest for a bare URL so those bytes are pinned too. edge run, edge test and edge build read the lock and never resolve a version themselves.
edge build: pack the app
Packs the project and its imports into one artifact, in one of three modes for three targets.
| Command | Output | Runs on |
|---|---|---|
edge build | a portable .edge | a host that already has edge, or a pool |
edge build --app | a standalone binary | any host of the same OS and CPU, offline, nothing installed |
edge build --web | a self-contained dist/ | any browser |
Each mode packs differently. The default .edge carries the project and its custom imports, since edge is already present where it lands. --app appends the project to this edge binary, so ./app runs it directly and honors the run flags --save-state, --restore-state, --preempt and --events. --web writes the JS host it carries under dist/js/ with compiler.wasm beside it, so neither reaches the network, adds every declared module with what it needs from the same origin (its relative imports and a sibling edge.json), and rewrites edge.json to the vendored paths, so the dist/ works offline.
All three carry every module the manifest declares by URL, with the files it imports beside it, which is what makes the artifact the whole program and lets a registry read what it needs without opening a CLI. A declared version resolves through edge.lock first, and a build refuses to pack while one is unlocked. A .edge and an app binary carry the lock beside each manifest they carry, so the artifact resolves its own names. A --web build rewrites every root import to a vendored path and so leaves the root lock out of dist/, while a nested package keeps its own.
A .edge and an app binary also carry the README.md and any LICENSE at the project root, so attribution reaches whoever receives the artifact. A .edge carries the documentation too when edge.json declares a docs directory, under a reserved @docs/ prefix no import can reach. An app binary leaves the pages behind, nobody reads documentation out of an executable.
Documentation is packed only after every page passes the rendering convention, so a package reaches a registry with pages the site can lay out. The build fails otherwise, since shipping documentation that cannot be laid out is worse than shipping none.
A page nests one folder deep at most and carries a numeric prefix on every path segment. It opens with a closed frontmatter block holding a title and a description, has exactly one top-level heading, and places an output block only ever directly after an edge-python block. An edge-manifest block sits only ever directly before one and holds a single JSON object, the edge.json that example runs under, such as { "permissions": { "main": ["time:wall"] } }, and an example without one runs under {}. The site holds its own docs/ to the same rules, read from the same place.
$ edge build
packed app.edge (2 files)
0.15 KB
run edge run app.edge or send it to an actor eval groupFlags: --out <path> (mode-specific default), --app, --web.
edge publish: send it to the registry
Uploads a .edge packed by edge build. The name and version come from the manifest inside the bundle, so the artifact carries what it is published as, and the doc pages travel beside it for the site to render. Set EDGE_TOKEN to a token from the settings first. The CLI signs each publish with it rather than sending it, so the registry keeps only a public key and the token never leaves your machine.
The artifact is the whole request. The CLI declares nothing beside it, and the registry opens the bundle and reads the name, the version, the description, the repository, the LICENSE and the doc pages out of the bytes it is about to store, so a listing can only ever show what you shipped.
Where a package runs is not declared and not yet worked out, so a release carries no claim about its hosts and a listing shows none.
$ export EDGE_TOKEN=edge_pat_...
$ edge publish app.edge
published slugify 0.1.0
add it with edge add slugify
https://cdn.edgepython.com/pkg/slugify/0.1.0/app.edgeA name is first come and permanent, and a version is never overwritten, so publishing the same one twice is refused, with exit code 2 instead of 1 so a script can tell it from a failure. The artifact is stored exactly as it was packed, so the registry never reads the bundle format.
One artifact is 10 MB at most, an account claims 10 names and publishes 60 versions a day, and its packages add up to a room the account can see in its settings. That room is 50 MB for an account that signs in by address alone and a gigabyte for one with GitHub or Google linked, since an address is cheap to come by and a provider account is not. Ask for more where the figure is shown.
edge actor: actor pool
Runs many edge-python programs as cooperative actors over a few threads, described by an actor.yml. See Actors for the manifest, groups, server, and untrusted code.
edge actor actor.ymlGroups resolve their imports through the edge.json beside actor.yml, or the one --manifest names. Sending needs no import, send() is a builtin.
edge uninstall
Removes the binary and its PATH entry. The non-interactive equivalent is curl -fsSL https://cdn.edgepython.com/cli/uninstall.sh | sh.
Global flags
| Flag | Effect |
|---|---|
--manifest <file> | Use a specific manifest instead of ./edge.json |
--version, -v | Print the version |
--help, -h | Print the command list |
Ctrl+C cancels a running command with exit code 130.