@rcrsr/rill-cli 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/dist/check/config.d.ts +20 -0
  3. package/dist/check/config.d.ts.map +1 -0
  4. package/dist/check/config.js +151 -0
  5. package/dist/check/config.js.map +1 -0
  6. package/dist/check/fixer.d.ts +39 -0
  7. package/dist/check/fixer.d.ts.map +1 -0
  8. package/dist/check/fixer.js +119 -0
  9. package/dist/check/fixer.js.map +1 -0
  10. package/dist/check/index.d.ts +10 -0
  11. package/dist/check/index.d.ts.map +1 -0
  12. package/dist/check/index.js +21 -0
  13. package/dist/check/index.js.map +1 -0
  14. package/dist/check/rules/anti-patterns.d.ts +65 -0
  15. package/dist/check/rules/anti-patterns.d.ts.map +1 -0
  16. package/dist/check/rules/anti-patterns.js +481 -0
  17. package/dist/check/rules/anti-patterns.js.map +1 -0
  18. package/dist/check/rules/closures.d.ts +66 -0
  19. package/dist/check/rules/closures.d.ts.map +1 -0
  20. package/dist/check/rules/closures.js +370 -0
  21. package/dist/check/rules/closures.js.map +1 -0
  22. package/dist/check/rules/collections.d.ts +90 -0
  23. package/dist/check/rules/collections.d.ts.map +1 -0
  24. package/dist/check/rules/collections.js +373 -0
  25. package/dist/check/rules/collections.js.map +1 -0
  26. package/dist/check/rules/conditionals.d.ts +41 -0
  27. package/dist/check/rules/conditionals.d.ts.map +1 -0
  28. package/dist/check/rules/conditionals.js +134 -0
  29. package/dist/check/rules/conditionals.js.map +1 -0
  30. package/dist/check/rules/flow.d.ts +46 -0
  31. package/dist/check/rules/flow.d.ts.map +1 -0
  32. package/dist/check/rules/flow.js +206 -0
  33. package/dist/check/rules/flow.js.map +1 -0
  34. package/dist/check/rules/formatting.d.ts +143 -0
  35. package/dist/check/rules/formatting.d.ts.map +1 -0
  36. package/dist/check/rules/formatting.js +656 -0
  37. package/dist/check/rules/formatting.js.map +1 -0
  38. package/dist/check/rules/helpers.d.ts +26 -0
  39. package/dist/check/rules/helpers.d.ts.map +1 -0
  40. package/dist/check/rules/helpers.js +66 -0
  41. package/dist/check/rules/helpers.js.map +1 -0
  42. package/dist/check/rules/index.d.ts +21 -0
  43. package/dist/check/rules/index.d.ts.map +1 -0
  44. package/dist/check/rules/index.js +78 -0
  45. package/dist/check/rules/index.js.map +1 -0
  46. package/dist/check/rules/loops.d.ts +77 -0
  47. package/dist/check/rules/loops.d.ts.map +1 -0
  48. package/dist/check/rules/loops.js +310 -0
  49. package/dist/check/rules/loops.js.map +1 -0
  50. package/dist/check/rules/naming.d.ts +21 -0
  51. package/dist/check/rules/naming.d.ts.map +1 -0
  52. package/dist/check/rules/naming.js +174 -0
  53. package/dist/check/rules/naming.js.map +1 -0
  54. package/dist/check/rules/strings.d.ts +28 -0
  55. package/dist/check/rules/strings.d.ts.map +1 -0
  56. package/dist/check/rules/strings.js +79 -0
  57. package/dist/check/rules/strings.js.map +1 -0
  58. package/dist/check/rules/types.d.ts +41 -0
  59. package/dist/check/rules/types.d.ts.map +1 -0
  60. package/dist/check/rules/types.js +167 -0
  61. package/dist/check/rules/types.js.map +1 -0
  62. package/dist/check/types.d.ts +112 -0
  63. package/dist/check/types.d.ts.map +1 -0
  64. package/dist/check/types.js +6 -0
  65. package/dist/check/types.js.map +1 -0
  66. package/dist/check/validator.d.ts +18 -0
  67. package/dist/check/validator.d.ts.map +1 -0
  68. package/dist/check/validator.js +110 -0
  69. package/dist/check/validator.js.map +1 -0
  70. package/dist/check/visitor.d.ts +33 -0
  71. package/dist/check/visitor.d.ts.map +1 -0
  72. package/dist/check/visitor.js +259 -0
  73. package/dist/check/visitor.js.map +1 -0
  74. package/dist/cli-check.d.ts +43 -0
  75. package/dist/cli-check.d.ts.map +1 -0
  76. package/dist/cli-check.js +366 -0
  77. package/dist/cli-check.js.map +1 -0
  78. package/dist/cli-error-enrichment.d.ts +73 -0
  79. package/dist/cli-error-enrichment.d.ts.map +1 -0
  80. package/dist/cli-error-enrichment.js +205 -0
  81. package/dist/cli-error-enrichment.js.map +1 -0
  82. package/dist/cli-error-formatter.d.ts +45 -0
  83. package/dist/cli-error-formatter.d.ts.map +1 -0
  84. package/dist/cli-error-formatter.js +218 -0
  85. package/dist/cli-error-formatter.js.map +1 -0
  86. package/dist/cli-eval.d.ts +15 -0
  87. package/dist/cli-eval.d.ts.map +1 -0
  88. package/dist/cli-eval.js +116 -0
  89. package/dist/cli-eval.js.map +1 -0
  90. package/dist/cli-exec.d.ts +58 -0
  91. package/dist/cli-exec.d.ts.map +1 -0
  92. package/dist/cli-exec.js +326 -0
  93. package/dist/cli-exec.js.map +1 -0
  94. package/dist/cli-explain.d.ts +24 -0
  95. package/dist/cli-explain.d.ts.map +1 -0
  96. package/dist/cli-explain.js +68 -0
  97. package/dist/cli-explain.js.map +1 -0
  98. package/dist/cli-lsp-diagnostic.d.ts +35 -0
  99. package/dist/cli-lsp-diagnostic.d.ts.map +1 -0
  100. package/dist/cli-lsp-diagnostic.js +98 -0
  101. package/dist/cli-lsp-diagnostic.js.map +1 -0
  102. package/dist/cli-module-loader.d.ts +19 -0
  103. package/dist/cli-module-loader.d.ts.map +1 -0
  104. package/dist/cli-module-loader.js +83 -0
  105. package/dist/cli-module-loader.js.map +1 -0
  106. package/dist/cli-shared.d.ts +62 -0
  107. package/dist/cli-shared.d.ts.map +1 -0
  108. package/dist/cli-shared.js +158 -0
  109. package/dist/cli-shared.js.map +1 -0
  110. package/dist/cli.d.ts +13 -0
  111. package/dist/cli.d.ts.map +1 -0
  112. package/dist/cli.js +62 -0
  113. package/dist/cli.js.map +1 -0
  114. package/dist/test-internal-import.d.ts +2 -0
  115. package/dist/test-internal-import.d.ts.map +1 -0
  116. package/dist/test-internal-import.js +7 -0
  117. package/dist/test-internal-import.js.map +1 -0
  118. package/package.json +24 -0
  119. package/src/check/config.ts +202 -0
  120. package/src/check/fixer.ts +174 -0
  121. package/src/check/index.ts +39 -0
  122. package/src/check/rules/anti-patterns.ts +585 -0
  123. package/src/check/rules/closures.ts +445 -0
  124. package/src/check/rules/collections.ts +437 -0
  125. package/src/check/rules/conditionals.ts +155 -0
  126. package/src/check/rules/flow.ts +262 -0
  127. package/src/check/rules/formatting.ts +811 -0
  128. package/src/check/rules/helpers.ts +89 -0
  129. package/src/check/rules/index.ts +140 -0
  130. package/src/check/rules/loops.ts +372 -0
  131. package/src/check/rules/naming.ts +242 -0
  132. package/src/check/rules/strings.ts +104 -0
  133. package/src/check/rules/types.ts +214 -0
  134. package/src/check/types.ts +163 -0
  135. package/src/check/validator.ts +136 -0
  136. package/src/check/visitor.ts +338 -0
  137. package/src/cli-check.ts +456 -0
  138. package/src/cli-error-enrichment.ts +274 -0
  139. package/src/cli-error-formatter.ts +313 -0
  140. package/src/cli-eval.ts +145 -0
  141. package/src/cli-exec.ts +408 -0
  142. package/src/cli-explain.ts +76 -0
  143. package/src/cli-lsp-diagnostic.ts +132 -0
  144. package/src/cli-module-loader.ts +101 -0
  145. package/src/cli-shared.ts +187 -0
  146. package/tests/check/cli-check.test.ts +189 -0
  147. package/tests/check/config.test.ts +350 -0
  148. package/tests/check/fixer.test.ts +373 -0
  149. package/tests/check/format-diagnostics.test.ts +327 -0
  150. package/tests/check/rules/anti-patterns.test.ts +467 -0
  151. package/tests/check/rules/closures.test.ts +192 -0
  152. package/tests/check/rules/collections.test.ts +380 -0
  153. package/tests/check/rules/conditionals.test.ts +185 -0
  154. package/tests/check/rules/flow.test.ts +250 -0
  155. package/tests/check/rules/formatting.test.ts +755 -0
  156. package/tests/check/rules/loops.test.ts +334 -0
  157. package/tests/check/rules/naming.test.ts +336 -0
  158. package/tests/check/rules/strings.test.ts +129 -0
  159. package/tests/check/rules/types.test.ts +257 -0
  160. package/tests/check/validator.test.ts +444 -0
  161. package/tests/check/visitor.test.ts +171 -0
  162. package/tests/cli/check.test.ts +801 -0
  163. package/tests/cli/error-enrichment.test.ts +510 -0
  164. package/tests/cli/error-formatter.test.ts +631 -0
  165. package/tests/cli/eval.test.ts +85 -0
  166. package/tests/cli/exec.test.ts +537 -0
  167. package/tests/cli-explain.test.ts +249 -0
  168. package/tests/cli-lsp-diagnostic.test.ts +202 -0
  169. package/tests/cli-shared.test.ts +439 -0
  170. package/tsconfig.json +9 -0
  171. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Rill CLI - Evaluate rill expressions
4
+ *
5
+ * Usage:
6
+ * rill-eval '"hello".len'
7
+ * rill-eval --help
8
+ * rill-eval --version
9
+ */
10
+
11
+ import {
12
+ createRuntimeContext,
13
+ execute,
14
+ parse,
15
+ type ExecutionResult,
16
+ } from '@rcrsr/rill';
17
+ import { formatOutput, determineExitCode, VERSION } from './cli-shared.js';
18
+
19
+ /**
20
+ * Parse command-line arguments into structured command
21
+ */
22
+ function parseArgs(
23
+ argv: string[]
24
+ ):
25
+ | { mode: 'exec'; file: string; args: string[] }
26
+ | { mode: 'eval'; expression: string }
27
+ | { mode: 'help' | 'version' } {
28
+ // Check for --help and --version in any position
29
+ if (argv.includes('--help')) {
30
+ return { mode: 'help' };
31
+ }
32
+ if (argv.includes('--version')) {
33
+ return { mode: 'version' };
34
+ }
35
+
36
+ // Check for unknown flags (anything starting with -)
37
+ for (const arg of argv) {
38
+ if (arg.startsWith('-') && arg !== '-') {
39
+ throw new Error(`Unknown option: ${arg}`);
40
+ }
41
+ }
42
+
43
+ // If no arguments, default to help
44
+ if (argv.length === 0) {
45
+ return { mode: 'help' };
46
+ }
47
+
48
+ // First positional arg determines mode
49
+ const firstArg = argv[0]!;
50
+
51
+ // Eval mode: direct expression
52
+ return { mode: 'eval', expression: firstArg };
53
+ }
54
+
55
+ /**
56
+ * Evaluate a Rill expression without file context
57
+ */
58
+ export async function evaluateExpression(
59
+ expression: string
60
+ ): Promise<ExecutionResult> {
61
+ const ctx = createRuntimeContext({
62
+ callbacks: {
63
+ onLog: (value) => console.log(formatOutput(value)),
64
+ },
65
+ });
66
+
67
+ // Set pipeValue to empty list (Rill has no null concept per language spec)
68
+ ctx.pipeValue = [];
69
+
70
+ const ast = parse(expression);
71
+ return execute(ast, ctx);
72
+ }
73
+
74
+ /**
75
+ * Display help information
76
+ */
77
+ function showHelp(): void {
78
+ console.log(`Rill Expression Evaluator
79
+
80
+ Usage:
81
+ rill-eval <expression> Evaluate a Rill expression
82
+ rill-eval --help Show this help message
83
+ rill-eval --version Show version information
84
+
85
+ Examples:
86
+ rill-eval '"hello".len'
87
+ rill-eval '5 + 3'
88
+ rill-eval '[1, 2, 3] -> map |x|($x * 2)'`);
89
+ }
90
+
91
+ /**
92
+ * Display version information
93
+ */
94
+ function showVersion(): void {
95
+ console.log(`rill-eval ${VERSION}`);
96
+ }
97
+
98
+ /**
99
+ * Entry point for rill-eval binary
100
+ */
101
+ async function main(): Promise<void> {
102
+ try {
103
+ const args = process.argv.slice(2);
104
+ const command = parseArgs(args);
105
+
106
+ if (command.mode === 'help') {
107
+ showHelp();
108
+ return;
109
+ }
110
+
111
+ if (command.mode === 'version') {
112
+ showVersion();
113
+ return;
114
+ }
115
+
116
+ if (command.mode === 'eval') {
117
+ const result = await evaluateExpression(command.expression);
118
+ const { code, message } = determineExitCode(result.value);
119
+
120
+ if (message !== undefined) {
121
+ console.log(message);
122
+ } else {
123
+ console.log(formatOutput(result.value));
124
+ }
125
+ process.exit(code);
126
+ }
127
+
128
+ // Unreachable - exec mode not supported in rill-eval
129
+ console.error('Unexpected command mode');
130
+ process.exit(1);
131
+ } catch (err) {
132
+ console.error(err instanceof Error ? err.message : String(err));
133
+ process.exit(1);
134
+ }
135
+ }
136
+
137
+ // Only run main if this is the entry point (not imported)
138
+ const shouldRunMain =
139
+ process.env['NODE_ENV'] !== 'test' &&
140
+ !process.env['VITEST'] &&
141
+ !process.env['VITEST_WORKER_ID'];
142
+
143
+ if (shouldRunMain) {
144
+ main();
145
+ }
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI Execution Entry Point
4
+ *
5
+ * Implements main(), parseArgs(), and executeScript() for rill-exec and rill-eval binaries.
6
+ * Handles file execution, stdin input, and module loading.
7
+ */
8
+
9
+ import * as fs from 'fs/promises';
10
+ import * as fsSync from 'fs';
11
+ import * as path from 'path';
12
+ import * as yaml from 'yaml';
13
+ import { parse, execute, createRuntimeContext } from '@rcrsr/rill';
14
+ import type { RillValue, ExecutionResult } from '@rcrsr/rill';
15
+ import {
16
+ formatOutput,
17
+ formatError,
18
+ determineExitCode,
19
+ VERSION,
20
+ detectHelpVersionFlag,
21
+ } from './cli-shared.js';
22
+ import { loadModule } from './cli-module-loader.js';
23
+ import { explainError } from './cli-explain.js';
24
+
25
+ /**
26
+ * Parsed command-line arguments
27
+ */
28
+ export type ParsedArgs =
29
+ | {
30
+ mode: 'exec';
31
+ file: string;
32
+ args: string[];
33
+ format: 'human' | 'json' | 'compact';
34
+ verbose: boolean;
35
+ maxStackDepth: number;
36
+ }
37
+ | { mode: 'eval'; expression: string }
38
+ | { mode: 'help' | 'version' }
39
+ | { mode: 'explain'; errorId: string };
40
+
41
+ /**
42
+ * Parse command-line arguments into structured command
43
+ *
44
+ * @param argv - Raw command-line arguments (typically process.argv.slice(2))
45
+ * @returns Parsed command object
46
+ */
47
+ export function parseArgs(argv: string[]): ParsedArgs {
48
+ // Check for --help or --version flags in any position
49
+ const helpVersionFlag = detectHelpVersionFlag(argv);
50
+ if (helpVersionFlag !== null) {
51
+ return helpVersionFlag;
52
+ }
53
+
54
+ // Check for --explain flag (IC-11)
55
+ const explainIndex = argv.findIndex((arg) => arg === '--explain');
56
+ if (explainIndex !== -1) {
57
+ const errorId = argv[explainIndex + 1];
58
+ if (!errorId) {
59
+ throw new Error('Missing error ID after --explain');
60
+ }
61
+ return { mode: 'explain', errorId };
62
+ }
63
+
64
+ // Parse format, verbose, and max-stack-depth flags (IC-11)
65
+ let format: 'human' | 'json' | 'compact' = 'human';
66
+ let verbose = false;
67
+ let maxStackDepth = 10;
68
+
69
+ const formatIndex = argv.findIndex((arg) => arg === '--format');
70
+ if (formatIndex !== -1) {
71
+ const formatValue = argv[formatIndex + 1];
72
+ // AC-15: Unknown --format value
73
+ if (
74
+ formatValue !== 'human' &&
75
+ formatValue !== 'json' &&
76
+ formatValue !== 'compact'
77
+ ) {
78
+ throw new Error(
79
+ `Invalid --format value: ${formatValue}. Must be one of: human, json, compact`
80
+ );
81
+ }
82
+ format = formatValue;
83
+ }
84
+
85
+ if (argv.includes('--verbose')) {
86
+ verbose = true;
87
+ }
88
+
89
+ const maxStackDepthIndex = argv.findIndex(
90
+ (arg) => arg === '--max-stack-depth'
91
+ );
92
+ if (maxStackDepthIndex !== -1) {
93
+ const depthValue = argv[maxStackDepthIndex + 1];
94
+ if (!depthValue) {
95
+ throw new Error('Missing value after --max-stack-depth');
96
+ }
97
+ const depth = parseInt(depthValue, 10);
98
+ if (isNaN(depth) || depth < 1 || depth > 100) {
99
+ throw new Error('--max-stack-depth must be a number between 1 and 100');
100
+ }
101
+ maxStackDepth = depth;
102
+ }
103
+
104
+ // Check for unknown flags
105
+ const knownFlags = [
106
+ '--help',
107
+ '-h',
108
+ '--version',
109
+ '-v',
110
+ '--explain',
111
+ '--format',
112
+ '--verbose',
113
+ '--max-stack-depth',
114
+ ];
115
+ for (let i = 0; i < argv.length; i++) {
116
+ const arg = argv[i];
117
+ if (arg && arg.startsWith('--')) {
118
+ if (!knownFlags.includes(arg)) {
119
+ throw new Error(`Unknown option: ${arg}`);
120
+ }
121
+ // Skip next argument if this is a flag that takes a value
122
+ if (
123
+ arg === '--format' ||
124
+ arg === '--max-stack-depth' ||
125
+ arg === '--explain'
126
+ ) {
127
+ i++;
128
+ }
129
+ } else if (arg && arg.startsWith('-') && arg !== '-') {
130
+ if (!knownFlags.includes(arg)) {
131
+ throw new Error(`Unknown option: ${arg}`);
132
+ }
133
+ }
134
+ }
135
+
136
+ // Determine mode from first positional argument (skip flags and their values)
137
+ let firstArg: string | undefined;
138
+ const positionalArgs: string[] = [];
139
+
140
+ for (let i = 0; i < argv.length; i++) {
141
+ const arg = argv[i];
142
+ if (!arg) continue;
143
+
144
+ // Skip flags
145
+ if (knownFlags.includes(arg)) {
146
+ // Skip the flag's value if it takes one
147
+ if (
148
+ arg === '--format' ||
149
+ arg === '--max-stack-depth' ||
150
+ arg === '--explain'
151
+ ) {
152
+ i++;
153
+ }
154
+ continue;
155
+ }
156
+
157
+ // This is a positional argument
158
+ if (!firstArg) {
159
+ firstArg = arg;
160
+ }
161
+ positionalArgs.push(arg);
162
+ }
163
+
164
+ if (!firstArg) {
165
+ throw new Error('Missing file argument');
166
+ }
167
+
168
+ // Eval mode is not supported in rill-exec (only rill-eval)
169
+ // This function is shared but context determines valid modes
170
+ if (firstArg === '-e') {
171
+ if (positionalArgs.length < 2) {
172
+ throw new Error('Missing expression after -e');
173
+ }
174
+ return { mode: 'eval', expression: positionalArgs[1]! };
175
+ }
176
+
177
+ // Exec mode (file or stdin)
178
+ const file = firstArg;
179
+ const args = positionalArgs.slice(1);
180
+ return { mode: 'exec', file, args, format, verbose, maxStackDepth };
181
+ }
182
+
183
+ /**
184
+ * Execute a Rill script file with arguments and module support
185
+ *
186
+ * @param file - File path or '-' for stdin
187
+ * @param args - Command-line arguments to pass as $ pipe value
188
+ * @param options - Execution options
189
+ * @returns Execution result with value, variables, and source text
190
+ * @throws Error if file not found or execution fails
191
+ */
192
+ export async function executeScript(
193
+ file: string,
194
+ args: string[],
195
+ options?: { stdin?: boolean; source?: string }
196
+ ): Promise<ExecutionResult & { source: string }> {
197
+ // Use pre-read source if provided, otherwise read from file or stdin
198
+ let source: string;
199
+ let scriptPath: string;
200
+
201
+ if (options?.source !== undefined) {
202
+ source = options.source;
203
+ scriptPath =
204
+ file === '-'
205
+ ? path.resolve(process.cwd(), '<stdin>')
206
+ : path.resolve(file);
207
+ } else if (file === '-' || options?.stdin) {
208
+ // Read from stdin (must use sync API for stdin)
209
+ source = fsSync.readFileSync(0, 'utf-8');
210
+ scriptPath = path.resolve(process.cwd(), '<stdin>');
211
+ } else {
212
+ // Check if file exists
213
+ try {
214
+ await fs.access(file);
215
+ } catch {
216
+ throw new Error(`File not found: ${file}`);
217
+ }
218
+
219
+ // Read from file
220
+ source = await fs.readFile(file, 'utf-8');
221
+ scriptPath = path.resolve(file);
222
+ }
223
+
224
+ // Parse the script
225
+ const ast = parse(source);
226
+
227
+ // Extract frontmatter for use: declarations
228
+ const frontmatter: Record<string, unknown> = ast.frontmatter
229
+ ? ((yaml.parse(ast.frontmatter.content) as Record<
230
+ string,
231
+ unknown
232
+ > | null) ?? {})
233
+ : {};
234
+
235
+ // Load modules if use: declarations exist
236
+ const variables: Record<string, RillValue> = {};
237
+ if (frontmatter['use'] && Array.isArray(frontmatter['use'])) {
238
+ const cache = new Map<string, Record<string, RillValue>>();
239
+ for (const entry of frontmatter['use']) {
240
+ if (typeof entry === 'object' && entry !== null) {
241
+ const [name, modulePath] = Object.entries(entry)[0] as [string, string];
242
+ variables[name] = await loadModule(modulePath, scriptPath, cache);
243
+ }
244
+ }
245
+ }
246
+
247
+ // Create runtime context with modules
248
+ const ctx = createRuntimeContext({
249
+ variables,
250
+ callbacks: {
251
+ onLog: (value) => console.log(formatOutput(value)),
252
+ },
253
+ });
254
+
255
+ // Set pipe value to arguments (string array)
256
+ ctx.pipeValue = args;
257
+
258
+ // Execute the script and return with source
259
+ const result = await execute(ast, ctx);
260
+ return { ...result, source };
261
+ }
262
+
263
+ /**
264
+ * Entry point for rill-exec and rill-eval binaries
265
+ *
266
+ * Parses command-line arguments, executes scripts, and handles errors.
267
+ * Writes results to stdout and errors to stderr.
268
+ * Sets process.exit(1) on any error.
269
+ */
270
+ export async function main(): Promise<void> {
271
+ let source: string | undefined;
272
+ let formatOptions:
273
+ | {
274
+ format: 'human' | 'json' | 'compact';
275
+ verbose: boolean;
276
+ maxStackDepth: number;
277
+ }
278
+ | undefined;
279
+
280
+ try {
281
+ const parsed = parseArgs(process.argv.slice(2));
282
+
283
+ switch (parsed.mode) {
284
+ case 'help':
285
+ console.log(`Usage:
286
+ rill-exec <script.rill> [args...] Execute a Rill script file
287
+ rill-exec - Read script from stdin
288
+ rill-exec --help Show this help message
289
+ rill-exec --version Show version information
290
+ rill-exec --explain RILL-XXXX Show error documentation
291
+
292
+ Options:
293
+ --format <format> Output format: human, json, compact (default: human)
294
+ --verbose Include additional error details
295
+ --max-stack-depth <n> Maximum call stack depth to display (default: 10, range: 1-100)
296
+
297
+ Arguments:
298
+ args are passed to the script as a list of strings in $ (pipe value)
299
+
300
+ Examples:
301
+ rill-exec script.rill
302
+ rill-exec script.rill arg1 arg2
303
+ rill-exec --format json script.rill
304
+ rill-exec --verbose --max-stack-depth 20 script.rill
305
+ rill-exec --explain RILL-R009
306
+ echo "log(\\"hello\\")" | rill-exec -`);
307
+ return;
308
+
309
+ case 'version': {
310
+ console.log(VERSION);
311
+ return;
312
+ }
313
+
314
+ case 'explain': {
315
+ // AC-16: Handle --explain command
316
+ const documentation = explainError(parsed.errorId);
317
+ if (documentation === null) {
318
+ // AC-16: Malformed errorId shows usage help
319
+ console.error(`Invalid error ID: ${parsed.errorId}`);
320
+ console.error(
321
+ 'Error ID must be in format RILL-{L|P|R|C}{3-digit}, e.g., RILL-R009'
322
+ );
323
+ process.exit(1);
324
+ return;
325
+ }
326
+ console.log(documentation);
327
+ return;
328
+ }
329
+
330
+ case 'eval':
331
+ // This shouldn't happen in rill-exec, but handle it anyway
332
+ console.error(
333
+ 'Eval mode not supported in rill-exec. Use rill-eval instead.'
334
+ );
335
+ process.exit(1);
336
+ return;
337
+
338
+ case 'exec': {
339
+ // Store format options for error handling
340
+ formatOptions = {
341
+ format: parsed.format,
342
+ verbose: parsed.verbose,
343
+ maxStackDepth: parsed.maxStackDepth,
344
+ };
345
+
346
+ // Read source early so it's available for error enrichment even if parsing fails
347
+ if (parsed.file === '-') {
348
+ source = fsSync.readFileSync(0, 'utf-8');
349
+ } else {
350
+ try {
351
+ source = await fs.readFile(parsed.file, 'utf-8');
352
+ } catch {
353
+ throw new Error(`File not found: ${parsed.file}`);
354
+ }
355
+ }
356
+
357
+ // Execute mode
358
+ const result = await executeScript(parsed.file, parsed.args, {
359
+ source,
360
+ });
361
+
362
+ const { code, message } = determineExitCode(result.value);
363
+
364
+ // Output message if present, otherwise output the result value
365
+ if (message !== undefined) {
366
+ console.log(message);
367
+ } else {
368
+ console.log(formatOutput(result.value));
369
+ }
370
+
371
+ // Exit with computed code
372
+ process.exit(code);
373
+ }
374
+ }
375
+ } catch (err) {
376
+ if (err instanceof Error) {
377
+ // IC-11: Pass source text and format options to formatError
378
+ console.error(
379
+ formatError(err, source, {
380
+ format: formatOptions?.format ?? 'human',
381
+ verbose: formatOptions?.verbose ?? false,
382
+ includeCallStack: true,
383
+ maxCallStackDepth: formatOptions?.maxStackDepth ?? 10,
384
+ })
385
+ );
386
+ } else {
387
+ console.error(
388
+ formatError(new Error(String(err)), source, {
389
+ format: formatOptions?.format ?? 'human',
390
+ verbose: formatOptions?.verbose ?? false,
391
+ includeCallStack: true,
392
+ maxCallStackDepth: formatOptions?.maxStackDepth ?? 10,
393
+ })
394
+ );
395
+ }
396
+ process.exit(1);
397
+ }
398
+ }
399
+
400
+ // Only run main if not in test environment
401
+ const shouldRunMain =
402
+ process.env['NODE_ENV'] !== 'test' &&
403
+ !process.env['VITEST'] &&
404
+ !process.env['VITEST_WORKER_ID'];
405
+
406
+ if (shouldRunMain) {
407
+ main();
408
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * CLI Error Explanation
3
+ * Function for rendering full error documentation
4
+ */
5
+
6
+ import { ERROR_REGISTRY } from '@rcrsr/rill';
7
+
8
+ /**
9
+ * Render full error documentation for --explain command.
10
+ *
11
+ * Constraints:
12
+ * - Lookup from ERROR_REGISTRY
13
+ * - Renders cause, resolution, examples sections
14
+ *
15
+ * @param errorId - Error identifier (format: RILL-{category}{3-digit})
16
+ * @returns Formatted documentation string, or null if errorId is invalid/unknown
17
+ *
18
+ * @example
19
+ * explainError("RILL-R009")
20
+ * // Returns: formatted documentation with cause, resolution, examples
21
+ *
22
+ * @example
23
+ * explainError("invalid")
24
+ * // Returns: null
25
+ */
26
+ export function explainError(errorId: string): string | null {
27
+ // EC-12: Invalid errorId format returns null
28
+ const errorIdPattern = /^RILL-[LPRC]\d{3}$/;
29
+ if (!errorIdPattern.test(errorId)) {
30
+ return null;
31
+ }
32
+
33
+ // EC-13: Unknown errorId returns null
34
+ const definition = ERROR_REGISTRY.get(errorId);
35
+ if (!definition) {
36
+ return null;
37
+ }
38
+
39
+ // Build documentation sections
40
+ const sections: string[] = [];
41
+
42
+ // Header: errorId and description
43
+ sections.push(`${definition.errorId}: ${definition.description}`);
44
+ sections.push('');
45
+
46
+ // Cause section (if present)
47
+ if (definition.cause) {
48
+ sections.push('Cause:');
49
+ sections.push(` ${definition.cause}`);
50
+ sections.push('');
51
+ }
52
+
53
+ // Resolution section (if present)
54
+ if (definition.resolution) {
55
+ sections.push('Resolution:');
56
+ sections.push(` ${definition.resolution}`);
57
+ sections.push('');
58
+ }
59
+
60
+ // Examples section (if present)
61
+ if (definition.examples && definition.examples.length > 0) {
62
+ sections.push('Examples:');
63
+ for (const example of definition.examples) {
64
+ sections.push(` ${example.description}`);
65
+ sections.push('');
66
+ // Indent code block
67
+ const codeLines = example.code.split('\n');
68
+ for (const line of codeLines) {
69
+ sections.push(` ${line}`);
70
+ }
71
+ sections.push('');
72
+ }
73
+ }
74
+
75
+ return sections.join('\n').trimEnd();
76
+ }