Run a project from orchd.json
orchd boots a project from a manifest that travels with it — one npm package that runs the same orchd.json on your machine, in a container and in a Lifo box.
A dev server needs to know two things: what to run, and where. Hand a box a command line and that knowledge lives in the caller. Put it in a manifest that ships inside the project and the box can work it out for itself.
That is what orchd.json does. ORCHD
provisions per-project workloads across substrates — host processes, Docker
containers, Lifo boxes — and one manifest describes all of them.
orchd is an ordinary npm package, not a Lifo-only tool. The same package is
the CLI on your machine and the command inside a box:
npx orchd up # your machine, CI, a container
npm install -g orchd
lifo install orchd # in a box — same packageIt works in both places because Lifo discovers commands by the lifo field in a
package's package.json, not by its name. Embedding Lifo yourself? Import the
box command directly:
import orchdCommand from 'orchd/lifo';
sandbox.commands.register('orchd', orchdCommand);orchd list # workloads in the manifest
orchd resolve --workload mobile --port 8082
orchd run --workload mobile --port 8082Boot the whole project
On a host, up supervises the workloads in the foreground the way
docker compose up does — output interleaved under a per-workload prefix, and
Ctrl-C stops the set. If one workload exits, the rest come down with it, because
a half-booted project is not a useful state to be left in:
$ npx orchd up
orchd: api -> PORT=3000 npm run dev (cwd /work/demo/api)
orchd: web -> API_URL=http://localhost:3000 PORT=3001 npm run dev (cwd /work/demo/web)
api | api dev server on 3000
web | web dev server on 3001
^C
orchd: stopping…Inside a box the same command backgrounds instead:
$ orchd up
orchd: api -> PORT=3000 node index.js (cwd /home/user/app/api)
orchd: mobile -> EXPO_PUBLIC_API_URL=http://localhost:3000 browser-metro . --port 8081 (cwd /home/user/app)
orchd: 2 workload(s) started; `jobs` to list themPorts are assigned first, so the workloads can find each other. Each takes its
declared "port", or one counting up from --port-base. A workload then refers
to a sibling by name:
{ "name": "mobile", "env": { "EXPO_PUBLIC_API_URL": "${url:api}" } }${url:<name>} and ${port:<name>} are what let one manifest work in both
worlds: on a host those siblings are separate subdomains, in a box they are
ports on localhost.
In a box, workloads start as background jobs, so up hands you back the prompt
with everything running — jobs lists them. It has to: a shell runs one
foreground command at a time, and a dev server never exits, so running them in
the foreground would boot only the first. On a host there is no such constraint,
so up keeps the children and the terminal.
The manifest
{
"name": "rapidnative",
"workloads": [
{ "name": "db", "kind": "tinbase" },
{
"name": "mobile",
"kind": "node",
"dir": "mobile",
"install": ["npm", "install"],
"run": ["npx", "expo", "start", "--web", "--port", "$PORT"],
"profiles": {
"lifo": { "run": ["browser-metro", ".", "--port", "$PORT"] }
}
}
]
}orchd looks for ./orchd.json, then /orchd.json — so a project restored at
the root of a box is found without being told where it is.
Profiles: same project, different runner
Which command is correct depends on what is executing it. Real Metro wants
expo start --web and reads node_modules. A Lifo box usually wants
browser-metro, which bundles through a hosted pre-bundler
and reads no node_modules at all — so the same project can boot from a
snapshot that never installed dependencies.
One manifest, one override block — and no profile applies unless the runner
asks for one. The base run is what your machine executes; the in-box command
asks for lifo, so a box gets browser-metro without the manifest ever
mentioning Lifo at the top level:
$ npx orchd resolve --workload mobile --port 8082 # on a host
npx expo start --web --port 8082
$ orchd resolve --workload mobile --port 8082 # in a box
browser-metro . --port 8082
$ npx orchd resolve --workload mobile --port 8082 --profile lifo
browser-metro . --port 8082That asymmetry is deliberate: profiles.lifo describes what a box should do and
must not leak onto a host that can run the real thing.
run, install and dir replace wholesale when a profile overrides them —
overriding run means exactly that. env merges key-wise, so a profile can add
one variable without restating the rest.
$PORT and ${PORT} are substituted in argv. A workload with
"port_env": "PORT" receives its port as an environment variable instead, which
suits servers that read process.env.PORT.
Supervising from the host
resolve --json answers identically on both runners — it is the machine
interface, so a supervising host gets { cwd, argv, env, install } and can run
a workload with its own logging and signals.
Inside a box, when something outside supervises the workloads instead, resolve on the inside and run from the outside, so the host keeps its own streaming and abort handling:
const spec = JSON.parse(
await sandbox.commands.run('orchd resolve -w mobile -p 8082 --json'),
);
// { workload, cwd, argv, env, install }
// `orchd resolve --all --json` returns one of these per workload.
const ac = new AbortController();
sandbox.shell.execute(spec.argv.join(' '), {
cwd: spec.cwd,
env: { ...sandbox.env, ...spec.env },
signal: ac.signal,
});
await sandbox.waitForPort(8082);On a Node host, publish it with exposePort; in the
browser, reach it with sandbox.fetch() or a preview.
orchd run does the same thing in one step, but it goes through
ctx.executeCapture, which buffers output and takes no AbortSignal — so a dev
server started that way stays silent until it exits and is not torn down when
the command is aborted. Use it interactively and for one-shot workloads; use
resolve when something is supervising.
With snapshots
Because the manifest travels with the files, a snapshot or a
plain project tarball is enough to boot a project end to end. importVfsSnapshot
accepts an ordinary gzipped tar — no lifo-snapshot.json required — so an
archive produced elsewhere restores just as well:
await importVfsSnapshot(sandbox.kernel.vfs, tarballBytes);
const spec = JSON.parse(await sandbox.commands.run('orchd resolve -w mobile -p 8082 --json'));A 24 KB source-only tarball of an Expo app — no node_modules — restores and
serves a bundle through browser-metro this way.
Note
browser-metro drops its first argument, so browser-metro --port 8082 binds
the default 8081 instead. Pass the project directory first —
browser-metro . --port 8082 — as the profile above does.