producs 1.0.1 → 3.0.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.
- package/README.md +245 -0
- package/README.pd +352 -0
- package/cli.js +103 -81
- package/examples/checks.pd +126 -0
- package/examples/demo.pd +113 -0
- package/examples/deploy.pd +43 -0
- package/index.js +1079 -149
- package/package.json +29 -11
package/index.js
CHANGED
|
@@ -1,185 +1,1115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
// ProDucs — Professional Documentation Shell DSL v3.0
|
|
6
|
+
//
|
|
7
|
+
// Pipe syntax: #$Cmd1 | #$Cmd2 | #$Cmd3
|
|
8
|
+
// Block syntax: #$Block['arg'] … #$End_Block
|
|
9
|
+
// Variables: ${name}
|
|
10
|
+
// Comments: // line comment
|
|
11
|
+
//
|
|
12
|
+
// Branching model (no If/Else):
|
|
13
|
+
// #$Set['x','10']
|
|
14
|
+
// #$Check_gt['x','5'] ← sets failed=0 (pass) or failed=1 (fail)
|
|
15
|
+
// #$Branch ← runs children only when failed=0
|
|
16
|
+
// #$Print['x is > 5']
|
|
17
|
+
// #$End_Branch
|
|
18
|
+
// #$BranchElse ← runs children only when failed=1
|
|
19
|
+
// #$Print['x is not > 5']
|
|
20
|
+
// #$End_BranchElse
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
1
23
|
const { execSync } = require("child_process");
|
|
2
|
-
const
|
|
24
|
+
const crypto = require("crypto");
|
|
25
|
+
const fs = require("fs");
|
|
3
26
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
27
|
+
let promptSync;
|
|
28
|
+
try { promptSync = require("prompt-sync")({ sigint: true }); }
|
|
29
|
+
catch { promptSync = (msg) => { process.stdout.write(msg || ""); return ""; }; }
|
|
30
|
+
|
|
31
|
+
// ─── ANSI ─────────────────────────────────────────────────────────────────────
|
|
32
|
+
const C = {
|
|
33
|
+
reset:"\x1b[0m", bold:"\x1b[1m", dim:"\x1b[2m",
|
|
34
|
+
red:"\x1b[31m", green:"\x1b[32m", yellow:"\x1b[33m",
|
|
35
|
+
blue:"\x1b[34m", cyan:"\x1b[36m", white:"\x1b[37m",
|
|
36
|
+
bgred:"\x1b[41m",bggreen:"\x1b[42m",bgblue:"\x1b[44m",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ─── JS reserved words (must not be injected as var names) ────────────────────
|
|
40
|
+
const RESERVED_JS = new Set([
|
|
41
|
+
"break","case","catch","class","const","continue","debugger","default",
|
|
42
|
+
"delete","do","else","export","extends","finally","for","function","if",
|
|
43
|
+
"import","in","instanceof","let","new","return","static","super","switch",
|
|
44
|
+
"this","throw","try","typeof","var","void","while","with","yield",
|
|
45
|
+
"enum","await","null","true","false","undefined","NaN","Infinity",
|
|
46
|
+
"constructor","prototype","__proto__",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// ─── Result ───────────────────────────────────────────────────────────────────
|
|
50
|
+
const mkResult = (VALUE="", failed=0, meta={}) =>
|
|
51
|
+
({ VALUE: String(VALUE ?? ""), failed: Number(failed), ...meta });
|
|
52
|
+
const okResult = (VALUE="", meta={}) => mkResult(VALUE, 0, meta);
|
|
53
|
+
const failResult = (VALUE="", meta={}) => mkResult(VALUE, 1, meta);
|
|
54
|
+
|
|
55
|
+
const out = (s) => process.stdout.write(s);
|
|
56
|
+
const err = (s) => process.stderr.write(s);
|
|
57
|
+
const pad = (s, n) => String(s).padEnd(n).slice(0, n);
|
|
58
|
+
|
|
59
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
60
|
+
// ERRORS
|
|
61
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
62
|
+
class ProDucsError extends Error {
|
|
63
|
+
constructor(msg, loc) {
|
|
64
|
+
super(loc ? `[${loc.line}:${loc.col}] ${msg}` : msg);
|
|
65
|
+
this.name = "ProDucsError";
|
|
66
|
+
this.loc = loc;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
class GotoSignal {
|
|
70
|
+
constructor(label) { this.label = label; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
74
|
+
// RUNTIME ENV (per-instance)
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
76
|
+
class RuntimeEnv {
|
|
77
|
+
constructor() { this.reset(); }
|
|
78
|
+
|
|
79
|
+
reset() {
|
|
80
|
+
this.vars = {};
|
|
81
|
+
this.labels = {}; // name → { node, list } — list is the actual array
|
|
82
|
+
this.log = [];
|
|
83
|
+
this.stepNum = 0;
|
|
84
|
+
this.startMs = Date.now();
|
|
85
|
+
this.debug = false;
|
|
86
|
+
this.strict = false;
|
|
87
|
+
this.colorOff = false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
col(code, s) { return this.colorOff ? s : `${code}${s}${C.reset}`; }
|
|
91
|
+
bold(s) { return this.col(C.bold, s); }
|
|
92
|
+
dim(s) { return this.col(C.dim, s); }
|
|
93
|
+
cyan(s) { return this.col(C.cyan, s); }
|
|
94
|
+
green(s) { return this.col(C.green,s); }
|
|
95
|
+
red(s) { return this.col(C.red, s); }
|
|
96
|
+
yellow(s) { return this.col(C.yellow,s);}
|
|
97
|
+
blue(s) { return this.col(C.blue, s); }
|
|
98
|
+
|
|
99
|
+
termW() { return process.stdout.columns || 80; }
|
|
100
|
+
hr(ch="─") { return ch.repeat(this.termW()); }
|
|
101
|
+
|
|
102
|
+
interpolate(str) {
|
|
103
|
+
return String(str ?? "").replace(/\$\{([^}]+)\}/g, (_, k) => this.vars[k] ?? "");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Produce safe `var x = "val";` bindings for new Function, skipping reserved names */
|
|
107
|
+
safeBindings(extra = {}) {
|
|
108
|
+
const SAFE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
109
|
+
return Object.entries({ ...this.vars, ...extra })
|
|
110
|
+
.filter(([k]) => SAFE.test(k) && !RESERVED_JS.has(k))
|
|
111
|
+
.map(([k, v]) => {
|
|
112
|
+
const n = Number(v);
|
|
113
|
+
const val = (v !== "" && !isNaN(n)) ? n : JSON.stringify(String(v));
|
|
114
|
+
return `var ${k} = ${val};`;
|
|
115
|
+
})
|
|
116
|
+
.join("\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
logEntry(cmd, msg, ok=true) {
|
|
120
|
+
this.log.push({ t: Date.now() - this.startMs, cmd, msg, ok });
|
|
121
|
+
if (this.debug) err(this.dim(`[DBG] ${cmd}: ${msg}`) + "\n");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
126
|
+
// TOKENIZER
|
|
127
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
128
|
+
class Tokenizer {
|
|
129
|
+
constructor(src) { this.src=src; this.i=0; this.line=1; this.col=1; }
|
|
59
130
|
|
|
60
|
-
|
|
61
|
-
|
|
131
|
+
peek(n=0) { return this.src[this.i+n]; }
|
|
132
|
+
eof() { return this.i >= this.src.length; }
|
|
133
|
+
next() {
|
|
134
|
+
const c = this.src[this.i++];
|
|
135
|
+
if (c==="\n") { this.line++; this.col=1; } else this.col++;
|
|
136
|
+
return c;
|
|
137
|
+
}
|
|
138
|
+
skipSpaces() { while (!this.eof() && /[ \t\r\n]/.test(this.peek())) this.next(); }
|
|
139
|
+
|
|
140
|
+
readIdent() {
|
|
141
|
+
let s="";
|
|
142
|
+
while (!this.eof() && /[A-Za-z0-9_>!]/.test(this.peek())) s += this.next();
|
|
143
|
+
return s;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Read [...] args supporting single-quote, double-quote, or bare words */
|
|
147
|
+
readArgs() {
|
|
148
|
+
if (this.peek() !== "[") return [];
|
|
149
|
+
this.next();
|
|
150
|
+
const args=[]; let buf="", inQ=null, wasQ=false;
|
|
151
|
+
while (!this.eof()) {
|
|
152
|
+
const c = this.peek();
|
|
153
|
+
if (!inQ && (c==="'" || c==='"')) { inQ=c; wasQ=true; this.next(); continue; }
|
|
154
|
+
if (inQ && c===inQ) { inQ=null; this.next(); continue; }
|
|
155
|
+
if (!inQ && c===",") {
|
|
156
|
+
args.push(wasQ ? buf : buf.trim());
|
|
157
|
+
buf=""; wasQ=false; this.next(); continue;
|
|
158
|
+
}
|
|
159
|
+
if (!inQ && c==="]") { this.next(); break; }
|
|
160
|
+
buf += this.next();
|
|
161
|
+
}
|
|
162
|
+
// push last arg: if quoted preserve spaces, else trim; skip if empty bare word
|
|
163
|
+
if (wasQ || buf.trim()) args.push(wasQ ? buf : buf.trim());
|
|
164
|
+
return args;
|
|
165
|
+
}
|
|
62
166
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
167
|
+
tokenize() {
|
|
168
|
+
const tokens=[];
|
|
169
|
+
while (!this.eof()) {
|
|
170
|
+
// line comment
|
|
171
|
+
if (this.peek()==="/" && this.peek(1)==="/") {
|
|
172
|
+
while (!this.eof() && this.peek()!=="\n") this.next();
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// explicit pipe | (not ||)
|
|
176
|
+
if (this.peek()==="|" && this.peek(1)!=="|") {
|
|
177
|
+
const loc={line:this.line,col:this.col};
|
|
178
|
+
this.next(); this.skipSpaces();
|
|
179
|
+
tokens.push({type:"PIPE",loc});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// command #$…
|
|
183
|
+
if (this.peek()==="#" && this.peek(1)==="$") {
|
|
184
|
+
this.next(); this.next();
|
|
185
|
+
const loc={line:this.line,col:this.col};
|
|
186
|
+
if (this.src.startsWith("End_", this.i)) {
|
|
187
|
+
this.i+=4;
|
|
188
|
+
tokens.push({type:"COMMAND_END", name:this.readIdent(), loc});
|
|
69
189
|
} else {
|
|
70
|
-
const name
|
|
71
|
-
|
|
72
|
-
tokens.push({ type: "COMMAND_START", name, args });
|
|
190
|
+
const name=this.readIdent(), args=this.readArgs();
|
|
191
|
+
tokens.push({type:"COMMAND_START", name, args, loc});
|
|
73
192
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
// raw text
|
|
196
|
+
let text="";
|
|
197
|
+
const loc={line:this.line,col:this.col};
|
|
198
|
+
while (!this.eof()
|
|
199
|
+
&& !(this.peek()==="#" && this.peek(1)==="$")
|
|
200
|
+
&& !(this.peek()==="/" && this.peek(1)==="/")
|
|
201
|
+
&& !(this.peek()==="|" && this.peek(1)!=="|")) {
|
|
202
|
+
text += this.next();
|
|
78
203
|
}
|
|
204
|
+
const trimmed=text.trim();
|
|
205
|
+
if (trimmed) tokens.push({type:"TEXT", value:trimmed, loc});
|
|
79
206
|
}
|
|
80
|
-
|
|
81
207
|
return tokens;
|
|
82
208
|
}
|
|
209
|
+
}
|
|
83
210
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
90
|
-
const token = tokens[i];
|
|
91
|
-
|
|
92
|
-
if (token.type === "COMMAND_START") {
|
|
93
|
-
const node = {
|
|
94
|
-
type: token.name,
|
|
95
|
-
args: token.args,
|
|
96
|
-
children: [],
|
|
97
|
-
pipedCommands: []
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// Inline (pipe) commands
|
|
101
|
-
if (stack.length && !token.name.startsWith("End_")) {
|
|
102
|
-
const parent = stack.at(-1);
|
|
103
|
-
if (parent.type === ">" || parent.pipedCommands.length) {
|
|
104
|
-
parent.pipedCommands.push(node);
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
211
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
212
|
+
// PARSER
|
|
213
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
214
|
+
class Parser {
|
|
215
|
+
constructor(tokens) { this.tokens=tokens; this.pos=0; }
|
|
108
216
|
|
|
109
|
-
|
|
110
|
-
|
|
217
|
+
// Block commands: opened with #$X, closed with #$End_X
|
|
218
|
+
// Children are collected between open/close.
|
|
219
|
+
// Everything else is inline (pipeable).
|
|
220
|
+
static BLOCK_CMDS = new Set([
|
|
221
|
+
">", "Branch", "BranchElse",
|
|
222
|
+
"Repeat", "ForEach",
|
|
223
|
+
"Try", "Catch",
|
|
224
|
+
"Section", "Step", "Group", "Table",
|
|
225
|
+
]);
|
|
111
226
|
|
|
112
|
-
|
|
113
|
-
|
|
227
|
+
peek() { return this.tokens[this.pos]; }
|
|
228
|
+
eof() { return this.pos >= this.tokens.length; }
|
|
229
|
+
mkNode(tok) {
|
|
230
|
+
return { type:tok.name, args:tok.args, loc:tok.loc, children:[], pipes:[] };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
collectPipes(head) {
|
|
234
|
+
while (!this.eof() && this.peek().type==="PIPE") {
|
|
235
|
+
const pipeTok = this.tokens[this.pos++];
|
|
236
|
+
const next = this.peek();
|
|
237
|
+
if (!next || next.type!=="COMMAND_START")
|
|
238
|
+
throw new SyntaxError(`[${pipeTok.loc.line}:${pipeTok.loc.col}] Expected command after |`);
|
|
239
|
+
if (Parser.BLOCK_CMDS.has(next.name))
|
|
240
|
+
throw new SyntaxError(`[${next.loc.line}:${next.loc.col}] Block command '${next.name}' cannot be piped`);
|
|
241
|
+
this.pos++;
|
|
242
|
+
head.pipes.push(this.mkNode(next));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
parse() {
|
|
247
|
+
const root=[], stack=[];
|
|
248
|
+
while (!this.eof()) {
|
|
249
|
+
const tok = this.tokens[this.pos++];
|
|
250
|
+
|
|
251
|
+
if (tok.type==="TEXT") {
|
|
252
|
+
const node={type:"Text", value:tok.value, loc:tok.loc};
|
|
253
|
+
(stack.length ? stack.at(-1).children : root).push(node);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (tok.type==="PIPE")
|
|
257
|
+
throw new SyntaxError(`[${tok.loc.line}:${tok.loc.col}] Unexpected | — must follow a command`);
|
|
258
|
+
|
|
259
|
+
if (tok.type==="COMMAND_END") {
|
|
260
|
+
const last=stack.pop();
|
|
114
261
|
if (!last)
|
|
115
|
-
throw new
|
|
116
|
-
if (last.type
|
|
117
|
-
throw new
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
} else if (token.type === "TEXT") {
|
|
121
|
-
const node = { type: "Text", value: token.value };
|
|
122
|
-
if (stack.length) stack.at(-1).children.push(node);
|
|
123
|
-
else root.push(node);
|
|
262
|
+
throw new SyntaxError(`[${tok.loc.line}:${tok.loc.col}] Unexpected #$End_${tok.name}`);
|
|
263
|
+
if (last.type!==tok.name)
|
|
264
|
+
throw new SyntaxError(`[${tok.loc.line}:${tok.loc.col}] Mismatched: expected #$End_${last.type}, got #$End_${tok.name}`);
|
|
265
|
+
(stack.length ? stack.at(-1).children : root).push(last);
|
|
266
|
+
continue;
|
|
124
267
|
}
|
|
125
|
-
}
|
|
126
268
|
|
|
127
|
-
|
|
269
|
+
if (tok.type==="COMMAND_START") {
|
|
270
|
+
const node=this.mkNode(tok);
|
|
271
|
+
if (Parser.BLOCK_CMDS.has(tok.name)) {
|
|
272
|
+
stack.push(node);
|
|
273
|
+
} else {
|
|
274
|
+
this.collectPipes(node);
|
|
275
|
+
(stack.length ? stack.at(-1).children : root).push(node);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (stack.length)
|
|
280
|
+
throw new SyntaxError(`Unclosed: #$${stack.at(-1).type} at line ${stack.at(-1).loc?.line}`);
|
|
128
281
|
return root;
|
|
129
282
|
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
286
|
+
// COMMAND REGISTRY
|
|
287
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
288
|
+
class CommandRegistry {
|
|
289
|
+
constructor(env) { this.env=env; this._cmds=new Map(); this._registerAll(); }
|
|
130
290
|
|
|
131
|
-
|
|
132
|
-
|
|
291
|
+
register(name, fn, meta={}) { this._cmds.set(name,{fn,...meta}); return this; }
|
|
292
|
+
get(name) { return this._cmds.get(name); }
|
|
293
|
+
has(name) { return this._cmds.has(name); }
|
|
294
|
+
list() { return [...this._cmds.keys()].sort(); }
|
|
295
|
+
|
|
296
|
+
resolveText(node) {
|
|
297
|
+
const tc=node.children?.find(c=>c.type==="Text");
|
|
298
|
+
return tc ? this.env.interpolate(tc.value) : "";
|
|
133
299
|
}
|
|
300
|
+
resolveArg(a) { return this.env.interpolate(a ?? ""); }
|
|
301
|
+
resolveArgs(args) { return (args||[]).map(a=>this.resolveArg(a)); }
|
|
134
302
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
303
|
+
// Safe expression evaluator — vars injected via safeBindings
|
|
304
|
+
evalExpr(expr, extra={}) {
|
|
305
|
+
const bindings = this.env.safeBindings(extra);
|
|
306
|
+
// eslint-disable-next-line no-new-func
|
|
307
|
+
return new Function(`"use strict";\n${bindings}\nreturn (${expr});`)();
|
|
308
|
+
}
|
|
140
309
|
|
|
141
|
-
|
|
142
|
-
|
|
310
|
+
// Coerce a value from env.vars to a number for comparisons
|
|
311
|
+
numVar(name) {
|
|
312
|
+
const v = this.env.vars[name];
|
|
313
|
+
if (v === undefined) throw new ProDucsError(`Variable not found: ${name}`);
|
|
314
|
+
const n = Number(v);
|
|
315
|
+
if (isNaN(n)) throw new ProDucsError(`Variable '${name}' = '${v}' is not numeric`);
|
|
316
|
+
return n;
|
|
317
|
+
}
|
|
143
318
|
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
const
|
|
319
|
+
_registerAll() {
|
|
320
|
+
const env=this.env, R=this;
|
|
321
|
+
const itp =(s) => env.interpolate(s);
|
|
322
|
+
const rA =(args) => R.resolveArgs(args);
|
|
323
|
+
const rT =(node) => R.resolveText(node);
|
|
147
324
|
|
|
148
|
-
//
|
|
149
|
-
|
|
325
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
326
|
+
// I / O
|
|
327
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
150
328
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
329
|
+
R.register(">", (node,input) => {
|
|
330
|
+
const [promptText=""] = rA(node.args);
|
|
331
|
+
const msg = promptText || rT(node) || "";
|
|
332
|
+
const val = promptSync(msg ? env.cyan(msg)+" " : "");
|
|
333
|
+
if (val===null) throw new Error("Aborted (Ctrl-C)");
|
|
334
|
+
return val.trim() ? okResult(val.trim()) : failResult("",{reason:"empty"});
|
|
335
|
+
}, { doc:"Prompt user for input" });
|
|
158
336
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
337
|
+
R.register("Print", (node,input) => {
|
|
338
|
+
const [str=""] = rA(node.args);
|
|
339
|
+
const msg = node.args.length > 0 ? str : (rT(node) || input.VALUE);
|
|
340
|
+
out(msg+"\n"); return okResult(msg);
|
|
341
|
+
}, { doc:"Print arg or VALUE to stdout" });
|
|
163
342
|
|
|
164
|
-
|
|
343
|
+
R.register("Warn", (node,input) => {
|
|
344
|
+
const [str=""] = rA(node.args);
|
|
345
|
+
const msg = str || input.VALUE;
|
|
346
|
+
err(env.yellow("⚠ WARN: "+msg)+"\n");
|
|
347
|
+
return mkResult(msg,1);
|
|
348
|
+
}, { doc:"Print yellow warning to stderr (sets failed=1)" });
|
|
349
|
+
|
|
350
|
+
R.register("Error_print", (node,input) => {
|
|
351
|
+
const [str=""] = rA(node.args);
|
|
352
|
+
const msg = str || input.VALUE;
|
|
353
|
+
err(env.red("✖ ERROR: "+msg)+"\n");
|
|
354
|
+
return failResult(msg);
|
|
355
|
+
}, { doc:"Print red error to stderr" });
|
|
356
|
+
|
|
357
|
+
R.register("Pause", (node,input) => {
|
|
358
|
+
const [msg="Press ENTER to continue…"] = rA(node.args);
|
|
359
|
+
promptSync(env.dim(msg)); return okResult(input.VALUE);
|
|
360
|
+
}, { doc:"Block until ENTER pressed" });
|
|
361
|
+
|
|
362
|
+
R.register("Input", (node,input) => {
|
|
363
|
+
const [promptText=""] = rA(node.args);
|
|
364
|
+
const val = promptSync(promptText ? env.cyan(promptText)+" " : "");
|
|
365
|
+
if (val===null) throw new Error("Aborted (Ctrl-C)");
|
|
366
|
+
return val.trim() ? okResult(val.trim()) : failResult("",{reason:"empty"});
|
|
367
|
+
}, { doc:"Input['prompt'] — prompt for input, pipeable inline (VALUE = trimmed input)" });
|
|
368
|
+
|
|
369
|
+
R.register("Confirm", (node,input) => {
|
|
370
|
+
const [msg="Continue?",def="y"] = rA(node.args);
|
|
371
|
+
const ans = promptSync(env.cyan(msg)+env.dim(` [${def==="y"?"Y/n":"y/N"}] `)) || def;
|
|
372
|
+
return mkResult(ans.trim(), /^y(es)?$/i.test(ans.trim()) ? 0 : 1);
|
|
373
|
+
}, { doc:"y/n prompt — Confirm['message','default']" });
|
|
374
|
+
|
|
375
|
+
R.register("Select", (node,input) => {
|
|
376
|
+
const [label="Choose",...opts] = rA(node.args);
|
|
377
|
+
out(env.bold(label)+"\n");
|
|
378
|
+
opts.forEach((o,i)=>out(` ${env.cyan(String(i+1))}. ${o}\n`));
|
|
379
|
+
const ans = promptSync(env.dim("Enter number: ")) || "";
|
|
380
|
+
const idx = parseInt(ans.trim(),10)-1;
|
|
381
|
+
return (idx<0||idx>=opts.length) ? failResult(ans,{reason:"invalid"}) : okResult(opts[idx]);
|
|
382
|
+
}, { doc:"Select['label','opt1','opt2',...] — numbered menu" });
|
|
383
|
+
|
|
384
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
385
|
+
// VARIABLES
|
|
386
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
387
|
+
|
|
388
|
+
R.register("Set", (node,input) => {
|
|
389
|
+
const [name,val=""] = rA(node.args);
|
|
390
|
+
if (!name) return failResult(input.VALUE,{reason:"Set requires a name"});
|
|
391
|
+
const value = val!=="" ? val : input.VALUE;
|
|
392
|
+
env.vars[name]=value;
|
|
393
|
+
env.logEntry("Set",`${name}=${value}`);
|
|
394
|
+
return okResult(value);
|
|
395
|
+
}, { doc:"Set['name','value'] or pipe VALUE into variable" });
|
|
396
|
+
|
|
397
|
+
R.register("Unset", (node,input) => {
|
|
398
|
+
const [name=""] = rA(node.args);
|
|
399
|
+
delete env.vars[name]; return okResult(input.VALUE);
|
|
400
|
+
}, { doc:"Delete a variable" });
|
|
401
|
+
|
|
402
|
+
R.register("Export", (node,input) => {
|
|
403
|
+
const [name,val=""] = rA(node.args);
|
|
404
|
+
const value = val!=="" ? val : input.VALUE;
|
|
405
|
+
process.env[name]=value; env.vars[name]=value;
|
|
406
|
+
return okResult(value);
|
|
407
|
+
}, { doc:"Export variable to process.env" });
|
|
408
|
+
|
|
409
|
+
R.register("Env_get", (node,input) => {
|
|
410
|
+
const [key=""] = rA(node.args);
|
|
411
|
+
const val = process.env[key] ?? "";
|
|
412
|
+
return val ? okResult(val) : failResult("",{reason:`env ${key} not set`});
|
|
413
|
+
}, { doc:"Get a process.env variable → VALUE" });
|
|
414
|
+
|
|
415
|
+
R.register("Env_set", (node,input) => {
|
|
416
|
+
const [key=""] = rA(node.args);
|
|
417
|
+
process.env[key]=input.VALUE; return okResult(input.VALUE);
|
|
418
|
+
}, { doc:"Set process.env[key] = VALUE" });
|
|
419
|
+
|
|
420
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
421
|
+
// CHECKS — all set failed=0 (pass) or failed=1 (fail)
|
|
422
|
+
// String checks operate on VALUE.
|
|
423
|
+
// Numeric / variable checks take variable names as args.
|
|
424
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
425
|
+
|
|
426
|
+
// ── String checks (on VALUE) ──────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
R.register("Check_eq", (node,input) => {
|
|
429
|
+
const [expected=""] = rA(node.args);
|
|
430
|
+
const ok = input.VALUE === expected;
|
|
431
|
+
env.logEntry("Check_eq",`"${input.VALUE}" === "${expected}" → ${ok}`);
|
|
432
|
+
return mkResult(input.VALUE, ok?0:1);
|
|
433
|
+
}, { doc:"Pass if VALUE === arg[0]" });
|
|
434
|
+
|
|
435
|
+
R.register("Check_neq", (node,input) => {
|
|
436
|
+
const [expected=""] = rA(node.args);
|
|
437
|
+
return mkResult(input.VALUE, input.VALUE!==expected?0:1);
|
|
438
|
+
}, { doc:"Pass if VALUE !== arg[0]" });
|
|
439
|
+
|
|
440
|
+
R.register("Check_contains", (node,input) => {
|
|
441
|
+
const [needle=""] = rA(node.args);
|
|
442
|
+
return mkResult(input.VALUE, input.VALUE.includes(needle)?0:1);
|
|
443
|
+
}, { doc:"Pass if VALUE contains arg[0]" });
|
|
444
|
+
|
|
445
|
+
R.register("Check_not_contains", (node,input) => {
|
|
446
|
+
const [needle=""] = rA(node.args);
|
|
447
|
+
return mkResult(input.VALUE, !input.VALUE.includes(needle)?0:1);
|
|
448
|
+
}, { doc:"Pass if VALUE does NOT contain arg[0]" });
|
|
449
|
+
|
|
450
|
+
R.register("Check_starts", (node,input) => {
|
|
451
|
+
const [prefix=""] = rA(node.args);
|
|
452
|
+
return mkResult(input.VALUE, input.VALUE.startsWith(prefix)?0:1);
|
|
453
|
+
}, { doc:"Pass if VALUE starts with arg[0]" });
|
|
454
|
+
|
|
455
|
+
R.register("Check_ends", (node,input) => {
|
|
456
|
+
const [suffix=""] = rA(node.args);
|
|
457
|
+
return mkResult(input.VALUE, input.VALUE.endsWith(suffix)?0:1);
|
|
458
|
+
}, { doc:"Pass if VALUE ends with arg[0]" });
|
|
459
|
+
|
|
460
|
+
R.register("Check_matches", (node,input) => {
|
|
461
|
+
const [pattern=".*",flags=""] = rA(node.args);
|
|
462
|
+
return mkResult(input.VALUE, new RegExp(pattern,flags).test(input.VALUE)?0:1);
|
|
463
|
+
}, { doc:"Pass if VALUE matches regex arg[0]" });
|
|
464
|
+
|
|
465
|
+
R.register("Check_empty", (node,input) => {
|
|
466
|
+
return mkResult(input.VALUE, input.VALUE===""?0:1);
|
|
467
|
+
}, { doc:"Pass if VALUE is empty string" });
|
|
468
|
+
|
|
469
|
+
R.register("Check_nonempty", (node,input) => {
|
|
470
|
+
return mkResult(input.VALUE, input.VALUE!==""?0:1);
|
|
471
|
+
}, { doc:"Pass if VALUE is not empty" });
|
|
472
|
+
|
|
473
|
+
// ── Numeric checks (arg = variable name, compared against a literal) ──────
|
|
474
|
+
|
|
475
|
+
R.register("Check_gt", (node,input) => {
|
|
476
|
+
// Check_gt['varname','number'] OR Check_gt['number'] (uses VALUE as number)
|
|
477
|
+
const [a,b] = rA(node.args);
|
|
478
|
+
if (b!==undefined) {
|
|
479
|
+
const left=Number(env.vars[a]??a), right=Number(b);
|
|
480
|
+
return mkResult(input.VALUE, left>right?0:1);
|
|
481
|
+
}
|
|
482
|
+
return mkResult(input.VALUE, Number(input.VALUE)>Number(a)?0:1);
|
|
483
|
+
}, { doc:"Pass if var/VALUE > number — Check_gt['var','n'] or Check_gt['n']" });
|
|
484
|
+
|
|
485
|
+
R.register("Check_gte", (node,input) => {
|
|
486
|
+
const [a,b] = rA(node.args);
|
|
487
|
+
if (b!==undefined) {
|
|
488
|
+
return mkResult(input.VALUE, Number(env.vars[a]??a)>=Number(b)?0:1);
|
|
489
|
+
}
|
|
490
|
+
return mkResult(input.VALUE, Number(input.VALUE)>=Number(a)?0:1);
|
|
491
|
+
}, { doc:"Pass if var/VALUE >= number" });
|
|
492
|
+
|
|
493
|
+
R.register("Check_lt", (node,input) => {
|
|
494
|
+
const [a,b] = rA(node.args);
|
|
495
|
+
if (b!==undefined) {
|
|
496
|
+
return mkResult(input.VALUE, Number(env.vars[a]??a)<Number(b)?0:1);
|
|
497
|
+
}
|
|
498
|
+
return mkResult(input.VALUE, Number(input.VALUE)<Number(a)?0:1);
|
|
499
|
+
}, { doc:"Pass if var/VALUE < number" });
|
|
500
|
+
|
|
501
|
+
R.register("Check_lte", (node,input) => {
|
|
502
|
+
const [a,b] = rA(node.args);
|
|
503
|
+
if (b!==undefined) {
|
|
504
|
+
return mkResult(input.VALUE, Number(env.vars[a]??a)<=Number(b)?0:1);
|
|
505
|
+
}
|
|
506
|
+
return mkResult(input.VALUE, Number(input.VALUE)<=Number(a)?0:1);
|
|
507
|
+
}, { doc:"Pass if var/VALUE <= number" });
|
|
508
|
+
|
|
509
|
+
R.register("Check_num_eq", (node,input) => {
|
|
510
|
+
const [a,b] = rA(node.args);
|
|
511
|
+
if (b!==undefined) {
|
|
512
|
+
return mkResult(input.VALUE, Number(env.vars[a]??a)===Number(b)?0:1);
|
|
513
|
+
}
|
|
514
|
+
return mkResult(input.VALUE, Number(input.VALUE)===Number(a)?0:1);
|
|
515
|
+
}, { doc:"Pass if var/VALUE == number (numeric equality)" });
|
|
516
|
+
|
|
517
|
+
R.register("Check_num_neq", (node,input) => {
|
|
518
|
+
const [a,b] = rA(node.args);
|
|
519
|
+
if (b!==undefined) {
|
|
520
|
+
return mkResult(input.VALUE, Number(env.vars[a]??a)!==Number(b)?0:1);
|
|
521
|
+
}
|
|
522
|
+
return mkResult(input.VALUE, Number(input.VALUE)!==Number(a)?0:1);
|
|
523
|
+
}, { doc:"Pass if var/VALUE != number" });
|
|
524
|
+
|
|
525
|
+
R.register("Check_between", (node,input) => {
|
|
526
|
+
const [a,lo,hi] = rA(node.args);
|
|
527
|
+
if (lo!==undefined && hi!==undefined) {
|
|
528
|
+
const v=Number(env.vars[a]??a);
|
|
529
|
+
return mkResult(input.VALUE, v>=Number(lo)&&v<=Number(hi)?0:1);
|
|
530
|
+
}
|
|
531
|
+
const v=Number(input.VALUE);
|
|
532
|
+
return mkResult(input.VALUE, v>=Number(a)&&v<=Number(lo)?0:1);
|
|
533
|
+
}, { doc:"Pass if var/VALUE between lo and hi (inclusive) — Check_between['var','lo','hi']" });
|
|
534
|
+
|
|
535
|
+
// ── Boolean / logical checks ──────────────────────────────────────────────
|
|
536
|
+
|
|
537
|
+
R.register("Check_failed", (node,input) => {
|
|
538
|
+
return mkResult(input.VALUE, input.failed?0:1);
|
|
539
|
+
}, { doc:"Pass if previous result was failed" });
|
|
540
|
+
|
|
541
|
+
R.register("Check_passed", (node,input) => {
|
|
542
|
+
return mkResult(input.VALUE, !input.failed?0:1);
|
|
543
|
+
}, { doc:"Pass if previous result was passed" });
|
|
544
|
+
|
|
545
|
+
R.register("Check_var_eq", (node,input) => {
|
|
546
|
+
// Compare two variables by name
|
|
547
|
+
const [a,b] = rA(node.args);
|
|
548
|
+
return mkResult(input.VALUE, (env.vars[a]??"")===(env.vars[b]??"")?0:1);
|
|
549
|
+
}, { doc:"Pass if var a === var b — Check_var_eq['a','b']" });
|
|
550
|
+
|
|
551
|
+
R.register("Check_var_neq", (node,input) => {
|
|
552
|
+
const [a,b] = rA(node.args);
|
|
553
|
+
return mkResult(input.VALUE, (env.vars[a]??"")===(env.vars[b]??"")?1:0);
|
|
554
|
+
}, { doc:"Pass if var a !== var b" });
|
|
555
|
+
|
|
556
|
+
// ── Error gates ───────────────────────────────────────────────────────────
|
|
557
|
+
|
|
558
|
+
R.register("Check_failed_error", (node,input) => {
|
|
559
|
+
const [msg="Check failed"] = rA(node.args);
|
|
560
|
+
if (input.failed) throw new ProDucsError(msg, node.loc);
|
|
561
|
+
return input;
|
|
562
|
+
}, { doc:"Throw if previous result is failed" });
|
|
563
|
+
|
|
564
|
+
R.register("Check_passed_error", (node,input) => {
|
|
565
|
+
const [msg="Unexpected pass"] = rA(node.args);
|
|
566
|
+
if (!input.failed) throw new ProDucsError(msg, node.loc);
|
|
567
|
+
return input;
|
|
568
|
+
}, { doc:"Throw if previous result passed" });
|
|
569
|
+
|
|
570
|
+
// ── Assert (arbitrary JS expression using vars) ───────────────────────────
|
|
571
|
+
R.register("Assert", (node,input) => {
|
|
572
|
+
const [expr="",msg="Assertion failed"] = rA(node.args);
|
|
573
|
+
try {
|
|
574
|
+
const ok = R.evalExpr(expr, {VALUE:input.VALUE});
|
|
575
|
+
if (!ok) throw new ProDucsError(msg, node.loc);
|
|
576
|
+
return input;
|
|
577
|
+
} catch(e) {
|
|
578
|
+
if (e instanceof ProDucsError) throw e;
|
|
579
|
+
throw new ProDucsError(`Assert eval error: ${e.message}`, node.loc);
|
|
580
|
+
}
|
|
581
|
+
}, { doc:"Assert['JS expr','msg'] — throw if expression is falsy" });
|
|
582
|
+
|
|
583
|
+
// ── Guards ────────────────────────────────────────────────────────────────
|
|
584
|
+
R.register("Require_env", (node,input) => {
|
|
585
|
+
const [key=""] = rA(node.args);
|
|
586
|
+
if (!process.env[key])
|
|
587
|
+
throw new ProDucsError(`Required env var not set: ${key}`, node.loc);
|
|
588
|
+
return okResult(process.env[key]);
|
|
589
|
+
}, { doc:"Halt if env var is missing" });
|
|
590
|
+
|
|
591
|
+
R.register("Require_cmd", (node,input) => {
|
|
592
|
+
const [cmd=""] = rA(node.args);
|
|
593
|
+
try { execSync(`command -v ${cmd}`,{stdio:"ignore"}); return okResult(cmd); }
|
|
594
|
+
catch { throw new ProDucsError(`Required command not found: ${cmd}`, node.loc); }
|
|
595
|
+
}, { doc:"Halt if shell command not in PATH" });
|
|
596
|
+
|
|
597
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
598
|
+
// SHELL
|
|
599
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
600
|
+
|
|
601
|
+
R.register("Shell_run", (node,input) => {
|
|
602
|
+
const cmd = itp(rT(node)||input.VALUE);
|
|
603
|
+
if (!cmd) return failResult("",{reason:"no command"});
|
|
604
|
+
try {
|
|
605
|
+
execSync(cmd,{stdio:"inherit",env:{...process.env,...env.vars}});
|
|
606
|
+
return okResult(cmd);
|
|
607
|
+
} catch(e) {
|
|
608
|
+
if (env.strict) throw e;
|
|
609
|
+
return failResult(cmd,{reason:e.message, code:e.status});
|
|
610
|
+
}
|
|
611
|
+
}, { doc:"Run VALUE or text child as shell command" });
|
|
612
|
+
|
|
613
|
+
R.register("Shell_capture", (node,input) => {
|
|
614
|
+
const cmd = itp(rT(node)||input.VALUE);
|
|
615
|
+
if (!cmd) return failResult("",{reason:"no command"});
|
|
616
|
+
try {
|
|
617
|
+
const stdout = execSync(cmd,{encoding:"utf8",env:{...process.env,...env.vars}});
|
|
618
|
+
return okResult(stdout.trim());
|
|
619
|
+
} catch(e) {
|
|
620
|
+
if (env.strict) throw e;
|
|
621
|
+
return failResult(e.stdout?.trim()||"",{reason:e.message});
|
|
622
|
+
}
|
|
623
|
+
}, { doc:"Run command, capture stdout → VALUE" });
|
|
624
|
+
|
|
625
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
626
|
+
// STRING OPS
|
|
627
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
628
|
+
|
|
629
|
+
R.register("Trim", (_,i)=>okResult(i.VALUE.trim()), {doc:"Trim whitespace from VALUE"});
|
|
630
|
+
R.register("Upper", (_,i)=>okResult(i.VALUE.toUpperCase()), {doc:"Uppercase VALUE"});
|
|
631
|
+
R.register("Lower", (_,i)=>okResult(i.VALUE.toLowerCase()), {doc:"Lowercase VALUE"});
|
|
632
|
+
R.register("Len", (_,i)=>okResult(String(i.VALUE.length)),{doc:"String length → VALUE"});
|
|
633
|
+
|
|
634
|
+
R.register("Replace", (node,input) => {
|
|
635
|
+
const [from="",to="",flags="g"] = rA(node.args);
|
|
636
|
+
return okResult(input.VALUE.replace(new RegExp(from,flags),to));
|
|
637
|
+
}, { doc:"Replace['pattern','replacement','flags']" });
|
|
638
|
+
|
|
639
|
+
R.register("Slice", (node,input) => {
|
|
640
|
+
const [start="0",end=""] = rA(node.args);
|
|
641
|
+
return okResult(end!=="" ? input.VALUE.slice(Number(start),Number(end)) : input.VALUE.slice(Number(start)));
|
|
642
|
+
}, { doc:"Slice['start','end']" });
|
|
643
|
+
|
|
644
|
+
R.register("Split", (node,input) => {
|
|
645
|
+
const [sep=","] = rA(node.args);
|
|
646
|
+
return okResult(JSON.stringify(input.VALUE.split(sep)));
|
|
647
|
+
}, { doc:"Split VALUE by sep → JSON array string" });
|
|
648
|
+
|
|
649
|
+
R.register("Join", (node,input) => {
|
|
650
|
+
const [sep=","] = rA(node.args);
|
|
651
|
+
let arr; try { arr=JSON.parse(input.VALUE); } catch { arr=[input.VALUE]; }
|
|
652
|
+
return okResult(arr.join(sep));
|
|
653
|
+
}, { doc:"Join JSON array VALUE by separator" });
|
|
654
|
+
|
|
655
|
+
R.register("Default", (node,input) => {
|
|
656
|
+
const [fallback=""] = rA(node.args);
|
|
657
|
+
return okResult(input.VALUE!=="" ? input.VALUE : fallback);
|
|
658
|
+
}, { doc:"Replace empty VALUE with fallback" });
|
|
659
|
+
|
|
660
|
+
R.register("Concat", (node,input) => {
|
|
661
|
+
const parts = rA(node.args);
|
|
662
|
+
return okResult(input.VALUE + parts.join(""));
|
|
663
|
+
}, { doc:"Concat['suffix1','suffix2',...] — append to VALUE" });
|
|
664
|
+
|
|
665
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
666
|
+
// NUMERIC / MATH
|
|
667
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
668
|
+
|
|
669
|
+
R.register("Int", (_,i)=>okResult(String(parseInt(i.VALUE,10)||0)));
|
|
670
|
+
R.register("Float", (_,i)=>okResult(String(parseFloat(i.VALUE)||0)));
|
|
671
|
+
R.register("Abs", (_,i)=>okResult(String(Math.abs(Number(i.VALUE)))));
|
|
672
|
+
R.register("Round", (_,i)=>okResult(String(Math.round(Number(i.VALUE)))));
|
|
673
|
+
R.register("Floor", (_,i)=>okResult(String(Math.floor(Number(i.VALUE)))));
|
|
674
|
+
R.register("Ceil", (_,i)=>okResult(String(Math.ceil(Number(i.VALUE)))));
|
|
675
|
+
|
|
676
|
+
R.register("Math", (node,input) => {
|
|
677
|
+
const [expr=""] = rA(node.args);
|
|
678
|
+
const expression = expr || input.VALUE;
|
|
679
|
+
try {
|
|
680
|
+
return okResult(String(R.evalExpr(expression,{VALUE:input.VALUE})));
|
|
681
|
+
} catch(e) {
|
|
682
|
+
return failResult("NaN",{reason:e.message});
|
|
683
|
+
}
|
|
684
|
+
}, { doc:"Math['expr'] — evaluate JS math using vars as identifiers" });
|
|
685
|
+
|
|
686
|
+
R.register("Inc", (node,input) => {
|
|
687
|
+
const [name="",step="1"] = rA(node.args);
|
|
688
|
+
const key = name || input.VALUE;
|
|
689
|
+
const newV = (Number(env.vars[key]||0)+Number(step));
|
|
690
|
+
env.vars[key]=String(newV);
|
|
691
|
+
return okResult(String(newV));
|
|
692
|
+
}, { doc:"Increment a variable: Inc['varname','step']" });
|
|
693
|
+
|
|
694
|
+
R.register("Dec", (node,input) => {
|
|
695
|
+
const [name="",step="1"] = rA(node.args);
|
|
696
|
+
const key = name || input.VALUE;
|
|
697
|
+
const newV = (Number(env.vars[key]||0)-Number(step));
|
|
698
|
+
env.vars[key]=String(newV);
|
|
699
|
+
return okResult(String(newV));
|
|
700
|
+
}, { doc:"Decrement a variable: Dec['varname','step']" });
|
|
701
|
+
|
|
702
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
703
|
+
// DATA / CRYPTO
|
|
704
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
705
|
+
|
|
706
|
+
R.register("Hash", (node,input) => {
|
|
707
|
+
const [algo="md5"] = rA(node.args);
|
|
708
|
+
try { return okResult(crypto.createHash(algo).update(input.VALUE).digest("hex")); }
|
|
709
|
+
catch(e) { return failResult("",{reason:e.message}); }
|
|
710
|
+
}, { doc:"Hash['algo'] VALUE (default md5)" });
|
|
711
|
+
|
|
712
|
+
R.register("Timestamp", (node,input) => {
|
|
713
|
+
const [fmt="iso"] = rA(node.args);
|
|
714
|
+
const now=new Date();
|
|
715
|
+
return okResult(fmt==="unix" ? String(Math.floor(now/1000)) : fmt==="ms" ? String(now.getTime()) : now.toISOString());
|
|
716
|
+
}, { doc:"Timestamp['iso'|'unix'|'ms']" });
|
|
717
|
+
|
|
718
|
+
R.register("Json_get", (node,input) => {
|
|
719
|
+
const [keyPath=""] = rA(node.args);
|
|
720
|
+
try {
|
|
721
|
+
const obj=JSON.parse(input.VALUE);
|
|
722
|
+
const val=keyPath.split(".").reduce((o,k)=>o?.[k],obj);
|
|
723
|
+
return val!==undefined ? okResult(JSON.stringify(val)) : failResult("",{reason:"key not found"});
|
|
724
|
+
} catch(e) { return failResult("",{reason:e.message}); }
|
|
725
|
+
}, { doc:"Get dot-path from JSON VALUE: Json_get['a.b.c']" });
|
|
726
|
+
|
|
727
|
+
R.register("Json_set", (node,input) => {
|
|
728
|
+
const [keyPath="",val=""] = rA(node.args);
|
|
729
|
+
try {
|
|
730
|
+
const obj=JSON.parse(input.VALUE);
|
|
731
|
+
const keys=keyPath.split(".");
|
|
732
|
+
let cur=obj;
|
|
733
|
+
for (let i=0;i<keys.length-1;i++) cur=(cur[keys[i]]??={});
|
|
734
|
+
cur[keys.at(-1)]=val;
|
|
735
|
+
return okResult(JSON.stringify(obj));
|
|
736
|
+
} catch(e) { return failResult(input.VALUE,{reason:e.message}); }
|
|
737
|
+
}, { doc:"Set dot-path in JSON VALUE: Json_set['a.b','val']" });
|
|
738
|
+
|
|
739
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
740
|
+
// FILE I/O
|
|
741
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
742
|
+
|
|
743
|
+
R.register("Read_file", (node,input) => {
|
|
744
|
+
const [p=""] = rA(node.args);
|
|
745
|
+
const path=p||input.VALUE;
|
|
746
|
+
try { return okResult(fs.readFileSync(path,"utf8").trim()); }
|
|
747
|
+
catch(e) { return failResult("",{reason:e.message}); }
|
|
748
|
+
}, { doc:"Read file → VALUE" });
|
|
749
|
+
|
|
750
|
+
R.register("Write_file", (node,input) => {
|
|
751
|
+
const [p=""] = rA(node.args);
|
|
752
|
+
fs.writeFileSync(p, rT(node)||input.VALUE, "utf8");
|
|
753
|
+
return okResult(p);
|
|
754
|
+
}, { doc:"Write VALUE to file: Write_file['path']" });
|
|
755
|
+
|
|
756
|
+
R.register("Append_file", (node,input) => {
|
|
757
|
+
const [p=""] = rA(node.args);
|
|
758
|
+
fs.appendFileSync(p, (rT(node)||input.VALUE)+"\n","utf8");
|
|
759
|
+
return okResult(p);
|
|
760
|
+
}, { doc:"Append VALUE to file" });
|
|
761
|
+
|
|
762
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
763
|
+
// CONTROL FLOW
|
|
764
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
765
|
+
|
|
766
|
+
R.register("Goto", (node,input) => {
|
|
767
|
+
const [label=""] = rA(node.args);
|
|
768
|
+
if (!label) throw new ProDucsError("Goto requires a label name", node.loc);
|
|
769
|
+
throw new GotoSignal(label);
|
|
770
|
+
}, { doc:"Goto['label'] — jump to matching Label" });
|
|
771
|
+
|
|
772
|
+
R.register("Label", (node,input) => input,
|
|
773
|
+
{ doc:"Label['name'] — jump target for Goto" });
|
|
774
|
+
|
|
775
|
+
R.register("Exit", (node,input) => {
|
|
776
|
+
const [code="0"] = rA(node.args);
|
|
777
|
+
process.exit(Number(code)||0);
|
|
778
|
+
}, { doc:"Exit['code'] — terminate process" });
|
|
779
|
+
|
|
780
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
781
|
+
// TIMING
|
|
782
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
783
|
+
|
|
784
|
+
R.register("Wait", (node,input) => {
|
|
785
|
+
const [ms="0"] = rA(node.args);
|
|
786
|
+
const n=Math.max(0,Number(ms)||0);
|
|
787
|
+
const end=Date.now()+n;
|
|
788
|
+
while (Date.now()<end) {}
|
|
789
|
+
return okResult(String(n));
|
|
790
|
+
}, { doc:"Wait['ms'] — blocking sleep in milliseconds" });
|
|
791
|
+
|
|
792
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
793
|
+
// LOGGING
|
|
794
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
795
|
+
|
|
796
|
+
R.register("Log", (node,input) => {
|
|
797
|
+
const [label="LOG"] = rA(node.args);
|
|
798
|
+
env.logEntry(label, input.VALUE, !input.failed);
|
|
799
|
+
return input;
|
|
800
|
+
}, { doc:"Append VALUE+status to execution log" });
|
|
801
|
+
|
|
802
|
+
R.register("Dump_log", (node,input) => {
|
|
803
|
+
out(env.bold("\n─── Execution Log ───\n"));
|
|
804
|
+
env.log.forEach(e=>{
|
|
805
|
+
const ts=env.dim(`[+${e.t}ms]`);
|
|
806
|
+
const sym=e.ok ? env.green("✔") : env.red("✖");
|
|
807
|
+
out(`${ts} ${sym} ${env.bold(e.cmd)}: ${e.msg}\n`);
|
|
808
|
+
});
|
|
809
|
+
return okResult(String(env.log.length));
|
|
810
|
+
}, { doc:"Print execution log to stdout" });
|
|
811
|
+
|
|
812
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
813
|
+
// VISUAL
|
|
814
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
815
|
+
|
|
816
|
+
R.register("HR", (node,input) => {
|
|
817
|
+
const [ch="─"] = rA(node.args);
|
|
818
|
+
out(env.dim(env.hr(ch))+"\n"); return okResult(input.VALUE);
|
|
819
|
+
}, { doc:"Print full-width horizontal rule" });
|
|
820
|
+
|
|
821
|
+
R.register("Header", (node,input) => {
|
|
822
|
+
const [title=""] = rA(node.args);
|
|
823
|
+
const t=title||input.VALUE, line=env.hr("═");
|
|
824
|
+
out(`\n${env.col(C.bold+C.cyan,line)}\n`);
|
|
825
|
+
out(`${env.col(C.bold+C.cyan," "+t)}\n`);
|
|
826
|
+
out(`${env.col(C.bold+C.cyan,line)}\n\n`);
|
|
827
|
+
return okResult(t);
|
|
828
|
+
}, { doc:"Print prominent banner header" });
|
|
829
|
+
|
|
830
|
+
R.register("Color", (node,input) => {
|
|
831
|
+
const [colorName="white",str=""] = rA(node.args);
|
|
832
|
+
const msg=str||input.VALUE;
|
|
833
|
+
out(env.col(C[colorName]||C.white,msg)+"\n");
|
|
834
|
+
return okResult(msg);
|
|
835
|
+
}, { doc:"Color['name','text'] — print in ANSI color" });
|
|
836
|
+
|
|
837
|
+
R.register("Progress", (node,input) => {
|
|
838
|
+
const [pct="0",width="40",label=""] = rA(node.args);
|
|
839
|
+
const p=Math.min(100,Math.max(0,Number(pct)));
|
|
840
|
+
const w=Number(width), fill=Math.round(p/100*w);
|
|
841
|
+
const bar=env.col(C.green,"█".repeat(fill))+env.dim("░".repeat(w-fill));
|
|
842
|
+
out(`${bar} ${env.bold(String(p)+"%")} ${label}\n`);
|
|
843
|
+
return okResult(String(p));
|
|
844
|
+
}, { doc:"Progress['percent','width','label']" });
|
|
845
|
+
|
|
846
|
+
R.register("Spinner", (node,input) => {
|
|
847
|
+
const [msg="Working…"] = rA(node.args);
|
|
848
|
+
out(env.cyan("⠿ ")+msg+"\n"); return okResult(input.VALUE);
|
|
849
|
+
}, { doc:"Print spinner-style status line" });
|
|
850
|
+
|
|
851
|
+
R.register("Status", (node,input) => {
|
|
852
|
+
const [label=""] = rA(node.args);
|
|
853
|
+
const ok=!input.failed;
|
|
854
|
+
const sym=ok ? env.green("✔") : env.red("✖");
|
|
855
|
+
const tag=ok ? env.col(C.bggreen+C.white," OK ") : env.col(C.bgred+C.white," FAIL ");
|
|
856
|
+
out(`${sym} ${tag} ${label||input.VALUE}\n`);
|
|
857
|
+
return input;
|
|
858
|
+
}, { doc:"Print OK/FAIL badge for current result" });
|
|
859
|
+
|
|
860
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
861
|
+
// RUNTIME CONTROL
|
|
862
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
863
|
+
|
|
864
|
+
R.register("Debug", (node,input) => {
|
|
865
|
+
const [onoff="on"] = rA(node.args);
|
|
866
|
+
env.debug=onoff!=="off";
|
|
867
|
+
err(env.dim(`[debug ${env.debug?"ON":"OFF"}]\n`));
|
|
868
|
+
return okResult(input.VALUE);
|
|
869
|
+
}, { doc:"Debug['on'|'off'] — toggle debug output" });
|
|
870
|
+
|
|
871
|
+
R.register("Strict", (node,input) => {
|
|
872
|
+
const [onoff="on"] = rA(node.args);
|
|
873
|
+
env.strict=onoff!=="off"; return okResult(input.VALUE);
|
|
874
|
+
}, { doc:"Strict['on'|'off'] — halt on shell failure" });
|
|
875
|
+
|
|
876
|
+
R.register("Color_off", (node,input) => {
|
|
877
|
+
env.colorOff=true; return okResult(input.VALUE);
|
|
878
|
+
}, { doc:"Disable ANSI colors" });
|
|
879
|
+
|
|
880
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
881
|
+
// META
|
|
882
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
883
|
+
|
|
884
|
+
R.register("Help", (node,input) => {
|
|
885
|
+
out(env.col(C.bold+C.cyan,"\nProDucs Commands\n"));
|
|
886
|
+
out(env.dim(env.hr()+"\n"));
|
|
887
|
+
for (const [name,def] of this._cmds)
|
|
888
|
+
out(` ${env.bold(pad(name,26))}${env.dim(def.doc||"")}\n`);
|
|
889
|
+
out("\n"); return okResult(input.VALUE);
|
|
890
|
+
}, { doc:"List all commands with descriptions" });
|
|
891
|
+
|
|
892
|
+
R.register("Version", (node,input) => {
|
|
893
|
+
const v="ProDucs v3.0.0";
|
|
894
|
+
out(env.cyan(v)+"\n"); return okResult(v);
|
|
895
|
+
}, { doc:"Print ProDucs version" });
|
|
896
|
+
|
|
897
|
+
// Block stubs (for Help only — executed by Executor)
|
|
898
|
+
const stub=(name,doc)=>R.register(name,null,{isBlock:true,doc});
|
|
899
|
+
stub(">", "Prompt block — VALUE piped into children");
|
|
900
|
+
stub("Branch", "Branch — runs children only when failed=0 (check passed)");
|
|
901
|
+
stub("BranchElse", "BranchElse — runs children only when failed=1 (check failed)");
|
|
902
|
+
stub("Repeat", "Repeat['N'] — loop N times (_i available as index)");
|
|
903
|
+
stub("ForEach", "ForEach['var','a','b',...] — iterate over items");
|
|
904
|
+
stub("Try", "Try — errors caught by sibling Catch block");
|
|
905
|
+
stub("Catch", "Catch — runs when Try threw (_error set)");
|
|
906
|
+
stub("Section", "Section['title'] — formatted section heading");
|
|
907
|
+
stub("Step", "Step['desc'] — auto-numbered step");
|
|
908
|
+
stub("Group", "Group['label'] — logical grouping with bracket lines");
|
|
909
|
+
stub("Table", "Table — ASCII table; Row children define rows");
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
914
|
+
// EXECUTOR
|
|
915
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
916
|
+
class Executor {
|
|
917
|
+
constructor(registry, env) { this.registry=registry; this.env=env; }
|
|
918
|
+
|
|
919
|
+
/** Pre-pass: walk all node lists and register Label positions */
|
|
920
|
+
indexLabels(nodes) {
|
|
921
|
+
for (const node of nodes) {
|
|
922
|
+
if (node.type==="Label") {
|
|
923
|
+
const [name=""] = this.registry.resolveArgs(node.args);
|
|
924
|
+
if (name) this.env.labels[name]={node, list:nodes};
|
|
925
|
+
}
|
|
926
|
+
if (node.children?.length) this.indexLabels(node.children);
|
|
927
|
+
}
|
|
165
928
|
}
|
|
166
929
|
|
|
167
930
|
execute(ast) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
931
|
+
this.indexLabels(ast);
|
|
932
|
+
return this.executeList(ast);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Execute a flat node list. Catches GotoSignal if the target label's
|
|
937
|
+
* canonical list is THIS list; otherwise re-throws so a parent list handles it.
|
|
938
|
+
*/
|
|
939
|
+
executeList(nodes, input=mkResult(), start=0, end=nodes.length) {
|
|
940
|
+
let result=input, i=start;
|
|
941
|
+
while (i<end) {
|
|
942
|
+
try {
|
|
943
|
+
result=this.executeNode(nodes[i], result);
|
|
944
|
+
i++;
|
|
945
|
+
} catch(signal) {
|
|
946
|
+
if (!(signal instanceof GotoSignal)) throw signal;
|
|
947
|
+
const entry=this.env.labels[signal.label];
|
|
948
|
+
if (!entry) throw new ProDucsError(`Goto: label '${signal.label}' not found`);
|
|
949
|
+
if (entry.list!==nodes) throw signal; // propagate to parent list
|
|
950
|
+
i=nodes.indexOf(entry.node)+1; // jump past the Label node
|
|
951
|
+
}
|
|
171
952
|
}
|
|
172
953
|
return result;
|
|
173
954
|
}
|
|
955
|
+
|
|
956
|
+
executeNode(node, input=mkResult()) {
|
|
957
|
+
this.env.stepNum++;
|
|
958
|
+
|
|
959
|
+
// ── Text ──────────────────────────────────────────────────────────────────
|
|
960
|
+
if (node.type==="Text") {
|
|
961
|
+
const msg=this.env.interpolate(node.value);
|
|
962
|
+
if (!msg.trim()) return okResult(msg);
|
|
963
|
+
out(msg+"\n"); return okResult(msg);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// ── Branch (run if failed===0) ────────────────────────────────────────────
|
|
967
|
+
if (node.type==="Branch") {
|
|
968
|
+
if (!input.failed) return this.executeList(node.children, input);
|
|
969
|
+
return input;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// ── BranchElse (run if failed===1) ────────────────────────────────────────
|
|
973
|
+
if (node.type==="BranchElse") {
|
|
974
|
+
if (input.failed) return this.executeList(node.children, input);
|
|
975
|
+
return input;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ── Repeat ────────────────────────────────────────────────────────────────
|
|
979
|
+
if (node.type==="Repeat") {
|
|
980
|
+
const [n="1"] = this.registry.resolveArgs(node.args);
|
|
981
|
+
const count=Math.max(0,parseInt(this.env.interpolate(n),10)||0);
|
|
982
|
+
let cur=input;
|
|
983
|
+
for (let i=0;i<count;i++) {
|
|
984
|
+
this.env.vars["_i"]=String(i);
|
|
985
|
+
cur=this.executeList(node.children,cur);
|
|
986
|
+
}
|
|
987
|
+
return cur;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// ── ForEach ───────────────────────────────────────────────────────────────
|
|
991
|
+
if (node.type==="ForEach") {
|
|
992
|
+
const [varName="_item",...items] = this.registry.resolveArgs(node.args);
|
|
993
|
+
let cur=input;
|
|
994
|
+
for (const item of items) {
|
|
995
|
+
this.env.vars[varName]=this.env.interpolate(item);
|
|
996
|
+
cur=this.executeList(node.children,cur);
|
|
997
|
+
}
|
|
998
|
+
return cur;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ── Try / Catch ───────────────────────────────────────────────────────────
|
|
1002
|
+
if (node.type==="Try") {
|
|
1003
|
+
const catchIdx=node.children.findIndex(c=>c.type==="Catch");
|
|
1004
|
+
const tryEnd=catchIdx===-1 ? node.children.length : catchIdx;
|
|
1005
|
+
let cur=input;
|
|
1006
|
+
try {
|
|
1007
|
+
cur=this.executeList(node.children,cur,0,tryEnd);
|
|
1008
|
+
} catch(e) {
|
|
1009
|
+
if (e instanceof GotoSignal) throw e;
|
|
1010
|
+
if (catchIdx!==-1) {
|
|
1011
|
+
this.env.vars["_error"]=e.message;
|
|
1012
|
+
cur=this.executeList(node.children[catchIdx].children, failResult(e.message));
|
|
1013
|
+
} else throw e;
|
|
1014
|
+
}
|
|
1015
|
+
return cur;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// ── Section ───────────────────────────────────────────────────────────────
|
|
1019
|
+
if (node.type==="Section") {
|
|
1020
|
+
const [title=""] = this.registry.resolveArgs(node.args);
|
|
1021
|
+
const t=this.env.interpolate(title);
|
|
1022
|
+
out(`\n${this.env.col(C.bold+C.blue,"▶ "+t)}\n`);
|
|
1023
|
+
out(this.env.col(C.blue,this.env.hr("─"))+"\n");
|
|
1024
|
+
return this.executeList(node.children, input);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// ── Step ──────────────────────────────────────────────────────────────────
|
|
1028
|
+
if (node.type==="Step") {
|
|
1029
|
+
const [desc=""] = this.registry.resolveArgs(node.args);
|
|
1030
|
+
out(`\n${this.env.col(C.bold+C.cyan,`[${this.env.stepNum}]`)} `+
|
|
1031
|
+
`${this.env.bold(this.env.interpolate(desc))}\n`);
|
|
1032
|
+
return this.executeList(node.children, input);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ── Group ─────────────────────────────────────────────────────────────────
|
|
1036
|
+
if (node.type==="Group") {
|
|
1037
|
+
const [label=""] = this.registry.resolveArgs(node.args);
|
|
1038
|
+
const l=this.env.interpolate(label);
|
|
1039
|
+
if (l) out(this.env.dim(`┌── ${l} ──\n`));
|
|
1040
|
+
const cur=this.executeList(node.children, input);
|
|
1041
|
+
if (l) out(this.env.dim("└──\n"));
|
|
1042
|
+
return cur;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// ── Table ─────────────────────────────────────────────────────────────────
|
|
1046
|
+
if (node.type==="Table") {
|
|
1047
|
+
const rowNodes=node.children.filter(c=>c.type==="Row");
|
|
1048
|
+
if (!rowNodes.length) return input;
|
|
1049
|
+
const rows=rowNodes.map(r=>this.registry.resolveArgs(r.args));
|
|
1050
|
+
const ncols=rows[0].length;
|
|
1051
|
+
const widths=Array.from({length:ncols},(_,ci)=>Math.max(...rows.map(r=>(r[ci]||"").length)));
|
|
1052
|
+
const sep="┼"+widths.map(w=>"─".repeat(w+2)).join("┼")+"┼";
|
|
1053
|
+
const fmt=(row,isHdr)=>"│"+row.map((c,i)=>{
|
|
1054
|
+
const s=pad(c||"",widths[i]);
|
|
1055
|
+
return " "+(isHdr?this.env.bold(s):s)+" ";
|
|
1056
|
+
}).join("│")+"│";
|
|
1057
|
+
out("\n");
|
|
1058
|
+
rows.forEach((row,ri)=>{
|
|
1059
|
+
if (ri===1) out(this.env.dim(sep)+"\n");
|
|
1060
|
+
out(fmt(row,ri===0)+"\n");
|
|
1061
|
+
});
|
|
1062
|
+
out("\n");
|
|
1063
|
+
return okResult(String(rows.length));
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// ── Inline command + pipe chain ───────────────────────────────────────────
|
|
1067
|
+
const def=this.registry.get(node.type);
|
|
1068
|
+
if (!def) throw new ProDucsError(`Unknown command: ${node.type}`, node.loc);
|
|
1069
|
+
if (def.isBlock) throw new ProDucsError(
|
|
1070
|
+
`Block command '${node.type}' must be closed with #$End_${node.type}`, node.loc);
|
|
1071
|
+
|
|
1072
|
+
let current=def.fn(node,input);
|
|
1073
|
+
for (const pipe of (node.pipes||[])) {
|
|
1074
|
+
const pdef=this.registry.get(pipe.type);
|
|
1075
|
+
if (!pdef) throw new ProDucsError(`Unknown piped command: ${pipe.type}`, pipe.loc);
|
|
1076
|
+
if (pdef.isBlock) throw new ProDucsError(
|
|
1077
|
+
`Block command '${pipe.type}' cannot be a pipe target`, pipe.loc);
|
|
1078
|
+
current=pdef.fn(pipe,current);
|
|
1079
|
+
}
|
|
1080
|
+
return current;
|
|
1081
|
+
}
|
|
174
1082
|
}
|
|
175
1083
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
1084
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1085
|
+
// PUBLIC API
|
|
1086
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1087
|
+
class ProDucs {
|
|
1088
|
+
constructor(opts={}) {
|
|
1089
|
+
this.env = new RuntimeEnv();
|
|
1090
|
+
this.registry = new CommandRegistry(this.env);
|
|
1091
|
+
this.executor = new Executor(this.registry, this.env);
|
|
1092
|
+
if (opts.debug) this.env.debug = true;
|
|
1093
|
+
if (opts.strict) this.env.strict = true;
|
|
1094
|
+
if (opts.noColor) this.env.colorOff = true;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
register(name,fn,meta={}) { this.registry.register(name,fn,meta); return this; }
|
|
1098
|
+
tokenize(src) { return new Tokenizer(src).tokenize(); }
|
|
1099
|
+
parse(tokens) { return new Parser(tokens).parse(); }
|
|
1100
|
+
astify(src) { return this.parse(this.tokenize(src)); }
|
|
1101
|
+
execute(ast) { return this.executor.execute(ast); }
|
|
1102
|
+
|
|
1103
|
+
run(src) {
|
|
1104
|
+
this.env.reset();
|
|
1105
|
+
return this.execute(this.astify(src));
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
set(name,value) { this.env.vars[name]=String(value); return this; }
|
|
1109
|
+
vars() { return {...this.env.vars}; }
|
|
1110
|
+
}
|
|
181
1111
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
1112
|
+
module.exports = {
|
|
1113
|
+
ProDucs, ProDucsError, GotoSignal,
|
|
1114
|
+
RuntimeEnv, Tokenizer, Parser, Executor, CommandRegistry,
|
|
1115
|
+
};
|