prodex 1.0.0 → 1.0.2

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
@@ -39,7 +39,13 @@ npm install --save-dev prodex
39
39
  Run directly from your project root:
40
40
 
41
41
  ```bash
42
- prodex
42
+ prodex
43
+ ```
44
+
45
+ OR
46
+
47
+ ```bash
48
+ npx prodex
43
49
  ```
44
50
 
45
51
  You’ll be guided through an interactive CLI:
@@ -159,21 +165,6 @@ prodex --depth 3 --output ./dump.txt --debug
159
165
 
160
166
  ---
161
167
 
162
- ## 🧾 Publishing to npm
163
-
164
- ```bash
165
- # Build and test locally
166
- npm run build
167
-
168
- # Version bump
169
- npm version patch # or minor / major
170
-
171
- # Log in and publish
172
- npm login
173
- npm publish --access public
174
- ```
175
-
176
- ---
177
168
 
178
169
  ## 💡 Philosophy
179
170
 
@@ -185,13 +176,6 @@ and designed to be both *safe* and *predictable.*
185
176
 
186
177
  ---
187
178
 
188
- ## 🧑‍💻 Maintained by **emxhive**
189
-
190
- Prodex is part of the **Forge** developer ecosystem by **emxhive**,
191
- a suite of modular tools for Laravel + React applications.
192
-
193
- ---
194
-
195
179
  ## 🧾 License
196
180
 
197
181
  MIT © 2025 emxhive
package/bin/prodex.js CHANGED
@@ -1,2 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import("../src/index.js").then(({ default: startProdex }) => startProdex());
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { pathToFileURL, fileURLToPath } from "url";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const devPath = path.resolve(__dirname, "../src/index.js");
10
+ const distPath = path.resolve(__dirname, "../dist/index.js");
11
+
12
+ const entry = fs.existsSync(distPath) ? distPath : devPath;
13
+
14
+ // Convert to file:// URL for Windows compatibility
15
+ const entryUrl = pathToFileURL(entry).href;
16
+
17
+ import(entryUrl).then(({ default: startProdex }) => startProdex());
package/dist/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Zeki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # 🧩 Prodex — Unified Project Indexer & Dependency Extractor
2
+
3
+ **Prodex** *(short for “Project Index”)* is a smart cross-language dependency combiner for modern web stacks — built to traverse **Laravel + React + TypeScript** projects and extract a clean, flattened scope of every linked file.
4
+
5
+ Whether you’re debugging imports, building AI context files, or simply auditing what your app actually depends on — Prodex builds you a unified **project index** in seconds.
6
+
7
+ ---
8
+
9
+ ## 🚀 Key Features
10
+
11
+ | Feature | Description |
12
+ |----------|-------------|
13
+ | ⚙️ **Cross-language resolver** | Understands both JavaScript / TypeScript (`import`, `require`, `export * from`) and PHP (`use`, `require`, `include`) dependency trees. |
14
+ | 🧩 **Laravel-aware bindings** | Reads your `app/Providers` and automatically maps interfaces to their concrete implementations. |
15
+ | 🧭 **Smart alias detection** | Parses `tsconfig.json` and `vite.config.*` for alias paths (`@/components/...`). |
16
+ | 🗂 **Grouped imports support** | Expands `use App\Http\Controllers\{A,B,C}` into individual files. |
17
+ | 🔄 **Recursive chain following** | Walks through imports, re-exports, and PSR-4 namespaces up to your configured depth. |
18
+ | 🪶 **Clean combined output** | Merges every resolved file into one `.txt` or `.md` file with region markers for readability. |
19
+ | 🧠 **Static & safe** | No runtime PHP execution — everything is parsed statically via regex + PSR-4 mapping. |
20
+
21
+ ---
22
+
23
+ ## 📦 Installation
24
+
25
+ ```bash
26
+ npm install -g prodex
27
+ ```
28
+
29
+ or locally:
30
+
31
+ ```bash
32
+ npm install --save-dev prodex
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 🧰 Usage
38
+
39
+ Run directly from your project root:
40
+
41
+ ```bash
42
+ prodex
43
+ ```
44
+
45
+ OR
46
+
47
+ ```bash
48
+ npx prodex
49
+ ```
50
+
51
+ You’ll be guided through an interactive CLI:
52
+
53
+ ```
54
+ 🧩 Prodex — Project Dependency Extractor
55
+ 🧩 Active Config:
56
+ • Output: ./combined.txt
57
+ • Scan Depth: 2
58
+ • Base Dirs: app, routes, resources/js
59
+ ```
60
+
61
+ After selecting files and confirming, Prodex generates:
62
+
63
+ ```
64
+ ✅ combined.txt written (12 file(s)).
65
+ ```
66
+
67
+ Each file appears wrapped in annotated regions:
68
+
69
+ ```
70
+ // ==== path: app/Services/Shots/ComputeService.php ====
71
+ // #region app/Services/Shots/ComputeService.php
72
+ <?php
73
+ // your code here...
74
+ // #endregion
75
+ ```
76
+
77
+ ---
78
+
79
+ ## ⚙️ Configuration
80
+
81
+ Create a `.prodex.json` in your project root (optional):
82
+
83
+ ```jsonc
84
+ {
85
+ "output": "./combined.txt",
86
+ "scanDepth": 3,
87
+ "baseDirs": ["app", "routes", "resources/js"],
88
+ "entryExcludes": ["vendor", "node_modules"],
89
+ "importExcludes": ["vendor", "tests"]
90
+ }
91
+ ```
92
+
93
+ Prodex automatically merges this with sane defaults.
94
+
95
+ ---
96
+
97
+ ## 🧩 How It Works
98
+
99
+ **1. Config Loader**
100
+ - Reads `.prodex.json`, `tsconfig.json`, and `vite.config.*`.
101
+ - Builds alias + exclusion map.
102
+
103
+ **2. JS Resolver**
104
+ - Extracts ES modules, dynamic imports, and re-exports.
105
+ - Resolves alias paths to absolute file locations.
106
+
107
+ **3. PHP Resolver**
108
+ - Parses `use`, grouped `use {}`, `require`, and `include`.
109
+ - Expands PSR-4 namespaces via `composer.json`.
110
+ - Loads bindings from all `app/Providers/*.php` to link interfaces to implementations.
111
+
112
+ **4. Combiner**
113
+ - Follows all dependency chains (recursive up to limit).
114
+ - Writes a single combined file with a TOC and inline region markers.
115
+
116
+ ---
117
+
118
+ ## 🧱 Example: Laravel + React Project
119
+
120
+ ```bash
121
+ prodex
122
+ ```
123
+
124
+ ```
125
+ 🧩 Following dependency chain...
126
+ ✅ combined.txt written (24 file(s)).
127
+ ```
128
+
129
+ Included files:
130
+
131
+ ```
132
+ resources/js/pages/accounts.tsx
133
+ app/Http/Controllers/Shots/AccountsController.php
134
+ app/Repositories/Shots/FireflyApiRepository.php
135
+ app/Enums/Shots/Granularity.php
136
+ app/Support/Shots/CacheKeys.php
137
+ ...
138
+ ```
139
+
140
+ ---
141
+
142
+ ## 🧠 Ideal Use Cases
143
+
144
+ - 🧩 Feeding combined source to **AI assistants / context engines**
145
+ - 🧪 Static dependency audits or architecture mapping
146
+ - 🧰 Quick “code snapshot” before refactors
147
+ - 📄 Documentation generation / single-file review
148
+
149
+ ---
150
+
151
+ ## 🔧 CLI Flags (optional)
152
+
153
+ | Flag | Description |
154
+ |------|-------------|
155
+ | `--depth <n>` | Override scan depth |
156
+ | `--output <path>` | Custom output path |
157
+ | `--no-chain` | Disable dependency recursion |
158
+ | `--debug` | Enable verbose logging |
159
+
160
+ Example:
161
+
162
+ ```bash
163
+ prodex --depth 3 --output ./dump.txt --debug
164
+ ```
165
+
166
+ ---
167
+
168
+
169
+ ## 💡 Philosophy
170
+
171
+ Prodex isn’t a linter or bundler —
172
+ it’s an **indexer** that unifies multi-language project contexts for smarter automation, analysis, and AI-assisted workflows.
173
+
174
+ Built with care for mixed stacks like **Laravel + Inertia + React**,
175
+ and designed to be both *safe* and *predictable.*
176
+
177
+ ---
178
+
179
+ ## 🧾 License
180
+
181
+ MIT © 2025 emxhive
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { pathToFileURL, fileURLToPath } from "url";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const devPath = path.resolve(__dirname, "../src/index.js");
10
+ const distPath = path.resolve(__dirname, "../dist/index.js");
11
+
12
+ const entry = fs.existsSync(distPath) ? distPath : devPath;
13
+
14
+ // Convert to file:// URL for Windows compatibility
15
+ const entryUrl = pathToFileURL(entry).href;
16
+
17
+ import(entryUrl).then(({ default: startProdex }) => startProdex());
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "prodex",
3
+ "version": "1.0.2",
4
+ "description": "Unified Project Indexer & Dependency Extractor for Laravel + React + Node stacks.",
5
+ "type": "module",
6
+ "bin": {
7
+ "prodex": "./bin/prodex.js"
8
+ },
9
+ "main": "./dist/core/combine.js",
10
+ "exports": {
11
+ ".": "./dist/core/combine.js"
12
+ },
13
+ "files": [
14
+ "dist/",
15
+ "bin/",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "keywords": [
20
+ "laravel",
21
+ "react",
22
+ "typescript",
23
+ "dependency",
24
+ "analyzer",
25
+ "cli",
26
+ "node",
27
+ "indexer"
28
+ ],
29
+ "scripts": {
30
+ "clean": "rm -rf dist",
31
+ "build": "npm run clean && mkdir -p dist && cp -r src bin package.json README.md LICENSE dist/",
32
+ "prepare": "npm run build"
33
+ },
34
+ "author": "emxhive",
35
+ "license": "MIT",
36
+ "devDependencies": {
37
+ "tsup": "^8.5.0",
38
+ "typescript": "^5.9.3"
39
+ },
40
+ "dependencies": {
41
+ "inquirer": "^12.10.0"
42
+ }
43
+ }
@@ -0,0 +1,51 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import inquirer from "inquirer";
4
+ import { ROOT } from "../constants/config.js";
5
+ import { walk, rel } from "../core/helpers.js";
6
+
7
+ export async function pickEntries(baseDirs, depth = 2) {
8
+ let selected = [];
9
+ while (true) {
10
+ const files = [];
11
+ for (const base of baseDirs) {
12
+ const full = path.join(ROOT, base);
13
+ if (!fs.existsSync(full)) continue;
14
+ for (const f of walk(full, 0, depth)) files.push(f);
15
+ }
16
+
17
+ const choices = files.map(f => ({ name: rel(f), value: f }));
18
+ choices.push(new inquirer.Separator());
19
+ choices.push({ name: "🔽 Load more (go deeper)", value: "__loadmore" });
20
+ choices.push({ name: "📝 Enter custom path", value: "__manual" });
21
+
22
+ const { picks } = await inquirer.prompt([
23
+ {
24
+ type: "checkbox",
25
+ name: "picks",
26
+ message: `Select entry files (depth ${depth})`,
27
+ choices,
28
+ loop: false,
29
+ pageSize: 20,
30
+ default: selected
31
+ }
32
+ ]);
33
+
34
+ if (picks.includes("__manual")) {
35
+ const { manual } = await inquirer.prompt([
36
+ { name: "manual", message: "Enter relative path:" }
37
+ ]);
38
+ if (manual.trim()) selected.push(path.resolve(ROOT, manual.trim()));
39
+ }
40
+
41
+ if (picks.includes("__loadmore")) {
42
+ depth++;
43
+ selected = picks.filter(p => !["__manual", "__loadmore"].includes(p));
44
+ continue;
45
+ }
46
+
47
+ selected = picks.filter(p => !["__manual", "__loadmore"].includes(p));
48
+ break;
49
+ }
50
+ return [...new Set(selected)];
51
+ }
@@ -0,0 +1,9 @@
1
+ export function showSummary({ outputDir, fileName, entries, scanDepth, limit, chain }) {
2
+ console.log("\n🧩 Active Run:");
3
+ console.log(" • Output Directory:", outputDir);
4
+ console.log(" • File Name:", fileName);
5
+ console.log(" • Entries:", entries.length);
6
+ console.log(" • Scan Depth:", scanDepth);
7
+ console.log(" • Limit:", limit);
8
+ console.log(" • Chain:", chain ? "Enabled" : "Disabled");
9
+ }
@@ -9,6 +9,11 @@ import {
9
9
  BASE_DIRS
10
10
  } from "./config.js";
11
11
 
12
+ /**
13
+ * Loads and merges the Prodex configuration.
14
+ * - `output` is treated strictly as a directory.
15
+ * - Defaults to ROOT/prodex when not defined.
16
+ */
12
17
  export function loadProdexConfig() {
13
18
  const configPath = path.join(ROOT, ".prodex.json");
14
19
  let userConfig = {};
@@ -17,24 +22,37 @@ export function loadProdexConfig() {
17
22
  try {
18
23
  const data = fs.readFileSync(configPath, "utf8");
19
24
  userConfig = JSON.parse(data);
20
- console.log("🧠 Loaded .prodex.json overrides");
25
+ console.log("? Loaded .prodex.json overrides");
21
26
  } catch (err) {
22
- console.warn("⚠️ Failed to parse .prodex.json:", err.message);
27
+ console.warn("?? Failed to parse .prodex.json:", err.message);
23
28
  }
24
29
  }
25
30
 
31
+ // Resolve output directory (always a folder now)
32
+ const outputDir = userConfig.output
33
+ ? path.resolve(ROOT, userConfig.output)
34
+ : path.join(ROOT, "prodex");
35
+
36
+ // Ensure directory exists
37
+ try {
38
+ fs.mkdirSync(outputDir, { recursive: true });
39
+ } catch (e) {
40
+ console.warn("?? Could not create output directory:", outputDir);
41
+ }
42
+
26
43
  const merged = {
27
- output: userConfig.output || OUT_FILE,
44
+ output: outputDir,
28
45
  scanDepth: userConfig.scanDepth || 2,
29
46
  codeExts: userConfig.codeExts || CODE_EXTS,
30
47
  entryExcludes: [...ENTRY_EXCLUDES, ...(userConfig.entryExcludes || [])],
31
48
  importExcludes: [...IMPORT_EXCLUDES, ...(userConfig.importExcludes || [])],
32
49
  baseDirs: [...new Set([...(userConfig.baseDirs || []), ...BASE_DIRS])],
33
- aliasOverrides: userConfig.aliasOverrides || {}
50
+ aliasOverrides: userConfig.aliasOverrides || {},
51
+ limit: userConfig.limit || 200
34
52
  };
35
53
 
36
- console.log("🧩 Active Config:");
37
- console.log(" • Output:", merged.output);
54
+ console.log("?? Active Config:");
55
+ console.log(" • Output Directory:", merged.output);
38
56
  console.log(" • Scan Depth:", merged.scanDepth);
39
57
  console.log(" • Base Dirs:", merged.baseDirs.join(", "));
40
58
  if (userConfig.entryExcludes || userConfig.importExcludes)
@@ -0,0 +1,84 @@
1
+ import { resolveJsImports } from "../resolvers/js-resolver.js";
2
+ import { resolvePhpImports } from "../resolvers/php-resolver.js";
3
+
4
+
5
+ export const ROOT = process.cwd();
6
+ export const OUT_FILE = ROOT + "/combined.txt";
7
+ export const CODE_EXTS = [".js", ".mjs", ".ts", ".tsx", ".d.ts", ".php"];
8
+ export const ENTRY_EXCLUDES = [
9
+ "resources/js/components/ui/",
10
+ "app/Enums/",
11
+ "app/DTOs/",
12
+ "app/Models/",
13
+ "app/Data/",
14
+ "resources/js/wayfinder/",
15
+ "resources/js/routes/",
16
+ "resources/js/actions/",
17
+ "resources/js/hooks/"
18
+ ];
19
+ export const IMPORT_EXCLUDES = [
20
+ "node_modules",
21
+ "@shadcn/",
22
+ "@/components/ui/",
23
+ "@components/ui/",
24
+ "resources/js/components/ui/",
25
+ "resources/js/hooks/",
26
+ "resources/js/wayfinder/",
27
+ "resources/js/routes/",
28
+ "resources/js/actions/"
29
+ ];
30
+ export const BASE_DIRS = ["src", "bin", "schema", "app", "routes", "resources/js"];
31
+
32
+
33
+ /**
34
+ * Resolver map — links file extensions to their resolver functions.
35
+ * Extend this to support new formats (.vue, .jsx, etc.).
36
+ */
37
+ export const RESOLVERS = {
38
+ ".php": resolvePhpImports,
39
+ ".ts": resolveJsImports,
40
+ ".tsx": resolveJsImports,
41
+ ".d.ts": resolveJsImports,
42
+ ".js": resolveJsImports
43
+ };
44
+
45
+ /**
46
+ * Prompt definitions used by Inquirer in combine.js.
47
+ * These are constants to keep UI consistent across releases.
48
+ */
49
+ export const PROMPTS = {
50
+ yesToAll: {
51
+ type: "confirm",
52
+ name: "yesToAll",
53
+ message: "Proceed automatically with default settings (Yes to all)?",
54
+ default: true
55
+ },
56
+ combine: [
57
+ {
58
+ type: "input",
59
+ name: "outputBase",
60
+ message: "Output base name (without extension):",
61
+ default: null, // will be set dynamically
62
+ filter: v => v.trim()
63
+ },
64
+ {
65
+ type: "number",
66
+ name: "limit",
67
+ message: "Limit number of merged files:",
68
+ default: 200, // will be overridden at runtime
69
+ validate: v => (!isNaN(v) && v > 0) || "Enter a valid positive number"
70
+ },
71
+ {
72
+ type: "confirm",
73
+ name: "chain",
74
+ message: "Follow dependency chain?",
75
+ default: true
76
+ },
77
+ {
78
+ type: "confirm",
79
+ name: "proceed",
80
+ message: "Proceed with combine?",
81
+ default: true
82
+ }
83
+ ]
84
+ };
@@ -0,0 +1,145 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import inquirer from "inquirer";
4
+ import {
5
+ ROOT,
6
+ CODE_EXTS,
7
+ RESOLVERS,
8
+ PROMPTS
9
+ } from "../constants/config.js";
10
+ import { loadProdexConfig } from "../constants/config-loader.js";
11
+ import { read, normalizeIndent, stripComments, rel } from "./helpers.js";
12
+ import { pickEntries } from "../cli/picker.js";
13
+ import { showSummary } from "../cli/summary.js";
14
+ import { generateOutputName, resolveOutputPath } from "./file-utils.js";
15
+
16
+ export async function runCombine() {
17
+ const cliLimitFlag = process.argv.find(arg => arg.startsWith("--limit="));
18
+ const customLimit = cliLimitFlag ? parseInt(cliLimitFlag.split("=")[1], 10) : null;
19
+
20
+ const cfg = loadProdexConfig();
21
+ const { baseDirs, scanDepth } = cfg;
22
+
23
+ const entries = await pickEntries(baseDirs, scanDepth);
24
+ if (!entries.length) {
25
+ console.log("❌ No entries selected.");
26
+ return;
27
+ }
28
+
29
+ const autoName = generateOutputName(entries);
30
+ const outputDir = cfg.output || path.join(ROOT, "prodex");
31
+ const defaultLimit = customLimit || cfg.limit || 200;
32
+
33
+ console.log("\n📋 You selected:");
34
+ for (const e of entries) console.log(" -", rel(e));
35
+
36
+ const { yesToAll } = await inquirer.prompt([PROMPTS.yesToAll]);
37
+
38
+ let outputBase = autoName,
39
+ limit = defaultLimit,
40
+ chain = true,
41
+ proceed = true;
42
+
43
+ if (!yesToAll) {
44
+ // clone static prompts with dynamic defaults
45
+ const combinePrompts = PROMPTS.combine.map(p => ({
46
+ ...p,
47
+ default:
48
+ p.name === "outputBase"
49
+ ? autoName
50
+ : p.name === "limit"
51
+ ? defaultLimit
52
+ : p.default
53
+ }));
54
+
55
+ const ans = await inquirer.prompt(combinePrompts);
56
+ outputBase = ans.outputBase || autoName;
57
+ limit = ans.limit;
58
+ chain = ans.chain;
59
+ proceed = ans.proceed;
60
+ }
61
+
62
+ if (!proceed) {
63
+ console.log("⚙️ Aborted.");
64
+ return;
65
+ }
66
+
67
+ // ensure output directory exists
68
+ try {
69
+ fs.mkdirSync(outputDir, { recursive: true });
70
+ } catch {
71
+ console.warn("⚠️ Could not create output directory:", outputDir);
72
+ }
73
+
74
+ const output = resolveOutputPath(outputDir, outputBase);
75
+
76
+ showSummary({
77
+ outputDir,
78
+ fileName: path.basename(output),
79
+ entries,
80
+ scanDepth: cfg.scanDepth,
81
+ limit,
82
+ chain
83
+ });
84
+
85
+ const finalFiles = chain ? await followChain(entries, limit) : entries;
86
+
87
+ fs.writeFileSync(
88
+ output,
89
+ [toc(finalFiles), ...finalFiles.map(render)].join(""),
90
+ "utf8"
91
+ );
92
+
93
+ console.log(`\n✅ ${output} written (${finalFiles.length} file(s)).`);
94
+ }
95
+
96
+ function header(p) {
97
+ return `##==== path: ${rel(p)} ====`;
98
+ }
99
+ function regionStart(p) {
100
+ return `##region ${rel(p)}`;
101
+ }
102
+ const regionEnd = "##endregion";
103
+
104
+ function render(p) {
105
+ const ext = path.extname(p);
106
+ let s = read(p);
107
+ s = stripComments(s, ext);
108
+ s = normalizeIndent(s);
109
+ return `${header(p)}\n${regionStart(p)}\n${s}\n${regionEnd}\n\n`;
110
+ }
111
+
112
+ function toc(files) {
113
+ return (
114
+ ["// ==== Combined Scope ====", ...files.map(f => "// - " + rel(f))].join(
115
+ "\n"
116
+ ) + "\n\n"
117
+ );
118
+ }
119
+
120
+ async function followChain(entryFiles, limit = 200) {
121
+ console.log("🧩 Following dependency chain...");
122
+ const visited = new Set();
123
+ const all = [];
124
+
125
+ for (const f of entryFiles) {
126
+ if (visited.has(f)) continue;
127
+ all.push(f);
128
+
129
+ const ext = path.extname(f);
130
+ if (!CODE_EXTS.includes(ext)) continue;
131
+
132
+ const resolver = RESOLVERS[ext];
133
+ if (resolver) {
134
+ const { files } = await resolver(f, visited);
135
+ all.push(...files);
136
+ }
137
+
138
+ if (all.length >= limit) {
139
+ console.log("⚠️ Limit reached:", limit);
140
+ break;
141
+ }
142
+ }
143
+
144
+ return [...new Set(all)];
145
+ }
@@ -0,0 +1,13 @@
1
+ import path from "path";
2
+
3
+ export function generateOutputName(entries) {
4
+ const names = entries.map(f => path.basename(f, path.extname(f)));
5
+ if (names.length === 1) return names[0];
6
+ if (names.length === 2) return `${names[0]}-${names[1]}`;
7
+ if (names.length > 2) return `${names[0]}-and-${names.length - 1}more`;
8
+ return "unknown";
9
+ }
10
+
11
+ export function resolveOutputPath(outputDir, base) {
12
+ return path.join(outputDir, `prodex-${base}-combined.txt`);
13
+ }
@@ -0,0 +1,116 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { ROOT, CODE_EXTS, ENTRY_EXCLUDES } from "../constants/config.js";
4
+
5
+ export function rel(p) {
6
+ return path.relative(ROOT, p).replaceAll("\\", "/");
7
+ }
8
+
9
+ export function read(p) {
10
+ try {
11
+ return fs.readFileSync(p, "utf8");
12
+ } catch {
13
+ return "";
14
+ }
15
+ }
16
+
17
+ export function normalizeIndent(s) {
18
+ return s
19
+ .replace(/\t/g, " ")
20
+ .split("\n")
21
+ .map(l => l.replace(/[ \t]+$/, ""))
22
+ .join("\n");
23
+ }
24
+
25
+ export function stripComments(code, ext) {
26
+ // Fast path for PHP or non-JS files — simple regex is fine
27
+ if (ext === ".php") {
28
+ return code
29
+ .replace(/\/\*[\s\S]*?\*\//g, "") // block comments
30
+ .replace(/^\s*#.*$/gm, ""); // line comments
31
+ }
32
+
33
+ // Robust JS/TS-safe parser — avoids stripping inside strings
34
+ let out = "";
35
+ let inStr = false;
36
+ let strChar = "";
37
+ let inBlockComment = false;
38
+ let inLineComment = false;
39
+
40
+ for (let i = 0; i < code.length; i++) {
41
+ const c = code[i];
42
+ const next = code[i + 1];
43
+
44
+ if (inBlockComment) {
45
+ if (c === "*" && next === "/") {
46
+ inBlockComment = false;
47
+ i++;
48
+ }
49
+ continue;
50
+ }
51
+
52
+ if (inLineComment) {
53
+ if (c === "\n") {
54
+ inLineComment = false;
55
+ out += c;
56
+ }
57
+ continue;
58
+ }
59
+
60
+ if (inStr) {
61
+ if (c === "\\" && next) {
62
+ out += c + next;
63
+ i++;
64
+ continue;
65
+ }
66
+ if (c === strChar) inStr = false;
67
+ out += c;
68
+ continue;
69
+ }
70
+
71
+ if (c === '"' || c === "'" || c === "`") {
72
+ inStr = true;
73
+ strChar = c;
74
+ out += c;
75
+ continue;
76
+ }
77
+
78
+ if (c === "/" && next === "*") {
79
+ inBlockComment = true;
80
+ i++;
81
+ continue;
82
+ }
83
+
84
+ if (c === "/" && next === "/") {
85
+ inLineComment = true;
86
+ i++;
87
+ continue;
88
+ }
89
+
90
+ out += c;
91
+ }
92
+
93
+ return out;
94
+ }
95
+
96
+
97
+ export function isEntryExcluded(p) {
98
+ const r = rel(p);
99
+ return ENTRY_EXCLUDES.some(ex => r.startsWith(ex) || r.includes(ex));
100
+ }
101
+
102
+ export function* walk(dir, depth = 0, maxDepth = 2) {
103
+ if (depth > maxDepth) return;
104
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
105
+ for (const e of entries) {
106
+ const full = path.join(dir, e.name);
107
+ if (e.isDirectory()) yield* walk(full, depth + 1, maxDepth);
108
+ else if (e.isFile()) {
109
+ const ext = path.extname(e.name).toLowerCase();
110
+ const relPath = rel(full);
111
+ if (CODE_EXTS.includes(ext) && !ENTRY_EXCLUDES.some(ex => relPath.startsWith(ex))) {
112
+ yield full;
113
+ }
114
+ }
115
+ }
116
+ }
@@ -15,7 +15,7 @@ export function loadLaravelBindings() {
15
15
 
16
16
  // Match: $this->app->bind(Interface::class, Implementation::class)
17
17
  const re =
18
- /\$this->app->(?:bind|singleton)\s*\(\s*([A-Za-z0-9_:\\\\]+)::class\s*,\s*([A-Za-z0-9_:\\\\]+)::class/g;
18
+ /$this->app->(?:bind|singleton)\s*\(\s*([A-Za-z0-9_:\\\\]+)::class\s*,\s*([A-Za-z0-9_:\\\\]+)::class/g;
19
19
 
20
20
  for (const file of files) {
21
21
  const code = fs.readFileSync(file, "utf8");
@@ -69,7 +69,7 @@ export async function resolvePhpImports(
69
69
  .split(",")
70
70
  .map(x => x.trim())
71
71
  .filter(Boolean);
72
- for (const p of parts) matches.add(`${base}\\${p}`);
72
+ for (const p of parts) matches.add(`${base}\${p}`);
73
73
  } else {
74
74
  matches.add(imp.trim());
75
75
  }
package/package.json CHANGED
@@ -1,18 +1,43 @@
1
1
  {
2
2
  "name": "prodex",
3
- "version": "1.0.0",
4
- "description": "Unified Project Indexer & Dependency Extractor for Laravel + React stacks.",
3
+ "version": "1.0.2",
4
+ "description": "Unified Project Indexer & Dependency Extractor for Laravel + React + Node stacks.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "prodex": "./bin/prodex.js"
8
8
  },
9
+ "main": "./dist/core/combine.js",
10
+ "exports": {
11
+ ".": "./dist/core/combine.js"
12
+ },
9
13
  "files": [
14
+ "dist/",
10
15
  "bin/",
11
- "src/",
12
16
  "README.md",
13
17
  "LICENSE"
14
18
  ],
15
- "keywords": ["laravel", "react", "typescript", "dependency", "analyzer", "cli"],
19
+ "keywords": [
20
+ "laravel",
21
+ "react",
22
+ "typescript",
23
+ "dependency",
24
+ "analyzer",
25
+ "cli",
26
+ "node",
27
+ "indexer"
28
+ ],
29
+ "scripts": {
30
+ "clean": "rm -rf dist",
31
+ "build": "npm run clean && mkdir -p dist && cp -r src bin package.json README.md LICENSE dist/",
32
+ "prepare": "npm run build"
33
+ },
16
34
  "author": "emxhive",
17
- "license": "MIT"
35
+ "license": "MIT",
36
+ "devDependencies": {
37
+ "tsup": "^8.5.0",
38
+ "typescript": "^5.9.3"
39
+ },
40
+ "dependencies": {
41
+ "inquirer": "^12.10.0"
42
+ }
18
43
  }
package/src/cli/picker.js DELETED
@@ -1,8 +0,0 @@
1
- import inquirer from "inquirer";
2
-
3
- export async function pickEntries() {
4
- const { path } = await inquirer.prompt([
5
- { name: "path", message: "Enter path to combine:", default: "src" }
6
- ]);
7
- return [path];
8
- }
@@ -1,4 +0,0 @@
1
- export function printSummary(entries) {
2
- console.log("\nYou selected:");
3
- for (const e of entries) console.log(" -", e);
4
- }
@@ -1,26 +0,0 @@
1
- export const ROOT = process.cwd();
2
- export const OUT_FILE = ROOT + "/combined.txt";
3
- export const CODE_EXTS = [".ts", ".tsx", ".d.ts", ".php"];
4
- export const ENTRY_EXCLUDES = [
5
- "resources/js/components/ui/",
6
- "app/Enums/",
7
- "app/DTOs/",
8
- "app/Models/",
9
- "app/Data/",
10
- "resources/js/wayfinder/",
11
- "resources/js/routes/",
12
- "resources/js/actions/",
13
- "resources/js/hooks/"
14
- ];
15
- export const IMPORT_EXCLUDES = [
16
- "node_modules",
17
- "@shadcn/",
18
- "@/components/ui/",
19
- "@components/ui/",
20
- "resources/js/components/ui/",
21
- "resources/js/hooks/",
22
- "resources/js/wayfinder/",
23
- "resources/js/routes/",
24
- "resources/js/actions/"
25
- ];
26
- export const BASE_DIRS = ["app", "routes", "resources/js"];
@@ -1,129 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import inquirer from "inquirer";
4
- import { ROOT } from "../constants/config.js";
5
- import { loadProdexConfig } from "../constants/config-loader.js";
6
- import { read, normalizeIndent, stripComments, walk, rel } from "./helpers.js";
7
- import { resolveJsImports } from "../resolvers/js-resolver.js";
8
- import { resolvePhpImports } from "../resolvers/php-resolver.js";
9
-
10
- export async function runCombine() {
11
- const cfg = loadProdexConfig();
12
- const { output, baseDirs, scanDepth } = cfg;
13
-
14
- const entries = await pickEntries(baseDirs, scanDepth);
15
- if (!entries.length) {
16
- console.log("❌ No entries selected.");
17
- return;
18
- }
19
-
20
- const { chain, limit, proceed } = await pickSettings(entries);
21
- if (!proceed) {
22
- console.log("⚙️ Aborted.");
23
- return;
24
- }
25
-
26
- const finalFiles = chain ? await followChain(entries, limit) : entries;
27
- fs.writeFileSync(output, [toc(finalFiles), ...finalFiles.map(render)].join(""), "utf8");
28
- console.log(`\n✅ ${output} written (${finalFiles.length} file(s)).`);
29
- }
30
-
31
- // ---------- UI ----------
32
- async function pickEntries(baseDirs, depth = 2) {
33
- let selected = [];
34
- while (true) {
35
- const files = [];
36
- for (const base of baseDirs) {
37
- const full = path.join(ROOT, base);
38
- if (!fs.existsSync(full)) continue;
39
- for (const f of walk(full, 0, depth)) files.push(f);
40
- }
41
-
42
- const choices = files.map(f => ({ name: rel(f), value: f }));
43
- choices.push(new inquirer.Separator());
44
- choices.push({ name: "🔽 Load more (go deeper)", value: "__loadmore" });
45
- choices.push({ name: "📝 Enter custom path", value: "__manual" });
46
-
47
- const { picks } = await inquirer.prompt([
48
- {
49
- type: "checkbox",
50
- name: "picks",
51
- message: `Select entry files (depth ${depth})`,
52
- choices,
53
- loop: false,
54
- pageSize: 20,
55
- default: selected
56
- }
57
- ]);
58
-
59
- if (picks.includes("__manual")) {
60
- const { manual } = await inquirer.prompt([{ name: "manual", message: "Enter relative path:" }]);
61
- if (manual.trim()) selected.push(path.resolve(ROOT, manual.trim()));
62
- }
63
-
64
- if (picks.includes("__loadmore")) {
65
- depth++;
66
- selected = picks.filter(p => !["__manual", "__loadmore"].includes(p));
67
- continue;
68
- }
69
-
70
- selected = picks.filter(p => !["__manual", "__loadmore"].includes(p));
71
- break;
72
- }
73
- return [...new Set(selected)];
74
- }
75
-
76
- async function pickSettings(entries) {
77
- console.log("\n📋 You selected:");
78
- for (const e of entries) console.log(" -", rel(e));
79
- const ans = await inquirer.prompt([
80
- { type: "confirm", name: "chain", message: "Follow dependency chain?", default: true },
81
- { type: "number", name: "limit", message: "Limit number of merged files:", default: 200, validate: v => (!isNaN(v) && v > 0) || "Enter valid number" },
82
- { type: "confirm", name: "proceed", message: "Proceed with combine?", default: true }
83
- ]);
84
- return ans;
85
- }
86
-
87
- // ---------- Combine logic ----------
88
- function header(p) { return `// ==== path: ${rel(p)} ====`; }
89
- function regionStart(p) { return `// #region ${rel(p)}`; }
90
- const regionEnd = "// #endregion";
91
-
92
- function render(p) {
93
- const ext = path.extname(p);
94
- let s = read(p);
95
- s = stripComments(s, ext);
96
- s = normalizeIndent(s);
97
- return `${header(p)}\n${regionStart(p)}\n${s}\n${regionEnd}\n\n`;
98
- }
99
-
100
- function toc(files) {
101
- return ["// ==== Combined Scope ====", ...files.map(f => "// - " + rel(f))].join("\n") + "\n\n";
102
- }
103
-
104
- async function followChain(entryFiles, limit = 200) {
105
- console.log("🧩 Following dependency chain...");
106
- const visited = new Set();
107
- const all = [];
108
-
109
- for (const f of entryFiles) {
110
- if (visited.has(f)) continue;
111
- all.push(f);
112
- const ext = path.extname(f);
113
-
114
- if ([".ts", ".tsx", ".d.ts"].includes(ext)) {
115
- const { files } = await resolveJsImports(f, visited);
116
- all.push(...files);
117
- } else if (ext === ".php") {
118
- const { files } = await resolvePhpImports(f, visited);
119
- all.push(...files);
120
- }
121
-
122
- if (all.length >= limit) {
123
- console.log("⚠️ Limit reached:", limit);
124
- break;
125
- }
126
- }
127
-
128
- return [...new Set(all)];
129
- }
@@ -1,23 +0,0 @@
1
- export const ENTRY_EXCLUDES = [
2
- "resources/js/components/ui/",
3
- "app/Enums/",
4
- "app/DTOs/",
5
- "app/Models/",
6
- "app/Data/",
7
- "resources/js/wayfinder/",
8
- "resources/js/routes/",
9
- "resources/js/actions/",
10
- "resources/js/hooks/",
11
- ];
12
-
13
- export const IMPORT_EXCLUDES = [
14
- "node_modules",
15
- "@shadcn/",
16
- "@/components/ui/",
17
- "@components/ui/",
18
- "resources/js/components/ui/",
19
- "resources/js/hooks/",
20
- "resources/js/wayfinder/",
21
- "resources/js/routes/",
22
- "resources/js/actions/",
23
- ];
@@ -1,19 +0,0 @@
1
- import inquirer from "inquirer";
2
- import fs from "fs";
3
- import path from "path";
4
-
5
- export async function runCombine() {
6
- console.log("📦 Combine mode active. (Stub)");
7
- console.log("This is where the full combine logic from your previous script will go.\n");
8
-
9
- const { confirm } = await inquirer.prompt([
10
- { type: "confirm", name: "confirm", message: "Would you like to list project files?", default: true }
11
- ]);
12
-
13
- if (confirm) {
14
- const files = fs.readdirSync(process.cwd());
15
- console.log("Found files:", files);
16
- } else {
17
- console.log("Aborted.");
18
- }
19
- }
@@ -1,51 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { ROOT, CODE_EXTS, ENTRY_EXCLUDES } from "../constants/config.js";
4
-
5
- export function rel(p) {
6
- return path.relative(ROOT, p).replaceAll("\\", "/");
7
- }
8
-
9
- export function read(p) {
10
- try {
11
- return fs.readFileSync(p, "utf8");
12
- } catch {
13
- return "";
14
- }
15
- }
16
-
17
- export function normalizeIndent(s) {
18
- return s
19
- .replace(/\t/g, " ")
20
- .split("\n")
21
- .map(l => l.replace(/[ \t]+$/, ""))
22
- .join("\n");
23
- }
24
-
25
- export function stripComments(code, ext) {
26
- let s = code.replace(/\/\*[\s\S]*?\*\//g, "");
27
- s = s.replace(/(^|[^:])\/\/.*$/gm, (_m, p1) => p1);
28
- if (ext === ".php") s = s.replace(/^\s*#.*$/gm, "");
29
- return s;
30
- }
31
-
32
- export function isEntryExcluded(p) {
33
- const r = rel(p);
34
- return ENTRY_EXCLUDES.some(ex => r.startsWith(ex) || r.includes(ex));
35
- }
36
-
37
- export function* walk(dir, depth = 0, maxDepth = 2) {
38
- if (depth > maxDepth) return;
39
- const entries = fs.readdirSync(dir, { withFileTypes: true });
40
- for (const e of entries) {
41
- const full = path.join(dir, e.name);
42
- if (e.isDirectory()) yield* walk(full, depth + 1, maxDepth);
43
- else if (e.isFile()) {
44
- const ext = path.extname(e.name).toLowerCase();
45
- const relPath = rel(full);
46
- if (CODE_EXTS.includes(ext) && !ENTRY_EXCLUDES.some(ex => relPath.startsWith(ex))) {
47
- yield full;
48
- }
49
- }
50
- }
51
- }
File without changes
File without changes
File without changes
File without changes