@eloquence98/ctx 0.1.4 → 0.1.6
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/dist/adapters/express.d.ts +2 -0
- package/dist/adapters/express.js +58 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/nextjs.d.ts +2 -0
- package/dist/adapters/nextjs.js +102 -0
- package/dist/adapters/types.d.ts +11 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/vanilla.d.ts +2 -0
- package/dist/adapters/vanilla.js +24 -0
- package/dist/cli.js +31 -83
- package/dist/formatters/ai-optimized.d.ts +2 -0
- package/dist/formatters/ai-optimized.js +215 -0
- package/dist/formatters/ai.d.ts +2 -2
- package/dist/formatters/ai.js +14 -50
- package/dist/formatters/human.d.ts +2 -0
- package/dist/formatters/human.js +200 -0
- package/dist/formatters/index.d.ts +3 -1
- package/dist/formatters/index.js +3 -1
- package/dist/formatters/markdown.d.ts +2 -2
- package/dist/formatters/markdown.js +39 -35
- package/dist/formatters/raw.d.ts +2 -0
- package/dist/formatters/raw.js +47 -0
- package/dist/parser.js +67 -10
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export const expressAdapter = {
|
|
4
|
+
name: "express",
|
|
5
|
+
async detect(dir) {
|
|
6
|
+
try {
|
|
7
|
+
const pkg = await fs.readFile(path.join(dir, "package.json"), "utf-8");
|
|
8
|
+
const parsed = JSON.parse(pkg);
|
|
9
|
+
const deps = { ...parsed.dependencies, ...parsed.devDependencies };
|
|
10
|
+
return ("express" in deps ||
|
|
11
|
+
"fastify" in deps ||
|
|
12
|
+
"koa" in deps ||
|
|
13
|
+
"hono" in deps);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
async analyze(dir, files) {
|
|
20
|
+
const sections = new Map();
|
|
21
|
+
const folderMappings = {
|
|
22
|
+
Routes: ["routes", "routers", "api"],
|
|
23
|
+
Controllers: ["controllers", "handlers"],
|
|
24
|
+
Services: ["services"],
|
|
25
|
+
Models: ["models", "entities", "schemas"],
|
|
26
|
+
Middleware: ["middleware", "middlewares"],
|
|
27
|
+
Utils: ["utils", "helpers", "lib"],
|
|
28
|
+
Config: ["config", "configs"],
|
|
29
|
+
};
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
const relativePath = path.relative(dir, file.filePath);
|
|
32
|
+
const parts = relativePath.split(path.sep);
|
|
33
|
+
let matched = false;
|
|
34
|
+
for (const [sectionName, folders] of Object.entries(folderMappings)) {
|
|
35
|
+
const folderIndex = parts.findIndex((p) => folders.includes(p.toLowerCase()));
|
|
36
|
+
if (folderIndex !== -1) {
|
|
37
|
+
if (!sections.has(sectionName)) {
|
|
38
|
+
sections.set(sectionName, []);
|
|
39
|
+
}
|
|
40
|
+
sections.get(sectionName).push(file);
|
|
41
|
+
matched = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!matched) {
|
|
46
|
+
const folder = parts.length > 1 ? parts[0] : "_root";
|
|
47
|
+
if (!sections.has(folder)) {
|
|
48
|
+
sections.set(folder, []);
|
|
49
|
+
}
|
|
50
|
+
sections.get(folder).push(file);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
projectType: "Express/Fastify",
|
|
55
|
+
sections,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expressAdapter } from "./express.js";
|
|
2
|
+
import { nextjsAdapter } from "./nextjs.js";
|
|
3
|
+
import { vanillaAdapter } from "./vanilla.js";
|
|
4
|
+
const adapters = {
|
|
5
|
+
nextjs: nextjsAdapter,
|
|
6
|
+
react: vanillaAdapter,
|
|
7
|
+
express: expressAdapter,
|
|
8
|
+
nestjs: expressAdapter, // Similar structure
|
|
9
|
+
vue: vanillaAdapter,
|
|
10
|
+
sveltekit: vanillaAdapter,
|
|
11
|
+
node: vanillaAdapter,
|
|
12
|
+
vanilla: vanillaAdapter,
|
|
13
|
+
};
|
|
14
|
+
export function getAdapter(projectType) {
|
|
15
|
+
return adapters[projectType] || vanillaAdapter;
|
|
16
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getRoutes } from "../scanner.js";
|
|
4
|
+
export const nextjsAdapter = {
|
|
5
|
+
name: "nextjs",
|
|
6
|
+
async detect(dir) {
|
|
7
|
+
const configFiles = ["next.config.js", "next.config.mjs", "next.config.ts"];
|
|
8
|
+
for (const file of configFiles) {
|
|
9
|
+
try {
|
|
10
|
+
await fs.access(path.join(dir, file));
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Check package.json for next dependency
|
|
18
|
+
try {
|
|
19
|
+
const pkg = await fs.readFile(path.join(dir, "package.json"), "utf-8");
|
|
20
|
+
const parsed = JSON.parse(pkg);
|
|
21
|
+
const deps = { ...parsed.dependencies, ...parsed.devDependencies };
|
|
22
|
+
return "next" in deps;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
async analyze(dir, files) {
|
|
29
|
+
const sections = new Map();
|
|
30
|
+
// Get routes
|
|
31
|
+
let routes = [];
|
|
32
|
+
const appDir = await findAppDirectory(dir);
|
|
33
|
+
if (appDir) {
|
|
34
|
+
routes = await getRoutes(appDir);
|
|
35
|
+
}
|
|
36
|
+
// Group by known Next.js folders
|
|
37
|
+
const folderMappings = {
|
|
38
|
+
Features: ["features", "modules", "domains"],
|
|
39
|
+
Components: ["components"],
|
|
40
|
+
Hooks: ["hooks"],
|
|
41
|
+
Lib: ["lib", "utils", "helpers", "services"],
|
|
42
|
+
API: ["api"],
|
|
43
|
+
};
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const relativePath = path.relative(dir, file.filePath);
|
|
46
|
+
const parts = relativePath.split(path.sep);
|
|
47
|
+
let matched = false;
|
|
48
|
+
for (const [sectionName, folders] of Object.entries(folderMappings)) {
|
|
49
|
+
const folderIndex = parts.findIndex((p) => folders.includes(p));
|
|
50
|
+
if (folderIndex !== -1) {
|
|
51
|
+
const subParts = parts.slice(folderIndex + 1);
|
|
52
|
+
if (subParts.length >= 1) {
|
|
53
|
+
// Use subfolder name if exists, otherwise use section name
|
|
54
|
+
const subFolder = subParts.length > 1 ? subParts[0] : "_direct";
|
|
55
|
+
const key = subFolder === "_direct"
|
|
56
|
+
? sectionName
|
|
57
|
+
: `${sectionName}/${subParts[0]}`;
|
|
58
|
+
if (!sections.has(key)) {
|
|
59
|
+
sections.set(key, []);
|
|
60
|
+
}
|
|
61
|
+
sections.get(key).push(file);
|
|
62
|
+
matched = true;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// If no match, group by top folder
|
|
68
|
+
if (!matched) {
|
|
69
|
+
const folder = parts.length > 1 ? parts[0] : "_root";
|
|
70
|
+
const key = `Other/${folder}`;
|
|
71
|
+
if (!sections.has(key)) {
|
|
72
|
+
sections.set(key, []);
|
|
73
|
+
}
|
|
74
|
+
sections.get(key).push(file);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
projectType: "Next.js",
|
|
79
|
+
sections,
|
|
80
|
+
routes,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
async function findAppDirectory(basePath) {
|
|
85
|
+
const possiblePaths = [
|
|
86
|
+
path.join(basePath, "app"),
|
|
87
|
+
path.join(basePath, "src", "app"),
|
|
88
|
+
path.join(basePath, "src/app"),
|
|
89
|
+
];
|
|
90
|
+
for (const p of possiblePaths) {
|
|
91
|
+
try {
|
|
92
|
+
const stat = await fs.stat(p);
|
|
93
|
+
if (stat.isDirectory()) {
|
|
94
|
+
return p;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FileExports } from "../types.js";
|
|
2
|
+
export interface AdapterContext {
|
|
3
|
+
projectType: string;
|
|
4
|
+
sections: Map<string, FileExports[]>;
|
|
5
|
+
routes?: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface Adapter {
|
|
8
|
+
name: string;
|
|
9
|
+
detect: (dir: string) => Promise<boolean>;
|
|
10
|
+
analyze: (dir: string, files: FileExports[]) => Promise<AdapterContext>;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
export const vanillaAdapter = {
|
|
3
|
+
name: "vanilla",
|
|
4
|
+
async detect() {
|
|
5
|
+
// Always returns true - this is the fallback
|
|
6
|
+
return true;
|
|
7
|
+
},
|
|
8
|
+
async analyze(dir, files) {
|
|
9
|
+
const sections = new Map();
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const relativePath = path.relative(dir, file.filePath);
|
|
12
|
+
const parts = relativePath.split(path.sep);
|
|
13
|
+
const folder = parts.length > 1 ? parts[0] : "_root";
|
|
14
|
+
if (!sections.has(folder)) {
|
|
15
|
+
sections.set(folder, []);
|
|
16
|
+
}
|
|
17
|
+
sections.get(folder).push(file);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
projectType: "JavaScript/TypeScript",
|
|
21
|
+
sections,
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,58 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from "commander";
|
|
3
|
-
import path from "path";
|
|
4
3
|
import fs from "fs/promises";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import { formatMarkdown } from "./formatters/index.js";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { getAdapter } from "./adapters/index.js";
|
|
8
6
|
import { detectProject, getProjectLabel } from "./detectors/index.js";
|
|
7
|
+
import { formatAIOptimized } from "./formatters/ai-optimized.js";
|
|
8
|
+
import { formatHuman } from "./formatters/human.js";
|
|
9
|
+
import { formatRaw } from "./formatters/raw.js";
|
|
10
|
+
import { parseFile } from "./parser.js";
|
|
11
|
+
import { scanDirectory } from "./scanner.js";
|
|
9
12
|
program
|
|
10
13
|
.name("ctx")
|
|
11
14
|
.description("Generate AI-ready context from your codebase")
|
|
12
|
-
.version("0.1.
|
|
13
|
-
.argument("[path]", "Path to scan", "
|
|
14
|
-
.option("
|
|
15
|
-
.option("--
|
|
15
|
+
.version("0.1.6")
|
|
16
|
+
.argument("[path]", "Path to scan", ".")
|
|
17
|
+
.option("--human", "Human-readable output for onboarding")
|
|
18
|
+
.option("--raw", "Verbose output with all details")
|
|
19
|
+
.option("-o, --output <format>", "Output format: json", "")
|
|
16
20
|
.action(async (targetPath, options) => {
|
|
17
21
|
const absolutePath = path.resolve(process.cwd(), targetPath);
|
|
18
|
-
// Detect project type
|
|
19
22
|
const projectRoot = await findProjectRoot(absolutePath);
|
|
20
23
|
const projectType = await detectProject(projectRoot);
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
const adapter = getAdapter(projectType);
|
|
25
|
+
// Only show scanning message for human/raw modes
|
|
26
|
+
if (options.human || options.raw) {
|
|
27
|
+
console.log(`\n📁 Scanning ${absolutePath}...`);
|
|
28
|
+
console.log(`📦 Detected: ${getProjectLabel(projectType)}\n`);
|
|
29
|
+
}
|
|
23
30
|
try {
|
|
24
|
-
// Scan all files
|
|
25
31
|
const files = await scanDirectory(absolutePath);
|
|
26
32
|
if (files.length === 0) {
|
|
27
33
|
console.log("No files found. Check your path.");
|
|
28
34
|
process.exit(1);
|
|
29
35
|
}
|
|
30
|
-
// Parse all files
|
|
31
36
|
const parsedFiles = await Promise.all(files.map((file) => parseFile(file)));
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
routes = await getRoutes(appDir);
|
|
37
|
+
const context = await adapter.analyze(absolutePath, parsedFiles);
|
|
38
|
+
// Output based on flags
|
|
39
|
+
if (options.output === "json") {
|
|
40
|
+
console.log(JSON.stringify(contextToJSON(context), null, 2));
|
|
37
41
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
routes,
|
|
41
|
-
features: groupByFolder(parsedFiles, absolutePath, "features"),
|
|
42
|
-
hooks: getHooks(parsedFiles),
|
|
43
|
-
lib: groupByFolder(parsedFiles, absolutePath, "lib"),
|
|
44
|
-
components: groupByFolder(parsedFiles, absolutePath, "components"),
|
|
45
|
-
};
|
|
46
|
-
// Output
|
|
47
|
-
if (options.ai) {
|
|
48
|
-
const { formatAI } = await import("./formatters/ai.js");
|
|
49
|
-
console.log(formatAI(context, absolutePath));
|
|
42
|
+
else if (options.raw) {
|
|
43
|
+
console.log(formatRaw(context));
|
|
50
44
|
}
|
|
51
|
-
else if (options.
|
|
52
|
-
console.log(
|
|
45
|
+
else if (options.human) {
|
|
46
|
+
console.log(formatHuman(context));
|
|
53
47
|
}
|
|
54
48
|
else {
|
|
55
|
-
|
|
49
|
+
// Default: AI-optimized
|
|
50
|
+
console.log(formatAIOptimized(context));
|
|
56
51
|
}
|
|
57
52
|
}
|
|
58
53
|
catch (error) {
|
|
@@ -73,58 +68,11 @@ async function findProjectRoot(startDir) {
|
|
|
73
68
|
}
|
|
74
69
|
return startDir;
|
|
75
70
|
}
|
|
76
|
-
async function findAppDirectory(basePath) {
|
|
77
|
-
const possiblePaths = [
|
|
78
|
-
path.join(basePath, "app"),
|
|
79
|
-
path.join(basePath, "src", "app"),
|
|
80
|
-
path.join(basePath, "src/app"),
|
|
81
|
-
];
|
|
82
|
-
for (const p of possiblePaths) {
|
|
83
|
-
try {
|
|
84
|
-
const stat = await fs.stat(p);
|
|
85
|
-
if (stat.isDirectory()) {
|
|
86
|
-
return p;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
// try next
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
function groupByFolder(files, basePath, folderName) {
|
|
96
|
-
const grouped = new Map();
|
|
97
|
-
for (const file of files) {
|
|
98
|
-
const relativePath = path.relative(basePath, file.filePath);
|
|
99
|
-
const parts = relativePath.split(path.sep);
|
|
100
|
-
const folderIndex = parts.indexOf(folderName);
|
|
101
|
-
if (folderIndex === -1)
|
|
102
|
-
continue;
|
|
103
|
-
const remainingParts = parts.slice(folderIndex + 1);
|
|
104
|
-
// Need at least 2 parts: subfolder + filename
|
|
105
|
-
if (remainingParts.length < 2)
|
|
106
|
-
continue;
|
|
107
|
-
const subFolder = remainingParts[0];
|
|
108
|
-
if (!grouped.has(subFolder)) {
|
|
109
|
-
grouped.set(subFolder, []);
|
|
110
|
-
}
|
|
111
|
-
grouped.get(subFolder).push(file);
|
|
112
|
-
}
|
|
113
|
-
return grouped;
|
|
114
|
-
}
|
|
115
|
-
function getHooks(files) {
|
|
116
|
-
return files.filter((f) => f.filePath.includes("/hooks/") ||
|
|
117
|
-
f.filePath.includes("\\hooks\\") ||
|
|
118
|
-
f.fileName.startsWith("use-") ||
|
|
119
|
-
f.fileName.startsWith("use."));
|
|
120
|
-
}
|
|
121
71
|
function contextToJSON(context) {
|
|
122
72
|
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
lib: Object.fromEntries(context.lib),
|
|
127
|
-
components: Object.fromEntries(context.components),
|
|
73
|
+
projectType: context.projectType,
|
|
74
|
+
routes: context.routes || [],
|
|
75
|
+
sections: Object.fromEntries(context.sections),
|
|
128
76
|
};
|
|
129
77
|
}
|
|
130
78
|
program.parse();
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export function formatAIOptimized(data) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
lines.push(`# Project Context (${data.projectType})`);
|
|
4
|
+
lines.push("");
|
|
5
|
+
// 1. Routes
|
|
6
|
+
if (data.routes && data.routes.length > 0) {
|
|
7
|
+
const routeGroups = parseRouteGroups(data.routes);
|
|
8
|
+
// Filter out empty shell routes
|
|
9
|
+
const filteredGroups = new Map();
|
|
10
|
+
for (const [key, value] of routeGroups) {
|
|
11
|
+
if (value.length > 0) {
|
|
12
|
+
filteredGroups.set(key, value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
lines.push(`## Routes (${filteredGroups.size})`);
|
|
16
|
+
for (const [group, paths] of filteredGroups) {
|
|
17
|
+
// Max 4 routes, no "+N more"
|
|
18
|
+
const displayPaths = paths.slice(0, 4);
|
|
19
|
+
lines.push(`/${group} → ${displayPaths.join(", ")}`);
|
|
20
|
+
}
|
|
21
|
+
lines.push("");
|
|
22
|
+
}
|
|
23
|
+
// 2. Core Domains
|
|
24
|
+
const features = getSectionsByPattern(data.sections, "features");
|
|
25
|
+
if (features.size > 0) {
|
|
26
|
+
lines.push(`## Core Domains (${features.size})`);
|
|
27
|
+
for (const [name, files] of features) {
|
|
28
|
+
lines.push(formatDomainLine(name, files));
|
|
29
|
+
}
|
|
30
|
+
lines.push("");
|
|
31
|
+
}
|
|
32
|
+
// 3. Auth & Session
|
|
33
|
+
const authFiles = getAuthFiles(data.sections);
|
|
34
|
+
if (authFiles.length > 0) {
|
|
35
|
+
lines.push("## Auth & Session");
|
|
36
|
+
lines.push("sign-in, session handling, token management");
|
|
37
|
+
lines.push("");
|
|
38
|
+
}
|
|
39
|
+
// 4. Shared Lib
|
|
40
|
+
const lib = getSectionsByPattern(data.sections, "lib");
|
|
41
|
+
if (lib.size > 0 || data.sections.has("LIB") || data.sections.has("Lib")) {
|
|
42
|
+
lines.push("## Shared Lib");
|
|
43
|
+
lines.push("utils — formatting, helpers");
|
|
44
|
+
const config = getSectionsByPattern(data.sections, "config");
|
|
45
|
+
if (config.size > 0) {
|
|
46
|
+
lines.push("config — api, uploads, pricing");
|
|
47
|
+
}
|
|
48
|
+
lines.push("");
|
|
49
|
+
}
|
|
50
|
+
// 5. Hooks
|
|
51
|
+
const hooks = data.sections.get("HOOKS") || data.sections.get("Hooks");
|
|
52
|
+
if (hooks && hooks.length > 0) {
|
|
53
|
+
const hookNames = hooks
|
|
54
|
+
.flatMap((f) => f.functions)
|
|
55
|
+
.filter((n) => n.startsWith("use"));
|
|
56
|
+
lines.push(`## Hooks (${hookNames.length})`);
|
|
57
|
+
lines.push(hookNames.join(", "));
|
|
58
|
+
lines.push("");
|
|
59
|
+
}
|
|
60
|
+
// 6. UI Layer
|
|
61
|
+
const components = getSectionsByPattern(data.sections, "components");
|
|
62
|
+
if (components.size > 0) {
|
|
63
|
+
const totalComponents = countTotalFiles(components);
|
|
64
|
+
lines.push("## UI Layer");
|
|
65
|
+
const folders = [...components.keys()]
|
|
66
|
+
.map((k) => k.split("/").pop()?.toLowerCase())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
const uniqueFolders = [...new Set(folders)];
|
|
69
|
+
lines.push(`~${totalComponents} components (${uniqueFolders.join(", ")})`);
|
|
70
|
+
lines.push("");
|
|
71
|
+
}
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
// === Route Parsing ===
|
|
75
|
+
function parseRouteGroups(routes) {
|
|
76
|
+
const groups = new Map();
|
|
77
|
+
let currentTopLevel = "";
|
|
78
|
+
let currentDynamic = "";
|
|
79
|
+
for (const route of routes) {
|
|
80
|
+
const trimmed = route.trim();
|
|
81
|
+
// Skip group markers like (auth), (website)
|
|
82
|
+
if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// Calculate depth by counting leading spaces
|
|
86
|
+
const depth = route.search(/\S/);
|
|
87
|
+
// Top-level route
|
|
88
|
+
if (depth === 0) {
|
|
89
|
+
currentTopLevel = trimmed;
|
|
90
|
+
currentDynamic = "";
|
|
91
|
+
if (!groups.has(currentTopLevel)) {
|
|
92
|
+
groups.set(currentTopLevel, []);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// First-level dynamic segment like [slug]
|
|
96
|
+
else if (depth === 2 && trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
97
|
+
currentDynamic = trimmed;
|
|
98
|
+
const dynamicKey = `${currentTopLevel}/${currentDynamic}`;
|
|
99
|
+
if (!groups.has(dynamicKey)) {
|
|
100
|
+
groups.set(dynamicKey, []);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Child routes
|
|
104
|
+
else if (depth >= 2) {
|
|
105
|
+
// Skip dynamic segments and technical routes
|
|
106
|
+
if (isSkippableRoute(trimmed)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Add to dynamic group if exists, otherwise to top level
|
|
110
|
+
if (currentDynamic && depth > 2) {
|
|
111
|
+
const dynamicKey = `${currentTopLevel}/${currentDynamic}`;
|
|
112
|
+
const children = groups.get(dynamicKey);
|
|
113
|
+
if (children && !children.includes(trimmed)) {
|
|
114
|
+
children.push(trimmed);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (currentTopLevel) {
|
|
118
|
+
const children = groups.get(currentTopLevel);
|
|
119
|
+
if (children && !children.includes(trimmed)) {
|
|
120
|
+
children.push(trimmed);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Clean up empty groups and dynamic groups with no children
|
|
126
|
+
const cleaned = new Map();
|
|
127
|
+
for (const [key, value] of groups) {
|
|
128
|
+
// Keep if has children OR is a standalone route
|
|
129
|
+
if (value.length > 0 || !key.includes("/")) {
|
|
130
|
+
cleaned.set(key, value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return cleaned;
|
|
134
|
+
}
|
|
135
|
+
function isSkippableRoute(route) {
|
|
136
|
+
const skipPatterns = [
|
|
137
|
+
"[",
|
|
138
|
+
"]",
|
|
139
|
+
"error",
|
|
140
|
+
"sync",
|
|
141
|
+
"verify-email",
|
|
142
|
+
"success",
|
|
143
|
+
"...nextauth",
|
|
144
|
+
];
|
|
145
|
+
const lower = route.toLowerCase();
|
|
146
|
+
return skipPatterns.some((p) => lower.includes(p));
|
|
147
|
+
}
|
|
148
|
+
// === Section Helpers ===
|
|
149
|
+
function getSectionsByPattern(sections, pattern) {
|
|
150
|
+
const result = new Map();
|
|
151
|
+
for (const [key, value] of sections) {
|
|
152
|
+
if (key.toLowerCase().includes(pattern.toLowerCase())) {
|
|
153
|
+
result.set(key, value);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
function getAuthFiles(sections) {
|
|
159
|
+
for (const [key, value] of sections) {
|
|
160
|
+
if (key.toLowerCase().includes("auth")) {
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
function countTotalFiles(sections) {
|
|
167
|
+
let count = 0;
|
|
168
|
+
for (const files of sections.values()) {
|
|
169
|
+
count += files.length;
|
|
170
|
+
}
|
|
171
|
+
return count;
|
|
172
|
+
}
|
|
173
|
+
// === Domain Formatting ===
|
|
174
|
+
function formatDomainLine(name, files) {
|
|
175
|
+
const cleanName = name
|
|
176
|
+
.replace(/features\//i, "")
|
|
177
|
+
.replace(/FEATURES\//i, "")
|
|
178
|
+
.toLowerCase();
|
|
179
|
+
const actionFiles = files.filter((f) => f.fileName.includes("action") || f.fileName.includes("actions"));
|
|
180
|
+
const actionCount = actionFiles.reduce((sum, f) => sum + f.functions.length, 0);
|
|
181
|
+
const intent = getIntent(cleanName, files);
|
|
182
|
+
return `${cleanName} — ${intent} (${actionCount} actions)`;
|
|
183
|
+
}
|
|
184
|
+
function getIntent(domain, files) {
|
|
185
|
+
const allFunctions = files
|
|
186
|
+
.flatMap((f) => f.functions)
|
|
187
|
+
.join(" ")
|
|
188
|
+
.toLowerCase();
|
|
189
|
+
// Domain-specific intent mapping
|
|
190
|
+
if (domain === "users") {
|
|
191
|
+
return "authenticate, edit profile, manage credentials";
|
|
192
|
+
}
|
|
193
|
+
if (domain === "orders") {
|
|
194
|
+
return "create/edit/cancel";
|
|
195
|
+
}
|
|
196
|
+
if (domain === "estimates") {
|
|
197
|
+
return "create/edit/convert";
|
|
198
|
+
}
|
|
199
|
+
if (domain === "files") {
|
|
200
|
+
return "upload/download";
|
|
201
|
+
}
|
|
202
|
+
// Fallback: derive from function names
|
|
203
|
+
const intents = [];
|
|
204
|
+
if (allFunctions.includes("create"))
|
|
205
|
+
intents.push("create");
|
|
206
|
+
if (allFunctions.includes("edit") || allFunctions.includes("update")) {
|
|
207
|
+
intents.push("edit");
|
|
208
|
+
}
|
|
209
|
+
if (allFunctions.includes("delete") || allFunctions.includes("cancel")) {
|
|
210
|
+
intents.push("cancel");
|
|
211
|
+
}
|
|
212
|
+
if (intents.length === 0)
|
|
213
|
+
return "manage";
|
|
214
|
+
return intents.slice(0, 3).join("/");
|
|
215
|
+
}
|
package/dist/formatters/ai.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function formatAI(data:
|
|
1
|
+
import type { AdapterContext } from "../adapters/types.js";
|
|
2
|
+
export declare function formatAI(data: AdapterContext): string;
|
package/dist/formatters/ai.js
CHANGED
|
@@ -1,57 +1,24 @@
|
|
|
1
|
-
export function formatAI(data
|
|
1
|
+
export function formatAI(data) {
|
|
2
2
|
const lines = [];
|
|
3
|
-
lines.push(
|
|
4
|
-
lines.push("");
|
|
5
|
-
lines.push("Use this to understand the codebase structure.");
|
|
3
|
+
lines.push(`# ${data.projectType} Project Context`);
|
|
6
4
|
lines.push("");
|
|
7
5
|
// Routes
|
|
8
|
-
if (data.routes.length > 0) {
|
|
6
|
+
if (data.routes && data.routes.length > 0) {
|
|
9
7
|
lines.push("## Routes");
|
|
10
8
|
lines.push("");
|
|
11
9
|
for (const route of data.routes) {
|
|
12
|
-
|
|
13
|
-
const name = route.trim();
|
|
14
|
-
const prefix = depth > 0 ? " ".repeat(depth / 2) + "└─ " : "- ";
|
|
15
|
-
lines.push(`${prefix}${name}`);
|
|
10
|
+
lines.push(`- ${route}`);
|
|
16
11
|
}
|
|
17
12
|
lines.push("");
|
|
18
13
|
}
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (data.components.size > 0) {
|
|
27
|
-
lines.push("## Components");
|
|
28
|
-
lines.push("");
|
|
29
|
-
lines.push(formatFolderCompact(data.components));
|
|
30
|
-
}
|
|
31
|
-
// Hooks
|
|
32
|
-
if (data.hooks.length > 0) {
|
|
33
|
-
lines.push("## Hooks");
|
|
14
|
+
// Sections
|
|
15
|
+
for (const [sectionName, files] of data.sections) {
|
|
16
|
+
if (sectionName === "_root")
|
|
17
|
+
continue;
|
|
18
|
+
if (files.length === 0)
|
|
19
|
+
continue;
|
|
20
|
+
lines.push(`## ${sectionName}`);
|
|
34
21
|
lines.push("");
|
|
35
|
-
for (const hook of data.hooks) {
|
|
36
|
-
const exports = getExportsSummary(hook);
|
|
37
|
-
if (exports) {
|
|
38
|
-
lines.push(`- ${hook.fileName}: ${exports}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
lines.push("");
|
|
42
|
-
}
|
|
43
|
-
// Lib
|
|
44
|
-
if (data.lib.size > 0) {
|
|
45
|
-
lines.push("## Lib / Utils");
|
|
46
|
-
lines.push("");
|
|
47
|
-
lines.push(formatFolderCompact(data.lib));
|
|
48
|
-
}
|
|
49
|
-
return lines.join("\n");
|
|
50
|
-
}
|
|
51
|
-
function formatFolderCompact(folders) {
|
|
52
|
-
const lines = [];
|
|
53
|
-
for (const [folderName, files] of folders) {
|
|
54
|
-
lines.push(`### ${folderName}`);
|
|
55
22
|
for (const file of files) {
|
|
56
23
|
const exports = getExportsSummary(file);
|
|
57
24
|
if (exports) {
|
|
@@ -65,19 +32,16 @@ function formatFolderCompact(folders) {
|
|
|
65
32
|
function getExportsSummary(file) {
|
|
66
33
|
const parts = [];
|
|
67
34
|
if (file.functions.length > 0) {
|
|
68
|
-
|
|
69
|
-
parts.push(fns);
|
|
35
|
+
parts.push(file.functions.map((f) => `${f}()`).join(", "));
|
|
70
36
|
}
|
|
71
37
|
if (file.constants.length > 0) {
|
|
72
38
|
parts.push(file.constants.join(", "));
|
|
73
39
|
}
|
|
74
40
|
if (file.types.length > 0) {
|
|
75
|
-
|
|
76
|
-
parts.push(types);
|
|
41
|
+
parts.push(file.types.map((t) => `type ${t}`).join(", "));
|
|
77
42
|
}
|
|
78
43
|
if (file.interfaces.length > 0) {
|
|
79
|
-
|
|
80
|
-
parts.push(ifaces);
|
|
44
|
+
parts.push(file.interfaces.map((i) => `interface ${i}`).join(", "));
|
|
81
45
|
}
|
|
82
46
|
return parts.join(" | ");
|
|
83
47
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
export function formatHuman(data) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
lines.push(`📁 Project: ${data.projectType} App`);
|
|
4
|
+
lines.push("");
|
|
5
|
+
// Routes - ordered: client, admin, api
|
|
6
|
+
if (data.routes && data.routes.length > 0) {
|
|
7
|
+
lines.push("┌─ Routes ─────────────────────────");
|
|
8
|
+
lines.push("│");
|
|
9
|
+
const routeGroups = parseRouteGroups(data.routes);
|
|
10
|
+
const orderedGroups = sortRouteGroups(routeGroups);
|
|
11
|
+
// Filter out empty groups
|
|
12
|
+
const filteredGroups = new Map();
|
|
13
|
+
for (const [key, value] of orderedGroups) {
|
|
14
|
+
if (value.length > 0) {
|
|
15
|
+
filteredGroups.set(key, value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const groupKeys = [...filteredGroups.keys()];
|
|
19
|
+
for (let i = 0; i < groupKeys.length; i++) {
|
|
20
|
+
const group = groupKeys[i];
|
|
21
|
+
const children = filteredGroups.get(group) || [];
|
|
22
|
+
const isLast = i === groupKeys.length - 1;
|
|
23
|
+
const prefix = isLast ? "└──" : "├──";
|
|
24
|
+
const childPrefix = isLast ? " " : "│ ";
|
|
25
|
+
const icon = getRouteIcon(group);
|
|
26
|
+
const displayGroup = group.split("/")[0];
|
|
27
|
+
lines.push(`${prefix} ${icon} ${capitalizeFirst(displayGroup)}`);
|
|
28
|
+
// Consolidate dynamic segments
|
|
29
|
+
const consolidatedChildren = consolidateRoutes(children);
|
|
30
|
+
// Show max 5 children
|
|
31
|
+
const visibleChildren = consolidatedChildren.slice(0, 5);
|
|
32
|
+
for (const child of visibleChildren) {
|
|
33
|
+
lines.push(`${childPrefix}/${child}`);
|
|
34
|
+
}
|
|
35
|
+
// Remaining as "(other X pages)"
|
|
36
|
+
if (consolidatedChildren.length > 5) {
|
|
37
|
+
const remaining = consolidatedChildren.length - 5;
|
|
38
|
+
const groupName = group.split("/")[0];
|
|
39
|
+
lines.push(`${childPrefix}(${remaining} more ${groupName} pages)`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
lines.push("");
|
|
43
|
+
}
|
|
44
|
+
// Domains
|
|
45
|
+
const features = getSectionsByPattern(data.sections, "features");
|
|
46
|
+
if (features.size > 0) {
|
|
47
|
+
lines.push("┌─ Domains ────────────────────────");
|
|
48
|
+
lines.push("│");
|
|
49
|
+
const featureNames = [...features.keys()];
|
|
50
|
+
for (let i = 0; i < featureNames.length; i++) {
|
|
51
|
+
const name = featureNames[i];
|
|
52
|
+
const cleanName = name.replace(/features\//i, "").toLowerCase();
|
|
53
|
+
const icon = getDomainIcon(cleanName);
|
|
54
|
+
const isLast = i === featureNames.length - 1;
|
|
55
|
+
const prefix = isLast ? "└──" : "├──";
|
|
56
|
+
lines.push(`${prefix} ${icon} features/${cleanName}`);
|
|
57
|
+
}
|
|
58
|
+
lines.push("");
|
|
59
|
+
}
|
|
60
|
+
// Infrastructure
|
|
61
|
+
lines.push("┌─ Infrastructure ─────────────────");
|
|
62
|
+
lines.push("│");
|
|
63
|
+
lines.push("├── auth / session");
|
|
64
|
+
lines.push("├── shared utils");
|
|
65
|
+
lines.push("└── ui components");
|
|
66
|
+
lines.push("");
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
// === Route Parsing ===
|
|
70
|
+
function parseRouteGroups(routes) {
|
|
71
|
+
const groups = new Map();
|
|
72
|
+
let currentTopLevel = "";
|
|
73
|
+
let currentDynamic = "";
|
|
74
|
+
for (const route of routes) {
|
|
75
|
+
const trimmed = route.trim();
|
|
76
|
+
if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const depth = route.search(/\S/);
|
|
80
|
+
if (depth === 0) {
|
|
81
|
+
currentTopLevel = trimmed;
|
|
82
|
+
currentDynamic = "";
|
|
83
|
+
if (!groups.has(currentTopLevel)) {
|
|
84
|
+
groups.set(currentTopLevel, []);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (depth === 2 && trimmed.startsWith("[")) {
|
|
88
|
+
// Dynamic segment at first level - create combined key
|
|
89
|
+
currentDynamic = trimmed;
|
|
90
|
+
const dynamicKey = `${currentTopLevel}/${currentDynamic}`;
|
|
91
|
+
if (!groups.has(dynamicKey)) {
|
|
92
|
+
groups.set(dynamicKey, []);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (currentTopLevel) {
|
|
96
|
+
if (!isSkippableRoute(trimmed)) {
|
|
97
|
+
// Add to dynamic group if exists
|
|
98
|
+
if (currentDynamic && depth > 2) {
|
|
99
|
+
const dynamicKey = `${currentTopLevel}/${currentDynamic}`;
|
|
100
|
+
const children = groups.get(dynamicKey);
|
|
101
|
+
if (children && !children.includes(trimmed)) {
|
|
102
|
+
children.push(trimmed);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const children = groups.get(currentTopLevel);
|
|
107
|
+
if (children && !children.includes(trimmed)) {
|
|
108
|
+
children.push(trimmed);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return groups;
|
|
115
|
+
}
|
|
116
|
+
function isSkippableRoute(route) {
|
|
117
|
+
const skip = ["error", "sync", "verify-email", "success", "...nextauth"];
|
|
118
|
+
const lower = route.toLowerCase();
|
|
119
|
+
return skip.some((p) => lower.includes(p));
|
|
120
|
+
}
|
|
121
|
+
function sortRouteGroups(groups) {
|
|
122
|
+
const sorted = new Map();
|
|
123
|
+
const order = ["client", "admin", "api"];
|
|
124
|
+
for (const key of order) {
|
|
125
|
+
for (const [group, children] of groups) {
|
|
126
|
+
if (group.toLowerCase().includes(key)) {
|
|
127
|
+
sorted.set(group, children);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const [group, children] of groups) {
|
|
132
|
+
if (!sorted.has(group)) {
|
|
133
|
+
sorted.set(group, children);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return sorted;
|
|
137
|
+
}
|
|
138
|
+
function consolidateRoutes(routes) {
|
|
139
|
+
const consolidated = [];
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
for (const route of routes) {
|
|
142
|
+
// Skip dynamic segments
|
|
143
|
+
if (route.startsWith("[") && route.endsWith("]")) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const baseName = route.toLowerCase();
|
|
147
|
+
if (!seen.has(baseName)) {
|
|
148
|
+
seen.add(baseName);
|
|
149
|
+
// Check if there's a dynamic child
|
|
150
|
+
const hasDynamic = routes.some((r) => r.startsWith("[") && r.toLowerCase().includes(baseName.slice(0, -1)));
|
|
151
|
+
if (hasDynamic) {
|
|
152
|
+
consolidated.push(`${route} (list, detail)`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
consolidated.push(route);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return consolidated;
|
|
160
|
+
}
|
|
161
|
+
// === Helpers ===
|
|
162
|
+
function getSectionsByPattern(sections, pattern) {
|
|
163
|
+
const result = new Map();
|
|
164
|
+
for (const [key, value] of sections) {
|
|
165
|
+
if (key.toLowerCase().includes(pattern.toLowerCase())) {
|
|
166
|
+
result.set(key, value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
function getRouteIcon(group) {
|
|
172
|
+
const lower = group.toLowerCase();
|
|
173
|
+
if (lower.includes("client") || lower.includes("user"))
|
|
174
|
+
return "👤";
|
|
175
|
+
if (lower.includes("admin"))
|
|
176
|
+
return "🛠️";
|
|
177
|
+
if (lower.includes("api"))
|
|
178
|
+
return "🔌";
|
|
179
|
+
if (lower.includes("auth") || lower.includes("login"))
|
|
180
|
+
return "🔐";
|
|
181
|
+
return "📄";
|
|
182
|
+
}
|
|
183
|
+
function getDomainIcon(name) {
|
|
184
|
+
if (name.includes("order"))
|
|
185
|
+
return "📦";
|
|
186
|
+
if (name.includes("estimate"))
|
|
187
|
+
return "📋";
|
|
188
|
+
if (name.includes("file"))
|
|
189
|
+
return "📁";
|
|
190
|
+
if (name.includes("user"))
|
|
191
|
+
return "👤";
|
|
192
|
+
if (name.includes("auth"))
|
|
193
|
+
return "🔐";
|
|
194
|
+
if (name.includes("payment"))
|
|
195
|
+
return "💳";
|
|
196
|
+
return "📂";
|
|
197
|
+
}
|
|
198
|
+
function capitalizeFirst(str) {
|
|
199
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
200
|
+
}
|
package/dist/formatters/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function formatMarkdown(data:
|
|
1
|
+
import type { AdapterContext } from "../adapters/types.js";
|
|
2
|
+
export declare function formatMarkdown(data: AdapterContext): string;
|
|
@@ -1,51 +1,55 @@
|
|
|
1
|
-
export function formatMarkdown(data
|
|
1
|
+
export function formatMarkdown(data) {
|
|
2
2
|
let output = "";
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// Project type header
|
|
4
|
+
output += `📦 Project: ${data.projectType}\n\n`;
|
|
5
|
+
// Routes section (if exists)
|
|
6
|
+
if (data.routes && data.routes.length > 0) {
|
|
7
|
+
output += `=== ROUTES ===\n\n`;
|
|
6
8
|
for (const route of data.routes) {
|
|
7
9
|
output += `${route}\n`;
|
|
8
10
|
}
|
|
9
11
|
output += "\n";
|
|
10
12
|
}
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
// All sections
|
|
14
|
+
for (const [sectionName, files] of data.sections) {
|
|
15
|
+
if (sectionName === "_root")
|
|
16
|
+
continue;
|
|
17
|
+
if (files.length === 0)
|
|
18
|
+
continue;
|
|
19
|
+
// Check if any file has exports
|
|
20
|
+
const hasExports = files.some((f) => f.functions.length > 0 ||
|
|
21
|
+
f.constants.length > 0 ||
|
|
22
|
+
f.types.length > 0 ||
|
|
23
|
+
f.interfaces.length > 0 ||
|
|
24
|
+
f.classes.length > 0);
|
|
25
|
+
if (!hasExports)
|
|
26
|
+
continue;
|
|
27
|
+
output += `=== ${sectionName.toUpperCase()} ===\n\n`;
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
output += formatFile(file);
|
|
26
30
|
}
|
|
27
31
|
output += "\n";
|
|
28
32
|
}
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
output +=
|
|
33
|
+
// Root files last
|
|
34
|
+
const rootFiles = data.sections.get("_root");
|
|
35
|
+
if (rootFiles && rootFiles.length > 0) {
|
|
36
|
+
const hasRootExports = rootFiles.some((f) => f.functions.length > 0 ||
|
|
37
|
+
f.constants.length > 0 ||
|
|
38
|
+
f.types.length > 0 ||
|
|
39
|
+
f.interfaces.length > 0 ||
|
|
40
|
+
f.classes.length > 0);
|
|
41
|
+
if (hasRootExports) {
|
|
42
|
+
output += `=== ROOT FILES ===\n\n`;
|
|
43
|
+
for (const file of rootFiles) {
|
|
44
|
+
output += formatFile(file);
|
|
45
|
+
}
|
|
46
|
+
output += "\n";
|
|
43
47
|
}
|
|
44
|
-
output += "\n";
|
|
45
48
|
}
|
|
49
|
+
output += `=== DONE ===\n`;
|
|
46
50
|
return output;
|
|
47
51
|
}
|
|
48
|
-
function
|
|
52
|
+
function formatFile(file) {
|
|
49
53
|
const hasExports = file.functions.length > 0 ||
|
|
50
54
|
file.constants.length > 0 ||
|
|
51
55
|
file.types.length > 0 ||
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function formatRaw(data) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
lines.push(`# ${data.projectType} Project Context`);
|
|
4
|
+
lines.push("");
|
|
5
|
+
// Routes
|
|
6
|
+
if (data.routes && data.routes.length > 0) {
|
|
7
|
+
lines.push("## Routes");
|
|
8
|
+
lines.push("");
|
|
9
|
+
for (const route of data.routes) {
|
|
10
|
+
lines.push(`- ${route}`);
|
|
11
|
+
}
|
|
12
|
+
lines.push("");
|
|
13
|
+
}
|
|
14
|
+
// Sections
|
|
15
|
+
for (const [sectionName, files] of data.sections) {
|
|
16
|
+
if (sectionName === "_root")
|
|
17
|
+
continue;
|
|
18
|
+
if (files.length === 0)
|
|
19
|
+
continue;
|
|
20
|
+
lines.push(`## ${sectionName}`);
|
|
21
|
+
lines.push("");
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const exports = getExportsSummary(file);
|
|
24
|
+
if (exports) {
|
|
25
|
+
lines.push(`- ${file.fileName}: ${exports}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
lines.push("");
|
|
29
|
+
}
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
function getExportsSummary(file) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
if (file.functions.length > 0) {
|
|
35
|
+
parts.push(file.functions.map((f) => `${f}()`).join(", "));
|
|
36
|
+
}
|
|
37
|
+
if (file.constants.length > 0) {
|
|
38
|
+
parts.push(file.constants.join(", "));
|
|
39
|
+
}
|
|
40
|
+
if (file.types.length > 0) {
|
|
41
|
+
parts.push(file.types.map((t) => `type ${t}`).join(", "));
|
|
42
|
+
}
|
|
43
|
+
if (file.interfaces.length > 0) {
|
|
44
|
+
parts.push(file.interfaces.map((i) => `interface ${i}`).join(", "));
|
|
45
|
+
}
|
|
46
|
+
return parts.join(" | ");
|
|
47
|
+
}
|
package/dist/parser.js
CHANGED
|
@@ -6,22 +6,27 @@ export async function parseFile(filePath) {
|
|
|
6
6
|
return {
|
|
7
7
|
filePath,
|
|
8
8
|
fileName,
|
|
9
|
-
functions:
|
|
10
|
-
|
|
9
|
+
functions: [
|
|
10
|
+
...extractFunctions(content),
|
|
11
|
+
...extractCommonJSFunctions(content),
|
|
12
|
+
],
|
|
13
|
+
constants: [
|
|
14
|
+
...extractConstants(content),
|
|
15
|
+
...extractCommonJSConstants(content),
|
|
16
|
+
],
|
|
11
17
|
types: extractTypes(content),
|
|
12
18
|
interfaces: extractInterfaces(content),
|
|
13
|
-
classes: extractClasses(content),
|
|
19
|
+
classes: [...extractClasses(content), ...extractMongooseModels(content)],
|
|
14
20
|
defaultExport: extractDefaultExport(content),
|
|
15
21
|
};
|
|
16
22
|
}
|
|
23
|
+
// ESM Exports
|
|
17
24
|
function extractFunctions(content) {
|
|
18
25
|
const functions = [];
|
|
19
|
-
// export function name
|
|
20
26
|
const funcMatches = content.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g);
|
|
21
27
|
for (const match of funcMatches) {
|
|
22
28
|
functions.push(match[1]);
|
|
23
29
|
}
|
|
24
|
-
// export const name = async? (...) => or function(
|
|
25
30
|
const arrowMatches = content.matchAll(/export\s+const\s+(\w+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*(?:=>|\{)/g);
|
|
26
31
|
for (const match of arrowMatches) {
|
|
27
32
|
if (!functions.includes(match[1])) {
|
|
@@ -32,15 +37,12 @@ function extractFunctions(content) {
|
|
|
32
37
|
}
|
|
33
38
|
function extractConstants(content) {
|
|
34
39
|
const constants = [];
|
|
35
|
-
// Match export const that are NOT functions
|
|
36
40
|
const lines = content.split("\n");
|
|
37
41
|
for (const line of lines) {
|
|
38
|
-
// export const NAME = value (not a function)
|
|
39
42
|
const match = line.match(/export\s+const\s+(\w+)\s*=\s*(?!(?:async\s*)?(?:\(|function|\w+\s*=>))/);
|
|
40
43
|
if (match) {
|
|
41
44
|
constants.push(match[1]);
|
|
42
45
|
}
|
|
43
|
-
// Also catch: export const NAME: Type =
|
|
44
46
|
const typedMatch = line.match(/export\s+const\s+(\w+)\s*:\s*[^=]+=\s*(?!(?:async\s*)?(?:\(|function|\w+\s*=>))/);
|
|
45
47
|
if (typedMatch && !constants.includes(typedMatch[1])) {
|
|
46
48
|
constants.push(typedMatch[1]);
|
|
@@ -61,13 +63,68 @@ function extractClasses(content) {
|
|
|
61
63
|
return [...matches].map((m) => m[1]);
|
|
62
64
|
}
|
|
63
65
|
function extractDefaultExport(content) {
|
|
64
|
-
// export default function Name
|
|
65
66
|
const funcMatch = content.match(/export\s+default\s+function\s+(\w+)/);
|
|
66
67
|
if (funcMatch)
|
|
67
68
|
return funcMatch[1];
|
|
68
|
-
// export default Name
|
|
69
69
|
const simpleMatch = content.match(/export\s+default\s+(\w+)/);
|
|
70
70
|
if (simpleMatch)
|
|
71
71
|
return simpleMatch[1];
|
|
72
72
|
return undefined;
|
|
73
73
|
}
|
|
74
|
+
// CommonJS Exports
|
|
75
|
+
function extractCommonJSFunctions(content) {
|
|
76
|
+
const functions = [];
|
|
77
|
+
// exports.functionName = async (req, res) => { }
|
|
78
|
+
const exportsMatches = content.matchAll(/exports\.(\w+)\s*=\s*(?:async\s*)?(?:function|\(|async\s*\()/g);
|
|
79
|
+
for (const match of exportsMatches) {
|
|
80
|
+
if (!functions.includes(match[1])) {
|
|
81
|
+
functions.push(match[1]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// module.exports.functionName = async (req, res) => { }
|
|
85
|
+
const moduleExportsMatches = content.matchAll(/module\.exports\.(\w+)\s*=\s*(?:async\s*)?(?:function|\(|async\s*\()/g);
|
|
86
|
+
for (const match of moduleExportsMatches) {
|
|
87
|
+
if (!functions.includes(match[1])) {
|
|
88
|
+
functions.push(match[1]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// module.exports = { functionName, anotherFunction }
|
|
92
|
+
const objectExportMatch = content.match(/module\.exports\s*=\s*\{([^}]+)\}/);
|
|
93
|
+
if (objectExportMatch) {
|
|
94
|
+
const names = objectExportMatch[1]
|
|
95
|
+
.split(",")
|
|
96
|
+
.map((s) => s.trim().split(":")[0].trim());
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
if (name && /^\w+$/.test(name) && !functions.includes(name)) {
|
|
99
|
+
functions.push(name);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return functions;
|
|
104
|
+
}
|
|
105
|
+
function extractCommonJSConstants(content) {
|
|
106
|
+
const constants = [];
|
|
107
|
+
// exports.CONSTANT_NAME = "value" or = { } (not functions)
|
|
108
|
+
const lines = content.split("\n");
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
// exports.NAME = "value" or number or object (not function)
|
|
111
|
+
const match = line.match(/exports\.(\w+)\s*=\s*(?!(?:async\s*)?(?:function|\(|async\s*\())/);
|
|
112
|
+
if (match) {
|
|
113
|
+
// Check it's likely a constant (UPPER_CASE or starts with config/options)
|
|
114
|
+
const name = match[1];
|
|
115
|
+
if (/^[A-Z_]+$/.test(name) || /^(config|options|settings)/i.test(name)) {
|
|
116
|
+
constants.push(name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return constants;
|
|
121
|
+
}
|
|
122
|
+
function extractMongooseModels(content) {
|
|
123
|
+
const models = [];
|
|
124
|
+
// mongoose.model('ModelName', schema)
|
|
125
|
+
const matches = content.matchAll(/mongoose\.model\s*\(\s*['"](\w+)['"]/g);
|
|
126
|
+
for (const match of matches) {
|
|
127
|
+
models.push(match[1]);
|
|
128
|
+
}
|
|
129
|
+
return models;
|
|
130
|
+
}
|