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 CHANGED
@@ -36,9 +36,9 @@ const color = {
36
36
  };
37
37
 
38
38
  const HELP = `
39
- ╦ ╦═╗ ╦
40
- ║ ╠╣
41
- ╩═╝╚═╝╩═╝╩ v${VERSION}
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(` ╩═╝╚═╝╩═╝╩ v${VERSION}\n`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Tova — a modern programming language that transpiles to JavaScript, unifying frontend and backend",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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 { collectServerBlockFunctions, installServerAnalyzer } from './server-analyzer.js';
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: collect named server block functions for inter-server RPC validation
331
- const hasServerBlocks = this.ast.body.some(n => n.type === 'ServerBlock');
332
- if (hasServerBlocks) {
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
- // Cross-block security validation (after all blocks are visited)
342
- this._validateSecurityCrossBlock();
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
- if (!Analyzer.prototype._clientAnalyzerInstalled) {
833
- installClientAnalyzer(Analyzer);
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
+ }