@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 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,73 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import { program } from "commander";
3
2
  import path from "path";
4
- import fs from "fs/promises";
5
- import { scanDirectory } from "./scanner.js";
6
- import { parseFile } from "./parser.js";
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
- program
12
- .name("ctx")
13
- .description("Generate AI-ready context from your codebase")
14
- .version("0.1.5")
15
- .argument("[path]", "Path to scan", ".")
16
- .option("-o, --output <format>", "Output format: md, json", "md")
17
- .option("--ai", "Output in AI-optimized compact format")
18
- .action(async (targetPath, options) => {
19
- const absolutePath = path.resolve(process.cwd(), targetPath);
20
- // Find project root and detect type
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
- async function findProjectRoot(startDir) {
54
- let current = startDir;
55
- while (current !== path.dirname(current)) {
56
- try {
57
- await fs.access(path.join(current, "package.json"));
58
- return current;
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
- program.parse();
27
+ main().catch(console.error);
@@ -0,0 +1,2 @@
1
+ import type { AdapterContext } from "../adapters/types.js";
2
+ export declare function formatAIOptimized(data: AdapterContext): string;
@@ -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
+ }
@@ -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
- }
@@ -0,0 +1,2 @@
1
+ import type { OrganizedContext } from "../types.js";
2
+ export declare function formatHuman(ctx: OrganizedContext): string;
@@ -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
+ }
@@ -1,2 +1,2 @@
1
- export { formatMarkdown } from "./markdown.js";
2
1
  export { formatAI } from "./ai.js";
2
+ export { formatHuman } from "./human.js";
@@ -1,2 +1,2 @@
1
- export { formatMarkdown } from "./markdown.js";
2
1
  export { formatAI } from "./ai.js";
2
+ export { formatHuman } from "./human.js";
@@ -0,0 +1,2 @@
1
+ import type { AdapterContext } from "../adapters/types.js";
2
+ export declare function formatRaw(data: AdapterContext): string;
@@ -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,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.5",
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
+ }