progga 1.0.4 → 1.0.6

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,8 +8,14 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const ProfileRegistry = require('./profiles/ProfileRegistry');
11
+ const ProjectTypeDetector = require('./core/ProjectTypeDetector');
12
+ const PresetSelector = require('./core/PresetSelector');
13
+ const Spinner = require('./core/Spinner');
11
14
 
12
15
 
16
+ const CLI_IGNORE_PATHS = new Set();
17
+ const CLI_IGNORE_EXTENSIONS = new Set();
18
+
13
19
  /**
14
20
  * Check if path should be ignored
15
21
  */
@@ -18,13 +24,19 @@ function shouldIgnore(filePath, basePath, profile) {
18
24
  const parts = relativePath.split(path.sep);
19
25
 
20
26
  for (const part of parts) {
21
- if (profile.ignorePaths().includes(part)) {
27
+ if (
28
+ profile.ignorePaths().includes(part) ||
29
+ CLI_IGNORE_PATHS.has(part)
30
+ ) {
22
31
  return true;
23
32
  }
24
33
  }
25
34
 
26
35
  const ext = path.extname(filePath);
27
- if (profile.ignoreExtensions().includes(ext)) {
36
+ if (
37
+ profile.ignoreExtensions().includes(ext) ||
38
+ CLI_IGNORE_EXTENSIONS.has(ext)
39
+ ) {
28
40
  return true;
29
41
  }
30
42
 
@@ -50,43 +62,43 @@ function isDirectoryEmpty(directory, basePath) {
50
62
  /**
51
63
  * Generate tree structure recursively
52
64
  */
53
- function generateTree( directory, prefix = '', isLast = true, basePath = null, profile ) {
65
+ function generateTree(directory, prefix = '', isLast = true, basePath = null, profile) {
54
66
  if (basePath === null) {
55
67
  basePath = directory;
56
68
  }
57
-
69
+
58
70
  const treeLines = [];
59
-
71
+
60
72
  try {
61
73
  let items = fs.readdirSync(directory).map(name => {
62
74
  const fullPath = path.join(directory, name);
63
75
  const stats = fs.statSync(fullPath);
64
76
  return { name, fullPath, isDir: stats.isDirectory() };
65
77
  });
66
-
78
+
67
79
  // Filter ignored items
68
80
  items = items.filter(item => !shouldIgnore(item.fullPath, basePath, profile));
69
-
81
+
70
82
  // Sort: directories first, then alphabetically
71
83
  items.sort((a, b) => {
72
84
  if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
73
85
  return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
74
86
  });
75
-
87
+
76
88
  for (let i = 0; i < items.length; i++) {
77
89
  const item = items[i];
78
90
  const isLastItem = i === items.length - 1;
79
-
91
+
80
92
  // Tree characters
81
93
  const connector = isLastItem ? '└── ' : '├── ';
82
94
  const extension = isLastItem ? ' ' : '│ ';
83
-
95
+
84
96
  if (item.isDir) {
85
97
  if (isDirectoryEmpty(item.fullPath, basePath)) {
86
98
  treeLines.push(`${prefix}${connector}${item.name}/ (empty)`);
87
99
  } else {
88
100
  treeLines.push(`${prefix}${connector}${item.name}/`);
89
- const subtree = generateTree( item.fullPath, prefix + extension, isLastItem, basePath, profile );
101
+ const subtree = generateTree(item.fullPath, prefix + extension, isLastItem, basePath, profile);
90
102
  treeLines.push(...subtree);
91
103
  }
92
104
  } else {
@@ -102,7 +114,7 @@ function generateTree( directory, prefix = '', isLast = true, basePath = null, p
102
114
  } catch (err) {
103
115
  // Permission error or other issues
104
116
  }
105
-
117
+
106
118
  return treeLines;
107
119
  }
108
120
 
@@ -140,7 +152,7 @@ function readFileContent(filePath) {
140
152
  if (stats.size === 0) {
141
153
  return '(empty file)';
142
154
  }
143
-
155
+
144
156
  return fs.readFileSync(filePath, 'utf-8');
145
157
  } catch (err) {
146
158
  if (err.message.includes('invalid')) {
@@ -182,7 +194,7 @@ function getLanguageFromExtension(filePath) {
182
194
  '.rb': 'ruby',
183
195
  '.php': 'php',
184
196
  };
185
-
197
+
186
198
  const ext = path.extname(filePath);
187
199
  return extMap[ext] || '';
188
200
  }
@@ -211,7 +223,7 @@ function collectFiles(directory, basePath, profile) {
211
223
  files.push(...collectFiles(fullPath, basePath, profile));
212
224
  }
213
225
  }
214
- } catch {}
226
+ } catch { }
215
227
 
216
228
  return files;
217
229
  }
@@ -221,15 +233,15 @@ function collectFiles(directory, basePath, profile) {
221
233
  */
222
234
  function generateDocumentation(projectPath, outputFile, profile) {
223
235
  const absProjectPath = path.resolve(projectPath);
224
-
236
+
225
237
  if (!fs.existsSync(absProjectPath)) {
226
238
  console.error(`Error: Path '${absProjectPath}' does not exist`);
227
239
  process.exit(1);
228
240
  }
229
-
241
+
230
242
  console.log(`Generating documentation for: ${absProjectPath}`);
231
243
  console.log(`Output file: ${outputFile}`);
232
-
244
+
233
245
  // Delete existing output file if it exists
234
246
  if (fs.existsSync(outputFile)) {
235
247
  try {
@@ -239,43 +251,48 @@ function generateDocumentation(projectPath, outputFile, profile) {
239
251
  console.warn(`Warning: Could not delete existing file: ${err.message}`);
240
252
  }
241
253
  }
242
-
254
+
243
255
  const projectName = path.basename(absProjectPath);
244
256
  let output = '';
245
-
257
+
246
258
  // Write header
247
259
  output += `# Project Documentation: ${projectName}\n\n`;
248
260
  output += `**Generated from:** \`${absProjectPath}\`\n\n`;
249
261
  output += '---\n\n';
250
-
262
+
251
263
  // Write folder structure
252
264
  output += '## 📁 Folder Structure\n\n';
253
265
  output += '```\n';
254
266
  output += `${projectName}/\n`;
255
-
267
+
268
+ const treeSpinner = new Spinner('Generating folder structure');
256
269
  const treeLines = generateTree(absProjectPath, '', true, absProjectPath, profile);
270
+ treeSpinner.succeed('Folder structure generated');
271
+
257
272
  for (const line of treeLines) {
258
273
  output += `${line}\n`;
259
274
  }
260
-
275
+
261
276
  output += '```\n\n';
262
277
  output += '---\n\n';
263
-
278
+
264
279
  // Write file contents
265
280
  output += '## 📄 File Contents\n\n';
266
-
281
+
282
+ const fileSpinner = new Spinner('Collecting files');
267
283
  const files = collectFiles(absProjectPath, absProjectPath, profile);
268
-
284
+ fileSpinner.succeed(`Collected ${files.length} files`);
285
+
269
286
  for (let i = 0; i < files.length; i++) {
270
287
  const filePath = files[i];
271
288
  const relPath = path.relative(absProjectPath, filePath);
272
289
  console.log(`Processing (${i + 1}/${files.length}): ${relPath}`);
273
-
290
+
274
291
  output += `### \`${relPath}\`\n\n`;
275
-
292
+
276
293
  const content = readFileContent(filePath);
277
294
  const language = getLanguageFromExtension(filePath);
278
-
295
+
279
296
  output += `\`\`\`${language}\n`;
280
297
  output += content;
281
298
  if (!content.endsWith('\n')) {
@@ -284,27 +301,75 @@ function generateDocumentation(projectPath, outputFile, profile) {
284
301
  output += '```\n\n';
285
302
  output += '---\n\n';
286
303
  }
287
-
304
+
288
305
  // Write to file
289
306
  fs.writeFileSync(outputFile, output, 'utf-8');
290
-
307
+
291
308
  console.log(`\n✅ Documentation generated successfully: ${outputFile}`);
292
309
  console.log(`📊 Total files processed: ${files.length}`);
293
310
  }
294
311
 
295
312
  // Main execution
296
- function main() {
313
+ async function main() {
314
+ let cliIgnores = [];
297
315
  const args = process.argv.slice(2);
298
316
 
299
- const projectPath = args[0] || '.';
300
- const outputFile = args[1] || 'PROJECT_DOCUMENTATION.md';
317
+ let projectPath = '.';
318
+ let outputFile = 'PROJECT_DOCUMENTATION.md';
319
+ let projectType = null;
320
+
321
+ for (let i = 0; i < args.length; i++) {
322
+ const arg = args[i];
323
+
324
+ if (arg === '--ignore' || arg === '-i') {
325
+ const value = args[i + 1];
326
+ if (value) {
327
+ value
328
+ .split(',')
329
+ .map(v => v.trim())
330
+ .filter(Boolean)
331
+ .forEach(item => {
332
+ if (item.startsWith('.')) {
333
+ CLI_IGNORE_EXTENSIONS.add(item);
334
+ } else {
335
+ CLI_IGNORE_PATHS.add(item);
336
+ }
337
+ });
338
+ i++;
339
+ }
340
+ }
341
+ }
342
+
343
+ let profile = ProfileRegistry.getByName(projectType, projectPath);
344
+
345
+ if (!profile) {
346
+ const detector = new ProjectTypeDetector(projectPath);
347
+ const detections = detector.detect();
348
+
349
+ const flutterSignal = detections.find(d => d.type === 'flutter');
301
350
 
302
- // future: parse --project-type
303
- const profile =
304
- ProfileRegistry.getByName(null, projectPath) ||
305
- ProfileRegistry.fallback(projectPath);
351
+ if (flutterSignal && process.stdin.isTTY) {
352
+ console.log('');
353
+ console.log(`Detected project type: ${flutterSignal.type}`);
354
+ console.log('');
355
+
356
+ const selectedPreset = await PresetSelector.choose(flutterSignal.type);
357
+ profile = ProfileRegistry.getByName(selectedPreset, projectPath);
358
+ }
359
+ }
306
360
 
361
+ if (!profile) {
362
+ profile = ProfileRegistry.fallback(projectPath);
363
+ }
364
+
365
+ if (cliIgnores.length) {
366
+ profile.applyCliIgnores(cliIgnores);
367
+ console.log(`🚫 CLI ignores applied: ${cliIgnores.join(', ')}`);
368
+ }
369
+
370
+ console.log(`Using profile: ${profile.name}`);
307
371
  generateDocumentation(projectPath, outputFile, profile);
372
+
308
373
  }
309
374
 
310
375
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progga",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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
  }
@@ -1,6 +1,18 @@
1
1
  class ProjectProfile {
2
2
  constructor(projectRoot) {
3
3
  this.projectRoot = projectRoot;
4
+ this.cliIgnorePaths = [];
5
+ this.cliIgnoreExtensions = [];
6
+ }
7
+
8
+ applyCliIgnores(list) {
9
+ for (const item of list) {
10
+ if (item.startsWith('.')) {
11
+ this.cliIgnoreExtensions.push(item);
12
+ } else {
13
+ this.cliIgnorePaths.push(item);
14
+ }
15
+ }
4
16
  }
5
17
 
6
18
  get name() {
@@ -9,12 +21,12 @@ class ProjectProfile {
9
21
 
10
22
  /** Paths to fully ignore */
11
23
  ignorePaths() {
12
- return [];
24
+ return this.cliIgnorePaths;
13
25
  }
14
26
 
15
27
  /** Extensions to ignore */
16
28
  ignoreExtensions() {
17
- return [];
29
+ return this.cliIgnoreExtensions;
18
30
  }
19
31
 
20
32
  /** Binary extensions */