progga 1.0.4 → 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 CHANGED
@@ -1,110 +1,124 @@
1
1
  # progga (প্রজ্ঞা)
2
2
 
3
- > *Progga* means "wisdom" or "insight" in Bengali
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
- Generate comprehensive project documentation in a single markdown file - perfect for sharing your entire codebase context with AI assistants like ChatGPT, Claude, or Gemini.
6
+ ---
6
7
 
7
- ## 🎯 Purpose
8
+ ## Getting Started
8
9
 
9
- Upload one file, understand the entire project. **progga** creates a complete project snapshot that AI assistants can instantly comprehend, making it easy to:
10
+ ### Run with npx (recommended)
10
11
 
11
- - 💬 Get AI help with your entire codebase
12
- - 📤 Share project context without multiple file uploads
13
- - 🤖 Enable ChatGPT/Claude/Gemini to understand your project structure
14
- - 📚 Create comprehensive documentation snapshots
15
-
16
- ## ✨ Features
17
-
18
- - 📁 Visual folder tree structure
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
23
-
24
- ## 🚀 Installation
25
-
26
- ### Using npx (Recommended - No Installation!)
27
12
  ```bash
28
13
  npx progga@latest
29
14
  ```
30
15
 
31
- ### Global Installation
32
- ```bash
33
- npm install -g progga
16
+ This generates a file named:
17
+
18
+ ```
19
+ PROJECT_DOCUMENTATION.md
34
20
  ```
35
21
 
36
- ## 📖 Usage
22
+ in the current directory.
37
23
 
38
- ### Basic Usage
24
+ ### Run on a specific project
39
25
 
40
- Generate documentation for current directory:
41
26
  ```bash
42
- npx progga@latest
27
+ progga /path/to/project
43
28
  ```
44
29
 
45
- This creates `PROJECT_DOCUMENTATION.md` in your current directory.
30
+ ### Custom output file
46
31
 
47
- ### Specify Project Path
48
32
  ```bash
49
- npx progga@latest /path/to/your/project
33
+ progga . my-ai-context.md
50
34
  ```
51
35
 
52
- ### Custom Output File
53
- ```bash
54
- npx progga@latest . my-ai-context.md
55
- ```
36
+ ---
56
37
 
57
- ### Full Example
58
- ```bash
59
- npx progga@latest ./my-app ./docs/ai-context.md
38
+ ## How Progga Works (Short Example)
39
+
40
+ Given a project like:
41
+
42
+ ```
43
+ my-app/
44
+ ├── src/
45
+ │ └── index.js
46
+ ├── package.json
47
+ ├── node_modules/
48
+ └── build/
60
49
  ```
61
50
 
62
- ## 💡 How to Use with AI Assistants
51
+ Progga generates a single Markdown file containing:
63
52
 
64
- 1. Run `npx progga` in your project directory
65
- 2. Upload the generated `PROJECT_DOCUMENTATION.md` to ChatGPT, Claude, or Gemini
66
- 3. Ask the AI anything about your project!
53
+ * A clean folder tree (excluding `node_modules`, `build`, etc.)
54
+ * The contents of relevant source files
55
+ * Proper code blocks with language hints
67
56
 
68
- Example prompts after upload:
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"
57
+ Example output structure:
74
58
 
75
- ## 🚫 What Gets Ignored
59
+ ````markdown
60
+ # Project Documentation: my-app
76
61
 
77
- Automatically excludes:
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)
62
+ ## Folder Structure
63
+ my-app/
64
+ ├── src/
65
+ │ └── index.js
66
+ ├── package.json
84
67
 
85
- ## 📊 Output Format
86
- ```markdown
87
- # Project Documentation: your-project
68
+ ## File Contents
69
+ ### src/index.js
70
+ ```js
71
+ // file content here
72
+ ````
88
73
 
89
- ## 📁 Folder Structure
90
- [Visual tree of all files and folders]
74
+ You can upload this file directly to an AI and ask questions about the project.
91
75
 
92
- ## 📄 File Contents
93
- [Complete contents of each file with syntax highlighting]
94
- ```
76
+ ## Project Presets
77
+
78
+ Progga supports project-type presets that control what files are included.
79
+
80
+ Currently supported:
81
+ - `generic` (default)
82
+ - `flutter` (Android, iOS, Web, Windows, macOS, Linux)
83
+
84
+ If no preset is provided, Progga attempts to detect the project type and asks which preset to use.
85
+
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
- ## 🌍 Requirements
111
+ Opening an issue to discuss ideas is also encouraged.
97
112
 
98
- - Node.js >= 12.0.0
113
+ ---
99
114
 
100
- ## 📝 License
115
+ ## Requirements
101
116
 
102
- MIT
117
+ * Node.js 12 or newer (Node 18+ recommended)
103
118
 
104
- ## 🤝 Contributing
119
+ ---
105
120
 
106
- Contributions are welcome! Please feel free to submit a Pull Request.
121
+ ## License
107
122
 
108
- ## 💖 Name Origin
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;
@@ -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
@@ -8,7 +8,9 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const ProfileRegistry = require('./profiles/ProfileRegistry');
11
-
11
+ const ProjectTypeDetector = require('./core/ProjectTypeDetector');
12
+ const PresetSelector = require('./core/PresetSelector');
13
+ const Spinner = require('./core/Spinner');
12
14
 
13
15
  /**
14
16
  * Check if path should be ignored
@@ -50,43 +52,43 @@ function isDirectoryEmpty(directory, basePath) {
50
52
  /**
51
53
  * Generate tree structure recursively
52
54
  */
53
- function generateTree( directory, prefix = '', isLast = true, basePath = null, profile ) {
55
+ function generateTree(directory, prefix = '', isLast = true, basePath = null, profile) {
54
56
  if (basePath === null) {
55
57
  basePath = directory;
56
58
  }
57
-
59
+
58
60
  const treeLines = [];
59
-
61
+
60
62
  try {
61
63
  let items = fs.readdirSync(directory).map(name => {
62
64
  const fullPath = path.join(directory, name);
63
65
  const stats = fs.statSync(fullPath);
64
66
  return { name, fullPath, isDir: stats.isDirectory() };
65
67
  });
66
-
68
+
67
69
  // Filter ignored items
68
70
  items = items.filter(item => !shouldIgnore(item.fullPath, basePath, profile));
69
-
71
+
70
72
  // Sort: directories first, then alphabetically
71
73
  items.sort((a, b) => {
72
74
  if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
73
75
  return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
74
76
  });
75
-
77
+
76
78
  for (let i = 0; i < items.length; i++) {
77
79
  const item = items[i];
78
80
  const isLastItem = i === items.length - 1;
79
-
81
+
80
82
  // Tree characters
81
83
  const connector = isLastItem ? '└── ' : '├── ';
82
84
  const extension = isLastItem ? ' ' : '│ ';
83
-
85
+
84
86
  if (item.isDir) {
85
87
  if (isDirectoryEmpty(item.fullPath, basePath)) {
86
88
  treeLines.push(`${prefix}${connector}${item.name}/ (empty)`);
87
89
  } else {
88
90
  treeLines.push(`${prefix}${connector}${item.name}/`);
89
- const subtree = generateTree( item.fullPath, prefix + extension, isLastItem, basePath, profile );
91
+ const subtree = generateTree(item.fullPath, prefix + extension, isLastItem, basePath, profile);
90
92
  treeLines.push(...subtree);
91
93
  }
92
94
  } else {
@@ -102,7 +104,7 @@ function generateTree( directory, prefix = '', isLast = true, basePath = null, p
102
104
  } catch (err) {
103
105
  // Permission error or other issues
104
106
  }
105
-
107
+
106
108
  return treeLines;
107
109
  }
108
110
 
@@ -140,7 +142,7 @@ function readFileContent(filePath) {
140
142
  if (stats.size === 0) {
141
143
  return '(empty file)';
142
144
  }
143
-
145
+
144
146
  return fs.readFileSync(filePath, 'utf-8');
145
147
  } catch (err) {
146
148
  if (err.message.includes('invalid')) {
@@ -182,7 +184,7 @@ function getLanguageFromExtension(filePath) {
182
184
  '.rb': 'ruby',
183
185
  '.php': 'php',
184
186
  };
185
-
187
+
186
188
  const ext = path.extname(filePath);
187
189
  return extMap[ext] || '';
188
190
  }
@@ -211,7 +213,7 @@ function collectFiles(directory, basePath, profile) {
211
213
  files.push(...collectFiles(fullPath, basePath, profile));
212
214
  }
213
215
  }
214
- } catch {}
216
+ } catch { }
215
217
 
216
218
  return files;
217
219
  }
@@ -221,15 +223,15 @@ function collectFiles(directory, basePath, profile) {
221
223
  */
222
224
  function generateDocumentation(projectPath, outputFile, profile) {
223
225
  const absProjectPath = path.resolve(projectPath);
224
-
226
+
225
227
  if (!fs.existsSync(absProjectPath)) {
226
228
  console.error(`Error: Path '${absProjectPath}' does not exist`);
227
229
  process.exit(1);
228
230
  }
229
-
231
+
230
232
  console.log(`Generating documentation for: ${absProjectPath}`);
231
233
  console.log(`Output file: ${outputFile}`);
232
-
234
+
233
235
  // Delete existing output file if it exists
234
236
  if (fs.existsSync(outputFile)) {
235
237
  try {
@@ -239,43 +241,48 @@ function generateDocumentation(projectPath, outputFile, profile) {
239
241
  console.warn(`Warning: Could not delete existing file: ${err.message}`);
240
242
  }
241
243
  }
242
-
244
+
243
245
  const projectName = path.basename(absProjectPath);
244
246
  let output = '';
245
-
247
+
246
248
  // Write header
247
249
  output += `# Project Documentation: ${projectName}\n\n`;
248
250
  output += `**Generated from:** \`${absProjectPath}\`\n\n`;
249
251
  output += '---\n\n';
250
-
252
+
251
253
  // Write folder structure
252
254
  output += '## 📁 Folder Structure\n\n';
253
255
  output += '```\n';
254
256
  output += `${projectName}/\n`;
255
-
257
+
258
+ const treeSpinner = new Spinner('Generating folder structure');
256
259
  const treeLines = generateTree(absProjectPath, '', true, absProjectPath, profile);
260
+ treeSpinner.succeed('Folder structure generated');
261
+
257
262
  for (const line of treeLines) {
258
263
  output += `${line}\n`;
259
264
  }
260
-
265
+
261
266
  output += '```\n\n';
262
267
  output += '---\n\n';
263
-
268
+
264
269
  // Write file contents
265
270
  output += '## 📄 File Contents\n\n';
266
-
271
+
272
+ const fileSpinner = new Spinner('Collecting files');
267
273
  const files = collectFiles(absProjectPath, absProjectPath, profile);
268
-
274
+ fileSpinner.succeed(`Collected ${files.length} files`);
275
+
269
276
  for (let i = 0; i < files.length; i++) {
270
277
  const filePath = files[i];
271
278
  const relPath = path.relative(absProjectPath, filePath);
272
279
  console.log(`Processing (${i + 1}/${files.length}): ${relPath}`);
273
-
280
+
274
281
  output += `### \`${relPath}\`\n\n`;
275
-
282
+
276
283
  const content = readFileContent(filePath);
277
284
  const language = getLanguageFromExtension(filePath);
278
-
285
+
279
286
  output += `\`\`\`${language}\n`;
280
287
  output += content;
281
288
  if (!content.endsWith('\n')) {
@@ -284,26 +291,58 @@ function generateDocumentation(projectPath, outputFile, profile) {
284
291
  output += '```\n\n';
285
292
  output += '---\n\n';
286
293
  }
287
-
294
+
288
295
  // Write to file
289
296
  fs.writeFileSync(outputFile, output, 'utf-8');
290
-
297
+
291
298
  console.log(`\n✅ Documentation generated successfully: ${outputFile}`);
292
299
  console.log(`📊 Total files processed: ${files.length}`);
293
300
  }
294
301
 
295
302
  // Main execution
296
- function main() {
303
+ async function main() {
297
304
  const args = process.argv.slice(2);
298
305
 
299
- const projectPath = args[0] || '.';
300
- const outputFile = args[1] || 'PROJECT_DOCUMENTATION.md';
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');
301
330
 
302
- // future: parse --project-type
303
- const profile =
304
- ProfileRegistry.getByName(null, projectPath) ||
305
- ProfileRegistry.fallback(projectPath);
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
+ }
306
344
 
345
+ console.log(`Using profile: ${profile.name}`);
307
346
  generateDocumentation(projectPath, outputFile, profile);
308
347
  }
309
348
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progga",
3
- "version": "1.0.4",
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;
@@ -1,9 +1,14 @@
1
1
  const GenericProfile = require('./GenericProfile');
2
+ const FlutterProfile = require('./FlutterProfile');
2
3
 
3
4
  class ProfileRegistry {
4
5
  static getByName(name, projectRoot) {
5
6
  if (!name) return null;
6
7
 
8
+ if (name === 'flutter') {
9
+ return new FlutterProfile(projectRoot);
10
+ }
11
+
7
12
  if (name === 'generic') {
8
13
  return new GenericProfile(projectRoot);
9
14
  }