@eloquence98/ctx 0.3.1 → 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 trivially 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 trivially detectable; ignores re-exports, computed exports, and unusual formatting)
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
 
@@ -36,9 +38,19 @@ src/
36
38
 
37
39
  Files whose exports cannot be determined are listed without symbols.
38
40
 
39
- ## 🧠 Why this exists
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`
46
+
47
+ #### CommonJS:
40
48
 
41
- 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.
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,48 +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
60
- - ❌ Parse complex exports (re-exports, barrel files, computed names)
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
61
70
 
62
71
  See [LIMITATIONS.md](https://github.com/Eloquence98/ctx/blob/main/limitation.md) for detailed edge cases.
63
72
 
64
- ## ⚙️ Configuration
73
+ ## Configuration
65
74
 
66
75
  No configuration required.
67
76
 
68
77
  ctx automatically ignores:
69
78
 
70
- - `node_modules`
71
- - `.git`
72
- - Build outputs (`dist`, `build`, etc.)
79
+ - `node_modules`, `.git`
80
+ - Build outputs (`dist`, `build`, `.next`)
73
81
  - Environment files (`.env`)
74
82
  - Test files (`.test`., `.spec`.)
83
+ - Hidden files and directories
75
84
 
76
- Only `.ts`, `.tsx`, `.js`, `.jsx` files are scanned.
85
+ Only `.ts`, `.tsx`, `.js`, `.jsx` files are scanned. Both ES module and CommonJS exports are detected.
77
86
 
78
- ## 💡 Philosophy
79
-
80
- Don’t explain the code. Show the codebase as it exists.
81
-
82
- ctx is intentionally shallow: it parses only what can be reliably read from source text.
83
-
84
- ## ⚡️ Install (optional)
87
+ ## Install (optional)
85
88
 
86
89
  ```bash
87
90
  npm install -g @eloquence98/ctx
88
91
  ctx ./src
89
92
  ```
90
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
+
91
100
  ## License
92
101
 
93
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@eloquence98/ctx",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Generate a truthful structural map of your codebase for AI and humans.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,7 @@
23
23
  "license": "MIT",
24
24
  "repository": {
25
25
  "type": "git",
26
- "url": "https://github.com/Eloquence98/ctx.git"
26
+ "url": "git+https://github.com/Eloquence98/ctx.git"
27
27
  },
28
28
  "homepage": "https://github.com/Eloquence98/ctx",
29
29
  "files": [
@@ -33,5 +33,8 @@
33
33
  "@types/node": "^20.0.0",
34
34
  "tsx": "^4.0.0",
35
35
  "typescript": "^5.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@eloquence98/ctx": "file:eloquence98-ctx-0.4.0.tgz"
36
39
  }
37
- }
40
+ }