thyscript 1.0.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 (49) hide show
  1. package/bin/thy-node.js +332 -0
  2. package/bin/thy.js +828 -0
  3. package/bin/thys.js +25 -0
  4. package/examples/calculator.thy +274 -0
  5. package/examples/calculator.thy.js +628 -0
  6. package/examples/hello.thy +34 -0
  7. package/examples/login.thy +382 -0
  8. package/examples/login.thy.js +710 -0
  9. package/package.json +54 -0
  10. package/scripts/_class.js +18 -0
  11. package/scripts/_class2.js +11 -0
  12. package/scripts/_class3.js +21 -0
  13. package/scripts/_dstruct.js +20 -0
  14. package/scripts/_dstruct2.js +24 -0
  15. package/scripts/_scaffold.js +16 -0
  16. package/scripts/_scaffold2.js +16 -0
  17. package/scripts/_scaffold3.js +26 -0
  18. package/scripts/_tpl_compile.js +13 -0
  19. package/scripts/_tpl_test.js +9 -0
  20. package/scripts/_typeof.js +26 -0
  21. package/scripts/_typeof2.js +25 -0
  22. package/scripts/patch_genmethod.js +44 -0
  23. package/scripts/smoke_test.js +167 -0
  24. package/scripts/smoke_test2.js +151 -0
  25. package/scripts/test_control_panel.js +58 -0
  26. package/src/compiler/CodeGenerator.js +1449 -0
  27. package/src/compiler/JITCompiler.js +325 -0
  28. package/src/database/DatabaseEngine.js +931 -0
  29. package/src/errors/ErrorDetector.js +305 -0
  30. package/src/errors/ErrorMessages.js +497 -0
  31. package/src/index.js +169 -0
  32. package/src/lexer/Lexer.js +342 -0
  33. package/src/lexer/TokenTypes.js +1234 -0
  34. package/src/memory/MemoryManager.js +283 -0
  35. package/src/parser/AST.js +606 -0
  36. package/src/parser/Parser.js +1942 -0
  37. package/src/runtime/ThyRuntime.js +265 -0
  38. package/src/runtime/stdlib/ArrayLib.js +207 -0
  39. package/src/runtime/stdlib/FileLib.js +168 -0
  40. package/src/runtime/stdlib/MathLib.js +411 -0
  41. package/src/runtime/stdlib/StringLib.js +387 -0
  42. package/src/server/ControlPanel.js +1188 -0
  43. package/src/server/HTMLCSSEngine.js +866 -0
  44. package/src/server/SecurityEngine.js +714 -0
  45. package/src/server/ServerEngine.js +738 -0
  46. package/tests/test1_math.thy +82 -0
  47. package/tests/test2_oop.thy +133 -0
  48. package/tests/test3_server.thy +143 -0
  49. package/tests/test4_functions.thy +159 -0
package/bin/thy.js ADDED
@@ -0,0 +1,828 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * thy - ThyScript multi-command CLI
6
+ * Commands: compile, check, use, new, run, repl, test, build, serve
7
+ */
8
+
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+ const { execSync, spawn } = require('child_process');
12
+
13
+ // ─── Bootstrap optional deps ─────────────────────────────────────────────────
14
+ let chalk, ora, commander;
15
+ try { chalk = require('chalk'); }
16
+ catch {
17
+ chalk = new Proxy({}, {
18
+ get: (_, k) => k === 'bold' || k === 'bgGreen' || k === 'bgRed'
19
+ ? new Proxy({}, { get: (_, k2) => (s) => s })
20
+ : (s) => s,
21
+ });
22
+ }
23
+ try { ora = require('ora'); }
24
+ catch {
25
+ ora = (text) => ({
26
+ text, start() { process.stdout.write(` ⏳ ${this.text}...\n`); return this; },
27
+ succeed(m) { process.stdout.write(` ✅ ${m || this.text}\n`); return this; },
28
+ fail(m) { process.stdout.write(` ❌ ${m || this.text}\n`); return this; },
29
+ info(m) { process.stdout.write(` ℹ️ ${m || this.text}\n`); return this; },
30
+ stop() { return this; },
31
+ });
32
+ }
33
+ try { commander = require('commander'); }
34
+ catch {
35
+ console.error('commander not found. Run: npm install commander');
36
+ process.exit(1);
37
+ }
38
+
39
+ const { Command } = commander;
40
+
41
+ // ─── Constants ────────────────────────────────────────────────────────────────
42
+ const THYSCRIPT_ROOT = path.resolve(__dirname, '..');
43
+ const PKG_PATH = path.join(THYSCRIPT_ROOT, 'package.json');
44
+ let VERSION = '1.0.0';
45
+ try { VERSION = require(PKG_PATH).version || VERSION; } catch {}
46
+
47
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
48
+ const log = (...a) => console.log(...a);
49
+ const info = (msg) => log(chalk.cyan(` ℹ️ ${msg}`));
50
+ const ok = (msg) => log(chalk.green(` ✔ ${msg}`));
51
+ const warn = (msg) => log(chalk.yellow(` ⚠️ ${msg}`));
52
+ const fail = (msg) => log(chalk.red(` ✖ ${msg}`));
53
+ const sep = () => log(chalk.cyan(' ' + '─'.repeat(68)));
54
+
55
+ function header(title) {
56
+ sep();
57
+ log(chalk.bold.cyan(` ${title}`));
58
+ sep();
59
+ }
60
+
61
+ function ensureThyFile(f) {
62
+ const abs = path.resolve(process.cwd(), f);
63
+ if (!fs.existsSync(abs)) { fail(`File not found: ${abs}`); process.exit(1); }
64
+ return abs;
65
+ }
66
+
67
+ function tryRequire(rel) {
68
+ try { return require(path.join(THYSCRIPT_ROOT, rel)); } catch { return null; }
69
+ }
70
+
71
+ function npmInstall(pkg, cwd) {
72
+ const spinner = ora(`Installing ${pkg}...`).start();
73
+ try {
74
+ execSync(`npm install ${pkg}`, { cwd, stdio: 'pipe' });
75
+ spinner.succeed(chalk.green(`Installed ${pkg}`));
76
+ return true;
77
+ } catch (err) {
78
+ spinner.fail(chalk.red(`Failed to install ${pkg}: ${err.message}`));
79
+ return false;
80
+ }
81
+ }
82
+
83
+ // ─── Mini banner (no full ASCII for subcommand output) ────────────────────────
84
+ function miniBanner() {
85
+ log('');
86
+ log(chalk.bold.cyan(' ⚡ ThyScript v' + VERSION));
87
+ log(chalk.gray(' High-Performance Language Built on Node.js'));
88
+ log('');
89
+ }
90
+
91
+ // ─── Scaffold templates ───────────────────────────────────────────────────────
92
+ function getMainTemplate(name) {
93
+ return `// ${name} - ThyScript Application
94
+ // Generated by ThyScript CLI v${VERSION}
95
+
96
+ print("Hello from ${name}!")
97
+ let x: int = 42
98
+ print("The answer is: " + x)
99
+
100
+ // Define a simple reusable function
101
+ func greet(person):
102
+ return "Hello, " + person + "!"
103
+ end()
104
+ print(greet("ThyScript"))
105
+ `;
106
+ }
107
+
108
+ function getPackageJsonTemplate(name) {
109
+ return JSON.stringify({
110
+ name: name.toLowerCase().replace(/\s+/g, '-'),
111
+ version: '0.1.0',
112
+ description: `ThyScript project: ${name}`,
113
+ main: 'src/main.thy',
114
+ scripts: {
115
+ start: `thy run src/main.thy`,
116
+ build: `thy build src/main.thy`,
117
+ test: `thy test`,
118
+ },
119
+ keywords: ['thyscript'],
120
+ license: 'MIT',
121
+ dependencies: {},
122
+ devDependencies: {},
123
+ thyscript: {
124
+ version: VERSION,
125
+ entry: 'src/main.thy',
126
+ outDir: 'dist',
127
+ },
128
+ }, null, 2);
129
+ }
130
+
131
+ function getReadmeTemplate(name) {
132
+ return `# ${name}
133
+
134
+ A ThyScript project.
135
+
136
+ ## Getting Started
137
+
138
+ \`\`\`bash
139
+ thy run src/main.thy
140
+ \`\`\`
141
+
142
+ ## Build
143
+
144
+ \`\`\`bash
145
+ thy build src/main.thy
146
+ \`\`\`
147
+
148
+ ## Test
149
+
150
+ \`\`\`bash
151
+ thy test
152
+ \`\`\`
153
+ `;
154
+ }
155
+
156
+ function getGitignoreTemplate() {
157
+ return `node_modules/
158
+ dist/
159
+ *.thy.js
160
+ *.thy.map
161
+ .env
162
+ .thy-cache/
163
+ `;
164
+ }
165
+
166
+ // ─── Program definition ───────────────────────────────────────────────────────
167
+ const program = new Command();
168
+
169
+ program
170
+ .name('thy')
171
+ .description('ThyScript - High-performance language CLI')
172
+ .version(VERSION, '-v, --version', 'Show ThyScript version');
173
+
174
+ // ╔══════════════════════════════════════╗
175
+ // ║ thy compile <filename> ║
176
+ // ╚══════════════════════════════════════╝
177
+ program
178
+ .command('compile <filename>')
179
+ .description('Compile a .thy file to Node.js JavaScript')
180
+ .option('-o, --out <outfile>', 'Output filename (default: <filename>.js)')
181
+ .option('--minify', 'Minify output')
182
+ .option('--sourcemap', 'Generate source map')
183
+ .action(async (filename, opts) => {
184
+ miniBanner();
185
+ header('COMPILE');
186
+
187
+ const abs = ensureThyFile(filename);
188
+ const outFile = opts.out || abs + '.js';
189
+ const source = fs.readFileSync(abs, 'utf8');
190
+ const display = path.basename(abs);
191
+
192
+ const spinner = ora(chalk.cyan(`Compiling ${display}...`)).start();
193
+
194
+ const ThyMod = tryRequire('src/index.js');
195
+ if (!ThyMod || typeof ThyMod.compile !== 'function') {
196
+ spinner.fail(chalk.red('ThyScript compiler not found. Run: thy check'));
197
+ process.exit(1);
198
+ }
199
+
200
+ try {
201
+ const t0 = Date.now();
202
+ let js = ThyMod.compile(source, abs);
203
+
204
+ if (opts.minify) {
205
+ // Basic whitespace minification (real minifier hooked via UglifyJS if available)
206
+ try {
207
+ const uglify = require('uglify-js');
208
+ const res = uglify.minify(js);
209
+ if (!res.error) js = res.code;
210
+ } catch {
211
+ warn('uglify-js not available — skipping minification. Install with: npm install uglify-js');
212
+ }
213
+ }
214
+
215
+ fs.writeFileSync(outFile, js, 'utf8');
216
+
217
+ if (opts.sourcemap) {
218
+ // Stub source map
219
+ const map = JSON.stringify({
220
+ version: 3, file: path.basename(outFile),
221
+ sources: [display], mappings: '',
222
+ });
223
+ fs.writeFileSync(outFile + '.map', map, 'utf8');
224
+ ok(`Source map: ${outFile}.map`);
225
+ }
226
+
227
+ const ms = Date.now() - t0;
228
+ spinner.succeed(chalk.green(`Compiled in ${ms}ms → ${outFile}`));
229
+ } catch (err) {
230
+ spinner.fail(chalk.red(`Compile error: ${err.message}`));
231
+ if (err.line) fail(` at line ${err.line}, col ${err.column}`);
232
+ if (err.suggestion) warn(` Fix: ${err.suggestion}`);
233
+ process.exit(1);
234
+ }
235
+ });
236
+
237
+ // ╔══════════════════════════════════════╗
238
+ // ║ thy check ║
239
+ // ╚══════════════════════════════════════╝
240
+ program
241
+ .command('check')
242
+ .description('Check npm dependencies and install missing ones')
243
+ .option('--fix', 'Automatically install missing packages')
244
+ .action((opts) => {
245
+ miniBanner();
246
+ header('DEPENDENCY CHECK');
247
+
248
+ const required = [
249
+ 'chalk', 'ora', 'commander', 'chokidar', 'express',
250
+ 'ws', 'better-sqlite3', 'jsonwebtoken', 'bcryptjs',
251
+ ];
252
+ const optional = [
253
+ 'uglify-js', 'terser', 'readline-sync',
254
+ ];
255
+
256
+ const cwd = process.cwd();
257
+ const hasPkg = fs.existsSync(path.join(cwd, 'package.json'));
258
+ if (!hasPkg) {
259
+ warn('No package.json found in current directory.');
260
+ if (opts.fix) {
261
+ log(chalk.cyan(' Running npm init -y...'));
262
+ execSync('npm init -y', { cwd, stdio: 'inherit' });
263
+ }
264
+ }
265
+
266
+ let missing = [];
267
+ log(chalk.bold.white(' Required packages:'));
268
+ for (const pkg of required) {
269
+ try {
270
+ require.resolve(pkg);
271
+ log(chalk.green(` ✔ ${pkg}`));
272
+ } catch {
273
+ log(chalk.red(` ✖ ${pkg} (missing)`));
274
+ missing.push(pkg);
275
+ }
276
+ }
277
+
278
+ log('');
279
+ log(chalk.bold.white(' Optional packages:'));
280
+ for (const pkg of optional) {
281
+ try {
282
+ require.resolve(pkg);
283
+ log(chalk.green(` ✔ ${pkg}`));
284
+ } catch {
285
+ log(chalk.gray(` ○ ${pkg} (not installed)`));
286
+ }
287
+ }
288
+
289
+ log('');
290
+ if (missing.length === 0) {
291
+ ok('All required dependencies are present!');
292
+ } else {
293
+ warn(`${missing.length} package(s) missing: ${missing.join(', ')}`);
294
+ if (opts.fix) {
295
+ log('');
296
+ log(chalk.cyan(' Installing missing packages...'));
297
+ for (const pkg of missing) npmInstall(pkg, cwd);
298
+ } else {
299
+ log(chalk.yellow(' Run: thy check --fix to auto-install'));
300
+ }
301
+ }
302
+ });
303
+
304
+ // ╔══════════════════════════════════════╗
305
+ // ║ thy use <package> ║
306
+ // ╚══════════════════════════════════════╝
307
+ program
308
+ .command('use <package>')
309
+ .description('Install an npm package and register it in the project')
310
+ .option('-D, --dev', 'Save as devDependency')
311
+ .action((pkg, opts) => {
312
+ miniBanner();
313
+ header(`ADDING PACKAGE: ${pkg}`);
314
+
315
+ const cwd = process.cwd();
316
+ const flag = opts.dev ? '--save-dev' : '--save';
317
+ const spinner = ora(`Installing ${pkg}...`).start();
318
+
319
+ try {
320
+ execSync(`npm install ${pkg} ${flag}`, { cwd, stdio: 'pipe' });
321
+ spinner.succeed(chalk.green(`Package '${pkg}' installed and saved.`));
322
+
323
+ // Update thyscript block in package.json if present
324
+ const pkgPath = path.join(cwd, 'package.json');
325
+ if (fs.existsSync(pkgPath)) {
326
+ const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
327
+ pkgJson.thyscript = pkgJson.thyscript || {};
328
+ pkgJson.thyscript.uses = pkgJson.thyscript.uses || [];
329
+ if (!pkgJson.thyscript.uses.includes(pkg)) {
330
+ pkgJson.thyscript.uses.push(pkg);
331
+ fs.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2), 'utf8');
332
+ ok(`Registered '${pkg}' in package.json [thyscript.uses]`);
333
+ }
334
+ }
335
+ } catch (err) {
336
+ spinner.fail(chalk.red(`Failed to install '${pkg}': ${err.message}`));
337
+ process.exit(1);
338
+ }
339
+ });
340
+
341
+ // ╔══════════════════════════════════════╗
342
+ // ║ thy new <project-name> ║
343
+ // ╚══════════════════════════════════════╝
344
+ program
345
+ .command('new <project-name>')
346
+ .description('Scaffold a new ThyScript project')
347
+ .option('--no-git', 'Skip git initialization')
348
+ .option('--no-install', 'Skip npm install')
349
+ .action((name, opts) => {
350
+ miniBanner();
351
+ header(`NEW PROJECT: ${name}`);
352
+
353
+ const projectDir = path.resolve(process.cwd(), name);
354
+ if (fs.existsSync(projectDir)) {
355
+ fail(`Directory already exists: ${projectDir}`);
356
+ process.exit(1);
357
+ }
358
+
359
+ const spinner = ora('Scaffolding project...').start();
360
+
361
+ try {
362
+ // Create directory structure
363
+ const dirs = [
364
+ projectDir,
365
+ path.join(projectDir, 'src'),
366
+ path.join(projectDir, 'tests'),
367
+ path.join(projectDir, 'dist'),
368
+ path.join(projectDir, 'docs'),
369
+ ];
370
+ dirs.forEach(d => fs.mkdirSync(d, { recursive: true }));
371
+
372
+ // Write files
373
+ fs.writeFileSync(path.join(projectDir, 'src', 'main.thy'),
374
+ getMainTemplate(name), 'utf8');
375
+ fs.writeFileSync(path.join(projectDir, 'package.json'),
376
+ getPackageJsonTemplate(name), 'utf8');
377
+ fs.writeFileSync(path.join(projectDir, 'README.md'),
378
+ getReadmeTemplate(name), 'utf8');
379
+ fs.writeFileSync(path.join(projectDir, '.gitignore'),
380
+ getGitignoreTemplate(), 'utf8');
381
+ fs.writeFileSync(path.join(projectDir, 'tests', 'main.test.thy'),
382
+ `// Tests for ${name}\nprint("Test suite for ${name} passed!")\n`, 'utf8');
383
+
384
+ spinner.succeed(chalk.green(`Project scaffolded: ${projectDir}`));
385
+
386
+ // Git init
387
+ if (opts.git !== false) {
388
+ try {
389
+ execSync('git init', { cwd: projectDir, stdio: 'pipe' });
390
+ ok('Initialized git repository');
391
+ } catch { warn('git not available — skipping'); }
392
+ }
393
+
394
+ // npm install
395
+ if (opts.install !== false) {
396
+ const installSpinner = ora('Installing ThyScript dependencies...').start();
397
+ try {
398
+ execSync(`npm install thyscript chalk ora commander`, {
399
+ cwd: projectDir, stdio: 'pipe',
400
+ });
401
+ installSpinner.succeed('Dependencies installed');
402
+ } catch {
403
+ installSpinner.fail('npm install failed — run manually inside the project');
404
+ }
405
+ }
406
+
407
+ log('');
408
+ ok(`Done! Your ThyScript project is ready.`);
409
+ log('');
410
+ log(chalk.cyan(` Next steps:`));
411
+ log(chalk.white(` cd ${name}`));
412
+ log(chalk.white(` thy run src/main.thy`));
413
+ log('');
414
+ } catch (err) {
415
+ spinner.fail(chalk.red(`Failed: ${err.message}`));
416
+ process.exit(1);
417
+ }
418
+ });
419
+
420
+ // ╔══════════════════════════════════════╗
421
+ // ║ thy run <filename> ║
422
+ // ╚══════════════════════════════════════╝
423
+ program
424
+ .command('run <filename>')
425
+ .description('Run a .thy file (alias for thy-node)')
426
+ .option('--no-stats', 'Skip performance stats')
427
+ .action((filename, opts) => {
428
+ const thyNode = path.join(__dirname, 'thy-node.js');
429
+ const args = [thyNode, filename];
430
+ if (opts.stats === false) args.push('--no-stats');
431
+
432
+ const child = spawn(process.execPath, args, {
433
+ stdio: 'inherit',
434
+ env: process.env,
435
+ });
436
+ child.on('exit', code => process.exit(code || 0));
437
+ });
438
+
439
+ // ╔══════════════════════════════════════╗
440
+ // ║ thy repl ║
441
+ // ╚══════════════════════════════════════╝
442
+ program
443
+ .command('repl')
444
+ .description('Start the interactive ThyScript REPL')
445
+ .action(async () => {
446
+ miniBanner();
447
+ header('THYSCRIPT REPL (type .exit to quit, .help for commands)');
448
+
449
+ const readline = require('readline');
450
+ const rl = readline.createInterface({
451
+ input: process.stdin,
452
+ output: process.stdout,
453
+ prompt: chalk.cyan('thy> '),
454
+ });
455
+
456
+ const ThyMod = tryRequire('src/index.js');
457
+ const history = [];
458
+ let scope = {};
459
+ let multi = ''; // multi-line buffer
460
+ let depth = 0; // brace depth
461
+
462
+ log(chalk.gray(' ThyScript Interactive REPL'));
463
+ log(chalk.gray(' .help .clear .history .exit'));
464
+ log('');
465
+ rl.prompt();
466
+
467
+ rl.on('line', async (line) => {
468
+ const trimmed = line.trim();
469
+
470
+ // REPL commands
471
+ if (trimmed === '.exit') { log(chalk.cyan(' Goodbye!')); rl.close(); process.exit(0); }
472
+ if (trimmed === '.clear') { scope = {}; multi = ''; depth = 0; ok('Scope cleared.'); rl.prompt(); return; }
473
+ if (trimmed === '.help') {
474
+ log(chalk.yellow(' .exit — quit REPL'));
475
+ log(chalk.yellow(' .clear — clear scope'));
476
+ log(chalk.yellow(' .history — show history'));
477
+ log(chalk.yellow(' .scope — show current variables'));
478
+ rl.prompt(); return;
479
+ }
480
+ if (trimmed === '.history') {
481
+ history.forEach((h, i) => log(chalk.gray(` ${i + 1}: ${h}`)));
482
+ rl.prompt(); return;
483
+ }
484
+ if (trimmed === '.scope') {
485
+ log(JSON.stringify(scope, null, 2));
486
+ rl.prompt(); return;
487
+ }
488
+
489
+ if (!trimmed) { rl.prompt(); return; }
490
+
491
+ // Multi-line detection: count braces
492
+ for (const ch of line) {
493
+ if (ch === '{') depth++;
494
+ else if (ch === '}') depth--;
495
+ }
496
+ multi += line + '\n';
497
+
498
+ if (depth > 0) {
499
+ rl.setPrompt(chalk.cyan('... '));
500
+ rl.prompt();
501
+ return;
502
+ }
503
+
504
+ // Execute
505
+ const code = multi.trim();
506
+ multi = ''; depth = 0;
507
+ rl.setPrompt(chalk.cyan('thy> '));
508
+
509
+ if (code) {
510
+ history.push(code);
511
+ try {
512
+ if (ThyMod && typeof ThyMod.compile === 'function') {
513
+ const js = ThyMod.compile(code, '<repl>');
514
+ const fn = new Function('__scope__', `with(__scope__){ ${js} }`);
515
+ const res = await fn(scope);
516
+ if (res !== undefined) log(chalk.green(' => ') + chalk.white(JSON.stringify(res)));
517
+ } else {
518
+ warn('ThyScript compiler not available — showing raw input.');
519
+ log(chalk.gray(` > ${code}`));
520
+ }
521
+ } catch (err) {
522
+ log(chalk.red(` ✖ ${err.message}`));
523
+ if (err.line) log(chalk.gray(` at line ${err.line}`));
524
+ }
525
+ }
526
+ rl.prompt();
527
+ });
528
+
529
+ rl.on('close', () => { log(''); process.exit(0); });
530
+ });
531
+
532
+ // ╔══════════════════════════════════════╗
533
+ // ║ thy test ║
534
+ // ╚══════════════════════════════════════╝
535
+ program
536
+ .command('test [pattern]')
537
+ .description('Run ThyScript test files (*.test.thy)')
538
+ .option('--watch', 'Watch for changes and re-run')
539
+ .action(async (pattern, opts) => {
540
+ miniBanner();
541
+ header('TEST RUNNER');
542
+
543
+ const cwd = process.cwd();
544
+ const glob = pattern || '**/*.test.thy';
545
+ const ThyMod = tryRequire('src/index.js');
546
+
547
+ // Find test files
548
+ let testFiles = [];
549
+ function findTests(dir) {
550
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
551
+ for (const e of entries) {
552
+ if (e.name === 'node_modules') continue;
553
+ const full = path.join(dir, e.name);
554
+ if (e.isDirectory()) findTests(full);
555
+ else if (e.name.endsWith('.test.thy')) testFiles.push(full);
556
+ }
557
+ }
558
+ try { findTests(cwd); } catch {}
559
+
560
+ if (!testFiles.length) {
561
+ warn('No test files found (*.test.thy)');
562
+ process.exit(0);
563
+ }
564
+
565
+ info(`Found ${testFiles.length} test file(s)`);
566
+ log('');
567
+
568
+ async function runTests() {
569
+ let passed = 0, failed = 0, skipped = 0;
570
+ const startAll = Date.now();
571
+
572
+ for (const file of testFiles) {
573
+ const display = path.relative(cwd, file);
574
+ const source = fs.readFileSync(file, 'utf8');
575
+ const t0 = Date.now();
576
+
577
+ try {
578
+ if (ThyMod && typeof ThyMod.run === 'function') {
579
+ await ThyMod.run(file);
580
+ const ms = Date.now() - t0;
581
+ log(chalk.green(` ✔ ${display}`) + chalk.gray(` (${ms}ms)`));
582
+ passed++;
583
+ } else {
584
+ log(chalk.yellow(` ○ ${display} (skipped — runtime unavailable)`));
585
+ skipped++;
586
+ }
587
+ } catch (err) {
588
+ const ms = Date.now() - t0;
589
+ log(chalk.red(` ✖ ${display}`) + chalk.gray(` (${ms}ms)`));
590
+ log(chalk.red(` ${err.message}`));
591
+ failed++;
592
+ }
593
+ }
594
+
595
+ const totalMs = Date.now() - startAll;
596
+ log('');
597
+ sep();
598
+ log(chalk.bold.white(` Results: `) +
599
+ chalk.green(`${passed} passed`) + chalk.gray(' ') +
600
+ chalk.red(`${failed} failed`) + chalk.gray(' ') +
601
+ chalk.yellow(`${skipped} skipped`) +
602
+ chalk.gray(` • ${totalMs}ms`));
603
+ sep();
604
+ log('');
605
+ if (failed) process.exit(1);
606
+ }
607
+
608
+ await runTests();
609
+
610
+ if (opts.watch) {
611
+ let chokidar;
612
+ try { chokidar = require('chokidar'); } catch {
613
+ warn('chokidar not installed — watch mode unavailable. Run: thy use chokidar');
614
+ process.exit(1);
615
+ }
616
+ log(chalk.cyan(' 👁 Watching for changes...'));
617
+ chokidar.watch('**/*.test.thy', { cwd, ignoreInitial: true })
618
+ .on('change', async (f) => {
619
+ log('');
620
+ info(`Changed: ${f}. Re-running tests...`);
621
+ testFiles = [];
622
+ findTests(cwd);
623
+ await runTests();
624
+ });
625
+ }
626
+ });
627
+
628
+ // ╔══════════════════════════════════════╗
629
+ // ║ thy build <filename> ║
630
+ // ╚══════════════════════════════════════╝
631
+ program
632
+ .command('build <filename>')
633
+ .description('Production build: compile, minify, and optimise')
634
+ .option('-o, --out <dir>', 'Output directory', 'dist')
635
+ .option('--no-minify', 'Skip minification')
636
+ .action(async (filename, opts) => {
637
+ miniBanner();
638
+ header('PRODUCTION BUILD');
639
+
640
+ const abs = ensureThyFile(filename);
641
+ const outDir = path.resolve(process.cwd(), opts.out);
642
+ const outFile = path.join(outDir, path.basename(abs) + '.js');
643
+ const source = fs.readFileSync(abs, 'utf8');
644
+ const ThyMod = tryRequire('src/index.js');
645
+
646
+ fs.mkdirSync(outDir, { recursive: true });
647
+
648
+ const s1 = ora('Compiling...').start();
649
+ let js;
650
+ try {
651
+ js = ThyMod && typeof ThyMod.compile === 'function'
652
+ ? ThyMod.compile(source, abs)
653
+ : `// Stub build: ${path.basename(abs)}\n${source}`;
654
+ s1.succeed('Compiled');
655
+ } catch (err) {
656
+ s1.fail(`Compile failed: ${err.message}`);
657
+ process.exit(1);
658
+ }
659
+
660
+ if (opts.minify !== false) {
661
+ const s2 = ora('Minifying...').start();
662
+ try {
663
+ const terser = require('terser');
664
+ const res = await terser.minify(js, { compress: true, mangle: true });
665
+ js = res.code || js;
666
+ s2.succeed(`Minified (${(js.length / 1024).toFixed(1)} KB)`);
667
+ } catch {
668
+ try {
669
+ const ug = require('uglify-js');
670
+ const res = ug.minify(js);
671
+ if (!res.error) js = res.code;
672
+ s2.succeed(`Minified (${(js.length / 1024).toFixed(1)} KB)`);
673
+ } catch {
674
+ s2.info('Minification skipped — terser/uglify-js not available');
675
+ }
676
+ }
677
+ }
678
+
679
+ const s3 = ora('Writing output...').start();
680
+ fs.writeFileSync(outFile, js, 'utf8');
681
+ s3.succeed(chalk.green(`Output: ${outFile}`));
682
+
683
+ // Write build manifest
684
+ const manifest = {
685
+ entry: abs, outFile, size: fs.statSync(outFile).size,
686
+ builtAt: new Date().toISOString(), version: VERSION,
687
+ };
688
+ fs.writeFileSync(path.join(outDir, 'thy-build.json'),
689
+ JSON.stringify(manifest, null, 2), 'utf8');
690
+
691
+ ok('Production build complete!');
692
+ log('');
693
+ });
694
+
695
+ // ╔══════════════════════════════════════╗
696
+ // ║ thy serve <filename> ║
697
+ // ╚══════════════════════════════════════╝
698
+ program
699
+ .command('serve <filename>')
700
+ .description('Run a .thy file as an HTTP web server')
701
+ .option('-p, --port <port>', 'Port to listen on', '3000')
702
+ .option('--host <host>', 'Host to bind to', 'localhost')
703
+ .option('--watch', 'Restart on file change')
704
+ .action(async (filename, opts) => {
705
+ miniBanner();
706
+ header('THY SERVER');
707
+
708
+ const abs = ensureThyFile(filename);
709
+ const port = parseInt(opts.port, 10) || 3000;
710
+ const host = opts.host || 'localhost';
711
+ const ThyMod = tryRequire('src/index.js');
712
+
713
+ info(`Starting server: ${path.basename(abs)}`);
714
+ info(`Listening on http://${host}:${port}`);
715
+ log('');
716
+
717
+ async function startServer() {
718
+ try {
719
+ if (ThyMod && ThyMod.ServerEngine) {
720
+ const engine = new ThyMod.ServerEngine({ filename: abs, port, host });
721
+ await engine.start();
722
+ } else {
723
+ // Fallback: plain express stub
724
+ let express;
725
+ try { express = require('express'); } catch {
726
+ fail('express not installed. Run: thy use express');
727
+ process.exit(1);
728
+ }
729
+ const source = fs.readFileSync(abs, 'utf8');
730
+ const app = express();
731
+ app.get('*', (_req, res) =>
732
+ res.send(`<pre>ThyScript stub server\n\nSource:\n${source}</pre>`));
733
+ app.listen(port, host, () =>
734
+ ok(`Server running at http://${host}:${port} (stub mode)`));
735
+ }
736
+ } catch (err) {
737
+ if (err.code === 'EADDRINUSE') {
738
+ fail(`Port ${port} is already in use.`);
739
+ warn(`Try: thy serve ${filename} --port ${port + 1}`);
740
+ } else {
741
+ fail(`Server error: ${err.message}`);
742
+ }
743
+ process.exit(1);
744
+ }
745
+ }
746
+
747
+ await startServer();
748
+
749
+ if (opts.watch) {
750
+ let chokidar;
751
+ try { chokidar = require('chokidar'); } catch {
752
+ warn('chokidar not installed — watch disabled');
753
+ return;
754
+ }
755
+ log(chalk.cyan(' 👁 Watching for file changes...'));
756
+ chokidar.watch(abs).on('change', async () => {
757
+ warn('File changed. Restarting server...');
758
+ await startServer();
759
+ });
760
+ }
761
+ });
762
+
763
+ // ╔══════════════════════════════════════╗
764
+ // ║ thy panel ║
765
+ // ╚══════════════════════════════════════╝
766
+ program
767
+ .command('panel')
768
+ .alias('control-panel')
769
+ .description('Start the ThyScript Web Control Panel (Live Host Server without compile)')
770
+ .option('-p, --port <port>', 'Port to listen on', '7777')
771
+ .option('--host <host>', 'Host to bind to', '0.0.0.0')
772
+ .option('--token <token>', 'Secret authorization token')
773
+ .action(async (opts) => {
774
+ miniBanner();
775
+ header('THYSCRIPT CONTROL PANEL');
776
+
777
+ const port = parseInt(opts.port, 10) || 7777;
778
+ const host = opts.host || '0.0.0.0';
779
+ const ControlPanel = tryRequire('src/server/ControlPanel.js');
780
+ if (!ControlPanel) {
781
+ fail('ControlPanel module not found.');
782
+ process.exit(1);
783
+ }
784
+
785
+ const panel = new ControlPanel({
786
+ port,
787
+ host,
788
+ authToken: opts.token || null,
789
+ workspaceDir: process.cwd(),
790
+ });
791
+
792
+ try {
793
+ await panel.start();
794
+ ok(`Control Panel running on http://localhost:${port}`);
795
+ log('');
796
+ log(chalk.bold.white(' 🌐 Access from any device on your local network (WiFi/LAN):'));
797
+ for (const net of panel.getNetworkAddresses()) {
798
+ log(chalk.cyan(` ▶ http://${net.address}:${port}`) + chalk.gray(` (${net.interface})`));
799
+ }
800
+ log('');
801
+ log(chalk.gray(' ⚡ Live Execution Sandbox Active (Zero disk compile needed)'));
802
+ log(chalk.gray(' Press Ctrl+C to stop the server.'));
803
+ log('');
804
+ } catch (err) {
805
+ fail(`Failed to start Control Panel: ${err.message}`);
806
+ process.exit(1);
807
+ }
808
+ });
809
+
810
+ // ─── Default: show help if no command given ──────────────────────────────────
811
+ program.addHelpText('after', `
812
+ ${chalk.cyan('Examples:')}
813
+ ${chalk.white('thy new my-app')} Scaffold new project
814
+ ${chalk.white('thy run src/main.thy')} Run a ThyScript file
815
+ ${chalk.white('thy compile src/main.thy')} Compile to JavaScript
816
+ ${chalk.white('thy check --fix')} Install missing dependencies
817
+ ${chalk.white('thy repl')} Start interactive REPL
818
+ ${chalk.white('thy serve src/server.thy')} Run as web server
819
+ ${chalk.white('thy test')} Run test suite
820
+ ${chalk.white('thy build src/main.thy')} Production build
821
+ `);
822
+
823
+ program.parse(process.argv);
824
+
825
+ if (!process.argv.slice(2).length) {
826
+ miniBanner();
827
+ program.outputHelp();
828
+ }