prodex 1.0.1 → 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/bin/prodex.js +16 -1
- package/dist/LICENSE +21 -0
- package/dist/README.md +181 -0
- package/dist/bin/prodex.js +17 -0
- package/dist/package.json +43 -0
- package/dist/src/cli/picker.js +51 -0
- package/dist/src/cli/summary.js +9 -0
- package/{src → dist/src}/constants/config-loader.js +24 -6
- package/dist/src/constants/config.js +84 -0
- package/dist/src/core/combine.js +145 -0
- package/dist/src/core/file-utils.js +13 -0
- package/dist/src/core/helpers.js +116 -0
- package/{src → dist/src}/resolvers/php-bindings.js +1 -1
- package/{src → dist/src}/resolvers/php-resolver.js +1 -1
- package/package.json +23 -5
- package/src/cli/picker.js +0 -8
- package/src/cli/summary.js +0 -4
- package/src/constants/config.js +0 -26
- package/src/core/combine.js +0 -129
- package/src/core/exclusions.js +0 -23
- package/src/core/file-utils.js +0 -19
- package/src/core/helpers.js +0 -51
- /package/{src → dist/src}/cli/init.js +0 -0
- /package/{src → dist/src}/core/alias-loader.js +0 -0
- /package/{src → dist/src}/index.js +0 -0
- /package/{src → dist/src}/resolvers/js-resolver.js +0 -0
package/bin/prodex.js
CHANGED
|
@@ -1,2 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
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("
|
|
25
|
+
console.log("? Loaded .prodex.json overrides");
|
|
21
26
|
} catch (err) {
|
|
22
|
-
console.warn("
|
|
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:
|
|
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("
|
|
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
|
-
|
|
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");
|
package/package.json
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prodex",
|
|
3
|
-
"version": "1.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
|
],
|
|
@@ -18,8 +22,22 @@
|
|
|
18
22
|
"typescript",
|
|
19
23
|
"dependency",
|
|
20
24
|
"analyzer",
|
|
21
|
-
"cli"
|
|
25
|
+
"cli",
|
|
26
|
+
"node",
|
|
27
|
+
"indexer"
|
|
22
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
|
+
},
|
|
23
34
|
"author": "emxhive",
|
|
24
|
-
"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
|
+
}
|
|
25
43
|
}
|
package/src/cli/picker.js
DELETED
package/src/cli/summary.js
DELETED
package/src/constants/config.js
DELETED
|
@@ -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"];
|
package/src/core/combine.js
DELETED
|
@@ -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
|
-
}
|
package/src/core/exclusions.js
DELETED
|
@@ -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
|
-
];
|
package/src/core/file-utils.js
DELETED
|
@@ -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
|
-
}
|
package/src/core/helpers.js
DELETED
|
@@ -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
|