@reventlessdev/reventless-spec 3.0.0-alpha.143 → 3.0.0-alpha.144

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,19 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.144 (2026-09-23)
7
+
8
+ * feat(node)!: every command-line tool reads its arguments through parseArgs ([2e41979](https://github.com/ReventlessDev/reventless-core/commit/2e41979b73646c46356e197e17eb4a3da0154c73))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * NodeProcess.onSignal takes the new NodeProcess.signal
13
+ variant (SIGINT | SIGTERM | SIGHUP) instead of a polymorphic variant:
14
+ write onSignal(SIGINT, …) for onSignal(#SIGINT, …). Node receives the
15
+ same string.
16
+
17
+
18
+
6
19
  # 3.0.0-alpha.143 (2026-09-23)
7
20
 
8
21
  **Note:** Version bump only for package @reventlessdev/reventless-spec
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.143",
3
+ "version": "3.0.0-alpha.144",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "sury": "11.0.0",
27
27
  "sury-ppx": "11.0.0",
28
28
  "yaml": "^2.8.3",
29
- "@reventlessdev/rescript-node": "2.0.0-alpha.14"
29
+ "@reventlessdev/rescript-node": "2.0.0-alpha.15"
30
30
  },
31
31
  "devDependencies": {
32
32
  "rescript": "12.3.1",
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import "./src/generator/CertifyTrait.res.mjs"
2
+ import { main } from "./src/generator/CertifyTrait.res.mjs"
3
+ await main()
package/run-generator.mjs CHANGED
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import "./src/generator/PluginGenerator.res.mjs"
2
+ import { main } from "./src/generator/PluginGenerator.res.mjs"
3
+ await main()
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import "./src/generator/GraftTrait.res.mjs"
2
+ import { main } from "./src/generator/GraftTrait.res.mjs"
3
+ await main()
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import "./src/generator/PlatformGenerator.res.mjs"
2
+ import { main } from "./src/generator/PlatformGenerator.res.mjs"
3
+ await main()
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import "./src/generator/TraitManifestCli.res.mjs"
2
+ import { main } from "./src/generator/TraitManifestCli.res.mjs"
3
+ await main()
@@ -0,0 +1,209 @@
1
+ /***
2
+ How every command-line tool in this repository reads `process.argv`: through
3
+ Node's `parseArgs`, with mistakes worded the way these tools word them, and one
4
+ behaviour on `--help`, on a usage mistake and on a bare invocation (see [run]).
5
+ Non-strict with tokens rather than strict mode, because strict mode names the
6
+ offending argument only inside English prose.
7
+ */
8
+
9
+ /** An undeclared `--key` under `~pairs=true`: with a value, or on its own. */
10
+ type value =
11
+ | Text(string)
12
+ | Present
13
+
14
+ type t = {
15
+ values: NodeUtil.values,
16
+ positionals: array<string>,
17
+ pairs: array<(string, value)>,
18
+ }
19
+
20
+ /** A usage mistake. A tool that ran and failed exits 1, so a caller can tell a
21
+ misinvocation from a run whose answer was no. */
22
+ let usageMistakeExit = 2
23
+
24
+ let unknown = (arg: string) => `unknown argument "${arg}"`
25
+
26
+ /** A positional the tool has no place for, worded like any other unknown argument. */
27
+ let extra = (arg: string): result<'a, string> => Error(unknown(arg))
28
+
29
+ /** `-` alone is a value (stdin, by convention); anything else starting with a
30
+ dash is the next flag, which a string option must not swallow. */
31
+ let flagShaped = (value: string) => value->String.startsWith("-") && value != "-"
32
+
33
+ /** Package managers can forward a bare `--` ahead of a script's own arguments.
34
+ Anywhere else it ends the options, as POSIX says. */
35
+ let withoutLeadingTerminator = (argv: array<string>) =>
36
+ argv->Array.get(0) == Some("--") ? argv->Array.slice(~start=1, ~end=Array.length(argv)) : argv
37
+
38
+ /** Parse against the declared options. `-h`/`--help` is declared for every
39
+ tool. The first mistake in argv order is the one reported. Under
40
+ `~pairs=true` an undeclared long option is kept as a `(key, value)` pair
41
+ instead of refused, taking the argument after it as its value when that
42
+ argument is not itself an option. */
43
+ let parse = (
44
+ ~strings: array<string>=[],
45
+ ~bools: array<string>=[],
46
+ ~lists: array<string>=[],
47
+ ~pairs: bool=false,
48
+ argv: array<string>,
49
+ ): result<t, string> => {
50
+ let args = withoutLeadingTerminator(argv)
51
+ let options: dict<NodeUtil.optionConfig> = Dict.make()
52
+ options->Dict.set("help", {type_: Boolean, short: "h"})
53
+ bools->Array.forEach(name => options->Dict.set(name, {type_: Boolean}))
54
+ strings->Array.forEach(name => options->Dict.set(name, {type_: String}))
55
+ lists->Array.forEach(name => options->Dict.set(name, {type_: String, multiple: true}))
56
+
57
+ let parsed = NodeUtil.parseArgs({
58
+ args,
59
+ options,
60
+ strict: false,
61
+ allowPositionals: true,
62
+ tokens: true,
63
+ })
64
+ let tokens = parsed.tokens->Option.getOr([])
65
+ let positionals = []
66
+ let found = []
67
+
68
+ // Non-strict `parseArgs` types an undeclared option as a boolean and emits
69
+ // the argument after it as the next positional token, so a pair's value is
70
+ // read back from token order.
71
+ let rec walk = i =>
72
+ switch tokens->Array.get(i) {
73
+ | None => Ok()
74
+ | Some(token) =>
75
+ switch token.kind {
76
+ | Terminator => walk(i + 1)
77
+ | Positional =>
78
+ positionals->Array.push(token.value->Option.getOr(""))
79
+ walk(i + 1)
80
+ | Flag =>
81
+ let name = token.name->Option.getOr("")
82
+ let rawName = token.rawName->Option.getOr(name)
83
+ let inline = token.inlineValue->Option.getOr(false)
84
+ switch (options->Dict.get(name), token.value) {
85
+ | (Some({type_: String}), None) => Error(`${rawName} needs a value`)
86
+ | (Some({type_: String}), Some(v)) if !inline && flagShaped(v) =>
87
+ Error(`${rawName} needs a value`)
88
+ | (Some({type_: Boolean}), Some(_)) => Error(`${rawName} takes no value`)
89
+ | (Some(_), _) => walk(i + 1)
90
+ | (None, _) if !pairs || !(rawName->String.startsWith("--")) => Error(unknown(rawName))
91
+ | (None, Some(v)) =>
92
+ found->Array.push((name, Text(v)))
93
+ walk(i + 1)
94
+ | (None, None) =>
95
+ switch tokens->Array.get(i + 1) {
96
+ | Some({kind: Positional, index, value: ?Some(v)}) if index == token.index + 1 =>
97
+ found->Array.push((name, Text(v)))
98
+ walk(i + 2)
99
+ | _ =>
100
+ found->Array.push((name, Present))
101
+ walk(i + 1)
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ walk(0)->Result.map(() => {values: parsed.values, positionals, pairs: found})
108
+ }
109
+
110
+ let string = (t: t, name: string): option<string> => t.values->NodeUtil.string(name)
111
+ let bool = (t: t, name: string): bool => t.values->NodeUtil.bool(name)->Option.getOr(false)
112
+ let strings = (t: t, name: string): array<string> =>
113
+ t.values->NodeUtil.strings(name)->Option.getOr([])
114
+ let positionals = (t: t): array<string> => t.positionals
115
+ let pairs = (t: t): array<(string, value)> => t.pairs
116
+ let help = (t: t): bool => t->bool("help")
117
+
118
+ /** For a tool that takes at most `max` positionals: the first beyond them is
119
+ refused as [extra]. */
120
+ let atMost = (t: t, max: int): result<t, string> =>
121
+ switch t.positionals->Array.get(max) {
122
+ | None => Ok(t)
123
+ | Some(arg) => extra(arg)
124
+ }
125
+
126
+ let noPositionals = (t: t): result<t, string> => t->atMost(0)
127
+
128
+ /** Whether `-h` or `--help` appears before any `--` that ends the options.
129
+ Read off argv rather than a parse, so it answers even when the other
130
+ arguments are wrong. */
131
+ let asksForHelp = (argv: array<string>): bool => {
132
+ let argv = withoutLeadingTerminator(argv)
133
+ let options = switch argv->Array.indexOf("--") {
134
+ | -1 => argv
135
+ | end => argv->Array.slice(~start=0, ~end)
136
+ }
137
+ options->Array.some(arg => arg == "-h" || arg == "--help")
138
+ }
139
+
140
+ /** A tool's command line: its name, its usage (first line `Usage: <bin> …`),
141
+ and how it turns argv into its arguments. */
142
+ type cli<'args> = {
143
+ bin: string,
144
+ usage: string,
145
+ parse: array<string> => result<'args, string>,
146
+ }
147
+
148
+ /** Where [run] writes and how it ends the process, so a test can watch it. */
149
+ type io = {
150
+ out: string => unit,
151
+ err: string => unit,
152
+ exit: int => unit,
153
+ }
154
+
155
+ let processIo = {
156
+ out: text => Console.log(text),
157
+ err: text => Console.error(text),
158
+ exit: NodeProcess.exit,
159
+ }
160
+
161
+ /** Help anywhere prints the usage to stdout and exits 0, whatever else is
162
+ wrong. A usage mistake prints `<bin>: <message>`, a blank line and the usage
163
+ to stderr and exits 2. Otherwise `main` gets the arguments, and its own
164
+ failures keep their exit 1. */
165
+ let run = async (
166
+ cli: cli<'args>,
167
+ ~argv: option<array<string>>=?,
168
+ ~io: io=processIo,
169
+ main: 'args => promise<unit>,
170
+ ): unit => {
171
+ let argv =
172
+ argv->Option.getOr(NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length))
173
+ let usage = cli.usage->String.trim
174
+ if asksForHelp(argv) {
175
+ io.out(usage)
176
+ io.exit(0)
177
+ } else {
178
+ switch cli.parse(argv) {
179
+ | Error(message) =>
180
+ io.err(`${cli.bin}: ${message}\n\n${usage}`)
181
+ io.exit(usageMistakeExit)
182
+ | Ok(args) => await main(args)
183
+ }
184
+ }
185
+ }
186
+
187
+ /** What [run] did with one argv, for a tool's own tests. */
188
+ type observed = {
189
+ stdout: array<string>,
190
+ stderr: array<string>,
191
+ exitCode: option<int>,
192
+ reachedMain: bool,
193
+ }
194
+
195
+ /** Run a tool's command line without a process: nothing is printed, nothing
196
+ exits, and `main` is not called — only whether it would have been. */
197
+ let observe = async (cli: cli<'args>, argv: array<string>): observed => {
198
+ let stdout = []
199
+ let stderr = []
200
+ let exitCode = ref(None)
201
+ let reachedMain = ref(false)
202
+ let io = {
203
+ out: text => stdout->Array.push(text),
204
+ err: text => stderr->Array.push(text),
205
+ exit: code => exitCode := Some(code),
206
+ }
207
+ await run(cli, ~argv, ~io, async _ => reachedMain := true)
208
+ {stdout, stderr, exitCode: exitCode.contents, reachedMain: reachedMain.contents}
209
+ }
@@ -0,0 +1,325 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodeutil from "node:util";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
6
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
7
+
8
+ function unknown(arg) {
9
+ return `unknown argument "` + arg + `"`;
10
+ }
11
+
12
+ function extra(arg) {
13
+ return {
14
+ TAG: "Error",
15
+ _0: unknown(arg)
16
+ };
17
+ }
18
+
19
+ function flagShaped(value) {
20
+ if (value.startsWith("-")) {
21
+ return value !== "-";
22
+ } else {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ function withoutLeadingTerminator(argv) {
28
+ if (Primitive_object.equal(argv[0], "--")) {
29
+ return argv.slice(1, argv.length);
30
+ } else {
31
+ return argv;
32
+ }
33
+ }
34
+
35
+ function parse(stringsOpt, boolsOpt, listsOpt, pairsOpt, argv) {
36
+ let strings = stringsOpt !== undefined ? stringsOpt : [];
37
+ let bools = boolsOpt !== undefined ? boolsOpt : [];
38
+ let lists = listsOpt !== undefined ? listsOpt : [];
39
+ let pairs = pairsOpt !== undefined ? pairsOpt : false;
40
+ let args = withoutLeadingTerminator(argv);
41
+ let options = {};
42
+ options["help"] = {
43
+ type: "boolean",
44
+ short: "h"
45
+ };
46
+ bools.forEach(name => {
47
+ options[name] = {
48
+ type: "boolean"
49
+ };
50
+ });
51
+ strings.forEach(name => {
52
+ options[name] = {
53
+ type: "string"
54
+ };
55
+ });
56
+ lists.forEach(name => {
57
+ options[name] = {
58
+ type: "string",
59
+ multiple: true
60
+ };
61
+ });
62
+ let parsed = Nodeutil.parseArgs({
63
+ args: args,
64
+ options: options,
65
+ strict: false,
66
+ allowPositionals: true,
67
+ tokens: true
68
+ });
69
+ let tokens = Stdlib_Option.getOr(parsed.tokens, []);
70
+ let positionals = [];
71
+ let found = [];
72
+ let walk = _i => {
73
+ while (true) {
74
+ let i = _i;
75
+ let token = tokens[i];
76
+ if (token === undefined) {
77
+ return {
78
+ TAG: "Ok",
79
+ _0: undefined
80
+ };
81
+ }
82
+ let match = token.kind;
83
+ switch (match) {
84
+ case "option" :
85
+ let name = Stdlib_Option.getOr(token.name, "");
86
+ let rawName = Stdlib_Option.getOr(token.rawName, name);
87
+ let inline = Stdlib_Option.getOr(token.inlineValue, false);
88
+ let match$1 = options[name];
89
+ let match$2 = token.value;
90
+ if (match$1 !== undefined) {
91
+ if (match$1.type === "string") {
92
+ if (match$2 === undefined) {
93
+ return {
94
+ TAG: "Error",
95
+ _0: rawName + ` needs a value`
96
+ };
97
+ }
98
+ if (!inline && flagShaped(match$2)) {
99
+ return {
100
+ TAG: "Error",
101
+ _0: rawName + ` needs a value`
102
+ };
103
+ }
104
+ _i = i + 1 | 0;
105
+ continue;
106
+ }
107
+ if (match$2 !== undefined) {
108
+ return {
109
+ TAG: "Error",
110
+ _0: rawName + ` takes no value`
111
+ };
112
+ }
113
+ _i = i + 1 | 0;
114
+ continue;
115
+ }
116
+ if (!pairs || !rawName.startsWith("--")) {
117
+ return {
118
+ TAG: "Error",
119
+ _0: unknown(rawName)
120
+ };
121
+ }
122
+ if (match$2 !== undefined) {
123
+ found.push([
124
+ name,
125
+ {
126
+ TAG: "Text",
127
+ _0: match$2
128
+ }
129
+ ]);
130
+ _i = i + 1 | 0;
131
+ continue;
132
+ }
133
+ let match$3 = tokens[i + 1 | 0];
134
+ if (match$3 !== undefined) {
135
+ switch (match$3.kind) {
136
+ case "positional" :
137
+ let v = match$3.value;
138
+ if (v !== undefined && match$3.index === (token.index + 1 | 0)) {
139
+ found.push([
140
+ name,
141
+ {
142
+ TAG: "Text",
143
+ _0: v
144
+ }
145
+ ]);
146
+ _i = i + 2 | 0;
147
+ continue;
148
+ }
149
+ break;
150
+ case "option" :
151
+ case "option-terminator" :
152
+ break;
153
+ }
154
+ }
155
+ found.push([
156
+ name,
157
+ "Present"
158
+ ]);
159
+ _i = i + 1 | 0;
160
+ continue;
161
+ break;
162
+ case "positional" :
163
+ positionals.push(Stdlib_Option.getOr(token.value, ""));
164
+ _i = i + 1 | 0;
165
+ continue;
166
+ case "option-terminator" :
167
+ _i = i + 1 | 0;
168
+ continue;
169
+ }
170
+ };
171
+ };
172
+ return Stdlib_Result.map(walk(0), () => ({
173
+ values: parsed.values,
174
+ positionals: positionals,
175
+ pairs: found
176
+ }));
177
+ }
178
+
179
+ function string(t, name) {
180
+ return t.values[name];
181
+ }
182
+
183
+ function bool(t, name) {
184
+ return Stdlib_Option.getOr(t.values[name], false);
185
+ }
186
+
187
+ function strings(t, name) {
188
+ return Stdlib_Option.getOr(t.values[name], []);
189
+ }
190
+
191
+ function positionals(t) {
192
+ return t.positionals;
193
+ }
194
+
195
+ function pairs(t) {
196
+ return t.pairs;
197
+ }
198
+
199
+ function help(t) {
200
+ return bool(t, "help");
201
+ }
202
+
203
+ function atMost(t, max) {
204
+ let arg = t.positionals[max];
205
+ if (arg !== undefined) {
206
+ return {
207
+ TAG: "Error",
208
+ _0: unknown(arg)
209
+ };
210
+ } else {
211
+ return {
212
+ TAG: "Ok",
213
+ _0: t
214
+ };
215
+ }
216
+ }
217
+
218
+ function noPositionals(t) {
219
+ return atMost(t, 0);
220
+ }
221
+
222
+ function asksForHelp(argv) {
223
+ let argv$1 = withoutLeadingTerminator(argv);
224
+ let end = argv$1.indexOf("--");
225
+ let options = end !== -1 ? argv$1.slice(0, end) : argv$1;
226
+ return options.some(arg => {
227
+ if (arg === "-h") {
228
+ return true;
229
+ } else {
230
+ return arg === "--help";
231
+ }
232
+ });
233
+ }
234
+
235
+ function processIo_out(text) {
236
+ console.log(text);
237
+ }
238
+
239
+ function processIo_err(text) {
240
+ console.error(text);
241
+ }
242
+
243
+ function processIo_exit(prim) {
244
+ process.exit(prim);
245
+ }
246
+
247
+ let processIo = {
248
+ out: processIo_out,
249
+ err: processIo_err,
250
+ exit: processIo_exit
251
+ };
252
+
253
+ async function run(cli, argv, ioOpt, main) {
254
+ let io = ioOpt !== undefined ? ioOpt : processIo;
255
+ let argv$1 = Stdlib_Option.getOr(argv, process.argv.slice(2, process.argv.length));
256
+ let usage = cli.usage.trim();
257
+ if (asksForHelp(argv$1)) {
258
+ io.out(usage);
259
+ return io.exit(0);
260
+ }
261
+ let message = cli.parse(argv$1);
262
+ if (message.TAG === "Ok") {
263
+ return await main(message._0);
264
+ }
265
+ io.err(cli.bin + `: ` + message._0 + `\n\n` + usage);
266
+ return io.exit(2);
267
+ }
268
+
269
+ async function observe(cli, argv) {
270
+ let stdout = [];
271
+ let stderr = [];
272
+ let exitCode = {
273
+ contents: undefined
274
+ };
275
+ let reachedMain = {
276
+ contents: false
277
+ };
278
+ let io_out = text => {
279
+ stdout.push(text);
280
+ };
281
+ let io_err = text => {
282
+ stderr.push(text);
283
+ };
284
+ let io_exit = code => {
285
+ exitCode.contents = code;
286
+ };
287
+ let io = {
288
+ out: io_out,
289
+ err: io_err,
290
+ exit: io_exit
291
+ };
292
+ await run(cli, argv, io, async param => {
293
+ reachedMain.contents = true;
294
+ });
295
+ return {
296
+ stdout: stdout,
297
+ stderr: stderr,
298
+ exitCode: exitCode.contents,
299
+ reachedMain: reachedMain.contents
300
+ };
301
+ }
302
+
303
+ let usageMistakeExit = 2;
304
+
305
+ export {
306
+ usageMistakeExit,
307
+ unknown,
308
+ extra,
309
+ flagShaped,
310
+ withoutLeadingTerminator,
311
+ parse,
312
+ string,
313
+ bool,
314
+ strings,
315
+ positionals,
316
+ pairs,
317
+ help,
318
+ atMost,
319
+ noPositionals,
320
+ asksForHelp,
321
+ processIo,
322
+ run,
323
+ observe,
324
+ }
325
+ /* node:util Not a pure module */