@eloquence98/ctx 0.1.6 ā 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 -72
- package/dist/formatters/ai.d.ts +2 -2
- package/dist/formatters/ai.js +40 -36
- package/dist/formatters/human.d.ts +2 -2
- package/dist/formatters/human.js +39 -186
- package/dist/formatters/index.d.ts +1 -3
- package/dist/formatters/index.js +1 -3
- 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,78 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { program } from "commander";
|
|
3
|
-
import fs from "fs/promises";
|
|
4
2
|
import path from "path";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { scan } from "./scanner.js";
|
|
4
|
+
import { parse } from "./parser.js";
|
|
5
|
+
import { organize } from "./organizer.js";
|
|
6
|
+
import { formatAI } from "./formatters/ai.js";
|
|
8
7
|
import { formatHuman } from "./formatters/human.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
.option("--raw", "Verbose output with all details")
|
|
19
|
-
.option("-o, --output <format>", "Output format: json", "")
|
|
20
|
-
.action(async (targetPath, options) => {
|
|
21
|
-
const absolutePath = path.resolve(process.cwd(), targetPath);
|
|
22
|
-
const projectRoot = await findProjectRoot(absolutePath);
|
|
23
|
-
const projectType = await detectProject(projectRoot);
|
|
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
|
-
}
|
|
30
|
-
try {
|
|
31
|
-
const files = await scanDirectory(absolutePath);
|
|
32
|
-
if (files.length === 0) {
|
|
33
|
-
console.log("No files found. Check your path.");
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
const parsedFiles = await Promise.all(files.map((file) => parseFile(file)));
|
|
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));
|
|
41
|
-
}
|
|
42
|
-
else if (options.raw) {
|
|
43
|
-
console.log(formatRaw(context));
|
|
44
|
-
}
|
|
45
|
-
else if (options.human) {
|
|
46
|
-
console.log(formatHuman(context));
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
// Default: AI-optimized
|
|
50
|
-
console.log(formatAIOptimized(context));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
console.error("Error:", error);
|
|
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.");
|
|
55
17
|
process.exit(1);
|
|
56
18
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
current = path.dirname(current);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return startDir;
|
|
70
|
-
}
|
|
71
|
-
function contextToJSON(context) {
|
|
72
|
-
return {
|
|
73
|
-
projectType: context.projectType,
|
|
74
|
-
routes: context.routes || [],
|
|
75
|
-
sections: Object.fromEntries(context.sections),
|
|
76
|
-
};
|
|
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);
|
|
77
26
|
}
|
|
78
|
-
|
|
27
|
+
main().catch(console.error);
|
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
|
-
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function formatHuman(
|
|
1
|
+
import type { OrganizedContext } from "../types.js";
|
|
2
|
+
export declare function formatHuman(ctx: OrganizedContext): string;
|
package/dist/formatters/human.js
CHANGED
|
@@ -1,200 +1,53 @@
|
|
|
1
|
-
export function formatHuman(
|
|
2
|
-
const lines = [
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
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
|
-
}
|
|
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}`);
|
|
41
12
|
}
|
|
42
13
|
lines.push("");
|
|
43
14
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
}
|
|
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}`);
|
|
110
22
|
}
|
|
111
23
|
}
|
|
112
24
|
}
|
|
113
|
-
|
|
114
|
-
|
|
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);
|
|
25
|
+
if (ctx.components.length > 10) {
|
|
26
|
+
lines.push(` ... and ${ctx.components.length - 10} more`);
|
|
134
27
|
}
|
|
28
|
+
lines.push("");
|
|
135
29
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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);
|
|
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}()`);
|
|
156
36
|
}
|
|
37
|
+
lines.push("");
|
|
157
38
|
}
|
|
158
39
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
}
|
|
167
49
|
}
|
|
50
|
+
lines.push("");
|
|
168
51
|
}
|
|
169
|
-
return
|
|
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);
|
|
52
|
+
return lines.join("\n");
|
|
200
53
|
}
|
package/dist/formatters/index.js
CHANGED
|
@@ -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
|
+
}
|