novac 2.0.1 → 2.2.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/LICENSE +1 -1
- package/README.md +1574 -597
- package/bin/novac +468 -171
- package/bin/nvc +522 -0
- package/bin/nvml +78 -17
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitansi/kitdef.js +1402 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitformat/kitdef.js +1485 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmatrix/ex.js +19 -0
- package/kits/kitmatrix/kitdef.js +960 -0
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitnovacweb/README.md +1416 -143
- package/kits/kitnovacweb/kitdef.js +92 -2
- package/kits/kitnovacweb/nvml/executor.js +578 -176
- package/kits/kitnovacweb/nvml/index.js +2 -2
- package/kits/kitnovacweb/nvml/lexer.js +72 -69
- package/kits/kitnovacweb/nvml/parser.js +328 -159
- package/kits/kitnovacweb/nvml/renderer.js +770 -270
- package/kits/kitparse/kitdef.js +1688 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitregex++/kitdef.js +1353 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/kitx11/kitdef.js +1 -0
- package/kits/kitx11/kitx11.js +2472 -0
- package/kits/kitx11/kitx11_conn.js +948 -0
- package/kits/kitx11/kitx11_worker.js +121 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/ex.js +285 -0
- package/kits/libterm/kitdef.js +1927 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libtea/tf.js +2691 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +6 -3
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +838 -362
- package/src/core/executor.js +2578 -170
- package/src/core/lexer.js +502 -54
- package/src/core/nova_builtins.js +21 -3
- package/src/core/parser.js +413 -72
- package/src/core/types.js +30 -2
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
package/bin/novac
CHANGED
|
@@ -10,24 +10,112 @@ const os = require('os');
|
|
|
10
10
|
const chalk = require('chalk').default;
|
|
11
11
|
const { getConfig, setConfig, interactiveSetup } = require('../src/core/config');
|
|
12
12
|
|
|
13
|
+
// ─── Wildcard helpers ─────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Expand a glob pattern (e.g. "src/*.nova", "**\/*.nv") into real file paths.
|
|
17
|
+
* Falls back gracefully: if the pattern matches no files and contains no glob
|
|
18
|
+
* characters it is returned as-is so exact paths still work.
|
|
19
|
+
*/
|
|
20
|
+
function expandGlob(pattern) {
|
|
21
|
+
if (!isGlobPattern(pattern)) return [pattern];
|
|
22
|
+
try {
|
|
23
|
+
const matches = fs.globSync(pattern, { cwd: process.cwd() });
|
|
24
|
+
return matches.length > 0 ? matches.map(m => path.resolve(m)) : [];
|
|
25
|
+
} catch {
|
|
26
|
+
return [pattern];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Expand an array of file arguments, resolving any wildcards in each entry.
|
|
32
|
+
* Returns a flat, deduplicated array of absolute paths.
|
|
33
|
+
*/
|
|
34
|
+
function expandFileArgs(args) {
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const arg of args) {
|
|
38
|
+
for (const f of expandGlob(arg)) {
|
|
39
|
+
const abs = path.resolve(f);
|
|
40
|
+
if (!seen.has(abs)) { seen.add(abs); out.push(abs); }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** True when a string contains glob metacharacters */
|
|
47
|
+
function isGlobPattern(str) {
|
|
48
|
+
return /[*?{}\[\]]/.test(str);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Expand a name list (module names, kit names) that may include wildcards
|
|
53
|
+
* against an installed directory. A bare "*" expands to all entries.
|
|
54
|
+
*/
|
|
55
|
+
function expandNamesGlob(patterns, baseDir) {
|
|
56
|
+
if (!fs.existsSync(baseDir)) return patterns;
|
|
57
|
+
const installed = fs.readdirSync(baseDir, { withFileTypes: true })
|
|
58
|
+
.filter(e => e.isDirectory())
|
|
59
|
+
.map(e => e.name);
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const pat of patterns) {
|
|
62
|
+
if (!isGlobPattern(pat)) { out.push(pat); continue; }
|
|
63
|
+
const re = globToRegex(pat);
|
|
64
|
+
const hits = installed.filter(n => re.test(n));
|
|
65
|
+
if (hits.length > 0) out.push(...hits);
|
|
66
|
+
else out.push(pat); // pass through so the caller gives a "not found" error
|
|
67
|
+
}
|
|
68
|
+
return [...new Set(out)];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Convert a simple shell-style glob (*, ?) to a RegExp */
|
|
72
|
+
function globToRegex(pattern) {
|
|
73
|
+
const escaped = pattern
|
|
74
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
75
|
+
.replace(/\*/g, '.*')
|
|
76
|
+
.replace(/\?/g, '.');
|
|
77
|
+
return new RegExp(`^${escaped}$`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Program ──────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
13
82
|
const program = new Command();
|
|
14
|
-
program.name('novac').description('Nova Language v2').version('2.0.0');
|
|
83
|
+
program.name('novac').description('Nova Language v2').version('2.0.0').enablePositionalOptions();
|
|
15
84
|
|
|
16
85
|
program
|
|
17
86
|
.argument('[file]')
|
|
18
87
|
.argument('[args...]')
|
|
19
|
-
.allowUnknownOption(true)
|
|
88
|
+
.allowUnknownOption(true)
|
|
20
89
|
.passThroughOptions()
|
|
21
90
|
.action((file, args, options, command) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
91
|
+
// No file given → start REPL
|
|
92
|
+
if (!file) {
|
|
93
|
+
console.log(chalk.gray('No file specified. Starting REPL. Type .help for commands, .exit to quit.\n'));
|
|
94
|
+
return program.commands.find(c => c.name() === 'repl')._actionHandler([]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const filePath = path.resolve(file);
|
|
98
|
+
if (!fs.existsSync(filePath)) {
|
|
99
|
+
console.error(chalk.red(`Error: File not found: ${file}`));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let src;
|
|
104
|
+
try {
|
|
105
|
+
src = fs.readFileSync(filePath, 'utf8');
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error(chalk.red(`Error reading file "${file}": ${e.message}`));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let cli = {
|
|
112
|
+
args: args.filter((a) => !(a.startsWith('-') || a.startsWith('@') || a.startsWith('+'))) || [],
|
|
25
113
|
options,
|
|
26
|
-
addrs: {},
|
|
27
|
-
additions: {},
|
|
114
|
+
addrs: {},
|
|
115
|
+
additions: {},
|
|
28
116
|
raw: process.argv,
|
|
29
|
-
|
|
30
|
-
const unknown = process.argv.slice(2).slice(options.length);
|
|
117
|
+
};
|
|
118
|
+
const unknown = process.argv.slice(2).slice(options.length);
|
|
31
119
|
for (let i = 0; i < unknown.length; i++) {
|
|
32
120
|
const arg = unknown[i];
|
|
33
121
|
if (arg.startsWith('--')) {
|
|
@@ -35,13 +123,13 @@ program
|
|
|
35
123
|
const next = unknown[i + 1];
|
|
36
124
|
if (next && !next.startsWith('-')) {
|
|
37
125
|
cli.options[key] = next;
|
|
38
|
-
i++;
|
|
126
|
+
i++;
|
|
39
127
|
} else {
|
|
40
128
|
if (key.includes('=')) {
|
|
41
129
|
const [k, v] = key.split('=');
|
|
42
130
|
cli.options[k] = v;
|
|
43
131
|
} else {
|
|
44
|
-
|
|
132
|
+
cli.options[key] = true;
|
|
45
133
|
}
|
|
46
134
|
}
|
|
47
135
|
} else if (arg.startsWith('-')) {
|
|
@@ -55,38 +143,122 @@ program
|
|
|
55
143
|
cli.additions[key] = true;
|
|
56
144
|
}
|
|
57
145
|
}
|
|
58
|
-
try { run(src, { cli }); }
|
|
59
|
-
catch (e) { console.error(e.message || e); process.exit(1); }
|
|
146
|
+
try { const _r = run(src, { cli }); if (_r instanceof Promise) _r.catch(e => { console.error(chalk.red(e.message || e)); process.exit(1); }); }
|
|
147
|
+
catch (e) { console.error(chalk.red(e.message || e)); process.exit(1); }
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ─── novac run <file> [args...] ───────────────────────────────────────────────
|
|
151
|
+
program
|
|
152
|
+
.command('run <file> [args...]')
|
|
153
|
+
.description('Run a Nova file (explicit alias for: novac <file>)')
|
|
154
|
+
.allowUnknownOption(true)
|
|
155
|
+
.action((file, args, options) => {
|
|
156
|
+
const filePath = path.resolve(file);
|
|
157
|
+
if (!fs.existsSync(filePath)) {
|
|
158
|
+
console.error(chalk.red(`Error: File not found: ${file}`));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
let src;
|
|
162
|
+
try {
|
|
163
|
+
src = fs.readFileSync(filePath, 'utf8');
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.error(chalk.red(`Error reading file "${file}": ${e.message}`));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
const cli = { args, options, addrs: {}, additions: {}, raw: process.argv };
|
|
169
|
+
try { const _r = run(src, { cli }); if (_r instanceof Promise) _r.catch(e => { console.error(chalk.red(e.message || e)); process.exit(1); }); }
|
|
170
|
+
catch (e) { console.error(chalk.red(e.message || e)); process.exit(1); }
|
|
60
171
|
});
|
|
61
172
|
|
|
173
|
+
// ─── novac test <file|glob> [file|glob ...] ───────────────────────────────────
|
|
62
174
|
program
|
|
63
|
-
.command('test <
|
|
64
|
-
.description('Test
|
|
65
|
-
.action((
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
175
|
+
.command('test <files...>')
|
|
176
|
+
.description('Test Nova file(s) by checking syntax — supports globs e.g. "src/*.nova"')
|
|
177
|
+
.action((files) => {
|
|
178
|
+
const resolved = expandFileArgs(files);
|
|
179
|
+
if (resolved.length === 0) {
|
|
180
|
+
console.error(chalk.red('No files matched:', files.join(', ')));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
let ok = 0, fail = 0;
|
|
184
|
+
for (const file of resolved) {
|
|
185
|
+
let src;
|
|
186
|
+
try {
|
|
187
|
+
src = fs.readFileSync(file, 'utf8');
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error(chalk.red('✗'), chalk.gray(path.relative(process.cwd(), file)),
|
|
190
|
+
chalk.red('— cannot read file:', e.message));
|
|
191
|
+
fail++; continue;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
parse(src);
|
|
195
|
+
console.log(chalk.green('✓'), chalk.gray(path.relative(process.cwd(), file)));
|
|
196
|
+
ok++;
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error(chalk.red('✗'), chalk.gray(path.relative(process.cwd(), file)),
|
|
199
|
+
chalk.red('—', e.message || e));
|
|
200
|
+
fail++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (resolved.length > 1) {
|
|
204
|
+
console.log(chalk.blue(`\n${ok} passed, ${fail} failed`));
|
|
205
|
+
}
|
|
206
|
+
if (fail > 0) process.exit(1);
|
|
69
207
|
});
|
|
70
208
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
209
|
+
// ─── novac format [file|glob ...] ────────────────────────────────────────────
|
|
210
|
+
program.command('format [files...]')
|
|
211
|
+
.description('Format Nova file(s) — supports globs e.g. "src/**/*.nova"')
|
|
212
|
+
.option('-w, --write', 'Write formatted output back to files instead of printing')
|
|
213
|
+
.option('-c, --check', 'Check if files are formatted; exit non-zero if any need changes (useful for CI)')
|
|
214
|
+
.action((files, opts) => {
|
|
215
|
+
if (files && files.length > 0) {
|
|
216
|
+
const resolved = expandFileArgs(files);
|
|
217
|
+
if (resolved.length === 0) {
|
|
218
|
+
console.error(chalk.red('No files matched:', files.join(', ')));
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
let anyError = false;
|
|
222
|
+
let needsFormat = 0;
|
|
223
|
+
for (const file of resolved) {
|
|
224
|
+
let src;
|
|
225
|
+
try {
|
|
226
|
+
src = fs.readFileSync(file, 'utf8');
|
|
227
|
+
} catch (e) {
|
|
228
|
+
console.error(chalk.red(`Error reading "${path.relative(process.cwd(), file)}": ${e.message}`));
|
|
229
|
+
anyError = true; continue;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const out = format(src);
|
|
233
|
+
if (opts.check) {
|
|
234
|
+
if (src !== out) {
|
|
235
|
+
console.log(chalk.red('✗ needs formatting:'), chalk.gray(path.relative(process.cwd(), file)));
|
|
236
|
+
needsFormat++;
|
|
237
|
+
} else {
|
|
238
|
+
console.log(chalk.green('✓'), chalk.gray(path.relative(process.cwd(), file)));
|
|
239
|
+
}
|
|
240
|
+
} else if (opts.write) {
|
|
241
|
+
fs.writeFileSync(file, out, 'utf8');
|
|
242
|
+
console.log(chalk.green('formatted'), chalk.gray(path.relative(process.cwd(), file)));
|
|
243
|
+
} else {
|
|
244
|
+
if (resolved.length > 1) console.log(chalk.bold(`\n// ── ${path.relative(process.cwd(), file)} ──`));
|
|
245
|
+
console.log(out);
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {
|
|
248
|
+
console.error(chalk.red('Error in', path.relative(process.cwd(), file) + ':'), e.message || e);
|
|
249
|
+
anyError = true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (opts.check && needsFormat > 0) {
|
|
253
|
+
console.log(chalk.red(`\n${needsFormat} file(s) need formatting. Run: novac format --write`));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
if (anyError) process.exit(1);
|
|
78
257
|
} else {
|
|
258
|
+
// stdin REPL mode
|
|
79
259
|
let input = '';
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
input: process.stdin,
|
|
83
|
-
output: process.stdout,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
rl.on('line', (line) => {
|
|
87
|
-
input += line + '\n';
|
|
88
|
-
});
|
|
89
|
-
|
|
260
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
261
|
+
rl.on('line', (line) => { input += line + '\n'; });
|
|
90
262
|
rl.on('close', () => {
|
|
91
263
|
try { console.log(format(input)); }
|
|
92
264
|
catch (e) { console.error(e.message || e); process.exit(1); }
|
|
@@ -94,6 +266,7 @@ program.command('format [file]')
|
|
|
94
266
|
}
|
|
95
267
|
});
|
|
96
268
|
|
|
269
|
+
// ─── novac eval <code> ────────────────────────────────────────────────────────
|
|
97
270
|
program.command('eval <code>')
|
|
98
271
|
.description('Evaluate Nova code inline')
|
|
99
272
|
.action((code) => {
|
|
@@ -101,22 +274,43 @@ program.command('eval <code>')
|
|
|
101
274
|
catch (e) { console.error(e.message || e); process.exit(1); }
|
|
102
275
|
});
|
|
103
276
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
277
|
+
// ─── novac tokens <file|glob> [file|glob ...] ─────────────────────────────────
|
|
278
|
+
program.command('tokens <files...>')
|
|
279
|
+
.description('Print token stream of Nova file(s) — supports globs e.g. "src/*.nova"')
|
|
280
|
+
.action((files) => {
|
|
281
|
+
const resolved = expandFileArgs(files);
|
|
282
|
+
if (resolved.length === 0) {
|
|
283
|
+
console.error(chalk.red('No files matched:', files.join(', ')));
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
for (const file of resolved) {
|
|
287
|
+
const src = fs.readFileSync(file, 'utf8');
|
|
288
|
+
try {
|
|
289
|
+
if (resolved.length > 1) console.log(chalk.bold(`\n// ── ${path.relative(process.cwd(), file)} ──`));
|
|
290
|
+
console.log(JSON.stringify(tokenize(src), null, 2));
|
|
291
|
+
} catch (e) { console.error(e.message || e); process.exit(1); }
|
|
292
|
+
}
|
|
110
293
|
});
|
|
111
294
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
295
|
+
// ─── novac ast <file|glob> [file|glob ...] ────────────────────────────────────
|
|
296
|
+
program.command('ast <files...>')
|
|
297
|
+
.description('Print AST of Nova file(s) — supports globs e.g. "src/*.nova"')
|
|
298
|
+
.action((files) => {
|
|
299
|
+
const resolved = expandFileArgs(files);
|
|
300
|
+
if (resolved.length === 0) {
|
|
301
|
+
console.error(chalk.red('No files matched:', files.join(', ')));
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
for (const file of resolved) {
|
|
305
|
+
const src = fs.readFileSync(file, 'utf8');
|
|
306
|
+
try {
|
|
307
|
+
if (resolved.length > 1) console.log(chalk.bold(`\n// ── ${path.relative(process.cwd(), file)} ──`));
|
|
308
|
+
console.log(JSON.stringify(parse(src), null, 2));
|
|
309
|
+
} catch (e) { console.error(e.message || e); process.exit(1); }
|
|
310
|
+
}
|
|
118
311
|
});
|
|
119
312
|
|
|
313
|
+
// ─── novac repl ───────────────────────────────────────────────────────────────
|
|
120
314
|
program.command('repl')
|
|
121
315
|
.description('Start interactive REPL')
|
|
122
316
|
.action(() => {
|
|
@@ -126,7 +320,6 @@ program.command('repl')
|
|
|
126
320
|
console.log('Nova REPL v2.0.0');
|
|
127
321
|
console.log('Type .exit to exit\n');
|
|
128
322
|
|
|
129
|
-
// Load history
|
|
130
323
|
let history = [];
|
|
131
324
|
try {
|
|
132
325
|
const historyContent = fs.readFileSync(historyFile, 'utf8');
|
|
@@ -137,51 +330,86 @@ program.command('repl')
|
|
|
137
330
|
input: process.stdin,
|
|
138
331
|
output: process.stdout,
|
|
139
332
|
prompt: 'nova> ',
|
|
140
|
-
history
|
|
333
|
+
history,
|
|
141
334
|
});
|
|
142
335
|
|
|
143
336
|
rl.prompt();
|
|
144
337
|
|
|
145
|
-
rl.on('line', (input) => {
|
|
338
|
+
rl.on('line', async (input) => {
|
|
146
339
|
const trimmed = input.trim();
|
|
147
340
|
|
|
148
|
-
if (trimmed)
|
|
149
|
-
fs.appendFileSync(historyFile, trimmed + '\n');
|
|
150
|
-
}
|
|
341
|
+
if (trimmed) fs.appendFileSync(historyFile, trimmed + '\n');
|
|
151
342
|
|
|
152
343
|
if (trimmed === '.exit') {
|
|
153
344
|
rl.close();
|
|
154
345
|
} else if (trimmed === '.clear') {
|
|
155
346
|
console.clear();
|
|
347
|
+
} else if (trimmed === '.reset') {
|
|
348
|
+
// Re-create executor to wipe all defined variables/functions
|
|
349
|
+
Object.keys(executor).forEach(k => delete executor[k]);
|
|
350
|
+
Object.assign(executor, new Executor('', stdlib));
|
|
351
|
+
console.log(chalk.green('REPL state reset.'));
|
|
352
|
+
} else if (trimmed.startsWith('.format')) {
|
|
353
|
+
const code = trimmed.slice(7).trim();
|
|
354
|
+
if (!code) { console.error('Usage: .format <code>'); rl.prompt(); return; }
|
|
355
|
+
try {
|
|
356
|
+
console.log(format(code));
|
|
357
|
+
} catch (e) { console.error('Error:', e.message || e); }
|
|
156
358
|
} else if (trimmed.startsWith('.ast')) {
|
|
157
359
|
try {
|
|
158
360
|
const ast = parse(trimmed.slice(4).trim());
|
|
159
361
|
console.log(JSON.stringify(ast, null, 2));
|
|
160
|
-
} catch (e) {
|
|
161
|
-
console.error('Error:', e.message || e);
|
|
162
|
-
}
|
|
362
|
+
} catch (e) { console.error('Error:', e.message || e); }
|
|
163
363
|
} else if (trimmed.startsWith('.tokens')) {
|
|
164
364
|
try {
|
|
165
365
|
const toks = tokenize(trimmed.slice(7).trim());
|
|
166
366
|
console.log(JSON.stringify(toks, null, 2));
|
|
167
|
-
} catch (e) {
|
|
168
|
-
|
|
367
|
+
} catch (e) { console.error('Error:', e.message || e); }
|
|
368
|
+
} else if (trimmed.startsWith('.load')) {
|
|
369
|
+
// .load <file|glob> — load one or more Nova files into REPL
|
|
370
|
+
const pattern = trimmed.slice(5).trim();
|
|
371
|
+
if (!pattern) { console.error('Usage: .load <file|glob>'); rl.prompt(); return; }
|
|
372
|
+
const files = expandFileArgs([pattern]);
|
|
373
|
+
if (files.length === 0) { console.error(chalk.red('No files matched:', pattern)); rl.prompt(); return; }
|
|
374
|
+
for (const file of files) {
|
|
375
|
+
try {
|
|
376
|
+
const src = fs.readFileSync(file, 'utf8');
|
|
377
|
+
const ast = parse(src);
|
|
378
|
+
let result = executor.run(ast);
|
|
379
|
+
if (result instanceof Promise) result = await result;
|
|
380
|
+
console.log(chalk.green('loaded'), chalk.gray(path.relative(process.cwd(), file)));
|
|
381
|
+
} catch (e) { console.error(chalk.red('Error loading', path.basename(file) + ':'), e.message || e); }
|
|
169
382
|
}
|
|
170
383
|
} else if (trimmed.startsWith('.help')) {
|
|
171
384
|
console.log('Nova REPL Commands:');
|
|
172
|
-
console.log(' .exit
|
|
173
|
-
console.log(' .clear
|
|
174
|
-
console.log(' .
|
|
175
|
-
console.log(' .
|
|
176
|
-
console.log(' .
|
|
385
|
+
console.log(' .exit Exit REPL');
|
|
386
|
+
console.log(' .clear Clear screen');
|
|
387
|
+
console.log(' .reset Reset all variables and definitions');
|
|
388
|
+
console.log(' .ast <code> Show AST for code');
|
|
389
|
+
console.log(' .tokens <code> Show token stream for code');
|
|
390
|
+
console.log(' .format <code> Show formatted version of code');
|
|
391
|
+
console.log(' .load <file|glob> Load Nova file(s) into REPL — supports wildcards');
|
|
392
|
+
console.log(' .help Show this help');
|
|
177
393
|
} else if (trimmed !== '') {
|
|
178
394
|
try {
|
|
179
395
|
const ast = parse(trimmed);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
396
|
+
let result = executor.run(ast);
|
|
397
|
+
// Top-level await / async fn call — resolve the Promise before printing
|
|
398
|
+
if (result instanceof Promise) {
|
|
399
|
+
rl.pause();
|
|
400
|
+
try {
|
|
401
|
+
result = await result;
|
|
402
|
+
} catch (e) {
|
|
403
|
+
console.error(chalk.red('Async error:'), e && e.message ? e.message.split('\n')[0] : e);
|
|
404
|
+
rl.resume();
|
|
405
|
+
rl.prompt();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
rl.resume();
|
|
409
|
+
}
|
|
410
|
+
if (result !== undefined && result !== null && result !== '')
|
|
411
|
+
console.log(executor.stringify(result));
|
|
412
|
+
} catch (e) { console.error('Error:', e.message ? e.message.split('\n')[0] : e.message || e); }
|
|
185
413
|
}
|
|
186
414
|
|
|
187
415
|
rl.prompt();
|
|
@@ -193,10 +421,12 @@ program.command('repl')
|
|
|
193
421
|
});
|
|
194
422
|
});
|
|
195
423
|
|
|
196
|
-
|
|
197
|
-
|
|
424
|
+
// ─── novac new <project> ──────────────────────────────────────────────────────
|
|
425
|
+
program
|
|
426
|
+
.command('new <project>')
|
|
198
427
|
.description('Create a new Nova project')
|
|
199
|
-
.
|
|
428
|
+
.option('--cd', 'Print the cd command to enter the new project directory')
|
|
429
|
+
.action((project, opts) => {
|
|
200
430
|
const projectPath = path.resolve(project);
|
|
201
431
|
if (fs.existsSync(projectPath)) {
|
|
202
432
|
console.error(chalk.red('Directory already exists:', projectPath));
|
|
@@ -210,8 +440,7 @@ program.command('new <project> ')
|
|
|
210
440
|
const runFile = path.join(projectPath, 'bin', `${project}.nv`);
|
|
211
441
|
fs.writeFileSync(runFile, ``, { encoding: 'utf8', mode: 0o755 });
|
|
212
442
|
fs.writeFileSync(path.join(projectPath, 'README.md'), `# ${project}\n\nThis is a Nova project.`, 'utf8');
|
|
213
|
-
fs.writeFileSync(path.join(projectPath, '.gitignore'),
|
|
214
|
-
// Write nova.config.json (project-local module config)
|
|
443
|
+
fs.writeFileSync(path.join(projectPath, '.gitignore'), `nova_modules/\n`, 'utf8');
|
|
215
444
|
const projectConfig = {
|
|
216
445
|
name: project,
|
|
217
446
|
version: '1.0.0',
|
|
@@ -225,13 +454,19 @@ program.command('new <project> ')
|
|
|
225
454
|
devDependencies: {},
|
|
226
455
|
};
|
|
227
456
|
fs.writeFileSync(path.join(projectPath, 'nova.config.json'), JSON.stringify(projectConfig, null, 2), 'utf8');
|
|
228
|
-
console.log(chalk.green(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
457
|
+
console.log(chalk.green(`✓ Project created: ${projectPath}`));
|
|
458
|
+
console.log(chalk.gray(` src/main.nova — entry point`));
|
|
459
|
+
console.log(chalk.gray(` nova.config.json — project config`));
|
|
460
|
+
if (opts.cd) {
|
|
461
|
+
// Note: a CLI child process cannot change the parent shell's directory.
|
|
462
|
+
// We print the command for the user to run instead.
|
|
463
|
+
console.log(chalk.blue(`\nRun: cd ${project}`));
|
|
464
|
+
} else {
|
|
465
|
+
console.log(chalk.blue(`\nGet started:\n cd ${project}\n novac src/main.nova`));
|
|
232
466
|
}
|
|
233
467
|
});
|
|
234
468
|
|
|
469
|
+
// ─── novac config ─────────────────────────────────────────────────────────────
|
|
235
470
|
program
|
|
236
471
|
.command('config <action>')
|
|
237
472
|
.argument('[key]', 'Configuration key (e.g., name, version, scripts.build)')
|
|
@@ -275,10 +510,11 @@ program
|
|
|
275
510
|
process.exit(1);
|
|
276
511
|
}
|
|
277
512
|
});
|
|
278
|
-
|
|
513
|
+
|
|
514
|
+
// ─── novac etc <cmd> ─────────────────────────────────────────────────────────
|
|
279
515
|
program
|
|
280
516
|
.command('etc <cmd> [args...]')
|
|
281
|
-
.description('Run extra Nova commands (e.g., notices, describe)')
|
|
517
|
+
.description('Run extra Nova commands (e.g., notices, describe, kit)')
|
|
282
518
|
.allowUnknownOption(true)
|
|
283
519
|
.action((cmd, args) => {
|
|
284
520
|
try {
|
|
@@ -288,42 +524,79 @@ program
|
|
|
288
524
|
console.log(chalk.green('No notices'));
|
|
289
525
|
} else {
|
|
290
526
|
console.log(chalk.yellow('Notices:'));
|
|
291
|
-
notices.forEach((n, i) => {
|
|
292
|
-
console.log(chalk.yellow(`${i + 1}. ${n}`));
|
|
293
|
-
});
|
|
527
|
+
notices.forEach((n, i) => console.log(chalk.yellow(`${i + 1}. ${n}`)));
|
|
294
528
|
}
|
|
295
529
|
} else if (cmd === 'describe') {
|
|
296
|
-
|
|
297
|
-
const
|
|
530
|
+
// describe supports globs: novac etc describe src/*.nova
|
|
531
|
+
const pattern = args[0];
|
|
532
|
+
if (!pattern) { console.error(chalk.red('Usage: novac etc describe <file|glob>')); process.exit(1); }
|
|
533
|
+
const files = expandFileArgs([pattern]);
|
|
534
|
+
if (files.length === 0) { console.error(chalk.red('No files matched:', pattern)); process.exit(1); }
|
|
298
535
|
const describer = require('../src/core/describe');
|
|
299
|
-
const
|
|
300
|
-
|
|
536
|
+
for (const file of files) {
|
|
537
|
+
if (files.length > 1) console.log(chalk.bold(`\n── ${path.relative(process.cwd(), file)} ──`));
|
|
538
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
539
|
+
const ast = parse(content);
|
|
540
|
+
console.log(describer.describe(ast));
|
|
541
|
+
}
|
|
301
542
|
} else if (cmd === 'kit') {
|
|
302
|
-
//
|
|
303
|
-
// Usage: novac etc kit <name> -> ./nova_modules/<name>
|
|
304
|
-
// novac etc kit <name> --global -> ~/.novac/nova_modules/<name>
|
|
305
|
-
const kitName = args[0];
|
|
543
|
+
// Supports multiple names and wildcards: novac etc kit math* utils --global
|
|
306
544
|
const isGlobal = args.includes('--global') || args.includes('-g');
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (!fs.existsSync(kitDir) || !fs.statSync(kitDir).isDirectory()) {
|
|
311
|
-
console.error(chalk.red(`Kit "${kitName}" not found. Available: ${fs.readdirSync(kitsRoot).join(', ')}`));
|
|
545
|
+
const kitNames = args.filter(a => a !== '--global' && a !== '-g');
|
|
546
|
+
if (kitNames.length === 0) {
|
|
547
|
+
console.error(chalk.red('Usage: novac etc kit <kitname|glob> [...] [--global]'));
|
|
312
548
|
process.exit(1);
|
|
313
549
|
}
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
550
|
+
const kitsRoot = path.join(__dirname, '..', 'kits');
|
|
551
|
+
const availableKits = fs.existsSync(kitsRoot)
|
|
552
|
+
? fs.readdirSync(kitsRoot, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name)
|
|
553
|
+
: [];
|
|
554
|
+
|
|
555
|
+
// Expand wildcards against available kits
|
|
556
|
+
const resolvedKits = [];
|
|
557
|
+
for (const pat of kitNames) {
|
|
558
|
+
if (isGlobPattern(pat)) {
|
|
559
|
+
const re = globToRegex(pat);
|
|
560
|
+
const hits = availableKits.filter(n => re.test(n));
|
|
561
|
+
if (hits.length === 0) {
|
|
562
|
+
console.error(chalk.red(`No kits matched pattern "${pat}". Available: ${availableKits.join(', ')}`));
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
resolvedKits.push(...hits);
|
|
566
|
+
} else {
|
|
567
|
+
resolvedKits.push(pat);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const baseDir = isGlobal
|
|
572
|
+
? path.join(os.homedir(), '.novac', 'nova_modules')
|
|
573
|
+
: path.join(process.cwd(), 'nova_modules');
|
|
574
|
+
|
|
575
|
+
let ok = 0, fail = 0;
|
|
576
|
+
for (const kitName of [...new Set(resolvedKits)]) {
|
|
577
|
+
const kitDir = path.join(kitsRoot, kitName);
|
|
578
|
+
if (!fs.existsSync(kitDir) || !fs.statSync(kitDir).isDirectory()) {
|
|
579
|
+
console.error(chalk.red(` fail "${kitName}" — not found. Available: ${availableKits.join(', ')}`));
|
|
580
|
+
fail++; continue;
|
|
581
|
+
}
|
|
582
|
+
const destPath = path.join(baseDir, kitName);
|
|
583
|
+
if (isGlobal) console.log(chalk.blue(`Installing globally to ${destPath}`));
|
|
584
|
+
if (fs.existsSync(destPath)) {
|
|
585
|
+
console.error(chalk.red(` skip "${kitName}" — already installed at ${destPath}`));
|
|
586
|
+
fail++; continue;
|
|
587
|
+
}
|
|
588
|
+
const kitdefPath = path.join(kitDir, 'kitdef.js');
|
|
589
|
+
if (!fs.existsSync(kitdefPath)) {
|
|
590
|
+
console.error(chalk.red(` fail "${kitName}" — no kitdef.js`));
|
|
591
|
+
fail++; continue;
|
|
592
|
+
}
|
|
593
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
594
|
+
fs.copyFileSync(kitdefPath, path.join(destPath, 'kitdef.js'));
|
|
595
|
+
console.log(chalk.green(` ok "${kitName}" → ${destPath}`));
|
|
596
|
+
console.log(chalk.blue(` Usage in Nova: import "${kitName}"`));
|
|
597
|
+
ok++;
|
|
320
598
|
}
|
|
321
|
-
|
|
322
|
-
if (!fs.existsSync(kitdefPath)) { console.error(chalk.red(`Kit "${kitName}" has no kitdef.js`)); process.exit(1); }
|
|
323
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
324
|
-
fs.copyFileSync(kitdefPath, path.join(destPath, 'kitdef.js'));
|
|
325
|
-
console.log(chalk.green(`Kit "${kitName}" installed to ${destPath}`));
|
|
326
|
-
console.log(chalk.blue(` Usage in Nova: import "${kitName}"`));
|
|
599
|
+
if (resolvedKits.length > 1) console.log(chalk.blue(`\nDone. ${ok} installed, ${fail} failed/skipped.`));
|
|
327
600
|
} else {
|
|
328
601
|
console.error(chalk.red(`Unknown command: ${cmd}. Use "notices", "describe", or "kit".`));
|
|
329
602
|
process.exit(1);
|
|
@@ -346,7 +619,6 @@ program
|
|
|
346
619
|
}
|
|
347
620
|
fs.mkdirSync(kitPath, { recursive: true });
|
|
348
621
|
|
|
349
|
-
// kitdef.js — the actual implementation
|
|
350
622
|
const kitdefSrc = `// ${dirname} — Nova kit
|
|
351
623
|
// Export all public API on the kitdef object.
|
|
352
624
|
// Each key becomes accessible after: import "${dirname}"
|
|
@@ -362,7 +634,6 @@ module.exports = { kitdef: kit };
|
|
|
362
634
|
`;
|
|
363
635
|
fs.writeFileSync(path.join(kitPath, 'kitdef.js'), kitdefSrc, 'utf8');
|
|
364
636
|
|
|
365
|
-
// index.nova — Nova-facing wrapper (generated manually here as a template)
|
|
366
637
|
const indexNova = `// ${dirname} kit — Nova wrapper
|
|
367
638
|
// Import this file from Nova: import "${dirname}"
|
|
368
639
|
// Then call: hello()
|
|
@@ -372,7 +643,6 @@ export hello = _kit.hello;
|
|
|
372
643
|
`;
|
|
373
644
|
fs.writeFileSync(path.join(kitPath, 'index.nova'), indexNova, 'utf8');
|
|
374
645
|
|
|
375
|
-
// nova.kit.json — kit manifest
|
|
376
646
|
const manifest = {
|
|
377
647
|
name: dirname,
|
|
378
648
|
version: '1.0.0',
|
|
@@ -382,8 +652,6 @@ export hello = _kit.hello;
|
|
|
382
652
|
license: 'MIT',
|
|
383
653
|
};
|
|
384
654
|
fs.writeFileSync(path.join(kitPath, 'nova.kit.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
|
385
|
-
|
|
386
|
-
// README
|
|
387
655
|
fs.writeFileSync(path.join(kitPath, 'README.md'), `# ${dirname}\n\nA Nova kit.\n\n## Usage\n\n\`\`\`nova\nimport "${dirname}"\nhello()\n\`\`\`\n`, 'utf8');
|
|
388
656
|
|
|
389
657
|
console.log(chalk.green(`Kit scaffolded at ${kitPath}`));
|
|
@@ -391,16 +659,13 @@ export hello = _kit.hello;
|
|
|
391
659
|
console.log(chalk.blue(' Update index.nova exports to match.'));
|
|
392
660
|
});
|
|
393
661
|
|
|
394
|
-
// ─── novac init build
|
|
395
|
-
// Reads nova.config.json in cwd, bundles all .nova sources into a .novamod file
|
|
396
|
-
// that can be dropped into another project's nova_modules/.
|
|
662
|
+
// ─── novac init build | PATH ──────────────────────────────────────────────────
|
|
397
663
|
program
|
|
398
664
|
.command('init')
|
|
399
665
|
.description('Initialize novac settings or build project module')
|
|
400
666
|
.argument('<option>', 'Option: PATH | build')
|
|
401
667
|
.action((option) => {
|
|
402
668
|
if (option.toUpperCase() === 'BUILD') {
|
|
403
|
-
// ── build ──
|
|
404
669
|
const configFile = path.join(process.cwd(), 'nova.config.json');
|
|
405
670
|
if (!fs.existsSync(configFile)) {
|
|
406
671
|
console.error(chalk.red('No nova.config.json found in current directory.'));
|
|
@@ -413,7 +678,6 @@ program
|
|
|
413
678
|
const main = config.main || 'src/main.nova';
|
|
414
679
|
const srcDir = config.srcDir || 'src';
|
|
415
680
|
|
|
416
|
-
// Collect all .nova files under srcDir
|
|
417
681
|
function collectNova(dir, base = dir) {
|
|
418
682
|
const results = {};
|
|
419
683
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -435,7 +699,6 @@ program
|
|
|
435
699
|
|
|
436
700
|
const sources = collectNova(srcAbsDir);
|
|
437
701
|
if (!sources[main] && !sources[path.relative(srcAbsDir, path.join(process.cwd(), main))]) {
|
|
438
|
-
// Try to find main relative to srcDir
|
|
439
702
|
const mainRel = path.relative(srcAbsDir, path.join(process.cwd(), main));
|
|
440
703
|
if (!sources[mainRel] && !sources[main]) {
|
|
441
704
|
console.error(chalk.red(`Main entry "${main}" not found in collected sources.`));
|
|
@@ -450,7 +713,6 @@ program
|
|
|
450
713
|
buildTime: new Date().toISOString(),
|
|
451
714
|
};
|
|
452
715
|
|
|
453
|
-
// Determine main key: prefer exact match, then first key
|
|
454
716
|
const mainKey = sources[main] ? main
|
|
455
717
|
: sources[path.relative(srcAbsDir, path.join(process.cwd(), main))]
|
|
456
718
|
? path.relative(srcAbsDir, path.join(process.cwd(), main))
|
|
@@ -473,7 +735,9 @@ program
|
|
|
473
735
|
execSync(`setx PATH "%PATH%;${binPath}"`, { stdio: 'inherit' });
|
|
474
736
|
console.log(chalk.green('PATH updated on Windows. Restart your terminal.'));
|
|
475
737
|
} else if (['darwin','linux','freebsd','openbsd','sunos','aix','android'].includes(platform)) {
|
|
476
|
-
const shellRc = platform === 'darwin'
|
|
738
|
+
const shellRc = platform === 'darwin'
|
|
739
|
+
? path.join(process.env.HOME, '.zshrc')
|
|
740
|
+
: path.join(process.env.HOME, '.bashrc');
|
|
477
741
|
const exportCmd = `export PATH="$PATH:${binPath}"`;
|
|
478
742
|
execSync(`echo '${exportCmd}' >> ${shellRc}`, { stdio: 'inherit' });
|
|
479
743
|
console.log(chalk.green(`Added to ${shellRc}. Run 'source ${shellRc}' or restart your shell.`));
|
|
@@ -492,18 +756,18 @@ program
|
|
|
492
756
|
// ─── novac module <subcommand> ────────────────────────────────────────────────
|
|
493
757
|
const moduleCmd = program.command('module').description('Manage Nova modules');
|
|
494
758
|
|
|
495
|
-
// novac module install [path] [--global]
|
|
759
|
+
// novac module install [path|glob] [path|glob ...] [--global]
|
|
496
760
|
moduleCmd
|
|
497
|
-
.command('install [
|
|
498
|
-
.description('Install
|
|
761
|
+
.command('install [modpaths...]')
|
|
762
|
+
.description('Install .novamod bundle(s) or all deps from nova.config.json — supports globs e.g. "dist/*.novamod"')
|
|
499
763
|
.option('-g, --global', 'Install into global nova_modules (~/.novac/nova_modules)')
|
|
500
|
-
.action((
|
|
764
|
+
.action((modpaths, opts) => {
|
|
501
765
|
const globalDir = path.join(os.homedir(), '.novac', 'nova_modules');
|
|
502
766
|
const novaModulesDir = opts.global ? globalDir : path.join(process.cwd(), 'nova_modules');
|
|
503
767
|
if (opts.global) console.log(chalk.blue(`Installing globally to ${novaModulesDir}`));
|
|
504
768
|
fs.mkdirSync(novaModulesDir, { recursive: true });
|
|
505
769
|
|
|
506
|
-
if (!
|
|
770
|
+
if (!modpaths || modpaths.length === 0) {
|
|
507
771
|
// ── install all deps from nova.config.json ──
|
|
508
772
|
const configFile = path.join(process.cwd(), 'nova.config.json');
|
|
509
773
|
if (!fs.existsSync(configFile)) {
|
|
@@ -520,20 +784,17 @@ moduleCmd
|
|
|
520
784
|
console.log(chalk.blue(`Installing ${entries.length} dependency(ies)...`));
|
|
521
785
|
let ok = 0, fail = 0;
|
|
522
786
|
for (const [name, ref] of entries) {
|
|
523
|
-
// ref can be: a local path ("./path/to/foo.novamod"), or a built-in kit name
|
|
524
787
|
const destDir = path.join(novaModulesDir, name);
|
|
525
788
|
if (fs.existsSync(destDir)) {
|
|
526
789
|
console.log(chalk.yellow(` skip ${name} (already installed)`));
|
|
527
790
|
ok++; continue;
|
|
528
791
|
}
|
|
529
|
-
// Try local path
|
|
530
792
|
const resolved = path.isAbsolute(ref) ? ref : path.resolve(process.cwd(), ref);
|
|
531
793
|
if (fs.existsSync(resolved) && resolved.endsWith('.novamod')) {
|
|
532
794
|
_installNovamod(resolved, name, novaModulesDir);
|
|
533
795
|
console.log(chalk.green(` ok ${name}`));
|
|
534
796
|
ok++;
|
|
535
797
|
} else {
|
|
536
|
-
// Try as a built-in kit
|
|
537
798
|
const kitDir = path.join(__dirname, '..', 'kits', name);
|
|
538
799
|
if (fs.existsSync(kitDir)) {
|
|
539
800
|
_installKit(kitDir, name, novaModulesDir);
|
|
@@ -547,48 +808,71 @@ moduleCmd
|
|
|
547
808
|
}
|
|
548
809
|
console.log(chalk.blue(`Done. ${ok} installed, ${fail} failed.`));
|
|
549
810
|
} else {
|
|
550
|
-
// ── install
|
|
551
|
-
const resolved =
|
|
552
|
-
if (
|
|
553
|
-
console.error(chalk.red(
|
|
554
|
-
process.exit(1);
|
|
555
|
-
}
|
|
556
|
-
if (!resolved.endsWith('.novamod')) {
|
|
557
|
-
console.error(chalk.red('Expected a .novamod file.'));
|
|
811
|
+
// ── install one or more .novamod files (supports globs) ──
|
|
812
|
+
const resolved = expandFileArgs(modpaths);
|
|
813
|
+
if (resolved.length === 0) {
|
|
814
|
+
console.error(chalk.red('No .novamod files matched:', modpaths.join(', ')));
|
|
558
815
|
process.exit(1);
|
|
559
816
|
}
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
817
|
+
let ok = 0, fail = 0;
|
|
818
|
+
for (const modpath of resolved) {
|
|
819
|
+
if (!fs.existsSync(modpath)) {
|
|
820
|
+
console.error(chalk.red(` fail File not found: ${modpath}`));
|
|
821
|
+
fail++; continue;
|
|
822
|
+
}
|
|
823
|
+
if (!modpath.endsWith('.novamod')) {
|
|
824
|
+
console.error(chalk.red(` fail Expected a .novamod file: ${modpath}`));
|
|
825
|
+
fail++; continue;
|
|
826
|
+
}
|
|
827
|
+
const bundle = JSON.parse(fs.readFileSync(modpath, 'utf8'));
|
|
828
|
+
const name = bundle.manifest.name;
|
|
829
|
+
if (!name) {
|
|
830
|
+
console.error(chalk.red(` fail Bundle has no manifest.name: ${modpath}`));
|
|
831
|
+
fail++; continue;
|
|
832
|
+
}
|
|
833
|
+
const destDir = path.join(novaModulesDir, name);
|
|
834
|
+
if (fs.existsSync(destDir)) {
|
|
835
|
+
console.error(chalk.red(` skip "${name}" already installed (remove nova_modules/${name} first)`));
|
|
836
|
+
fail++; continue;
|
|
837
|
+
}
|
|
838
|
+
_installNovamod(modpath, name, novaModulesDir);
|
|
839
|
+
console.log(chalk.green(` ok "${name}" → nova_modules/${name}`));
|
|
840
|
+
console.log(chalk.blue(` Usage: import "${name}"`));
|
|
841
|
+
ok++;
|
|
567
842
|
}
|
|
568
|
-
|
|
569
|
-
console.log(chalk.green(`Module "${name}" installed to nova_modules/${name}`));
|
|
570
|
-
console.log(chalk.blue(` Usage: import "${name}"`));
|
|
843
|
+
if (resolved.length > 1) console.log(chalk.blue(`\nDone. ${ok} installed, ${fail} failed/skipped.`));
|
|
571
844
|
}
|
|
572
845
|
});
|
|
573
846
|
|
|
574
|
-
// novac module list
|
|
847
|
+
// novac module list [pattern]
|
|
575
848
|
moduleCmd
|
|
576
|
-
.command('list')
|
|
577
|
-
.description('List installed modules
|
|
578
|
-
.
|
|
579
|
-
|
|
849
|
+
.command('list [pattern]')
|
|
850
|
+
.description('List installed modules — optional wildcard filter e.g. "math*"')
|
|
851
|
+
.option('-g, --global', 'List globally installed modules (~/.novac/nova_modules)')
|
|
852
|
+
.action((pattern, opts) => {
|
|
853
|
+
const localDir = path.join(process.cwd(), 'nova_modules');
|
|
854
|
+
const globalDir = path.join(os.homedir(), '.novac', 'nova_modules');
|
|
855
|
+
const novaModulesDir = opts.global ? globalDir : localDir;
|
|
856
|
+
const scope = opts.global ? 'global' : 'local';
|
|
857
|
+
|
|
580
858
|
if (!fs.existsSync(novaModulesDir)) {
|
|
581
|
-
console.log(chalk.yellow(
|
|
859
|
+
console.log(chalk.yellow(`No ${scope} nova_modules directory found.`));
|
|
582
860
|
return;
|
|
583
861
|
}
|
|
584
|
-
|
|
862
|
+
let entries = fs.readdirSync(novaModulesDir, { withFileTypes: true }).filter(e => e.isDirectory());
|
|
863
|
+
if (pattern && isGlobPattern(pattern)) {
|
|
864
|
+
const re = globToRegex(pattern);
|
|
865
|
+
entries = entries.filter(e => re.test(e.name));
|
|
866
|
+
} else if (pattern) {
|
|
867
|
+
entries = entries.filter(e => e.name.includes(pattern));
|
|
868
|
+
}
|
|
585
869
|
if (entries.length === 0) {
|
|
586
|
-
console.log(chalk.yellow(
|
|
870
|
+
console.log(chalk.yellow(pattern ? `No ${scope} modules matching "${pattern}".` : `No ${scope} modules installed.`));
|
|
587
871
|
return;
|
|
588
872
|
}
|
|
589
|
-
console.log(chalk.blue(
|
|
873
|
+
console.log(chalk.blue(`Installed ${scope} modules${pattern ? ` matching "${pattern}"` : ''}:`));
|
|
590
874
|
for (const e of entries) {
|
|
591
|
-
const manifestPath
|
|
875
|
+
const manifestPath = path.join(novaModulesDir, e.name, 'nova.kit.json');
|
|
592
876
|
const bundleManifest = path.join(novaModulesDir, e.name, 'nova.mod.json');
|
|
593
877
|
let version = '?';
|
|
594
878
|
if (fs.existsSync(manifestPath)) {
|
|
@@ -600,35 +884,48 @@ moduleCmd
|
|
|
600
884
|
}
|
|
601
885
|
});
|
|
602
886
|
|
|
603
|
-
// novac module remove <name>
|
|
887
|
+
// novac module remove <name|glob> [name|glob ...]
|
|
604
888
|
moduleCmd
|
|
605
|
-
.command('remove <
|
|
606
|
-
.description('Remove
|
|
607
|
-
.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
889
|
+
.command('remove <names...>')
|
|
890
|
+
.description('Remove installed module(s) — supports wildcards e.g. "math*" or "*"')
|
|
891
|
+
.option('-y, --yes', 'Skip confirmation when removing multiple modules')
|
|
892
|
+
.action((names, opts) => {
|
|
893
|
+
const novaModulesDir = path.join(process.cwd(), 'nova_modules');
|
|
894
|
+
const resolved = expandNamesGlob(names, novaModulesDir);
|
|
895
|
+
|
|
896
|
+
if (resolved.length > 1 && !opts.yes) {
|
|
897
|
+
console.log(chalk.yellow(`About to remove ${resolved.length} module(s): ${resolved.join(', ')}`));
|
|
898
|
+
console.log(chalk.yellow('Re-run with --yes to confirm, or remove them one by one.'));
|
|
899
|
+
process.exit(0);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
let ok = 0, fail = 0;
|
|
903
|
+
for (const name of resolved) {
|
|
904
|
+
const destDir = path.join(novaModulesDir, name);
|
|
905
|
+
if (!fs.existsSync(destDir)) {
|
|
906
|
+
console.error(chalk.red(` fail "${name}" is not installed.`));
|
|
907
|
+
fail++; continue;
|
|
908
|
+
}
|
|
909
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
910
|
+
console.log(chalk.green(` ok "${name}" removed.`));
|
|
911
|
+
ok++;
|
|
612
912
|
}
|
|
613
|
-
|
|
614
|
-
|
|
913
|
+
if (resolved.length > 1) console.log(chalk.blue(`\nDone. ${ok} removed, ${fail} not found.`));
|
|
914
|
+
if (fail > 0 && ok === 0) process.exit(1);
|
|
615
915
|
});
|
|
616
916
|
|
|
617
|
-
// ── helpers
|
|
917
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
618
918
|
|
|
619
919
|
function _installNovamod(resolved, name, novaModulesDir) {
|
|
620
920
|
const bundle = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
621
921
|
const destDir = path.join(novaModulesDir, name);
|
|
622
922
|
fs.mkdirSync(destDir, { recursive: true });
|
|
623
|
-
// Write each source file into the module dir (preserving sub-paths)
|
|
624
923
|
for (const [relPath, content] of Object.entries(bundle.sources)) {
|
|
625
924
|
const fileDest = path.join(destDir, relPath);
|
|
626
925
|
fs.mkdirSync(path.dirname(fileDest), { recursive: true });
|
|
627
926
|
fs.writeFileSync(fileDest, content, 'utf8');
|
|
628
927
|
}
|
|
629
|
-
// Write the .novamod itself so _loadModuleExports can use it directly
|
|
630
928
|
fs.copyFileSync(resolved, path.join(destDir, `${name}.novamod`));
|
|
631
|
-
// Write manifest
|
|
632
929
|
fs.writeFileSync(path.join(destDir, 'nova.mod.json'), JSON.stringify(bundle.manifest, null, 2), 'utf8');
|
|
633
930
|
}
|
|
634
931
|
|