@eloquence98/ctx 0.1.5 ā 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.
- package/README.md +30 -12
- package/dist/cli.js +21 -67
- 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 +40 -36
- package/dist/formatters/human.d.ts +2 -0
- package/dist/formatters/human.js +53 -0
- package/dist/formatters/index.d.ts +1 -1
- package/dist/formatters/index.js +1 -1
- package/dist/formatters/raw.d.ts +2 -0
- package/dist/formatters/raw.js +47 -0
- package/dist/organizer.d.ts +2 -0
- package/dist/organizer.js +70 -0
- package/dist/parser.d.ts +2 -2
- package/dist/parser.js +28 -118
- package/dist/scanner.d.ts +1 -3
- package/dist/scanner.js +13 -53
- package/dist/types.d.ts +14 -20
- package/package.json +3 -7
package/README.md
CHANGED
|
@@ -1,32 +1,50 @@
|
|
|
1
1
|
# ctx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scan your codebase. Get a clean summary. Paste it to AI.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
# Scan current directory
|
|
9
|
+
ctx .
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
That's it. Copy the output, paste it to ChatGPT/Claude.
|
|
12
|
-
|
|
13
|
-
## Options
|
|
14
|
-
|
|
15
12
|
```bash
|
|
16
|
-
#
|
|
17
|
-
|
|
13
|
+
# Scan specific folder
|
|
14
|
+
ctx ./src
|
|
18
15
|
```
|
|
19
16
|
|
|
20
17
|
```bash
|
|
21
|
-
#
|
|
22
|
-
|
|
18
|
+
# Human-readable output
|
|
19
|
+
ctx ./src --human
|
|
23
20
|
```
|
|
24
21
|
|
|
22
|
+
## Output Example
|
|
23
|
+
|
|
25
24
|
```bash
|
|
26
|
-
#
|
|
27
|
-
|
|
25
|
+
# Codebase Context
|
|
26
|
+
|
|
27
|
+
## Routes
|
|
28
|
+
- /admin
|
|
29
|
+
- /admin/orders
|
|
30
|
+
- /client/[slug]/orders
|
|
31
|
+
|
|
32
|
+
## Components
|
|
33
|
+
- navbar.tsx: Navbar
|
|
34
|
+
- sidebar.tsx: Sidebar
|
|
35
|
+
|
|
36
|
+
## Hooks
|
|
37
|
+
- useAuth
|
|
38
|
+
- useFetch
|
|
39
|
+
|
|
40
|
+
## Utils
|
|
41
|
+
- utils.ts: cn, formatDate
|
|
28
42
|
```
|
|
29
43
|
|
|
44
|
+
## That's It
|
|
45
|
+
|
|
46
|
+
Copy. Paste to ChatGPT/Claude. Done.
|
|
47
|
+
|
|
30
48
|
## License
|
|
31
49
|
|
|
32
50
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -1,73 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { program } from "commander";
|
|
3
2
|
import path from "path";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { detectProject, getProjectLabel } from "./detectors/index.js";
|
|
8
|
-
import { getAdapter } from "./adapters/index.js";
|
|
9
|
-
import { formatMarkdown } from "./formatters/markdown.js";
|
|
3
|
+
import { scan } from "./scanner.js";
|
|
4
|
+
import { parse } from "./parser.js";
|
|
5
|
+
import { organize } from "./organizer.js";
|
|
10
6
|
import { formatAI } from "./formatters/ai.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const projectRoot = await findProjectRoot(absolutePath);
|
|
22
|
-
const projectType = await detectProject(projectRoot);
|
|
23
|
-
const adapter = getAdapter(projectType);
|
|
24
|
-
console.log(`\nš Scanning ${absolutePath}...`);
|
|
25
|
-
console.log(`š¦ Detected: ${getProjectLabel(projectType)}\n`);
|
|
26
|
-
try {
|
|
27
|
-
// Scan all files
|
|
28
|
-
const files = await scanDirectory(absolutePath);
|
|
29
|
-
if (files.length === 0) {
|
|
30
|
-
console.log("No files found. Check your path.");
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
// Parse all files
|
|
34
|
-
const parsedFiles = await Promise.all(files.map((file) => parseFile(file)));
|
|
35
|
-
// Use adapter to analyze
|
|
36
|
-
const context = await adapter.analyze(absolutePath, parsedFiles);
|
|
37
|
-
// Output
|
|
38
|
-
if (options.output === "json") {
|
|
39
|
-
console.log(JSON.stringify(contextToJSON(context), null, 2));
|
|
40
|
-
}
|
|
41
|
-
else if (options.ai) {
|
|
42
|
-
console.log(formatAI(context));
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
console.log(formatMarkdown(context));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
console.error("Error:", error);
|
|
7
|
+
import { formatHuman } from "./formatters/human.js";
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const targetPath = args.find((a) => !a.startsWith("-")) || ".";
|
|
10
|
+
const humanMode = args.includes("--human");
|
|
11
|
+
async function main() {
|
|
12
|
+
const dir = path.resolve(process.cwd(), targetPath);
|
|
13
|
+
// 1. Scan
|
|
14
|
+
const files = await scan(dir);
|
|
15
|
+
if (files.length === 0) {
|
|
16
|
+
console.log("No files found.");
|
|
50
17
|
process.exit(1);
|
|
51
18
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
current = path.dirname(current);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return startDir;
|
|
65
|
-
}
|
|
66
|
-
function contextToJSON(context) {
|
|
67
|
-
return {
|
|
68
|
-
projectType: context.projectType,
|
|
69
|
-
routes: context.routes || [],
|
|
70
|
-
sections: Object.fromEntries(context.sections),
|
|
71
|
-
};
|
|
19
|
+
// 2. Parse
|
|
20
|
+
const parsed = await Promise.all(files.map(parse));
|
|
21
|
+
// 3. Organize
|
|
22
|
+
const context = await organize(parsed, dir);
|
|
23
|
+
// 4. Format
|
|
24
|
+
const output = humanMode ? formatHuman(context) : formatAI(context);
|
|
25
|
+
console.log(output);
|
|
72
26
|
}
|
|
73
|
-
|
|
27
|
+
main().catch(console.error);
|
|
@@ -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(
|
|
1
|
+
import type { OrganizedContext } from "../types.js";
|
|
2
|
+
export declare function formatAI(ctx: OrganizedContext): string;
|
package/dist/formatters/ai.js
CHANGED
|
@@ -1,47 +1,51 @@
|
|
|
1
|
-
export function formatAI(
|
|
2
|
-
const lines = [];
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (
|
|
7
|
-
lines.push("##
|
|
1
|
+
export function formatAI(ctx) {
|
|
2
|
+
const lines = ["# Codebase Context", ""];
|
|
3
|
+
if (ctx.routes.length) {
|
|
4
|
+
lines.push("## Routes", ...ctx.routes.map((r) => `- ${r}`), "");
|
|
5
|
+
}
|
|
6
|
+
if (ctx.components.length) {
|
|
7
|
+
lines.push("## Components");
|
|
8
|
+
for (const f of ctx.components) {
|
|
9
|
+
const exp = f.exports.components.join(", ");
|
|
10
|
+
if (exp) {
|
|
11
|
+
lines.push(`- ${f.name}: ${exp}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
8
14
|
lines.push("");
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
}
|
|
16
|
+
if (ctx.hooks.length) {
|
|
17
|
+
lines.push("## Hooks");
|
|
18
|
+
for (const f of ctx.hooks) {
|
|
19
|
+
const hookFns = f.exports.functions.filter((fn) => fn.startsWith("use"));
|
|
20
|
+
if (hookFns.length) {
|
|
21
|
+
lines.push(`- ${hookFns.join(", ")}`);
|
|
22
|
+
}
|
|
11
23
|
}
|
|
12
24
|
lines.push("");
|
|
13
25
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
if (ctx.utils.length) {
|
|
27
|
+
lines.push("## Utils");
|
|
28
|
+
for (const f of ctx.utils) {
|
|
29
|
+
const exp = [...f.exports.functions, ...f.exports.constants].join(", ");
|
|
30
|
+
if (exp) {
|
|
31
|
+
lines.push(`- ${f.name}: ${exp}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
21
34
|
lines.push("");
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
}
|
|
36
|
+
if (ctx.other.length) {
|
|
37
|
+
lines.push("## Other");
|
|
38
|
+
for (const f of ctx.other) {
|
|
39
|
+
const exp = [
|
|
40
|
+
...f.exports.functions,
|
|
41
|
+
...f.exports.constants,
|
|
42
|
+
...f.exports.components,
|
|
43
|
+
].join(", ");
|
|
44
|
+
if (exp) {
|
|
45
|
+
lines.push(`- ${f.name}: ${exp}`);
|
|
26
46
|
}
|
|
27
47
|
}
|
|
28
48
|
lines.push("");
|
|
29
49
|
}
|
|
30
50
|
return lines.join("\n");
|
|
31
51
|
}
|
|
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
|
-
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function formatHuman(ctx) {
|
|
2
|
+
const lines = [
|
|
3
|
+
"āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā®",
|
|
4
|
+
"ā CODEBASE OVERVIEW ā",
|
|
5
|
+
"ā°āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāÆ",
|
|
6
|
+
"",
|
|
7
|
+
];
|
|
8
|
+
if (ctx.routes.length) {
|
|
9
|
+
lines.push("š ROUTES", "");
|
|
10
|
+
for (const r of ctx.routes) {
|
|
11
|
+
lines.push(` ${r}`);
|
|
12
|
+
}
|
|
13
|
+
lines.push("");
|
|
14
|
+
}
|
|
15
|
+
if (ctx.components.length) {
|
|
16
|
+
lines.push(`š§© COMPONENTS (${ctx.components.length} files)`, "");
|
|
17
|
+
for (const f of ctx.components.slice(0, 10)) {
|
|
18
|
+
if (f.exports.components.length) {
|
|
19
|
+
lines.push(` ${f.name}`);
|
|
20
|
+
for (const c of f.exports.components.slice(0, 3)) {
|
|
21
|
+
lines.push(` āā ${c}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (ctx.components.length > 10) {
|
|
26
|
+
lines.push(` ... and ${ctx.components.length - 10} more`);
|
|
27
|
+
}
|
|
28
|
+
lines.push("");
|
|
29
|
+
}
|
|
30
|
+
if (ctx.hooks.length) {
|
|
31
|
+
const allHooks = ctx.hooks.flatMap((f) => f.exports.functions.filter((fn) => fn.startsWith("use")));
|
|
32
|
+
if (allHooks.length) {
|
|
33
|
+
lines.push(`šŖ HOOKS (${allHooks.length})`, "");
|
|
34
|
+
for (const hook of allHooks) {
|
|
35
|
+
lines.push(` ⢠${hook}()`);
|
|
36
|
+
}
|
|
37
|
+
lines.push("");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (ctx.utils.length) {
|
|
41
|
+
lines.push(`š§ UTILITIES`, "");
|
|
42
|
+
for (const f of ctx.utils.slice(0, 10)) {
|
|
43
|
+
if (f.exports.functions.length) {
|
|
44
|
+
lines.push(` ${f.name}`);
|
|
45
|
+
for (const fn of f.exports.functions.slice(0, 3)) {
|
|
46
|
+
lines.push(` ⢠${fn}()`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
lines.push("");
|
|
51
|
+
}
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
package/dist/formatters/index.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
export async function organize(files, baseDir) {
|
|
4
|
+
const routes = await getRoutes(baseDir);
|
|
5
|
+
const components = [];
|
|
6
|
+
const hooks = [];
|
|
7
|
+
const utils = [];
|
|
8
|
+
const other = [];
|
|
9
|
+
for (const file of files) {
|
|
10
|
+
const rel = file.path.toLowerCase();
|
|
11
|
+
const hasHooks = file.exports.functions.some((fn) => fn.startsWith("use"));
|
|
12
|
+
if (hasHooks || file.name.startsWith("use") || rel.includes("/hooks/")) {
|
|
13
|
+
hooks.push(file);
|
|
14
|
+
}
|
|
15
|
+
else if (rel.includes("/components/") || rel.includes("/ui/")) {
|
|
16
|
+
components.push(file);
|
|
17
|
+
}
|
|
18
|
+
else if (rel.includes("/lib/") ||
|
|
19
|
+
rel.includes("/utils/") ||
|
|
20
|
+
rel.includes("/helpers/")) {
|
|
21
|
+
utils.push(file);
|
|
22
|
+
}
|
|
23
|
+
else if (!rel.includes("/app/") && !rel.includes("/pages/")) {
|
|
24
|
+
other.push(file);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { routes, components, hooks, utils, other };
|
|
28
|
+
}
|
|
29
|
+
async function getRoutes(baseDir) {
|
|
30
|
+
const possibleDirs = [
|
|
31
|
+
path.join(baseDir, "app"),
|
|
32
|
+
path.join(baseDir, "src", "app"),
|
|
33
|
+
path.join(baseDir, "pages"),
|
|
34
|
+
path.join(baseDir, "src", "pages"),
|
|
35
|
+
];
|
|
36
|
+
for (const dir of possibleDirs) {
|
|
37
|
+
try {
|
|
38
|
+
await fs.access(dir);
|
|
39
|
+
return walkRoutes(dir, "");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
async function walkRoutes(dir, prefix) {
|
|
48
|
+
const routes = [];
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return routes;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (!entry.isDirectory())
|
|
58
|
+
continue;
|
|
59
|
+
if (entry.name.startsWith("_"))
|
|
60
|
+
continue;
|
|
61
|
+
if (entry.name.startsWith("("))
|
|
62
|
+
continue;
|
|
63
|
+
if (entry.name === "api")
|
|
64
|
+
continue;
|
|
65
|
+
const route = prefix + "/" + entry.name;
|
|
66
|
+
routes.push(route);
|
|
67
|
+
routes.push(...(await walkRoutes(path.join(dir, entry.name), route)));
|
|
68
|
+
}
|
|
69
|
+
return routes;
|
|
70
|
+
}
|
package/dist/parser.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function
|
|
1
|
+
import type { ParsedFile } from "./types.js";
|
|
2
|
+
export declare function parse(filePath: string): Promise<ParsedFile>;
|
package/dist/parser.js
CHANGED
|
@@ -1,130 +1,40 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
-
export async function
|
|
3
|
+
export async function parse(filePath) {
|
|
4
4
|
const content = await fs.readFile(filePath, "utf-8");
|
|
5
|
-
const
|
|
6
|
-
return {
|
|
7
|
-
filePath,
|
|
8
|
-
fileName,
|
|
9
|
-
functions: [
|
|
10
|
-
...extractFunctions(content),
|
|
11
|
-
...extractCommonJSFunctions(content),
|
|
12
|
-
],
|
|
13
|
-
constants: [
|
|
14
|
-
...extractConstants(content),
|
|
15
|
-
...extractCommonJSConstants(content),
|
|
16
|
-
],
|
|
17
|
-
types: extractTypes(content),
|
|
18
|
-
interfaces: extractInterfaces(content),
|
|
19
|
-
classes: [...extractClasses(content), ...extractMongooseModels(content)],
|
|
20
|
-
defaultExport: extractDefaultExport(content),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
// ESM Exports
|
|
24
|
-
function extractFunctions(content) {
|
|
5
|
+
const name = path.basename(filePath);
|
|
25
6
|
const functions = [];
|
|
26
|
-
const funcMatches = content.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g);
|
|
27
|
-
for (const match of funcMatches) {
|
|
28
|
-
functions.push(match[1]);
|
|
29
|
-
}
|
|
30
|
-
const arrowMatches = content.matchAll(/export\s+const\s+(\w+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*(?:=>|\{)/g);
|
|
31
|
-
for (const match of arrowMatches) {
|
|
32
|
-
if (!functions.includes(match[1])) {
|
|
33
|
-
functions.push(match[1]);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return functions;
|
|
37
|
-
}
|
|
38
|
-
function extractConstants(content) {
|
|
39
7
|
const constants = [];
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
function extractTypes(content) {
|
|
54
|
-
const matches = content.matchAll(/export\s+type\s+(\w+)/g);
|
|
55
|
-
return [...matches].map((m) => m[1]);
|
|
56
|
-
}
|
|
57
|
-
function extractInterfaces(content) {
|
|
58
|
-
const matches = content.matchAll(/export\s+interface\s+(\w+)/g);
|
|
59
|
-
return [...matches].map((m) => m[1]);
|
|
60
|
-
}
|
|
61
|
-
function extractClasses(content) {
|
|
62
|
-
const matches = content.matchAll(/export\s+(?:default\s+)?class\s+(\w+)/g);
|
|
63
|
-
return [...matches].map((m) => m[1]);
|
|
64
|
-
}
|
|
65
|
-
function extractDefaultExport(content) {
|
|
66
|
-
const funcMatch = content.match(/export\s+default\s+function\s+(\w+)/);
|
|
67
|
-
if (funcMatch)
|
|
68
|
-
return funcMatch[1];
|
|
69
|
-
const simpleMatch = content.match(/export\s+default\s+(\w+)/);
|
|
70
|
-
if (simpleMatch)
|
|
71
|
-
return simpleMatch[1];
|
|
72
|
-
return undefined;
|
|
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]);
|
|
8
|
+
const types = [];
|
|
9
|
+
const components = [];
|
|
10
|
+
// Export function
|
|
11
|
+
for (const match of content.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g)) {
|
|
12
|
+
const fn = match[1];
|
|
13
|
+
isPascalCase(fn) ? components.push(fn) : functions.push(fn);
|
|
14
|
+
}
|
|
15
|
+
// Export const
|
|
16
|
+
for (const match of content.matchAll(/export\s+const\s+(\w+)\s*=/g)) {
|
|
17
|
+
const n = match[1];
|
|
18
|
+
if (isPascalCase(n)) {
|
|
19
|
+
components.push(n);
|
|
82
20
|
}
|
|
83
|
-
|
|
84
|
-
|
|
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]);
|
|
21
|
+
else if (n.startsWith("use")) {
|
|
22
|
+
functions.push(n);
|
|
89
23
|
}
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
}
|
|
24
|
+
else {
|
|
25
|
+
constants.push(n);
|
|
101
26
|
}
|
|
102
27
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
}
|
|
28
|
+
// Types & interfaces
|
|
29
|
+
for (const match of content.matchAll(/export\s+(?:type|interface)\s+(\w+)/g)) {
|
|
30
|
+
types.push(match[1]);
|
|
119
31
|
}
|
|
120
|
-
return
|
|
32
|
+
return {
|
|
33
|
+
path: filePath,
|
|
34
|
+
name,
|
|
35
|
+
exports: { functions, constants, types, components },
|
|
36
|
+
};
|
|
121
37
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
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;
|
|
38
|
+
function isPascalCase(str) {
|
|
39
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(str);
|
|
130
40
|
}
|
package/dist/scanner.d.ts
CHANGED
|
@@ -1,3 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function scanDirectory(dir: string, options?: Partial<ScanOptions>): Promise<string[]>;
|
|
3
|
-
export declare function getRoutes(appDir: string): Promise<string[]>;
|
|
1
|
+
export declare function scan(dir: string): Promise<string[]>;
|
package/dist/scanner.js
CHANGED
|
@@ -1,38 +1,29 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const IGNORE = ["node_modules", ".git", "dist", ".next", "build", "__tests__"];
|
|
4
|
+
const EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
|
|
5
|
+
export async function scan(dir) {
|
|
6
6
|
const files = [];
|
|
7
|
-
async function walk(
|
|
8
|
-
if (depth > config.maxDepth)
|
|
9
|
-
return;
|
|
7
|
+
async function walk(current) {
|
|
10
8
|
let entries;
|
|
11
9
|
try {
|
|
12
|
-
entries = await fs.readdir(
|
|
10
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
13
11
|
}
|
|
14
12
|
catch {
|
|
15
13
|
return;
|
|
16
14
|
}
|
|
17
15
|
for (const entry of entries) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (pattern.includes("*")) {
|
|
22
|
-
const regex = new RegExp(pattern.replace(/\./g, "\\.").replace(/\*/g, ".*"));
|
|
23
|
-
return regex.test(entry.name);
|
|
24
|
-
}
|
|
25
|
-
return entry.name === pattern;
|
|
26
|
-
});
|
|
27
|
-
if (shouldIgnore)
|
|
16
|
+
if (IGNORE.includes(entry.name))
|
|
17
|
+
continue;
|
|
18
|
+
if (entry.name.startsWith("."))
|
|
28
19
|
continue;
|
|
20
|
+
const full = path.join(current, entry.name);
|
|
29
21
|
if (entry.isDirectory()) {
|
|
30
|
-
await walk(
|
|
22
|
+
await walk(full);
|
|
31
23
|
}
|
|
32
|
-
else if (entry.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
files.push(fullPath);
|
|
24
|
+
else if (EXTENSIONS.includes(path.extname(entry.name))) {
|
|
25
|
+
if (!entry.name.includes(".test.") && !entry.name.includes(".spec.")) {
|
|
26
|
+
files.push(full);
|
|
36
27
|
}
|
|
37
28
|
}
|
|
38
29
|
}
|
|
@@ -40,34 +31,3 @@ export async function scanDirectory(dir, options = {}) {
|
|
|
40
31
|
await walk(dir);
|
|
41
32
|
return files;
|
|
42
33
|
}
|
|
43
|
-
export async function getRoutes(appDir) {
|
|
44
|
-
const routes = [];
|
|
45
|
-
async function walkRoutes(dir, indent = "") {
|
|
46
|
-
let entries;
|
|
47
|
-
try {
|
|
48
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
// Sort: groups first (parentheses), then regular folders
|
|
54
|
-
const sorted = entries
|
|
55
|
-
.filter((e) => e.isDirectory())
|
|
56
|
-
.filter((e) => !e.name.startsWith("_"))
|
|
57
|
-
.sort((a, b) => {
|
|
58
|
-
const aIsGroup = a.name.startsWith("(");
|
|
59
|
-
const bIsGroup = b.name.startsWith("(");
|
|
60
|
-
if (aIsGroup && !bIsGroup)
|
|
61
|
-
return -1;
|
|
62
|
-
if (!aIsGroup && bIsGroup)
|
|
63
|
-
return 1;
|
|
64
|
-
return a.name.localeCompare(b.name);
|
|
65
|
-
});
|
|
66
|
-
for (const entry of sorted) {
|
|
67
|
-
routes.push(`${indent}${entry.name}`);
|
|
68
|
-
await walkRoutes(path.join(dir, entry.name), indent + " ");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
await walkRoutes(appDir);
|
|
72
|
-
return routes;
|
|
73
|
-
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
export interface ParsedFile {
|
|
2
|
+
path: string;
|
|
3
|
+
name: string;
|
|
4
|
+
exports: {
|
|
5
|
+
functions: string[];
|
|
6
|
+
constants: string[];
|
|
7
|
+
types: string[];
|
|
8
|
+
components: string[];
|
|
9
|
+
};
|
|
10
10
|
}
|
|
11
|
-
export interface
|
|
12
|
-
entry: string;
|
|
13
|
-
extensions: string[];
|
|
14
|
-
ignore: string[];
|
|
15
|
-
maxDepth: number;
|
|
16
|
-
}
|
|
17
|
-
export interface ProjectContext {
|
|
11
|
+
export interface OrganizedContext {
|
|
18
12
|
routes: string[];
|
|
19
|
-
|
|
20
|
-
hooks:
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
components: ParsedFile[];
|
|
14
|
+
hooks: ParsedFile[];
|
|
15
|
+
utils: ParsedFile[];
|
|
16
|
+
other: ParsedFile[];
|
|
23
17
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eloquence98/ctx",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Scan your codebase. Get a clean summary. Paste it to AI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ctx": "dist/cli.js"
|
|
@@ -35,9 +35,5 @@
|
|
|
35
35
|
"@types/node": "^20.0.0",
|
|
36
36
|
"tsx": "^4.0.0",
|
|
37
37
|
"typescript": "^5.0.0"
|
|
38
|
-
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"commander": "^12.0.0",
|
|
41
|
-
"fast-glob": "^3.3.0"
|
|
42
38
|
}
|
|
43
|
-
}
|
|
39
|
+
}
|