forma-dsl-cli 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +74 -9
  2. package/package.json +2 -2
  3. package/src/cli.js +43 -1
package/README.md CHANGED
@@ -36,12 +36,74 @@ tree - is refused with exit code 2.
36
36
  | `-m, --model <name>` | which model to build. Defaults to the first declared |
37
37
  | `--part <name>` | one part by name. Defaults to the only part, or every part unioned |
38
38
  | `-p, --param <name=val>` | a value for one declared param. Repeatable |
39
+ | `--params <file>` | a JSON object of values by name. `-p` wins over it |
39
40
  | `--segments <n>` | a fixed segment count for every curve |
40
41
  | `--min-angle <deg>` | smallest angle between adjacent segments of a curve |
41
42
  | `--min-edge <n>` | smallest length a curve segment may be shortened to |
42
43
  | `--max-nodes <n>` | refuse a document that would build more blocks than this |
43
44
  | `-q, --quiet` | do not print the summary |
44
45
 
46
+ ## Passing parameters
47
+
48
+ Three ways, and they compose.
49
+
50
+ **One at a time, on the command line.** A value is JSON where it parses as JSON, and the
51
+ string as written otherwise - so there is no second layer of quoting to get wrong:
52
+
53
+ ```bash
54
+ forma render part.forma -p count=6 -p center=true -p size=[10,20] -p label=M6
55
+ ```
56
+
57
+ | Written | Arrives as |
58
+ | --- | --- |
59
+ | `-p count=6` | the number `6` |
60
+ | `-p center=true` | the boolean `true` |
61
+ | `-p size=[10,20]` | the list `[10, 20]` |
62
+ | `-p label=M6` | the string `"M6"` |
63
+
64
+ **From a file.** `--params` takes a JSON object of values by name - the thing you commit and
65
+ diff, rather than assemble in a shell loop:
66
+
67
+ ```json
68
+ {
69
+ "height": 36,
70
+ "label": "M6",
71
+ "center": true
72
+ }
73
+ ```
74
+
75
+ ```bash
76
+ forma render riser.forma --params params.json -o riser.stl
77
+ ```
78
+
79
+ It has to be a JSON **object**. An array or a bare number parses but names nothing, and is
80
+ refused with exit code 2 rather than quietly contributing nothing. Like every other path, it
81
+ must resolve inside the directory the command runs in.
82
+
83
+ **Both.** `-p` wins over `--params`, which is what a build matrix wants: the values every
84
+ variant shares live in the file, and the one that varies goes on the line.
85
+
86
+ ```bash
87
+ for h in 8 20 40; do
88
+ forma render riser.forma --params shared.json -p "height=$h" -o "build/riser-$h.stl"
89
+ done
90
+ ```
91
+
92
+ And the document will tell you what it takes, so the first `params.json` need not be written
93
+ by hand:
94
+
95
+ ```bash
96
+ forma params riser.forma --json | jq 'map({(.name): .default}) | add' > params.json
97
+ ```
98
+
99
+ ```json
100
+ {
101
+ "height": 20,
102
+ "label": "A",
103
+ "center": false
104
+ }
105
+ ```
106
+
45
107
  ## In a pipeline
46
108
 
47
109
  Only what was asked for goes to stdout — the STL under `-o -`, and the listings. The summary
@@ -57,13 +119,13 @@ that does not build, and `2` for a mistake in the invocation itself.
57
119
  A failure is reported where an editor and a CI annotation can both find it:
58
120
 
59
121
  ```
60
- riser.forma:7:23: size must be positive
61
- 5 | part "body" {
62
- 6 | extrude {
63
- 7 | rect { size = [-40, 20] }
64
- | ^
65
- 8 | }
66
- 9 | }
122
+ riser.forma:7:7: rounded_rect: unknown attribute "corner_radius" - rounded_rect reads "center", "radius", "segments", "size"
123
+ 5 | extrude {
124
+ 6 | height = var.height
125
+ 7 | rounded_rect { size = [40, 20] corner_radius = 4 }
126
+ | ^
127
+ 8 | }
128
+ 9 | }
67
129
  ```
68
130
 
69
131
  Build a matrix of variants from what the document itself declares:
@@ -91,8 +153,11 @@ millimetres.
91
153
  | [`forma-dsl`](https://www.npmjs.com/package/forma-dsl) | the language, its evaluator and the geometry kernel binding |
92
154
  | [`forma-dsl-mcp`](https://www.npmjs.com/package/forma-dsl-mcp) | an MCP server: it knows the language, solves what it writes, and produces `.forma` files |
93
155
 
94
- The [reference manual](https://kalpak44.github.io/forma-dsl/docs/) documents the language, and
95
- the [editor](https://kalpak44.github.io/forma-dsl/editor/) runs the same kernel in a browser.
156
+ **[The site](https://kalpak44.github.io/forma-dsl/)** ·
157
+ **[Try the editor](https://kalpak44.github.io/forma-dsl/editor/)** ·
158
+ **[Read the reference](https://kalpak44.github.io/forma-dsl/docs/)**
159
+
160
+ The reference manual documents the language, and the editor runs the same kernel in a browser.
96
161
 
97
162
  ## License
98
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forma-dsl-cli",
3
- "version": "0.3.9",
3
+ "version": "0.4.0",
4
4
  "description": "The forma command line: render .forma documents to STL, and list what one declares.",
5
5
  "keywords": [
6
6
  "cli",
@@ -44,6 +44,6 @@
44
44
  "check:package": "node scripts/check-package.mjs"
45
45
  },
46
46
  "dependencies": {
47
- "forma-dsl": "^0.3.9"
47
+ "forma-dsl": "^0.4.0"
48
48
  }
49
49
  }
package/src/cli.js CHANGED
@@ -40,6 +40,7 @@ Render options
40
40
  -m, --model <name> which model to build. Defaults to the first declared
41
41
  --part <name> one part by name. Defaults to the only part, or every part union
42
42
  -p, --param <name=val> a value for one declared param. Repeatable
43
+ --params <file> a JSON object of values, by name. -p wins over it
43
44
  --segments <n> a fixed segment count for every curve
44
45
  --min-angle <deg> smallest angle between adjacent segments of a curve
45
46
  --min-edge <n> smallest length a curve segment may be shortened to
@@ -61,6 +62,7 @@ const OPTIONS = {
61
62
  model: { type: 'string', short: 'm' },
62
63
  part: { type: 'string' },
63
64
  param: { type: 'string', short: 'p', multiple: true },
65
+ params: { type: 'string' },
64
66
  segments: { type: 'string' },
65
67
  'min-angle': { type: 'string' },
66
68
  'min-edge': { type: 'string' },
@@ -173,6 +175,41 @@ async function readSource(file) {
173
175
  }
174
176
  }
175
177
 
178
+ /**
179
+ * Reads a JSON object of parameter values.
180
+ *
181
+ * A file rather than a flag per value is what a build matrix wants: the values a variant
182
+ * needs are a thing you generate, commit and diff, and a shell loop assembling forty `-p`
183
+ * flags is a worse version of that file.
184
+ *
185
+ * Held to an object on purpose. A JSON array or a bare number parses but names nothing, and
186
+ * `params` the language understands are named.
187
+ *
188
+ * @param {string} path The file, which must sit inside the working directory.
189
+ * @returns {Promise<Record<string, ParameterValue>>} The values, by name.
190
+ * @throws {UsageError} If it cannot be read, is not JSON, or is not an object.
191
+ */
192
+ async function parameterFile(path) {
193
+ let text;
194
+ try {
195
+ text = await readFile(await containedPath(path, true), 'utf8');
196
+ } catch (error) {
197
+ throw new UsageError(`cannot read ${path}: ${error.message}`);
198
+ }
199
+
200
+ let parsed;
201
+ try {
202
+ parsed = JSON.parse(text);
203
+ } catch (error) {
204
+ throw new UsageError(`${path} is not valid JSON: ${error.message}`);
205
+ }
206
+
207
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
208
+ throw new UsageError(`${path} must hold a JSON object of name/value pairs`);
209
+ }
210
+ return parsed;
211
+ }
212
+
176
213
  /**
177
214
  * Where an STL goes when `--out` was not given: beside the document, named after it.
178
215
  *
@@ -261,7 +298,12 @@ async function renderCommand(file, source, flags, io) {
261
298
  try {
262
299
  const result = await render(source, {
263
300
  model: flags.model ?? null,
264
- params: Object.fromEntries((flags.param ?? []).map(parameter)),
301
+ // The file first, so a `-p` on the command line overrides it. A matrix keeps the
302
+ // values it shares in the file and varies the one that differs on the line.
303
+ params: {
304
+ ...(flags.params ? await parameterFile(flags.params) : {}),
305
+ ...Object.fromEntries((flags.param ?? []).map(parameter)),
306
+ },
265
307
  maxNodes: numeric(flags['max-nodes'], '--max-nodes') ?? null,
266
308
  context,
267
309
  });