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.
Files changed (161) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1574 -597
  3. package/bin/novac +468 -171
  4. package/bin/nvc +522 -0
  5. package/bin/nvml +78 -17
  6. package/demo.nv +0 -0
  7. package/demo_builtins.nv +0 -0
  8. package/demo_http.nv +0 -0
  9. package/examples/bf.nv +69 -0
  10. package/examples/math.nv +21 -0
  11. package/kits/birdAPI/kitdef.js +954 -0
  12. package/kits/kitRNG/kitdef.js +740 -0
  13. package/kits/kitSSH/kitdef.js +1272 -0
  14. package/kits/kitadb/kitdef.js +606 -0
  15. package/kits/kitai/kitdef.js +2185 -0
  16. package/kits/kitansi/kitdef.js +1402 -0
  17. package/kits/kitcanvas/kitdef.js +914 -0
  18. package/kits/kitclippy/kitdef.js +925 -0
  19. package/kits/kitformat/kitdef.js +1485 -0
  20. package/kits/kitgps/kitdef.js +1862 -0
  21. package/kits/kitlibproc/kitdef.js +3 -2
  22. package/kits/kitmatrix/ex.js +19 -0
  23. package/kits/kitmatrix/kitdef.js +960 -0
  24. package/kits/kitmorse/kitdef.js +229 -0
  25. package/kits/kitmpatch/kitdef.js +906 -0
  26. package/kits/kitnet/kitdef.js +1401 -0
  27. package/kits/kitnovacweb/README.md +1416 -143
  28. package/kits/kitnovacweb/kitdef.js +92 -2
  29. package/kits/kitnovacweb/nvml/executor.js +578 -176
  30. package/kits/kitnovacweb/nvml/index.js +2 -2
  31. package/kits/kitnovacweb/nvml/lexer.js +72 -69
  32. package/kits/kitnovacweb/nvml/parser.js +328 -159
  33. package/kits/kitnovacweb/nvml/renderer.js +770 -270
  34. package/kits/kitparse/kitdef.js +1688 -0
  35. package/kits/kitproto/kitdef.js +613 -0
  36. package/kits/kitqr/kitdef.js +637 -0
  37. package/kits/kitregex++/kitdef.js +1353 -0
  38. package/kits/kitrequire/kitdef.js +1599 -0
  39. package/kits/kitx11/kitdef.js +1 -0
  40. package/kits/kitx11/kitx11.js +2472 -0
  41. package/kits/kitx11/kitx11_conn.js +948 -0
  42. package/kits/kitx11/kitx11_worker.js +121 -0
  43. package/kits/libtea/kitdef.js +2691 -0
  44. package/kits/libterm/ex.js +285 -0
  45. package/kits/libterm/kitdef.js +1927 -0
  46. package/novac/LICENSE +21 -0
  47. package/novac/README.md +1823 -0
  48. package/novac/bin/novac +950 -0
  49. package/novac/bin/nvc +522 -0
  50. package/novac/bin/nvml +542 -0
  51. package/novac/demo.nv +245 -0
  52. package/novac/demo_builtins.nv +209 -0
  53. package/novac/demo_http.nv +62 -0
  54. package/novac/examples/bf.nv +69 -0
  55. package/novac/examples/math.nv +21 -0
  56. package/novac/kits/kitai/kitdef.js +2185 -0
  57. package/novac/kits/kitansi/kitdef.js +1402 -0
  58. package/novac/kits/kitformat/kitdef.js +1485 -0
  59. package/novac/kits/kitgps/kitdef.js +1862 -0
  60. package/novac/kits/kitlibfs/kitdef.js +231 -0
  61. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  62. package/novac/kits/kitmatrix/ex.js +19 -0
  63. package/novac/kits/kitmatrix/kitdef.js +960 -0
  64. package/novac/kits/kitmpatch/kitdef.js +906 -0
  65. package/novac/kits/kitnovacweb/README.md +1572 -0
  66. package/novac/kits/kitnovacweb/demo.nv +12 -0
  67. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  68. package/novac/kits/kitnovacweb/index.nova +12 -0
  69. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  70. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  71. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  72. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  73. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  74. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  75. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  76. package/novac/kits/kitparse/kitdef.js +1688 -0
  77. package/novac/kits/kitregex++/kitdef.js +1353 -0
  78. package/novac/kits/kitrequire/kitdef.js +1599 -0
  79. package/novac/kits/kitx11/kitdef.js +1 -0
  80. package/novac/kits/kitx11/kitx11.js +2472 -0
  81. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  82. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  83. package/novac/kits/libtea/tf.js +2691 -0
  84. package/novac/kits/libterm/ex.js +285 -0
  85. package/novac/kits/libterm/kitdef.js +1927 -0
  86. package/novac/node_modules/chalk/license +9 -0
  87. package/novac/node_modules/chalk/package.json +83 -0
  88. package/novac/node_modules/chalk/readme.md +297 -0
  89. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  90. package/novac/node_modules/chalk/source/index.js +225 -0
  91. package/novac/node_modules/chalk/source/utilities.js +33 -0
  92. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  93. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  94. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  95. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  96. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  97. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  98. package/novac/node_modules/commander/LICENSE +22 -0
  99. package/novac/node_modules/commander/Readme.md +1176 -0
  100. package/novac/node_modules/commander/esm.mjs +16 -0
  101. package/novac/node_modules/commander/index.js +24 -0
  102. package/novac/node_modules/commander/lib/argument.js +150 -0
  103. package/novac/node_modules/commander/lib/command.js +2777 -0
  104. package/novac/node_modules/commander/lib/error.js +39 -0
  105. package/novac/node_modules/commander/lib/help.js +747 -0
  106. package/novac/node_modules/commander/lib/option.js +380 -0
  107. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  108. package/novac/node_modules/commander/package-support.json +19 -0
  109. package/novac/node_modules/commander/package.json +82 -0
  110. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  111. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  112. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  113. package/novac/node_modules/node-addon-api/README.md +95 -0
  114. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  115. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  116. package/novac/node_modules/node-addon-api/index.js +14 -0
  117. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  118. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  119. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  120. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  121. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  122. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  123. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  124. package/novac/node_modules/node-addon-api/package.json +480 -0
  125. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  126. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  127. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  128. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  129. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  130. package/novac/node_modules/serialize-javascript/README.md +149 -0
  131. package/novac/node_modules/serialize-javascript/index.js +297 -0
  132. package/novac/node_modules/serialize-javascript/package.json +33 -0
  133. package/novac/package.json +27 -0
  134. package/novac/scripts/update-bin.js +24 -0
  135. package/novac/src/core/bstd.js +1035 -0
  136. package/novac/src/core/config.js +155 -0
  137. package/novac/src/core/describe.js +187 -0
  138. package/novac/src/core/emitter.js +499 -0
  139. package/novac/src/core/error.js +86 -0
  140. package/novac/src/core/executor.js +5606 -0
  141. package/novac/src/core/formatter.js +686 -0
  142. package/novac/src/core/lexer.js +1026 -0
  143. package/novac/src/core/nova_builtins.js +717 -0
  144. package/novac/src/core/nova_thread_worker.js +166 -0
  145. package/novac/src/core/parser.js +2181 -0
  146. package/novac/src/core/types.js +112 -0
  147. package/novac/src/index.js +28 -0
  148. package/novac/src/runtime/stdlib.js +244 -0
  149. package/package.json +6 -3
  150. package/scripts/update-bin.js +0 -0
  151. package/src/core/bstd.js +838 -362
  152. package/src/core/executor.js +2578 -170
  153. package/src/core/lexer.js +502 -54
  154. package/src/core/nova_builtins.js +21 -3
  155. package/src/core/parser.js +413 -72
  156. package/src/core/types.js +30 -2
  157. package/src/index.js +0 -0
  158. package/examples/example-project/README.md +0 -3
  159. package/examples/example-project/src/main.nova +0 -3
  160. package/src/core/environment.js +0 -0
  161. /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) // allow extra options
88
+ .allowUnknownOption(true)
20
89
  .passThroughOptions()
21
90
  .action((file, args, options, command) => {
22
- const src = fs.readFileSync(path.resolve(file), 'utf8');
23
- let cli = {
24
- args: args.filter((a) => !(a.startsWith('-') || a.startsWith('@') || a.startsWith('+'))) || [],
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: {}, // @addr
27
- additions: {}, // +feature
114
+ addrs: {},
115
+ additions: {},
28
116
  raw: process.argv,
29
- };
30
- const unknown = process.argv.slice(2).slice(options.length); // array
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++; // skip next
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
- cli.options[key] = true;
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 <file>')
64
- .description('Test a Nova file by checking it\'s syntax')
65
- .action((file) => {
66
- const src = fs.readFileSync(path.resolve(file), 'utf8');
67
- try { parse(src); console.log(chalk.green('Syntax OK')); }
68
- catch (e) { console.error(chalk.red('Syntax Error:', e.message || e)); process.exit(1); }
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
- program.command('format [file]')
72
- .description('Format a Nova file')
73
- .action((file) => {
74
- if (file) {
75
- const src = fs.readFileSync(path.resolve(file), 'utf8');
76
- try { console.log(format(src)); }
77
- catch (e) { console.error(e.message || e); process.exit(1); }
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
- // a repl
81
- const rl = readline.createInterface({
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
- program.command('tokens <file>')
105
- .description('Print token stream of a Nova file')
106
- .action((file) => {
107
- const src = fs.readFileSync(path.resolve(file), 'utf8');
108
- try { console.log(JSON.stringify(tokenize(src), null, 2)); }
109
- catch (e) { console.error(e.message || e); process.exit(1); }
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
- program.command('ast <file>')
113
- .description('Print AST of a Nova file')
114
- .action((file) => {
115
- const src = fs.readFileSync(path.resolve(file), 'utf8');
116
- try { console.log(JSON.stringify(parse(src), null, 2)); }
117
- catch (e) { console.error(e.message || e); process.exit(1); }
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: 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
- console.error('Error:', e.message || e);
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 Exit REPL');
173
- console.log(' .clear Clear screen');
174
- console.log(' .ast <code> Show AST');
175
- console.log(' .tokens <code> Show tokens');
176
- console.log(' .help Show this help');
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
- const result = executor.run(ast);
181
- if (result !== undefined) console.log(result);
182
- } catch (e) {
183
- console.error('Error:', e.message || e);
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
- program.command('new <project> ')
197
- .option('--cd, cd into project directory after creation')
424
+ // ─── novac new <project> ──────────────────────────────────────────────────────
425
+ program
426
+ .command('new <project>')
198
427
  .description('Create a new Nova project')
199
- .action((project, cd) => {
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'), ``, 'utf8');
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('Project created at', projectPath));
229
- if (cd.cd) {
230
- execSync(`cd ${projectPath}`, { stdio: 'inherit', shell: true });
231
- console.log(chalk.blue('Changed directory to', projectPath));
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
- //extra commands: novac etc <cmd> to run extra nova commands like notices, describe, etc.
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
- const content = fs.readFileSync(args[0], 'utf8');
297
- const ast = parse(content);
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 description = describer.describe(ast);
300
- console.log(description);
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
- // Install a built-in kit into nova_modules (local or global)
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
- if (!kitName || kitName.startsWith('-')) { console.error(chalk.red('Usage: novac etc kit <kitname> [--global]')); process.exit(1); }
308
- const kitsRoot = path.join(__dirname, '..', 'kits');
309
- const kitDir = path.join(kitsRoot, kitName);
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 baseDir = isGlobal ? path.join(os.homedir(), '.novac', 'nova_modules') : path.join(process.cwd(), 'nova_modules');
315
- const destPath = path.join(baseDir, kitName);
316
- if (isGlobal) console.log(chalk.blue(`Installing globally to ${destPath}`));
317
- if (fs.existsSync(destPath)) {
318
- console.error(chalk.red(`Kit "${kitName}" already installed at ${destPath}. Remove it first.`));
319
- process.exit(1);
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
- const kitdefPath = path.join(kitDir, 'kitdef.js');
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' ? path.join(process.env.HOME, '.zshrc') : path.join(process.env.HOME, '.bashrc');
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 [modpath]')
498
- .description('Install a .novamod bundle, or install all dependencies from nova.config.json')
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((modpath, opts) => {
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 (!modpath) {
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 single .novamod ──
551
- const resolved = path.isAbsolute(modpath) ? modpath : path.resolve(process.cwd(), modpath);
552
- if (!fs.existsSync(resolved)) {
553
- console.error(chalk.red(`File not found: ${resolved}`));
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
- const bundle = JSON.parse(fs.readFileSync(resolved, 'utf8'));
561
- const name = bundle.manifest.name;
562
- if (!name) { console.error(chalk.red('Bundle has no manifest.name')); process.exit(1); }
563
- const destDir = path.join(novaModulesDir, name);
564
- if (fs.existsSync(destDir)) {
565
- console.error(chalk.red(`Module "${name}" already installed. Remove nova_modules/${name} first.`));
566
- process.exit(1);
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
- _installNovamod(resolved, name, novaModulesDir);
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 in nova_modules/')
578
- .action(() => {
579
- const novaModulesDir = path.join(process.cwd(), 'nova_modules');
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('No nova_modules directory found.'));
859
+ console.log(chalk.yellow(`No ${scope} nova_modules directory found.`));
582
860
  return;
583
861
  }
584
- const entries = fs.readdirSync(novaModulesDir, { withFileTypes: true }).filter(e => e.isDirectory());
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('No modules installed.'));
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('Installed modules:'));
873
+ console.log(chalk.blue(`Installed ${scope} modules${pattern ? ` matching "${pattern}"` : ''}:`));
590
874
  for (const e of entries) {
591
- const manifestPath = path.join(novaModulesDir, e.name, 'nova.kit.json');
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 <name>')
606
- .description('Remove an installed module from nova_modules/')
607
- .action((name) => {
608
- const destDir = path.join(process.cwd(), 'nova_modules', name);
609
- if (!fs.existsSync(destDir)) {
610
- console.error(chalk.red(`Module "${name}" is not installed.`));
611
- process.exit(1);
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
- fs.rmSync(destDir, { recursive: true, force: true });
614
- console.log(chalk.green(`Module "${name}" removed.`));
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