refactor(cli)!: split the contributor tool out of the ludic CLI

`ludic help` ended with a section titled "contributing to the toolchain itself",
listing bootstrap, reseed, docs-gen and release tasks. None of that is available
to someone who installed the language — those tasks need the repository — so the
shipped tool was advertising work its user cannot do, in a namespace they have to
read past to find `new` and `run`.

The tasks move to a second program, dev.ludic -> bin/ludic-dev, built from a
checkout and excluded from every release artifact. `ludic` keeps the project and
package commands and nothing else; `ludic dev …` now explains where the tasks
went instead of failing as an unknown command.

What this shook out: the two programs share prelude/build/project/pkg, so the
helpers each had accreted in whichever file first needed them — cc(),
ensure_ludicc, the string functions, title_case, cmd_version — moved to where
both can see them. The argument-shift indirection added for the `dev` namespace
is gone with the namespace, so commands read argv directly again.

`ludic-dev test` asserts the split rather than trusting it: the staged install
must build a project, and `ludic dev build` there must fail while naming
ludic-dev. install.sh keeps building older tags, whose bootstrap goes through
main.ludic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-05 23:15:12 +03:00
parent f369fbd227
commit e175619543
46 changed files with 630 additions and 558 deletions

View file

@ -131,11 +131,11 @@ function template_readme(name: pointer) -> pointer {
# ludic new <name> — scaffold a project that builds and runs as it stands.
function cmd_new() -> int {
if arg_total() < 3 {
if arg_count() < 3 {
err("usage: ludic new <name>\n")
return 1
}
let name = arg_n(2)
let name = arg(2)
if file_exists(name) {
err(`ludic new: {name} already exists\n`)
return 1
@ -173,12 +173,12 @@ function parse_build_args(start: int) -> pointer {
g_out = ""
g_save = false
var ai = start
while ai < arg_total() {
let a = arg_n(ai)
while ai < arg_count() {
let a = arg(ai)
if a == "--headless" { g_mode = 2 }
else if a == "--windowed" { g_mode = 1 }
else if a == "--save-temps" { g_save = true }
else if a == "-o" { ai += 1; if ai < arg_total() { g_out = arg_n(ai) } }
else if a == "-o" { ai += 1; if ai < arg_count() { g_out = arg(ai) } }
else if a[0] != '-' { src = a }
ai += 1
}
@ -245,7 +245,7 @@ function test_files() -> []pointer {
function cmd_test() -> int {
var files = new []pointer
var ai = 2
while ai < arg_total() { if arg_n(ai)[0] != '-' { push(files, arg_n(ai)) }; ai += 1 }
while ai < arg_count() { if arg(ai)[0] != '-' { push(files, arg(ai)) }; ai += 1 }
if len(files) == 0 { files = test_files() }
if len(files) == 0 {
err("ludic test: no tests found (expected tests/*.ludic or src/**/*_test.ludic)\n")
@ -298,7 +298,7 @@ function strip_ext(p: pointer) -> pointer {
function cmd_fmt() -> int {
var args = ""
var ai = 2
while ai < arg_total() { args = `{args} {arg_n(ai)}`; ai += 1 }
while ai < arg_count() { args = `{args} {arg(ai)}`; ai += 1 }
if args == "" {
let found = capture_line("find src tests -name '*.ludic' 2>/dev/null | sort")
if found == "" {
@ -315,7 +315,7 @@ function cmd_fmt() -> int {
function cmd_lsp() -> int {
var args = ""
var ai = 2
while ai < arg_total() { args = `{args} {arg_n(ai)}`; ai += 1 }
while ai < arg_count() { args = `{args} {arg(ai)}`; ai += 1 }
return sh(`exec {tool("ludic-lsp")}{args}`)
}
@ -377,6 +377,25 @@ function cmd_doctor() -> int {
return 1
}
# ludic version — report the version of the toolchain this CLI belongs to.
#
# It asks the compiler (proving that path works), addressed through ludic_home()
# rather than as a relative bin/ludicc: `ludic version` is run from a user's
# project, not from the toolchain's own directory, and looking beside the current
# directory found nothing there and reported "(version unknown)" for a perfectly
# good install.
function cmd_version() -> int {
let cc = ludicc()
if is_exec(cc) {
run(`{cc} --version`)
return 0
}
let v = read_file(`{ludic_home()}VERSION`)
if v != null { print(`ludic {s_trim(v)}`); return 0 }
print("ludic (version unknown)")
return 0
}
# ---- ludic upgrade ----------------------------------------------------------
function install_url() -> pointer { return getenv_or("LUDIC_INSTALL_URL", "https://workshopsoft.pages.workshopsoft.io/ludic/install.sh") }
@ -386,7 +405,7 @@ function install_url() -> pointer { return getenv_or("LUDIC_INSTALL_URL", "https
# drift from a fresh install.
function cmd_upgrade() -> int {
var ver = ""
if arg_total() >= 3 { ver = arg_n(2) }
if arg_count() >= 3 { ver = arg(2) }
if not shq("command -v curl >/dev/null 2>&1") {
err("ludic upgrade: needs curl\n")
return 1