|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +import { readFileSync } from 'node:fs'; |
| 4 | +import { createInterface } from 'node:readline'; |
| 5 | +import { stdin, stdout, stderr, exit, argv, version } from 'node:process'; |
| 6 | +import { fileURLToPath } from 'node:url'; |
| 7 | +import { dirname, resolve } from 'node:path'; |
| 8 | + |
| 9 | +const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 10 | + |
| 11 | +let pkg; |
| 12 | +try { |
| 13 | + pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8')); |
| 14 | +} catch { |
| 15 | + pkg = { version: 'unknown' }; |
| 16 | +} |
| 17 | + |
| 18 | +let Exprify; |
| 19 | +try { |
| 20 | + const mod = await import('../src/core/Exprify.js'); |
| 21 | + Exprify = mod.default || mod.Exprify; |
| 22 | +} catch { |
| 23 | + stderr.write('Error: Could not load Exprify module\n'); |
| 24 | + exit(1); |
| 25 | +} |
| 26 | + |
| 27 | +const expr = new Exprify(); |
| 28 | + |
| 29 | +const USAGE = `Usage: exprify [options] [expression...] |
| 30 | +
|
| 31 | +Options: |
| 32 | + --help Show this help message |
| 33 | + --version Show version number |
| 34 | + --parse <expr> Parse expression and show token/AST structure |
| 35 | + --tokens <expr> Tokenize expression and show tokens |
| 36 | +
|
| 37 | +If no expression is provided and stdin is a TTY, starts interactive REPL. |
| 38 | +If stdin is piped, reads expression from stdin. |
| 39 | +
|
| 40 | +Examples: |
| 41 | + exprify "2 + 2" |
| 42 | + exprify "sqrt(16)" "5 * 3" |
| 43 | + exprify --parse "x ^ 2 + 2 * x + 1" |
| 44 | + echo "2 + 2" | exprify |
| 45 | +`; |
| 46 | + |
| 47 | +const COLORS = { |
| 48 | + reset: '\x1b[0m', |
| 49 | + red: '\x1b[31m', |
| 50 | + green: '\x1b[32m', |
| 51 | + yellow: '\x1b[33m', |
| 52 | + cyan: '\x1b[36m', |
| 53 | + bold: '\x1b[1m', |
| 54 | + dim: '\x1b[2m', |
| 55 | +}; |
| 56 | + |
| 57 | +function formatResult(value) { |
| 58 | + if (value === null) return 'null'; |
| 59 | + if (value === undefined) return 'undefined'; |
| 60 | + if (typeof value === 'object' || Array.isArray(value)) { |
| 61 | + return JSON.stringify(value, null, 2); |
| 62 | + } |
| 63 | + return String(value); |
| 64 | +} |
| 65 | + |
| 66 | +function printError(msg) { |
| 67 | + stderr.write(COLORS.red + 'Error: ' + COLORS.reset + msg + '\n'); |
| 68 | +} |
| 69 | + |
| 70 | +function evaluateAndPrint(expression, mode) { |
| 71 | + try { |
| 72 | + if (mode === 'parse') { |
| 73 | + const result = expr.parse(expression); |
| 74 | + console.log(JSON.stringify(result, null, 2)); |
| 75 | + } else if (mode === 'tokens') { |
| 76 | + const result = expr.tokenize(expression); |
| 77 | + console.log(JSON.stringify(result, null, 2)); |
| 78 | + } else { |
| 79 | + const result = expr.evaluate(expression); |
| 80 | + console.log(formatResult(result)); |
| 81 | + } |
| 82 | + } catch (err) { |
| 83 | + printError(err.message); |
| 84 | + exit(1); |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +const args = argv.slice(2); |
| 89 | + |
| 90 | +if (args.length === 0) { |
| 91 | + if (stdin.isTTY) { |
| 92 | + startREPL(); |
| 93 | + } else { |
| 94 | + let input = ''; |
| 95 | + stdin.setEncoding('utf-8'); |
| 96 | + stdin.on('data', (chunk) => { input += chunk; }); |
| 97 | + stdin.on('end', () => { |
| 98 | + const exprStr = input.trim(); |
| 99 | + if (exprStr) evaluateAndPrint(exprStr); |
| 100 | + }); |
| 101 | + } |
| 102 | + exit(0); |
| 103 | +} |
| 104 | + |
| 105 | +const flag = args[0]; |
| 106 | + |
| 107 | +if (flag === '--help' || flag === '-h') { |
| 108 | + console.log(USAGE); |
| 109 | + exit(0); |
| 110 | +} |
| 111 | + |
| 112 | +if (flag === '--version' || flag === '-v') { |
| 113 | + console.log(pkg.version); |
| 114 | + exit(0); |
| 115 | +} |
| 116 | + |
| 117 | +if (flag === '--parse' || flag === '--tokens') { |
| 118 | + if (args.length < 2) { |
| 119 | + printError('Missing expression argument'); |
| 120 | + console.log(USAGE); |
| 121 | + exit(2); |
| 122 | + } |
| 123 | + const mode = flag.slice(2); |
| 124 | + for (let i = 1; i < args.length; i++) { |
| 125 | + evaluateAndPrint(args[i], mode); |
| 126 | + } |
| 127 | + exit(0); |
| 128 | +} |
| 129 | + |
| 130 | +for (const arg of args) { |
| 131 | + evaluateAndPrint(arg); |
| 132 | +} |
| 133 | + |
| 134 | +function startREPL() { |
| 135 | + console.log(`Exprify v${pkg.version} — interactive REPL`); |
| 136 | + console.log('Type an expression or .help for commands\n'); |
| 137 | + |
| 138 | + const rl = createInterface({ |
| 139 | + input: stdin, |
| 140 | + output: stdout, |
| 141 | + prompt: COLORS.cyan + '» ' + COLORS.reset, |
| 142 | + completer: (line) => { |
| 143 | + const completions = [ |
| 144 | + 'help', '.help', '.exit', |
| 145 | + 'pi', 'e', 'i', 'PHI', 'TAU', 'INFINITY', 'NaN', |
| 146 | + 'sin', 'cos', 'tan', 'sqrt', 'abs', 'log', 'exp', |
| 147 | + 'map', 'filter', 'sum', 'prod', 'mean', 'max', 'min', |
| 148 | + 'if', 'parse', 'leafCount', 'random', |
| 149 | + 'simplify', 'expand', 'factor', 'solve', 'derivative', |
| 150 | + 'integral', 'sigma', 'limit', 'substitute', |
| 151 | + 'det', 'transpose', 'inverse', 'trace', 'rank', |
| 152 | + ]; |
| 153 | + const hits = completions.filter((c) => c.startsWith(line)); |
| 154 | + return [hits.length ? hits : completions, line]; |
| 155 | + }, |
| 156 | + }); |
| 157 | + |
| 158 | + rl.on('line', (line) => { |
| 159 | + const input = line.trim(); |
| 160 | + |
| 161 | + if (!input) { |
| 162 | + rl.prompt(); |
| 163 | + return; |
| 164 | + } |
| 165 | + |
| 166 | + if (input === '.exit' || input === 'exit' || input === 'quit') { |
| 167 | + rl.close(); |
| 168 | + return; |
| 169 | + } |
| 170 | + |
| 171 | + if (input === '.help' || input === 'help') { |
| 172 | + console.log(`\n ${COLORS.bold}Commands:${COLORS.reset}`); |
| 173 | + console.log(' .exit Exit the REPL'); |
| 174 | + console.log(' .help Show this message'); |
| 175 | + console.log(' <expr> Evaluate an expression'); |
| 176 | + console.log(' Ctrl+C Cancel / exit'); |
| 177 | + console.log(''); |
| 178 | + rl.prompt(); |
| 179 | + return; |
| 180 | + } |
| 181 | + |
| 182 | + try { |
| 183 | + const result = expr.evaluate(input); |
| 184 | + console.log(COLORS.green + formatResult(result) + COLORS.reset); |
| 185 | + } catch (err) { |
| 186 | + console.log(COLORS.red + 'Error: ' + COLORS.reset + err.message); |
| 187 | + } |
| 188 | + |
| 189 | + rl.prompt(); |
| 190 | + }); |
| 191 | + |
| 192 | + rl.on('close', () => { |
| 193 | + console.log(''); |
| 194 | + exit(0); |
| 195 | + }); |
| 196 | + |
| 197 | + rl.prompt(); |
| 198 | +} |
0 commit comments