{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/runcode",
  "slug": "packages/runcode",
  "kind": "capabilities",
  "bucket": "package",
  "title": "@leumas/runcode",
  "name": "@leumas/runcode",
  "eyebrow": null,
  "chip": null,
  "summary": "ONE seam for running any language from JS - runCode('python'|'rust'|'perl'|..., { code | file }), plus variable crossing: vars become native variables and emit() returns a value, so runs chain across...",
  "keywords": [
    "runcode",
    "perl",
    "runcode pyrun",
    "runcode lifecycle",
    "crossing",
    "runcode api",
    "leumas runcode",
    "how to use runcode"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# `@leumas/runcode`\n\nOne seam for running **any language** from JS. A language is a declarative table row, not a code path.\n\n```js\nimport { runCode } from '@leumas/runcode';\n\nawait runCode('python', { code: 'print(1+1)' });\nawait runCode('node',   { file: 'scripts/report.mjs', args: ['--week'] });\nawait runCode('rust',   { code: rustSource, input: { n: 10 } });\nawait runCode('bat',    { code: 'echo hi' });\n```\n\n**Variable crossing** — values go in as native variables, and come back out through `emit`:\n\n```js\nconst r = await runCode('python', {\n  code: 'emit({\"sum\": a + b, \"who\": name.upper()})',\n  vars: { a: 20, b: 22, name: 'leumas' },\n});\nr.output; // { sum: 42, who: 'LEUMAS' }\n```\n\nThe same `vars` and the same expected `output` work in `node`, `bun`, `deno`, `typescript`, `ruby`,\n`perl` and `php` — only the snippet changes. And because `output` is already a legal `vars`, runs\n**chain across languages** with nothing in between:\n\n```js\nconst one   = await runCode('python', { code: 'emit({\"total\": sum(nums)})', vars: { nums: [1,2,3] } });\nconst two   = await runCode('node',   { code: 'emit({half: total / 2})',    vars: one.output });\nconst three = await runCode('perl',   { code: 'emit({ok => $half > 2})',    vars: two.output });\n```\n\n## Why it exists\n\n`@leumas/pyrun` already owns Python *spawning* and *sidecar lifecycle*, and it is still the right\nanswer for a long-running FastAPI sidecar. What it does not give you is a **call convention** — so\nevery place that reached another language grew its own copy of the same four problems: find the\ninterpreter, build an argv, bound the run, parse the result. This package solves those once, for\nevery language, and delegates Python interpreter discovery back to `pyrun` rather than owning a\nsecond opinion about which Python this repo means.\n\n## The one invariant\n\n**`runcode` never decides who may call it.** It has no notion of a session, a role, a tenant or a\npolicy, and it deliberately accepts inline `code`. That is safe *here*: first-party JS running in\nthis process could already call `spawn()` itself, so an easier way to do it escalates nothing.\n\nIt stops being safe the moment a language ref crosses a **trust boundary**. A stored database row, an\nHTTP body, a fabric dispatch, a plugin — anything whose author is not the person who deployed this\nprocess — must be given `file` plus an allow-list, **never `code`**.\n`products/standalone/leumas-node/src/exec/python.js` is the worked example of that seam: two\nempty-by-default gates, a realpath fence, and refusal by name. Its header records why, and it is\nworth reading before wiring this package into anything outward-facing.\n\nStructural properties, always on: argv **arrays** only (`shell: false` — nothing in an input can be a\ncommand), a wall-clock timeout with SIGTERM then SIGKILL, and a byte cap on captured output.\n\n## API — six exports\n\n### `runCode(language, opts) → Promise<result>`\n\nGive it **either** `code` (inline source, written to a scratch file with the language's extension and\ndeleted afterwards) **or** `file` (a path used in place). Both, or neither, is a refusal.\n\n| Option | Default | Meaning |\n|---|---|---|\n| `code` | — | Inline source. Mutually exclusive with `file`. |\n| `file` | — | Path to a source file. Relative paths resolve against `cwd`. |\n| `args` | `[]` | Arguments after the script, as an argv array. |\n| `input` | — | Serialized to **one JSON argv element** appended last. Omitted entirely when absent. |\n| `vars` | — | **Variable crossing** — each key becomes a native variable, and the script gains `emit(value)`. Needs `code` and a crossing language. Travels by file, so no argv cap. |\n| `stdin` | — | Written to standard input, which is then closed. **Use this for large payloads** — see below. |\n| `env` | — | Merged **over** `process.env`. |\n| `cwd` | — | Working directory. |\n| `timeoutMs` | `120000` | Kill after this long. `0` disables the timer. |\n| `maxBytes` | `8388608` | Cap on stdout+stderr **combined**. |\n| `onStderr` | — | `(line) => void` per stderr line, live. stdout is buffered until exit. |\n\nThe result — **it never throws for an expected condition.** A missing toolchain, an unknown language,\na non-zero exit and a timeout all *resolve* with `ok: false` and a field that tells them apart:\n\n| Field | Meaning |\n|---|---|\n| `ok` | True **only** when the program ran and exited 0. Check this, don't just `await`. |\n| `output` | `stdout` parsed as one JSON document, else `null`. Parsed on the failure path too. |\n| `stdout` / `stderr` | Always raw, always present. Many tools log to stderr on success. |\n| `exitCode` | `null` when killed or never started. |\n| `refused` | This package declined to act — unknown language, no source, both sources. Nothing spawned. |\n| `unavailable` | The toolchain is not installed. The fix is an install, so it is not a `refused`. |\n| `timedOut` / `truncated` | Killed by the clock, or by the byte cap (the text kept is the **prefix**). |\n| `error` | Why it is not `ok`, as a sentence. For a compile failure this is the **compiler's own diagnostics**. |\n| `install` | How to get the toolchain. Non-empty only when `unavailable`. |\n| `bin` | The executable actually used — the interpreter, or the compiler for a compiled language. |\n| `compileMs` | Compiled languages only. `0` means the artifact cache hit. |\n\n`output` being `null` does not mean failure, and a script that genuinely printed `null` is\nindistinguishable from one that printed prose — use `stdout` when that distinction matters.\n\n### `available(language, { refresh }) → Promise<{ ok, bin, version, unavailable, install, reason }>`\n\nIs this language runnable here? Memoised for ten minutes, since a probe costs a spawn; pass\n`refresh: true` right after installing a toolchain.\n\n### `listLanguages() → string[]`\n\nEvery registered id. Presence means the **row** exists, not that the toolchain is installed.\n\n### `varsLanguages() → string[]` · `supportsVars(language) → boolean`\n\nWhich languages can take `vars`. Membership means the **row** declares a prelude, not that the\ntoolchain is installed.\n\n### `registerLanguage(spec, { overwrite }) → row`\n\nTeach it a language this package does not ship. Throws on a malformed spec or on shadowing an\nexisting id without `overwrite` — those are wire-up errors, not runtime conditions, so unlike the\nrest of the package they throw.\n\n## Adding a language\n\nA row in `src/runtimes.js`. If you ever need `if (language === 'x')` outside that file, the row is\nmissing a field and the field is the fix.\n\n```js\n{\n  id: 'ruby',\n  ext: '.rb',\n  bin: { env: ['RUNCODE_RUBY'], candidates: ['ruby'] },\n  argv: ({ file, args }) => [file, ...args],\n  install: 'Install Ruby (https://ruby-lang.org).',\n}\n```\n\n**Compiled languages** add `compile` and **omit `argv`** — the built artifact *is* the executable, so\n`args` reach it untouched. (Declaring `argv` there invites `[file, ...args]`, which hands the program\nits own path as `argv[1]`; the table rejects it for that reason.)\n\n```js\n{\n  id: 'rust',\n  ext: '.rs',\n  bin: { env: ['RUNCODE_RUSTC'], candidates: ['rustc'] },\n  compile: ({ src, out }) => ['-O', src, '-o', out],\n  install: 'Install Rust (https://rustup.rs).',\n}\n```\n\nArtifacts are cached by a hash of the **source plus the compiler's version string** — hashing the\nsource alone would reuse a binary built by a compiler the operator has since replaced.\n\n## The shipped rows\n\n**Interpreted** — `python` · `node` · `bun` · `deno` · `typescript` · `ruby` · `perl` · `php` · `lua` ·\n`awk` · `java` (single-file source mode, JEP 330) · `r` · `julia` · `elixir` · `zig` (`zig run`) ·\n`swift` · `dart` · `bash` · `sh` · `powershell` · `bat`\n\n**Compiled** — `cpp` · `c` · `rust` · `go`\n\n`listLanguages()` is the live answer; the list above goes stale, the function does not.\n\nThe `typescript` row is served by whichever of `bun`, `deno` or `tsx` is installed, and its `argv`\nbranches on which one answered — that is why a row's `argv` is handed `bin`. One row means a caller\nnever has to ask which TypeScript runner a machine has.\n\n## Variable crossing, in full\n\n`vars` binds each key as a **native variable** and defines **`emit(value)`**, which prints one JSON\ndocument — the same thing `output` parses. So a value crosses *in* as a variable and back *out* as a\nvalue, and `output` from one run is a legal `vars` for the next. That is what makes runs chain across\nlanguages.\n\nSupported in **`python` · `node` · `bun` · `deno` · `typescript` · `ruby` · `perl` · `php`** — ask\n`varsLanguages()`. A row without a prelude (`awk`, `lua`, `java`, and every compiled row) **refuses**\n`vars` by name and points at `input`; it never ignores them silently.\n\nThree rules, each with a reason:\n\n- **`vars` needs `code`, not `file`.** The prelude is prepended to the source, and this will not\n  rewrite a file on disk. Pass `input` to a file instead.\n- **Keys must be plain identifiers** (`[A-Za-z_][A-Za-z0-9_]*`) and must not be a keyword in *any*\n  supported language. `class` binds fine in Python and is a syntax error in Ruby, PHP and JS —\n  refusing it everywhere is what keeps a snippet portable. `emit` is reserved too.\n- **The prelude is exactly one line**, so every reported line number is off by exactly one. A\n  multi-line preamble would make every stack trace quietly wrong by an unknown amount.\n\nTwo language-specific notes:\n\n- **Perl and PHP names carry their sigil.** A var named `total` arrives as `$total`. That is those\n  languages being themselves, not an inconsistency.\n- **A PHP snippet must not reopen `<?php`** — the prelude already did, and PHP outside a tag is echoed\n  verbatim.\n\nBecause the payload travels by **file**, `vars` is not subject to the argv cap below — a 400 KB\nobject crosses fine, where the same payload through `input` is refused.\n\n## Two limits worth knowing before you wire this in\n\n**`input` is an argument list, not a file transfer.** The whole argv is capped at **24 KB on Windows**\n(CreateProcess allows 32767 characters for the entire command line, shared with the interpreter path\nand flags) and 256 KB elsewhere. Over that, `runCode` **refuses** with a sentence naming the size and\nthe limit — rather than letting the OS answer `spawn ENAMETOOLONG`, which names neither the offending\nargument nor the fix. Pass large payloads on **`stdin`**, which has no such cap.\n\n**Output is decoded across chunk boundaries.** stdout/stderr arrive as byte chunks that split wherever\nthe pipe buffer fills — routinely mid-character. Decoding each chunk independently corrupts every\nsplit multi-byte sequence, so this uses one `StringDecoder` per stream. `maxBytes` is counted in real\n**bytes** off the buffer, not in decoded characters.\n\nRelatedly, the `python` row passes **`-X utf8`**: on Windows Python still defaults stdout to the legacy\nANSI codepage, so printing any character outside Latin-1 raises `UnicodeEncodeError` and kills the\nscript. `PYTHONIOENCODING` cannot fix that here because `-I` implies `-E` (ignore all `PYTHON*` env\nvars) — the flag form survives isolation, so the row keeps both.\n\n## Scratch files\n\nInline `code` and compiled artifacts live under `resolveDataRoot()/runcode` (override with\n`RUNCODE_SCRATCH_DIR`), so the packaged exe behaves like dev and an operator reclaiming disk has one\ndirectory to look at. `clearScratch()` empties it. Source files are deleted after every run,\nincluding the timeout path.\n\n## Tests\n\n```\nnode --test test/*.test.js\n```\n\nThree suites:\n\n- **`runcode.test.js`** — toolchain-free. It only needs `node`, which is what runs the tests.\n- **`toolchains.test.js`** — python and the compiled lane. **Skips loudly** when a toolchain is\n  absent, naming what is missing and how to install it. A green run does not mean g++ was exercised;\n  read the skip lines.\n- **`vars.test.js`** — variable crossing and cross-scripting. Every installed crossing language runs\n  the *same logical program* and must produce the *same* `output`; a three-language chain\n  (python -> node -> perl) passes values with no glue; and each refusal is asserted separately,\n  because they have three different fixes.\n- **`integration.test.js`** — does it *fit this codebase*: parity with `@leumas/pyrun`'s interpreter\n  ladder, binding behind `@leumas/invoke`'s `bindDescriptor`, running through the real adapter\n  registry, adding no signal handlers of its own (`@leumas/lifecycle` owns those), surviving a\n  SIGTERM-ignoring child, driving `check-toolchains.mjs`, and the no-shell guarantee across every\n  installed language. Cross-package modules are reached by **dynamic import from the workspace root**\n  — never a relative path climbing out of the package, which `pnpm check:escapes` forbids — and each\n  such test skips when the module is not on disk.\n",
  "source": {
    "path": "shared/packages/runcode/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 13263,
    "hash": "c3c0fb15d205eef2d661525dc184fc4d608fa308"
  },
  "urls": {
    "html": "/p/packages/runcode",
    "json": "/docs/packages/runcode.json",
    "md": "/docs/packages/runcode.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  },
  "exports": {
    "total": 16,
    "component": 3,
    "hook": 0,
    "helper": 13,
    "names": [
      {
        "n": "BUILTIN_IDS",
        "k": "component"
      },
      {
        "n": "DEFAULT_MAX_BYTES",
        "k": "component"
      },
      {
        "n": "DEFAULT_TIMEOUT_MS",
        "k": "component"
      },
      {
        "n": "available",
        "k": "helper"
      },
      {
        "n": "clearAvailabilityCache",
        "k": "helper"
      },
      {
        "n": "clearScratch",
        "k": "helper"
      },
      {
        "n": "getLanguage",
        "k": "helper"
      },
      {
        "n": "killAll",
        "k": "helper"
      },
      {
        "n": "listLanguages",
        "k": "helper"
      },
      {
        "n": "registerLanguage",
        "k": "helper"
      },
      {
        "n": "runBounded",
        "k": "helper"
      },
      {
        "n": "runCode",
        "k": "helper"
      },
      {
        "n": "scratchRoot",
        "k": "helper"
      },
      {
        "n": "supportsVars",
        "k": "helper"
      },
      {
        "n": "validateLanguage",
        "k": "helper"
      },
      {
        "n": "varsLanguages",
        "k": "helper"
      }
    ]
  }
}
