santoku
santoku

santoku

A Lua framework for building applications end to end

Lua 5.1 from the build system to the server to the browser. Every example on this site runs in your browser. Edit one and run it.


santoku targets Lua 5.1, because 5.1 is the version its platforms actually share. LuaJIT implements 5.1 and OpenResty embeds LuaJIT, so server code, client code and the build harness all run the same language. Compiled to WebAssembly, the 5.1 core is smaller to embed, and its single number type sidesteps the integer and float split introduced in 5.3, which keeps numeric values crossing the JavaScript boundary simple. Support for newer versions is meant to come from compatibility shims layered over the 5.1 core rather than version branches in the tree; contributions and design ideas for that layer are welcome.

santoku-cli

View on LuaRocks

the toku command line: build, test, install, release, license notices, and scaffolding for santoku projects

santoku-cli ships toku, the command-line front end of the framework: every santoku rock, and this documentation site itself, is built, tested, and released with it. The commands fall into two groups. The project lifecycle (init, test, install, pack, release, env, clean, plus build, start, and stop for web projects) is driven by santoku-make from a plain-Lua make.lua descriptor: sources are rendered into a build tree, dependencies are installed with luarocks into a private lua_modules, specs run against that tree, and the same tree feeds install and the luarocks release flow. The standalone utilities (template, bundle, lua) expose santoku-template, santoku-bundle, and an instrumented interpreter directly. This tab covers the whole surface, basics to advanced. toku runs processes and touches the real filesystem, so the shell examples are display only; the make.lua descriptor example is plain Lua and runs in the page.

The command surface

Every subcommand, listed here with its description verbatim from the argparse declarations in bin/toku.tk.lua. Every invocation also accepts --verbosity N (default 1): 0 silences the [make] line printed per rebuilt target, 2 and up adds [ok], [src], and [phony] trace lines. build, start, and stop apply to web projects, and a server-only API project is a web project to the harness, so all three apply to it too. install and pack are the mirror image: library projects only.

toku init Initialize a new project toku license Write LICENSE and per-file SPDX headers from make.lua's license and copyright toku test Run tests toku install Install the project toku pack Build rockspec and tarball without releasing toku release Release the project toku env Print shell exports for a project lua tree, for eval toku clean Clean build artifacts toku build Build the project toku start Start the server toku stop Stop the server toku template Process templates toku bundle Create standalone executables toku lua Run the lua interpreter on a file toku setup Maintain the toku-managed lua 5.1 toolchain (provisioned by setup-toku.sh from santoku.dev) toku skills Install the santoku agent skills from santoku.dev into your home directory, never into a project toku doctor Diagnose the managed toolchain and PATH wiring toku luarocks Run the managed luarocks toku luac Run the resolved luac toku resty Run the managed OpenResty resty script toku openresty Run the managed OpenResty nginx binary toku serve Serve a directory over HTTP with the managed OpenResty
Illustrative: runs outside the browser.

toku init: scaffold a library

Extracts a boilerplate, renames every tokuboilerplate occurrence to your name (which must match ^[a-z][a-z0-9-]*$), and runs git init. --here uses the current directory and its name; --dir picks the destination. Either target must be empty or missing, or init refuses before writing anything. The scaffold is a working rock, not a stub: a sqlite-backed library with a migration, a C extension (capi.c), a bin/ entry point, and a passing spec suite. --license ID --copyright HOLDER sets both fields in make.lua and writes LICENSE and SPDX headers. --copyright alone writes a one-line all-rights-reserved LICENSE. With neither flag, as below, the scaffold has no LICENSE and no license field, and toku warns.

$ toku init --name my-lib Created library project: my-lib Next steps: cd my-lib toku test # Run tests toku install # Install locally $ find my-lib -type f -not -path "*/.git/*" | sort my-lib/.gitignore my-lib/bin/my-lib.lua my-lib/lib/my_lib.tk.lua my-lib/lib/my_lib/capi.c my-lib/make.lua my-lib/res/migrations/0.0.1.sql my-lib/test/spec/my_lib.lua
Illustrative: runs outside the browser.

make.lua: the project descriptor

Everything toku knows about a project comes from make.lua, a plain Lua file returning { env = ... }. This one is santoku-cli's own descriptor: name and version become the rockspec, dependencies are luarocks constraints, env.release is what makes toku release available, and computed fields are plain Lua expressions. The rockspec's source url derives from the release host, so the descriptor never spells it out.

local release = require("santoku.make.release") local env = { name = "santoku-cli", version = "3.0.0-1", variable_prefix = "TK_CLI", license = "MIT", copyright = "Birch Point SWE", dependencies = { "lua == 5.1", "santoku >= 2.0.0, < 3.0.0", "santoku-fs >= 2.2.0, < 3.0.0", "santoku-template >= 2.0.0, < 3.0.0", "santoku-bundle >= 2.0.0, < 3.0.0", "santoku-system >= 2.2.0, < 3.0.0", "santoku-test-runner >= 2.0.3, < 3.0.0", "santoku-make >= 6.0.0, < 7.0.0", "argparse >= 0.7.1-1, < 1.0.0", }, } env.homepage = "https://santoku.dev/#" .. env.name env.release = { push = release.git_push(), host = release.s3_upload({ bucket = "birchpointswe-releases", base_url = "https://releases.santoku.dev" }), registry = release.luarocks_upload(), } return { env = env }
Illustrative: runs outside the browser.

toku license: LICENSE and SPDX headers

Writes LICENSE and a two-line SPDX header (SPDX-License-Identifier, SPDX-FileCopyrightText) into every tracked source file, from make.lua's license and copyright. The header uses the file's own comment syntax and goes after any shebang or tk directive. LICENSE holds the license text, fetched from the SPDX license list, then one section per vendored entry. A stale header is replaced; a file that already carries another notice is left alone and reported. --check writes and fetches nothing, prints each problem and exits nonzero. --license, --copyright and --exclude override make.lua, and --year defaults to the year of the first commit. Name files to limit the run. copyright alone writes a one-line all-rights-reserved LICENSE and no headers; with neither set, nothing is written and toku warns. The make tab covers the vendored and license_exclude keys.

$ toku license --check toku license: LICENSE doesn't name 2023 Birch Point SWE toku license: lib/my_lib/capi.c: missing header $ toku license lib/my_lib/capi.c: added a header LICENSE written for 2023 Birch Point SWE $ head -2 lib/my_lib/capi.c // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2023 Birch Point SWE
Illustrative: runs outside the browser.

Environments and the build tree

Work happens under build/<env>/: --env prod reads make.prod.lua instead of make.lua and builds under build/prod/. Files ending .tk are rendered through santoku-template against the descriptor env with the extension stripped, so lib/my_lib.tk.lua lands as lib/my_lib.lua; a .d sidecar next to each rendered file records what the template read through readfile and declared through depend, so editing either restales the output. The *.flag files persist CLI options like --single and --skip-check so that changing them restales the generated run and check scripts.

$ toku test $ ls build/default lua.flag single.flag test lua_cpath_extra.flag skip_check.flag lua_path_extra.flag $ ls build/default/test Makefile check.sh lua_modules luacheck.lua res test bin lib lua_modules.ok luarocks.lua run.sh my-lib-0.0.1-1.rockspec $ toku test --env prod $ ls build default prod
Illustrative: runs outside the browser.

The build tree for web and API projects

A library project builds one tree per environment. A web or API project builds two, main and test, and each holds three subtrees: client, server and dist. Only dist carries a lua_modules, because dist is what OpenResty runs with -p, so the rendered nginx.conf refers to it by relative path. A server-only project still gets a client subtree, holding little more than a lua_modules.ok stamp. When a script needs the project's own modules and its pinned rocks, toku lua --tree test or --tree build points LUA_PATH and LUA_CPATH at the matching dist/lua_modules. dist also holds run.sh, nginx.conf, server.pid and logs.

$ ls build/default/test client dist server $ ls build/default/test/dist # public/ too, when the project has a client lua_modules nginx.conf nginx-fg.conf run.sh $ ls build/default/test/dist # after a start: run.sh makes these logs lua_modules nginx.conf nginx-fg.conf run.sh server.pid temp $ toku lua --tree test scripts/load.lua
Illustrative: runs outside the browser.

toku test: the full pipeline

One command does the whole loop: render sources into build/default/test, luarocks make the test rockspec into a private lua_modules there, run sh run.sh (which invokes the standalone runner as toku test -i "$LUA" --match "^.*%.lua$" test/spec), then sh check.sh (luacheck over lib, bin, and test/spec unless --skip-check). The runner prints one Test: line per spec file and is silent on pass; a failing assertion prints the nested test tag chain, the error, and a traceback. Every spec runs by default, so one command reports every failure; pass -s to stop at the first one.

$ toku test [make] build/default/test/lib/my_lib.lua [make] build/default/test/my-lib-0.0.1-1.rockspec [make] build/default/test/run.sh Test: test/spec/my_lib.lua Total: 0 warnings / 0 errors in 3 files
Illustrative: runs outside the browser.

Selecting and instrumenting tests

In project mode --single runs one spec file, --skip-check drops luacheck, --lua swaps the interpreter, -s stops at the first failure instead of running the whole suite, and --trace preloads santoku.trace into the test interpreter for a source trace. Passing file or directory arguments switches to the standalone runner, bypassing the project build entirely: -m filters files by Lua pattern and -i runs each file under an arbitrary interpreter command instead of dofile. -i only applies in that standalone mode; in project mode the interpreter comes from --lua.

$ toku test --single test/spec/my_lib.lua $ toku test --skip-check $ toku test --trace $ toku test --lua luajit $ toku test -s $ toku test -m core -i luajit test/spec Test: test/spec/core.lua
Illustrative: runs outside the browser.

toku test --iterate: the watch loop

Runs test plus check, then blocks on inotifywait watching lib, bin, test, res, and every directory referenced by the .d sidecars, and reruns on any write. It requires inotifywait on PATH (errors with "inotify not found" otherwise), asks for a restart when make.lua itself changes, and bails out after two consecutive failures with no file changes in between. This is the standing dev loop: develop with toku test --iterate --show-logs.

$ toku test --iterate Test: test/spec/my_lib.lua [iterate] make.lua changed - please restart iterate
Illustrative: runs outside the browser.

toku test --wasm

Builds a parallel tree under build/default-wasm/ with the C toolchain swapped for emscripten: dependencies compile with emcc, each spec is bundled to a .js file, and run.sh drives them with toku test -i "node --expose-gc" so the same suite runs against the WebAssembly build. --single maps the .lua spec to its .js twin. --wasm-browser uses the same tree but links each spec for the web and runs it in headless Chromium through toku-browser run from santoku-cdp, which must be installed in toku's tree. TOKU_CHROMIUM names the browser command and is required; TOKU_CHROMIUM_FLAGS adds flags, such as --no-sandbox when the run is root inside a container; TOKU_BROWSER_TIMEOUT_MS bounds each spec, 60000 by default. A spec named *.browser.wasm.lua needs a DOM: --wasm-browser runs it and --wasm skips it by name.

$ toku test --wasm Test: test/spec/my_lib.js $ toku test --wasm --single test/spec/my_lib.lua $ TOKU_CHROMIUM=chromium toku test --wasm-browser Test: test-browser/test/spec/my_lib.js $ TOKU_CHROMIUM=chromium TOKU_CHROMIUM_FLAGS=--no-sandbox toku test --wasm-browser
Illustrative: runs outside the browser.

toku install

Runs test and check, renders the shippable tree under build/default/build, and luarocks-makes the rockspec there into the active rocks tree. --skip-tests goes straight to the build. --luarocks-config points luarocks at another config, which is how to pull a locally edited santoku rock into a project's build trees: run toku install from the library repo against each luarocks.lua found under the app's build/ directory.

$ toku install $ toku install --skip-tests $ find ~/git/myapp/build -name luarocks.lua \ -exec toku install --luarocks-config {} --skip-tests \;
Illustrative: runs outside the browser.

--deps-only: cache dependencies in their own image layer

toku install --deps-only and toku build --deps-only read only the descriptor (make.lua and whatever it runfiles) and install the dependencies it declares into the same trees the full build uses: the rocks tree for a library, and the server and client trees for a web project, plus each local dep's own dependencies. No project source is read or compiled. A later full build finds every dependency installed and builds only the project. In a container build, copy the descriptor files first, run the deps-only step, then copy the rest, so a source change reuses the cached dependency layer. Copy each local dep's make.lua too, plus every file any of these descriptors runfiles, and keep them free of reads from other project files.

COPY make.lua make.common.lua ./ COPY submodules/my-lib/make.lua submodules/my-lib/make.common.lua submodules/my-lib/ RUN toku build --env prod --deps-only COPY . . RUN toku build --env prod
Illustrative: runs outside the browser.

toku install --bundled: standalone executables

Instead of installing the rock, bundle every bin/*.lua through santoku-bundle into a native executable and copy it to <prefix>/bin (prefix defaults to $PREFIX when set, then $HOME/.local). Lua modules are embedded and C extensions are linked statically from the .o and vendored archives each rock installs alongside its .so, so the binary keeps working after the build tree is gone. Lua itself links statically, since the managed toolchain ships only liblua.a, so readelf -d lists libc and the system libraries your dependencies link, such as libopenblas; the binary needs no Lua modules on disk and no LUA_PATH. A dependency built before santoku-make 5.0.3 ships no such objects, and the bundler falls back to dynamic linking for it with a warning naming the module. --bundle-mods preloads modules the static require scan cannot see (comma separated), --bundle-ignores skips names (debug is always ignored), --bundle-cc picks the compiler, and --bundle-flags replaces the auto-derived compile flags rather than adding to them, so supply your own -I, -L and -l when you pass it. Any value starting with a dash needs the equals form, since the option parser would otherwise read it as a flag. With --wasm the compiler defaults to emcc and each entry point lands as a .js file plus a node wrapper script. That path needs emcc, emmake and node on PATH, and it rebuilds the whole dependency tree under emcc into build/<env>-wasm, so the first run is slow even for a small project.

$ toku install --bundled --prefix "$HOME/.local" $ ls ~/.local/bin my-lib $ toku install --bundled --bundle-mods my-lib.plugins \ --bundle-ignores ssl --bundle-flags="-O2 -I/usr/include -lm"
Illustrative: runs outside the browser.

toku pack: the release tarball

Builds build/default/build and tars it up as <name>-<version>.tar.gz without touching git or luarocks.org: sources with .tk stripped, the generated Makefile (plus lib/Makefile and bin/Makefile when those trees exist), and LICENSE, all under a <name>-<version>/ prefix. The rockspec sits alongside the tarball, and its source url is env.download: derived from the release host's download(name, version) when make.lua leaves it unset, so a project hosted with s3_upload gets the base_url, name and version joined for it.

$ toku pack $ ls build/default/build Makefile bin santoku-cli-3.0.1-1.rockspec santoku-cli-3.0.1-1.tar.gz $ tar -tzf build/default/build/santoku-cli-3.0.1-1.tar.gz santoku-cli-3.0.1-1/bin/toku.lua santoku-cli-3.0.1-1/bin/Makefile santoku-cli-3.0.1-1/Makefile santoku-cli-3.0.1-1/LICENSE
Illustrative: runs outside the browser.

toku release: the hooks in env.release

Only exists when make.lua declares env.release; otherwise toku prints the first line below and stops. release runs pack, which runs test and check unless --skip-tests, refuses a tree with uncommitted or untracked files, tags the rock version (an existing tag on HEAD is reused, so a failed release reruns without a bump), then runs the hooks phase by phase: artifacts, push, host, registry. Each hook is a function of ctx, or a table with a run function; ctx carries name, version, tag, commit, tarball, rockspec, build_dir, dir and artifacts. An artifacts hook pushes { path, key, content_type } entries onto ctx.artifacts. From santoku.make.release: git_push pushes HEAD and the tag; s3_upload PUTs ctx.artifacts when the artifacts phase added any, otherwise the tarball and rockspec, with If-None-Match so a key is never overwritten, and an existing key passes only when its bytes match. An entry with mutable = true goes up after the others, at <name>/<key> with no version, as a plain PUT with Cache-Control max-age=300, and s3_upload's latest = true option writes <name>/latest.txt the same way. gh_release creates a GitHub release carrying both files, and points the rockspec's source url at it, taking the repo from the origin remote or opts.repo; luarocks_upload reads LUAROCKS_API_KEY, refuses to run without luasec, and uploads the rockspec alone. It refuses a rockspec whose source url isn't https, since luarocks installs fetch the tarball from that url. A failure names the phases that completed, and the local tag is removed only when no phase ran.

$ toku release release is not available (requires a non-wasm project with env.release in make.lua) $ LUAROCKS_API_KEY=... AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... toku release ... 03074fe..5191a0d HEAD -> master * [new tag] 6.0.1-1 -> 6.0.1-1 uploaded: https://releases.santoku.dev/santoku-make/6.0.1-1/santoku-make-6.0.1-1.tar.gz uploaded: https://releases.santoku.dev/santoku-make/6.0.1-1/santoku-make-6.0.1-1.rockspec Done: https://luarocks.org/modules/birchpointswe/santoku-make -- make.lua: publish extra files beside the tarball env.release = { artifacts = function (ctx) local fs = require("santoku.fs") local arr = require("santoku.array") arr.push(ctx.artifacts, { path = ctx.tarball, key = fs.basename(ctx.tarball), content_type = "application/gzip" }, { path = ctx.rockspec, key = fs.basename(ctx.rockspec), content_type = "text/plain; charset=utf-8" }, { path = "install.sh", key = "install.sh", content_type = "text/x-shellscript" }) end, push = release.git_push(), host = release.s3_upload({ bucket = "my-releases", base_url = "https://releases.example.com" }), registry = release.luarocks_upload(), }
Illustrative: runs outside the browser.

Releasing a Claude Code plugin by URL

release.claude_plugin_archive({ plugin, base_url }) is an artifacts hook for a repo that holds plugins/<plugin>/ and .claude-plugin/marketplace.json. It checks that plugin.json carries the plugin's name and env.version, zips the plugin directory with its root at the top of the zip, and builds the same bytes on a rerun, so a retried release passes s3_upload's same-bytes check. It then writes a marketplace.json whose entry for the plugin has an archive source: the zip's URL under base_url and its sha256. The zip uploads write-once under the version, and marketplace.json uploads as a mutable key at <name>/marketplace.json, so the marketplace URL stays fixed and moves to each new version within five minutes. Users add that URL with claude plugin marketplace add. The archive source needs Claude Code v2.1.224 or later. It needs zip on the release host.

-- make.lua local release = require("santoku.make.release") local env = { name = "fleet", version = release.json_field("plugins/fleet/.claude-plugin/plugin.json", "version"), license = "MIT", } env.release = { artifacts = release.claude_plugin_archive({ plugin = "fleet", base_url = "https://releases.example.com" }), push = release.git_push(), host = release.s3_upload({ bucket = "my-releases", base_url = "https://releases.example.com" }), } return { env = env } $ claude plugin marketplace add https://releases.example.com/fleet/marketplace.json
Illustrative: runs outside the browser.

Releasing a Claude Code plugin as pasted text

release.claude_plugin_bundle({ plugin, base_url }) is an artifacts hook for a client that installs from text rather than a URL. It checks plugin.json the same way, then writes <plugin>-<version>.txt: every file under plugins/<plugin>/ in path order, each as a FILE line with its path, byte count and sha256, the exact contents, and an END line. A file must end with a newline and must not contain the marker. It also renders the repo's install.md into paste.md: {{url}} becomes the bundle's URL, {{version}} the version, and {{bundle}} the full bundle text, inserted last so a file holding % or {{ passes through unchanged. Any other placeholder fails the release. Both files upload under the version.

-- install.md Install Office {{version}}. The files follow; write each one, and check its sha256 against its FILE line. {{bundle}} -- make.lua env.release = { artifacts = release.claude_plugin_bundle({ plugin = "office", base_url = "https://releases.example.com" }), push = release.git_push(), host = release.s3_upload({ bucket = "my-releases", base_url = "https://releases.example.com" }), }
Illustrative: runs outside the browser.

toku init --web: scaffold a web app

The web boilerplate is a working todo app: client/ holds the Lua that compiles to WebAssembly (an entry point and a sqlite-backed db module that runs in a worker), server/ the OpenResty side with its nginx.tk.conf and a sync endpoint, and res/ the migrations for both databases. One descriptor, make.lua. It is deliberately not a PWA; see Getting started for adding a service worker and TLS.

$ toku init --web --name my-app Created web project: my-app Next steps: cd my-app toku build --test # Build for testing toku start --test # Start development server $ find my-app -type d -not -path "*/.git*" | sort my-app my-app/client my-app/client/bin my-app/client/lib my-app/client/lib/my-app my-app/client/res my-app/client/static my-app/client/test my-app/client/test/spec my-app/res my-app/res/client my-app/res/client/migrations my-app/res/server my-app/res/server/migrations my-app/server my-app/server/lib my-app/server/lib/my-app my-app/server/lib/my-app/web my-app/server/test my-app/server/test/spec
Illustrative: runs outside the browser.

The web loop: build, start, test, stop

build renders the client to WASM and the server tree; --test targets the test environment, without it the main one. start launches OpenResty against the built dist directory via its run.sh (openresty -p <dist> -c nginx.conf), backgrounded by default; --fg execs it in the foreground with the error log on stderr. stop shuts down both environments' servers. Environment variables prefix as usual: a dev server runs as DB_FILE=tmp.db toku start --test. toku test on a web project needs no manual start: when server specs exist it stops any old server, starts a fresh test one, waits for its pid file, and fails fast if the process dies (check the nginx error log). All of this applies to an API project unchanged.

$ toku build --test $ DB_FILE=tmp.db toku start --test $ toku test $ toku stop $ toku start --fg
Illustrative: runs outside the browser.

Scripting start and stop

start returns once dist/server.pid exists, or fails after nginx.start_timeout seconds (60 by default) with the [emerg] line from the error log. --fg, or TOKU_FG set to anything but 0 or empty, execs openresty instead and never returns, which is what a container needs. One trap in between: the backgrounded server inherits the standard output it was started with, so a piped or non-interactive invocation (ssh host 'toku start --test; curl ...') hangs on the open pipe even though toku itself has already exited. Redirect the server's output, or drive dist/run.sh yourself: it is a plain script, it writes the same pid file, and stop reads that file to send SIGTERM.

$ toku start --test >/dev/null 2>&1 # returns once dist/server.pid exists $ curl -s http://127.0.0.1:8080/items $ toku stop # or run the generated script directly, detached from the calling shell $ cd build/default/test/dist $ setsid nohup sh run.sh >/dev/null 2>&1 $ cat server.pid $ kill -15 "$(cat server.pid)"
Illustrative: runs outside the browser.

Web tests: root, client, server, logs

A web project has three suites: test/spec at the root (shared lib code), client/test/spec (compiled to WASM, run under node), and server/test/spec (run against the live test server). --root, --client, and --server select them; --single infers the suite from the path prefix. --show-logs tails the test server's access.log and error.log alongside the run, and toku stop cleans up the tail process with the server; toku stop --test stops only the test server and leaves a main one running. Server specs run under the same interpreter the server itself uses: OpenResty's LuaJIT when --openresty-dir (or OPENRESTY_DIR) resolves, so a rock that loads in nginx also loads in the specs. Root specs run under the plain interpreter. --lua overrides both.

$ toku test --root $ toku test --client $ toku test --server --show-logs $ toku test --single client/test/spec/my-app.lua $ toku test --single server/test/spec/my-app.lua
Illustrative: runs outside the browser.

--tree and toku env: run against a project lua tree

toku lua --tree <name> builds a project lua tree if needed, then runs the interpreter with LUA_PATH and LUA_CPATH pointing at it, so ad-hoc scripts see the same rendered sources and pinned rocks the specs do. A library project has one tree, test. A web or API project has test and build, the test and production server trees, run under OpenResty's luajit when it's installed. Paths are absolute and the script runs in your current directory. --env, --dir and --config pick the build environment as they do elsewhere. For anything other than lua, toku env --tree <name> prints export lines to eval into your shell.

$ toku lua --tree test scripts/seed.lua $ toku lua --env prod --tree build --string 'print(require("my_app.db"))' $ eval "$(toku env --tree test)" $ luarocks list $ sqlite3 tmp.db
Illustrative: runs outside the browser.

toku clean

Default: remove generated files inside build/<env>/ (rendered .lua sources, .d sidecars, stamp files, generated luarocks.lua and luacheck.lua) while keeping the expensive lua_modules trees. --deps removes exactly those trees instead, --all removes the whole build directory, --dry-run prints without removing, and web projects add --wasm, --client, and --server to scope the sweep. Paths outside the project directory are refused outright.

$ toku clean --dry-run Would remove: build/default/test/lib/my_lib.lua build/default/test/bin/my-lib.lua build/default/test/luarocks.lua build/default/test/luacheck.lua $ toku clean --deps Removed: build/default/test/lua_modules build/default/test/lua_modules.ok $ toku clean --all Removed: build $ toku clean Removed: (nothing to clean)
Illustrative: runs outside the browser.

toku serve: a static directory over HTTP

Serves a directory with the managed OpenResty, for a scratch site, a built web project's dist/public-staging, or a page a browser test drives. It writes a temporary nginx prefix (one worker, error log on stderr, no access log, directory listings on, index.html as the index, Cache-Control: no-store), runs openresty in the foreground and prints the URL; Ctrl-C stops it and the prefix is removed. --host defaults to 127.0.0.1 and --port to 8000. It needs the managed OpenResty, which toku setup --resty installs. It refuses a path that is not a directory ("serve: not a directory") and a path holding a space, semicolon or quote ("serve: paths with spaces or quotes are not supported"), since the path goes into nginx.conf unquoted. If openresty exits with an error, such as a port already in use, its own message prints on stderr and toku exits nonzero after removing the prefix.

$ toku serve build/default/test/dist/public-staging --port 8080 serving /home/me/my-app/build/default/test/dist/public-staging at http://127.0.0.1:8080/
Illustrative: runs outside the browser.

toku skills: agent guidance, installed globally

Fetches the santoku agent skills from santoku.dev and installs them into your home directory: one SKILL.md per skill under ~/.claude/skills for Claude Code, and one AGENTS.md under ~/.codex for Codex and the other tools that read that standard. It never writes into a project. A manifest beside the installed skills records what it put there, so a later run removes skills that are no longer published rather than leaving stale guidance behind, and it refuses to overwrite an AGENTS.md it did not write unless you pass --force. Override the target directories with --claude-dir and --codex-dir, and see what would happen with --dry-run.

$ toku skills [skills] fetching https://santoku.dev/skills.txt [skills] fetching https://santoku.dev/AGENTS.md [skills] wrote /home/you/.claude/skills/toku-setup/SKILL.md ... [skills] wrote /home/you/.codex/AGENTS.md
Illustrative: runs outside the browser.

toku lua: an instrumented interpreter

Runs a string or file under the configured interpreter (--lua overrides). The file comes positionally or with --file. Positionals after the file reach the script as arg[1], arg[2] and so on; with --file every positional is an argument. Put -- before the file when an argument starts with a dash. --string takes no arguments. --tree runs against a project lua tree, covered above. The instruments become -l preloads: --trace loads santoku.trace for line tracing, and --serialize loads santoku.autoserialize, which wraps the global print so tables come out as readable Lua literals instead of table: 0x... addresses.

$ toku lua scripts/load.lua $ toku lua --string 'print(1 + 2)' 3 $ toku lua --serialize --string 'print({ a = 1, b = { 2, 3 } })' $ toku lua --trace scripts/step-through.lua $ toku lua scripts/report.lua input.csv 10 $ toku lua -- scripts/report.lua -v input.csv $ toku lua --string 'x()' extra toku lua takes no script arguments with --string: extra
Illustrative: runs outside the browser.

toku template: standalone rendering

The same engine that expands .tk files in the build, exposed directly. -f renders one file (- is stdin or stdout), -d renders a directory tree with -t trimming a prefix from output paths, and -c loads a Lua config whose env table becomes the render environment. -M writes a make-style .d file next to each output: one line naming the source and every file the template read through readfile. toku template injects readfile only; depend exists in project builds.

$ echo '<% return "hello" %>' | toku template -f - -o - hello $ toku template -d res/tmpl -t res/tmpl -o build/out -c cfg.lua $ toku template -f res/index.tk.html -o build/index.html -M $ cat build/index.html.d res/index.tk.html: res/header.html
Illustrative: runs outside the browser.

toku bundle: one file in, one executable out

The CLI face of santoku-bundle (see its tab for the library). It scans the entry for string-literal require calls, resolves them against --path and --cpath, merges every Lua module into one file via package.preload, embeds the (optionally luac-stripped) bytecode in a generated C program that also preloads the C extensions' luaopen_ symbols, and compiles it, printing the compile command. Four options are required: --input, --output-directory, --path and --cpath. Unlike toku install --bundled, this command derives no compile flags for you, so --flags is the complete list and must carry the -I that finds lua.h and the -l that supplies liblua, or the compile fails before it starts. Dynamic requires need --mod; names to leave to the runtime take --ignore; --env NAME VALUE bakes in a setenv at startup; --deps writes a make .d file covering every bundled module. Where a C extension ships a .link sidecar it is linked statically, and where it does not the .so is linked directly and a warning names it.

$ toku bundle --input bin/tool.lua --output-directory out \ --path "lua_modules/share/lua/5.1/?.lua" \ --cpath "lua_modules/lib/lua/5.1/?.so" \ --luac-default \ --flags="-O2 -I$(luarocks config variables.LUA_INCDIR) \ -L$(luarocks config variables.LUA_LIBDIR) -llua5.1 -lm" \ --env TOOL_HOME /opt/tool \ --mod tool.plugins --ignore debug cc out/tool.c -O2 -I... -L... -llua5.1 -lm \ lua_modules/lib/lua/5.1/tool/capi.o -o out/tool $ ls out tool tool.c tool.lua tool.luac
Illustrative: runs outside the browser.