@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.
- package/LICENSE +21 -0
- package/dist/check/config.d.ts +20 -0
- package/dist/check/config.d.ts.map +1 -0
- package/dist/check/config.js +151 -0
- package/dist/check/config.js.map +1 -0
- package/dist/check/fixer.d.ts +39 -0
- package/dist/check/fixer.d.ts.map +1 -0
- package/dist/check/fixer.js +119 -0
- package/dist/check/fixer.js.map +1 -0
- package/dist/check/index.d.ts +10 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +21 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/rules/anti-patterns.d.ts +65 -0
- package/dist/check/rules/anti-patterns.d.ts.map +1 -0
- package/dist/check/rules/anti-patterns.js +481 -0
- package/dist/check/rules/anti-patterns.js.map +1 -0
- package/dist/check/rules/closures.d.ts +66 -0
- package/dist/check/rules/closures.d.ts.map +1 -0
- package/dist/check/rules/closures.js +370 -0
- package/dist/check/rules/closures.js.map +1 -0
- package/dist/check/rules/collections.d.ts +90 -0
- package/dist/check/rules/collections.d.ts.map +1 -0
- package/dist/check/rules/collections.js +373 -0
- package/dist/check/rules/collections.js.map +1 -0
- package/dist/check/rules/conditionals.d.ts +41 -0
- package/dist/check/rules/conditionals.d.ts.map +1 -0
- package/dist/check/rules/conditionals.js +134 -0
- package/dist/check/rules/conditionals.js.map +1 -0
- package/dist/check/rules/flow.d.ts +46 -0
- package/dist/check/rules/flow.d.ts.map +1 -0
- package/dist/check/rules/flow.js +206 -0
- package/dist/check/rules/flow.js.map +1 -0
- package/dist/check/rules/formatting.d.ts +143 -0
- package/dist/check/rules/formatting.d.ts.map +1 -0
- package/dist/check/rules/formatting.js +656 -0
- package/dist/check/rules/formatting.js.map +1 -0
- package/dist/check/rules/helpers.d.ts +26 -0
- package/dist/check/rules/helpers.d.ts.map +1 -0
- package/dist/check/rules/helpers.js +66 -0
- package/dist/check/rules/helpers.js.map +1 -0
- package/dist/check/rules/index.d.ts +21 -0
- package/dist/check/rules/index.d.ts.map +1 -0
- package/dist/check/rules/index.js +78 -0
- package/dist/check/rules/index.js.map +1 -0
- package/dist/check/rules/loops.d.ts +77 -0
- package/dist/check/rules/loops.d.ts.map +1 -0
- package/dist/check/rules/loops.js +310 -0
- package/dist/check/rules/loops.js.map +1 -0
- package/dist/check/rules/naming.d.ts +21 -0
- package/dist/check/rules/naming.d.ts.map +1 -0
- package/dist/check/rules/naming.js +174 -0
- package/dist/check/rules/naming.js.map +1 -0
- package/dist/check/rules/strings.d.ts +28 -0
- package/dist/check/rules/strings.d.ts.map +1 -0
- package/dist/check/rules/strings.js +79 -0
- package/dist/check/rules/strings.js.map +1 -0
- package/dist/check/rules/types.d.ts +41 -0
- package/dist/check/rules/types.d.ts.map +1 -0
- package/dist/check/rules/types.js +167 -0
- package/dist/check/rules/types.js.map +1 -0
- package/dist/check/types.d.ts +112 -0
- package/dist/check/types.d.ts.map +1 -0
- package/dist/check/types.js +6 -0
- package/dist/check/types.js.map +1 -0
- package/dist/check/validator.d.ts +18 -0
- package/dist/check/validator.d.ts.map +1 -0
- package/dist/check/validator.js +110 -0
- package/dist/check/validator.js.map +1 -0
- package/dist/check/visitor.d.ts +33 -0
- package/dist/check/visitor.d.ts.map +1 -0
- package/dist/check/visitor.js +259 -0
- package/dist/check/visitor.js.map +1 -0
- package/dist/cli-check.d.ts +43 -0
- package/dist/cli-check.d.ts.map +1 -0
- package/dist/cli-check.js +366 -0
- package/dist/cli-check.js.map +1 -0
- package/dist/cli-error-enrichment.d.ts +73 -0
- package/dist/cli-error-enrichment.d.ts.map +1 -0
- package/dist/cli-error-enrichment.js +205 -0
- package/dist/cli-error-enrichment.js.map +1 -0
- package/dist/cli-error-formatter.d.ts +45 -0
- package/dist/cli-error-formatter.d.ts.map +1 -0
- package/dist/cli-error-formatter.js +218 -0
- package/dist/cli-error-formatter.js.map +1 -0
- package/dist/cli-eval.d.ts +15 -0
- package/dist/cli-eval.d.ts.map +1 -0
- package/dist/cli-eval.js +116 -0
- package/dist/cli-eval.js.map +1 -0
- package/dist/cli-exec.d.ts +58 -0
- package/dist/cli-exec.d.ts.map +1 -0
- package/dist/cli-exec.js +326 -0
- package/dist/cli-exec.js.map +1 -0
- package/dist/cli-explain.d.ts +24 -0
- package/dist/cli-explain.d.ts.map +1 -0
- package/dist/cli-explain.js +68 -0
- package/dist/cli-explain.js.map +1 -0
- package/dist/cli-lsp-diagnostic.d.ts +35 -0
- package/dist/cli-lsp-diagnostic.d.ts.map +1 -0
- package/dist/cli-lsp-diagnostic.js +98 -0
- package/dist/cli-lsp-diagnostic.js.map +1 -0
- package/dist/cli-module-loader.d.ts +19 -0
- package/dist/cli-module-loader.d.ts.map +1 -0
- package/dist/cli-module-loader.js +83 -0
- package/dist/cli-module-loader.js.map +1 -0
- package/dist/cli-shared.d.ts +62 -0
- package/dist/cli-shared.d.ts.map +1 -0
- package/dist/cli-shared.js +158 -0
- package/dist/cli-shared.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -0
- package/dist/test-internal-import.d.ts +2 -0
- package/dist/test-internal-import.d.ts.map +1 -0
- package/dist/test-internal-import.js +7 -0
- package/dist/test-internal-import.js.map +1 -0
- package/package.json +24 -0
- package/src/check/config.ts +202 -0
- package/src/check/fixer.ts +174 -0
- package/src/check/index.ts +39 -0
- package/src/check/rules/anti-patterns.ts +585 -0
- package/src/check/rules/closures.ts +445 -0
- package/src/check/rules/collections.ts +437 -0
- package/src/check/rules/conditionals.ts +155 -0
- package/src/check/rules/flow.ts +262 -0
- package/src/check/rules/formatting.ts +811 -0
- package/src/check/rules/helpers.ts +89 -0
- package/src/check/rules/index.ts +140 -0
- package/src/check/rules/loops.ts +372 -0
- package/src/check/rules/naming.ts +242 -0
- package/src/check/rules/strings.ts +104 -0
- package/src/check/rules/types.ts +214 -0
- package/src/check/types.ts +163 -0
- package/src/check/validator.ts +136 -0
- package/src/check/visitor.ts +338 -0
- package/src/cli-check.ts +456 -0
- package/src/cli-error-enrichment.ts +274 -0
- package/src/cli-error-formatter.ts +313 -0
- package/src/cli-eval.ts +145 -0
- package/src/cli-exec.ts +408 -0
- package/src/cli-explain.ts +76 -0
- package/src/cli-lsp-diagnostic.ts +132 -0
- package/src/cli-module-loader.ts +101 -0
- package/src/cli-shared.ts +187 -0
- package/tests/check/cli-check.test.ts +189 -0
- package/tests/check/config.test.ts +350 -0
- package/tests/check/fixer.test.ts +373 -0
- package/tests/check/format-diagnostics.test.ts +327 -0
- package/tests/check/rules/anti-patterns.test.ts +467 -0
- package/tests/check/rules/closures.test.ts +192 -0
- package/tests/check/rules/collections.test.ts +380 -0
- package/tests/check/rules/conditionals.test.ts +185 -0
- package/tests/check/rules/flow.test.ts +250 -0
- package/tests/check/rules/formatting.test.ts +755 -0
- package/tests/check/rules/loops.test.ts +334 -0
- package/tests/check/rules/naming.test.ts +336 -0
- package/tests/check/rules/strings.test.ts +129 -0
- package/tests/check/rules/types.test.ts +257 -0
- package/tests/check/validator.test.ts +444 -0
- package/tests/check/visitor.test.ts +171 -0
- package/tests/cli/check.test.ts +801 -0
- package/tests/cli/error-enrichment.test.ts +510 -0
- package/tests/cli/error-formatter.test.ts +631 -0
- package/tests/cli/eval.test.ts +85 -0
- package/tests/cli/exec.test.ts +537 -0
- package/tests/cli-explain.test.ts +249 -0
- package/tests/cli-lsp-diagnostic.test.ts +202 -0
- package/tests/cli-shared.test.ts +439 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI LSP Diagnostic Conversion
|
|
3
|
+
* Convert RillError to LSP Diagnostic format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RillError, SourceLocation, ErrorSeverity } from '@rcrsr/rill';
|
|
7
|
+
import { ERROR_REGISTRY } from '@rcrsr/rill';
|
|
8
|
+
|
|
9
|
+
// ============================================================
|
|
10
|
+
// PUBLIC TYPES
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
export interface LspDiagnostic {
|
|
14
|
+
readonly range: LspRange | null;
|
|
15
|
+
readonly severity: 1 | 2 | 3;
|
|
16
|
+
readonly code: string;
|
|
17
|
+
readonly source: 'rill';
|
|
18
|
+
readonly message: string;
|
|
19
|
+
readonly suggestions?: string[] | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LspRange {
|
|
23
|
+
readonly start: LspPosition;
|
|
24
|
+
readonly end: LspPosition;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LspPosition {
|
|
28
|
+
readonly line: number;
|
|
29
|
+
readonly character: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================
|
|
33
|
+
// LSP DIAGNOSTIC CONVERSION
|
|
34
|
+
// ============================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convert RillError to LSP Diagnostic format.
|
|
38
|
+
*
|
|
39
|
+
* Constraints:
|
|
40
|
+
* - LSP uses zero-based line/character positions
|
|
41
|
+
* - Severity mapping: Error=1, Warning=2, Info=3
|
|
42
|
+
* - Source is always 'rill'
|
|
43
|
+
* - Returns diagnostic with null range when error has no span
|
|
44
|
+
*
|
|
45
|
+
* @param error - RillError to convert
|
|
46
|
+
* @returns LSP Diagnostic
|
|
47
|
+
*/
|
|
48
|
+
export function toLspDiagnostic(error: RillError): LspDiagnostic {
|
|
49
|
+
// Get error definition for severity mapping
|
|
50
|
+
const definition = ERROR_REGISTRY.get(error.errorId);
|
|
51
|
+
const errorSeverity: ErrorSeverity = definition?.severity ?? 'error';
|
|
52
|
+
|
|
53
|
+
// Map ErrorSeverity to LSP severity (Error=1, Warning=2, Info=3)
|
|
54
|
+
const severity = mapSeverityToLsp(errorSeverity);
|
|
55
|
+
|
|
56
|
+
// Extract message without location suffix
|
|
57
|
+
const message = error.message.replace(/ at \d+:\d+$/, '');
|
|
58
|
+
|
|
59
|
+
// Convert span to LSP range (zero-based positions)
|
|
60
|
+
// EC-11: Missing span returns diagnostic with null range
|
|
61
|
+
const range = error.location
|
|
62
|
+
? {
|
|
63
|
+
start: sourceLocationToLspPosition(error.location),
|
|
64
|
+
end: sourceLocationToLspPosition(error.location),
|
|
65
|
+
}
|
|
66
|
+
: null;
|
|
67
|
+
|
|
68
|
+
// Extract suggestions if present (max 3)
|
|
69
|
+
const errorData = error.toData();
|
|
70
|
+
const contextSuggestions = errorData.context?.['suggestions'];
|
|
71
|
+
let suggestions: string[] | undefined;
|
|
72
|
+
|
|
73
|
+
if (contextSuggestions) {
|
|
74
|
+
if (Array.isArray(contextSuggestions) && contextSuggestions.length > 0) {
|
|
75
|
+
const filtered = contextSuggestions
|
|
76
|
+
.slice(0, 3)
|
|
77
|
+
.map((s) => String(s))
|
|
78
|
+
.filter((s) => s.length > 0);
|
|
79
|
+
if (filtered.length > 0) {
|
|
80
|
+
suggestions = filtered;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Build diagnostic
|
|
86
|
+
return {
|
|
87
|
+
range,
|
|
88
|
+
severity,
|
|
89
|
+
code: error.errorId,
|
|
90
|
+
source: 'rill',
|
|
91
|
+
message,
|
|
92
|
+
...(suggestions ? { suggestions } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Convert SourceLocation to zero-based LspPosition.
|
|
98
|
+
*
|
|
99
|
+
* Rill uses 1-based line and column numbers; LSP uses 0-based.
|
|
100
|
+
*
|
|
101
|
+
* @param location - Source location (1-based line, 1-based column)
|
|
102
|
+
* @returns LSP position (0-based line, 0-based character)
|
|
103
|
+
*/
|
|
104
|
+
function sourceLocationToLspPosition(location: SourceLocation): LspPosition {
|
|
105
|
+
return {
|
|
106
|
+
line: location.line - 1, // Convert 1-based to 0-based
|
|
107
|
+
character: location.column - 1, // Convert 1-based to 0-based
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Map ErrorSeverity to LSP severity code.
|
|
113
|
+
*
|
|
114
|
+
* Mapping:
|
|
115
|
+
* - 'error' -> 1 (Error)
|
|
116
|
+
* - 'warning' -> 2 (Warning)
|
|
117
|
+
* - (future) 'info' -> 3 (Information)
|
|
118
|
+
*
|
|
119
|
+
* @param severity - Error severity level
|
|
120
|
+
* @returns LSP severity code (1, 2, or 3)
|
|
121
|
+
*/
|
|
122
|
+
function mapSeverityToLsp(severity: ErrorSeverity): 1 | 2 | 3 {
|
|
123
|
+
switch (severity) {
|
|
124
|
+
case 'error':
|
|
125
|
+
return 1;
|
|
126
|
+
case 'warning':
|
|
127
|
+
return 2;
|
|
128
|
+
default:
|
|
129
|
+
// Future-proof: if other severities are added, default to error
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Module Loader
|
|
3
|
+
*
|
|
4
|
+
* Implements module loading for the Rill CLI with circular dependency detection.
|
|
5
|
+
* See docs/integration-modules.md for module convention specification.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs/promises';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as yaml from 'yaml';
|
|
11
|
+
import { parse } from '@rcrsr/rill';
|
|
12
|
+
import { execute, createRuntimeContext } from '@rcrsr/rill';
|
|
13
|
+
import type { RillValue } from '@rcrsr/rill';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load a module and its dependencies recursively.
|
|
17
|
+
*
|
|
18
|
+
* @param specifier - Module path (relative or absolute)
|
|
19
|
+
* @param fromPath - Path of the importing file
|
|
20
|
+
* @param cache - Module cache keyed by canonical path
|
|
21
|
+
* @param chain - Set of paths in current import chain for circular detection
|
|
22
|
+
* @returns Dict of exported values
|
|
23
|
+
* @throws Error if module not found or circular dependency detected
|
|
24
|
+
*/
|
|
25
|
+
export async function loadModule(
|
|
26
|
+
specifier: string,
|
|
27
|
+
fromPath: string,
|
|
28
|
+
cache: Map<string, Record<string, RillValue>>,
|
|
29
|
+
chain: Set<string> = new Set()
|
|
30
|
+
): Promise<Record<string, RillValue>> {
|
|
31
|
+
// Resolve to absolute canonical path
|
|
32
|
+
const absolutePath = path.resolve(path.dirname(fromPath), specifier);
|
|
33
|
+
|
|
34
|
+
// Check for circular dependency
|
|
35
|
+
if (chain.has(absolutePath)) {
|
|
36
|
+
const cycle = [...chain, absolutePath].join(' -> ');
|
|
37
|
+
throw new Error(`Circular dependency detected: ${cycle}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Return cached module if already loaded
|
|
41
|
+
if (cache.has(absolutePath)) {
|
|
42
|
+
return cache.get(absolutePath)!;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if module file exists
|
|
46
|
+
try {
|
|
47
|
+
await fs.access(absolutePath);
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error(`Module not found: ${specifier}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Add to chain to detect cycles in dependencies
|
|
53
|
+
chain.add(absolutePath);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Load and parse module source
|
|
57
|
+
const source = await fs.readFile(absolutePath, 'utf-8');
|
|
58
|
+
const ast = parse(source);
|
|
59
|
+
|
|
60
|
+
// Extract frontmatter (yaml.parse returns null for empty content)
|
|
61
|
+
const frontmatter: Record<string, unknown> = ast.frontmatter
|
|
62
|
+
? ((yaml.parse(ast.frontmatter.content) as Record<
|
|
63
|
+
string,
|
|
64
|
+
unknown
|
|
65
|
+
> | null) ?? {})
|
|
66
|
+
: {};
|
|
67
|
+
|
|
68
|
+
// Resolve dependencies first
|
|
69
|
+
const imports: Record<string, RillValue> = {};
|
|
70
|
+
if (frontmatter['use'] && Array.isArray(frontmatter['use'])) {
|
|
71
|
+
for (const entry of frontmatter['use']) {
|
|
72
|
+
if (typeof entry === 'object' && entry !== null) {
|
|
73
|
+
const [name, depPath] = Object.entries(entry)[0] as [string, string];
|
|
74
|
+
imports[name] = await loadModule(depPath, absolutePath, cache, chain);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Execute module with dependencies
|
|
80
|
+
const ctx = createRuntimeContext({ variables: imports });
|
|
81
|
+
const result = await execute(ast, ctx);
|
|
82
|
+
|
|
83
|
+
// Extract exports
|
|
84
|
+
const exports: Record<string, RillValue> = {};
|
|
85
|
+
const exportList: unknown = frontmatter['export'];
|
|
86
|
+
if (Array.isArray(exportList)) {
|
|
87
|
+
for (const name of exportList) {
|
|
88
|
+
if (typeof name === 'string' && result.variables[name] !== undefined) {
|
|
89
|
+
exports[name] = result.variables[name];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Cache and return
|
|
95
|
+
cache.set(absolutePath, exports);
|
|
96
|
+
return exports;
|
|
97
|
+
} finally {
|
|
98
|
+
// Remove from chain after processing
|
|
99
|
+
chain.delete(absolutePath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Shared Utilities
|
|
3
|
+
* Common formatting functions for CLI tools
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isCallable, VERSION } from '@rcrsr/rill';
|
|
7
|
+
import type { RillValue } from '@rcrsr/rill';
|
|
8
|
+
import { ParseError, RuntimeError } from '@rcrsr/rill';
|
|
9
|
+
import { LexerError } from '@rcrsr/rill';
|
|
10
|
+
import { enrichError, type ScopeInfo } from './cli-error-enrichment.js';
|
|
11
|
+
import {
|
|
12
|
+
formatError as formatEnrichedError,
|
|
13
|
+
type FormatOptions,
|
|
14
|
+
} from './cli-error-formatter.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert execution result to human-readable string
|
|
18
|
+
*
|
|
19
|
+
* @param value - The value to format
|
|
20
|
+
* @returns Formatted string representation
|
|
21
|
+
*/
|
|
22
|
+
export function formatOutput(value: RillValue): string {
|
|
23
|
+
if (value === null) return 'null';
|
|
24
|
+
if (typeof value === 'string') return value;
|
|
25
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
26
|
+
return String(value);
|
|
27
|
+
}
|
|
28
|
+
if (isCallable(value)) return '[closure]';
|
|
29
|
+
return JSON.stringify(value, null, 2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format error for stderr output
|
|
34
|
+
*
|
|
35
|
+
* When source is available, uses enrichment pipeline to add source snippets and suggestions.
|
|
36
|
+
* Otherwise, falls back to simple formatting for backward compatibility.
|
|
37
|
+
*
|
|
38
|
+
* @param err - The error to format
|
|
39
|
+
* @param source - Optional source code for enrichment
|
|
40
|
+
* @param options - Optional format options (defaults to human format)
|
|
41
|
+
* @param scope - Optional scope information for suggestions
|
|
42
|
+
* @returns Formatted error message
|
|
43
|
+
*/
|
|
44
|
+
export function formatError(
|
|
45
|
+
err: Error,
|
|
46
|
+
source?: string,
|
|
47
|
+
options?: Partial<FormatOptions>,
|
|
48
|
+
scope?: ScopeInfo
|
|
49
|
+
): string {
|
|
50
|
+
// IC-12: Use enrichment pipeline when source is available and error is RillError
|
|
51
|
+
if (
|
|
52
|
+
source !== undefined &&
|
|
53
|
+
(err instanceof LexerError ||
|
|
54
|
+
err instanceof ParseError ||
|
|
55
|
+
err instanceof RuntimeError)
|
|
56
|
+
) {
|
|
57
|
+
try {
|
|
58
|
+
const enriched = enrichError(err, source, scope);
|
|
59
|
+
const formatOpts: FormatOptions = {
|
|
60
|
+
format: options?.format ?? 'human',
|
|
61
|
+
verbose: options?.verbose ?? false,
|
|
62
|
+
includeCallStack: options?.includeCallStack ?? false,
|
|
63
|
+
maxCallStackDepth: options?.maxCallStackDepth ?? 10,
|
|
64
|
+
};
|
|
65
|
+
return formatEnrichedError(enriched, formatOpts);
|
|
66
|
+
} catch {
|
|
67
|
+
// If enrichment fails, fall back to simple formatting
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// IC-12: Fallback to existing behavior for backward compatibility
|
|
72
|
+
if (err instanceof LexerError) {
|
|
73
|
+
const location = err.location;
|
|
74
|
+
return `Lexer error at line ${location.line}: ${err.message.replace(/ at \d+:\d+$/, '')}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (err instanceof ParseError) {
|
|
78
|
+
const location = err.location;
|
|
79
|
+
if (location) {
|
|
80
|
+
return `Parse error at line ${location.line}: ${err.message.replace(/ at \d+:\d+$/, '')}`;
|
|
81
|
+
}
|
|
82
|
+
return `Parse error: ${err.message}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (err instanceof RuntimeError) {
|
|
86
|
+
const location = err.location;
|
|
87
|
+
const baseMessage = err.message.replace(/ at \d+:\d+$/, '');
|
|
88
|
+
if (location) {
|
|
89
|
+
return `Runtime error at line ${location.line}: ${baseMessage}`;
|
|
90
|
+
}
|
|
91
|
+
return `Runtime error: ${baseMessage}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle file not found errors (ENOENT)
|
|
95
|
+
if (
|
|
96
|
+
err instanceof Error &&
|
|
97
|
+
'code' in err &&
|
|
98
|
+
err.code === 'ENOENT' &&
|
|
99
|
+
'path' in err
|
|
100
|
+
) {
|
|
101
|
+
return `File not found: ${err.path}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Handle module errors
|
|
105
|
+
if (err.message.includes('Cannot find module')) {
|
|
106
|
+
return `Module error: ${err.message}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return err.message;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Determine exit code from script result
|
|
114
|
+
*
|
|
115
|
+
* Implements exit code semantics per language spec:
|
|
116
|
+
* - true / non-empty string: exit 0
|
|
117
|
+
* - false / empty string: exit 1
|
|
118
|
+
* - [0, "message"]: exit 0 with message
|
|
119
|
+
* - [1, "message"]: exit 1 with message
|
|
120
|
+
*
|
|
121
|
+
* @param value - The script return value
|
|
122
|
+
* @returns Exit code and optional message
|
|
123
|
+
*/
|
|
124
|
+
export function determineExitCode(value: RillValue): {
|
|
125
|
+
code: number;
|
|
126
|
+
message?: string;
|
|
127
|
+
} {
|
|
128
|
+
// Handle tuple format: [code, message]
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
if (value.length >= 2) {
|
|
131
|
+
const code = value[0];
|
|
132
|
+
const message = value[1];
|
|
133
|
+
|
|
134
|
+
// Validate code is 0 or 1
|
|
135
|
+
if (typeof code === 'number' && (code === 0 || code === 1)) {
|
|
136
|
+
// Return with message if provided as string
|
|
137
|
+
if (typeof message === 'string' && message !== '') {
|
|
138
|
+
return { code, message };
|
|
139
|
+
}
|
|
140
|
+
return { code };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Non-conforming array: treat as truthy (exit 0)
|
|
144
|
+
return { code: 0 };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Boolean values
|
|
148
|
+
if (typeof value === 'boolean') {
|
|
149
|
+
return { code: value ? 0 : 1 };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// String values
|
|
153
|
+
if (typeof value === 'string') {
|
|
154
|
+
return { code: value === '' ? 1 : 0 };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// All other values (number, dict, closure, etc.) are truthy: exit 0
|
|
158
|
+
return { code: 0 };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Detect help or version flags in CLI argument array.
|
|
163
|
+
* Checks for --help, -h, --version, -v in any position.
|
|
164
|
+
*
|
|
165
|
+
* @param argv - Command-line arguments (process.argv.slice(2))
|
|
166
|
+
* @returns Object with mode if flag found, null otherwise
|
|
167
|
+
*/
|
|
168
|
+
export function detectHelpVersionFlag(
|
|
169
|
+
argv: string[]
|
|
170
|
+
): { mode: 'help' | 'version' } | null {
|
|
171
|
+
// Help takes precedence over version
|
|
172
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
173
|
+
return { mode: 'help' };
|
|
174
|
+
}
|
|
175
|
+
if (argv.includes('--version') || argv.includes('-v')) {
|
|
176
|
+
return { mode: 'version' };
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Package version string (re-exported from version-data.ts)
|
|
183
|
+
*
|
|
184
|
+
* This replaces the previous async readVersion() function with a synchronous constant.
|
|
185
|
+
* The version is now generated at build time by packages/core/scripts/generate-version.ts.
|
|
186
|
+
*/
|
|
187
|
+
export { VERSION };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for parseCheckArgs function
|
|
3
|
+
*
|
|
4
|
+
* Test Coverage Matrix (maps TCs to specification requirements):
|
|
5
|
+
* TC-1: --help flag returns help mode [AC-S6]
|
|
6
|
+
* TC-2: --version flag returns version mode [AC-S5]
|
|
7
|
+
* TC-3: Unknown flag throws error [EC-1]
|
|
8
|
+
* TC-4: Missing file throws error [EC-2]
|
|
9
|
+
* TC-5: --fix flag parsed correctly [IR-2]
|
|
10
|
+
* TC-6: --verbose flag parsed correctly [IR-2]
|
|
11
|
+
* TC-7: --format text parsed correctly [IR-2]
|
|
12
|
+
* TC-8: --format json parsed correctly [IR-2]
|
|
13
|
+
* TC-9: --format with invalid value throws error [EC-1]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { parseCheckArgs } from '../../src/cli-check.js';
|
|
18
|
+
|
|
19
|
+
describe('parseCheckArgs', () => {
|
|
20
|
+
describe('help and version modes', () => {
|
|
21
|
+
it('returns help mode when --help flag present [TC-1]', () => {
|
|
22
|
+
const result = parseCheckArgs(['--help']);
|
|
23
|
+
expect(result).toEqual({ mode: 'help' });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns help mode when -h flag present [TC-1]', () => {
|
|
27
|
+
const result = parseCheckArgs(['-h']);
|
|
28
|
+
expect(result).toEqual({ mode: 'help' });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns help mode when --help flag present with other args [TC-1]', () => {
|
|
32
|
+
const result = parseCheckArgs(['file.rill', '--help', '--fix']);
|
|
33
|
+
expect(result).toEqual({ mode: 'help' });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns version mode when --version flag present [TC-2]', () => {
|
|
37
|
+
const result = parseCheckArgs(['--version']);
|
|
38
|
+
expect(result).toEqual({ mode: 'version' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns version mode when -v flag present [TC-2]', () => {
|
|
42
|
+
const result = parseCheckArgs(['-v']);
|
|
43
|
+
expect(result).toEqual({ mode: 'version' });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns version mode when --version flag present with other args [TC-2]', () => {
|
|
47
|
+
const result = parseCheckArgs(['file.rill', '--version', '--fix']);
|
|
48
|
+
expect(result).toEqual({ mode: 'version' });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('error cases', () => {
|
|
53
|
+
it('throws error for unknown flag [TC-3]', () => {
|
|
54
|
+
expect(() => parseCheckArgs(['--unknown', 'file.rill'])).toThrow(
|
|
55
|
+
'Unknown option: --unknown'
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('throws error for unknown short flag [TC-3]', () => {
|
|
60
|
+
expect(() => parseCheckArgs(['-x', 'file.rill'])).toThrow(
|
|
61
|
+
'Unknown option: -x'
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('throws error when file argument missing [TC-4]', () => {
|
|
66
|
+
expect(() => parseCheckArgs(['--fix'])).toThrow('Missing file argument');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('throws error when only flags provided [TC-4]', () => {
|
|
70
|
+
expect(() => parseCheckArgs(['--fix', '--verbose'])).toThrow(
|
|
71
|
+
'Missing file argument'
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('throws error when --format has no value [TC-9]', () => {
|
|
76
|
+
expect(() => parseCheckArgs(['file.rill', '--format'])).toThrow(
|
|
77
|
+
'--format requires argument: text or json'
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('throws error when --format value is another flag [TC-9]', () => {
|
|
82
|
+
expect(() => parseCheckArgs(['file.rill', '--format', '--fix'])).toThrow(
|
|
83
|
+
'--format requires argument: text or json'
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('throws error when --format value is invalid [TC-9]', () => {
|
|
88
|
+
expect(() => parseCheckArgs(['file.rill', '--format', 'xml'])).toThrow(
|
|
89
|
+
'Invalid format: xml. Expected text or json'
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('check mode parsing', () => {
|
|
95
|
+
it('parses file path correctly [TC-4]', () => {
|
|
96
|
+
const result = parseCheckArgs(['test.rill']);
|
|
97
|
+
expect(result).toEqual({
|
|
98
|
+
mode: 'check',
|
|
99
|
+
file: 'test.rill',
|
|
100
|
+
fix: false,
|
|
101
|
+
verbose: false,
|
|
102
|
+
format: 'text',
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('parses --fix flag correctly [TC-5]', () => {
|
|
107
|
+
const result = parseCheckArgs(['test.rill', '--fix']);
|
|
108
|
+
expect(result).toEqual({
|
|
109
|
+
mode: 'check',
|
|
110
|
+
file: 'test.rill',
|
|
111
|
+
fix: true,
|
|
112
|
+
verbose: false,
|
|
113
|
+
format: 'text',
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('parses --verbose flag correctly [TC-6]', () => {
|
|
118
|
+
const result = parseCheckArgs(['test.rill', '--verbose']);
|
|
119
|
+
expect(result).toEqual({
|
|
120
|
+
mode: 'check',
|
|
121
|
+
file: 'test.rill',
|
|
122
|
+
fix: false,
|
|
123
|
+
verbose: true,
|
|
124
|
+
format: 'text',
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('parses --format text correctly [TC-7]', () => {
|
|
129
|
+
const result = parseCheckArgs(['test.rill', '--format', 'text']);
|
|
130
|
+
expect(result).toEqual({
|
|
131
|
+
mode: 'check',
|
|
132
|
+
file: 'test.rill',
|
|
133
|
+
fix: false,
|
|
134
|
+
verbose: false,
|
|
135
|
+
format: 'text',
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('parses --format json correctly [TC-8]', () => {
|
|
140
|
+
const result = parseCheckArgs(['test.rill', '--format', 'json']);
|
|
141
|
+
expect(result).toEqual({
|
|
142
|
+
mode: 'check',
|
|
143
|
+
file: 'test.rill',
|
|
144
|
+
fix: false,
|
|
145
|
+
verbose: false,
|
|
146
|
+
format: 'json',
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('parses multiple flags together [TC-5, TC-6, TC-7]', () => {
|
|
151
|
+
const result = parseCheckArgs([
|
|
152
|
+
'test.rill',
|
|
153
|
+
'--fix',
|
|
154
|
+
'--verbose',
|
|
155
|
+
'--format',
|
|
156
|
+
'json',
|
|
157
|
+
]);
|
|
158
|
+
expect(result).toEqual({
|
|
159
|
+
mode: 'check',
|
|
160
|
+
file: 'test.rill',
|
|
161
|
+
fix: true,
|
|
162
|
+
verbose: true,
|
|
163
|
+
format: 'json',
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('extracts file path when mixed with flags', () => {
|
|
168
|
+
const result = parseCheckArgs(['--fix', 'test.rill', '--verbose']);
|
|
169
|
+
expect(result).toEqual({
|
|
170
|
+
mode: 'check',
|
|
171
|
+
file: 'test.rill',
|
|
172
|
+
fix: true,
|
|
173
|
+
verbose: true,
|
|
174
|
+
format: 'text',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('uses default format when not specified', () => {
|
|
179
|
+
const result = parseCheckArgs(['test.rill']);
|
|
180
|
+
expect(result).toEqual({
|
|
181
|
+
mode: 'check',
|
|
182
|
+
file: 'test.rill',
|
|
183
|
+
fix: false,
|
|
184
|
+
verbose: false,
|
|
185
|
+
format: 'text',
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|