@side-quest/kit 0.0.0 → 0.2.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 (127) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +54 -352
  3. package/dist/cli.d.ts +14 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +156 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/index.d.ts +8 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +8 -2509
  10. package/dist/index.js.map +1 -0
  11. package/dist/lib/ast/index.d.ts +11 -0
  12. package/dist/lib/ast/index.d.ts.map +1 -0
  13. package/dist/lib/ast/index.js +15 -0
  14. package/dist/lib/ast/index.js.map +1 -0
  15. package/dist/lib/ast/languages.d.ts +55 -0
  16. package/dist/lib/ast/languages.d.ts.map +1 -0
  17. package/dist/lib/ast/languages.js +146 -0
  18. package/dist/lib/ast/languages.js.map +1 -0
  19. package/dist/lib/ast/pattern.d.ts +84 -0
  20. package/dist/lib/ast/pattern.d.ts.map +1 -0
  21. package/dist/lib/ast/pattern.js +268 -0
  22. package/dist/lib/ast/pattern.js.map +1 -0
  23. package/dist/lib/ast/searcher.d.ts +89 -0
  24. package/dist/lib/ast/searcher.d.ts.map +1 -0
  25. package/dist/lib/ast/searcher.js +316 -0
  26. package/dist/lib/ast/searcher.js.map +1 -0
  27. package/dist/lib/ast/types.d.ts +93 -0
  28. package/dist/lib/ast/types.d.ts.map +1 -0
  29. package/dist/lib/ast/types.js +23 -0
  30. package/dist/lib/ast/types.js.map +1 -0
  31. package/dist/lib/commands/callers.d.ts +20 -0
  32. package/dist/lib/commands/callers.d.ts.map +1 -0
  33. package/dist/lib/commands/callers.js +162 -0
  34. package/dist/lib/commands/callers.js.map +1 -0
  35. package/dist/lib/commands/find.d.ts +15 -0
  36. package/dist/lib/commands/find.d.ts.map +1 -0
  37. package/dist/lib/commands/find.js +113 -0
  38. package/dist/lib/commands/find.js.map +1 -0
  39. package/dist/lib/commands/overview.d.ts +6 -0
  40. package/dist/lib/commands/overview.d.ts.map +1 -0
  41. package/dist/lib/commands/overview.js +52 -0
  42. package/dist/lib/commands/overview.js.map +1 -0
  43. package/dist/lib/commands/prime.d.ts +16 -0
  44. package/dist/lib/commands/prime.d.ts.map +1 -0
  45. package/dist/lib/commands/prime.js +168 -0
  46. package/dist/lib/commands/prime.js.map +1 -0
  47. package/dist/lib/commands/search.d.ts +20 -0
  48. package/dist/lib/commands/search.d.ts.map +1 -0
  49. package/dist/lib/commands/search.js +111 -0
  50. package/dist/lib/commands/search.js.map +1 -0
  51. package/dist/lib/errors.d.ts +80 -0
  52. package/dist/lib/errors.d.ts.map +1 -0
  53. package/dist/lib/errors.js +189 -0
  54. package/dist/lib/errors.js.map +1 -0
  55. package/dist/lib/formatters/output.d.ts +5 -0
  56. package/dist/lib/formatters/output.d.ts.map +1 -0
  57. package/dist/lib/formatters/output.js +5 -0
  58. package/dist/lib/formatters/output.js.map +1 -0
  59. package/dist/lib/formatters.d.ts +29 -0
  60. package/dist/lib/formatters.d.ts.map +1 -0
  61. package/dist/lib/formatters.js +141 -0
  62. package/dist/lib/formatters.js.map +1 -0
  63. package/dist/lib/index-tools.d.ts +108 -0
  64. package/dist/lib/index-tools.d.ts.map +1 -0
  65. package/dist/lib/index-tools.js +311 -0
  66. package/dist/lib/index-tools.js.map +1 -0
  67. package/dist/lib/index.d.ts +21 -0
  68. package/dist/lib/index.d.ts.map +1 -0
  69. package/dist/lib/index.js +42 -0
  70. package/dist/lib/index.js.map +1 -0
  71. package/dist/lib/kit-wrapper.d.ts +70 -0
  72. package/dist/lib/kit-wrapper.d.ts.map +1 -0
  73. package/dist/lib/kit-wrapper.js +462 -0
  74. package/dist/lib/kit-wrapper.js.map +1 -0
  75. package/dist/lib/logger.d.ts +28 -0
  76. package/dist/lib/logger.d.ts.map +1 -0
  77. package/dist/lib/logger.js +39 -0
  78. package/dist/lib/logger.js.map +1 -0
  79. package/dist/lib/types.d.ts +179 -0
  80. package/dist/lib/types.d.ts.map +1 -0
  81. package/dist/lib/types.js +48 -0
  82. package/dist/lib/types.js.map +1 -0
  83. package/dist/lib/utils/args.d.ts +40 -0
  84. package/dist/lib/utils/args.d.ts.map +1 -0
  85. package/dist/lib/utils/args.js +58 -0
  86. package/dist/lib/utils/args.js.map +1 -0
  87. package/dist/lib/utils/git.d.ts +23 -0
  88. package/dist/lib/utils/git.d.ts.map +1 -0
  89. package/dist/lib/utils/git.js +50 -0
  90. package/dist/lib/utils/git.js.map +1 -0
  91. package/dist/lib/utils/index-parser.d.ts +155 -0
  92. package/dist/lib/utils/index-parser.d.ts.map +1 -0
  93. package/dist/lib/utils/index-parser.js +252 -0
  94. package/dist/lib/utils/index-parser.js.map +1 -0
  95. package/dist/lib/validators.d.ts +138 -0
  96. package/dist/lib/validators.d.ts.map +1 -0
  97. package/dist/lib/validators.js +302 -0
  98. package/dist/lib/validators.js.map +1 -0
  99. package/dist/mcp/index.d.ts +19 -0
  100. package/dist/mcp/index.d.ts.map +1 -0
  101. package/dist/mcp/index.js +769 -0
  102. package/dist/mcp/index.js.map +1 -0
  103. package/package.json +5 -2
  104. package/src/cli.ts +170 -0
  105. package/src/lib/ast/index.ts +32 -0
  106. package/src/lib/ast/languages.ts +172 -0
  107. package/src/lib/ast/pattern.ts +299 -0
  108. package/src/lib/ast/searcher.ts +381 -0
  109. package/src/lib/ast/types.ts +99 -0
  110. package/src/lib/commands/callers.ts +226 -0
  111. package/src/lib/commands/find.ts +159 -0
  112. package/src/lib/commands/overview.ts +73 -0
  113. package/src/lib/commands/prime.ts +271 -0
  114. package/src/lib/commands/search.ts +146 -0
  115. package/src/lib/errors.ts +221 -0
  116. package/src/lib/formatters/output.ts +9 -0
  117. package/src/lib/formatters.ts +189 -0
  118. package/src/lib/index-tools.ts +471 -0
  119. package/src/lib/index.ts +122 -0
  120. package/src/lib/kit-wrapper.ts +675 -0
  121. package/src/lib/logger.ts +57 -0
  122. package/src/lib/types.ts +228 -0
  123. package/src/lib/utils/args.ts +72 -0
  124. package/src/lib/utils/git.ts +65 -0
  125. package/src/lib/utils/index-parser.ts +350 -0
  126. package/src/lib/validators.ts +437 -0
  127. package/src/mcp/index.ts +144 -79
@@ -0,0 +1,99 @@
1
+ /**
2
+ * AST Search Types
3
+ *
4
+ * Type definitions for AST-based code search using tree-sitter.
5
+ */
6
+
7
+ /**
8
+ * Search mode for AST patterns.
9
+ *
10
+ * - `simple`: Natural language patterns like "async function", "class"
11
+ * - `pattern`: JSON object criteria like {"type": "function_declaration"}
12
+ */
13
+ export enum SearchMode {
14
+ /** Natural language patterns: "async function", "class", "try" */
15
+ SIMPLE = 'simple',
16
+ /** JSON criteria: {"type": "function_declaration", "async": true} */
17
+ PATTERN = 'pattern',
18
+ }
19
+
20
+ /**
21
+ * A single AST match found during search.
22
+ */
23
+ export interface ASTMatch {
24
+ /** File path relative to repository root */
25
+ file: string
26
+ /** Line number (1-indexed) */
27
+ line: number
28
+ /** Column number (0-indexed) */
29
+ column: number
30
+ /** Tree-sitter node type (e.g., "function_declaration") */
31
+ nodeType: string
32
+ /** Matched source text (truncated to 500 chars) */
33
+ text: string
34
+ /** Context about where the match was found */
35
+ context: ASTMatchContext
36
+ }
37
+
38
+ /**
39
+ * Context information for an AST match.
40
+ */
41
+ export interface ASTMatchContext {
42
+ /** The tree-sitter node type */
43
+ nodeType: string
44
+ /** Parent function name, if match is inside a function */
45
+ parentFunction?: string
46
+ /** Parent class name, if match is inside a class */
47
+ parentClass?: string
48
+ }
49
+
50
+ /**
51
+ * Options for AST search.
52
+ */
53
+ export interface ASTSearchOptions {
54
+ /** Search pattern (natural language or JSON depending on mode) */
55
+ pattern: string
56
+ /** Search mode: 'simple' or 'pattern' */
57
+ mode?: SearchMode
58
+ /** File glob pattern (default: all supported files) */
59
+ filePattern?: string
60
+ /** Repository path to search */
61
+ path?: string
62
+ /** Maximum results to return (default: 100) */
63
+ maxResults?: number
64
+ }
65
+
66
+ /**
67
+ * Result of an AST search operation.
68
+ */
69
+ export interface ASTSearchResult {
70
+ /** Number of matches found */
71
+ count: number
72
+ /** Array of AST matches */
73
+ matches: ASTMatch[]
74
+ /** The search pattern used */
75
+ pattern: string
76
+ /** The search mode used */
77
+ mode: SearchMode
78
+ /** Repository path searched */
79
+ path: string
80
+ }
81
+
82
+ /**
83
+ * Criteria for pattern mode matching.
84
+ */
85
+ export interface PatternCriteria {
86
+ /** Tree-sitter node type to match */
87
+ type?: string
88
+ /** Whether node should be async */
89
+ async?: boolean
90
+ /** Text that must appear in the node */
91
+ textMatch?: string
92
+ /** Name of the symbol to match */
93
+ name?: string
94
+ }
95
+
96
+ /**
97
+ * Timeout for AST search operations (ms).
98
+ */
99
+ export const AST_SEARCH_TIMEOUT = 60000
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Callers command - Find who calls a function
3
+ *
4
+ * Uses kit grep to find all call sites of a function, filtering out the
5
+ * definition itself to show only actual usage locations.
6
+ */
7
+
8
+ import { color, OutputFormat } from '../formatters/output'
9
+ import { executeKitGrep } from '../kit-wrapper'
10
+ import { findSymbol, loadProjectIndex } from '../utils/index-parser'
11
+
12
+ /**
13
+ * Call site result - a location where the function is called
14
+ */
15
+ interface CallSite {
16
+ file: string
17
+ line: number
18
+ context: string
19
+ }
20
+
21
+ /**
22
+ * Result of callers search
23
+ */
24
+ interface CallersResult {
25
+ functionName: string
26
+ definitionFile?: string
27
+ definitionLine?: number
28
+ callSites: CallSite[]
29
+ count: number
30
+ }
31
+
32
+ /**
33
+ * Format callers result as markdown
34
+ *
35
+ * @param result - Callers result to format
36
+ * @returns Formatted markdown string
37
+ */
38
+ function formatMarkdown(result: CallersResult): string {
39
+ const { functionName, definitionFile, definitionLine, callSites, count } =
40
+ result
41
+
42
+ if (count === 0) {
43
+ return color(
44
+ 'yellow',
45
+ `\n⚠️ No call sites found for function: ${functionName}\n`,
46
+ )
47
+ }
48
+
49
+ let output = color('cyan', `\n📞 Found ${count} call site(s) for: `)
50
+ output += color('blue', functionName)
51
+ output += '\n\n'
52
+
53
+ // Show definition location if available
54
+ if (definitionFile && definitionLine) {
55
+ output += color(
56
+ 'dim',
57
+ `Definition: ${definitionFile}:${definitionLine}\n\n`,
58
+ )
59
+ }
60
+
61
+ // Group by file
62
+ const byFile = new Map<string, CallSite[]>()
63
+ for (const callSite of callSites) {
64
+ if (!byFile.has(callSite.file)) {
65
+ byFile.set(callSite.file, [])
66
+ }
67
+ byFile.get(callSite.file)?.push(callSite)
68
+ }
69
+
70
+ // Output each file
71
+ for (const [file, sites] of byFile.entries()) {
72
+ output += color('dim', `${file}:\n`)
73
+ for (const site of sites) {
74
+ output += ` ${color('dim', `L${site.line}:`)} ${site.context.trim()}\n`
75
+ }
76
+ output += '\n'
77
+ }
78
+
79
+ return output
80
+ }
81
+
82
+ /**
83
+ * Format callers result as JSON
84
+ *
85
+ * @param result - Callers result to format
86
+ * @returns JSON string
87
+ */
88
+ function formatJSON(result: CallersResult): string {
89
+ return JSON.stringify(result, null, 2)
90
+ }
91
+
92
+ /**
93
+ * Execute callers command
94
+ *
95
+ * Finds all locations where a function is called by:
96
+ * 1. Looking up the function definition in PROJECT_INDEX.json
97
+ * 2. Using kit grep to find all occurrences
98
+ * 3. Filtering out the definition line to show only call sites
99
+ *
100
+ * @param functionName - Name of the function to find callers for
101
+ * @param format - Output format (markdown or JSON)
102
+ */
103
+ export async function executeCallers(
104
+ functionName: string,
105
+ format: OutputFormat,
106
+ ): Promise<void> {
107
+ try {
108
+ // Find the function definition in the index
109
+ let definitionFile: string | undefined
110
+ let definitionLine: number | undefined
111
+
112
+ try {
113
+ const index = await loadProjectIndex()
114
+ const definitions = findSymbol(index, functionName)
115
+
116
+ // Use the first definition found (functions should be unique)
117
+ const firstDef = definitions[0]
118
+ if (firstDef) {
119
+ definitionFile = firstDef.file
120
+ definitionLine = firstDef.symbol.start_line
121
+ }
122
+ } catch {
123
+ // Index not available - we'll proceed without filtering the definition
124
+ }
125
+
126
+ // Use kit grep to find all occurrences
127
+ const grepResult = executeKitGrep({
128
+ pattern: functionName,
129
+ caseSensitive: true,
130
+ maxResults: 500,
131
+ })
132
+
133
+ // Handle errors
134
+ if ('error' in grepResult) {
135
+ if (format === OutputFormat.JSON) {
136
+ console.error(
137
+ JSON.stringify(
138
+ {
139
+ error: grepResult.error,
140
+ functionName,
141
+ isError: true,
142
+ },
143
+ null,
144
+ 2,
145
+ ),
146
+ )
147
+ } else {
148
+ console.error(color('red', '\n❌ Error:'), grepResult.error, '\n')
149
+ }
150
+ process.exit(1)
151
+ }
152
+
153
+ // Filter matches to exclude the definition line
154
+ let matches = grepResult.matches
155
+
156
+ if (definitionFile && definitionLine) {
157
+ matches = matches.filter(
158
+ (match) =>
159
+ !(match.file === definitionFile && match.line === definitionLine),
160
+ )
161
+ }
162
+
163
+ // Filter out matches that look like definitions rather than calls
164
+ // Heuristics:
165
+ // - Lines starting with "function ", "const ", "let ", "var ", "export function"
166
+ // - Lines with "= function" or "= (" (function assignments)
167
+ // - Lines with "export async function" (exported async functions)
168
+ matches = matches.filter((match) => {
169
+ const trimmed = match.content.trim()
170
+ const definitionPatterns = [
171
+ /^function\s+/, // function declarations
172
+ /^export\s+(async\s+)?function\s+/, // exported functions (sync or async)
173
+ /^(const|let|var)\s+\w+\s*=\s*function/, // function expressions
174
+ /^(const|let|var)\s+\w+\s*=\s*\(/, // arrow functions
175
+ /^(const|let|var)\s+\w+\s*=\s*async\s*\(/, // async arrow functions
176
+ /^async\s+function\s+/, // async function declarations
177
+ ]
178
+
179
+ return !definitionPatterns.some((pattern) => pattern.test(trimmed))
180
+ })
181
+
182
+ // Convert to call sites
183
+ const callSites: CallSite[] = matches.map((match) => ({
184
+ file: match.file,
185
+ line: match.line || 0,
186
+ context: match.content,
187
+ }))
188
+
189
+ // Build result
190
+ const result: CallersResult = {
191
+ functionName,
192
+ definitionFile,
193
+ definitionLine,
194
+ callSites,
195
+ count: callSites.length,
196
+ }
197
+
198
+ // Output results
199
+ if (format === OutputFormat.JSON) {
200
+ console.log(formatJSON(result))
201
+ } else {
202
+ console.log(formatMarkdown(result))
203
+ }
204
+ } catch (error) {
205
+ if (format === OutputFormat.JSON) {
206
+ console.error(
207
+ JSON.stringify(
208
+ {
209
+ error: error instanceof Error ? error.message : 'Unknown error',
210
+ functionName,
211
+ isError: true,
212
+ },
213
+ null,
214
+ 2,
215
+ ),
216
+ )
217
+ } else {
218
+ console.error(
219
+ color('red', '\n❌ Error:'),
220
+ error instanceof Error ? error.message : error,
221
+ '\n',
222
+ )
223
+ }
224
+ process.exit(1)
225
+ }
226
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Find command - Locate symbol definitions by name
3
+ *
4
+ * Searches PROJECT_INDEX.json for symbol definitions with exact or fuzzy matching.
5
+ * Supports dual output formats (markdown table or JSON).
6
+ */
7
+
8
+ import { color, OutputFormat } from '../formatters/output'
9
+ import {
10
+ findSymbol,
11
+ findSymbolFuzzy,
12
+ type Symbol as IndexSymbol,
13
+ loadProjectIndex,
14
+ } from '../utils/index-parser'
15
+
16
+ /**
17
+ * Format symbol results as markdown table
18
+ */
19
+ function formatMarkdown(
20
+ results: Array<{ file: string; symbol: IndexSymbol }>,
21
+ query: string,
22
+ ): string {
23
+ if (results.length === 0) {
24
+ return color('yellow', `\n⚠️ No symbols found matching: ${query}\n`)
25
+ }
26
+
27
+ let output = color('cyan', `\n📍 Found ${results.length} symbol(s)\n\n`)
28
+
29
+ // Group by file
30
+ const byFile = new Map<string, IndexSymbol[]>()
31
+ for (const { file, symbol } of results) {
32
+ if (!byFile.has(file)) {
33
+ byFile.set(file, [])
34
+ }
35
+ byFile.get(file)?.push(symbol)
36
+ }
37
+
38
+ // Output each file
39
+ for (const [file, symbols] of byFile.entries()) {
40
+ output += color('dim', `${file}:\n`)
41
+ for (const symbol of symbols) {
42
+ const typeColor = getTypeColor(symbol.type)
43
+ output += ` ${color('dim', '•')} ${color(typeColor, symbol.type.padEnd(10))} ${color('blue', symbol.name)} ${color('dim', `(line ${symbol.start_line}`)}\n`
44
+ }
45
+ output += '\n'
46
+ }
47
+
48
+ return output
49
+ }
50
+
51
+ /**
52
+ * Get color for symbol type
53
+ */
54
+ function getTypeColor(
55
+ type: string,
56
+ ): 'blue' | 'green' | 'magenta' | 'cyan' | 'yellow' {
57
+ switch (type) {
58
+ case 'function':
59
+ return 'blue'
60
+ case 'class':
61
+ return 'green'
62
+ case 'interface':
63
+ case 'type':
64
+ return 'magenta'
65
+ case 'constant':
66
+ return 'cyan'
67
+ default:
68
+ return 'yellow'
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Format symbol results as JSON
74
+ */
75
+ function formatJSON(
76
+ results: Array<{ file: string; symbol: IndexSymbol }>,
77
+ query: string,
78
+ ): string {
79
+ return JSON.stringify(
80
+ {
81
+ query,
82
+ count: results.length,
83
+ results: results.map(({ file, symbol }) => ({
84
+ file,
85
+ name: symbol.name,
86
+ type: symbol.type,
87
+ line: symbol.start_line,
88
+ code: symbol.code,
89
+ })),
90
+ },
91
+ null,
92
+ 2,
93
+ )
94
+ }
95
+
96
+ /**
97
+ * Execute find command
98
+ *
99
+ * @param symbol - Symbol name to search for
100
+ * @param format - Output format (markdown or JSON)
101
+ */
102
+ export async function executeFind(
103
+ symbol: string,
104
+ format: OutputFormat,
105
+ ): Promise<void> {
106
+ try {
107
+ const index = await loadProjectIndex()
108
+
109
+ // Try exact match first
110
+ let results: Array<{ file: string; symbol: IndexSymbol }> = findSymbol(
111
+ index,
112
+ symbol,
113
+ )
114
+
115
+ // If no exact match, try fuzzy search
116
+ if (results.length === 0) {
117
+ const fuzzyResults = findSymbolFuzzy(index, symbol)
118
+ if (fuzzyResults.length > 0 && format === OutputFormat.MARKDOWN) {
119
+ console.log(
120
+ color(
121
+ 'yellow',
122
+ `\n⚠️ No exact match for "${symbol}". Showing fuzzy matches:\n`,
123
+ ),
124
+ )
125
+ }
126
+ results = fuzzyResults.map(({ file, symbol: sym }) => ({
127
+ file,
128
+ symbol: sym,
129
+ }))
130
+ }
131
+
132
+ // Output results
133
+ if (format === OutputFormat.JSON) {
134
+ console.log(formatJSON(results, symbol))
135
+ } else {
136
+ console.log(formatMarkdown(results, symbol))
137
+ }
138
+ } catch (error) {
139
+ if (format === OutputFormat.JSON) {
140
+ console.error(
141
+ JSON.stringify(
142
+ {
143
+ error: error instanceof Error ? error.message : 'Unknown error',
144
+ isError: true,
145
+ },
146
+ null,
147
+ 2,
148
+ ),
149
+ )
150
+ } else {
151
+ console.error(
152
+ color('red', '\n❌ Error:'),
153
+ error instanceof Error ? error.message : error,
154
+ )
155
+ }
156
+
157
+ process.exit(1)
158
+ }
159
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Overview command - List all symbols in a file
3
+ */
4
+
5
+ import { color, OutputFormat } from '../formatters/output'
6
+ import {
7
+ getFileSymbols,
8
+ type Symbol as IndexSymbol,
9
+ loadProjectIndex,
10
+ } from '../utils/index-parser'
11
+
12
+ export async function executeOverview(
13
+ file: string,
14
+ format: OutputFormat,
15
+ ): Promise<void> {
16
+ try {
17
+ const index = await loadProjectIndex()
18
+ const symbols = getFileSymbols(index, file)
19
+
20
+ if (symbols.length === 0) {
21
+ if (format === OutputFormat.JSON) {
22
+ console.log(JSON.stringify({ file, symbols: [] }, null, 2))
23
+ } else {
24
+ console.log(color('yellow', `\n⚠️ No symbols found in: ${file}\n`))
25
+ }
26
+ return
27
+ }
28
+
29
+ if (format === OutputFormat.JSON) {
30
+ console.log(
31
+ JSON.stringify({ file, count: symbols.length, symbols }, null, 2),
32
+ )
33
+ } else {
34
+ // Group by type
35
+ const grouped = symbols.reduce(
36
+ (acc, sym) => {
37
+ if (!acc[sym.type]) acc[sym.type] = []
38
+ acc[sym.type]?.push(sym)
39
+ return acc
40
+ },
41
+ {} as Record<string, IndexSymbol[]>,
42
+ )
43
+
44
+ console.log(color('cyan', `\n📄 ${file}\n`))
45
+ console.log(color('dim', `Total symbols: ${symbols.length}\n`))
46
+
47
+ for (const [type, syms] of Object.entries(grouped)) {
48
+ console.log(color('magenta', `${type}s:`))
49
+ for (const sym of syms) {
50
+ console.log(
51
+ ` ${color('dim', '•')} ${color('blue', sym.name)} ${color('dim', `(line ${sym.start_line})`)}`,
52
+ )
53
+ }
54
+ console.log('')
55
+ }
56
+ }
57
+ } catch (error) {
58
+ console.error(
59
+ format === OutputFormat.JSON
60
+ ? JSON.stringify(
61
+ {
62
+ error: error instanceof Error ? error.message : 'Unknown error',
63
+ isError: true,
64
+ },
65
+ null,
66
+ 2,
67
+ )
68
+ : color('red', '\n❌ Error:') +
69
+ ` ${error instanceof Error ? error.message : error}`,
70
+ )
71
+ process.exit(1)
72
+ }
73
+ }