tova 0.5.0 → 0.5.1
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/tova.js +40 -7
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +100 -72
- package/src/codegen/cli-codegen.js +386 -0
- package/src/codegen/codegen.js +69 -22
- package/src/parser/ast.js +9 -0
- package/src/parser/cli-ast.js +35 -0
- package/src/parser/cli-parser.js +140 -0
- package/src/parser/parser.js +25 -42
- package/src/registry/block-registry.js +56 -0
- package/src/registry/plugins/bench-plugin.js +23 -0
- package/src/registry/plugins/cli-plugin.js +24 -0
- package/src/registry/plugins/client-plugin.js +30 -0
- package/src/registry/plugins/data-plugin.js +21 -0
- package/src/registry/plugins/security-plugin.js +27 -0
- package/src/registry/plugins/server-plugin.js +46 -0
- package/src/registry/plugins/shared-plugin.js +17 -0
- package/src/registry/plugins/test-plugin.js +23 -0
- package/src/registry/register-all.js +23 -0
- package/src/stdlib/inline.js +175 -0
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -36,9 +36,9 @@ const color = {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
const HELP = `
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
╔╦╗╔═╗╦ ╦╔═╗
|
|
40
|
+
║ ║ ║╚╗╔╝╠═╣
|
|
41
|
+
╩ ╚═╝ ╚╝ ╩ ╩ v${VERSION}
|
|
42
42
|
|
|
43
43
|
Created by Enoch Kujem Abassey
|
|
44
44
|
A modern full-stack language that transpiles to JavaScript
|
|
@@ -601,6 +601,18 @@ async function runFile(filePath, options = {}) {
|
|
|
601
601
|
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
602
602
|
const stdlib = getRunStdlib();
|
|
603
603
|
|
|
604
|
+
// CLI mode: execute the cli code directly
|
|
605
|
+
if (output.isCli) {
|
|
606
|
+
let code = stdlib + '\n' + output.cli;
|
|
607
|
+
code = code.replace(/^export /gm, '');
|
|
608
|
+
// Override process.argv for cli dispatch
|
|
609
|
+
const scriptArgs = options.scriptArgs || [];
|
|
610
|
+
code = `process.argv = ["node", ${JSON.stringify(resolved)}, ...${JSON.stringify(scriptArgs)}];\n` + code;
|
|
611
|
+
const fn = new AsyncFunction('__tova_args', '__tova_filename', '__tova_dirname', code);
|
|
612
|
+
await fn(scriptArgs, resolved, dirname(resolved));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
604
616
|
// Compile .tova dependencies and inline them
|
|
605
617
|
let depCode = '';
|
|
606
618
|
if (hasTovaImports) {
|
|
@@ -766,8 +778,29 @@ async function buildProject(args) {
|
|
|
766
778
|
const outSubDir = dirname(join(outDir, outBaseName));
|
|
767
779
|
if (outSubDir !== outDir) mkdirSync(outSubDir, { recursive: true });
|
|
768
780
|
|
|
781
|
+
// CLI files: write single executable <name>.js with shebang
|
|
782
|
+
if (output.isCli) {
|
|
783
|
+
if (output.cli && output.cli.trim()) {
|
|
784
|
+
const cliPath = join(outDir, `${outBaseName}.js`);
|
|
785
|
+
const shebang = '#!/usr/bin/env node\n';
|
|
786
|
+
writeFileSync(cliPath, shebang + output.cli);
|
|
787
|
+
try { chmodSync(cliPath, 0o755); } catch (e) { /* ignore on Windows */ }
|
|
788
|
+
if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', cliPath)} [cli]${timing}`);
|
|
789
|
+
}
|
|
790
|
+
if (!noCache) {
|
|
791
|
+
const outputPaths = {};
|
|
792
|
+
if (output.cli && output.cli.trim()) outputPaths.cli = join(outDir, `${outBaseName}.js`);
|
|
793
|
+
if (single) {
|
|
794
|
+
const absFile = files[0];
|
|
795
|
+
const sourceContent = readFileSync(absFile, 'utf-8');
|
|
796
|
+
buildCache.set(absFile, sourceContent, outputPaths);
|
|
797
|
+
} else {
|
|
798
|
+
buildCache.setGroup(`dir:${dir}`, files, outputPaths);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
769
802
|
// Module files: write single <name>.js (not .shared.js)
|
|
770
|
-
if (output.isModule) {
|
|
803
|
+
else if (output.isModule) {
|
|
771
804
|
if (output.shared && output.shared.trim()) {
|
|
772
805
|
const modulePath = join(outDir, `${outBaseName}.js`);
|
|
773
806
|
writeFileSync(modulePath, generateSourceMap(output.shared, modulePath));
|
|
@@ -4655,9 +4688,9 @@ async function infoCommand() {
|
|
|
4655
4688
|
const config = resolveConfig(process.cwd());
|
|
4656
4689
|
const hasTOML = config._source === 'tova.toml';
|
|
4657
4690
|
|
|
4658
|
-
console.log(`\n
|
|
4659
|
-
console.log(`
|
|
4660
|
-
console.log(`
|
|
4691
|
+
console.log(`\n ╔╦╗╔═╗╦ ╦╔═╗`);
|
|
4692
|
+
console.log(` ║ ║ ║╚╗╔╝╠═╣`);
|
|
4693
|
+
console.log(` ╩ ╚═╝ ╚╝ ╩ ╩ v${VERSION}\n`);
|
|
4661
4694
|
|
|
4662
4695
|
// Bun version
|
|
4663
4696
|
let bunVersion = 'not found';
|
package/package.json
CHANGED
package/src/analyzer/analyzer.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Scope, Symbol } from './scope.js';
|
|
2
2
|
import { PIPE_TARGET } from '../parser/ast.js';
|
|
3
3
|
import { BUILTIN_NAMES } from '../stdlib/inline.js';
|
|
4
|
-
import {
|
|
5
|
-
import { installClientAnalyzer } from './client-analyzer.js';
|
|
4
|
+
import { BlockRegistry } from '../registry/register-all.js';
|
|
6
5
|
import {
|
|
7
6
|
Type, PrimitiveType, NilType, AnyType, UnknownType,
|
|
8
7
|
ArrayType, TupleType, FunctionType, RecordType, ADTType,
|
|
@@ -327,19 +326,17 @@ export class Analyzer {
|
|
|
327
326
|
}
|
|
328
327
|
|
|
329
328
|
analyze() {
|
|
330
|
-
// Pre-pass
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
installServerAnalyzer(Analyzer);
|
|
334
|
-
this.serverBlockFunctions = collectServerBlockFunctions(this.ast);
|
|
335
|
-
} else {
|
|
336
|
-
this.serverBlockFunctions = new Map();
|
|
329
|
+
// Pre-pass hooks (e.g., server block function collection for RPC validation)
|
|
330
|
+
for (const plugin of BlockRegistry.all()) {
|
|
331
|
+
if (plugin.analyzer?.prePass) plugin.analyzer.prePass(this);
|
|
337
332
|
}
|
|
338
333
|
|
|
339
334
|
this.visitProgram(this.ast);
|
|
340
335
|
|
|
341
|
-
//
|
|
342
|
-
|
|
336
|
+
// Post-visit cross-block validation
|
|
337
|
+
for (const plugin of BlockRegistry.all()) {
|
|
338
|
+
if (plugin.analyzer?.crossBlockValidate) plugin.analyzer.crossBlockValidate(this);
|
|
339
|
+
}
|
|
343
340
|
|
|
344
341
|
// Check for unused variables/imports (#9)
|
|
345
342
|
this._collectAllScopes(this.globalScope);
|
|
@@ -732,43 +729,14 @@ export class Analyzer {
|
|
|
732
729
|
visitNode(node) {
|
|
733
730
|
if (!node) return;
|
|
734
731
|
|
|
732
|
+
// Single registry lookup: returns plugin, NOOP sentinel, or null
|
|
733
|
+
const entry = BlockRegistry.getByAstType(node.type);
|
|
734
|
+
if (entry) {
|
|
735
|
+
if (entry === BlockRegistry.NOOP) return;
|
|
736
|
+
if (entry.analyzer?.visit) return entry.analyzer.visit(this, node);
|
|
737
|
+
}
|
|
738
|
+
|
|
735
739
|
switch (node.type) {
|
|
736
|
-
case 'ServerBlock':
|
|
737
|
-
case 'RouteDeclaration':
|
|
738
|
-
case 'MiddlewareDeclaration':
|
|
739
|
-
case 'HealthCheckDeclaration':
|
|
740
|
-
case 'CorsDeclaration':
|
|
741
|
-
case 'ErrorHandlerDeclaration':
|
|
742
|
-
case 'WebSocketDeclaration':
|
|
743
|
-
case 'StaticDeclaration':
|
|
744
|
-
case 'DiscoverDeclaration':
|
|
745
|
-
case 'AuthDeclaration':
|
|
746
|
-
case 'MaxBodyDeclaration':
|
|
747
|
-
case 'RouteGroupDeclaration':
|
|
748
|
-
case 'RateLimitDeclaration':
|
|
749
|
-
case 'LifecycleHookDeclaration':
|
|
750
|
-
case 'SubscribeDeclaration':
|
|
751
|
-
case 'EnvDeclaration':
|
|
752
|
-
case 'ScheduleDeclaration':
|
|
753
|
-
case 'UploadDeclaration':
|
|
754
|
-
case 'SessionDeclaration':
|
|
755
|
-
case 'DbDeclaration':
|
|
756
|
-
case 'TlsDeclaration':
|
|
757
|
-
case 'CompressionDeclaration':
|
|
758
|
-
case 'BackgroundJobDeclaration':
|
|
759
|
-
case 'CacheDeclaration':
|
|
760
|
-
case 'SseDeclaration':
|
|
761
|
-
case 'ModelDeclaration':
|
|
762
|
-
return this._visitServerNode(node);
|
|
763
|
-
case 'AiConfigDeclaration': return; // handled at block level
|
|
764
|
-
case 'ClientBlock':
|
|
765
|
-
case 'StateDeclaration':
|
|
766
|
-
case 'ComputedDeclaration':
|
|
767
|
-
case 'EffectDeclaration':
|
|
768
|
-
case 'ComponentDeclaration':
|
|
769
|
-
case 'StoreDeclaration':
|
|
770
|
-
return this._visitClientNode(node);
|
|
771
|
-
case 'SharedBlock': return this.visitSharedBlock(node);
|
|
772
740
|
case 'Assignment': return this.visitAssignment(node);
|
|
773
741
|
case 'VarDeclaration': return this.visitVarDeclaration(node);
|
|
774
742
|
case 'LetDestructure': return this.visitLetDestructure(node);
|
|
@@ -790,24 +758,7 @@ export class Analyzer {
|
|
|
790
758
|
case 'ContinueStatement': return this.visitContinueStatement(node);
|
|
791
759
|
case 'GuardStatement': return this.visitGuardStatement(node);
|
|
792
760
|
case 'InterfaceDeclaration': return this.visitInterfaceDeclaration(node);
|
|
793
|
-
case 'DataBlock': return this.visitDataBlock(node);
|
|
794
|
-
case 'SecurityBlock': return this.visitSecurityBlock(node);
|
|
795
|
-
case 'SecurityAuthDeclaration': return;
|
|
796
|
-
case 'SecurityRoleDeclaration': return;
|
|
797
|
-
case 'SecurityProtectDeclaration': return;
|
|
798
|
-
case 'SecuritySensitiveDeclaration': return;
|
|
799
|
-
case 'SecurityCorsDeclaration': return;
|
|
800
|
-
case 'SecurityCspDeclaration': return;
|
|
801
|
-
case 'SecurityRateLimitDeclaration': return;
|
|
802
|
-
case 'SecurityCsrfDeclaration': return;
|
|
803
|
-
case 'SecurityAuditDeclaration': return;
|
|
804
|
-
case 'SourceDeclaration': return;
|
|
805
|
-
case 'PipelineDeclaration': return;
|
|
806
|
-
case 'ValidateBlock': return;
|
|
807
|
-
case 'RefreshPolicy': return;
|
|
808
761
|
case 'RefinementType': return;
|
|
809
|
-
case 'TestBlock': return this.visitTestBlock(node);
|
|
810
|
-
case 'BenchBlock': return this.visitTestBlock(node);
|
|
811
762
|
case 'ComponentStyleBlock': return; // raw CSS — no analysis needed
|
|
812
763
|
case 'ImplDeclaration': return this.visitImplDeclaration(node);
|
|
813
764
|
case 'TraitDeclaration': return this.visitTraitDeclaration(node);
|
|
@@ -821,19 +772,14 @@ export class Analyzer {
|
|
|
821
772
|
}
|
|
822
773
|
|
|
823
774
|
_visitServerNode(node) {
|
|
824
|
-
if (!Analyzer.prototype._serverAnalyzerInstalled) {
|
|
825
|
-
installServerAnalyzer(Analyzer);
|
|
826
|
-
}
|
|
827
775
|
const methodName = 'visit' + node.type;
|
|
828
776
|
return this[methodName](node);
|
|
829
777
|
}
|
|
830
778
|
|
|
831
779
|
_visitClientNode(node) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const methodName = 'visit' + node.type;
|
|
836
|
-
return this[methodName](node);
|
|
780
|
+
// Ensure client analyzer is installed (may be called from visitExpression for JSX)
|
|
781
|
+
const plugin = BlockRegistry.get('client');
|
|
782
|
+
return plugin.analyzer.visit(this, node);
|
|
837
783
|
}
|
|
838
784
|
|
|
839
785
|
visitExpression(node) {
|
|
@@ -1021,6 +967,88 @@ export class Analyzer {
|
|
|
1021
967
|
}
|
|
1022
968
|
}
|
|
1023
969
|
|
|
970
|
+
visitCliBlock(node) {
|
|
971
|
+
const validKeys = new Set(['name', 'version', 'description']);
|
|
972
|
+
|
|
973
|
+
// Validate config keys
|
|
974
|
+
for (const field of node.config) {
|
|
975
|
+
if (!validKeys.has(field.key)) {
|
|
976
|
+
this.warnings.push({
|
|
977
|
+
message: `Unknown cli config key '${field.key}' — valid keys are: ${[...validKeys].join(', ')}`,
|
|
978
|
+
loc: field.loc,
|
|
979
|
+
code: 'W_UNKNOWN_CLI_CONFIG',
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Validate commands
|
|
985
|
+
const commandNames = new Set();
|
|
986
|
+
for (const cmd of node.commands) {
|
|
987
|
+
// Duplicate command names
|
|
988
|
+
if (commandNames.has(cmd.name)) {
|
|
989
|
+
this.warnings.push({
|
|
990
|
+
message: `Duplicate cli command '${cmd.name}'`,
|
|
991
|
+
loc: cmd.loc,
|
|
992
|
+
code: 'W_DUPLICATE_CLI_COMMAND',
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
commandNames.add(cmd.name);
|
|
996
|
+
|
|
997
|
+
// Check for positional args after flags
|
|
998
|
+
let seenFlag = false;
|
|
999
|
+
for (const param of cmd.params) {
|
|
1000
|
+
if (param.isFlag) {
|
|
1001
|
+
seenFlag = true;
|
|
1002
|
+
} else if (seenFlag) {
|
|
1003
|
+
this.warnings.push({
|
|
1004
|
+
message: `Positional argument '${param.name}' after flag in command '${cmd.name}' — positionals should come before flags`,
|
|
1005
|
+
loc: param.loc,
|
|
1006
|
+
code: 'W_POSITIONAL_AFTER_FLAG',
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Visit command body with params in scope
|
|
1012
|
+
this.pushScope('function');
|
|
1013
|
+
for (const param of cmd.params) {
|
|
1014
|
+
this.currentScope.define(param.name,
|
|
1015
|
+
new Symbol(param.name, 'parameter', null, false, param.loc));
|
|
1016
|
+
}
|
|
1017
|
+
this.visitNode(cmd.body);
|
|
1018
|
+
this.popScope();
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
_validateCliCrossBlock() {
|
|
1023
|
+
const cliBlocks = this.ast.body.filter(n => n.type === 'CliBlock');
|
|
1024
|
+
if (cliBlocks.length === 0) return;
|
|
1025
|
+
|
|
1026
|
+
// Warn if cli + server coexist
|
|
1027
|
+
const hasServer = this.ast.body.some(n => n.type === 'ServerBlock');
|
|
1028
|
+
if (hasServer) {
|
|
1029
|
+
this.warnings.push({
|
|
1030
|
+
message: 'cli {} and server {} blocks in the same file — cli produces a standalone executable, not a web server',
|
|
1031
|
+
loc: cliBlocks[0].loc,
|
|
1032
|
+
code: 'W_CLI_WITH_SERVER',
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Check for missing name across all cli blocks
|
|
1037
|
+
let hasName = false;
|
|
1038
|
+
for (const block of cliBlocks) {
|
|
1039
|
+
for (const field of block.config) {
|
|
1040
|
+
if (field.key === 'name') hasName = true;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (!hasName) {
|
|
1044
|
+
this.warnings.push({
|
|
1045
|
+
message: 'cli block has no name: field — consider adding name: "your-tool"',
|
|
1046
|
+
loc: cliBlocks[0].loc,
|
|
1047
|
+
code: 'W_CLI_MISSING_NAME',
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1024
1052
|
_validateSecurityCrossBlock() {
|
|
1025
1053
|
// Collect ALL security declarations across ALL security blocks in the AST
|
|
1026
1054
|
const allRoles = new Set();
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// CLI code generator for the Tova language
|
|
2
|
+
// Produces a complete zero-dependency CLI executable from cli { } blocks.
|
|
3
|
+
|
|
4
|
+
import { BaseCodegen } from './base-codegen.js';
|
|
5
|
+
|
|
6
|
+
export class CliCodegen extends BaseCodegen {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Merge all CliBlock nodes into a single config.
|
|
10
|
+
* Multiple cli blocks are merged (last wins on config, commands accumulate).
|
|
11
|
+
*/
|
|
12
|
+
static mergeCliBlocks(cliBlocks) {
|
|
13
|
+
const config = {
|
|
14
|
+
name: null,
|
|
15
|
+
version: null,
|
|
16
|
+
description: null,
|
|
17
|
+
};
|
|
18
|
+
const commands = [];
|
|
19
|
+
|
|
20
|
+
for (const block of cliBlocks) {
|
|
21
|
+
for (const field of block.config) {
|
|
22
|
+
if (field.key === 'name' && field.value.type === 'StringLiteral') {
|
|
23
|
+
config.name = field.value.value;
|
|
24
|
+
} else if (field.key === 'version' && field.value.type === 'StringLiteral') {
|
|
25
|
+
config.version = field.value.value;
|
|
26
|
+
} else if (field.key === 'description' && field.value.type === 'StringLiteral') {
|
|
27
|
+
config.description = field.value.value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
commands.push(...block.commands);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { config, commands };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate a complete CLI executable.
|
|
38
|
+
* @param {Object} cliConfig — merged config from mergeCliBlocks
|
|
39
|
+
* @param {string} sharedCode — shared/top-level compiled code
|
|
40
|
+
* @returns {string} — complete executable JS
|
|
41
|
+
*/
|
|
42
|
+
generate(cliConfig, sharedCode) {
|
|
43
|
+
const { config, commands } = cliConfig;
|
|
44
|
+
const lines = [];
|
|
45
|
+
|
|
46
|
+
// Emit shared code (stdlib + top-level)
|
|
47
|
+
if (sharedCode && sharedCode.trim()) {
|
|
48
|
+
lines.push(sharedCode);
|
|
49
|
+
lines.push('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const singleCommand = commands.length === 1;
|
|
53
|
+
|
|
54
|
+
// Emit each command as a function
|
|
55
|
+
for (const cmd of commands) {
|
|
56
|
+
lines.push(this._genCommandFunction(cmd));
|
|
57
|
+
lines.push('');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Generate help functions
|
|
61
|
+
lines.push(this._genMainHelp(config, commands));
|
|
62
|
+
lines.push('');
|
|
63
|
+
|
|
64
|
+
for (const cmd of commands) {
|
|
65
|
+
lines.push(this._genCommandHelp(cmd, config, singleCommand));
|
|
66
|
+
lines.push('');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Generate dispatchers for each command
|
|
70
|
+
for (const cmd of commands) {
|
|
71
|
+
lines.push(this._genCommandDispatcher(cmd));
|
|
72
|
+
lines.push('');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Generate main entry point
|
|
76
|
+
lines.push(this._genMain(config, commands, singleCommand));
|
|
77
|
+
lines.push('');
|
|
78
|
+
|
|
79
|
+
// Auto-invoke
|
|
80
|
+
lines.push('__cli_main(process.argv.slice(2));');
|
|
81
|
+
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate a command function: __cmd_<name>(params...)
|
|
87
|
+
*/
|
|
88
|
+
_genCommandFunction(cmd) {
|
|
89
|
+
const paramNames = cmd.params.map(p => p.name);
|
|
90
|
+
const asyncPrefix = cmd.isAsync ? 'async ' : '';
|
|
91
|
+
const body = this.genBlockStatements(cmd.body);
|
|
92
|
+
return `${asyncPrefix}function __cmd_${cmd.name}(${paramNames.join(', ')}) {\n${body}\n}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Generate overall --help output
|
|
97
|
+
*/
|
|
98
|
+
_genMainHelp(config, commands) {
|
|
99
|
+
const lines = [];
|
|
100
|
+
lines.push('function __cli_help() {');
|
|
101
|
+
lines.push(' const lines = [];');
|
|
102
|
+
|
|
103
|
+
if (config.name) {
|
|
104
|
+
if (config.description) {
|
|
105
|
+
lines.push(` lines.push("${config.name} — ${this._escStr(config.description)}");`);
|
|
106
|
+
} else {
|
|
107
|
+
lines.push(` lines.push("${this._escStr(config.name)}");`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (config.version) {
|
|
111
|
+
lines.push(` lines.push("Version: ${this._escStr(config.version)}");`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
lines.push(' lines.push("");');
|
|
115
|
+
lines.push(' lines.push("USAGE:");');
|
|
116
|
+
|
|
117
|
+
if (commands.length === 1) {
|
|
118
|
+
const cmd = commands[0];
|
|
119
|
+
const usage = this._buildUsageLine(cmd, config);
|
|
120
|
+
lines.push(` lines.push(" ${usage}");`);
|
|
121
|
+
} else {
|
|
122
|
+
lines.push(` lines.push(" ${config.name || 'cli'} <command> [options]");`);
|
|
123
|
+
lines.push(' lines.push("");');
|
|
124
|
+
lines.push(' lines.push("COMMANDS:");');
|
|
125
|
+
|
|
126
|
+
for (const cmd of commands) {
|
|
127
|
+
const desc = this._getCommandDescription(cmd);
|
|
128
|
+
lines.push(` lines.push(" ${cmd.name.padEnd(16)}${this._escStr(desc)}");`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lines.push(' lines.push("");');
|
|
133
|
+
lines.push(' lines.push("OPTIONS:");');
|
|
134
|
+
lines.push(' lines.push(" --help, -h Show help");');
|
|
135
|
+
if (config.version) {
|
|
136
|
+
lines.push(' lines.push(" --version, -v Show version");');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
lines.push(' console.log(lines.join("\\n"));');
|
|
140
|
+
lines.push('}');
|
|
141
|
+
return lines.join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate per-command help: __cli_command_help_<name>()
|
|
146
|
+
*/
|
|
147
|
+
_genCommandHelp(cmd, config, singleCommand) {
|
|
148
|
+
const lines = [];
|
|
149
|
+
lines.push(`function __cli_command_help_${cmd.name}() {`);
|
|
150
|
+
lines.push(' const lines = [];');
|
|
151
|
+
|
|
152
|
+
const usage = this._buildUsageLine(cmd, config);
|
|
153
|
+
lines.push(` lines.push("USAGE:");`);
|
|
154
|
+
lines.push(` lines.push(" ${usage}");`);
|
|
155
|
+
|
|
156
|
+
const positionals = cmd.params.filter(p => !p.isFlag);
|
|
157
|
+
const flags = cmd.params.filter(p => p.isFlag);
|
|
158
|
+
|
|
159
|
+
if (positionals.length > 0) {
|
|
160
|
+
lines.push(' lines.push("");');
|
|
161
|
+
lines.push(' lines.push("ARGUMENTS:");');
|
|
162
|
+
for (const p of positionals) {
|
|
163
|
+
const typeSuffix = p.typeAnnotation ? ` <${p.typeAnnotation}>` : '';
|
|
164
|
+
const optSuffix = p.isOptional ? ' (optional)' : '';
|
|
165
|
+
const defSuffix = p.defaultValue ? ` (default: ${this._getDefaultStr(p)})` : '';
|
|
166
|
+
lines.push(` lines.push(" ${p.name.padEnd(16)}${this._escStr(typeSuffix + optSuffix + defSuffix)}");`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (flags.length > 0) {
|
|
171
|
+
lines.push(' lines.push("");');
|
|
172
|
+
lines.push(' lines.push("OPTIONS:");');
|
|
173
|
+
for (const f of flags) {
|
|
174
|
+
const typePart = f.typeAnnotation === 'Bool' ? '' : (f.typeAnnotation ? ` <${f.typeAnnotation}>` : '');
|
|
175
|
+
const defPart = f.defaultValue ? ` (default: ${this._getDefaultStr(f)})` : '';
|
|
176
|
+
lines.push(` lines.push(" --${f.name.padEnd(14)}${this._escStr(typePart + defPart)}");`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
lines.push(' lines.push(" --help, -h".padEnd(18) + "Show help");');
|
|
181
|
+
lines.push(' console.log(lines.join("\\n"));');
|
|
182
|
+
lines.push('}');
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Generate argv dispatcher for a command: __cli_dispatch_<name>(argv)
|
|
188
|
+
*/
|
|
189
|
+
_genCommandDispatcher(cmd) {
|
|
190
|
+
const lines = [];
|
|
191
|
+
const asyncPrefix = cmd.isAsync ? 'async ' : '';
|
|
192
|
+
lines.push(`${asyncPrefix}function __cli_dispatch_${cmd.name}(argv) {`);
|
|
193
|
+
|
|
194
|
+
const positionals = cmd.params.filter(p => !p.isFlag);
|
|
195
|
+
const flags = cmd.params.filter(p => p.isFlag);
|
|
196
|
+
|
|
197
|
+
// Initialize flag variables with defaults
|
|
198
|
+
for (const f of flags) {
|
|
199
|
+
if (f.typeAnnotation === 'Bool') {
|
|
200
|
+
lines.push(` let __flag_${f.name} = false;`);
|
|
201
|
+
} else if (f.isRepeated) {
|
|
202
|
+
lines.push(` let __flag_${f.name} = [];`);
|
|
203
|
+
} else if (f.defaultValue) {
|
|
204
|
+
lines.push(` let __flag_${f.name} = ${this.genExpression(f.defaultValue)};`);
|
|
205
|
+
} else {
|
|
206
|
+
lines.push(` let __flag_${f.name} = undefined;`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Positional collector
|
|
211
|
+
lines.push(' const __positionals = [];');
|
|
212
|
+
|
|
213
|
+
// Parse argv
|
|
214
|
+
lines.push(' for (let __i = 0; __i < argv.length; __i++) {');
|
|
215
|
+
lines.push(' const __arg = argv[__i];');
|
|
216
|
+
|
|
217
|
+
// Check --help
|
|
218
|
+
lines.push(` if (__arg === "--help" || __arg === "-h") { __cli_command_help_${cmd.name}(); return; }`);
|
|
219
|
+
|
|
220
|
+
// Check each flag
|
|
221
|
+
for (const f of flags) {
|
|
222
|
+
if (f.typeAnnotation === 'Bool') {
|
|
223
|
+
lines.push(` if (__arg === "--${f.name}") { __flag_${f.name} = true; continue; }`);
|
|
224
|
+
lines.push(` if (__arg === "--no-${f.name}") { __flag_${f.name} = false; continue; }`);
|
|
225
|
+
} else if (f.isRepeated) {
|
|
226
|
+
lines.push(` if (__arg === "--${f.name}") {`);
|
|
227
|
+
lines.push(` if (__i + 1 >= argv.length) { console.error("Error: --${f.name} requires a value"); process.exit(1); }`);
|
|
228
|
+
lines.push(` __flag_${f.name}.push(${this._genCoercion(`argv[++__i]`, f.typeAnnotation, f.name)});`);
|
|
229
|
+
lines.push(' continue;');
|
|
230
|
+
lines.push(' }');
|
|
231
|
+
} else {
|
|
232
|
+
lines.push(` if (__arg === "--${f.name}") {`);
|
|
233
|
+
lines.push(` if (__i + 1 >= argv.length) { console.error("Error: --${f.name} requires a value"); process.exit(1); }`);
|
|
234
|
+
lines.push(` __flag_${f.name} = ${this._genCoercion(`argv[++__i]`, f.typeAnnotation, f.name)};`);
|
|
235
|
+
lines.push(' continue;');
|
|
236
|
+
lines.push(' }');
|
|
237
|
+
// Support --flag=value syntax
|
|
238
|
+
lines.push(` if (__arg.startsWith("--${f.name}=")) {`);
|
|
239
|
+
lines.push(` __flag_${f.name} = ${this._genCoercion(`__arg.slice(${f.name.length + 3})`, f.typeAnnotation, f.name)};`);
|
|
240
|
+
lines.push(' continue;');
|
|
241
|
+
lines.push(' }');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Unknown flags
|
|
246
|
+
lines.push(' if (__arg.startsWith("--")) { console.error("Error: Unknown flag " + __arg); process.exit(1); }');
|
|
247
|
+
|
|
248
|
+
// Collect positionals
|
|
249
|
+
lines.push(' __positionals.push(__arg);');
|
|
250
|
+
lines.push(' }');
|
|
251
|
+
|
|
252
|
+
// Validate and assign positionals
|
|
253
|
+
for (let i = 0; i < positionals.length; i++) {
|
|
254
|
+
const p = positionals[i];
|
|
255
|
+
if (!p.isOptional && !p.defaultValue) {
|
|
256
|
+
lines.push(` if (__positionals.length <= ${i}) {`);
|
|
257
|
+
lines.push(` console.error("Error: Missing required argument <${p.name}>");`);
|
|
258
|
+
lines.push(` __cli_command_help_${cmd.name}();`);
|
|
259
|
+
lines.push(' process.exit(1);');
|
|
260
|
+
lines.push(' }');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Build call arguments
|
|
265
|
+
const callArgs = [];
|
|
266
|
+
for (const p of cmd.params) {
|
|
267
|
+
if (p.isFlag) {
|
|
268
|
+
callArgs.push(`__flag_${p.name}`);
|
|
269
|
+
} else {
|
|
270
|
+
const idx = positionals.indexOf(p);
|
|
271
|
+
if (p.isOptional || p.defaultValue) {
|
|
272
|
+
const def = p.defaultValue ? this.genExpression(p.defaultValue) : 'undefined';
|
|
273
|
+
callArgs.push(`__positionals.length > ${idx} ? ${this._genCoercion(`__positionals[${idx}]`, p.typeAnnotation, p.name)} : ${def}`);
|
|
274
|
+
} else {
|
|
275
|
+
callArgs.push(this._genCoercion(`__positionals[${idx}]`, p.typeAnnotation, p.name));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const awaitPrefix = cmd.isAsync ? 'await ' : '';
|
|
281
|
+
lines.push(` ${awaitPrefix}__cmd_${cmd.name}(${callArgs.join(', ')});`);
|
|
282
|
+
lines.push('}');
|
|
283
|
+
return lines.join('\n');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Generate main entry point
|
|
288
|
+
*/
|
|
289
|
+
_genMain(config, commands, singleCommand) {
|
|
290
|
+
const lines = [];
|
|
291
|
+
lines.push('async function __cli_main(argv) {');
|
|
292
|
+
|
|
293
|
+
if (singleCommand) {
|
|
294
|
+
const cmd = commands[0];
|
|
295
|
+
// Single-command mode: no subcommand routing
|
|
296
|
+
lines.push(' if (argv.includes("--help") || argv.includes("-h")) { __cli_help(); return; }');
|
|
297
|
+
if (config.version) {
|
|
298
|
+
lines.push(` if (argv.includes("--version") || argv.includes("-v")) { console.log("${this._escStr(config.version)}"); return; }`);
|
|
299
|
+
}
|
|
300
|
+
lines.push(` await __cli_dispatch_${cmd.name}(argv);`);
|
|
301
|
+
} else {
|
|
302
|
+
// Multi-command: subcommand routing
|
|
303
|
+
lines.push(' if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") { __cli_help(); return; }');
|
|
304
|
+
if (config.version) {
|
|
305
|
+
lines.push(` if (argv[0] === "--version" || argv[0] === "-v") { console.log("${this._escStr(config.version)}"); return; }`);
|
|
306
|
+
}
|
|
307
|
+
lines.push(' const __subcmd = argv[0];');
|
|
308
|
+
lines.push(' const __subargv = argv.slice(1);');
|
|
309
|
+
lines.push(' switch (__subcmd) {');
|
|
310
|
+
for (const cmd of commands) {
|
|
311
|
+
lines.push(` case "${cmd.name}": await __cli_dispatch_${cmd.name}(__subargv); break;`);
|
|
312
|
+
}
|
|
313
|
+
lines.push(' default:');
|
|
314
|
+
lines.push(' console.error("Error: Unknown command \\"" + __subcmd + "\\"");');
|
|
315
|
+
lines.push(' __cli_help();');
|
|
316
|
+
lines.push(' process.exit(1);');
|
|
317
|
+
lines.push(' }');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
lines.push('}');
|
|
321
|
+
return lines.join('\n');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Generate type coercion code for argv string → target type
|
|
326
|
+
*/
|
|
327
|
+
_genCoercion(expr, type, name) {
|
|
328
|
+
switch (type) {
|
|
329
|
+
case 'Int':
|
|
330
|
+
return `(function(v) { const n = parseInt(v, 10); if (isNaN(n)) { console.error("Error: --${name} must be an integer, got \\"" + v + "\\""); process.exit(1); } return n; })(${expr})`;
|
|
331
|
+
case 'Float':
|
|
332
|
+
return `(function(v) { const n = parseFloat(v); if (isNaN(n)) { console.error("Error: --${name} must be a number, got \\"" + v + "\\""); process.exit(1); } return n; })(${expr})`;
|
|
333
|
+
case 'Bool':
|
|
334
|
+
return `(${expr} === "true" || ${expr} === "1" || ${expr} === "yes")`;
|
|
335
|
+
case 'String':
|
|
336
|
+
default:
|
|
337
|
+
return expr;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Build a usage line for a command
|
|
343
|
+
*/
|
|
344
|
+
_buildUsageLine(cmd, config) {
|
|
345
|
+
const prefix = config.name || 'cli';
|
|
346
|
+
const parts = [prefix];
|
|
347
|
+
// Only show command name if multi-command
|
|
348
|
+
// (Caller should decide; we always include for per-command help)
|
|
349
|
+
parts.push(cmd.name);
|
|
350
|
+
|
|
351
|
+
for (const p of cmd.params) {
|
|
352
|
+
if (p.isFlag) {
|
|
353
|
+
if (p.typeAnnotation === 'Bool') {
|
|
354
|
+
parts.push(`[--${p.name}]`);
|
|
355
|
+
} else {
|
|
356
|
+
parts.push(`[--${p.name} <${p.typeAnnotation || 'value'}>]`);
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
if (p.isOptional || p.defaultValue) {
|
|
360
|
+
parts.push(`[${p.name}]`);
|
|
361
|
+
} else {
|
|
362
|
+
parts.push(`<${p.name}>`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return parts.join(' ');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_getCommandDescription(cmd) {
|
|
370
|
+
// Could be extended to parse docstrings — for now, just the command name
|
|
371
|
+
return '';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_getDefaultStr(param) {
|
|
375
|
+
if (!param.defaultValue) return '';
|
|
376
|
+
if (param.defaultValue.type === 'StringLiteral') return `"${param.defaultValue.value}"`;
|
|
377
|
+
if (param.defaultValue.type === 'NumberLiteral') return String(param.defaultValue.value);
|
|
378
|
+
if (param.defaultValue.type === 'BooleanLiteral') return String(param.defaultValue.value);
|
|
379
|
+
return '...';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_escStr(s) {
|
|
383
|
+
if (!s) return '';
|
|
384
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
385
|
+
}
|
|
386
|
+
}
|