ludic/tools/ludic-cli/main.ludic
Orkuncakilkaya 320ce42626 ludic remove <module> and ludic get --json (R9)
remove is the inverse of add: the require line leaves package.ludic; the lock keeps exactly what the
remaining requires still reach, read from the store's copy of each locked package.ludic (no
network), so a package another still requires stays locked and what only the removed one brought in
leaves with it; each leaving package loses the ludic_modules/ symlink add made, never the shared
store entry. Refused (exit 1) when package.ludic does not require the module; source still
importing a removed package is a warning. With no store copy to read, only the named module leaves.

get --json diffs the lock before and after in memory and prints {added, removed, changed,
unchanged} on stdout (an entry as the lock records it: name, version, hash, kind, provides; a change
as name, from, to, from_hash, to_hash), the resolver's lines on stderr. Cases added to test-pkg,
not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:24:49 +03:00

182 lines
9.3 KiB
Text

# ludic — the command-line interface to the Ludic language. One native binary
# (bin/ludic) written in Ludic and compiled by Ludic; the unix tools, clang and
# the compiler it drives are invoked through the `run` intrinsic.
#
# This is everything a user of the language does: create a project, build it, run
# it, test it, add a package, format, upgrade. Nothing here is about the
# toolchain's own repository — building the compiler, reseeding it, generating
# the docs site, cutting a release live in a separate tool (dev.ludic ->
# bin/ludic-dev) that only exists in a checkout and is never installed. Someone
# who installs Ludic should not have to read past commands they cannot use.
#
# Installing (macOS, Linux):
#
# curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh
#
# Building from a checkout — the one step Ludic cannot do for itself, since
# compiling Ludic needs a compiler:
#
# mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
# bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
# bin/ludic-dev build
program Ludic {
import "prelude.ludic"
import "build.ludic"
import "split.ludic"
import "project.ludic"
import "pkg.ludic"
import "pkg_lock.ludic"
import "pkg_remove.ludic"
import "assets.ludic"
import "packignore.ludic"
import "pack.ludic"
import "bundle.ludic"
import "scripts.ludic"
import "deps.ludic"
import "schema.ludic"
import "ui_preview.ludic"
import "testpar.ludic"
import "migrate.ludic"
function usage() -> void {
print("ludic — the toolchain for the Ludic language")
print("")
print("usage: ludic <command> [arguments]")
print("")
print("start a game:")
print(" new <name> create a project that builds and runs as it stands")
print(" run [file] [--headless] build the project and run it")
print(" build [file] [--headless] [-o out]")
print(" compile to build/<name> (a native binary, nothing to ship beside it)")
print(" build [file] --check only check it: types, modules, uses and ports; nothing emitted")
print(" build [file] --check --diagnostics=json every error as a JSON array on stdout")
print(" build [file] --check --stdin-file PATH stdin is PATH's unsaved text (an editor's buffer)")
print(" test [file|dir...] [-j N] [-v] [--test NAME]")
print(" compile and run the project's tests (-v: every test's line)")
print(" deps [file] [--graph|--dot|--writes|--uses MOD|--check F|--baseline F]")
print(" the module graph as the compiler sees it, and how tangled it is")
print(" schema [file] [-o FILE] records, registries and their entries, consts, as JSON (for editors)")
print(" migrate state [file] [--prune] [--runtime] module-level vars into states, passed as parameters (0.S); --prune drops states nothing uses, --tighten also mut nothing writes")
print(" clean remove build/")
print("")
print("packages:")
print(" add <module>[@version] add a dependency, then resolve + fetch + link")
print(" remove <module> drop a dependency: its require, what only it locked, its ludic_modules/ link")
print(" get [--json] resolve every package.ludic dependency and write the lock")
print(" (--json: what changed in the lock, as a JSON object on stdout)")
print(" update [module] bump a dependency (or all) to its latest published version")
print(" verify check every locked package against the store by content hash")
print(" vendor copy the resolved packages into ./vendor for offline builds")
print(" assets [--force] fetch the CC0 materials the renderer needs into assets/polyhaven/")
print(" build-lib <module.ludic> compile a package's module to a prebuilt dylib in lib/<target>/")
print(" link-flags print the clang flags to link this project's prebuilt module dylibs")
print("")
print("shipping:")
print(" pack [--out FILE] [dirs...] pack the assets into build/<name>.lpak (honouring .packignore)")
print(" pack --no-ignore ... pack everything, ignoring .packignore")
print(" pack --list FILE print what is in a pack")
print(" pack --verify FILE re-hash every entry against the table")
print(" bundle [file] build a macOS .app: icon, metadata, assets, splash")
print("")
print("the toolchain:")
print(" version print the toolchain version")
print(" upgrade [version] reinstall from the docs site (the same script that installed it)")
print(" doctor check that the install is complete and usable")
print(" fmt [--check] [paths...] format Ludic source in place (--check: report, write nothing)")
print(" fmt --lint [--json] check the project's `lint` rules (--json: one JSON array on stdout)")
print(" fmt - [--stdin-name PATH] format stdin to stdout under the project's rules, as if it were PATH")
print(" fmt - --lint [--json] [--stdin-name PATH] lint stdin as PATH")
print(" lsp run the language server on stdio (what editors spawn)")
print(" ui-preview [--font DIR] preview a game's ludic.ui components for a studio, on stdio")
print(" help this message")
print("")
print("package scripts (package.ludic):")
print(" script <name> [args] run a `script \"name\" \"command\"` line (or just: ludic <name>)")
print(" scripts list the package's scripts and hooks")
print(" `hook before|after <command> \"…\"` wraps build, run, test, bundle, …")
print("")
print("A file argument runs it: ludic mygame.ludic")
}
# does `s` end in ".ludic"? — `ludic mygame.ludic` runs that file, the shape
# the compiler-runner had before the CLI grew subcommands.
function is_ludic_file(s: pointer) -> bool {
let n = slen(s)
if n <= 6 { return false }
return sslice(s, n - 6, n) == ".ludic"
}
# route one subcommand to its implementation and return its exit code; -1 when
# the name is unknown. Returning (rather than exiting in place) lets entry tear
# down the per-process scratch directory before the process ends.
function dispatch(cmd: pointer) -> int {
if (cmd == "new") { return cmd_new() }
if (cmd == "build") { return cmd_build() }
if (cmd == "run") { return cmd_run() }
if (cmd == "test") { return cmd_test() }
if (cmd == "deps") { return cmd_deps() }
if (cmd == "schema") { return cmd_schema() }
if (cmd == "migrate") { return cmd_migrate() }
if (cmd == "clean") { return cmd_clean() }
if (cmd == "fmt") { return cmd_fmt() }
if (cmd == "lsp") { return cmd_lsp() }
if (cmd == "ui-preview") { return cmd_ui_preview() }
if (cmd == "doctor") { return cmd_doctor() }
if (cmd == "upgrade") { return cmd_upgrade() }
if (cmd == "script") { return cmd_script() }
if (cmd == "scripts") { return cmd_scripts() }
if (cmd == "add") { return cmd_pkg_add() }
if (cmd == "get") { return cmd_pkg_get() }
if (cmd == "remove") { return cmd_pkg_remove() }
if (cmd == "update") { return cmd_pkg_update() }
if (cmd == "verify") { return cmd_pkg_verify() }
if (cmd == "vendor") { return cmd_pkg_vendor() }
if (cmd == "assets") { return cmd_fetch_assets() }
if (cmd == "pack") { return cmd_pack() }
if (cmd == "bundle") { return cmd_bundle() }
if (cmd == "build-lib") { return cmd_pkg_build_lib() }
if (cmd == "link-flags") { return cmd_pkg_link_flags() }
if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { return cmd_version() }
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 }
# The toolchain's own tasks used to live here under `ludic dev`. Say where
# they went rather than printing "unknown command" at someone who read an
# older README or a commit message.
if (cmd == "dev") {
err("ludic: the toolchain's own tasks live in `ludic-dev`, which is built from a\n")
err(" checkout of the Ludic repository and is not part of an install:\n")
err(" clang selfhost/ludicc.seed.ll -o bin/ludicc\n")
err(" bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev && bin/ludic-dev help\n")
return 1
}
return -1
}
entry {
if (arg_count() < 2) { usage(); exit(1) }
let cmd = arg(1)
# `ludic mygame.ludic` — build and run that file
if is_ludic_file(cmd) {
let rc = cmd_run_file()
tmp_cleanup()
exit(rc)
}
# package.ludic hooks wrap the built-in commands: `before` can veto,
# `after` runs only when the command succeeded
if is_hookable(cmd) {
let pre = run_hooks("before", cmd)
if pre != 0 { tmp_cleanup(); exit(pre) }
}
var code = dispatch(cmd)
if code == 0 and is_hookable(cmd) { code = run_hooks("after", cmd) }
# an unknown command may be one of the package's scripts
if code == -1 { code = run_script(cmd, 2) }
tmp_cleanup()
if code >= 0 { exit(code) }
err(`ludic: unknown command '{cmd}'\n`)
usage()
exit(1)
}
}