The ckext command line
tools/ckext.mjs packs a module folder into a .ckext and tests it with no
browser: the same MicroPython runtime and the same cufsm-rs engine as the app's sandbox, under Node. It is the loop an
author, a reviewer or a CI job runs.
Setting up#
You need Node 20 or later and a checkout of CivilKit Buckling's source. The tool has no dependencies of its own beyond
what is committed (the engine cufsm.wasm and vendor/civilkit):
cd civilkit-buckling
node tools/ckext.mjs test extensions/dsm-compression
npm run ckext -- test … runs the same. Paths are relative to where you run it.
pack#
node tools/ckext.mjs pack <folder> [-o out.ckext]
Zips manifest.json, main.py and everything under tests/ from the folder, checks
the result exactly as the app's installer does (the manifest's required fields, a semantic version, a
buckling.tool contribution, a main.py), and writes it. With no -o the file is named
after the folder, in the current directory.
$ node tools/ckext.mjs pack squash-load -o squash-load.ckext
packed yourname.squash-load 1.0.0 -> squash-load.ckext (1467 bytes)
A bundle that would not install is not written; the error names why (see install errors). Other files in the folder, such as a README, are not packed.
test#
node tools/ckext.mjs test <file.ckext | folder> [--inputs '{"fy":450}'] [--model model.json] [--json]
It does four things, in order:
- runs every worked example, which decides the badge;
- calls
build_ui({}, [], [])and lists the form's fields and buttons; - runs
checkonce: on the first example's inputs and fixture by default, on--inputsif given, against the model in--modelif given (a JSON file in the model shape); - reports which capabilities were declared, which were used, and any called without being declared.
A folder is packed in memory first, so testing a folder and testing its packed file give the same answer.
$ node tools/ckext.mjs test squash-load
verified
Squash load 1.0.0 (yourname.squash-load)
worked examples: 1 of 1 pass
pass lipped-c-150-60-40
form: fy; buttons: Run
run on {"fy":450} (the first worked example and its fixture):
Py = 236.25 kN
Squash load: Py = A fy = 236.25 kN
capabilities: declared firstYield; used firstYield
The run used the first worked example and its fixture, which is never solved, so hasResults is false and getResults is empty. Pass --model (and --inputs) to run on another model.
The first line is the badge. A failing example prints FAIL and its diffs under it; a run that stops
prints error: and the message (a Python error with its main.py line); calc lines are indented
under the results, with their clause in brackets; proposals print as proposal: 25 nodes, 24 elements or
proposal: refused: ‹reason›; civilkit.log messages as log:; and anything the module
printed under stdout:. Numbers are shown to 12 significant figures.
--json#
Prints the same report as JSON, the shape CivilKit Studio's run_extension returns:
{
"id": "yourname.squash-load",
"name": "Squash load",
"version": "1.0.0",
"badge": "verified",
"examples": [
{
"id": "lipped-c-150-60-40",
"pass": true,
"diffs": [],
"source": "Hand calc: lipped C 150 x 60 x 40 mm (centreline), t = 1.5 mm, sharp corners. A = (150 + 2 x 60 + 2 x 40) x 1.5 = 525 mm2; Py = A fy = 525 x 450 = 236 250 N = 236.25 kN."
}
],
"form": {
"title": "Squash load",
"fields": [
{
"id": "fy",
"label": "fy (MPa)",
"inputType": "number",
"default": 450
}
],
"buttons": [
{
"id": "run",
"label": "Run",
"action": "run-check"
}
]
},
"run": {
"inputs": {
"fy": 450
},
"fixture": true,
"result": [
{
"unit": "kN",
"label": "Py",
"value": 236.25
}
],
"calcLines": [
{
"eq": "Py = A fy = 236.25 kN",
"label": "Squash load"
}
],
"proposals": [],
"logs": [],
"error": null
},
"capabilities": {
"declared": [
"firstYield"
],
"used": [
"firstYield"
],
"undeclaredCalls": [],
"unusedInThisRun": []
},
"note": "The run used the first worked example and its fixture, which is never solved, so hasResults is false and getResults is empty. Pass --model (and --inputs) to run on another model."
}
| Field | Meaning |
|---|---|
badge | "verified" or "community". |
examples[] | Each example's id, pass, diffs and source. |
form | The title, fields (id, label, inputType, default) and buttons; {"error": …} when build_ui failed. |
run | The one check: its inputs, whether it ran on the first example's fixture, and its result, calcLines, proposals, logs and error. |
capabilities | declared; used (anywhere: examples and run); undeclaredCalls; unusedInThisRun, the declared ones the single run did not call. |
note | Set when the run used a fixture, which is never solved, so the results capabilities show unused there. |
Exit codes#
| Code | When |
|---|---|
0 | Verified, and no capability was called without being declared. |
1 | Community, an undeclared call, or a bundle that cannot be read. |
2 | A usage error: no command, an unknown option, --inputs that is not JSON. The usage is printed. |
So node tools/ckext.mjs test my-module && echo ok is a complete CI gate.
The loop#
- Write the three files, with at least one worked example and its cited source.
node tools/ckext.mjs test my-module, and read the diffs and the error.- Fix, and repeat until the first line is
verifiedand nothing is listed undercalled without declaring. - Run it on another section:
--modelwith a model JSON (copy one out of a fixture), and--inputs. packit, install it in the app, and hand it to a reviewer with its README.
The same loop works for an AI agent: give it this guide, ask for a module, and run test on what it
writes.
Building the store#
node tools/build-store.mjs
Packs every folder under extensions/ into store/<id>.ckext, tests each with the command
above, and writes store/index.json ([{ "id", "name", "version", "description", "url" }]) listing
only the Verified ones. A module that is not Verified is left out and named, and the script exits 1:
store: 3 module(s): civilkit.dsm-compression 1.0.0, civilkit.section-rounded-cz 1.0.0, civilkit.thickness-study 1.0.0
Re-run it after changing a module, and commit store/ with the change. The app's Extensions list reads
store/index.json from the site, so the store is whatever was committed and deployed.