userland.rundocs70Try the demo
SDK

Filesystem

nano.fs is a typed view of the VM's in-memory POSIX filesystem. Reads and single-file writes are synchronous; directory mutations run BusyBox under the VM lock and are async; archives and lazy files cover bulk loading.

Every runner sees the same filesystem: files you write here are visible to BusyBox, Node.js, wasm apps and sandboxed scripts. Over the worker transport the same methods exist but every one returns a promise.

Fast path (sync)

OptionTypeDefaultDescription
writeFile(path, content, mode?)voidmode 0o644Create or overwrite a file; parent directories are created. content is a string or Uint8Array.
readText(path)string | null—UTF-8 text, or null when missing.
readFile(path)Uint8Array | null—Raw bytes, or null.
list(path)DirEntry[] | null—Directory entries { name, type: "file" | "dir" | "symlink", size }.
exists(path)boolean—True for an existing file or directory.
walk(root?)string[]"/"Every file path under root, recursively.
SDK · filesystem

Sync reads and writes

No VM step and no await: ideal for seeding inputs before a run and reading results after.

javascript
nano.fs.writeFile("/work/src/index.js", "export const x = 1;\n");
nano.fs.writeFile("/work/data.bin", new Uint8Array([1, 2, 3]));
print(nano.fs.readText("/work/src/index.js").trim());
print(nano.fs.readFile("/work/data.bin"));
print(nano.fs.exists("/work/src"), nano.fs.exists("/work/missing"));
print(JSON.stringify(nano.fs.list("/work/src")));
loading…
Expected output
export const x = 1;
Uint8Array(3)
true false
[{"name":"index.js","type":"file","size":20}]

── /work ──
/work/data.bin
/work/src/index.js

File modes

The guest enforces the execute bit, so pass a mode such as 0o755 for scripts and binaries you want to run. Catalog installs do this for you.

SDK · filesystem

writeFile with mode 0o755

javascript
nano.fs.writeFile("/work/hello.sh", "#!/bin/sh\necho running $0\n", 0o755);
nano.fs.writeFile("/work/plain.sh", "#!/bin/sh\necho never\n");
print((await nano.shExec("ls -l /work/hello.sh /work/plain.sh | cut -c1-10; /work/hello.sh; /work/plain.sh")).stdout.trim());
loading…
Expected output
-rwxr-xr-x
-rw-r--r--
running /work/hello.sh
/tmp/.nano-exec-0.sh: line 1: /work/plain.sh: Permission denied

── /work ──
/work/data.bin
/work/hello.sh
/work/plain.sh
/work/src/index.js

Mutations (async)

Directory operations run the real BusyBox tool, so they have shell semantics (recursive delete, attribute-preserving copy, rename over). They take the VM lock and throw nano-sdk: fs op failed (exit N): <command> with the tool's message on failure.

OptionTypeDefaultDescription
mkdir(path)Promise<void>—mkdir -p.
remove(path)Promise<void>—rm -rf.
move(from, to)Promise<void>—mv.
copy(from, to)Promise<void>—cp -a.
SDK · filesystem

mkdir, copy, move, remove

javascript
await nano.fs.mkdir("/work/a/b/c");
nano.fs.writeFile("/work/a/b/c/note.txt", "hi");
await nano.fs.copy("/work/a", "/work/a-copy");
await nano.fs.move("/work/a-copy/b", "/work/moved");
await nano.fs.remove("/work/a");
try {
  await nano.fs.move("/work/nope", "/work/x");
} catch (e) {
  print(e.message.split("\n")[0]);
}
loading…
Expected output
nano-sdk: fs op failed (exit 1): mv '/work/nope' '/work/x'

── /work ──
/work/data.bin
/work/hello.sh
/work/plain.sh
/work/moved/c/note.txt
/work/src/index.js

Archives

loadTarGz(bytes) unpacks a gzipped tar archive into the filesystem at the paths it contains. It is how overlays are applied at boot (see image.overlays) and works on npm package tarballs.

SDK · filesystem

Unpack an npm tarball

Fetches left-pad-1.3.0.tgz from the npm registry (CORS-enabled) and unpacks it. npm tarballs put everything under package/.

javascript
const res = await fetch("https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz");
await nano.fs.loadTarGz(new Uint8Array(await res.arrayBuffer()));
print(nano.fs.walk("/package").filter((f) => !f.includes("/perf/")).sort().join("\n"));
print((await nano.node(["-e", "console.log(require('/package/index.js')('7', 3, '0'))"])).stdout.trim());
loading…
Expected output
/package/.travis.yml
/package/COPYING
/package/README.md
/package/index.d.ts
/package/index.js
/package/package.json
/package/test.js
007

── /work ──
/work/data.bin
/work/hello.sh
/work/plain.sh
/work/moved/c/note.txt
/work/src/index.js

Lazy files

registerLazyFile(path, { size, mode, resolve }) records a file whose bytes are produced by resolve() the first time a guest program opens it. The catalog's lazy: true installs use this. Until it is materialized, exists() reports false.

SDK · filesystem

registerLazyFile

javascript
let calls = 0;
nano.fs.registerLazyFile("/work/lazy.txt", {
  size: 23,
  mode: "0644",
  resolve: async () => {
    calls++;
    return new TextEncoder().encode("materialized on demand\n");
  },
});
print("resolve() calls before:", calls);
print((await nano.shExec("cat /work/lazy.txt; cat /work/lazy.txt")).stdout.trim());
print("resolve() calls after:", calls);
loading…
Expected output
resolve() calls before: 0
materialized on demand
materialized on demand
resolve() calls after: 1

The cli-toolkit example seeds files and runs catalog tools over them.

ln -s inside the guest doesn't create symlinks (there is no symlinkat syscall yet; ln reports "Function not implemented"), so list() reports symlink entries only for links created by the image, such as the BusyBox applets in /bin. See Limitations.