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.
- package/bin/thy-node.js +332 -0
- package/bin/thy.js +828 -0
- package/bin/thys.js +25 -0
- package/examples/calculator.thy +274 -0
- package/examples/calculator.thy.js +628 -0
- package/examples/hello.thy +34 -0
- package/examples/login.thy +382 -0
- package/examples/login.thy.js +710 -0
- package/package.json +54 -0
- package/scripts/_class.js +18 -0
- package/scripts/_class2.js +11 -0
- package/scripts/_class3.js +21 -0
- package/scripts/_dstruct.js +20 -0
- package/scripts/_dstruct2.js +24 -0
- package/scripts/_scaffold.js +16 -0
- package/scripts/_scaffold2.js +16 -0
- package/scripts/_scaffold3.js +26 -0
- package/scripts/_tpl_compile.js +13 -0
- package/scripts/_tpl_test.js +9 -0
- package/scripts/_typeof.js +26 -0
- package/scripts/_typeof2.js +25 -0
- package/scripts/patch_genmethod.js +44 -0
- package/scripts/smoke_test.js +167 -0
- package/scripts/smoke_test2.js +151 -0
- package/scripts/test_control_panel.js +58 -0
- package/src/compiler/CodeGenerator.js +1449 -0
- package/src/compiler/JITCompiler.js +325 -0
- package/src/database/DatabaseEngine.js +931 -0
- package/src/errors/ErrorDetector.js +305 -0
- package/src/errors/ErrorMessages.js +497 -0
- package/src/index.js +169 -0
- package/src/lexer/Lexer.js +342 -0
- package/src/lexer/TokenTypes.js +1234 -0
- package/src/memory/MemoryManager.js +283 -0
- package/src/parser/AST.js +606 -0
- package/src/parser/Parser.js +1942 -0
- package/src/runtime/ThyRuntime.js +265 -0
- package/src/runtime/stdlib/ArrayLib.js +207 -0
- package/src/runtime/stdlib/FileLib.js +168 -0
- package/src/runtime/stdlib/MathLib.js +411 -0
- package/src/runtime/stdlib/StringLib.js +387 -0
- package/src/server/ControlPanel.js +1188 -0
- package/src/server/HTMLCSSEngine.js +866 -0
- package/src/server/SecurityEngine.js +714 -0
- package/src/server/ServerEngine.js +738 -0
- package/tests/test1_math.thy +82 -0
- package/tests/test2_oop.thy +133 -0
- package/tests/test3_server.thy +143 -0
- 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
|
+
}
|