@eloquence98/ctx 0.3.0 → 0.4.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
@@ -4,9 +4,9 @@ Dump a truthful structural index of a codebase.
4
4
 
5
5
  No analysis. No opinions. No guessing.
6
6
 
7
- ctx scans a directory and prints a map of folders, files, and statically detectable exported symbols. It tells you exactly what exists—nothing more, nothing less.
7
+ ctx scans a directory and prints a map of folders, files, and trivially detectable exported symbols. It tells you exactly what exists nothing more, nothing less.
8
8
 
9
- ## ⚡️ Quick Start
9
+ ## Quick Start
10
10
 
11
11
  No installation required. Run it directly with npx:
12
12
 
@@ -14,13 +14,15 @@ No installation required. Run it directly with npx:
14
14
  npx @eloquence98/ctx ./path-to-project
15
15
  ```
16
16
 
17
- ## 📖 What it does
17
+ ## What It Does
18
18
 
19
19
  ctx provides a high-level map of a project. It identifies:
20
20
 
21
- - 📂 Folders
22
- - 📄 Files
23
- - ➡️ Exported Symbols (when statically detectable)
21
+ - Folders
22
+ - Files
23
+ - Exported symbols (both ES modules and CommonJS, when trivially detectable)
24
+
25
+ Exports that cannot be statically determined from source text are silently ignored.
24
26
 
25
27
  ## Example Output
26
28
 
@@ -34,11 +36,21 @@ src/
34
36
  └─ styles.css
35
37
  ```
36
38
 
37
- If exports cannot be determined (e.g., non-code files or complex dynamic exports), the file is listed without symbols.
39
+ Files whose exports cannot be determined are listed without symbols.
40
+
41
+ ## Supported Export Patterns
42
+
43
+ #### ES Modules:
44
+
45
+ `export function`, `export const/let/var`, `export class`, `export type`, `export interface`, `export default function/class`
38
46
 
39
- ## 🧠 Why this exists
47
+ #### CommonJS:
40
48
 
41
- When working with LLMs (ChatGPT, Claude, etc.), new contributors, or legacy codebases, you don't always need the content of the files immediately; you need to understand the topology of the project first.
49
+ `exports.name = ...`, `module.exports.name = ...`, `module.exports = { name1, name2 }`, `module.exports = function/class`
50
+
51
+ ## Why This Exists
52
+
53
+ When working with LLMs, new contributors, or legacy codebases, you don't always need the content of the files immediately, you need to understand the topology of the project first.
42
54
 
43
55
  ctx gives you that map.
44
56
 
@@ -46,43 +58,45 @@ ctx gives you that map.
46
58
  2. Paste it into an LLM context window.
47
59
  3. Ask informed questions about the architecture before dumping raw code.
48
60
 
49
- ## 🚫 What it does NOT do
50
-
51
- ctx is intentionally dumb. That is why it is reliable.
61
+ ## What It Does Not Do
52
62
 
53
- It does not:
63
+ ctx is intentionally shallow. That is why it is reliable.
54
64
 
55
- - Interpret architecture or infer domains.
56
- - Explain code intent.
57
- - Refactor or execute code.
58
- - Read node_modules or .git folders.
59
- - Read environment variables.
65
+ - Does not interpret architecture or infer domains
66
+ - Does not explain code intent
67
+ - Does not refactor or execute code
68
+ - Does not read `node_modules`, `.git`, or environment files
69
+ - Does not parse re-exports, barrel files, or computed names
60
70
 
61
- It is not a framework detector, a dependency graph tool, or a documentation generator.
71
+ See [LIMITATIONS.md](https://github.com/Eloquence98/ctx/blob/main/limitation.md) for detailed edge cases.
62
72
 
63
- ## ⚙️ Configuration
73
+ ## Configuration
64
74
 
65
75
  No configuration required.
66
76
 
67
77
  ctx automatically ignores:
68
78
 
69
- - node_modules
70
- - .git
71
- - Build outputs (dist, build, etc.)
72
- - Environment files (.env)
73
- - Test files (.test., .spec.)
79
+ - `node_modules`, `.git`
80
+ - Build outputs (`dist`, `build`, `.next`)
81
+ - Environment files (`.env`)
82
+ - Test files (`.test`., `.spec`.)
83
+ - Hidden files and directories
74
84
 
75
- ## 💡 Philosophy
85
+ Only `.ts`, `.tsx`, `.js`, `.jsx` files are scanned. Both ES module and CommonJS exports are detected.
76
86
 
77
- Don't explain the code. Show the codebase as it exists.
78
-
79
- ## ⚡️ Install (optional)
87
+ ## Install (optional)
80
88
 
81
89
  ```bash
82
90
  npm install -g @eloquence98/ctx
83
91
  ctx ./src
84
92
  ```
85
93
 
94
+ ## Philosophy
95
+
96
+ Don't explain the code. Show the codebase as it exists.
97
+
98
+ ctx prefers truthful omission over incorrect inference. If something cannot be determined reliably, it is excluded.
99
+
86
100
  ## License
87
101
 
88
- [MIT](https://choosealicense.com/licenses/mit/)
102
+ [MIT ](https://choosealicense.com/licenses/mit/)
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for ctx.
4
+ * Scans a directory, parses exports, and prints a formatted tree.
5
+ *
6
+ * @example
7
+ * npx ctx ./src
8
+ */
2
9
  export {};
package/dist/cli.js CHANGED
@@ -1,21 +1,29 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for ctx.
4
+ * Scans a directory, parses exports, and prints a formatted tree.
5
+ *
6
+ * @example
7
+ * npx ctx ./src
8
+ */
2
9
  import path from "path";
3
- import { scan } from "./scanner.js";
4
- import { parse } from "./parser.js";
5
10
  import { format } from "./formatter.js";
11
+ import { parse } from "./parser.js";
12
+ import { scan } from "./scanner.js";
6
13
  const args = process.argv.slice(2);
7
14
  const targetPath = args[0] || ".";
15
+ /**
16
+ * Main CLI execution flow.
17
+ * Orchestrates scanning → parsing → formatting.
18
+ */
8
19
  async function main() {
9
20
  const dir = path.resolve(process.cwd(), targetPath);
10
- // Scan
11
21
  const files = await scan(dir);
12
22
  if (files.length === 0) {
13
23
  console.log("No files found.");
14
24
  process.exit(1);
15
25
  }
16
- // Parse
17
26
  const parsed = await Promise.all(files.map(parse));
18
- // Format and print
19
27
  console.log(format(parsed, dir));
20
28
  }
21
29
  main().catch(console.error);
@@ -1,2 +1,15 @@
1
1
  import type { ParsedFile } from "./types.js";
2
+ /**
3
+ * Formats parsed files into a tree-style string representation.
4
+ *
5
+ * @param files - Array of parsed files
6
+ * @param baseDir - Root directory path for relative path calculation
7
+ * @returns Formatted tree string
8
+ *
9
+ * @example
10
+ * const output = format(parsedFiles, "/project/src");
11
+ * // src/
12
+ * // ├─ index.ts → main
13
+ * // └─ utils.ts → format, parse
14
+ */
2
15
  export declare function format(files: ParsedFile[], baseDir: string): string;
package/dist/formatter.js CHANGED
@@ -1,6 +1,18 @@
1
1
  import path from "path";
2
+ /**
3
+ * Formats parsed files into a tree-style string representation.
4
+ *
5
+ * @param files - Array of parsed files
6
+ * @param baseDir - Root directory path for relative path calculation
7
+ * @returns Formatted tree string
8
+ *
9
+ * @example
10
+ * const output = format(parsedFiles, "/project/src");
11
+ * // src/
12
+ * // ├─ index.ts → main
13
+ * // └─ utils.ts → format, parse
14
+ */
2
15
  export function format(files, baseDir) {
3
- // Build tree structure
4
16
  const root = { name: path.basename(baseDir), children: new Map() };
5
17
  for (const file of files) {
6
18
  const rel = path.relative(baseDir, file.path);
@@ -22,12 +34,18 @@ export function format(files, baseDir) {
22
34
  current = current.children.get(part);
23
35
  }
24
36
  }
25
- // Render tree
26
37
  const lines = [];
27
38
  lines.push(root.name + "/");
28
39
  renderTree(root, "", lines);
29
40
  return lines.join("\n");
30
41
  }
42
+ /**
43
+ * Recursively renders tree nodes into formatted lines.
44
+ *
45
+ * @param node - Current tree node
46
+ * @param prefix - Indentation prefix for current depth
47
+ * @param lines - Output array to append lines to
48
+ */
31
49
  function renderTree(node, prefix, lines) {
32
50
  const children = [...node.children.values()];
33
51
  for (let i = 0; i < children.length; i++) {
@@ -35,7 +53,6 @@ function renderTree(node, prefix, lines) {
35
53
  const isLast = i === children.length - 1;
36
54
  const connector = isLast ? "└─ " : "├─ ";
37
55
  const extension = isLast ? " " : "│ ";
38
- // Is it a file (has exports defined) or folder?
39
56
  const isFile = child.exports !== undefined;
40
57
  if (isFile) {
41
58
  const exportsStr = child.exports.length > 0 ? ` → ${child.exports.join(", ")}` : "";
package/dist/parser.d.ts CHANGED
@@ -1,2 +1,13 @@
1
1
  import type { ParsedFile } from "./types.js";
2
+ /**
3
+ * Parses a source file and extracts exported symbol names.
4
+ * Supports both ES modules and CommonJS exports.
5
+ *
6
+ * @param filePath - Absolute path to the file
7
+ * @returns Parsed file object with export names
8
+ *
9
+ * @example
10
+ * const result = await parse("./src/utils.ts");
11
+ * // { path: "./src/utils.ts", name: "utils.ts", exports: ["format", "parse"] }
12
+ */
2
13
  export declare function parse(filePath: string): Promise<ParsedFile>;
package/dist/parser.js CHANGED
@@ -1,15 +1,29 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
+ /**
4
+ * Parses a source file and extracts exported symbol names.
5
+ * Supports both ES modules and CommonJS exports.
6
+ *
7
+ * @param filePath - Absolute path to the file
8
+ * @returns Parsed file object with export names
9
+ *
10
+ * @example
11
+ * const result = await parse("./src/utils.ts");
12
+ * // { path: "./src/utils.ts", name: "utils.ts", exports: ["format", "parse"] }
13
+ */
3
14
  export async function parse(filePath) {
4
- const content = await fs.readFile(filePath, "utf-8");
15
+ let content = await fs.readFile(filePath, "utf-8");
5
16
  const name = path.basename(filePath);
6
17
  const exports = [];
18
+ // Remove comments to avoid false positives from commented-out code
19
+ content = stripComments(content);
20
+ // === ES Module Exports ===
7
21
  // export function Name
8
22
  for (const match of content.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g)) {
9
23
  exports.push(match[1]);
10
24
  }
11
- // export const Name
12
- for (const match of content.matchAll(/export\s+const\s+(\w+)/g)) {
25
+ // export const/let/var Name
26
+ for (const match of content.matchAll(/export\s+(?:const|let|var)\s+(\w+)/g)) {
13
27
  exports.push(match[1]);
14
28
  }
15
29
  // export type/interface Name
@@ -24,5 +38,167 @@ export async function parse(filePath) {
24
38
  for (const match of content.matchAll(/export\s+default\s+(?:function|class)\s+(\w+)/g)) {
25
39
  exports.push(match[1]);
26
40
  }
27
- return { path: filePath, name, exports };
41
+ // === CommonJS Exports ===
42
+ // module.exports.name = ...
43
+ for (const match of content.matchAll(/module\.exports\.(\w+)\s*=/g)) {
44
+ exports.push(match[1]);
45
+ }
46
+ // exports.name = ... (but not module.exports.name)
47
+ for (const match of content.matchAll(/(?<!module\.)exports\.(\w+)\s*=/g)) {
48
+ exports.push(match[1]);
49
+ }
50
+ // module.exports = { name1, name2: value, ... }
51
+ for (const match of content.matchAll(/module\.exports\s*=\s*\{/g)) {
52
+ const startIdx = match.index + match[0].length;
53
+ const objectExports = parseExportsObject(content, startIdx);
54
+ exports.push(...objectExports);
55
+ }
56
+ // module.exports = function name() {} or async function name() {}
57
+ for (const match of content.matchAll(/module\.exports\s*=\s*(?:async\s+)?function\s+(\w+)/g)) {
58
+ exports.push(match[1]);
59
+ }
60
+ // module.exports = class Name {}
61
+ for (const match of content.matchAll(/module\.exports\s*=\s*class\s+(\w+)/g)) {
62
+ exports.push(match[1]);
63
+ }
64
+ // module.exports = Identifier (e.g., module.exports = MyClass;)
65
+ for (const match of content.matchAll(/module\.exports\s*=\s*([A-Z]\w*)\s*;?\s*$/gm)) {
66
+ exports.push(match[1]);
67
+ }
68
+ return { path: filePath, name, exports: [...new Set(exports)] };
69
+ }
70
+ /**
71
+ * Removes single-line (//) and multi-line (/* *\/) comments from source code.
72
+ * Preserves string literals to avoid breaking code structure.
73
+ *
74
+ * @param code - Raw source code
75
+ * @returns Code with comments removed
76
+ */
77
+ function stripComments(code) {
78
+ let result = "";
79
+ let i = 0;
80
+ while (i < code.length) {
81
+ // Handle string literals - don't strip inside strings
82
+ if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
83
+ const quote = code[i];
84
+ result += code[i++];
85
+ while (i < code.length && code[i] !== quote) {
86
+ if (code[i] === "\\") {
87
+ result += code[i++];
88
+ }
89
+ if (i < code.length) {
90
+ result += code[i++];
91
+ }
92
+ }
93
+ if (i < code.length) {
94
+ result += code[i++];
95
+ }
96
+ }
97
+ // Single-line comment
98
+ else if (code[i] === "/" && code[i + 1] === "/") {
99
+ while (i < code.length && code[i] !== "\n") {
100
+ i++;
101
+ }
102
+ }
103
+ // Multi-line comment
104
+ else if (code[i] === "/" && code[i + 1] === "*") {
105
+ i += 2;
106
+ while (i < code.length && !(code[i] === "*" && code[i + 1] === "/")) {
107
+ i++;
108
+ }
109
+ i += 2;
110
+ }
111
+ // Regular character
112
+ else {
113
+ result += code[i++];
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+ /**
119
+ * Parses a CommonJS exports object literal and extracts property names.
120
+ * Handles nested objects/arrays by tracking bracket depth.
121
+ *
122
+ * @param content - Source code content
123
+ * @param startIdx - Index where the object literal begins (after opening brace)
124
+ * @returns Array of exported property names
125
+ *
126
+ * @example
127
+ * // For: module.exports = { foo, bar: value }
128
+ * parseExportsObject(content, indexAfterBrace);
129
+ * // Returns: ["foo", "bar"]
130
+ */
131
+ function parseExportsObject(content, startIdx) {
132
+ const exports = [];
133
+ let depth = 0;
134
+ let i = startIdx;
135
+ let token = "";
136
+ let afterColon = false;
137
+ const keywords = new Set([
138
+ "function",
139
+ "async",
140
+ "class",
141
+ "return",
142
+ "const",
143
+ "let",
144
+ "var",
145
+ "true",
146
+ "false",
147
+ "null",
148
+ "undefined",
149
+ "new",
150
+ "require",
151
+ "await",
152
+ ]);
153
+ while (i < content.length) {
154
+ const char = content[i];
155
+ if (char === "{" || char === "[" || char === "(") {
156
+ depth++;
157
+ token = "";
158
+ }
159
+ else if (char === "}") {
160
+ if (depth === 0) {
161
+ if (token && !afterColon && !keywords.has(token)) {
162
+ exports.push(token);
163
+ }
164
+ break;
165
+ }
166
+ depth--;
167
+ token = "";
168
+ }
169
+ else if (char === "]" || char === ")") {
170
+ depth--;
171
+ token = "";
172
+ }
173
+ else if (depth === 0) {
174
+ if (/[a-zA-Z_$]/.test(char)) {
175
+ token += char;
176
+ }
177
+ else if (/[0-9]/.test(char) && token) {
178
+ token += char;
179
+ }
180
+ else if (char === ":") {
181
+ if (token && !keywords.has(token)) {
182
+ exports.push(token);
183
+ }
184
+ token = "";
185
+ afterColon = true;
186
+ }
187
+ else if (char === ",") {
188
+ if (token && !afterColon && !keywords.has(token)) {
189
+ exports.push(token);
190
+ }
191
+ token = "";
192
+ afterColon = false;
193
+ }
194
+ else if (/\s/.test(char)) {
195
+ // continue
196
+ }
197
+ else {
198
+ token = "";
199
+ }
200
+ }
201
+ i++;
202
+ }
203
+ return exports;
28
204
  }
package/dist/scanner.d.ts CHANGED
@@ -1 +1,12 @@
1
+ /**
2
+ * Recursively scans a directory for JavaScript/TypeScript files.
3
+ * Ignores node_modules, build outputs, hidden files, and test files.
4
+ *
5
+ * @param dir - The directory path to scan
6
+ * @returns Array of absolute file paths
7
+ *
8
+ * @example
9
+ * const files = await scan("./src");
10
+ * // ["/project/src/index.ts", "/project/src/utils.ts", ...]
11
+ */
1
12
  export declare function scan(dir: string): Promise<string[]>;
package/dist/scanner.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
+ /** Directories and files to ignore during scanning */
3
4
  const IGNORE = [
4
5
  "node_modules",
5
6
  ".git",
@@ -14,7 +15,19 @@ const IGNORE = [
14
15
  ".idea",
15
16
  "coverage",
16
17
  ];
18
+ /** File extensions to include in scan results */
17
19
  const EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
20
+ /**
21
+ * Recursively scans a directory for JavaScript/TypeScript files.
22
+ * Ignores node_modules, build outputs, hidden files, and test files.
23
+ *
24
+ * @param dir - The directory path to scan
25
+ * @returns Array of absolute file paths
26
+ *
27
+ * @example
28
+ * const files = await scan("./src");
29
+ * // ["/project/src/index.ts", "/project/src/utils.ts", ...]
30
+ */
18
31
  export async function scan(dir) {
19
32
  const files = [];
20
33
  async function walk(current) {
@@ -26,7 +39,6 @@ export async function scan(dir) {
26
39
  return;
27
40
  }
28
41
  for (const entry of entries) {
29
- // Skip ignored
30
42
  if (IGNORE.includes(entry.name))
31
43
  continue;
32
44
  if (entry.name.startsWith("."))
package/dist/types.d.ts CHANGED
@@ -1,9 +1,20 @@
1
+ /**
2
+ * Represents a parsed source file with its exported symbols.
3
+ */
1
4
  export interface ParsedFile {
5
+ /** Absolute path to the file */
2
6
  path: string;
7
+ /** Base filename (e.g., "parser.ts") */
3
8
  name: string;
9
+ /** List of exported symbol names */
4
10
  exports: string[];
5
11
  }
12
+ /**
13
+ * Represents a folder and its parsed files.
14
+ */
6
15
  export interface FolderContent {
16
+ /** Folder path */
7
17
  folder: string;
18
+ /** Parsed files within this folder */
8
19
  files: ParsedFile[];
9
20
  }
package/dist/types.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eloquence98/ctx",
3
- "version": "0.3.0",
4
- "description": "Scan your codebase. Get a clean summary. Paste it to AI.",
3
+ "version": "0.4.0",
4
+ "description": "Generate a truthful structural map of your codebase for AI and humans.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ctx": "dist/cli.js"
@@ -15,17 +15,15 @@
15
15
  "ai",
16
16
  "context",
17
17
  "codebase",
18
- "documentation",
19
18
  "cli",
20
19
  "typescript",
21
- "nextjs",
22
- "react"
20
+ "javascript"
23
21
  ],
24
22
  "author": "Eloquence98",
25
23
  "license": "MIT",
26
24
  "repository": {
27
25
  "type": "git",
28
- "url": "https://github.com/Eloquence98/ctx.git"
26
+ "url": "git+https://github.com/Eloquence98/ctx.git"
29
27
  },
30
28
  "homepage": "https://github.com/Eloquence98/ctx",
31
29
  "files": [
@@ -35,5 +33,8 @@
35
33
  "@types/node": "^20.0.0",
36
34
  "tsx": "^4.0.0",
37
35
  "typescript": "^5.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@eloquence98/ctx": "file:eloquence98-ctx-0.4.0.tgz"
38
39
  }
39
- }
40
+ }