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/novac/bin/novac
ADDED
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { Command } = require('commander');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { run, tokenize, parse, format, Executor } = require('../src/index');
|
|
7
|
+
const stdlib = require('../src/runtime/stdlib');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const chalk = require('chalk').default;
|
|
11
|
+
const { getConfig, setConfig, interactiveSetup } = require('../src/core/config');
|
|
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
|
+
|
|
82
|
+
const program = new Command();
|
|
83
|
+
program.name('novac').description('Nova Language v2').version('2.0.0').enablePositionalOptions();
|
|
84
|
+
|
|
85
|
+
program
|
|
86
|
+
.argument('[file]')
|
|
87
|
+
.argument('[args...]')
|
|
88
|
+
.allowUnknownOption(true)
|
|
89
|
+
.passThroughOptions()
|
|
90
|
+
.action((file, args, options, command) => {
|
|
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('+'))) || [],
|
|
113
|
+
options,
|
|
114
|
+
addrs: {},
|
|
115
|
+
additions: {},
|
|
116
|
+
raw: process.argv,
|
|
117
|
+
};
|
|
118
|
+
const unknown = process.argv.slice(2).slice(options.length);
|
|
119
|
+
for (let i = 0; i < unknown.length; i++) {
|
|
120
|
+
const arg = unknown[i];
|
|
121
|
+
if (arg.startsWith('--')) {
|
|
122
|
+
const key = arg.slice(2);
|
|
123
|
+
const next = unknown[i + 1];
|
|
124
|
+
if (next && !next.startsWith('-')) {
|
|
125
|
+
cli.options[key] = next;
|
|
126
|
+
i++;
|
|
127
|
+
} else {
|
|
128
|
+
if (key.includes('=')) {
|
|
129
|
+
const [k, v] = key.split('=');
|
|
130
|
+
cli.options[k] = v;
|
|
131
|
+
} else {
|
|
132
|
+
cli.options[key] = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else if (arg.startsWith('-')) {
|
|
136
|
+
const key = arg.slice(1);
|
|
137
|
+
cli.options[key] = true;
|
|
138
|
+
} else if (arg.startsWith('@')) {
|
|
139
|
+
const key = arg.slice(1);
|
|
140
|
+
cli.addrs[key] = true;
|
|
141
|
+
} else if (arg.startsWith('+')) {
|
|
142
|
+
const key = arg.slice(1);
|
|
143
|
+
cli.additions[key] = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
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); }
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ─── novac test <file|glob> [file|glob ...] ───────────────────────────────────
|
|
174
|
+
program
|
|
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);
|
|
207
|
+
});
|
|
208
|
+
|
|
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);
|
|
257
|
+
} else {
|
|
258
|
+
// stdin REPL mode
|
|
259
|
+
let input = '';
|
|
260
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
261
|
+
rl.on('line', (line) => { input += line + '\n'; });
|
|
262
|
+
rl.on('close', () => {
|
|
263
|
+
try { console.log(format(input)); }
|
|
264
|
+
catch (e) { console.error(e.message || e); process.exit(1); }
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ─── novac eval <code> ────────────────────────────────────────────────────────
|
|
270
|
+
program.command('eval <code>')
|
|
271
|
+
.description('Evaluate Nova code inline')
|
|
272
|
+
.action((code) => {
|
|
273
|
+
try { run(code); }
|
|
274
|
+
catch (e) { console.error(e.message || e); process.exit(1); }
|
|
275
|
+
});
|
|
276
|
+
|
|
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
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
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
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ─── novac repl ───────────────────────────────────────────────────────────────
|
|
314
|
+
program.command('repl')
|
|
315
|
+
.description('Start interactive REPL')
|
|
316
|
+
.action(() => {
|
|
317
|
+
const executor = new Executor('', stdlib);
|
|
318
|
+
const historyFile = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.nova_repl_history');
|
|
319
|
+
|
|
320
|
+
console.log('Nova REPL v2.0.0');
|
|
321
|
+
console.log('Type .exit to exit\n');
|
|
322
|
+
|
|
323
|
+
let history = [];
|
|
324
|
+
try {
|
|
325
|
+
const historyContent = fs.readFileSync(historyFile, 'utf8');
|
|
326
|
+
history = historyContent.split('\n').filter(line => line.trim().length > 0);
|
|
327
|
+
} catch (e) { }
|
|
328
|
+
|
|
329
|
+
const rl = readline.createInterface({
|
|
330
|
+
input: process.stdin,
|
|
331
|
+
output: process.stdout,
|
|
332
|
+
prompt: 'nova> ',
|
|
333
|
+
history,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
rl.prompt();
|
|
337
|
+
|
|
338
|
+
rl.on('line', async (input) => {
|
|
339
|
+
const trimmed = input.trim();
|
|
340
|
+
|
|
341
|
+
if (trimmed) fs.appendFileSync(historyFile, trimmed + '\n');
|
|
342
|
+
|
|
343
|
+
if (trimmed === '.exit') {
|
|
344
|
+
rl.close();
|
|
345
|
+
} else if (trimmed === '.clear') {
|
|
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); }
|
|
358
|
+
} else if (trimmed.startsWith('.ast')) {
|
|
359
|
+
try {
|
|
360
|
+
const ast = parse(trimmed.slice(4).trim());
|
|
361
|
+
console.log(JSON.stringify(ast, null, 2));
|
|
362
|
+
} catch (e) { console.error('Error:', e.message || e); }
|
|
363
|
+
} else if (trimmed.startsWith('.tokens')) {
|
|
364
|
+
try {
|
|
365
|
+
const toks = tokenize(trimmed.slice(7).trim());
|
|
366
|
+
console.log(JSON.stringify(toks, null, 2));
|
|
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); }
|
|
382
|
+
}
|
|
383
|
+
} else if (trimmed.startsWith('.help')) {
|
|
384
|
+
console.log('Nova REPL Commands:');
|
|
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');
|
|
393
|
+
} else if (trimmed !== '') {
|
|
394
|
+
try {
|
|
395
|
+
const ast = parse(trimmed);
|
|
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); }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
rl.prompt();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
rl.on('close', () => {
|
|
419
|
+
console.log('\nGoodbye!');
|
|
420
|
+
process.exit(0);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// ─── novac new <project> ──────────────────────────────────────────────────────
|
|
425
|
+
program
|
|
426
|
+
.command('new <project>')
|
|
427
|
+
.description('Create a new Nova project')
|
|
428
|
+
.option('--cd', 'Print the cd command to enter the new project directory')
|
|
429
|
+
.action((project, opts) => {
|
|
430
|
+
const projectPath = path.resolve(project);
|
|
431
|
+
if (fs.existsSync(projectPath)) {
|
|
432
|
+
console.error(chalk.red('Directory already exists:', projectPath));
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
fs.mkdirSync(projectPath);
|
|
436
|
+
fs.mkdirSync(path.join(projectPath, 'src'));
|
|
437
|
+
fs.mkdirSync(path.join(projectPath, 'bin'));
|
|
438
|
+
fs.mkdirSync(path.join(projectPath, 'nova_modules'));
|
|
439
|
+
fs.writeFileSync(path.join(projectPath, 'src', 'main.nova'), `// Welcome to Nova!\n\nprint("Hello, Nova!");\n`, 'utf8');
|
|
440
|
+
const runFile = path.join(projectPath, 'bin', `${project}.nv`);
|
|
441
|
+
fs.writeFileSync(runFile, ``, { encoding: 'utf8', mode: 0o755 });
|
|
442
|
+
fs.writeFileSync(path.join(projectPath, 'README.md'), `# ${project}\n\nThis is a Nova project.`, 'utf8');
|
|
443
|
+
fs.writeFileSync(path.join(projectPath, '.gitignore'), `nova_modules/\n`, 'utf8');
|
|
444
|
+
const projectConfig = {
|
|
445
|
+
name: project,
|
|
446
|
+
version: '1.0.0',
|
|
447
|
+
description: '',
|
|
448
|
+
author: '',
|
|
449
|
+
license: 'MIT',
|
|
450
|
+
main: 'src/main.nova',
|
|
451
|
+
srcDir: 'src',
|
|
452
|
+
scripts: { run: `novac src/main.nova` },
|
|
453
|
+
dependencies: {},
|
|
454
|
+
devDependencies: {},
|
|
455
|
+
};
|
|
456
|
+
fs.writeFileSync(path.join(projectPath, 'nova.config.json'), JSON.stringify(projectConfig, null, 2), 'utf8');
|
|
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`));
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// ─── novac config ─────────────────────────────────────────────────────────────
|
|
470
|
+
program
|
|
471
|
+
.command('config <action>')
|
|
472
|
+
.argument('[key]', 'Configuration key (e.g., name, version, scripts.build)')
|
|
473
|
+
.argument('[value...]', 'Configuration value (optional)')
|
|
474
|
+
.description('Manage Nova configuration')
|
|
475
|
+
.action((action, key, value) => {
|
|
476
|
+
try {
|
|
477
|
+
if (action === 'get') {
|
|
478
|
+
if (!key) {
|
|
479
|
+
const allConfig = getConfig();
|
|
480
|
+
console.log(JSON.stringify(allConfig, null, 2));
|
|
481
|
+
} else {
|
|
482
|
+
const val = getConfig(key);
|
|
483
|
+
if (val === undefined) {
|
|
484
|
+
console.log(chalk.yellow(`Config key "${key}" not found`));
|
|
485
|
+
} else {
|
|
486
|
+
if (typeof val === 'object') {
|
|
487
|
+
console.log(JSON.stringify(val, null, 2));
|
|
488
|
+
} else {
|
|
489
|
+
console.log(val);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
} else if (action === 'set') {
|
|
494
|
+
if (!key || value.length === 0) {
|
|
495
|
+
console.error(chalk.red('Usage: novac config set <key> <value>'));
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
const valueStr = value.join(' ');
|
|
499
|
+
setConfig(key, valueStr);
|
|
500
|
+
console.log(chalk.green(`Config updated: ${key} = ${valueStr}`));
|
|
501
|
+
} else if (action === 'init') {
|
|
502
|
+
console.log('Initializing Nova configuration...\n');
|
|
503
|
+
interactiveSetup();
|
|
504
|
+
} else {
|
|
505
|
+
console.error(chalk.red(`Unknown action: ${action}. Use "get", "set", or "init".`));
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.error(chalk.red('Config error:', error.message));
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ─── novac etc <cmd> ─────────────────────────────────────────────────────────
|
|
515
|
+
program
|
|
516
|
+
.command('etc <cmd> [args...]')
|
|
517
|
+
.description('Run extra Nova commands (e.g., notices, describe, kit)')
|
|
518
|
+
.allowUnknownOption(true)
|
|
519
|
+
.action((cmd, args) => {
|
|
520
|
+
try {
|
|
521
|
+
if (cmd === 'notices') {
|
|
522
|
+
const notices = stdlib.getNotices();
|
|
523
|
+
if (notices.length === 0) {
|
|
524
|
+
console.log(chalk.green('No notices'));
|
|
525
|
+
} else {
|
|
526
|
+
console.log(chalk.yellow('Notices:'));
|
|
527
|
+
notices.forEach((n, i) => console.log(chalk.yellow(`${i + 1}. ${n}`)));
|
|
528
|
+
}
|
|
529
|
+
} else if (cmd === 'describe') {
|
|
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); }
|
|
535
|
+
const describer = require('../src/core/describe');
|
|
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
|
+
}
|
|
542
|
+
} else if (cmd === 'kit') {
|
|
543
|
+
// Supports multiple names and wildcards: novac etc kit math* utils --global
|
|
544
|
+
const isGlobal = args.includes('--global') || args.includes('-g');
|
|
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]'));
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
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++;
|
|
598
|
+
}
|
|
599
|
+
if (resolvedKits.length > 1) console.log(chalk.blue(`\nDone. ${ok} installed, ${fail} failed/skipped.`));
|
|
600
|
+
} else {
|
|
601
|
+
console.error(chalk.red(`Unknown command: ${cmd}. Use "notices", "describe", or "kit".`));
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
} catch (error) {
|
|
605
|
+
console.error(chalk.red('Error:', error.message));
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// ─── novac new-kit <dirname> ──────────────────────────────────────────────────
|
|
611
|
+
program
|
|
612
|
+
.command('new-kit <dirname>')
|
|
613
|
+
.description('Scaffold a new Nova kit in the current directory')
|
|
614
|
+
.action((dirname) => {
|
|
615
|
+
const kitPath = path.resolve(dirname);
|
|
616
|
+
if (fs.existsSync(kitPath)) {
|
|
617
|
+
console.error(chalk.red(`Directory already exists: ${kitPath}`));
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
fs.mkdirSync(kitPath, { recursive: true });
|
|
621
|
+
|
|
622
|
+
const kitdefSrc = `// ${dirname} — Nova kit
|
|
623
|
+
// Export all public API on the kitdef object.
|
|
624
|
+
// Each key becomes accessible after: import "${dirname}"
|
|
625
|
+
|
|
626
|
+
const kit = {
|
|
627
|
+
// TODO: implement your kit API here
|
|
628
|
+
hello() {
|
|
629
|
+
return 'Hello from ${dirname}!';
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
module.exports = { kitdef: kit };
|
|
634
|
+
`;
|
|
635
|
+
fs.writeFileSync(path.join(kitPath, 'kitdef.js'), kitdefSrc, 'utf8');
|
|
636
|
+
|
|
637
|
+
const indexNova = `// ${dirname} kit — Nova wrapper
|
|
638
|
+
// Import this file from Nova: import "${dirname}"
|
|
639
|
+
// Then call: hello()
|
|
640
|
+
|
|
641
|
+
let _kit = __js_require("./${dirname}/kitdef.js").kitdef;
|
|
642
|
+
export hello = _kit.hello;
|
|
643
|
+
`;
|
|
644
|
+
fs.writeFileSync(path.join(kitPath, 'index.nova'), indexNova, 'utf8');
|
|
645
|
+
|
|
646
|
+
const manifest = {
|
|
647
|
+
name: dirname,
|
|
648
|
+
version: '1.0.0',
|
|
649
|
+
description: '',
|
|
650
|
+
main: 'index.nova',
|
|
651
|
+
author: '',
|
|
652
|
+
license: 'MIT',
|
|
653
|
+
};
|
|
654
|
+
fs.writeFileSync(path.join(kitPath, 'nova.kit.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
|
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');
|
|
656
|
+
|
|
657
|
+
console.log(chalk.green(`Kit scaffolded at ${kitPath}`));
|
|
658
|
+
console.log(chalk.blue(' Edit kitdef.js to implement your API.'));
|
|
659
|
+
console.log(chalk.blue(' Update index.nova exports to match.'));
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// ─── novac init build | PATH ──────────────────────────────────────────────────
|
|
663
|
+
program
|
|
664
|
+
.command('init')
|
|
665
|
+
.description('Initialize novac settings or build project module')
|
|
666
|
+
.argument('<option>', 'Option: PATH | build')
|
|
667
|
+
.action((option) => {
|
|
668
|
+
if (option.toUpperCase() === 'BUILD') {
|
|
669
|
+
const configFile = path.join(process.cwd(), 'nova.config.json');
|
|
670
|
+
if (!fs.existsSync(configFile)) {
|
|
671
|
+
console.error(chalk.red('No nova.config.json found in current directory.'));
|
|
672
|
+
console.error(chalk.yellow('Run: novac config init to create one, or create nova.config.json manually.'));
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
676
|
+
const name = config.name || path.basename(process.cwd());
|
|
677
|
+
const version = config.version || '1.0.0';
|
|
678
|
+
const main = config.main || 'src/main.nova';
|
|
679
|
+
const srcDir = config.srcDir || 'src';
|
|
680
|
+
|
|
681
|
+
function collectNova(dir, base = dir) {
|
|
682
|
+
const results = {};
|
|
683
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
684
|
+
const full = path.join(dir, entry.name);
|
|
685
|
+
const rel = path.relative(base, full);
|
|
686
|
+
if (entry.isDirectory()) Object.assign(results, collectNova(full, base));
|
|
687
|
+
else if (entry.name.endsWith('.nova') || entry.name.endsWith('.nv')) {
|
|
688
|
+
results[rel] = fs.readFileSync(full, 'utf8');
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return results;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const srcAbsDir = path.join(process.cwd(), srcDir);
|
|
695
|
+
if (!fs.existsSync(srcAbsDir)) {
|
|
696
|
+
console.error(chalk.red(`Source directory not found: ${srcDir}`));
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const sources = collectNova(srcAbsDir);
|
|
701
|
+
if (!sources[main] && !sources[path.relative(srcAbsDir, path.join(process.cwd(), main))]) {
|
|
702
|
+
const mainRel = path.relative(srcAbsDir, path.join(process.cwd(), main));
|
|
703
|
+
if (!sources[mainRel] && !sources[main]) {
|
|
704
|
+
console.error(chalk.red(`Main entry "${main}" not found in collected sources.`));
|
|
705
|
+
console.error(chalk.yellow('Sources found: ' + Object.keys(sources).join(', ')));
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const bundle = {
|
|
711
|
+
manifest: { name, version, main: Object.keys(sources)[0], ...config },
|
|
712
|
+
sources,
|
|
713
|
+
buildTime: new Date().toISOString(),
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const mainKey = sources[main] ? main
|
|
717
|
+
: sources[path.relative(srcAbsDir, path.join(process.cwd(), main))]
|
|
718
|
+
? path.relative(srcAbsDir, path.join(process.cwd(), main))
|
|
719
|
+
: Object.keys(sources)[0];
|
|
720
|
+
bundle.manifest.main = mainKey;
|
|
721
|
+
|
|
722
|
+
const outFile = path.join(process.cwd(), `${name}.novamod`);
|
|
723
|
+
fs.writeFileSync(outFile, JSON.stringify(bundle, null, 2), 'utf8');
|
|
724
|
+
console.log(chalk.green(`Built module: ${name}.novamod`));
|
|
725
|
+
console.log(chalk.blue(` ${Object.keys(sources).length} source file(s) bundled`));
|
|
726
|
+
console.log(chalk.blue(` Entry: ${mainKey}`));
|
|
727
|
+
console.log(chalk.blue(` Install in another project: novac module install ./${name}.novamod`));
|
|
728
|
+
} else if (option.toUpperCase() === 'PATH') {
|
|
729
|
+
const platform = os.platform();
|
|
730
|
+
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
|
731
|
+
const binPath = path.join(npmPrefix, 'bin');
|
|
732
|
+
console.log(chalk.blue(`Adding ${binPath} to PATH for ${platform}...`));
|
|
733
|
+
try {
|
|
734
|
+
if (platform === 'win32') {
|
|
735
|
+
execSync(`setx PATH "%PATH%;${binPath}"`, { stdio: 'inherit' });
|
|
736
|
+
console.log(chalk.green('PATH updated on Windows. Restart your terminal.'));
|
|
737
|
+
} else if (['darwin','linux','freebsd','openbsd','sunos','aix','android'].includes(platform)) {
|
|
738
|
+
const shellRc = platform === 'darwin'
|
|
739
|
+
? path.join(process.env.HOME, '.zshrc')
|
|
740
|
+
: path.join(process.env.HOME, '.bashrc');
|
|
741
|
+
const exportCmd = `export PATH="$PATH:${binPath}"`;
|
|
742
|
+
execSync(`echo '${exportCmd}' >> ${shellRc}`, { stdio: 'inherit' });
|
|
743
|
+
console.log(chalk.green(`Added to ${shellRc}. Run 'source ${shellRc}' or restart your shell.`));
|
|
744
|
+
} else {
|
|
745
|
+
console.log(chalk.red('Unsupported platform. Manually add to PATH.'));
|
|
746
|
+
}
|
|
747
|
+
} catch (error) {
|
|
748
|
+
console.error(chalk.red('Failed to update PATH:', error.message));
|
|
749
|
+
console.log(chalk.yellow('Please manually add to PATH or run with admin/sudo.'));
|
|
750
|
+
}
|
|
751
|
+
} else {
|
|
752
|
+
console.log(chalk.red(`Unknown option: ${option}. Use 'PATH' or 'build'.`));
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// ─── novac module <subcommand> ────────────────────────────────────────────────
|
|
757
|
+
const moduleCmd = program.command('module').description('Manage Nova modules');
|
|
758
|
+
|
|
759
|
+
// novac module install [path|glob] [path|glob ...] [--global]
|
|
760
|
+
moduleCmd
|
|
761
|
+
.command('install [modpaths...]')
|
|
762
|
+
.description('Install .novamod bundle(s) or all deps from nova.config.json — supports globs e.g. "dist/*.novamod"')
|
|
763
|
+
.option('-g, --global', 'Install into global nova_modules (~/.novac/nova_modules)')
|
|
764
|
+
.action((modpaths, opts) => {
|
|
765
|
+
const globalDir = path.join(os.homedir(), '.novac', 'nova_modules');
|
|
766
|
+
const novaModulesDir = opts.global ? globalDir : path.join(process.cwd(), 'nova_modules');
|
|
767
|
+
if (opts.global) console.log(chalk.blue(`Installing globally to ${novaModulesDir}`));
|
|
768
|
+
fs.mkdirSync(novaModulesDir, { recursive: true });
|
|
769
|
+
|
|
770
|
+
if (!modpaths || modpaths.length === 0) {
|
|
771
|
+
// ── install all deps from nova.config.json ──
|
|
772
|
+
const configFile = path.join(process.cwd(), 'nova.config.json');
|
|
773
|
+
if (!fs.existsSync(configFile)) {
|
|
774
|
+
console.error(chalk.red('No nova.config.json found. Nothing to install.'));
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
778
|
+
const deps = { ...(config.dependencies || {}), ...(config.devDependencies || {}) };
|
|
779
|
+
const entries = Object.entries(deps);
|
|
780
|
+
if (entries.length === 0) {
|
|
781
|
+
console.log(chalk.yellow('No dependencies listed in nova.config.json.'));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
console.log(chalk.blue(`Installing ${entries.length} dependency(ies)...`));
|
|
785
|
+
let ok = 0, fail = 0;
|
|
786
|
+
for (const [name, ref] of entries) {
|
|
787
|
+
const destDir = path.join(novaModulesDir, name);
|
|
788
|
+
if (fs.existsSync(destDir)) {
|
|
789
|
+
console.log(chalk.yellow(` skip ${name} (already installed)`));
|
|
790
|
+
ok++; continue;
|
|
791
|
+
}
|
|
792
|
+
const resolved = path.isAbsolute(ref) ? ref : path.resolve(process.cwd(), ref);
|
|
793
|
+
if (fs.existsSync(resolved) && resolved.endsWith('.novamod')) {
|
|
794
|
+
_installNovamod(resolved, name, novaModulesDir);
|
|
795
|
+
console.log(chalk.green(` ok ${name}`));
|
|
796
|
+
ok++;
|
|
797
|
+
} else {
|
|
798
|
+
const kitDir = path.join(__dirname, '..', 'kits', name);
|
|
799
|
+
if (fs.existsSync(kitDir)) {
|
|
800
|
+
_installKit(kitDir, name, novaModulesDir);
|
|
801
|
+
console.log(chalk.green(` ok ${name} (built-in kit)`));
|
|
802
|
+
ok++;
|
|
803
|
+
} else {
|
|
804
|
+
console.error(chalk.red(` fail ${name}: cannot resolve "${ref}"`));
|
|
805
|
+
fail++;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
console.log(chalk.blue(`Done. ${ok} installed, ${fail} failed.`));
|
|
810
|
+
} else {
|
|
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(', ')));
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
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++;
|
|
842
|
+
}
|
|
843
|
+
if (resolved.length > 1) console.log(chalk.blue(`\nDone. ${ok} installed, ${fail} failed/skipped.`));
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// novac module list [pattern]
|
|
848
|
+
moduleCmd
|
|
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
|
+
|
|
858
|
+
if (!fs.existsSync(novaModulesDir)) {
|
|
859
|
+
console.log(chalk.yellow(`No ${scope} nova_modules directory found.`));
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
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
|
+
}
|
|
869
|
+
if (entries.length === 0) {
|
|
870
|
+
console.log(chalk.yellow(pattern ? `No ${scope} modules matching "${pattern}".` : `No ${scope} modules installed.`));
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
console.log(chalk.blue(`Installed ${scope} modules${pattern ? ` matching "${pattern}"` : ''}:`));
|
|
874
|
+
for (const e of entries) {
|
|
875
|
+
const manifestPath = path.join(novaModulesDir, e.name, 'nova.kit.json');
|
|
876
|
+
const bundleManifest = path.join(novaModulesDir, e.name, 'nova.mod.json');
|
|
877
|
+
let version = '?';
|
|
878
|
+
if (fs.existsSync(manifestPath)) {
|
|
879
|
+
try { version = JSON.parse(fs.readFileSync(manifestPath, 'utf8')).version || '?'; } catch {}
|
|
880
|
+
} else if (fs.existsSync(bundleManifest)) {
|
|
881
|
+
try { version = JSON.parse(fs.readFileSync(bundleManifest, 'utf8')).version || '?'; } catch {}
|
|
882
|
+
}
|
|
883
|
+
console.log(` ${chalk.green(e.name)} ${chalk.gray(version)}`);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
// novac module remove <name|glob> [name|glob ...]
|
|
888
|
+
moduleCmd
|
|
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++;
|
|
912
|
+
}
|
|
913
|
+
if (resolved.length > 1) console.log(chalk.blue(`\nDone. ${ok} removed, ${fail} not found.`));
|
|
914
|
+
if (fail > 0 && ok === 0) process.exit(1);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
918
|
+
|
|
919
|
+
function _installNovamod(resolved, name, novaModulesDir) {
|
|
920
|
+
const bundle = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
921
|
+
const destDir = path.join(novaModulesDir, name);
|
|
922
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
923
|
+
for (const [relPath, content] of Object.entries(bundle.sources)) {
|
|
924
|
+
const fileDest = path.join(destDir, relPath);
|
|
925
|
+
fs.mkdirSync(path.dirname(fileDest), { recursive: true });
|
|
926
|
+
fs.writeFileSync(fileDest, content, 'utf8');
|
|
927
|
+
}
|
|
928
|
+
fs.copyFileSync(resolved, path.join(destDir, `${name}.novamod`));
|
|
929
|
+
fs.writeFileSync(path.join(destDir, 'nova.mod.json'), JSON.stringify(bundle.manifest, null, 2), 'utf8');
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function _installKit(kitDir, name, novaModulesDir) {
|
|
933
|
+
const kitdefPath = path.join(kitDir, 'kitdef.js');
|
|
934
|
+
if (!fs.existsSync(kitdefPath)) throw new Error(`Kit ${name} has no kitdef.js`);
|
|
935
|
+
const { kitdef } = require(kitdefPath);
|
|
936
|
+
const destDir = path.join(novaModulesDir, name);
|
|
937
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
938
|
+
fs.copyFileSync(kitdefPath, path.join(destDir, 'kitdef.js'));
|
|
939
|
+
const members = Object.keys(kitdef);
|
|
940
|
+
const novaLines = [
|
|
941
|
+
`// Auto-generated kit wrapper for ${name}`,
|
|
942
|
+
`let _kit = __js_require("${path.join(destDir, 'kitdef.js')}").kitdef;`,
|
|
943
|
+
...members.map(m => `export ${m} = _kit.${m};`),
|
|
944
|
+
].join('\n') + '\n';
|
|
945
|
+
fs.writeFileSync(path.join(destDir, 'index.nova'), novaLines, 'utf8');
|
|
946
|
+
const manifest = { name, version: '1.0.0', main: 'index.nova', type: 'kit' };
|
|
947
|
+
fs.writeFileSync(path.join(destDir, 'nova.kit.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
program.parse();
|