tokenlean 0.1.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/README.md +248 -0
- package/bin/tl-api.mjs +515 -0
- package/bin/tl-blame.mjs +345 -0
- package/bin/tl-complexity.mjs +514 -0
- package/bin/tl-component.mjs +274 -0
- package/bin/tl-config.mjs +135 -0
- package/bin/tl-context.mjs +156 -0
- package/bin/tl-coverage.mjs +456 -0
- package/bin/tl-deps.mjs +474 -0
- package/bin/tl-diff.mjs +183 -0
- package/bin/tl-entry.mjs +256 -0
- package/bin/tl-env.mjs +376 -0
- package/bin/tl-exports.mjs +583 -0
- package/bin/tl-flow.mjs +324 -0
- package/bin/tl-history.mjs +289 -0
- package/bin/tl-hotspots.mjs +321 -0
- package/bin/tl-impact.mjs +345 -0
- package/bin/tl-prompt.mjs +175 -0
- package/bin/tl-related.mjs +227 -0
- package/bin/tl-routes.mjs +627 -0
- package/bin/tl-search.mjs +123 -0
- package/bin/tl-structure.mjs +161 -0
- package/bin/tl-symbols.mjs +430 -0
- package/bin/tl-todo.mjs +341 -0
- package/bin/tl-types.mjs +441 -0
- package/bin/tl-unused.mjs +494 -0
- package/package.json +55 -0
- package/src/config.mjs +271 -0
- package/src/output.mjs +251 -0
- package/src/project.mjs +277 -0
package/bin/tl-api.mjs
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-api - Extract REST/GraphQL API endpoints from code
|
|
5
|
+
*
|
|
6
|
+
* Scans source files for API endpoint definitions in common frameworks
|
|
7
|
+
* (Express, Fastify, Koa, Hono, NestJS, etc.) and GraphQL schemas.
|
|
8
|
+
*
|
|
9
|
+
* Usage: tl-api [dir] [--rest-only] [--graphql-only]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Prompt info for tl-prompt
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
name: 'tl-api',
|
|
16
|
+
desc: 'Extract REST/GraphQL API endpoints',
|
|
17
|
+
when: 'before-read',
|
|
18
|
+
example: 'tl-api src/api/'
|
|
19
|
+
}));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
24
|
+
import { join, relative, extname } from 'path';
|
|
25
|
+
import {
|
|
26
|
+
createOutput,
|
|
27
|
+
parseCommonArgs,
|
|
28
|
+
COMMON_OPTIONS_HELP
|
|
29
|
+
} from '../src/output.mjs';
|
|
30
|
+
import { findProjectRoot, shouldSkip } from '../src/project.mjs';
|
|
31
|
+
|
|
32
|
+
const HELP = `
|
|
33
|
+
tl-api - Extract REST/GraphQL API endpoints from code
|
|
34
|
+
|
|
35
|
+
Usage: tl-api [dir] [options]
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--rest-only, -r Only show REST endpoints
|
|
39
|
+
--graphql-only, -g Only show GraphQL operations
|
|
40
|
+
--group-by-file Group endpoints by file (default: by method)
|
|
41
|
+
--with-handlers Show handler function names
|
|
42
|
+
${COMMON_OPTIONS_HELP}
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
tl-api # All endpoints
|
|
46
|
+
tl-api src/routes/ # Scan specific directory
|
|
47
|
+
tl-api -r # REST only
|
|
48
|
+
tl-api --group-by-file # Group by file
|
|
49
|
+
|
|
50
|
+
Detects:
|
|
51
|
+
REST: Express, Fastify, Koa, Hono, NestJS decorators, fetch handlers
|
|
52
|
+
GraphQL: Query/Mutation/Subscription definitions, resolvers
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
// ─────────────────────────────────────────────────────────────
|
|
56
|
+
// File Discovery
|
|
57
|
+
// ─────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
const CODE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.mts']);
|
|
60
|
+
|
|
61
|
+
function findCodeFiles(dir, files = []) {
|
|
62
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
63
|
+
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const fullPath = join(dir, entry.name);
|
|
66
|
+
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
if (!shouldSkip(entry.name, true)) {
|
|
69
|
+
findCodeFiles(fullPath, files);
|
|
70
|
+
}
|
|
71
|
+
} else if (entry.isFile()) {
|
|
72
|
+
const ext = extname(entry.name).toLowerCase();
|
|
73
|
+
if (CODE_EXTENSIONS.has(ext) && !shouldSkip(entry.name, false)) {
|
|
74
|
+
files.push(fullPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return files;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─────────────────────────────────────────────────────────────
|
|
83
|
+
// REST Endpoint Extraction
|
|
84
|
+
// ─────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function extractRestEndpoints(content, filePath) {
|
|
87
|
+
const endpoints = [];
|
|
88
|
+
const lines = content.split('\n');
|
|
89
|
+
|
|
90
|
+
// Common HTTP methods
|
|
91
|
+
const methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'all'];
|
|
92
|
+
const methodsUpper = methods.map(m => m.toUpperCase());
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < lines.length; i++) {
|
|
95
|
+
const line = lines[i];
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
|
|
98
|
+
// Express/Koa/Fastify style: app.get('/path', handler) or router.post('/path', ...)
|
|
99
|
+
for (const method of methods) {
|
|
100
|
+
// Match: app.get('/path' or router.get("/path" or .get(`/path`
|
|
101
|
+
const routerPattern = new RegExp(`\\.(${method})\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`, 'i');
|
|
102
|
+
const match = trimmed.match(routerPattern);
|
|
103
|
+
if (match) {
|
|
104
|
+
const handler = extractHandlerName(trimmed, lines, i);
|
|
105
|
+
endpoints.push({
|
|
106
|
+
method: match[1].toUpperCase(),
|
|
107
|
+
path: match[2],
|
|
108
|
+
line: i + 1,
|
|
109
|
+
handler,
|
|
110
|
+
framework: 'express-like'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// NestJS decorators: @Get('/path'), @Post('/path'), etc.
|
|
116
|
+
for (const method of methodsUpper) {
|
|
117
|
+
const decoratorPattern = new RegExp(`@(${method})\\s*\\(\\s*['"\`]?([^'"\`\\)]*)?['"\`]?\\s*\\)`, 'i');
|
|
118
|
+
const match = trimmed.match(decoratorPattern);
|
|
119
|
+
if (match) {
|
|
120
|
+
// Get the method name from next non-decorator line
|
|
121
|
+
let handler = '';
|
|
122
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
123
|
+
const nextLine = lines[j].trim();
|
|
124
|
+
if (!nextLine.startsWith('@') && nextLine.includes('(')) {
|
|
125
|
+
const funcMatch = nextLine.match(/(?:async\s+)?(\w+)\s*\(/);
|
|
126
|
+
if (funcMatch) handler = funcMatch[1];
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
endpoints.push({
|
|
131
|
+
method: match[1].toUpperCase(),
|
|
132
|
+
path: match[2] || '/',
|
|
133
|
+
line: i + 1,
|
|
134
|
+
handler,
|
|
135
|
+
framework: 'nestjs'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Hono style: app.get('/path', (c) => ...) or new Hono().get(...)
|
|
141
|
+
const honoPattern = /\.(?:on|get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/i;
|
|
142
|
+
const honoMatch = trimmed.match(honoPattern);
|
|
143
|
+
if (honoMatch && !endpoints.some(e => e.line === i + 1)) {
|
|
144
|
+
const methodMatch = trimmed.match(/\.(get|post|put|patch|delete|on)\s*\(/i);
|
|
145
|
+
if (methodMatch) {
|
|
146
|
+
endpoints.push({
|
|
147
|
+
method: methodMatch[1].toUpperCase(),
|
|
148
|
+
path: honoMatch[1],
|
|
149
|
+
line: i + 1,
|
|
150
|
+
handler: '',
|
|
151
|
+
framework: 'hono'
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Next.js API routes: export async function GET/POST/etc
|
|
157
|
+
const nextPattern = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(/;
|
|
158
|
+
const nextMatch = trimmed.match(nextPattern);
|
|
159
|
+
if (nextMatch) {
|
|
160
|
+
endpoints.push({
|
|
161
|
+
method: nextMatch[1],
|
|
162
|
+
path: '(from filename)',
|
|
163
|
+
line: i + 1,
|
|
164
|
+
handler: nextMatch[1],
|
|
165
|
+
framework: 'nextjs'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Fetch API route handlers: case 'GET': or method === 'POST'
|
|
170
|
+
const fetchPattern = /(?:case\s+['"]|method\s*===?\s*['"])(GET|POST|PUT|PATCH|DELETE)['"]:/i;
|
|
171
|
+
const fetchMatch = trimmed.match(fetchPattern);
|
|
172
|
+
if (fetchMatch) {
|
|
173
|
+
endpoints.push({
|
|
174
|
+
method: fetchMatch[1].toUpperCase(),
|
|
175
|
+
path: '(handler)',
|
|
176
|
+
line: i + 1,
|
|
177
|
+
handler: '',
|
|
178
|
+
framework: 'fetch'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return endpoints;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function extractHandlerName(line, lines, lineIndex) {
|
|
187
|
+
// Try to find handler name in the same line or next lines
|
|
188
|
+
// Pattern: , handlerName) or , (req, res) =>
|
|
189
|
+
const inlineMatch = line.match(/,\s*(\w+)\s*\)/);
|
|
190
|
+
if (inlineMatch && !['req', 'res', 'ctx', 'c', 'request', 'response'].includes(inlineMatch[1])) {
|
|
191
|
+
return inlineMatch[1];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check for arrow function or function reference
|
|
195
|
+
const arrowMatch = line.match(/,\s*(?:async\s+)?\(.*?\)\s*=>/);
|
|
196
|
+
if (arrowMatch) return '(inline)';
|
|
197
|
+
|
|
198
|
+
return '';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─────────────────────────────────────────────────────────────
|
|
202
|
+
// GraphQL Extraction
|
|
203
|
+
// ─────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
function extractGraphqlOperations(content, filePath) {
|
|
206
|
+
const operations = [];
|
|
207
|
+
const lines = content.split('\n');
|
|
208
|
+
|
|
209
|
+
// Track if we're in a type definition
|
|
210
|
+
let currentType = null;
|
|
211
|
+
let braceDepth = 0;
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
214
|
+
const line = lines[i];
|
|
215
|
+
const trimmed = line.trim();
|
|
216
|
+
|
|
217
|
+
// GraphQL SDL: type Query { ... }
|
|
218
|
+
const typeMatch = trimmed.match(/^type\s+(Query|Mutation|Subscription)\s*\{?/);
|
|
219
|
+
if (typeMatch) {
|
|
220
|
+
currentType = typeMatch[1];
|
|
221
|
+
braceDepth = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Track brace depth
|
|
226
|
+
if (currentType) {
|
|
227
|
+
braceDepth += (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
228
|
+
|
|
229
|
+
if (braceDepth <= 0) {
|
|
230
|
+
currentType = null;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Field definition: fieldName(args): Type
|
|
235
|
+
const fieldMatch = trimmed.match(/^(\w+)\s*(?:\([^)]*\))?\s*:/);
|
|
236
|
+
if (fieldMatch && !trimmed.startsWith('#')) {
|
|
237
|
+
operations.push({
|
|
238
|
+
type: currentType,
|
|
239
|
+
name: fieldMatch[1],
|
|
240
|
+
line: i + 1,
|
|
241
|
+
source: 'schema'
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Resolver definitions: Query: { fieldName: ... } or Mutation: { ... }
|
|
247
|
+
const resolverTypeMatch = trimmed.match(/^(Query|Mutation|Subscription)\s*:\s*\{/);
|
|
248
|
+
if (resolverTypeMatch) {
|
|
249
|
+
currentType = resolverTypeMatch[1];
|
|
250
|
+
braceDepth = 1;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// NestJS GraphQL decorators: @Query(), @Mutation()
|
|
255
|
+
const decoratorMatch = trimmed.match(/@(Query|Mutation|Subscription)\s*\(/);
|
|
256
|
+
if (decoratorMatch) {
|
|
257
|
+
// Get operation name from next line
|
|
258
|
+
let opName = '';
|
|
259
|
+
for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
|
|
260
|
+
const nextLine = lines[j].trim();
|
|
261
|
+
if (!nextLine.startsWith('@')) {
|
|
262
|
+
const funcMatch = nextLine.match(/(?:async\s+)?(\w+)\s*\(/);
|
|
263
|
+
if (funcMatch) opName = funcMatch[1];
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
operations.push({
|
|
268
|
+
type: decoratorMatch[1],
|
|
269
|
+
name: opName || '(unknown)',
|
|
270
|
+
line: i + 1,
|
|
271
|
+
source: 'decorator'
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// gql tagged template: Query { fieldName }
|
|
276
|
+
if (trimmed.includes('gql`') || trimmed.includes('gql(')) {
|
|
277
|
+
// Simple extraction from template literals
|
|
278
|
+
const gqlContent = extractGqlTemplate(lines, i);
|
|
279
|
+
const gqlOps = parseGqlString(gqlContent, i + 1);
|
|
280
|
+
operations.push(...gqlOps);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return operations;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function extractGqlTemplate(lines, startLine) {
|
|
288
|
+
let content = '';
|
|
289
|
+
let depth = 0;
|
|
290
|
+
let started = false;
|
|
291
|
+
|
|
292
|
+
for (let i = startLine; i < Math.min(startLine + 50, lines.length); i++) {
|
|
293
|
+
const line = lines[i];
|
|
294
|
+
|
|
295
|
+
if (line.includes('`')) {
|
|
296
|
+
if (!started) {
|
|
297
|
+
started = true;
|
|
298
|
+
content += line.split('`')[1] || '';
|
|
299
|
+
} else {
|
|
300
|
+
content += line.split('`')[0] || '';
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
} else if (started) {
|
|
304
|
+
content += line + '\n';
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return content;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function parseGqlString(content, baseLine) {
|
|
312
|
+
const operations = [];
|
|
313
|
+
const lines = content.split('\n');
|
|
314
|
+
|
|
315
|
+
let currentType = null;
|
|
316
|
+
|
|
317
|
+
for (let i = 0; i < lines.length; i++) {
|
|
318
|
+
const trimmed = lines[i].trim();
|
|
319
|
+
|
|
320
|
+
const typeMatch = trimmed.match(/^type\s+(Query|Mutation|Subscription)/);
|
|
321
|
+
if (typeMatch) {
|
|
322
|
+
currentType = typeMatch[1];
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (currentType && trimmed.match(/^\w+\s*[(:]/)) {
|
|
327
|
+
const fieldMatch = trimmed.match(/^(\w+)/);
|
|
328
|
+
if (fieldMatch) {
|
|
329
|
+
operations.push({
|
|
330
|
+
type: currentType,
|
|
331
|
+
name: fieldMatch[1],
|
|
332
|
+
line: baseLine + i,
|
|
333
|
+
source: 'gql-template'
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (trimmed === '}') {
|
|
339
|
+
currentType = null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return operations;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─────────────────────────────────────────────────────────────
|
|
347
|
+
// Main
|
|
348
|
+
// ─────────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
const args = process.argv.slice(2);
|
|
351
|
+
const options = parseCommonArgs(args);
|
|
352
|
+
|
|
353
|
+
// Parse custom options
|
|
354
|
+
let restOnly = false;
|
|
355
|
+
let graphqlOnly = false;
|
|
356
|
+
let groupByFile = false;
|
|
357
|
+
let withHandlers = false;
|
|
358
|
+
|
|
359
|
+
const remaining = [];
|
|
360
|
+
for (let i = 0; i < options.remaining.length; i++) {
|
|
361
|
+
const arg = options.remaining[i];
|
|
362
|
+
|
|
363
|
+
if (arg === '--rest-only' || arg === '-r') {
|
|
364
|
+
restOnly = true;
|
|
365
|
+
} else if (arg === '--graphql-only' || arg === '-g') {
|
|
366
|
+
graphqlOnly = true;
|
|
367
|
+
} else if (arg === '--group-by-file') {
|
|
368
|
+
groupByFile = true;
|
|
369
|
+
} else if (arg === '--with-handlers') {
|
|
370
|
+
withHandlers = true;
|
|
371
|
+
} else if (!arg.startsWith('-')) {
|
|
372
|
+
remaining.push(arg);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const targetDir = remaining[0] || '.';
|
|
377
|
+
|
|
378
|
+
if (options.help) {
|
|
379
|
+
console.log(HELP);
|
|
380
|
+
process.exit(0);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!existsSync(targetDir)) {
|
|
384
|
+
console.error(`Directory not found: ${targetDir}`);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const projectRoot = findProjectRoot();
|
|
389
|
+
const out = createOutput(options);
|
|
390
|
+
|
|
391
|
+
// Find all code files
|
|
392
|
+
let files = [];
|
|
393
|
+
const stat = statSync(targetDir);
|
|
394
|
+
if (stat.isFile()) {
|
|
395
|
+
files = [targetDir];
|
|
396
|
+
} else {
|
|
397
|
+
files = findCodeFiles(targetDir);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (files.length === 0) {
|
|
401
|
+
console.error('No code files found');
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const allRest = [];
|
|
406
|
+
const allGraphql = [];
|
|
407
|
+
|
|
408
|
+
for (const file of files) {
|
|
409
|
+
const content = readFileSync(file, 'utf-8');
|
|
410
|
+
const relPath = relative(projectRoot, file);
|
|
411
|
+
|
|
412
|
+
if (!graphqlOnly) {
|
|
413
|
+
const rest = extractRestEndpoints(content, file);
|
|
414
|
+
rest.forEach(e => allRest.push({ ...e, file: relPath }));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!restOnly) {
|
|
418
|
+
const graphql = extractGraphqlOperations(content, file);
|
|
419
|
+
graphql.forEach(o => allGraphql.push({ ...o, file: relPath }));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Deduplicate GraphQL operations (same name+type+file)
|
|
424
|
+
const seenGraphql = new Set();
|
|
425
|
+
const dedupedGraphql = allGraphql.filter(op => {
|
|
426
|
+
const key = `${op.type}:${op.name}:${op.file}`;
|
|
427
|
+
if (seenGraphql.has(key)) return false;
|
|
428
|
+
seenGraphql.add(key);
|
|
429
|
+
return true;
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Set JSON data
|
|
433
|
+
out.setData('rest', allRest);
|
|
434
|
+
out.setData('graphql', dedupedGraphql);
|
|
435
|
+
out.setData('totalEndpoints', allRest.length + dedupedGraphql.length);
|
|
436
|
+
|
|
437
|
+
// Output REST endpoints
|
|
438
|
+
if (allRest.length > 0) {
|
|
439
|
+
out.header(`🌐 REST Endpoints (${allRest.length})`);
|
|
440
|
+
out.blank();
|
|
441
|
+
|
|
442
|
+
if (groupByFile) {
|
|
443
|
+
const byFile = new Map();
|
|
444
|
+
for (const ep of allRest) {
|
|
445
|
+
if (!byFile.has(ep.file)) byFile.set(ep.file, []);
|
|
446
|
+
byFile.get(ep.file).push(ep);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
for (const [file, endpoints] of byFile) {
|
|
450
|
+
out.add(` ${file}`);
|
|
451
|
+
for (const ep of endpoints) {
|
|
452
|
+
const handler = withHandlers && ep.handler ? ` → ${ep.handler}` : '';
|
|
453
|
+
out.add(` ${ep.method.padEnd(7)} ${ep.path}${handler}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
// Group by method
|
|
458
|
+
const byMethod = new Map();
|
|
459
|
+
for (const ep of allRest) {
|
|
460
|
+
if (!byMethod.has(ep.method)) byMethod.set(ep.method, []);
|
|
461
|
+
byMethod.get(ep.method).push(ep);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'ALL'];
|
|
465
|
+
for (const method of methodOrder) {
|
|
466
|
+
const endpoints = byMethod.get(method);
|
|
467
|
+
if (!endpoints) continue;
|
|
468
|
+
|
|
469
|
+
out.add(` ${method}`);
|
|
470
|
+
for (const ep of endpoints) {
|
|
471
|
+
const handler = withHandlers && ep.handler ? ` → ${ep.handler}` : '';
|
|
472
|
+
const location = groupByFile ? '' : ` (${ep.file}:${ep.line})`;
|
|
473
|
+
out.add(` ${ep.path}${handler}${location}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
out.blank();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Output GraphQL operations
|
|
481
|
+
if (dedupedGraphql.length > 0) {
|
|
482
|
+
out.header(`📊 GraphQL Operations (${dedupedGraphql.length})`);
|
|
483
|
+
out.blank();
|
|
484
|
+
|
|
485
|
+
const byType = new Map();
|
|
486
|
+
for (const op of dedupedGraphql) {
|
|
487
|
+
if (!byType.has(op.type)) byType.set(op.type, []);
|
|
488
|
+
byType.get(op.type).push(op);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
for (const type of ['Query', 'Mutation', 'Subscription']) {
|
|
492
|
+
const ops = byType.get(type);
|
|
493
|
+
if (!ops) continue;
|
|
494
|
+
|
|
495
|
+
out.add(` ${type}`);
|
|
496
|
+
for (const op of ops) {
|
|
497
|
+
out.add(` ${op.name} (${op.file}:${op.line})`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
out.blank();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Summary
|
|
504
|
+
if (!options.quiet) {
|
|
505
|
+
if (allRest.length === 0 && dedupedGraphql.length === 0) {
|
|
506
|
+
out.add('No API endpoints found');
|
|
507
|
+
} else {
|
|
508
|
+
const parts = [];
|
|
509
|
+
if (allRest.length > 0) parts.push(`${allRest.length} REST`);
|
|
510
|
+
if (dedupedGraphql.length > 0) parts.push(`${dedupedGraphql.length} GraphQL`);
|
|
511
|
+
out.add(`Total: ${parts.join(', ')} endpoints`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
out.print();
|