@grafema/cli 0.1.1-alpha
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 +190 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +36 -0
- package/dist/commands/analyze.d.ts +6 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +209 -0
- package/dist/commands/check.d.ts +10 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +295 -0
- package/dist/commands/coverage.d.ts +11 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +96 -0
- package/dist/commands/explore.d.ts +6 -0
- package/dist/commands/explore.d.ts.map +1 -0
- package/dist/commands/explore.js +633 -0
- package/dist/commands/get.d.ts +10 -0
- package/dist/commands/get.d.ts.map +1 -0
- package/dist/commands/get.js +189 -0
- package/dist/commands/impact.d.ts +10 -0
- package/dist/commands/impact.d.ts.map +1 -0
- package/dist/commands/impact.js +313 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +94 -0
- package/dist/commands/overview.d.ts +6 -0
- package/dist/commands/overview.d.ts.map +1 -0
- package/dist/commands/overview.js +91 -0
- package/dist/commands/query.d.ts +13 -0
- package/dist/commands/query.d.ts.map +1 -0
- package/dist/commands/query.js +340 -0
- package/dist/commands/server.d.ts +11 -0
- package/dist/commands/server.d.ts.map +1 -0
- package/dist/commands/server.js +300 -0
- package/dist/commands/stats.d.ts +6 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +52 -0
- package/dist/commands/trace.d.ts +10 -0
- package/dist/commands/trace.d.ts.map +1 -0
- package/dist/commands/trace.js +270 -0
- package/dist/utils/codePreview.d.ts +28 -0
- package/dist/utils/codePreview.d.ts.map +1 -0
- package/dist/utils/codePreview.js +51 -0
- package/dist/utils/errorFormatter.d.ts +24 -0
- package/dist/utils/errorFormatter.d.ts.map +1 -0
- package/dist/utils/errorFormatter.js +32 -0
- package/dist/utils/formatNode.d.ts +53 -0
- package/dist/utils/formatNode.d.ts.map +1 -0
- package/dist/utils/formatNode.js +49 -0
- package/package.json +54 -0
- package/src/cli.ts +41 -0
- package/src/commands/analyze.ts +271 -0
- package/src/commands/check.ts +379 -0
- package/src/commands/coverage.ts +108 -0
- package/src/commands/explore.tsx +1056 -0
- package/src/commands/get.ts +265 -0
- package/src/commands/impact.ts +400 -0
- package/src/commands/init.ts +112 -0
- package/src/commands/overview.ts +108 -0
- package/src/commands/query.ts +425 -0
- package/src/commands/server.ts +335 -0
- package/src/commands/stats.ts +58 -0
- package/src/commands/trace.ts +341 -0
- package/src/utils/codePreview.ts +77 -0
- package/src/utils/errorFormatter.ts +35 -0
- package/src/utils/formatNode.ts +88 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace command - Data flow analysis
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* grafema trace "userId from authenticate"
|
|
6
|
+
* grafema trace "config"
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import { resolve, join } from 'path';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { RFDBServerBackend, parseSemanticId } from '@grafema/core';
|
|
13
|
+
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
14
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
15
|
+
|
|
16
|
+
interface TraceOptions {
|
|
17
|
+
project: string;
|
|
18
|
+
json?: boolean;
|
|
19
|
+
depth: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface NodeInfo {
|
|
23
|
+
id: string;
|
|
24
|
+
type: string;
|
|
25
|
+
name: string;
|
|
26
|
+
file: string;
|
|
27
|
+
line?: number;
|
|
28
|
+
value?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TraceStep {
|
|
32
|
+
node: NodeInfo;
|
|
33
|
+
edgeType: string;
|
|
34
|
+
depth: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const traceCommand = new Command('trace')
|
|
38
|
+
.description('Trace data flow for a variable')
|
|
39
|
+
.argument('<pattern>', 'Pattern: "varName from functionName" or just "varName"')
|
|
40
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
41
|
+
.option('-j, --json', 'Output as JSON')
|
|
42
|
+
.option('-d, --depth <n>', 'Max trace depth', '10')
|
|
43
|
+
.action(async (pattern: string, options: TraceOptions) => {
|
|
44
|
+
const projectPath = resolve(options.project);
|
|
45
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
46
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
47
|
+
|
|
48
|
+
if (!existsSync(dbPath)) {
|
|
49
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
53
|
+
await backend.connect();
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Parse pattern: "varName from functionName" or just "varName"
|
|
57
|
+
const { varName, scopeName } = parseTracePattern(pattern);
|
|
58
|
+
const maxDepth = parseInt(options.depth, 10);
|
|
59
|
+
|
|
60
|
+
console.log(`Tracing ${varName}${scopeName ? ` from ${scopeName}` : ''}...`);
|
|
61
|
+
console.log('');
|
|
62
|
+
|
|
63
|
+
// Find starting variable(s)
|
|
64
|
+
const variables = await findVariables(backend, varName, scopeName);
|
|
65
|
+
|
|
66
|
+
if (variables.length === 0) {
|
|
67
|
+
console.log(`No variable "${varName}" found${scopeName ? ` in ${scopeName}` : ''}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Trace each variable
|
|
72
|
+
for (const variable of variables) {
|
|
73
|
+
console.log(formatNodeDisplay(variable, { projectPath }));
|
|
74
|
+
console.log('');
|
|
75
|
+
|
|
76
|
+
// Trace backwards through ASSIGNED_FROM
|
|
77
|
+
const backwardTrace = await traceBackward(backend, variable.id, maxDepth);
|
|
78
|
+
|
|
79
|
+
if (backwardTrace.length > 0) {
|
|
80
|
+
console.log('Data sources (where value comes from):');
|
|
81
|
+
displayTrace(backwardTrace, projectPath, ' ');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Trace forward through ASSIGNED_FROM (where this value flows to)
|
|
85
|
+
const forwardTrace = await traceForward(backend, variable.id, maxDepth);
|
|
86
|
+
|
|
87
|
+
if (forwardTrace.length > 0) {
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log('Data sinks (where value flows to):');
|
|
90
|
+
displayTrace(forwardTrace, projectPath, ' ');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Show value domain if available
|
|
94
|
+
const sources = await getValueSources(backend, variable.id);
|
|
95
|
+
if (sources.length > 0) {
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log('Possible values:');
|
|
98
|
+
for (const src of sources) {
|
|
99
|
+
if (src.type === 'LITERAL' && src.value !== undefined) {
|
|
100
|
+
console.log(` • ${JSON.stringify(src.value)} (literal)`);
|
|
101
|
+
} else if (src.type === 'PARAMETER') {
|
|
102
|
+
console.log(` • <parameter ${src.name}> (runtime input)`);
|
|
103
|
+
} else if (src.type === 'CALL') {
|
|
104
|
+
console.log(` • <return from ${src.name || 'call'}> (computed)`);
|
|
105
|
+
} else {
|
|
106
|
+
console.log(` • <${src.type.toLowerCase()}> ${src.name || ''}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (variables.length > 1) {
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log('---');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.json) {
|
|
118
|
+
// TODO: structured JSON output
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
} finally {
|
|
122
|
+
await backend.close();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parse trace pattern
|
|
128
|
+
*/
|
|
129
|
+
function parseTracePattern(pattern: string): { varName: string; scopeName: string | null } {
|
|
130
|
+
const fromMatch = pattern.match(/^(.+?)\s+from\s+(.+)$/i);
|
|
131
|
+
if (fromMatch) {
|
|
132
|
+
return { varName: fromMatch[1].trim(), scopeName: fromMatch[2].trim() };
|
|
133
|
+
}
|
|
134
|
+
return { varName: pattern.trim(), scopeName: null };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Find variables by name, optionally scoped to a function
|
|
139
|
+
*/
|
|
140
|
+
async function findVariables(
|
|
141
|
+
backend: RFDBServerBackend,
|
|
142
|
+
varName: string,
|
|
143
|
+
scopeName: string | null
|
|
144
|
+
): Promise<NodeInfo[]> {
|
|
145
|
+
const results: NodeInfo[] = [];
|
|
146
|
+
const lowerScopeName = scopeName ? scopeName.toLowerCase() : null;
|
|
147
|
+
|
|
148
|
+
// Search VARIABLE, CONSTANT, PARAMETER
|
|
149
|
+
for (const nodeType of ['VARIABLE', 'CONSTANT', 'PARAMETER']) {
|
|
150
|
+
for await (const node of backend.queryNodes({ nodeType: nodeType as any })) {
|
|
151
|
+
const name = node.name || '';
|
|
152
|
+
if (name.toLowerCase() === varName.toLowerCase()) {
|
|
153
|
+
// If scope specified, check if variable is in that scope
|
|
154
|
+
if (scopeName) {
|
|
155
|
+
const parsed = parseSemanticId(node.id);
|
|
156
|
+
if (!parsed) continue; // Skip nodes with invalid IDs
|
|
157
|
+
|
|
158
|
+
// Check if scopeName appears anywhere in the scope chain
|
|
159
|
+
if (!parsed.scopePath.some(s => s.toLowerCase() === lowerScopeName)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
results.push({
|
|
165
|
+
id: node.id,
|
|
166
|
+
type: node.type || nodeType,
|
|
167
|
+
name: name,
|
|
168
|
+
file: node.file || '',
|
|
169
|
+
line: node.line,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (results.length >= 5) break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (results.length >= 5) break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Trace backward through ASSIGNED_FROM edges
|
|
183
|
+
*/
|
|
184
|
+
async function traceBackward(
|
|
185
|
+
backend: RFDBServerBackend,
|
|
186
|
+
startId: string,
|
|
187
|
+
maxDepth: number
|
|
188
|
+
): Promise<TraceStep[]> {
|
|
189
|
+
const trace: TraceStep[] = [];
|
|
190
|
+
const visited = new Set<string>();
|
|
191
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
|
|
192
|
+
|
|
193
|
+
while (queue.length > 0) {
|
|
194
|
+
const { id, depth } = queue.shift()!;
|
|
195
|
+
|
|
196
|
+
if (visited.has(id) || depth > maxDepth) continue;
|
|
197
|
+
visited.add(id);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const edges = await backend.getOutgoingEdges(id, ['ASSIGNED_FROM', 'DERIVES_FROM']);
|
|
201
|
+
|
|
202
|
+
for (const edge of edges) {
|
|
203
|
+
const targetNode = await backend.getNode(edge.dst);
|
|
204
|
+
if (!targetNode) continue;
|
|
205
|
+
|
|
206
|
+
const nodeInfo: NodeInfo = {
|
|
207
|
+
id: targetNode.id,
|
|
208
|
+
type: targetNode.type || 'UNKNOWN',
|
|
209
|
+
name: targetNode.name || '',
|
|
210
|
+
file: targetNode.file || '',
|
|
211
|
+
line: targetNode.line,
|
|
212
|
+
value: targetNode.value,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
trace.push({
|
|
216
|
+
node: nodeInfo,
|
|
217
|
+
edgeType: edge.edgeType || edge.type,
|
|
218
|
+
depth: depth + 1,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Continue tracing unless we hit a leaf
|
|
222
|
+
const leafTypes = ['LITERAL', 'PARAMETER', 'EXTERNAL_MODULE'];
|
|
223
|
+
if (!leafTypes.includes(nodeInfo.type)) {
|
|
224
|
+
queue.push({ id: targetNode.id, depth: depth + 1 });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// Ignore errors
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return trace;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Trace forward - find what uses this variable
|
|
237
|
+
*/
|
|
238
|
+
async function traceForward(
|
|
239
|
+
backend: RFDBServerBackend,
|
|
240
|
+
startId: string,
|
|
241
|
+
maxDepth: number
|
|
242
|
+
): Promise<TraceStep[]> {
|
|
243
|
+
const trace: TraceStep[] = [];
|
|
244
|
+
const visited = new Set<string>();
|
|
245
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
|
|
246
|
+
|
|
247
|
+
while (queue.length > 0) {
|
|
248
|
+
const { id, depth } = queue.shift()!;
|
|
249
|
+
|
|
250
|
+
if (visited.has(id) || depth > maxDepth) continue;
|
|
251
|
+
visited.add(id);
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// Find nodes that get their value FROM this node
|
|
255
|
+
const edges = await backend.getIncomingEdges(id, ['ASSIGNED_FROM', 'DERIVES_FROM']);
|
|
256
|
+
|
|
257
|
+
for (const edge of edges) {
|
|
258
|
+
const sourceNode = await backend.getNode(edge.src);
|
|
259
|
+
if (!sourceNode) continue;
|
|
260
|
+
|
|
261
|
+
const nodeInfo: NodeInfo = {
|
|
262
|
+
id: sourceNode.id,
|
|
263
|
+
type: sourceNode.type || 'UNKNOWN',
|
|
264
|
+
name: sourceNode.name || '',
|
|
265
|
+
file: sourceNode.file || '',
|
|
266
|
+
line: sourceNode.line,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
trace.push({
|
|
270
|
+
node: nodeInfo,
|
|
271
|
+
edgeType: edge.edgeType || edge.type,
|
|
272
|
+
depth: depth + 1,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Continue forward
|
|
276
|
+
if (depth < maxDepth - 1) {
|
|
277
|
+
queue.push({ id: sourceNode.id, depth: depth + 1 });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
// Ignore errors
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return trace;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get immediate value sources (for "possible values" display)
|
|
290
|
+
*/
|
|
291
|
+
async function getValueSources(
|
|
292
|
+
backend: RFDBServerBackend,
|
|
293
|
+
nodeId: string
|
|
294
|
+
): Promise<NodeInfo[]> {
|
|
295
|
+
const sources: NodeInfo[] = [];
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const edges = await backend.getOutgoingEdges(nodeId, ['ASSIGNED_FROM']);
|
|
299
|
+
|
|
300
|
+
for (const edge of edges.slice(0, 5)) {
|
|
301
|
+
const node = await backend.getNode(edge.dst);
|
|
302
|
+
if (node) {
|
|
303
|
+
sources.push({
|
|
304
|
+
id: node.id,
|
|
305
|
+
type: node.type || 'UNKNOWN',
|
|
306
|
+
name: node.name || '',
|
|
307
|
+
file: node.file || '',
|
|
308
|
+
line: node.line,
|
|
309
|
+
value: node.value,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
// Ignore
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return sources;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Display trace results with semantic IDs
|
|
322
|
+
*/
|
|
323
|
+
function displayTrace(trace: TraceStep[], _projectPath: string, indent: string): void {
|
|
324
|
+
// Group by depth
|
|
325
|
+
const byDepth = new Map<number, TraceStep[]>();
|
|
326
|
+
for (const step of trace) {
|
|
327
|
+
if (!byDepth.has(step.depth)) {
|
|
328
|
+
byDepth.set(step.depth, []);
|
|
329
|
+
}
|
|
330
|
+
byDepth.get(step.depth)!.push(step);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
for (const [_depth, steps] of [...byDepth.entries()].sort((a, b) => a[0] - b[0])) {
|
|
334
|
+
for (const step of steps) {
|
|
335
|
+
const valueStr = step.node.value !== undefined ? ` = ${JSON.stringify(step.node.value)}` : '';
|
|
336
|
+
console.log(`${indent}<- ${step.node.name || step.node.type} (${step.node.type})${valueStr}`);
|
|
337
|
+
console.log(`${indent} ${formatNodeInline(step.node)}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Preview Utility
|
|
3
|
+
*
|
|
4
|
+
* Reads source files and extracts code snippets around a given line number
|
|
5
|
+
* for displaying in the explorer UI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync } from 'fs';
|
|
9
|
+
|
|
10
|
+
export interface CodePreviewOptions {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
contextBefore?: number; // default: 2
|
|
14
|
+
contextAfter?: number; // default: 12
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CodePreviewResult {
|
|
18
|
+
lines: string[];
|
|
19
|
+
startLine: number;
|
|
20
|
+
endLine: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get a code preview snippet from a source file.
|
|
25
|
+
* Returns lines around the specified line number with context.
|
|
26
|
+
*/
|
|
27
|
+
export function getCodePreview(options: CodePreviewOptions): CodePreviewResult | null {
|
|
28
|
+
const { file, line, contextBefore = 2, contextAfter = 12 } = options;
|
|
29
|
+
|
|
30
|
+
if (!existsSync(file)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const content = readFileSync(file, 'utf-8');
|
|
36
|
+
const allLines = content.split('\n');
|
|
37
|
+
|
|
38
|
+
// Calculate range (1-indexed)
|
|
39
|
+
const startLine = Math.max(1, line - contextBefore);
|
|
40
|
+
const endLine = Math.min(allLines.length, line + contextAfter);
|
|
41
|
+
|
|
42
|
+
// Extract lines (convert to 0-indexed for array access)
|
|
43
|
+
const lines = allLines.slice(startLine - 1, endLine);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
lines,
|
|
47
|
+
startLine,
|
|
48
|
+
endLine
|
|
49
|
+
};
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format code preview lines with line numbers for display.
|
|
57
|
+
* Returns an array of formatted strings like " 42 | code here"
|
|
58
|
+
*/
|
|
59
|
+
export function formatCodePreview(
|
|
60
|
+
preview: CodePreviewResult,
|
|
61
|
+
highlightLine?: number
|
|
62
|
+
): string[] {
|
|
63
|
+
const { lines, startLine } = preview;
|
|
64
|
+
const maxLineNum = startLine + lines.length - 1;
|
|
65
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
66
|
+
|
|
67
|
+
return lines.map((line, index) => {
|
|
68
|
+
const lineNum = startLine + index;
|
|
69
|
+
const paddedNum = String(lineNum).padStart(lineNumWidth, ' ');
|
|
70
|
+
const prefix = highlightLine === lineNum ? '>' : ' ';
|
|
71
|
+
|
|
72
|
+
// Truncate very long lines
|
|
73
|
+
const displayLine = line.length > 80 ? line.slice(0, 77) + '...' : line;
|
|
74
|
+
|
|
75
|
+
return `${prefix}${paddedNum} | ${displayLine}`;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized error formatting for CLI commands - REG-157
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent error messages across all CLI commands.
|
|
5
|
+
* Format:
|
|
6
|
+
* ✗ Main error message (1 line, concise)
|
|
7
|
+
*
|
|
8
|
+
* → Next action 1
|
|
9
|
+
* → Next action 2
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Print a standardized error message and exit.
|
|
14
|
+
*
|
|
15
|
+
* @param title - Main error message (should be under 80 chars)
|
|
16
|
+
* @param nextSteps - Optional array of actionable suggestions
|
|
17
|
+
* @returns never - always calls process.exit(1)
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* exitWithError('No graph database found', [
|
|
21
|
+
* 'Run: grafema analyze'
|
|
22
|
+
* ]);
|
|
23
|
+
*/
|
|
24
|
+
export function exitWithError(title: string, nextSteps?: string[]): never {
|
|
25
|
+
console.error(`✗ ${title}`);
|
|
26
|
+
|
|
27
|
+
if (nextSteps && nextSteps.length > 0) {
|
|
28
|
+
console.error('');
|
|
29
|
+
for (const step of nextSteps) {
|
|
30
|
+
console.error(`→ ${step}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node display formatting utilities - REG-125
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent formatting for node display across all CLI commands.
|
|
5
|
+
* Semantic IDs are shown as the PRIMARY identifier, with location as secondary.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { relative } from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format options for node display
|
|
12
|
+
*/
|
|
13
|
+
export interface FormatNodeOptions {
|
|
14
|
+
/** Project path for relative file paths */
|
|
15
|
+
projectPath: string;
|
|
16
|
+
/** Include location line (default: true) */
|
|
17
|
+
showLocation?: boolean;
|
|
18
|
+
/** Prefix for each line (default: '') */
|
|
19
|
+
indent?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Node information required for display
|
|
24
|
+
*/
|
|
25
|
+
export interface DisplayableNode {
|
|
26
|
+
/** Semantic ID (e.g., "auth/service.ts->AuthService->FUNCTION->authenticate") */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Node type (e.g., "FUNCTION", "CLASS") */
|
|
29
|
+
type: string;
|
|
30
|
+
/** Human-readable name */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Absolute file path */
|
|
33
|
+
file: string;
|
|
34
|
+
/** Line number (optional) */
|
|
35
|
+
line?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format a node for primary display (multi-line)
|
|
40
|
+
*
|
|
41
|
+
* Output format:
|
|
42
|
+
* [FUNCTION] authenticate
|
|
43
|
+
* ID: auth/service.ts->AuthService->FUNCTION->authenticate
|
|
44
|
+
* Location: auth/service.ts:42
|
|
45
|
+
*/
|
|
46
|
+
export function formatNodeDisplay(node: DisplayableNode, options: FormatNodeOptions): string {
|
|
47
|
+
const { projectPath, showLocation = true, indent = '' } = options;
|
|
48
|
+
const lines: string[] = [];
|
|
49
|
+
|
|
50
|
+
// Line 1: [TYPE] name
|
|
51
|
+
lines.push(`${indent}[${node.type}] ${node.name}`);
|
|
52
|
+
|
|
53
|
+
// Line 2: ID (semantic ID)
|
|
54
|
+
lines.push(`${indent} ID: ${node.id}`);
|
|
55
|
+
|
|
56
|
+
// Line 3: Location (optional)
|
|
57
|
+
if (showLocation) {
|
|
58
|
+
const loc = formatLocation(node.file, node.line, projectPath);
|
|
59
|
+
if (loc) {
|
|
60
|
+
lines.push(`${indent} Location: ${loc}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format a node for inline display in lists (semantic ID only)
|
|
69
|
+
*
|
|
70
|
+
* Output format:
|
|
71
|
+
* auth/service.ts->AuthService->FUNCTION->authenticate
|
|
72
|
+
*/
|
|
73
|
+
export function formatNodeInline(node: DisplayableNode): string {
|
|
74
|
+
return node.id;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format file location relative to project
|
|
79
|
+
*/
|
|
80
|
+
export function formatLocation(
|
|
81
|
+
file: string | undefined,
|
|
82
|
+
line: number | undefined,
|
|
83
|
+
projectPath: string
|
|
84
|
+
): string {
|
|
85
|
+
if (!file) return '';
|
|
86
|
+
const relPath = relative(projectPath, file);
|
|
87
|
+
return line ? `${relPath}:${line}` : relPath;
|
|
88
|
+
}
|