@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 CHANGED
@@ -1,32 +1,50 @@
1
1
  # ctx
2
2
 
3
- Generate AI-ready context from your codebase.
3
+ Scan your codebase. Get a clean summary. Paste it to AI.
4
4
 
5
5
  ## Usage
6
6
 
7
7
  ```bash
8
- npx @eloquence98/ctx ./src
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
- # AI-optimized compact format
17
- npx @eloquence98/ctx ./src --ai
13
+ # Scan specific folder
14
+ ctx ./src
18
15
  ```
19
16
 
20
17
  ```bash
21
- # JSON output
22
- npx @eloquence98/ctx ./src -o json
18
+ # Human-readable output
19
+ ctx ./src --human
23
20
  ```
24
21
 
22
+ ## Output Example
23
+
25
24
  ```bash
26
- # Save to file
27
- npx @eloquence98/ctx ./src > context.md
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 { getAdapter } from "./adapters/index.js";
6
- import { detectProject, getProjectLabel } from "./detectors/index.js";
7
- import { formatAIOptimized } from "./formatters/ai-optimized.js";
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
- import { formatRaw } from "./formatters/raw.js";
10
- import { parseFile } from "./parser.js";
11
- import { scanDirectory } from "./scanner.js";
12
- program
13
- .name("ctx")
14
- .description("Generate AI-ready context from your codebase")
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", "")
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
- async function findProjectRoot(startDir) {
59
- let current = startDir;
60
- while (current !== path.dirname(current)) {
61
- try {
62
- await fs.access(path.join(current, "package.json"));
63
- return current;
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
- program.parse();
27
+ main().catch(console.error);
@@ -1,2 +1,2 @@
1
- import type { AdapterContext } from "../adapters/types.js";
2
- export declare function formatAI(data: AdapterContext): string;
1
+ import type { OrganizedContext } from "../types.js";
2
+ export declare function formatAI(ctx: OrganizedContext): string;
@@ -1,47 +1,51 @@
1
- export function formatAI(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");
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
- for (const route of data.routes) {
10
- lines.push(`- ${route}`);
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
- // 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}`);
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
- for (const file of files) {
23
- const exports = getExportsSummary(file);
24
- if (exports) {
25
- lines.push(`- ${file.fileName}: ${exports}`);
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 { AdapterContext } from "../adapters/types.js";
2
- export declare function formatHuman(data: AdapterContext): string;
1
+ import type { OrganizedContext } from "../types.js";
2
+ export declare function formatHuman(ctx: OrganizedContext): string;
@@ -1,200 +1,53 @@
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
- }
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
- // 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
- }
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
- 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);
25
+ if (ctx.components.length > 10) {
26
+ lines.push(` ... and ${ctx.components.length - 10} more`);
134
27
  }
28
+ lines.push("");
135
29
  }
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);
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
- 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);
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 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);
52
+ return lines.join("\n");
200
53
  }
@@ -1,4 +1,2 @@
1
- export { formatMarkdown } from "./markdown.js";
2
- export { formatAIOptimized } from "./ai-optimized.js";
3
- export { formatRaw } from "./raw.js";
1
+ export { formatAI } from "./ai.js";
4
2
  export { formatHuman } from "./human.js";
@@ -1,4 +1,2 @@
1
- export { formatMarkdown } from "./markdown.js";
2
- export { formatAIOptimized } from "./ai-optimized.js";
3
- export { formatRaw } from "./raw.js";
1
+ export { formatAI } from "./ai.js";
4
2
  export { formatHuman } from "./human.js";
@@ -0,0 +1,2 @@
1
+ import type { ParsedFile, OrganizedContext } from "./types.js";
2
+ export declare function organize(files: ParsedFile[], baseDir: string): Promise<OrganizedContext>;
@@ -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 { FileExports } from "./types.js";
2
- export declare function parseFile(filePath: string): Promise<FileExports>;
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 parseFile(filePath) {
3
+ export async function parse(filePath) {
4
4
  const content = await fs.readFile(filePath, "utf-8");
5
- const fileName = path.basename(filePath);
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 lines = content.split("\n");
41
- for (const line of lines) {
42
- const match = line.match(/export\s+const\s+(\w+)\s*=\s*(?!(?:async\s*)?(?:\(|function|\w+\s*=>))/);
43
- if (match) {
44
- constants.push(match[1]);
45
- }
46
- const typedMatch = line.match(/export\s+const\s+(\w+)\s*:\s*[^=]+=\s*(?!(?:async\s*)?(?:\(|function|\w+\s*=>))/);
47
- if (typedMatch && !constants.includes(typedMatch[1])) {
48
- constants.push(typedMatch[1]);
49
- }
50
- }
51
- return constants;
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
- // 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]);
21
+ else if (n.startsWith("use")) {
22
+ functions.push(n);
89
23
  }
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
- }
24
+ else {
25
+ constants.push(n);
101
26
  }
102
27
  }
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
- }
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 constants;
32
+ return {
33
+ path: filePath,
34
+ name,
35
+ exports: { functions, constants, types, components },
36
+ };
121
37
  }
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;
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
- import type { ScanOptions } from "./types.js";
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
- import { defaultConfig } from "./config.js";
4
- export async function scanDirectory(dir, options = {}) {
5
- const config = { ...defaultConfig, ...options };
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(currentDir, depth = 0) {
8
- if (depth > config.maxDepth)
9
- return;
7
+ async function walk(current) {
10
8
  let entries;
11
9
  try {
12
- entries = await fs.readdir(currentDir, { withFileTypes: true });
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
- const fullPath = path.join(currentDir, entry.name);
19
- // Check ignore patterns
20
- const shouldIgnore = config.ignore.some((pattern) => {
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(fullPath, depth + 1);
22
+ await walk(full);
31
23
  }
32
- else if (entry.isFile()) {
33
- const ext = path.extname(entry.name);
34
- if (config.extensions.includes(ext)) {
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 FileExports {
2
- filePath: string;
3
- fileName: string;
4
- functions: string[];
5
- constants: string[];
6
- types: string[];
7
- interfaces: string[];
8
- classes: string[];
9
- defaultExport?: string;
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 ScanOptions {
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
- features: Map<string, FileExports[]>;
20
- hooks: FileExports[];
21
- lib: Map<string, FileExports[]>;
22
- components: Map<string, FileExports[]>;
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.1.6",
4
- "description": "Generate AI-ready context from your codebase. One command, zero config.",
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
+ }