@flux-lang/cli 0.1.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/dist/args.js ADDED
@@ -0,0 +1,59 @@
1
+ // packages/cli/src/args.ts
2
+ /**
3
+ * Very small, predictable argv parser.
4
+ * Supports:
5
+ * --flag
6
+ * --flag=value
7
+ * --flag value
8
+ * -q (single-char flags)
9
+ * Everything else is treated as positional.
10
+ */
11
+ export function parseArgs(argv) {
12
+ const flags = {};
13
+ const positional = [];
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const arg = argv[i];
16
+ if (arg === "--") {
17
+ positional.push(...argv.slice(i + 1));
18
+ break;
19
+ }
20
+ if (arg.startsWith("--")) {
21
+ const body = arg.slice(2);
22
+ const eqIdx = body.indexOf("=");
23
+ if (eqIdx !== -1) {
24
+ const key = body.slice(0, eqIdx);
25
+ const value = coerce(body.slice(eqIdx + 1));
26
+ flags[key] = value;
27
+ }
28
+ else {
29
+ const next = argv[i + 1];
30
+ if (next && !next.startsWith("-")) {
31
+ flags[body] = coerce(next);
32
+ i++;
33
+ }
34
+ else {
35
+ flags[body] = true;
36
+ }
37
+ }
38
+ continue;
39
+ }
40
+ if (arg.startsWith("-") && arg.length > 1) {
41
+ const letters = arg.slice(1).split("");
42
+ for (const ch of letters) {
43
+ flags[ch] = true;
44
+ }
45
+ continue;
46
+ }
47
+ positional.push(arg);
48
+ }
49
+ return { flags, positional };
50
+ }
51
+ function coerce(raw) {
52
+ if (raw === "true")
53
+ return true;
54
+ if (raw === "false")
55
+ return false;
56
+ if (/^-?\d+(\.\d+)?$/.test(raw))
57
+ return Number(raw);
58
+ return raw;
59
+ }
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import { stdin as nodeStdin } from "node:process";
4
+ import { parseDocument, initRuntimeState, checkDocument, } from "@flux-lang/core";
5
+ const VERSION = "0.1.0";
6
+ // Entry
7
+ void (async () => {
8
+ try {
9
+ const code = await main(process.argv.slice(2));
10
+ if (code !== 0) {
11
+ process.exit(code);
12
+ }
13
+ }
14
+ catch (error) {
15
+ const msg = error?.message ?? String(error);
16
+ console.error(`Internal error: ${msg}`);
17
+ process.exit(2);
18
+ }
19
+ })();
20
+ async function main(argv) {
21
+ if (argv.length === 0) {
22
+ printGlobalHelp();
23
+ return 0;
24
+ }
25
+ // Global --help / -h
26
+ if (argv.includes("-h") || argv.includes("--help")) {
27
+ const idx = argv.findIndex((a) => a === "-h" || a === "--help");
28
+ // No subcommand yet → global
29
+ if (idx === 0) {
30
+ printGlobalHelp();
31
+ return 0;
32
+ }
33
+ const cmd = argv[0];
34
+ if (cmd === "parse") {
35
+ printParseHelp();
36
+ }
37
+ else if (cmd === "check") {
38
+ printCheckHelp();
39
+ }
40
+ else {
41
+ printGlobalHelp();
42
+ }
43
+ return 0;
44
+ }
45
+ // Global --version / -v
46
+ if (argv.includes("-v") || argv.includes("--version")) {
47
+ console.log(`flux v${VERSION}`);
48
+ return 0;
49
+ }
50
+ const [cmd, ...rest] = argv;
51
+ if (cmd === "parse") {
52
+ return runParse(rest);
53
+ }
54
+ if (cmd === "check") {
55
+ return runCheck(rest);
56
+ }
57
+ console.error(`Unknown command '${cmd}'.`);
58
+ printGlobalHelp();
59
+ return 1;
60
+ }
61
+ /* -------------------------------------------------------------------------- */
62
+ /* Help text */
63
+ /* -------------------------------------------------------------------------- */
64
+ function printGlobalHelp() {
65
+ console.log([
66
+ `Flux CLI v${VERSION}`,
67
+ "",
68
+ "Usage:",
69
+ " flux parse [options] <files...>",
70
+ " flux check [options] <files...>",
71
+ "",
72
+ "Commands:",
73
+ " parse Parse Flux source files and print their IR as JSON.",
74
+ " check Parse and run basic static checks.",
75
+ "",
76
+ "Global options:",
77
+ " -h, --help Show this help message.",
78
+ " -v, --version Show CLI version.",
79
+ "",
80
+ ].join("\n"));
81
+ }
82
+ function printParseHelp() {
83
+ console.log([
84
+ "Usage:",
85
+ " flux parse [options] <files...>",
86
+ "",
87
+ "Description:",
88
+ " Parse Flux source files and print their IR as JSON.",
89
+ "",
90
+ "Options:",
91
+ " --ndjson Emit one JSON object per line: { \"file\", \"doc\" }.",
92
+ " --pretty Pretty-print JSON (2-space indent). (default for a single file)",
93
+ " --compact Compact JSON (no whitespace).",
94
+ " -h, --help Show this message.",
95
+ "",
96
+ ].join("\n"));
97
+ }
98
+ function printCheckHelp() {
99
+ console.log([
100
+ "Usage:",
101
+ " flux check [options] <files...>",
102
+ "",
103
+ "Description:",
104
+ " Parse Flux files and run basic static checks (grid references,",
105
+ " neighbors.* usage, and runtime shape).",
106
+ "",
107
+ "Options:",
108
+ " --json Emit NDJSON diagnostics to stdout.",
109
+ " -h, --help Show this message.",
110
+ "",
111
+ ].join("\n"));
112
+ }
113
+ /* -------------------------------------------------------------------------- */
114
+ /* flux parse */
115
+ /* -------------------------------------------------------------------------- */
116
+ async function runParse(args) {
117
+ const opts = {
118
+ ndjson: false,
119
+ pretty: false,
120
+ compact: false,
121
+ };
122
+ const files = [];
123
+ for (const arg of args) {
124
+ if (arg === "--ndjson") {
125
+ opts.ndjson = true;
126
+ }
127
+ else if (arg === "--pretty") {
128
+ opts.pretty = true;
129
+ }
130
+ else if (arg === "--compact") {
131
+ opts.compact = true;
132
+ }
133
+ else {
134
+ files.push(arg);
135
+ }
136
+ }
137
+ if (files.length === 0) {
138
+ console.error("flux parse: No input files specified.");
139
+ printParseHelp();
140
+ return 1;
141
+ }
142
+ const usesStdin = files.includes("-");
143
+ if (usesStdin && files.length > 1) {
144
+ console.error("flux parse: '-' (stdin) can only be used with a single input.");
145
+ return 1;
146
+ }
147
+ if (opts.pretty && opts.compact && !opts.ndjson) {
148
+ console.error("flux parse: --pretty and --compact are mutually exclusive.");
149
+ return 1;
150
+ }
151
+ const docs = [];
152
+ for (const file of files) {
153
+ let source;
154
+ try {
155
+ source = await readSource(file);
156
+ }
157
+ catch (error) {
158
+ const msg = formatIoError(file, error);
159
+ console.error(msg);
160
+ return 1;
161
+ }
162
+ try {
163
+ const doc = parseDocument(source);
164
+ docs.push({ file: file === "-" ? "<stdin>" : file, doc });
165
+ }
166
+ catch (error) {
167
+ const msg = formatParseOrLexerError(file, error);
168
+ console.error(msg);
169
+ return 1;
170
+ }
171
+ }
172
+ const useNdjson = opts.ndjson || docs.length > 1;
173
+ if (useNdjson) {
174
+ for (const item of docs) {
175
+ const payload = { file: item.file, doc: item.doc };
176
+ process.stdout.write(JSON.stringify(payload) + "\n");
177
+ }
178
+ return 0;
179
+ }
180
+ // Single file → pretty by default unless compact explicitly requested
181
+ const doc = docs[0].doc;
182
+ const space = opts.compact ? 0 : 2;
183
+ const json = JSON.stringify(doc, null, space);
184
+ process.stdout.write(json + "\n");
185
+ return 0;
186
+ }
187
+ /* -------------------------------------------------------------------------- */
188
+ /* flux check */
189
+ /* -------------------------------------------------------------------------- */
190
+ async function runCheck(args) {
191
+ const opts = {
192
+ json: false,
193
+ };
194
+ const files = [];
195
+ for (const arg of args) {
196
+ if (arg === "--json") {
197
+ opts.json = true;
198
+ }
199
+ else {
200
+ files.push(arg);
201
+ }
202
+ }
203
+ if (files.length === 0) {
204
+ console.error("flux check: No input files specified.");
205
+ printCheckHelp();
206
+ return 1;
207
+ }
208
+ const results = [];
209
+ for (const file of files) {
210
+ let source;
211
+ try {
212
+ source = await readSource(file);
213
+ }
214
+ catch (error) {
215
+ const diagnostic = formatIoError(file, error);
216
+ results.push({
217
+ file,
218
+ ok: false,
219
+ errors: [diagnostic],
220
+ });
221
+ continue;
222
+ }
223
+ let doc;
224
+ try {
225
+ doc = parseDocument(source);
226
+ }
227
+ catch (error) {
228
+ const diagnostic = formatParseOrLexerError(file, error);
229
+ results.push({
230
+ file,
231
+ ok: false,
232
+ errors: [diagnostic],
233
+ });
234
+ continue;
235
+ }
236
+ const errors = [];
237
+ // initRuntimeState smoke check — should not throw for valid IR.
238
+ try {
239
+ initRuntimeState(doc);
240
+ }
241
+ catch (error) {
242
+ const detail = error?.message ?? String(error);
243
+ errors.push(`${file}:0:0: Check error: initRuntimeState failed: ${detail}`);
244
+ }
245
+ // Static checks (grids, neighbors, timers, etc.)
246
+ errors.push(...checkDocument(file, doc));
247
+ results.push({
248
+ file,
249
+ ok: errors.length === 0,
250
+ errors: errors.length ? errors : undefined,
251
+ });
252
+ }
253
+ const failed = results.filter((r) => !r.ok);
254
+ const hasFailure = failed.length > 0;
255
+ // JSON (NDJSON) diagnostics
256
+ if (opts.json) {
257
+ for (const r of results) {
258
+ const payload = {
259
+ file: r.file,
260
+ ok: r.ok,
261
+ };
262
+ if (r.errors) {
263
+ payload.errors = r.errors.map((message) => ({ message }));
264
+ }
265
+ process.stdout.write(JSON.stringify(payload) + "\n");
266
+ }
267
+ return hasFailure ? 1 : 0;
268
+ }
269
+ // Human-readable: diagnostics per failing file + summary
270
+ for (const r of results) {
271
+ if (!r.errors)
272
+ continue;
273
+ for (const msg of r.errors) {
274
+ console.error(msg);
275
+ }
276
+ }
277
+ if (hasFailure) {
278
+ console.log(`✗ ${failed.length} of ${results.length} files failed checks`);
279
+ return 1;
280
+ }
281
+ console.log(`✓ ${results.length} files OK`);
282
+ return 0;
283
+ }
284
+ /* -------------------------------------------------------------------------- */
285
+ /* I/O + errors */
286
+ /* -------------------------------------------------------------------------- */
287
+ async function readSource(file) {
288
+ if (file === "-") {
289
+ return readAllFromStdin();
290
+ }
291
+ return fs.readFile(file, "utf8");
292
+ }
293
+ function readAllFromStdin() {
294
+ return new Promise((resolve, reject) => {
295
+ let data = "";
296
+ nodeStdin.setEncoding("utf8");
297
+ nodeStdin.on("data", (chunk) => {
298
+ data += chunk;
299
+ });
300
+ nodeStdin.on("error", reject);
301
+ nodeStdin.on("end", () => resolve(data));
302
+ });
303
+ }
304
+ function formatIoError(file, error) {
305
+ const err = error;
306
+ const code = err.code ?? "UNKNOWN";
307
+ return `${file}:0:0: Error: Cannot read file (${code})`;
308
+ }
309
+ function formatParseOrLexerError(file, error) {
310
+ const err = error;
311
+ const message = err?.message ?? String(error);
312
+ const parseMatch = /Parse error at (\d+):(\d+) near '([^']*)': (.*)/.exec(message);
313
+ if (parseMatch) {
314
+ const [, line, column, near, detail] = parseMatch;
315
+ return `${file}:${line}:${column}: Parse error near '${near}': ${detail}`;
316
+ }
317
+ const lexMatch = /Lexer error at (\d+):(\d+)\s*-\s*(.*)/.exec(message);
318
+ if (lexMatch) {
319
+ const [, line, column, detail] = lexMatch;
320
+ return `${file}:${line}:${column}: Lexer error: ${detail}`;
321
+ }
322
+ return `${file}:0:0: ${message}`;
323
+ }
@@ -0,0 +1,33 @@
1
+ // packages/cli/src/fs-utils.ts
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ export function readFileText(filePath) {
5
+ return fs.readFileSync(filePath, "utf8");
6
+ }
7
+ export function writeFileText(filePath, text) {
8
+ fs.writeFileSync(filePath, text, "utf8");
9
+ }
10
+ export function collectFluxFiles(root, recursive = true) {
11
+ const result = [];
12
+ function walk(dir) {
13
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
14
+ for (const entry of entries) {
15
+ const full = path.join(dir, entry.name);
16
+ if (entry.isDirectory()) {
17
+ if (recursive)
18
+ walk(full);
19
+ }
20
+ else if (entry.isFile() && full.endsWith(".flux")) {
21
+ result.push(full);
22
+ }
23
+ }
24
+ }
25
+ const stat = fs.statSync(root);
26
+ if (stat.isDirectory()) {
27
+ walk(root);
28
+ }
29
+ else if (stat.isFile() && root.endsWith(".flux")) {
30
+ result.push(root);
31
+ }
32
+ return result;
33
+ }
@@ -0,0 +1,2 @@
1
+ // packages/cli/src/version.ts
2
+ export const FLUX_CLI_VERSION = "0.1.0";
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@flux-lang/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tooling for the Flux score language",
5
+ "license": "MIT",
6
+ "author": "Sebastian Suarez-Solis",
7
+ "bin": {
8
+ "flux": "dist/bin/flux.js"
9
+ },
10
+ "type": "module",
11
+ "main": "dist/bin/flux.js",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "dev": "tsc -p tsconfig.json --watch",
22
+ "lint": "eslint src --ext .ts",
23
+ "test": "vitest",
24
+ "prepublishOnly": "npm run build && chmod +x dist/bin/flux.js"
25
+ },
26
+ "dependencies": {
27
+ "@flux-lang/core": "0.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "execa": "^8.0.0",
31
+ "vitest": "^2.1.0",
32
+ "typescript": "^5.6.0"
33
+ }
34
+ }