@eloquence98/ctx 0.1.3 → 0.1.5

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.
@@ -0,0 +1,2 @@
1
+ import type { Adapter } from "./types.js";
2
+ export declare const expressAdapter: Adapter;
@@ -0,0 +1,58 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ export const expressAdapter = {
4
+ name: "express",
5
+ async detect(dir) {
6
+ try {
7
+ const pkg = await fs.readFile(path.join(dir, "package.json"), "utf-8");
8
+ const parsed = JSON.parse(pkg);
9
+ const deps = { ...parsed.dependencies, ...parsed.devDependencies };
10
+ return ("express" in deps ||
11
+ "fastify" in deps ||
12
+ "koa" in deps ||
13
+ "hono" in deps);
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ },
19
+ async analyze(dir, files) {
20
+ const sections = new Map();
21
+ const folderMappings = {
22
+ Routes: ["routes", "routers", "api"],
23
+ Controllers: ["controllers", "handlers"],
24
+ Services: ["services"],
25
+ Models: ["models", "entities", "schemas"],
26
+ Middleware: ["middleware", "middlewares"],
27
+ Utils: ["utils", "helpers", "lib"],
28
+ Config: ["config", "configs"],
29
+ };
30
+ for (const file of files) {
31
+ const relativePath = path.relative(dir, file.filePath);
32
+ const parts = relativePath.split(path.sep);
33
+ let matched = false;
34
+ for (const [sectionName, folders] of Object.entries(folderMappings)) {
35
+ const folderIndex = parts.findIndex((p) => folders.includes(p.toLowerCase()));
36
+ if (folderIndex !== -1) {
37
+ if (!sections.has(sectionName)) {
38
+ sections.set(sectionName, []);
39
+ }
40
+ sections.get(sectionName).push(file);
41
+ matched = true;
42
+ break;
43
+ }
44
+ }
45
+ if (!matched) {
46
+ const folder = parts.length > 1 ? parts[0] : "_root";
47
+ if (!sections.has(folder)) {
48
+ sections.set(folder, []);
49
+ }
50
+ sections.get(folder).push(file);
51
+ }
52
+ }
53
+ return {
54
+ projectType: "Express/Fastify",
55
+ sections,
56
+ };
57
+ },
58
+ };
@@ -0,0 +1,4 @@
1
+ import type { ProjectType } from "../detectors/index.js";
2
+ import type { Adapter } from "./types.js";
3
+ export declare function getAdapter(projectType: ProjectType): Adapter;
4
+ export type { Adapter, AdapterContext } from "./types.js";
@@ -0,0 +1,16 @@
1
+ import { expressAdapter } from "./express.js";
2
+ import { nextjsAdapter } from "./nextjs.js";
3
+ import { vanillaAdapter } from "./vanilla.js";
4
+ const adapters = {
5
+ nextjs: nextjsAdapter,
6
+ react: vanillaAdapter,
7
+ express: expressAdapter,
8
+ nestjs: expressAdapter, // Similar structure
9
+ vue: vanillaAdapter,
10
+ sveltekit: vanillaAdapter,
11
+ node: vanillaAdapter,
12
+ vanilla: vanillaAdapter,
13
+ };
14
+ export function getAdapter(projectType) {
15
+ return adapters[projectType] || vanillaAdapter;
16
+ }
@@ -0,0 +1,2 @@
1
+ import type { Adapter } from "./types.js";
2
+ export declare const nextjsAdapter: Adapter;
@@ -0,0 +1,102 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { getRoutes } from "../scanner.js";
4
+ export const nextjsAdapter = {
5
+ name: "nextjs",
6
+ async detect(dir) {
7
+ const configFiles = ["next.config.js", "next.config.mjs", "next.config.ts"];
8
+ for (const file of configFiles) {
9
+ try {
10
+ await fs.access(path.join(dir, file));
11
+ return true;
12
+ }
13
+ catch {
14
+ continue;
15
+ }
16
+ }
17
+ // Check package.json for next dependency
18
+ try {
19
+ const pkg = await fs.readFile(path.join(dir, "package.json"), "utf-8");
20
+ const parsed = JSON.parse(pkg);
21
+ const deps = { ...parsed.dependencies, ...parsed.devDependencies };
22
+ return "next" in deps;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ },
28
+ async analyze(dir, files) {
29
+ const sections = new Map();
30
+ // Get routes
31
+ let routes = [];
32
+ const appDir = await findAppDirectory(dir);
33
+ if (appDir) {
34
+ routes = await getRoutes(appDir);
35
+ }
36
+ // Group by known Next.js folders
37
+ const folderMappings = {
38
+ Features: ["features", "modules", "domains"],
39
+ Components: ["components"],
40
+ Hooks: ["hooks"],
41
+ Lib: ["lib", "utils", "helpers", "services"],
42
+ API: ["api"],
43
+ };
44
+ for (const file of files) {
45
+ const relativePath = path.relative(dir, file.filePath);
46
+ const parts = relativePath.split(path.sep);
47
+ let matched = false;
48
+ for (const [sectionName, folders] of Object.entries(folderMappings)) {
49
+ const folderIndex = parts.findIndex((p) => folders.includes(p));
50
+ if (folderIndex !== -1) {
51
+ const subParts = parts.slice(folderIndex + 1);
52
+ if (subParts.length >= 1) {
53
+ // Use subfolder name if exists, otherwise use section name
54
+ const subFolder = subParts.length > 1 ? subParts[0] : "_direct";
55
+ const key = subFolder === "_direct"
56
+ ? sectionName
57
+ : `${sectionName}/${subParts[0]}`;
58
+ if (!sections.has(key)) {
59
+ sections.set(key, []);
60
+ }
61
+ sections.get(key).push(file);
62
+ matched = true;
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ // If no match, group by top folder
68
+ if (!matched) {
69
+ const folder = parts.length > 1 ? parts[0] : "_root";
70
+ const key = `Other/${folder}`;
71
+ if (!sections.has(key)) {
72
+ sections.set(key, []);
73
+ }
74
+ sections.get(key).push(file);
75
+ }
76
+ }
77
+ return {
78
+ projectType: "Next.js",
79
+ sections,
80
+ routes,
81
+ };
82
+ },
83
+ };
84
+ async function findAppDirectory(basePath) {
85
+ const possiblePaths = [
86
+ path.join(basePath, "app"),
87
+ path.join(basePath, "src", "app"),
88
+ path.join(basePath, "src/app"),
89
+ ];
90
+ for (const p of possiblePaths) {
91
+ try {
92
+ const stat = await fs.stat(p);
93
+ if (stat.isDirectory()) {
94
+ return p;
95
+ }
96
+ }
97
+ catch {
98
+ continue;
99
+ }
100
+ }
101
+ return null;
102
+ }
@@ -0,0 +1,11 @@
1
+ import type { FileExports } from "../types.js";
2
+ export interface AdapterContext {
3
+ projectType: string;
4
+ sections: Map<string, FileExports[]>;
5
+ routes?: string[];
6
+ }
7
+ export interface Adapter {
8
+ name: string;
9
+ detect: (dir: string) => Promise<boolean>;
10
+ analyze: (dir: string, files: FileExports[]) => Promise<AdapterContext>;
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { Adapter } from "./types.js";
2
+ export declare const vanillaAdapter: Adapter;
@@ -0,0 +1,24 @@
1
+ import path from "path";
2
+ export const vanillaAdapter = {
3
+ name: "vanilla",
4
+ async detect() {
5
+ // Always returns true - this is the fallback
6
+ return true;
7
+ },
8
+ async analyze(dir, files) {
9
+ const sections = new Map();
10
+ for (const file of files) {
11
+ const relativePath = path.relative(dir, file.filePath);
12
+ const parts = relativePath.split(path.sep);
13
+ const folder = parts.length > 1 ? parts[0] : "_root";
14
+ if (!sections.has(folder)) {
15
+ sections.set(folder, []);
16
+ }
17
+ sections.get(folder).push(file);
18
+ }
19
+ return {
20
+ projectType: "JavaScript/TypeScript",
21
+ sections,
22
+ };
23
+ },
24
+ };
package/dist/cli.js CHANGED
@@ -2,19 +2,27 @@
2
2
  import { program } from "commander";
3
3
  import path from "path";
4
4
  import fs from "fs/promises";
5
- import { scanDirectory, getRoutes } from "./scanner.js";
5
+ import { scanDirectory } from "./scanner.js";
6
6
  import { parseFile } from "./parser.js";
7
- import { formatMarkdown } from "./formatters/index.js";
7
+ import { detectProject, getProjectLabel } from "./detectors/index.js";
8
+ import { getAdapter } from "./adapters/index.js";
9
+ import { formatMarkdown } from "./formatters/markdown.js";
10
+ import { formatAI } from "./formatters/ai.js";
8
11
  program
9
12
  .name("ctx")
10
13
  .description("Generate AI-ready context from your codebase")
11
- .version("0.1.0")
12
- .argument("[path]", "Path to scan", "./src")
14
+ .version("0.1.5")
15
+ .argument("[path]", "Path to scan", ".")
13
16
  .option("-o, --output <format>", "Output format: md, json", "md")
14
17
  .option("--ai", "Output in AI-optimized compact format")
15
18
  .action(async (targetPath, options) => {
16
19
  const absolutePath = path.resolve(process.cwd(), targetPath);
17
- console.log(`\n📁 Scanning ${absolutePath}...\n`);
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`);
18
26
  try {
19
27
  // Scan all files
20
28
  const files = await scanDirectory(absolutePath);
@@ -24,30 +32,17 @@ program
24
32
  }
25
33
  // Parse all files
26
34
  const parsedFiles = await Promise.all(files.map((file) => parseFile(file)));
27
- // Get routes - try to find app directory
28
- let routes = [];
29
- const appDir = await findAppDirectory(absolutePath);
30
- if (appDir) {
31
- routes = await getRoutes(appDir);
32
- }
33
- // Build context
34
- const context = {
35
- routes,
36
- features: groupByFolder(parsedFiles, absolutePath, "features"),
37
- hooks: getHooks(parsedFiles),
38
- lib: groupByFolder(parsedFiles, absolutePath, "lib"),
39
- components: groupByFolder(parsedFiles, absolutePath, "components"),
40
- };
35
+ // Use adapter to analyze
36
+ const context = await adapter.analyze(absolutePath, parsedFiles);
41
37
  // Output
42
- if (options.ai) {
43
- const { formatAI } = await import("./formatters/ai.js");
44
- console.log(formatAI(context, absolutePath));
45
- }
46
- else if (options.output === "json") {
38
+ if (options.output === "json") {
47
39
  console.log(JSON.stringify(contextToJSON(context), null, 2));
48
40
  }
41
+ else if (options.ai) {
42
+ console.log(formatAI(context));
43
+ }
49
44
  else {
50
- console.log(formatMarkdown(context, absolutePath));
45
+ console.log(formatMarkdown(context));
51
46
  }
52
47
  }
53
48
  catch (error) {
@@ -55,58 +50,24 @@ program
55
50
  process.exit(1);
56
51
  }
57
52
  });
58
- async function findAppDirectory(basePath) {
59
- const possiblePaths = [
60
- path.join(basePath, "app"),
61
- path.join(basePath, "src", "app"),
62
- path.join(basePath, "src/app"),
63
- ];
64
- for (const p of possiblePaths) {
53
+ async function findProjectRoot(startDir) {
54
+ let current = startDir;
55
+ while (current !== path.dirname(current)) {
65
56
  try {
66
- const stat = await fs.stat(p);
67
- if (stat.isDirectory()) {
68
- return p;
69
- }
57
+ await fs.access(path.join(current, "package.json"));
58
+ return current;
70
59
  }
71
60
  catch {
72
- // Path doesn't exist, try next
61
+ current = path.dirname(current);
73
62
  }
74
63
  }
75
- return null;
76
- }
77
- function groupByFolder(files, basePath, folderName) {
78
- const grouped = new Map();
79
- for (const file of files) {
80
- const relativePath = path.relative(basePath, file.filePath);
81
- const parts = relativePath.split(path.sep);
82
- const folderIndex = parts.indexOf(folderName);
83
- if (folderIndex === -1)
84
- continue;
85
- const remainingParts = parts.slice(folderIndex + 1);
86
- // Need at least 2 parts: subfolder + filename
87
- if (remainingParts.length < 2)
88
- continue;
89
- const subFolder = remainingParts[0];
90
- if (!grouped.has(subFolder)) {
91
- grouped.set(subFolder, []);
92
- }
93
- grouped.get(subFolder).push(file);
94
- }
95
- return grouped;
96
- }
97
- function getHooks(files) {
98
- return files.filter((f) => f.filePath.includes("/hooks/") ||
99
- f.filePath.includes("\\hooks\\") ||
100
- f.fileName.startsWith("use-") ||
101
- f.fileName.startsWith("use."));
64
+ return startDir;
102
65
  }
103
66
  function contextToJSON(context) {
104
67
  return {
105
- routes: context.routes,
106
- features: Object.fromEntries(context.features),
107
- hooks: context.hooks,
108
- lib: Object.fromEntries(context.lib),
109
- components: Object.fromEntries(context.components),
68
+ projectType: context.projectType,
69
+ routes: context.routes || [],
70
+ sections: Object.fromEntries(context.sections),
110
71
  };
111
72
  }
112
73
  program.parse();
package/dist/config.js CHANGED
@@ -4,6 +4,7 @@ export const defaultConfig = {
4
4
  ignore: [
5
5
  "node_modules",
6
6
  ".git",
7
+ ".directory",
7
8
  "dist",
8
9
  "build",
9
10
  ".next",
@@ -0,0 +1,3 @@
1
+ export type ProjectType = "nextjs" | "react" | "express" | "nestjs" | "vue" | "sveltekit" | "node" | "vanilla";
2
+ export declare function detectProject(dir: string): Promise<ProjectType>;
3
+ export declare function getProjectLabel(type: ProjectType): string;
@@ -0,0 +1,73 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ async function readPackageJson(dir) {
4
+ try {
5
+ const content = await fs.readFile(path.join(dir, "package.json"), "utf-8");
6
+ return JSON.parse(content);
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ async function fileExists(filePath) {
13
+ try {
14
+ await fs.access(filePath);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ export async function detectProject(dir) {
22
+ const pkg = await readPackageJson(dir);
23
+ const deps = {
24
+ ...pkg?.dependencies,
25
+ ...pkg?.devDependencies,
26
+ };
27
+ // Check config files first (more specific)
28
+ const hasNextConfig = (await fileExists(path.join(dir, "next.config.js"))) ||
29
+ (await fileExists(path.join(dir, "next.config.mjs"))) ||
30
+ (await fileExists(path.join(dir, "next.config.ts")));
31
+ if (hasNextConfig || deps["next"]) {
32
+ return "nextjs";
33
+ }
34
+ // NestJS
35
+ if (deps["@nestjs/core"]) {
36
+ return "nestjs";
37
+ }
38
+ // SvelteKit
39
+ if (deps["@sveltejs/kit"]) {
40
+ return "sveltekit";
41
+ }
42
+ // Vue
43
+ if (deps["vue"] || deps["nuxt"]) {
44
+ return "vue";
45
+ }
46
+ // Express
47
+ if (deps["express"] || deps["fastify"] || deps["koa"] || deps["hono"]) {
48
+ return "express";
49
+ }
50
+ // React (without Next.js)
51
+ if (deps["react"] && !deps["next"]) {
52
+ return "react";
53
+ }
54
+ // Node.js project (has package.json but no framework)
55
+ if (pkg) {
56
+ return "node";
57
+ }
58
+ // No package.json
59
+ return "vanilla";
60
+ }
61
+ export function getProjectLabel(type) {
62
+ const labels = {
63
+ nextjs: "Next.js",
64
+ react: "React",
65
+ express: "Express/Fastify",
66
+ nestjs: "NestJS",
67
+ vue: "Vue/Nuxt",
68
+ sveltekit: "SvelteKit",
69
+ node: "Node.js",
70
+ vanilla: "JavaScript/TypeScript",
71
+ };
72
+ return labels[type];
73
+ }
@@ -1,2 +1,2 @@
1
- import type { ProjectContext } from "../types.js";
2
- export declare function formatAI(data: ProjectContext, basePath: string): string;
1
+ import type { AdapterContext } from "../adapters/types.js";
2
+ export declare function formatAI(data: AdapterContext): string;
@@ -1,57 +1,24 @@
1
- export function formatAI(data, basePath) {
1
+ export function formatAI(data) {
2
2
  const lines = [];
3
- lines.push("# Project Context");
4
- lines.push("");
5
- lines.push("Use this to understand the codebase structure.");
3
+ lines.push(`# ${data.projectType} Project Context`);
6
4
  lines.push("");
7
5
  // Routes
8
- if (data.routes.length > 0) {
6
+ if (data.routes && data.routes.length > 0) {
9
7
  lines.push("## Routes");
10
8
  lines.push("");
11
9
  for (const route of data.routes) {
12
- const depth = route.search(/\S/); // Count leading spaces
13
- const name = route.trim();
14
- const prefix = depth > 0 ? " ".repeat(depth / 2) + "└─ " : "- ";
15
- lines.push(`${prefix}${name}`);
10
+ lines.push(`- ${route}`);
16
11
  }
17
12
  lines.push("");
18
13
  }
19
- // Features
20
- if (data.features.size > 0) {
21
- lines.push("## Features");
22
- lines.push("");
23
- lines.push(formatFolderCompact(data.features));
24
- }
25
- // Components
26
- if (data.components.size > 0) {
27
- lines.push("## Components");
28
- lines.push("");
29
- lines.push(formatFolderCompact(data.components));
30
- }
31
- // Hooks
32
- if (data.hooks.length > 0) {
33
- lines.push("## Hooks");
14
+ // Sections
15
+ for (const [sectionName, files] of data.sections) {
16
+ if (sectionName === "_root")
17
+ continue;
18
+ if (files.length === 0)
19
+ continue;
20
+ lines.push(`## ${sectionName}`);
34
21
  lines.push("");
35
- for (const hook of data.hooks) {
36
- const exports = getExportsSummary(hook);
37
- if (exports) {
38
- lines.push(`- ${hook.fileName}: ${exports}`);
39
- }
40
- }
41
- lines.push("");
42
- }
43
- // Lib
44
- if (data.lib.size > 0) {
45
- lines.push("## Lib / Utils");
46
- lines.push("");
47
- lines.push(formatFolderCompact(data.lib));
48
- }
49
- return lines.join("\n");
50
- }
51
- function formatFolderCompact(folders) {
52
- const lines = [];
53
- for (const [folderName, files] of folders) {
54
- lines.push(`### ${folderName}`);
55
22
  for (const file of files) {
56
23
  const exports = getExportsSummary(file);
57
24
  if (exports) {
@@ -65,19 +32,16 @@ function formatFolderCompact(folders) {
65
32
  function getExportsSummary(file) {
66
33
  const parts = [];
67
34
  if (file.functions.length > 0) {
68
- const fns = file.functions.map((f) => `${f}()`).join(", ");
69
- parts.push(fns);
35
+ parts.push(file.functions.map((f) => `${f}()`).join(", "));
70
36
  }
71
37
  if (file.constants.length > 0) {
72
38
  parts.push(file.constants.join(", "));
73
39
  }
74
40
  if (file.types.length > 0) {
75
- const types = file.types.map((t) => `type ${t}`).join(", ");
76
- parts.push(types);
41
+ parts.push(file.types.map((t) => `type ${t}`).join(", "));
77
42
  }
78
43
  if (file.interfaces.length > 0) {
79
- const ifaces = file.interfaces.map((i) => `interface ${i}`).join(", ");
80
- parts.push(ifaces);
44
+ parts.push(file.interfaces.map((i) => `interface ${i}`).join(", "));
81
45
  }
82
46
  return parts.join(" | ");
83
47
  }
@@ -1,2 +1,2 @@
1
- import type { ProjectContext } from "../types.js";
2
- export declare function formatMarkdown(data: ProjectContext, basePath: string): string;
1
+ import type { AdapterContext } from "../adapters/types.js";
2
+ export declare function formatMarkdown(data: AdapterContext): string;
@@ -1,51 +1,55 @@
1
- export function formatMarkdown(data, basePath) {
1
+ export function formatMarkdown(data) {
2
2
  let output = "";
3
- // Routes section
4
- if (data.routes.length > 0) {
5
- output += `=== ROUTES (src/app) ===\n\n`;
3
+ // Project type header
4
+ output += `📦 Project: ${data.projectType}\n\n`;
5
+ // Routes section (if exists)
6
+ if (data.routes && data.routes.length > 0) {
7
+ output += `=== ROUTES ===\n\n`;
6
8
  for (const route of data.routes) {
7
9
  output += `${route}\n`;
8
10
  }
9
11
  output += "\n";
10
12
  }
11
- // Features section
12
- if (data.features.size > 0) {
13
- output += `=== FEATURES ===\n\n`;
14
- output += formatFolderGroup(data.features);
15
- }
16
- // Components section
17
- if (data.components.size > 0) {
18
- output += `=== COMPONENTS ===\n\n`;
19
- output += formatFolderGroup(data.components);
20
- }
21
- // Hooks section
22
- if (data.hooks.length > 0) {
23
- output += `=== HOOKS ===\n\n`;
24
- for (const file of data.hooks) {
25
- output += formatSingleFile(file);
13
+ // All sections
14
+ for (const [sectionName, files] of data.sections) {
15
+ if (sectionName === "_root")
16
+ continue;
17
+ if (files.length === 0)
18
+ continue;
19
+ // Check if any file has exports
20
+ const hasExports = files.some((f) => f.functions.length > 0 ||
21
+ f.constants.length > 0 ||
22
+ f.types.length > 0 ||
23
+ f.interfaces.length > 0 ||
24
+ f.classes.length > 0);
25
+ if (!hasExports)
26
+ continue;
27
+ output += `=== ${sectionName.toUpperCase()} ===\n\n`;
28
+ for (const file of files) {
29
+ output += formatFile(file);
26
30
  }
27
31
  output += "\n";
28
32
  }
29
- // Lib section
30
- if (data.lib.size > 0) {
31
- output += `=== LIB ===\n\n`;
32
- output += formatFolderGroup(data.lib);
33
- }
34
- output += `=== DONE ===\n`;
35
- return output;
36
- }
37
- function formatFolderGroup(folders) {
38
- let output = "";
39
- for (const [folderName, files] of folders) {
40
- output += `${folderName}/\n`;
41
- for (const file of files) {
42
- output += formatSingleFile(file);
33
+ // Root files last
34
+ const rootFiles = data.sections.get("_root");
35
+ if (rootFiles && rootFiles.length > 0) {
36
+ const hasRootExports = rootFiles.some((f) => f.functions.length > 0 ||
37
+ f.constants.length > 0 ||
38
+ f.types.length > 0 ||
39
+ f.interfaces.length > 0 ||
40
+ f.classes.length > 0);
41
+ if (hasRootExports) {
42
+ output += `=== ROOT FILES ===\n\n`;
43
+ for (const file of rootFiles) {
44
+ output += formatFile(file);
45
+ }
46
+ output += "\n";
43
47
  }
44
- output += "\n";
45
48
  }
49
+ output += `=== DONE ===\n`;
46
50
  return output;
47
51
  }
48
- function formatSingleFile(file) {
52
+ function formatFile(file) {
49
53
  const hasExports = file.functions.length > 0 ||
50
54
  file.constants.length > 0 ||
51
55
  file.types.length > 0 ||
package/dist/parser.js CHANGED
@@ -6,22 +6,27 @@ export async function parseFile(filePath) {
6
6
  return {
7
7
  filePath,
8
8
  fileName,
9
- functions: extractFunctions(content),
10
- constants: extractConstants(content),
9
+ functions: [
10
+ ...extractFunctions(content),
11
+ ...extractCommonJSFunctions(content),
12
+ ],
13
+ constants: [
14
+ ...extractConstants(content),
15
+ ...extractCommonJSConstants(content),
16
+ ],
11
17
  types: extractTypes(content),
12
18
  interfaces: extractInterfaces(content),
13
- classes: extractClasses(content),
19
+ classes: [...extractClasses(content), ...extractMongooseModels(content)],
14
20
  defaultExport: extractDefaultExport(content),
15
21
  };
16
22
  }
23
+ // ESM Exports
17
24
  function extractFunctions(content) {
18
25
  const functions = [];
19
- // export function name
20
26
  const funcMatches = content.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g);
21
27
  for (const match of funcMatches) {
22
28
  functions.push(match[1]);
23
29
  }
24
- // export const name = async? (...) => or function(
25
30
  const arrowMatches = content.matchAll(/export\s+const\s+(\w+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*(?:=>|\{)/g);
26
31
  for (const match of arrowMatches) {
27
32
  if (!functions.includes(match[1])) {
@@ -32,15 +37,12 @@ function extractFunctions(content) {
32
37
  }
33
38
  function extractConstants(content) {
34
39
  const constants = [];
35
- // Match export const that are NOT functions
36
40
  const lines = content.split("\n");
37
41
  for (const line of lines) {
38
- // export const NAME = value (not a function)
39
42
  const match = line.match(/export\s+const\s+(\w+)\s*=\s*(?!(?:async\s*)?(?:\(|function|\w+\s*=>))/);
40
43
  if (match) {
41
44
  constants.push(match[1]);
42
45
  }
43
- // Also catch: export const NAME: Type =
44
46
  const typedMatch = line.match(/export\s+const\s+(\w+)\s*:\s*[^=]+=\s*(?!(?:async\s*)?(?:\(|function|\w+\s*=>))/);
45
47
  if (typedMatch && !constants.includes(typedMatch[1])) {
46
48
  constants.push(typedMatch[1]);
@@ -61,13 +63,68 @@ function extractClasses(content) {
61
63
  return [...matches].map((m) => m[1]);
62
64
  }
63
65
  function extractDefaultExport(content) {
64
- // export default function Name
65
66
  const funcMatch = content.match(/export\s+default\s+function\s+(\w+)/);
66
67
  if (funcMatch)
67
68
  return funcMatch[1];
68
- // export default Name
69
69
  const simpleMatch = content.match(/export\s+default\s+(\w+)/);
70
70
  if (simpleMatch)
71
71
  return simpleMatch[1];
72
72
  return undefined;
73
73
  }
74
+ // CommonJS Exports
75
+ function extractCommonJSFunctions(content) {
76
+ const functions = [];
77
+ // exports.functionName = async (req, res) => { }
78
+ const exportsMatches = content.matchAll(/exports\.(\w+)\s*=\s*(?:async\s*)?(?:function|\(|async\s*\()/g);
79
+ for (const match of exportsMatches) {
80
+ if (!functions.includes(match[1])) {
81
+ functions.push(match[1]);
82
+ }
83
+ }
84
+ // module.exports.functionName = async (req, res) => { }
85
+ const moduleExportsMatches = content.matchAll(/module\.exports\.(\w+)\s*=\s*(?:async\s*)?(?:function|\(|async\s*\()/g);
86
+ for (const match of moduleExportsMatches) {
87
+ if (!functions.includes(match[1])) {
88
+ functions.push(match[1]);
89
+ }
90
+ }
91
+ // module.exports = { functionName, anotherFunction }
92
+ const objectExportMatch = content.match(/module\.exports\s*=\s*\{([^}]+)\}/);
93
+ if (objectExportMatch) {
94
+ const names = objectExportMatch[1]
95
+ .split(",")
96
+ .map((s) => s.trim().split(":")[0].trim());
97
+ for (const name of names) {
98
+ if (name && /^\w+$/.test(name) && !functions.includes(name)) {
99
+ functions.push(name);
100
+ }
101
+ }
102
+ }
103
+ return functions;
104
+ }
105
+ function extractCommonJSConstants(content) {
106
+ const constants = [];
107
+ // exports.CONSTANT_NAME = "value" or = { } (not functions)
108
+ const lines = content.split("\n");
109
+ for (const line of lines) {
110
+ // exports.NAME = "value" or number or object (not function)
111
+ const match = line.match(/exports\.(\w+)\s*=\s*(?!(?:async\s*)?(?:function|\(|async\s*\())/);
112
+ if (match) {
113
+ // Check it's likely a constant (UPPER_CASE or starts with config/options)
114
+ const name = match[1];
115
+ if (/^[A-Z_]+$/.test(name) || /^(config|options|settings)/i.test(name)) {
116
+ constants.push(name);
117
+ }
118
+ }
119
+ }
120
+ return constants;
121
+ }
122
+ function extractMongooseModels(content) {
123
+ const models = [];
124
+ // mongoose.model('ModelName', schema)
125
+ const matches = content.matchAll(/mongoose\.model\s*\(\s*['"](\w+)['"]/g);
126
+ for (const match of matches) {
127
+ models.push(match[1]);
128
+ }
129
+ return models;
130
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eloquence98/ctx",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Generate AI-ready context from your codebase. One command, zero config.",
5
5
  "type": "module",
6
6
  "bin": {