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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-30 00:24:49 +03:00
parent 047bdf4189
commit 320ce42626
10 changed files with 341 additions and 10 deletions

View file

@ -144,6 +144,7 @@ cached in a content-addressed store:
```bash ```bash
ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/ ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/
ludic get # install from package.ludic, write the lock ludic get # install from package.ludic, write the lock
ludic remove git.workshopsoft.io/user/pkg # the inverse of add
ludic verify # check locked packages against the store ludic verify # check locked packages against the store
``` ```

View file

@ -0,0 +1,10 @@
bump: minor
type: feat
**`ludic remove <module>` and `ludic get --json`.** `remove` is the inverse of `add`: the `require`
leaves `package.ludic`, the lock keeps what the remaining requires still reach (read from the store's
copies of each package's manifest, no network), and each package leaving the lock loses the
`ludic_modules/` link `add` made - never the shared store entry. A module that is not required is
refused; source still importing a removed package is a warning. `ludic get --json` prints what the run
changed in the lock as one JSON object on stdout - `{"added", "removed", "changed", "unchanged"}`, an
entry as the lock records it (`name`, `version`, `hash`, `kind`, `provides`), a change as `{name, from,
to, from_hash, to_hash}` - with the resolver's own lines on stderr.

View file

@ -19,7 +19,8 @@ This is the v1 implementation of the direction decided in issue #63.
``` ```
ludic add <module>[@version] add a dependency to package.ludic, then resolve + fetch + link ludic add <module>[@version] add a dependency to package.ludic, then resolve + fetch + link
ludic get resolve every dependency in package.ludic, link them, write the lock ludic remove <module> the inverse of add: drop the require, what only it locked, its links
ludic get [--json] resolve every dependency in package.ludic, link them, write the lock
ludic update [module] bump a dependency (or all) to its latest published version, then relock ludic update [module] bump a dependency (or all) to its latest published version, then relock
ludic verify check every locked package against the store by content hash ludic verify check every locked package against the store by content hash
ludic vendor copy the resolved packages into ./vendor for hermetic/offline builds ludic vendor copy the resolved packages into ./vendor for hermetic/offline builds
@ -29,6 +30,23 @@ ludic vendor copy the resolved packages into ./vendor for herm
minimum. All the install commands print the resolved build list and write minimum. All the install commands print the resolved build list and write
`package.lock.ludic`. `package.lock.ludic`.
`ludic get --json` prints what the run changed in the lock as one JSON object on stdout (the
resolver's lines go to stderr): `{"added": [...], "removed": [...], "changed": [...], "unchanged": n}`,
an added or removed entry being the lock's line - `{"name", "version", "hash", "kind", "provides"}` -
and a changed one `{"name", "from", "to", "from_hash", "to_hash"}` (the versions, and the content
hashes beside them). The lock is compared before and after, in memory.
`ludic remove <module>` takes the `require` line out of `package.ludic` and re-reads the dependency
graph from the store's own copy of each locked package's manifest - no network - so the lock keeps
exactly what the remaining requires still reach: a package another one still requires stays locked
(at the version it had; `ludic get` settles versions), and what only the removed one brought in
leaves the lock with it. Each package leaving the lock loses its `ludic_modules/` link - the link
`add` made, never the store entry it points at, which other projects share; anything there that is
not such a link is left and reported, as is a copy under `vendor/`. A module `package.ludic` does
not require is refused (exit 1). Source that still imports a removed package is a warning, not a
failure - the caller may be about to delete it. If the store has no copy of a locked package, only
the named module leaves the lock, and it says so.
## The manifest — `package.ludic` ## The manifest — `package.ludic`
A line-oriented manifest. `#` starts a comment; strings are double-quoted. A line-oriented manifest. `#` starts a comment; strings are double-quoted.

View file

@ -39,6 +39,7 @@ program LudicDev {
import "assets.ludic" import "assets.ludic"
import "release.ludic" import "release.ludic"
import "pkg.ludic" import "pkg.ludic"
import "pkg_lock.ludic"
import "pkg_test.ludic" import "pkg_test.ludic"
function usage() -> void { function usage() -> void {

View file

@ -25,6 +25,8 @@ program Ludic {
import "split.ludic" import "split.ludic"
import "project.ludic" import "project.ludic"
import "pkg.ludic" import "pkg.ludic"
import "pkg_lock.ludic"
import "pkg_remove.ludic"
import "assets.ludic" import "assets.ludic"
import "packignore.ludic" import "packignore.ludic"
import "pack.ludic" import "pack.ludic"
@ -59,7 +61,9 @@ program Ludic {
print("") print("")
print("packages:") print("packages:")
print(" add <module>[@version] add a dependency, then resolve + fetch + link") print(" add <module>[@version] add a dependency, then resolve + fetch + link")
print(" get resolve every package.ludic dependency and write the lock") 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(" 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(" verify check every locked package against the store by content hash")
print(" vendor copy the resolved packages into ./vendor for offline builds") print(" vendor copy the resolved packages into ./vendor for offline builds")
@ -124,6 +128,7 @@ program Ludic {
if (cmd == "add") { return cmd_pkg_add() } if (cmd == "add") { return cmd_pkg_add() }
if (cmd == "get") { return cmd_pkg_get() } if (cmd == "get") { return cmd_pkg_get() }
if (cmd == "remove") { return cmd_pkg_remove() }
if (cmd == "update") { return cmd_pkg_update() } if (cmd == "update") { return cmd_pkg_update() }
if (cmd == "verify") { return cmd_pkg_verify() } if (cmd == "verify") { return cmd_pkg_verify() }
if (cmd == "vendor") { return cmd_pkg_vendor() } if (cmd == "vendor") { return cmd_pkg_vendor() }

View file

@ -1,5 +1,5 @@
# pkg.ludic — the Ludic package manager (issue #63), the `ludic add`/`get`/ # pkg.ludic — the Ludic package manager (issue #63), the `ludic add`/`get`/
# `update`/`verify`/`vendor` commands. # `update`/`verify`/`vendor` commands (`remove` and `get --json`: pkg_remove.ludic, pkg_lock.ludic).
# #
# It realises the v1 direction decided in the RFC: # It realises the v1 direction decided in the RFC:
# #
@ -484,7 +484,7 @@ function write_lock(sels: []Manifest) -> bool {
function do_install(root: Manifest) -> int { function do_install(root: Manifest) -> int {
let sels = resolve(root) let sels = resolve(root)
if len(sels) == 0 { if len(sels) == 0 {
print("no dependencies to resolve") pkg_say("no dependencies to resolve")
write_lock(sels) write_lock(sels)
return 0 return 0
} }
@ -505,11 +505,11 @@ function do_install(root: Manifest) -> int {
if slen(h) == 0 { err(`ludic: failed to snapshot {m.module}@{m.ver}\n`); return 1 } if slen(h) == 0 { err(`ludic: failed to snapshot {m.module}@{m.ver}\n`); return 1 }
m.hash = `sha256:{h}` m.hash = `sha256:{h}`
link_module(m.module, h) link_module(m.module, h)
print(` {m.module} {m.ver} ({m.kind}) sha256:{sslice(h, 0, 12)}…`) pkg_say(` {m.module} {m.ver} ({m.kind}) sha256:{sslice(h, 0, 12)}…`)
i += 1 i += 1
} }
if not write_lock(sels) { err("ludic: cannot write package.lock.ludic\n"); return 1 } if not write_lock(sels) { err("ludic: cannot write package.lock.ludic\n"); return 1 }
print(`resolved {string(len(sels))} package(s) — see package.lock.ludic; linked under ludic_modules/`) pkg_say(`resolved {string(len(sels))} package(s) — see package.lock.ludic; linked under ludic_modules/`)
return 0 return 0
} }
@ -596,12 +596,19 @@ function cmd_pkg_add() -> int {
return do_install(read_root_manifest()) return do_install(read_root_manifest())
} }
# ludic get — resolve + fetch + link every dependency in package.ludic, write lock # ludic get [--json] — resolve + fetch + link every dependency in package.ludic, write lock. --json
# says what changed in the lock as one JSON object on stdout (lock_diff_json), the rest on stderr.
function cmd_pkg_get() -> int { function cmd_pkg_get() -> int {
let txt = read_file("package.ludic") let txt = read_file("package.ludic")
if txt == null { err("ludic: no package.ludic in the current directory (ludic add <module> to start one)\n"); return 1 } if txt == null { err("ludic: no package.ludic in the current directory (ludic add <module> to start one)\n"); return 1 }
print("resolving dependencies (MVS)…") let json = pkg_has_flag("--json")
return do_install(parse_manifest(txt)) let before = read_lock_or_empty()
g_pkg_err = json
pkg_say("resolving dependencies (MVS)…")
let rc = do_install(parse_manifest(txt))
if rc != 0 or not json { return rc }
out(lock_diff_json(before, read_lock_or_empty()))
return 0
} }
# ludic update [module] — bump a dep (or all) to its latest published version, relock # ludic update [module] — bump a dep (or all) to its latest published version, relock

View file

@ -0,0 +1,116 @@
# pkg_lock.ludic — the package manager's answers for editors and tools: `ludic get --json` (what a
# get changed in package.lock.ludic, as one JSON object on stdout) and `ludic remove <module>`, the
# inverse of `ludic add`. Neither touches the network: the diff is of the lock before and after, and
# a removal re-reads the dependency graph from the store's own copies of each package.ludic.
# the human lines of add / get / update: on stdout, or on stderr under --json
var g_pkg_err: bool = false
function pkg_say(line: pointer) -> void {
if g_pkg_err { err(line + nl()) } else { print(line) }
}
function pkg_has_flag(flag: pointer) -> bool {
var ai = 2
while ai < arg_count() {
if arg(ai) == flag { return true }
ai += 1
}
return false
}
# ---- JSON out --------------------------------------------------------------------
function pkg_hex(d: int) -> pointer {
let b = bytes(2)
b[0] = '0' + d
if d > 9 { b[0] = 'a' + d - 10 }
b[1] = 0
return b
}
function pkg_jesc(c: int) -> pointer {
if c == '"' { return "\\\"" }
if c == 92 { return "\\\\" }
if c == '\n' { return "\\n" }
if c == '\t' { return "\\t" }
if c == '\r' { return "\\r" }
return "\\u00" + pkg_hex(c >> 4) + pkg_hex(c & 15)
}
# `s` as a JSON string, quotes included
function pkg_jq(s: pointer) -> pointer {
var o = "\""
let n = slen(s)
var start = 0
var i = 0
while i < n {
let c = s[i] & 255
if c == '"' or c == 92 or c < 32 {
o = o + sslice(s, start, i) + pkg_jesc(c)
start = i + 1
}
i += 1
}
return o + sslice(s, start, n) + "\""
}
# a lock line as the lock records it
function lock_entry_json(m: Manifest) -> pointer {
var o = `{{"name": {pkg_jq(m.module)}, "version": {pkg_jq(m.ver)}, "hash": {pkg_jq(m.hash)}, "kind": {pkg_jq(m.kind)}, "provides": [`
var p = 0
while p < len(m.provides) {
if p > 0 { o = o + ", " }
o = o + pkg_jq(m.provides[p])
p += 1
}
return o + "]}"
}
function lock_index(ms: []Manifest, module: pointer) -> int {
var i = 0
while i < len(ms) {
if ms[i].module == module { return i }
i += 1
}
return -1
}
function json_list(items: []pointer) -> pointer {
if len(items) == 0 { return "[]" }
var o = "["
var i = 0
while i < len(items) {
if i > 0 { o = o + "," }
o = o + nl() + " " + items[i]
i += 1
}
return o + nl() + " ]"
}
# what changed between two locks: {"added", "removed", "changed", "unchanged"}. A module whose version
# or content hash moved is changed; `from` / `to` are its versions, the hashes beside them.
function lock_diff_json(before: []Manifest, after: []Manifest) -> pointer {
let added = new []pointer
let removed = new []pointer
let changed = new []pointer
var same = 0
var i = 0
while i < len(after) {
let m = after[i]
let at = lock_index(before, m.module)
if at < 0 { push(added, lock_entry_json(m)) }
else {
let o = before[at]
if o.ver == m.ver and o.hash == m.hash { same += 1 }
else { push(changed, `{{"name": {pkg_jq(m.module)}, "from": {pkg_jq(o.ver)}, "to": {pkg_jq(m.ver)}, "from_hash": {pkg_jq(o.hash)}, "to_hash": {pkg_jq(m.hash)}}}`) }
}
i += 1
}
i = 0
while i < len(before) {
if lock_index(after, before[i].module) < 0 { push(removed, lock_entry_json(before[i])) }
i += 1
}
var o = "{" + nl() + ` "added": {json_list(added)},` + nl()
o = o + ` "removed": {json_list(removed)},` + nl()
o = o + ` "changed": {json_list(changed)},` + nl()
return o + ` "unchanged": {string(same)}` + nl() + "}" + nl()
}
function read_lock_or_empty() -> []Manifest {
let t = read_file("package.lock.ludic")
if t == null { return new []Manifest }
return parse_lock(t)
}

View file

@ -0,0 +1,134 @@
# pkg_remove.ludic — `ludic remove <module>`, the inverse of `ludic add`: the `require` line leaves
# package.ludic, and the lock keeps exactly what the remaining requires still reach. The graph is
# read from the store's copies of each locked package.ludic, so nothing is fetched; what the removal
# leaves unreachable leaves the lock and its ludic_modules/ link (the link `add` made, never the store
# entry it points at, which other projects share). Source still importing a removed package is a
# warning: the caller may be about to delete it.
# package.ludic's text with every `require "<module>" ...` line dropped
function drop_require(txt: pointer, module: pointer) -> pointer {
var out = ""
let n = slen(txt)
var i = 0
while i < n {
let line = line_at(txt, i)
i = i + slen(line) + 1
let ts = tok_line(line)
if not (len(ts) >= 2 and ts[0] == "require" and ts[1] == module) { out = out + line + nl() }
}
return out
}
function requires(m: Manifest, module: pointer) -> bool {
var i = 0
while i < len(m.deps) {
if m.deps[i].module == module { return true }
i += 1
}
return false
}
var g_graph_unknown: pointer = "" # a locked package the store has no copy of, when there is one
# the locked entries the root's requires still reach, through the store's package.ludic files
function lock_reachable(root: Manifest, locked: []Manifest) -> []Manifest {
let seen = new []pointer
let todo = new []pointer
var d = 0
while d < len(root.deps) {
push(todo, root.deps[d].module)
d += 1
}
while len(todo) > 0 {
let m = List.pop(todo)
let at = lock_index(locked, m)
if find_mod(seen, m) < 0 and at >= 0 {
push(seen, m)
let dir = `{store_root()}{strip_prefix(locked[at].hash, "sha256:")}`
if not shq(`test -d "{dir}"`) { g_graph_unknown = m }
else {
let mt = read_file(`{dir}/package.ludic`)
if mt != null {
let dm = parse_manifest(mt)
var k = 0
while k < len(dm.deps) {
push(todo, dm.deps[k].module)
k += 1
}
}
}
}
}
let keep = new []Manifest
var i = 0
while i < len(locked) {
if find_mod(seen, locked[i].module) >= 0 { push(keep, locked[i]) }
i += 1
}
return keep
}
# take down the ludic_modules/ view `add` linked, and the directories it leaves empty
function unlink_module(module: pointer) -> void {
let link = `ludic_modules/{module}`
if shq(`test -L "{link}"`) {
shell(`rm -f "{link}"; d=$(dirname "{link}"); while [ "$d" != ludic_modules ] && [ "$d" != . ] && rmdir "$d" 2>/dev/null; do d=$(dirname "$d"); done; rmdir ludic_modules 2>/dev/null`)
} else if shq(`test -e "{link}"`) {
err(`ludic remove: warning: {link} is not a link 'ludic add' made; left as it is\n`)
}
if shq(`test -e "vendor/{module}"`) { err(`ludic remove: vendor/{module} is still there (run 'ludic vendor' again)\n`) }
}
# the project's own files that still import `module`
function warn_importers(module: pointer) -> void {
let hits = split_lines(capture(`grep -rlF --include='*.ludic' --exclude-dir=ludic_modules --exclude-dir=vendor --exclude-dir=build --exclude-dir=.git -e 'import "{module}/' -e 'import "{module}"' . | sed 's|^\\./||' | LC_ALL=C sort`))
if len(hits) == 0 { return }
err(`ludic remove: warning: {module} is still imported by:\n`)
var i = 0
while i < len(hits) {
err(` {hits[i]}\n`)
i += 1
}
}
# ludic remove <module> — drop a dependency: the require, what only it brought into the lock, the links
function cmd_pkg_remove() -> int {
if arg_count() < 3 { err("usage: ludic remove <module>\n"); return 1 }
let module = split_spec(arg(2))[0]
let txt = read_file("package.ludic")
if txt == null { err("ludic remove: no package.ludic in the current directory\n"); return 1 }
let locked = read_lock_or_empty()
if not requires(parse_manifest(txt), module) {
var why = ""
if lock_index(locked, module) >= 0 { why = " (the lock has it because another package requires it)" }
err(`ludic remove: package.ludic does not require {module}{why}\n`)
return 1
}
if not write_file("package.ludic", drop_require(txt, module)) { err("ludic remove: cannot write package.ludic\n"); return 1 }
print(`ludic remove: {module} is no longer required`)
if file_exists("package.lock.ludic") {
var keep = lock_reachable(read_root_manifest(), locked)
if g_graph_unknown != "" {
err(`ludic remove: the store has no copy of {g_graph_unknown}, so only {module} leaves the lock; 'ludic get' settles the rest\n`)
keep = new []Manifest
var j = 0
while j < len(locked) {
if not (locked[j].module == module) { push(keep, locked[j]) }
j += 1
}
}
if not write_lock(keep) { err("ludic remove: cannot write package.lock.ludic\n"); return 1 }
var i = 0
while i < len(locked) {
let m = locked[i]
if lock_index(keep, m.module) < 0 {
unlink_module(m.module)
print(` removed {m.module} {m.ver}`)
if not (m.module == module) { warn_importers(m.module) }
}
i += 1
}
if lock_index(keep, module) >= 0 { print(` {module} stays locked: another package still requires it`) }
}
warn_importers(module)
return 0
}

View file

@ -12,6 +12,13 @@ function pt_write(path: pointer, body: pointer) -> void {
write_file(path, body) write_file(path, body)
} }
# a file's text, "" when it is not there
function pt_read(path: pointer) -> pointer {
let t = read_file(path)
if t == null { return "" }
return t
}
# commit the current tree of `dir` and tag it `tag` # commit the current tree of `dir` and tag it `tag`
function pt_commit_tag(dir: pointer, tag: pointer) -> void { function pt_commit_tag(dir: pointer, tag: pointer) -> void {
shell(`git -C {dir} add -A`) shell(`git -C {dir} add -A`)
@ -170,6 +177,38 @@ function cmd_test_pkg() -> int {
ok("ludic vendor copies the resolved packages into ./vendor") ok("ludic vendor copies the resolved packages into ./vendor")
} else { bad2("ludic vendor failed", capture_line(`tail -1 {work}/vendor.out`)) } } else { bad2("ludic vendor failed", capture_line(`tail -1 {work}/vendor.out`)) }
# ---- for editors (R9): get --json says what changed in the lock; remove undoes add ----
let proj5 = `{work}/proj5`
pt_write(`{proj5}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl())
pt_write(`{proj5}/src/app.ludic`, "program App {\n import \"example.test/greeter/greet.ludic\"\n}\n")
shell(`( cd {proj5} && {envp} {root}/bin/ludic get --json > {work}/gj.json 2> {work}/gj.err )`)
let gj = pt_read(`{work}/gj.json`)
if s_contains(gj, `"name": "example.test/greeter", "version": "1.0.0"`) and s_contains(gj, `"name": "example.test/util", "version": "1.0.0"`) and s_contains(gj, `"unchanged": 0`) and shq(`python3 -m json.tool {work}/gj.json > /dev/null`) and not s_contains(gj, "resolving") {
ok("ludic get --json: a fresh lock is two added packages, one JSON object on stdout")
} else { bad2("ludic get --json (fresh)", gj) }
pt_write(`{proj5}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl() + `require "example.test/util" "1.2.0"` + nl())
let gj2 = capture(`( cd {proj5} && {envp} {root}/bin/ludic get --json 2>/dev/null )`)
if s_contains(gj2, `"added": []`) and s_contains(gj2, `"name": "example.test/util", "from": "1.0.0", "to": "1.2.0"`) and s_contains(gj2, `"unchanged": 1`) {
ok("ludic get --json: a raised minimum is a change from one version to the other")
} else { bad2("ludic get --json (changed)", gj2) }
if not shq(`( cd {proj5} && {root}/bin/ludic remove example.test/nothere > {work}/rm0.out 2>&1 )`) and shq(`grep -q 'does not require' {work}/rm0.out`) {
ok("ludic remove refuses a module package.ludic does not require")
} else { bad2("ludic remove of an unrequired module", capture_line(`cat {work}/rm0.out`)) }
let store_n = capture_line(`ls {store} | wc -l`)
shell(`( cd {proj5} && LUDIC_STORE={store} {root}/bin/ludic remove example.test/util > {work}/rm1.out 2>&1 )`)
let lk1 = pt_read(`{proj5}/package.lock.ludic`)
if not s_contains(pt_read(`{proj5}/package.ludic`), "example.test/util") and s_contains(lk1, "example.test/util") and file_exists(`{proj5}/ludic_modules/example.test/util`) {
ok("ludic remove keeps what another package still requires locked and linked")
} else { bad2("ludic remove dropped a package still required", capture_line(`cat {work}/rm1.out`)) }
shell(`( cd {proj5} && LUDIC_STORE={store} {root}/bin/ludic remove example.test/greeter > {work}/rm2.out 2>&1 )`)
let lk2 = pt_read(`{proj5}/package.lock.ludic`)
if not s_contains(lk2, "example.test/") and not file_exists(`{proj5}/ludic_modules`) and capture_line(`ls {store} | wc -l`) == store_n {
ok("ludic remove drops the require, what only it locked and its links, and leaves the shared store alone")
} else { bad2("ludic remove of the last require", capture_line(`cat {work}/rm2.out`)) }
if shq(`grep -q 'still imported by' {work}/rm2.out`) and shq(`grep -q 'src/app.ludic' {work}/rm2.out`) {
ok("ludic remove warns about source still importing what it removed")
} else { bad("ludic remove said nothing about src/app.ludic importing the package") }
# ---- package-declarable engine-system + namespace (issue #62) ------------- # ---- package-declarable engine-system + namespace (issue #62) -------------
# A source package registers a compile-time engine system (@EngineSystem) and a # A source package registers a compile-time engine system (@EngineSystem) and a
# Foo.* namespace (@Namespace) with no compiler edit; a consumer game imports it # Foo.* namespace (@Namespace) with no compiler edit; a consumer game imports it

View file

@ -12,7 +12,7 @@
# the built-in commands a hook can wrap (every user command that does work) # the built-in commands a hook can wrap (every user command that does work)
function is_hookable(cmd: pointer) -> bool { function is_hookable(cmd: pointer) -> bool {
return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "deps") or (cmd == "schema") or (cmd == "migrate") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib") return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "deps") or (cmd == "schema") or (cmd == "migrate") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "remove") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib")
} }
# the directory this `ludic` binary lives in, with no trailing slash # the directory this `ludic` binary lives in, with no trailing slash