@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,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
+ });