progga 1.0.2 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -71
- package/core/PresetSelector.js +23 -0
- package/core/ProjectTypeDetector.js +34 -0
- package/core/Spinner.js +32 -0
- package/index.js +108 -109
- package/package.json +7 -2
- package/profiles/FlutterProfile.js +65 -0
- package/profiles/GenericProfile.js +52 -0
- package/profiles/ProfileRegistry.js +24 -0
- package/profiles/ProjectProfile.js +31 -0
package/README.md
CHANGED
|
@@ -1,110 +1,124 @@
|
|
|
1
1
|
# progga (প্রজ্ঞা)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Progga** is a CLI tool that generates a single Markdown file representing the essential context of a software project.
|
|
4
|
+
The output is optimized for uploading to AI assistants (ChatGPT, Claude, Gemini) so they can understand a project quickly and accurately.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
---
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
+
## Getting Started
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
### Run with npx (recommended)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
```bash
|
|
13
|
+
npx progga@latest
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This generates a file named:
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
```
|
|
19
|
+
PROJECT_DOCUMENTATION.md
|
|
20
|
+
```
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
- 📄 All file contents with syntax highlighting
|
|
20
|
-
- 🚫 Automatically ignores dependencies and build artifacts
|
|
21
|
-
- ⚡ One command, one file, complete context
|
|
22
|
-
- 🎯 Optimized for AI consumption
|
|
22
|
+
in the current directory.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
### Run on a specific project
|
|
25
25
|
|
|
26
|
-
### Using npx (Recommended - No Installation!)
|
|
27
26
|
```bash
|
|
28
|
-
|
|
27
|
+
progga /path/to/project
|
|
29
28
|
```
|
|
30
29
|
|
|
31
|
-
###
|
|
30
|
+
### Custom output file
|
|
31
|
+
|
|
32
32
|
```bash
|
|
33
|
-
|
|
33
|
+
progga . my-ai-context.md
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
---
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
## How Progga Works (Short Example)
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
Given a project like:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
my-app/
|
|
44
|
+
├── src/
|
|
45
|
+
│ └── index.js
|
|
46
|
+
├── package.json
|
|
47
|
+
├── node_modules/
|
|
48
|
+
└── build/
|
|
43
49
|
```
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
Progga generates a single Markdown file containing:
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```
|
|
53
|
+
* A clean folder tree (excluding `node_modules`, `build`, etc.)
|
|
54
|
+
* The contents of relevant source files
|
|
55
|
+
* Proper code blocks with language hints
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
npx progga . my-ai-context.md
|
|
55
|
-
```
|
|
57
|
+
Example output structure:
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
npx progga ./my-app ./docs/ai-context.md
|
|
60
|
-
```
|
|
59
|
+
````markdown
|
|
60
|
+
# Project Documentation: my-app
|
|
61
61
|
|
|
62
|
-
##
|
|
62
|
+
## Folder Structure
|
|
63
|
+
my-app/
|
|
64
|
+
├── src/
|
|
65
|
+
│ └── index.js
|
|
66
|
+
├── package.json
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
## File Contents
|
|
69
|
+
### src/index.js
|
|
70
|
+
```js
|
|
71
|
+
// file content here
|
|
72
|
+
````
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
- "Review my code architecture"
|
|
70
|
-
- "Find potential bugs"
|
|
71
|
-
- "Suggest improvements"
|
|
72
|
-
- "Explain how this project works"
|
|
73
|
-
- "Help me add a new feature"
|
|
74
|
+
You can upload this file directly to an AI and ask questions about the project.
|
|
74
75
|
|
|
75
|
-
##
|
|
76
|
+
## Project Presets
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
- `node_modules/`, `.git/`
|
|
79
|
-
- Build directories (`dist/`, `build/`, `.next/`)
|
|
80
|
-
- Virtual environments (`venv/`, `env/`)
|
|
81
|
-
- Cache directories
|
|
82
|
-
- Lock files
|
|
83
|
-
- Binary files (images, videos, fonts)
|
|
78
|
+
Progga supports project-type presets that control what files are included.
|
|
84
79
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
Currently supported:
|
|
81
|
+
- `generic` (default)
|
|
82
|
+
- `flutter` (Android, iOS, Web, Windows, macOS, Linux)
|
|
88
83
|
|
|
89
|
-
|
|
90
|
-
[Visual tree of all files and folders]
|
|
84
|
+
If no preset is provided, Progga attempts to detect the project type and asks which preset to use.
|
|
91
85
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
```bash
|
|
87
|
+
progga --preset flutter
|
|
88
|
+
````
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Contributing
|
|
93
|
+
|
|
94
|
+
Contributions are welcome.
|
|
95
|
+
|
|
96
|
+
Good areas to contribute:
|
|
97
|
+
|
|
98
|
+
* New project presets (Node.js, Python, Go, etc.)
|
|
99
|
+
* Improving Flutter include-only rules
|
|
100
|
+
* Performance improvements
|
|
101
|
+
* Better project auto-detection
|
|
102
|
+
* Documentation and examples
|
|
103
|
+
|
|
104
|
+
### How to contribute
|
|
105
|
+
|
|
106
|
+
1. Fork the repository
|
|
107
|
+
2. Create a feature branch
|
|
108
|
+
3. Make focused changes
|
|
109
|
+
4. Open a pull request with a clear description
|
|
95
110
|
|
|
96
|
-
|
|
111
|
+
Opening an issue to discuss ideas is also encouraged.
|
|
97
112
|
|
|
98
|
-
|
|
113
|
+
---
|
|
99
114
|
|
|
100
|
-
##
|
|
115
|
+
## Requirements
|
|
101
116
|
|
|
102
|
-
|
|
117
|
+
* Node.js 12 or newer (Node 18+ recommended)
|
|
103
118
|
|
|
104
|
-
|
|
119
|
+
---
|
|
105
120
|
|
|
106
|
-
|
|
121
|
+
## License
|
|
107
122
|
|
|
108
|
-
|
|
123
|
+
MIT License
|
|
109
124
|
|
|
110
|
-
**Progga** (প্রজ্ঞা) is a Bengali word meaning "wisdom" or "insight" - representing the wisdom you share with AI assistants about your codebase.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const select = require('@inquirer/select').default;
|
|
2
|
+
|
|
3
|
+
class PresetSelector {
|
|
4
|
+
static async choose(detectedType) {
|
|
5
|
+
const preset = await select({
|
|
6
|
+
message: 'Select how you want to proceed:',
|
|
7
|
+
choices: [
|
|
8
|
+
{
|
|
9
|
+
name: `Use ${detectedType} preset (recommended)`,
|
|
10
|
+
value: detectedType
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Use generic preset',
|
|
14
|
+
value: 'generic'
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return preset;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = PresetSelector;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class ProjectTypeDetector {
|
|
5
|
+
constructor(projectRoot) {
|
|
6
|
+
this.projectRoot = projectRoot;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
detect() {
|
|
10
|
+
const signals = [];
|
|
11
|
+
|
|
12
|
+
if (this.isFlutterProject()) {
|
|
13
|
+
signals.push({
|
|
14
|
+
type: 'flutter',
|
|
15
|
+
confidence: 'high',
|
|
16
|
+
reason: 'pubspec.yaml found'
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// future:
|
|
21
|
+
// if (this.isNodeProject()) ...
|
|
22
|
+
// if (this.isPythonProject()) ...
|
|
23
|
+
|
|
24
|
+
return signals;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
isFlutterProject() {
|
|
28
|
+
return fs.existsSync(
|
|
29
|
+
path.join(this.projectRoot, 'pubspec.yaml')
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = ProjectTypeDetector;
|
package/core/Spinner.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const ora = require('ora').default;
|
|
2
|
+
|
|
3
|
+
class Spinner {
|
|
4
|
+
constructor(text) {
|
|
5
|
+
this.enabled = process.stdout.isTTY;
|
|
6
|
+
this.spinner = this.enabled ? ora(text).start() : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
succeed(text) {
|
|
10
|
+
if (this.spinner) {
|
|
11
|
+
this.spinner.succeed(text);
|
|
12
|
+
} else {
|
|
13
|
+
console.log(`✓ ${text}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fail(text) {
|
|
18
|
+
if (this.spinner) {
|
|
19
|
+
this.spinner.fail(text);
|
|
20
|
+
} else {
|
|
21
|
+
console.error(`✗ ${text}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
update(text) {
|
|
26
|
+
if (this.spinner) {
|
|
27
|
+
this.spinner.text = text;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = Spinner;
|
package/index.js
CHANGED
|
@@ -7,65 +7,29 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
'.git',
|
|
15
|
-
'__pycache__',
|
|
16
|
-
'.vscode',
|
|
17
|
-
'dist',
|
|
18
|
-
'build',
|
|
19
|
-
'.next',
|
|
20
|
-
'venv',
|
|
21
|
-
'env',
|
|
22
|
-
'.env',
|
|
23
|
-
'coverage',
|
|
24
|
-
'.pytest_cache',
|
|
25
|
-
'.DS_Store',
|
|
26
|
-
'package-lock.json',
|
|
27
|
-
'yarn.lock',
|
|
28
|
-
'pnpm-lock.yaml',
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
// File extensions to exclude
|
|
32
|
-
const IGNORE_EXTENSIONS = new Set([
|
|
33
|
-
'.pyc',
|
|
34
|
-
'.pyo',
|
|
35
|
-
'.so',
|
|
36
|
-
'.dylib',
|
|
37
|
-
'.exe',
|
|
38
|
-
'.dll',
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
// Binary file extensions to skip
|
|
42
|
-
const BINARY_EXTENSIONS = new Set([
|
|
43
|
-
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg',
|
|
44
|
-
'.pdf', '.zip', '.tar', '.gz', '.rar',
|
|
45
|
-
'.mp4', '.mp3', '.wav',
|
|
46
|
-
'.woff', '.woff2', '.ttf', '.eot',
|
|
47
|
-
]);
|
|
10
|
+
const ProfileRegistry = require('./profiles/ProfileRegistry');
|
|
11
|
+
const ProjectTypeDetector = require('./core/ProjectTypeDetector');
|
|
12
|
+
const PresetSelector = require('./core/PresetSelector');
|
|
13
|
+
const Spinner = require('./core/Spinner');
|
|
48
14
|
|
|
49
15
|
/**
|
|
50
16
|
* Check if path should be ignored
|
|
51
17
|
*/
|
|
52
|
-
function shouldIgnore(filePath, basePath) {
|
|
18
|
+
function shouldIgnore(filePath, basePath, profile) {
|
|
53
19
|
const relativePath = path.relative(basePath, filePath);
|
|
54
20
|
const parts = relativePath.split(path.sep);
|
|
55
|
-
|
|
56
|
-
// Check each part of the path
|
|
21
|
+
|
|
57
22
|
for (const part of parts) {
|
|
58
|
-
if (
|
|
23
|
+
if (profile.ignorePaths().includes(part)) {
|
|
59
24
|
return true;
|
|
60
25
|
}
|
|
61
26
|
}
|
|
62
|
-
|
|
63
|
-
// Check file extension
|
|
27
|
+
|
|
64
28
|
const ext = path.extname(filePath);
|
|
65
|
-
if (
|
|
29
|
+
if (profile.ignoreExtensions().includes(ext)) {
|
|
66
30
|
return true;
|
|
67
31
|
}
|
|
68
|
-
|
|
32
|
+
|
|
69
33
|
return false;
|
|
70
34
|
}
|
|
71
35
|
|
|
@@ -88,43 +52,43 @@ function isDirectoryEmpty(directory, basePath) {
|
|
|
88
52
|
/**
|
|
89
53
|
* Generate tree structure recursively
|
|
90
54
|
*/
|
|
91
|
-
function generateTree(directory, prefix = '', isLast = true, basePath = null) {
|
|
55
|
+
function generateTree(directory, prefix = '', isLast = true, basePath = null, profile) {
|
|
92
56
|
if (basePath === null) {
|
|
93
57
|
basePath = directory;
|
|
94
58
|
}
|
|
95
|
-
|
|
59
|
+
|
|
96
60
|
const treeLines = [];
|
|
97
|
-
|
|
61
|
+
|
|
98
62
|
try {
|
|
99
63
|
let items = fs.readdirSync(directory).map(name => {
|
|
100
64
|
const fullPath = path.join(directory, name);
|
|
101
65
|
const stats = fs.statSync(fullPath);
|
|
102
66
|
return { name, fullPath, isDir: stats.isDirectory() };
|
|
103
67
|
});
|
|
104
|
-
|
|
68
|
+
|
|
105
69
|
// Filter ignored items
|
|
106
|
-
items = items.filter(item => !shouldIgnore(item.fullPath, basePath));
|
|
107
|
-
|
|
70
|
+
items = items.filter(item => !shouldIgnore(item.fullPath, basePath, profile));
|
|
71
|
+
|
|
108
72
|
// Sort: directories first, then alphabetically
|
|
109
73
|
items.sort((a, b) => {
|
|
110
74
|
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
111
75
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
112
76
|
});
|
|
113
|
-
|
|
77
|
+
|
|
114
78
|
for (let i = 0; i < items.length; i++) {
|
|
115
79
|
const item = items[i];
|
|
116
80
|
const isLastItem = i === items.length - 1;
|
|
117
|
-
|
|
81
|
+
|
|
118
82
|
// Tree characters
|
|
119
83
|
const connector = isLastItem ? '└── ' : '├── ';
|
|
120
84
|
const extension = isLastItem ? ' ' : '│ ';
|
|
121
|
-
|
|
85
|
+
|
|
122
86
|
if (item.isDir) {
|
|
123
87
|
if (isDirectoryEmpty(item.fullPath, basePath)) {
|
|
124
88
|
treeLines.push(`${prefix}${connector}${item.name}/ (empty)`);
|
|
125
89
|
} else {
|
|
126
90
|
treeLines.push(`${prefix}${connector}${item.name}/`);
|
|
127
|
-
const subtree = generateTree(item.fullPath, prefix + extension, isLastItem, basePath);
|
|
91
|
+
const subtree = generateTree(item.fullPath, prefix + extension, isLastItem, basePath, profile);
|
|
128
92
|
treeLines.push(...subtree);
|
|
129
93
|
}
|
|
130
94
|
} else {
|
|
@@ -140,35 +104,32 @@ function generateTree(directory, prefix = '', isLast = true, basePath = null) {
|
|
|
140
104
|
} catch (err) {
|
|
141
105
|
// Permission error or other issues
|
|
142
106
|
}
|
|
143
|
-
|
|
107
|
+
|
|
144
108
|
return treeLines;
|
|
145
109
|
}
|
|
146
110
|
|
|
147
111
|
/**
|
|
148
112
|
* Check if file is binary
|
|
149
113
|
*/
|
|
150
|
-
function isBinaryFile(filePath) {
|
|
114
|
+
function isBinaryFile(filePath, profile) {
|
|
151
115
|
const ext = path.extname(filePath);
|
|
152
|
-
if (
|
|
116
|
+
if (profile.binaryExtensions().includes(ext)) {
|
|
153
117
|
return true;
|
|
154
118
|
}
|
|
155
|
-
|
|
119
|
+
|
|
156
120
|
try {
|
|
157
121
|
const buffer = Buffer.alloc(1024);
|
|
158
122
|
const fd = fs.openSync(filePath, 'r');
|
|
159
123
|
const bytesRead = fs.readSync(fd, buffer, 0, 1024, 0);
|
|
160
124
|
fs.closeSync(fd);
|
|
161
|
-
|
|
162
|
-
// Check for null bytes
|
|
125
|
+
|
|
163
126
|
for (let i = 0; i < bytesRead; i++) {
|
|
164
|
-
if (buffer[i] === 0)
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
127
|
+
if (buffer[i] === 0) return true;
|
|
167
128
|
}
|
|
168
|
-
} catch
|
|
129
|
+
} catch {
|
|
169
130
|
return true;
|
|
170
131
|
}
|
|
171
|
-
|
|
132
|
+
|
|
172
133
|
return false;
|
|
173
134
|
}
|
|
174
135
|
|
|
@@ -181,7 +142,7 @@ function readFileContent(filePath) {
|
|
|
181
142
|
if (stats.size === 0) {
|
|
182
143
|
return '(empty file)';
|
|
183
144
|
}
|
|
184
|
-
|
|
145
|
+
|
|
185
146
|
return fs.readFileSync(filePath, 'utf-8');
|
|
186
147
|
} catch (err) {
|
|
187
148
|
if (err.message.includes('invalid')) {
|
|
@@ -223,7 +184,7 @@ function getLanguageFromExtension(filePath) {
|
|
|
223
184
|
'.rb': 'ruby',
|
|
224
185
|
'.php': 'php',
|
|
225
186
|
};
|
|
226
|
-
|
|
187
|
+
|
|
227
188
|
const ext = path.extname(filePath);
|
|
228
189
|
return extMap[ext] || '';
|
|
229
190
|
}
|
|
@@ -231,50 +192,46 @@ function getLanguageFromExtension(filePath) {
|
|
|
231
192
|
/**
|
|
232
193
|
* Collect all files recursively
|
|
233
194
|
*/
|
|
234
|
-
function collectFiles(directory, basePath) {
|
|
195
|
+
function collectFiles(directory, basePath, profile) {
|
|
235
196
|
const files = [];
|
|
236
|
-
|
|
197
|
+
|
|
237
198
|
try {
|
|
238
199
|
const items = fs.readdirSync(directory);
|
|
239
|
-
|
|
200
|
+
|
|
240
201
|
for (const item of items.sort()) {
|
|
241
202
|
const fullPath = path.join(directory, item);
|
|
242
|
-
|
|
243
|
-
if (shouldIgnore(fullPath, basePath))
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
203
|
+
|
|
204
|
+
if (shouldIgnore(fullPath, basePath, profile)) continue;
|
|
205
|
+
|
|
247
206
|
const stats = fs.statSync(fullPath);
|
|
248
|
-
|
|
207
|
+
|
|
249
208
|
if (stats.isFile()) {
|
|
250
|
-
if (!isBinaryFile(fullPath)) {
|
|
209
|
+
if (!isBinaryFile(fullPath, profile)) {
|
|
251
210
|
files.push(fullPath);
|
|
252
211
|
}
|
|
253
212
|
} else if (stats.isDirectory()) {
|
|
254
|
-
files.push(...collectFiles(fullPath, basePath));
|
|
213
|
+
files.push(...collectFiles(fullPath, basePath, profile));
|
|
255
214
|
}
|
|
256
215
|
}
|
|
257
|
-
} catch
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
216
|
+
} catch { }
|
|
217
|
+
|
|
261
218
|
return files;
|
|
262
219
|
}
|
|
263
220
|
|
|
264
221
|
/**
|
|
265
222
|
* Generate complete documentation markdown file
|
|
266
223
|
*/
|
|
267
|
-
function generateDocumentation(projectPath, outputFile) {
|
|
224
|
+
function generateDocumentation(projectPath, outputFile, profile) {
|
|
268
225
|
const absProjectPath = path.resolve(projectPath);
|
|
269
|
-
|
|
226
|
+
|
|
270
227
|
if (!fs.existsSync(absProjectPath)) {
|
|
271
228
|
console.error(`Error: Path '${absProjectPath}' does not exist`);
|
|
272
229
|
process.exit(1);
|
|
273
230
|
}
|
|
274
|
-
|
|
231
|
+
|
|
275
232
|
console.log(`Generating documentation for: ${absProjectPath}`);
|
|
276
233
|
console.log(`Output file: ${outputFile}`);
|
|
277
|
-
|
|
234
|
+
|
|
278
235
|
// Delete existing output file if it exists
|
|
279
236
|
if (fs.existsSync(outputFile)) {
|
|
280
237
|
try {
|
|
@@ -284,43 +241,48 @@ function generateDocumentation(projectPath, outputFile) {
|
|
|
284
241
|
console.warn(`Warning: Could not delete existing file: ${err.message}`);
|
|
285
242
|
}
|
|
286
243
|
}
|
|
287
|
-
|
|
244
|
+
|
|
288
245
|
const projectName = path.basename(absProjectPath);
|
|
289
246
|
let output = '';
|
|
290
|
-
|
|
247
|
+
|
|
291
248
|
// Write header
|
|
292
249
|
output += `# Project Documentation: ${projectName}\n\n`;
|
|
293
250
|
output += `**Generated from:** \`${absProjectPath}\`\n\n`;
|
|
294
251
|
output += '---\n\n';
|
|
295
|
-
|
|
252
|
+
|
|
296
253
|
// Write folder structure
|
|
297
254
|
output += '## 📁 Folder Structure\n\n';
|
|
298
255
|
output += '```\n';
|
|
299
256
|
output += `${projectName}/\n`;
|
|
300
|
-
|
|
301
|
-
const
|
|
257
|
+
|
|
258
|
+
const treeSpinner = new Spinner('Generating folder structure');
|
|
259
|
+
const treeLines = generateTree(absProjectPath, '', true, absProjectPath, profile);
|
|
260
|
+
treeSpinner.succeed('Folder structure generated');
|
|
261
|
+
|
|
302
262
|
for (const line of treeLines) {
|
|
303
263
|
output += `${line}\n`;
|
|
304
264
|
}
|
|
305
|
-
|
|
265
|
+
|
|
306
266
|
output += '```\n\n';
|
|
307
267
|
output += '---\n\n';
|
|
308
|
-
|
|
268
|
+
|
|
309
269
|
// Write file contents
|
|
310
270
|
output += '## 📄 File Contents\n\n';
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
271
|
+
|
|
272
|
+
const fileSpinner = new Spinner('Collecting files');
|
|
273
|
+
const files = collectFiles(absProjectPath, absProjectPath, profile);
|
|
274
|
+
fileSpinner.succeed(`Collected ${files.length} files`);
|
|
275
|
+
|
|
314
276
|
for (let i = 0; i < files.length; i++) {
|
|
315
277
|
const filePath = files[i];
|
|
316
278
|
const relPath = path.relative(absProjectPath, filePath);
|
|
317
279
|
console.log(`Processing (${i + 1}/${files.length}): ${relPath}`);
|
|
318
|
-
|
|
280
|
+
|
|
319
281
|
output += `### \`${relPath}\`\n\n`;
|
|
320
|
-
|
|
282
|
+
|
|
321
283
|
const content = readFileContent(filePath);
|
|
322
284
|
const language = getLanguageFromExtension(filePath);
|
|
323
|
-
|
|
285
|
+
|
|
324
286
|
output += `\`\`\`${language}\n`;
|
|
325
287
|
output += content;
|
|
326
288
|
if (!content.endsWith('\n')) {
|
|
@@ -329,22 +291,59 @@ function generateDocumentation(projectPath, outputFile) {
|
|
|
329
291
|
output += '```\n\n';
|
|
330
292
|
output += '---\n\n';
|
|
331
293
|
}
|
|
332
|
-
|
|
294
|
+
|
|
333
295
|
// Write to file
|
|
334
296
|
fs.writeFileSync(outputFile, output, 'utf-8');
|
|
335
|
-
|
|
297
|
+
|
|
336
298
|
console.log(`\n✅ Documentation generated successfully: ${outputFile}`);
|
|
337
299
|
console.log(`📊 Total files processed: ${files.length}`);
|
|
338
300
|
}
|
|
339
301
|
|
|
340
302
|
// Main execution
|
|
341
|
-
function main() {
|
|
303
|
+
async function main() {
|
|
342
304
|
const args = process.argv.slice(2);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
305
|
+
|
|
306
|
+
let projectPath = '.';
|
|
307
|
+
let outputFile = 'PROJECT_DOCUMENTATION.md';
|
|
308
|
+
let projectType = null;
|
|
309
|
+
|
|
310
|
+
for (let i = 0; i < args.length; i++) {
|
|
311
|
+
const arg = args[i];
|
|
312
|
+
|
|
313
|
+
if (arg === '--project-type' || arg === '--preset') {
|
|
314
|
+
projectType = args[i + 1];
|
|
315
|
+
i++;
|
|
316
|
+
} else if (!projectPath) {
|
|
317
|
+
projectPath = arg;
|
|
318
|
+
} else if (!outputFile) {
|
|
319
|
+
outputFile = arg;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let profile = ProfileRegistry.getByName(projectType, projectPath);
|
|
324
|
+
|
|
325
|
+
if (!profile) {
|
|
326
|
+
const detector = new ProjectTypeDetector(projectPath);
|
|
327
|
+
const detections = detector.detect();
|
|
328
|
+
|
|
329
|
+
const flutterSignal = detections.find(d => d.type === 'flutter');
|
|
330
|
+
|
|
331
|
+
if (flutterSignal && process.stdin.isTTY) {
|
|
332
|
+
console.log('');
|
|
333
|
+
console.log(`Detected project type: ${flutterSignal.type}`);
|
|
334
|
+
console.log('');
|
|
335
|
+
|
|
336
|
+
const selectedPreset = await PresetSelector.choose(flutterSignal.type);
|
|
337
|
+
profile = ProfileRegistry.getByName(selectedPreset, projectPath);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!profile) {
|
|
342
|
+
profile = ProfileRegistry.fallback(projectPath);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log(`Using profile: ${profile.name}`);
|
|
346
|
+
generateDocumentation(projectPath, outputFile, profile);
|
|
348
347
|
}
|
|
349
348
|
|
|
350
349
|
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progga",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Generate comprehensive project documentation for AI assistants - Share your entire codebase context in one markdown file",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -37,5 +37,10 @@
|
|
|
37
37
|
"bugs": {
|
|
38
38
|
"url": "https://github.com/Yousuf-Basir/progga/issues"
|
|
39
39
|
},
|
|
40
|
-
"homepage": "https://github.com/Yousuf-Basir/progga#readme"
|
|
40
|
+
"homepage": "https://github.com/Yousuf-Basir/progga#readme",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@inquirer/select": "^5.0.4",
|
|
43
|
+
"inquirer": "^13.2.0",
|
|
44
|
+
"ora": "^9.0.0"
|
|
45
|
+
}
|
|
41
46
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const ProjectProfile = require('./ProjectProfile');
|
|
2
|
+
|
|
3
|
+
class FlutterProfile extends ProjectProfile {
|
|
4
|
+
get name() {
|
|
5
|
+
return 'flutter';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
ignorePaths() {
|
|
9
|
+
return [
|
|
10
|
+
// Dart / Flutter
|
|
11
|
+
'.dart_tool',
|
|
12
|
+
'build',
|
|
13
|
+
'.flutter-plugins',
|
|
14
|
+
'.flutter-plugins-dependencies',
|
|
15
|
+
|
|
16
|
+
// Android
|
|
17
|
+
'android/.gradle',
|
|
18
|
+
'android/build',
|
|
19
|
+
'android/app/build',
|
|
20
|
+
|
|
21
|
+
// iOS / macOS
|
|
22
|
+
'ios/Flutter/ephemeral',
|
|
23
|
+
'ios/Runner.xcodeproj',
|
|
24
|
+
'ios/Runner.xcworkspace',
|
|
25
|
+
'macos/Flutter/ephemeral',
|
|
26
|
+
'macos/Runner.xcodeproj',
|
|
27
|
+
|
|
28
|
+
// Web
|
|
29
|
+
'build/web',
|
|
30
|
+
|
|
31
|
+
// Windows
|
|
32
|
+
'windows/build',
|
|
33
|
+
|
|
34
|
+
// Linux
|
|
35
|
+
'linux/build',
|
|
36
|
+
|
|
37
|
+
// Common IDE noise
|
|
38
|
+
'.git',
|
|
39
|
+
'.idea',
|
|
40
|
+
'.vscode',
|
|
41
|
+
'.DS_Store',
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ignoreExtensions() {
|
|
46
|
+
return [
|
|
47
|
+
// Assets & binaries
|
|
48
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico',
|
|
49
|
+
'.mp4', '.mp3', '.wav',
|
|
50
|
+
'.ttf', '.otf', '.woff', '.woff2',
|
|
51
|
+
|
|
52
|
+
// Archives
|
|
53
|
+
'.zip', '.rar', '.tar', '.gz',
|
|
54
|
+
|
|
55
|
+
// Compiled outputs
|
|
56
|
+
'.exe', '.dll', '.so', '.dylib',
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
binaryExtensions() {
|
|
61
|
+
return this.ignoreExtensions();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = FlutterProfile;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const ProjectProfile = require('./ProjectProfile');
|
|
2
|
+
|
|
3
|
+
class GenericProfile extends ProjectProfile {
|
|
4
|
+
get name() {
|
|
5
|
+
return 'generic';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
ignorePaths() {
|
|
9
|
+
return [
|
|
10
|
+
'node_modules',
|
|
11
|
+
'.git',
|
|
12
|
+
'__pycache__',
|
|
13
|
+
'.vscode',
|
|
14
|
+
'dist',
|
|
15
|
+
'build',
|
|
16
|
+
'.next',
|
|
17
|
+
'venv',
|
|
18
|
+
'env',
|
|
19
|
+
'.env',
|
|
20
|
+
'coverage',
|
|
21
|
+
'.pytest_cache',
|
|
22
|
+
'.DS_Store',
|
|
23
|
+
'package-lock.json',
|
|
24
|
+
'yarn.lock',
|
|
25
|
+
'pnpm-lock.yaml',
|
|
26
|
+
'bun.lock',
|
|
27
|
+
'.turbo'
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ignoreExtensions() {
|
|
32
|
+
return [
|
|
33
|
+
'.pyc',
|
|
34
|
+
'.pyo',
|
|
35
|
+
'.so',
|
|
36
|
+
'.dylib',
|
|
37
|
+
'.exe',
|
|
38
|
+
'.dll',
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
binaryExtensions() {
|
|
43
|
+
return [
|
|
44
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg',
|
|
45
|
+
'.pdf', '.zip', '.tar', '.gz', '.rar',
|
|
46
|
+
'.mp4', '.mp3', '.wav',
|
|
47
|
+
'.woff', '.woff2', '.ttf', '.eot',
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = GenericProfile;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const GenericProfile = require('./GenericProfile');
|
|
2
|
+
const FlutterProfile = require('./FlutterProfile');
|
|
3
|
+
|
|
4
|
+
class ProfileRegistry {
|
|
5
|
+
static getByName(name, projectRoot) {
|
|
6
|
+
if (!name) return null;
|
|
7
|
+
|
|
8
|
+
if (name === 'flutter') {
|
|
9
|
+
return new FlutterProfile(projectRoot);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (name === 'generic') {
|
|
13
|
+
return new GenericProfile(projectRoot);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static fallback(projectRoot) {
|
|
20
|
+
return new GenericProfile(projectRoot);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = ProfileRegistry;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class ProjectProfile {
|
|
2
|
+
constructor(projectRoot) {
|
|
3
|
+
this.projectRoot = projectRoot;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
get name() {
|
|
7
|
+
return 'base';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Paths to fully ignore */
|
|
11
|
+
ignorePaths() {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Extensions to ignore */
|
|
16
|
+
ignoreExtensions() {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Binary extensions */
|
|
21
|
+
binaryExtensions() {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Should this profile auto-detect the project */
|
|
26
|
+
detect() {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = ProjectProfile;
|