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/novac/bin/nvml ADDED
@@ -0,0 +1,542 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * nvml — NVML CLI
6
+ * Part of the novac toolchain.
7
+ *
8
+ * Usage:
9
+ * nvml <port> <file.nvml> [route] Serve file.nvml on localhost:<port>
10
+ * nvml compile <file.nvml> Compile to HTML (stdout)
11
+ * nvml ast <file.nvml> Print AST as JSON
12
+ * nvml tokens <file.nvml> Print token stream as JSON
13
+ * nvml check <file.nvml> Check syntax
14
+ * nvml --help Show help
15
+ * nvml --version Show version
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const http = require('http');
21
+ const os = require('os');
22
+ const url = require('url');
23
+
24
+ const chalk = (() => {
25
+ try { return require('chalk').default ?? require('chalk'); } catch { return null; }
26
+ })();
27
+
28
+ const VERSION = '1.0.0';
29
+
30
+ // ── colour helpers (graceful fallback if chalk absent) ────────
31
+ const c = {
32
+ green: s => chalk ? chalk.green(s) : s,
33
+ red: s => chalk ? chalk.red(s) : s,
34
+ cyan: s => chalk ? chalk.cyan(s) : s,
35
+ bold: s => chalk ? chalk.bold(s) : s,
36
+ dim: s => chalk ? chalk.dim(s) : s,
37
+ yellow: s => chalk ? chalk.yellow(s) : s,
38
+ };
39
+
40
+ function die(msg) {
41
+ process.stderr.write(c.red('[nvml] ' + msg) + '\n');
42
+ process.exit(1);
43
+ }
44
+
45
+ // ── locate kitnovacweb/nvml/index.js ─────────────────────────
46
+ function resolveNvmlKit() {
47
+ const candidates = [
48
+ path.join(__dirname, '..', 'kits', 'kitnovacweb', 'nvml', 'index.js'),
49
+ path.join(process.cwd(), 'nova_modules', 'kitnovacweb', 'nvml', 'index.js'),
50
+ path.join(os.homedir(), '.novac', 'nova_modules', 'kitnovacweb', 'nvml', 'index.js'),
51
+ path.join(__dirname, '..', 'node_modules', 'kitnovacweb', 'nvml', 'index.js'),
52
+ ];
53
+ for (const p of candidates) {
54
+ if (fs.existsSync(p)) return p;
55
+ }
56
+ return null;
57
+ }
58
+
59
+ function loadKit() {
60
+ const kitPath = resolveNvmlKit();
61
+ if (!kitPath) {
62
+ die(
63
+ 'kitnovacweb not found.\n\n' +
64
+ ' Install it with:\n' +
65
+ ' novac etc kit kitnovacweb\n\n' +
66
+ ' Or put it in nova_modules/kitnovacweb/'
67
+ );
68
+ }
69
+ return require(kitPath);
70
+ }
71
+
72
+ function loadLexer() {
73
+ const kitPath = resolveNvmlKit();
74
+ if (!kitPath) die('kitnovacweb not found.');
75
+ return require(path.join(path.dirname(kitPath), 'lexer.js'));
76
+ }
77
+
78
+ // ── Nova runtime loader ───────────────────────────────────────
79
+ // Finds src/index.js and returns the `run` function, or null if not found.
80
+ function resolveNovaRun() {
81
+ const searchPaths = [
82
+ path.join(__dirname, '..', 'src', 'index.js'),
83
+ path.join(process.cwd(), 'src', 'index.js'),
84
+ path.join(os.homedir(), '.novac', 'node_modules', 'novac', 'src', 'index.js'),
85
+ path.resolve(process.cwd(), 'node_modules', 'novac', 'src', 'index.js'),
86
+ ];
87
+ for (const p of searchPaths) {
88
+ if (fs.existsSync(p)) {
89
+ try {
90
+ const m = require(p);
91
+ return m.run || m.default?.run || null;
92
+ } catch (e) {
93
+ process.stderr.write(`[nvml] Warning: failed to load novac runtime from ${p}: ${e.message}\n`);
94
+ return null;
95
+ }
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+
101
+ // ── document proxy for server-side Nova scripts ───────────────
102
+ function findElementById(elements, id) {
103
+ for (const el of elements) {
104
+ if (el.props && el.props.id === id) return el;
105
+ if (el.children) {
106
+ const found = findElementById(el.children, id);
107
+ if (found) return found;
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+
113
+ function makeDocumentProxy(doc) {
114
+ return {
115
+ setConfig: (k, v) => { doc.config[k] = v; },
116
+ getConfig: (k) => doc.config[k],
117
+ setTitle: (t) => { doc.config.title = t; },
118
+ setMeta: (k, v) => { doc.config[k] = v; },
119
+ set: (id, content) => {
120
+ const el = findElementById(doc.visual, id);
121
+ if (el) el.textValue = String(content);
122
+ },
123
+ get: (id) => {
124
+ const el = findElementById(doc.visual, id);
125
+ return el ? (el.textValue || null) : null;
126
+ },
127
+ setProp: (id, key, value) => {
128
+ const el = findElementById(doc.visual, id);
129
+ if (el) el.props[String(key)] = String(value);
130
+ },
131
+ getProp: (id, key) => {
132
+ const el = findElementById(doc.visual, id);
133
+ return el ? (el.props[String(key)] || null) : null;
134
+ },
135
+ addClass: (id, cls) => {
136
+ const el = findElementById(doc.visual, id);
137
+ if (!el) return;
138
+ const existing = el.props.class || '';
139
+ el.props.class = (existing ? existing + ' ' : '') + cls;
140
+ },
141
+ setClass: (id, cls) => {
142
+ const el = findElementById(doc.visual, id);
143
+ if (el) el.props.class = String(cls);
144
+ },
145
+ addElementStyle: (id, css) => {
146
+ const el = findElementById(doc.visual, id);
147
+ if (el) el.ss = (el.ss ? el.ss + '\n' : '') + css;
148
+ },
149
+ addStyle: (css) => { doc.globalStyles += '\n' + css; },
150
+ hide: (id) => {
151
+ const el = findElementById(doc.visual, id);
152
+ if (el) el.ss = (el.ss ? el.ss + '\n' : '') + 'display: none;';
153
+ },
154
+ show: (id, display) => {
155
+ const el = findElementById(doc.visual, id);
156
+ if (el) el.ss = (el.ss ? el.ss + '\n' : '') + `display: ${display || 'block'};`;
157
+ },
158
+ alert: (_msg) => {},
159
+ config: doc.config,
160
+ _doc: doc,
161
+ };
162
+ }
163
+
164
+ // ── error page (served when compile fails during --serve) ─────
165
+ function errorPage(file, err) {
166
+ const esc = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
167
+ return `<!DOCTYPE html>
168
+ <html><head><meta charset="UTF-8"><title>NVML Error</title>
169
+ <style>
170
+ body{font-family:monospace;background:#1a1a1a;color:#eee;padding:2rem;margin:0}
171
+ h1{color:#f87171;font-size:1.1rem;margin:0 0 .75rem}
172
+ .file{color:#94a3b8;font-size:.8rem;margin-bottom:1.5rem}
173
+ pre{background:#111;border:1px solid #333;padding:1rem;border-radius:6px;
174
+ white-space:pre-wrap;word-break:break-word;font-size:.85rem;line-height:1.6}
175
+ .msg{color:#fca5a5}
176
+ .stack{color:#475569;font-size:.75rem;margin-top:.75rem}
177
+ </style></head><body>
178
+ <h1>NVML Error</h1>
179
+ <div class="file">${esc(file)}</div>
180
+ <pre class="msg">${esc(err.message)}</pre>
181
+ <pre class="stack">${esc(err.stack || '')}</pre>
182
+ </body></html>`;
183
+ }
184
+
185
+ // ── serve ─────────────────────────────────────────────────────
186
+ function cmdServe(port, nvmlFile, route) {
187
+ const nvml = loadKit();
188
+ const absFile = path.resolve(process.cwd(), nvmlFile);
189
+
190
+ if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
191
+ if (!route.startsWith('/')) route = '/' + route;
192
+
193
+ // Load the Nova runtime once — used for every server-side script block
194
+ const novaRun = resolveNovaRun();
195
+ if (!novaRun) {
196
+ process.stderr.write(
197
+ c.yellow('[nvml] Warning: novac runtime not found — server-side Nova scripts will not run.\n') +
198
+ c.dim(' Searched: src/index.js relative to bin/ and cwd\n\n')
199
+ );
200
+ }
201
+
202
+ // Build a novaRunner for a given NvmlDocument + request context
203
+ function makeNovaRunner(reqCtx) {
204
+ if (!novaRun) return null;
205
+ return (code, doc) => {
206
+ const scope = {
207
+ document: makeDocumentProxy(doc),
208
+ request: reqCtx || {},
209
+ };
210
+ novaRun(code, scope);
211
+ };
212
+ }
213
+
214
+ // Build a mutation-capturing proxy for /_nvml/run
215
+ const _sseClients = new Set(); // Server-Sent Events connections
216
+
217
+ function makeMutationProxy(mutations, live) {
218
+ return {
219
+ // config
220
+ setConfig: (k, v) => mutations.push({ type: 'setConfig', key: String(k), value: v }),
221
+ setTitle: (t) => mutations.push({ type: 'setConfig', key: 'title', value: t }),
222
+ setMeta: (k, v) => mutations.push({ type: 'setConfig', key: String(k), value: v }),
223
+ // text / html
224
+ set: (id, val) => mutations.push({ type: 'setText', id: String(id), value: String(val) }),
225
+ setHTML: (id, val) => mutations.push({ type: 'setHTML', id: String(id), value: String(val) }),
226
+ // props / attrs
227
+ setProp: (id, k, v) => mutations.push({ type: 'setProp', id: String(id), key: String(k), value: String(v) }),
228
+ setAttr: (id, k, v) => mutations.push({ type: 'setAttr', id: String(id), key: String(k), value: String(v) }),
229
+ removeAttr: (id, k) => mutations.push({ type: 'removeAttr', id: String(id), key: String(k) }),
230
+ // classes
231
+ addClass: (id, cls) => mutations.push({ type: 'addClass', id: String(id), value: String(cls) }),
232
+ removeClass: (id, cls) => mutations.push({ type: 'removeClass', id: String(id), value: String(cls) }),
233
+ toggleClass: (id, cls, f) => mutations.push({ type: 'toggleClass', id: String(id), value: String(cls), force: f }),
234
+ setClass: (id, cls) => mutations.push({ type: 'setClass', id: String(id), value: String(cls) }),
235
+ // style
236
+ addStyle: (css) => mutations.push({ type: 'addStyle', value: String(css) }),
237
+ addElementStyle:(id, css) => mutations.push({ type: 'addStyle', id: String(id), value: String(css) }),
238
+ setStyle: (id, k, v) => mutations.push({ type: 'setStyle', id: String(id), key: String(k), value: String(v) }),
239
+ setCSSVar: (name, val) => mutations.push({ type: 'setCSSVar', name: String(name), value: String(val) }),
240
+ // visibility
241
+ hide: (id, t) => mutations.push({ type: 'hide', id: String(id), transition: t || null }),
242
+ show: (id, d, t) => mutations.push({ type: 'show', id: String(id), value: d || 'block', transition: t || null }),
243
+ // DOM manipulation
244
+ remove: (id, t) => mutations.push({ type: 'remove', id: String(id), transition: t || null }),
245
+ appendChild: (id, html) => mutations.push({ type: 'appendChild', id: String(id), html: String(html) }),
246
+ insertBefore: (id, html) => mutations.push({ type: 'insertBefore', id: String(id), html: String(html) }),
247
+ insertAfter: (id, html) => mutations.push({ type: 'insertAfter', id: String(id), html: String(html) }),
248
+ // focus / scroll
249
+ focus: (id) => mutations.push({ type: 'focus', id: String(id) }),
250
+ blur: (id) => mutations.push({ type: 'blur', id: String(id) }),
251
+ scroll: (id, beh) => mutations.push({ type: 'scroll', id: String(id), behavior: beh || 'smooth' }),
252
+ // signals
253
+ setSignal: (name, val) => mutations.push({ type: 'setSignal', name: String(name), value: val }),
254
+ // list patch
255
+ patchList: (id, items, tpl, key) => mutations.push({ type: 'patchList', id: String(id), items, template: String(tpl), key: key || null }),
256
+ // navigation
257
+ navigate: (path) => mutations.push({ type: 'navigate', path: String(path) }),
258
+ reload: () => mutations.push({ type: 'reload' }),
259
+ redirect: (url) => mutations.push({ type: 'redirect', url: String(url) }),
260
+ // feedback
261
+ alert: (msg) => mutations.push({ type: 'alert', value: String(msg) }),
262
+ toast: (msg, d, t) => mutations.push({ type: 'toast', value: String(msg), duration: d || 3000, type: t || 'info' }),
263
+ console: (msg, lvl) => mutations.push({ type: 'console', value: String(msg), level: lvl || 'log' }),
264
+ // reads from live snapshot
265
+ get: (id) => (live.elements && live.elements[id]) ? live.elements[id].text : null,
266
+ getValue: (id) => (live.elements && live.elements[id]) ? live.elements[id].value : null,
267
+ getClass: (id) => (live.elements && live.elements[id]) ? live.elements[id].class : null,
268
+ getConfig: (k) => live[k] ?? null,
269
+ getSignal: (n) => (live.state && live.state[n]) ?? null,
270
+ config: live,
271
+ // SSE push (server → all connected clients)
272
+ push: (name, val) => {
273
+ const msg = `event: signal\ndata: ${JSON.stringify({ name, value: val })}\n\n`;
274
+ _sseClients.forEach(c => { try { c.write(msg); } catch(_) {} });
275
+ },
276
+ pushMutations: (muts) => {
277
+ const msg = `event: mutations\ndata: ${JSON.stringify(muts)}\n\n`;
278
+ _sseClients.forEach(c => { try { c.write(msg); } catch(_) {} });
279
+ },
280
+ };
281
+ }
282
+
283
+ const server = http.createServer((req, res) => {
284
+ const parsed = url.parse(req.url || '/');
285
+ const urlPath = parsed.pathname || '/';
286
+
287
+ // CORS
288
+ res.setHeader('Access-Control-Allow-Origin', '*');
289
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
290
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
291
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
292
+
293
+ // ── /_nvml/sse — Server-Sent Events for server-push signal updates ──
294
+ if (urlPath === '/_nvml/sse' && req.method === 'GET') {
295
+ res.writeHead(200, {
296
+ 'Content-Type': 'text/event-stream',
297
+ 'Cache-Control': 'no-cache',
298
+ 'Connection': 'keep-alive',
299
+ 'X-Accel-Buffering': 'no',
300
+ });
301
+ res.write('retry: 3000\n\n'); // tell client to reconnect after 3s if disconnected
302
+ _sseClients.add(res);
303
+ req.on('close', () => _sseClients.delete(res));
304
+ return;
305
+ }
306
+
307
+ // ── /_nvml/run — triggered server-side Nova scripts ──────
308
+ if (urlPath === '/_nvml/run' && req.method === 'POST') {
309
+ let rawBody = '';
310
+ req.on('data', chunk => { rawBody += chunk; });
311
+ req.on('end', () => {
312
+ let code, live;
313
+ try {
314
+ const body = JSON.parse(rawBody);
315
+ code = String(body.code || '');
316
+ live = body.live || {};
317
+ } catch (e) {
318
+ return sendJSON(res, 400, { error: 'Invalid JSON body' });
319
+ }
320
+
321
+ if (!code.trim()) return sendJSON(res, 200, { mutations: [] });
322
+
323
+ if (!novaRun) {
324
+ return sendJSON(res, 503, { error: 'Nova runtime not available' });
325
+ }
326
+
327
+ const mutations = [];
328
+ const docProxy = makeMutationProxy(mutations, live);
329
+ try {
330
+ novaRun(code, { document: docProxy, request: live });
331
+ return sendJSON(res, 200, { mutations });
332
+ } catch (e) {
333
+ process.stderr.write(c.red(`[nvml] /_nvml/run error: ${e.message}`) + '\n');
334
+ return sendJSON(res, 500, { error: e.message, mutations });
335
+ }
336
+ });
337
+ return;
338
+ }
339
+
340
+ // ── main route ───────────────────────────────────────────
341
+ if (urlPath !== route) {
342
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
343
+ res.end(`404 — this server only handles: ${route}`);
344
+ return;
345
+ }
346
+
347
+ let source;
348
+ try { source = fs.readFileSync(absFile, 'utf8'); }
349
+ catch (e) { res.writeHead(500); res.end(e.message); return; }
350
+
351
+ // Build request context
352
+ const reqContext = {
353
+ method: req.method,
354
+ url: req.url,
355
+ path: urlPath,
356
+ query: Object.fromEntries(new URLSearchParams(parsed.query || '')),
357
+ headers: req.headers,
358
+ params: {},
359
+ body: null,
360
+ };
361
+
362
+ const compileAndSend = () => {
363
+ let html;
364
+ try {
365
+ html = nvml.compile(source, {
366
+ novaRunner: makeNovaRunner(reqContext),
367
+ novaEmitter: null,
368
+ });
369
+ } catch (e) {
370
+ process.stderr.write(c.red(`[nvml] compile error: ${e.message}`) + '\n');
371
+ const errHtml = errorPage(nvmlFile, e);
372
+ res.writeHead(500, {
373
+ 'Content-Type': 'text/html; charset=UTF-8',
374
+ 'Content-Length': Buffer.byteLength(errHtml),
375
+ });
376
+ res.end(errHtml);
377
+ return;
378
+ }
379
+
380
+ res.writeHead(200, {
381
+ 'Content-Type': 'text/html; charset=UTF-8',
382
+ 'Content-Length': Buffer.byteLength(html),
383
+ 'Cache-Control': 'no-store',
384
+ 'X-Powered-By': `nvml/${VERSION}`,
385
+ });
386
+ res.end(html);
387
+ process.stdout.write(c.dim(` ${req.method} ${urlPath} → 200`) + '\n');
388
+ };
389
+
390
+ // Collect body for POST/PUT/PATCH before compiling
391
+ if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
392
+ let rawBody = '';
393
+ req.on('data', chunk => { rawBody += chunk; });
394
+ req.on('end', () => {
395
+ try { reqContext.body = JSON.parse(rawBody); } catch { reqContext.body = rawBody; }
396
+ compileAndSend();
397
+ });
398
+ } else {
399
+ compileAndSend();
400
+ }
401
+ });
402
+
403
+ server.on('error', e => {
404
+ if (e.code === 'EADDRINUSE') die(`Port ${port} is already in use.`);
405
+ die(`Server error: ${e.message}`);
406
+ });
407
+
408
+ server.listen(port, '127.0.0.1', () => {
409
+ process.stdout.write(
410
+ c.green('\n nvml server running') + '\n' +
411
+ c.cyan(` http://localhost:${port}${route}`) + '\n\n' +
412
+ c.dim(` file : ${absFile}`) + '\n' +
413
+ c.dim(` route : ${route}`) + '\n' +
414
+ c.dim(` reload : automatic (re-reads file on every request)`) + '\n\n' +
415
+ ` Press ${c.bold('Ctrl+C')} to stop.\n\n`
416
+ );
417
+ });
418
+
419
+ process.on('SIGINT', () => { process.stdout.write('\n'); process.exit(0); });
420
+ process.on('SIGTERM', () => process.exit(0));
421
+ }
422
+
423
+ // ── helpers ───────────────────────────────────────────────────
424
+ function sendJSON(res, code, obj) {
425
+ const body = JSON.stringify(obj);
426
+ res.writeHead(code, {
427
+ 'Content-Type': 'application/json; charset=UTF-8',
428
+ 'Content-Length': Buffer.byteLength(body),
429
+ 'Access-Control-Allow-Origin': '*',
430
+ });
431
+ res.end(body);
432
+ }
433
+
434
+ // ── compile ───────────────────────────────────────────────────
435
+ function cmdCompile(nvmlFile) {
436
+ const nvml = loadKit();
437
+ const absFile = path.resolve(process.cwd(), nvmlFile);
438
+ if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
439
+ const novaRun = resolveNovaRun();
440
+ const novaRunner = novaRun
441
+ ? (code, doc) => novaRun(code, { document: makeDocumentProxy(doc), request: {} })
442
+ : null;
443
+ try {
444
+ process.stdout.write(nvml.compile(fs.readFileSync(absFile, 'utf8'), { novaRunner, novaEmitter: null }));
445
+ } catch (e) {
446
+ die(`Compile error: ${e.message}`);
447
+ }
448
+ }
449
+
450
+ // ── ast ───────────────────────────────────────────────────────
451
+ function cmdAst(nvmlFile) {
452
+ const nvml = loadKit();
453
+ const absFile = path.resolve(process.cwd(), nvmlFile);
454
+ if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
455
+ try {
456
+ process.stdout.write(JSON.stringify(nvml.parse(fs.readFileSync(absFile, 'utf8')), null, 2) + '\n');
457
+ } catch (e) {
458
+ die(`Parse error: ${e.message}`);
459
+ }
460
+ }
461
+
462
+ // ── tokens ────────────────────────────────────────────────────
463
+ function cmdTokens(nvmlFile) {
464
+ const { Lexer } = loadLexer();
465
+ const absFile = path.resolve(process.cwd(), nvmlFile);
466
+ if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
467
+ try {
468
+ process.stdout.write(JSON.stringify(new Lexer(fs.readFileSync(absFile, 'utf8')).tokenize(), null, 2) + '\n');
469
+ } catch (e) {
470
+ die(`Lex error: ${e.message}`);
471
+ }
472
+ }
473
+
474
+ // ── check ─────────────────────────────────────────────────────
475
+ function cmdCheck(nvmlFile) {
476
+ const nvml = loadKit();
477
+ const absFile = path.resolve(process.cwd(), nvmlFile);
478
+ if (!fs.existsSync(absFile)) die(`File not found: ${absFile}`);
479
+ try {
480
+ nvml.parse(fs.readFileSync(absFile, 'utf8'));
481
+ process.stdout.write(c.green(` Syntax OK`) + c.dim(` — ${nvmlFile}`) + '\n');
482
+ } catch (e) {
483
+ die(`Syntax error: ${e.message}`);
484
+ }
485
+ }
486
+
487
+ // ── help ──────────────────────────────────────────────────────
488
+ function printHelp() {
489
+ process.stdout.write(`
490
+ ${c.bold('nvml')} v${VERSION} — NVML CLI ${c.dim('(part of novac)')}
491
+
492
+ ${c.bold('Usage:')}
493
+ nvml ${c.cyan('<port>')} ${c.cyan('<file.nvml>')} Serve file on localhost:<port> (route: /)
494
+ nvml ${c.cyan('<port>')} ${c.cyan('<file.nvml>')} ${c.cyan('<route>')} Serve on a specific route
495
+ nvml compile ${c.cyan('<file.nvml>')} Compile to HTML and print to stdout
496
+ nvml ast ${c.cyan('<file.nvml>')} Print parsed AST as JSON
497
+ nvml tokens ${c.cyan('<file.nvml>')} Print token stream as JSON
498
+ nvml check ${c.cyan('<file.nvml>')} Validate NVML syntax
499
+ nvml --help Show this help
500
+ nvml --version Show version
501
+
502
+ ${c.bold('Examples:')}
503
+ nvml 3000 index.nvml
504
+ nvml 8080 about.nvml /about
505
+ nvml compile index.nvml > index.html
506
+ nvml check index.nvml
507
+ nvml ast index.nvml | head -30
508
+
509
+ ${c.bold('Notes:')}
510
+ The server re-reads the NVML file on every request — no restart needed.
511
+ CORS headers are set so nvml-browser can connect without issues.
512
+
513
+ `);
514
+ }
515
+
516
+ // ── entry point ───────────────────────────────────────────────
517
+ const args = process.argv.slice(2);
518
+
519
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
520
+ printHelp(); process.exit(0);
521
+ }
522
+
523
+ if (args[0] === '--version' || args[0] === '-v') {
524
+ process.stdout.write(`nvml v${VERSION}\n`); process.exit(0);
525
+ }
526
+
527
+ // subcommands
528
+ switch (args[0]) {
529
+ case 'compile': if (!args[1]) die('Usage: nvml compile <file.nvml>'); cmdCompile(args[1]); break;
530
+ case 'ast': if (!args[1]) die('Usage: nvml ast <file.nvml>'); cmdAst(args[1]); break;
531
+ case 'tokens': if (!args[1]) die('Usage: nvml tokens <file.nvml>'); cmdTokens(args[1]); break;
532
+ case 'check': if (!args[1]) die('Usage: nvml check <file.nvml>'); cmdCheck(args[1]); break;
533
+ default: {
534
+ // nvml <port> <file> [route]
535
+ const port = parseInt(args[0], 10);
536
+ const file = args[1];
537
+ const route = args[2] || '/';
538
+ if (!port || port < 1 || port > 65535) die(`Invalid port: "${args[0]}"\nUsage: nvml <port> <file.nvml>`);
539
+ if (!file) die('Usage: nvml <port> <file.nvml> [route]');
540
+ cmdServe(port, file, route);
541
+ }
542
+ }