blytz 1.0.0
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/LICENSE +21 -0
- package/README.md +60 -0
- package/bin/cli.js +122 -0
- package/package.json +45 -0
- package/server/analytics.js +99 -0
- package/server/bot.js +210 -0
- package/server/github.js +11 -0
- package/server/server.js +67 -0
- package/src/fileTree.js +26 -0
- package/src/index.js +73 -0
- package/src/processReadme.js +83 -0
- package/src/projectReader.js +5 -0
- package/src/template.js +113 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aryan Sharma
|
|
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/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
## Description
|
|
2
|
+
|
|
3
|
+
diglet is a Node.js application. Add a brief description of its purpose and what problem it solves.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Follow these steps to install the project:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
You can run the following scripts:
|
|
16
|
+
|
|
17
|
+
- `npm start`
|
|
18
|
+
|
|
19
|
+
## Dependencies
|
|
20
|
+
|
|
21
|
+
This project uses the following dependencies:
|
|
22
|
+
|
|
23
|
+
- @octokit/app
|
|
24
|
+
- @octokit/rest
|
|
25
|
+
- dotenv
|
|
26
|
+
- express
|
|
27
|
+
|
|
28
|
+
## Folder Structure
|
|
29
|
+
|
|
30
|
+
Project structure:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
├── .env
|
|
34
|
+
├── .gitignore
|
|
35
|
+
├── bin
|
|
36
|
+
│ └── cli.js
|
|
37
|
+
├── LICENSE
|
|
38
|
+
├── package-lock.json
|
|
39
|
+
├── package.json
|
|
40
|
+
├── README.md
|
|
41
|
+
├── server
|
|
42
|
+
│ ├── analytics.js
|
|
43
|
+
│ ├── bot.js
|
|
44
|
+
│ ├── github.js
|
|
45
|
+
│ └── server.js
|
|
46
|
+
└── src
|
|
47
|
+
├── fileTree.js
|
|
48
|
+
├── index.js
|
|
49
|
+
├── processReadme.js
|
|
50
|
+
├── projectReader.js
|
|
51
|
+
└── template.js
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
Add your license information here.
|
|
57
|
+
|
|
58
|
+
## Built By
|
|
59
|
+
|
|
60
|
+
Built with ❤️ by @Aryan Sharma
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import processReadme from '../src/processReadme.js';
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const shouldShowHelp = args.includes('--help') || args.includes('-h');
|
|
10
|
+
const shouldInit = args.includes('--init');
|
|
11
|
+
const shouldForce = args.includes('--force');
|
|
12
|
+
const shouldUpdate = args.length === 0 || args.includes('--update');
|
|
13
|
+
const hasAction = shouldUpdate || shouldInit || shouldForce;
|
|
14
|
+
|
|
15
|
+
if (shouldShowHelp) {
|
|
16
|
+
console.log('Usage: fixme [--update|--init|--force]');
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!hasAction) {
|
|
21
|
+
console.log('Usage: fixme [--update|--init|--force]');
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildFileTree(dirPath, depth = 0, maxDepth = 4) {
|
|
26
|
+
if (depth >= maxDepth) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tree = {};
|
|
31
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
32
|
+
.filter(entry => entry.name !== 'node_modules' && entry.name !== '.git')
|
|
33
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
34
|
+
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
37
|
+
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
tree[entry.name] = buildFileTree(entryPath, depth + 1, maxDepth);
|
|
40
|
+
} else {
|
|
41
|
+
tree[entry.name] = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return tree;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collectDependencies(packageJson) {
|
|
49
|
+
return Object.keys(packageJson.dependencies || {}).sort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function collectScripts(packageJson) {
|
|
53
|
+
const scripts = new Map();
|
|
54
|
+
|
|
55
|
+
for (const [name, command] of Object.entries(packageJson.scripts || {})) {
|
|
56
|
+
scripts.set(name, [{ package: '(root)', command }]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return scripts;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log("Scanning for project files...");
|
|
63
|
+
|
|
64
|
+
const targetDir = process.cwd();
|
|
65
|
+
const readmePath = path.join(targetDir, 'README.md');
|
|
66
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
67
|
+
const readmeExists = fs.existsSync(readmePath);
|
|
68
|
+
|
|
69
|
+
if (!readmeExists && !shouldInit && !shouldForce) {
|
|
70
|
+
console.error("Error: No README.md found in this directory. Try --init.");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (readmeExists && shouldInit && !shouldForce) {
|
|
75
|
+
console.error("README.md already exists. Try --force.");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
80
|
+
console.error("Error: No package.json found in this directory.");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log("Files found. Processing README...");
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (shouldForce && readmeExists) {
|
|
88
|
+
fs.unlinkSync(readmePath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 1. Read the raw text of both files
|
|
92
|
+
const readmeContent = fs.existsSync(readmePath) ? fs.readFileSync(readmePath, 'utf-8') : '';
|
|
93
|
+
const packageJsonData = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
94
|
+
|
|
95
|
+
// 2. Parse package.json to build the context object your engine expects
|
|
96
|
+
const packageJson = JSON.parse(packageJsonData);
|
|
97
|
+
const fileTree = buildFileTree(targetDir);
|
|
98
|
+
const context = {
|
|
99
|
+
packageJson,
|
|
100
|
+
packages: [{ path: 'package.json', content: packageJson }],
|
|
101
|
+
dependencies: collectDependencies(packageJson),
|
|
102
|
+
scripts: collectScripts(packageJson),
|
|
103
|
+
fileTree,
|
|
104
|
+
username: packageJson.author || 'Unknown Author',
|
|
105
|
+
projectName: packageJson.name || 'this project',
|
|
106
|
+
hasPackageJson: true,
|
|
107
|
+
isMonorepo: false
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// 3. Feed everything into your pure engine
|
|
111
|
+
// Hardcoding 'node' as projectType for now since we rely on package.json
|
|
112
|
+
const updatedReadme = processReadme(readmeContent, 'node', context);
|
|
113
|
+
|
|
114
|
+
// 4. Overwrite the existing README.md with the new content
|
|
115
|
+
fs.writeFileSync(readmePath, updatedReadme, 'utf-8');
|
|
116
|
+
|
|
117
|
+
console.log("Success! README.md has been auto-fixed.");
|
|
118
|
+
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error("An error occurred during processing:", error.message);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blytz",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"bin": {
|
|
5
|
+
"blytz": "./bin/cli.js"
|
|
6
|
+
},
|
|
7
|
+
"description": "An automated CLI tool to fix and maintain project READMEs",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/AryanSharma48/readme-auto-fixer.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"cli",
|
|
18
|
+
"readme",
|
|
19
|
+
"automation",
|
|
20
|
+
"github",
|
|
21
|
+
"bot",
|
|
22
|
+
"readmefixer",
|
|
23
|
+
"readmeauto",
|
|
24
|
+
"readme-maintainer",
|
|
25
|
+
"readme-updater",
|
|
26
|
+
"readme-helper",
|
|
27
|
+
"readme-fixer",
|
|
28
|
+
"readme-bot",
|
|
29
|
+
"readme-automation",
|
|
30
|
+
"readme-enhancer",
|
|
31
|
+
"readme-improver"
|
|
32
|
+
],
|
|
33
|
+
"author": "Aryan Sharma",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/AryanSharma48/readme-auto-fixer/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/AryanSharma48/readme-auto-fixer#readme",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@octokit/app": "^16.1.2",
|
|
41
|
+
"@octokit/rest": "^22.0.1",
|
|
42
|
+
"dotenv": "^17.4.0",
|
|
43
|
+
"express": "^5.2.1"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const DATA_FILE = path.join(__dirname, "../data/analytics.json");
|
|
7
|
+
|
|
8
|
+
const DEFAULT_DATA = {
|
|
9
|
+
installs: [],
|
|
10
|
+
events: []
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async function ensureDataDir() {
|
|
14
|
+
const dir = path.dirname(DATA_FILE);
|
|
15
|
+
try {
|
|
16
|
+
await fs.mkdir(dir, { recursive: true });
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (err.code !== "EEXIST") throw err;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function readData() {
|
|
23
|
+
try {
|
|
24
|
+
const raw = await fs.readFile(DATA_FILE, "utf-8");
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err.code === "ENOENT") {
|
|
28
|
+
return { ...DEFAULT_DATA };
|
|
29
|
+
}
|
|
30
|
+
if (err instanceof SyntaxError) {
|
|
31
|
+
console.error("Invalid JSON in analytics file, resetting...");
|
|
32
|
+
return { ...DEFAULT_DATA };
|
|
33
|
+
}
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function writeData(data) {
|
|
39
|
+
await ensureDataDir();
|
|
40
|
+
await fs.writeFile(DATA_FILE, JSON.stringify(data, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function trackInstall(installationId, accountLogin, accountType) {
|
|
44
|
+
const data = await readData();
|
|
45
|
+
|
|
46
|
+
const exists = data.installs.some(i => i.installationId === installationId);
|
|
47
|
+
if (!exists) {
|
|
48
|
+
data.installs.push({
|
|
49
|
+
installationId,
|
|
50
|
+
accountLogin,
|
|
51
|
+
accountType,
|
|
52
|
+
installedAt: new Date().toISOString()
|
|
53
|
+
});
|
|
54
|
+
await writeData(data);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return !exists;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function trackUninstall(installationId) {
|
|
61
|
+
const data = await readData();
|
|
62
|
+
const initialLength = data.installs.length;
|
|
63
|
+
|
|
64
|
+
data.installs = data.installs.filter(i => i.installationId !== installationId);
|
|
65
|
+
|
|
66
|
+
if (data.installs.length !== initialLength) {
|
|
67
|
+
await writeData(data);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function trackEvent(eventType, payload = {}) {
|
|
74
|
+
const data = await readData();
|
|
75
|
+
|
|
76
|
+
data.events.push({
|
|
77
|
+
type: eventType,
|
|
78
|
+
payload,
|
|
79
|
+
timestamp: new Date().toISOString()
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await writeData(data);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function getStats() {
|
|
86
|
+
const data = await readData();
|
|
87
|
+
|
|
88
|
+
const eventCounts = data.events.reduce((acc, e) => {
|
|
89
|
+
acc[e.type] = (acc[e.type] || 0) + 1;
|
|
90
|
+
return acc;
|
|
91
|
+
}, {});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
totalInstalls: data.installs.length,
|
|
95
|
+
totalEvents: data.events.length,
|
|
96
|
+
eventsByType: eventCounts,
|
|
97
|
+
recentEvents: data.events.slice(-10).reverse()
|
|
98
|
+
};
|
|
99
|
+
}
|
package/server/bot.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { getOctokit } from "./github.js";
|
|
2
|
+
import processReadme from "../src/processReadme.js";
|
|
3
|
+
|
|
4
|
+
async function findAllPackageJsonPaths(octokit, owner, repo) {
|
|
5
|
+
try {
|
|
6
|
+
const { data: repoData } = await octokit.request("GET /repos/{owner}/{repo}", {
|
|
7
|
+
owner,
|
|
8
|
+
repo,
|
|
9
|
+
});
|
|
10
|
+
const defaultBranch = repoData.default_branch;
|
|
11
|
+
|
|
12
|
+
const { data: treeData } = await octokit.request("GET /repos/{owner}/{repo}/git/trees/{tree_sha}", {
|
|
13
|
+
owner,
|
|
14
|
+
repo,
|
|
15
|
+
tree_sha: defaultBranch,
|
|
16
|
+
recursive: "true",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const packageJsonPaths = treeData.tree
|
|
20
|
+
.filter(item =>
|
|
21
|
+
item.type === "blob" &&
|
|
22
|
+
item.path.endsWith("package.json") &&
|
|
23
|
+
!item.path.includes("node_modules/")
|
|
24
|
+
)
|
|
25
|
+
.map(item => item.path);
|
|
26
|
+
|
|
27
|
+
return { packageJsonPaths, treeData };
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error("Error scanning repository tree:", err.message);
|
|
30
|
+
return { packageJsonPaths: [], treeData: null };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function fetchPackageJson(octokit, owner, repo, path) {
|
|
35
|
+
try {
|
|
36
|
+
const { data } = await octokit.request("GET /repos/{owner}/{repo}/contents/{path}", {
|
|
37
|
+
owner,
|
|
38
|
+
repo,
|
|
39
|
+
path,
|
|
40
|
+
});
|
|
41
|
+
const content = JSON.parse(Buffer.from(data.content, "base64").toString());
|
|
42
|
+
return { path, content, error: null };
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.warn(`Failed to fetch/parse ${path}:`, err.message);
|
|
45
|
+
return { path, content: null, error: err.message };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function fetchAllPackages(octokit, owner, repo) {
|
|
50
|
+
const { packageJsonPaths, treeData } = await findAllPackageJsonPaths(octokit, owner, repo);
|
|
51
|
+
const fileTree = treeData ? buildFileTree(treeData) : null;
|
|
52
|
+
|
|
53
|
+
if (packageJsonPaths.length === 0) {
|
|
54
|
+
return { packages: [], fileTree };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const results = await Promise.all(
|
|
58
|
+
packageJsonPaths.map(path => fetchPackageJson(octokit, owner, repo, path))
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const packages = results.filter(pkg => pkg.content !== null);
|
|
62
|
+
return { packages, fileTree };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function aggregateDependencies(packages) {
|
|
66
|
+
const ignoreList = [
|
|
67
|
+
'eslint', 'prettier', 'husky', 'lint-staged', 'stylelint',
|
|
68
|
+
'typescript', 'vite', 'webpack', 'rollup', 'parcel', 'esbuild',
|
|
69
|
+
'babel', 'tsc', 'ts-node', 'tsx', 'nodemon', 'concurrently',
|
|
70
|
+
'jest', 'mocha', 'chai', 'vitest', 'cypress', 'playwright', 'supertest',
|
|
71
|
+
'postcss', 'autoprefixer', 'sass', 'less', 'dotenv', 'cross-env', 'rimraf',
|
|
72
|
+
'black', 'flake8', 'pylint', 'mypy', 'isort', 'autopep8', 'bandit',
|
|
73
|
+
'pytest', 'coverage', 'tox', 'mock', 'hypothesis', 'pytest-cov',
|
|
74
|
+
'setuptools', 'wheel', 'twine', 'build',
|
|
75
|
+
'python-dotenv', 'pip-tools', 'virtualenv', 'pre-commit'
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const depsSet = new Set();
|
|
79
|
+
|
|
80
|
+
for (const pkg of packages) {
|
|
81
|
+
const { content } = pkg;
|
|
82
|
+
if (content?.dependencies) {
|
|
83
|
+
Object.keys(content.dependencies).forEach(dep => {
|
|
84
|
+
if (dep.startsWith('@types/')) return;
|
|
85
|
+
if (dep.startsWith('@babel/')) return;
|
|
86
|
+
if (dep.startsWith('@vitejs/')) return;
|
|
87
|
+
if (dep.startsWith('types-')) return;
|
|
88
|
+
if (ignoreList.includes(dep)) return;
|
|
89
|
+
depsSet.add(dep);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Array.from(depsSet).sort();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildFileTree(treeData, maxDepth = 4) {
|
|
98
|
+
const fileTree = {};
|
|
99
|
+
|
|
100
|
+
if (!treeData?.tree || !Array.isArray(treeData.tree)) {
|
|
101
|
+
return fileTree;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const item of treeData.tree) {
|
|
105
|
+
if (
|
|
106
|
+
item.path === "node_modules" ||
|
|
107
|
+
item.path.startsWith("node_modules/") ||
|
|
108
|
+
item.path === ".git" ||
|
|
109
|
+
item.path.startsWith(".git/")
|
|
110
|
+
) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const parts = item.path.split("/");
|
|
115
|
+
|
|
116
|
+
if (parts.length > maxDepth) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let current = fileTree;
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < parts.length; i++) {
|
|
123
|
+
const part = parts[i];
|
|
124
|
+
const isFile = i === parts.length - 1 && item.type === "blob";
|
|
125
|
+
|
|
126
|
+
if (isFile) {
|
|
127
|
+
current[part] = null;
|
|
128
|
+
} else {
|
|
129
|
+
if (!current[part]) {
|
|
130
|
+
current[part] = {};
|
|
131
|
+
}
|
|
132
|
+
current = current[part];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return fileTree;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function aggregateScripts(packages) {
|
|
141
|
+
const scriptsMap = new Map();
|
|
142
|
+
|
|
143
|
+
for (const pkg of packages) {
|
|
144
|
+
const { path, content } = pkg;
|
|
145
|
+
const packageDir = path === "package.json" ? "(root)" : path.replace("/package.json", "");
|
|
146
|
+
|
|
147
|
+
if (content?.scripts) {
|
|
148
|
+
for (const [name, command] of Object.entries(content.scripts)) {
|
|
149
|
+
if (!scriptsMap.has(name)) {
|
|
150
|
+
scriptsMap.set(name, []);
|
|
151
|
+
}
|
|
152
|
+
scriptsMap.get(name).push({ package: packageDir, command });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return scriptsMap;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function runBot(payload) {
|
|
161
|
+
try {
|
|
162
|
+
const installationId = payload.installation.id;
|
|
163
|
+
const owner = payload.repository.owner.login;
|
|
164
|
+
const repo = payload.repository.name;
|
|
165
|
+
|
|
166
|
+
const octokit = await getOctokit(installationId);
|
|
167
|
+
|
|
168
|
+
const { data } = await octokit.request("GET /repos/{owner}/{repo}/contents/{path}", {
|
|
169
|
+
owner,
|
|
170
|
+
repo,
|
|
171
|
+
path: "README.md",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const content = Buffer.from(data.content, "base64").toString();
|
|
175
|
+
const { packages, fileTree } = await fetchAllPackages(octokit, owner, repo);
|
|
176
|
+
const dependencies = aggregateDependencies(packages);
|
|
177
|
+
const scripts = aggregateScripts(packages);
|
|
178
|
+
const projectType = packages.length > 0 ? "node" : "unknown";
|
|
179
|
+
|
|
180
|
+
const context = {
|
|
181
|
+
packages,
|
|
182
|
+
dependencies,
|
|
183
|
+
scripts,
|
|
184
|
+
fileTree,
|
|
185
|
+
username: owner,
|
|
186
|
+
isMonorepo: packages.length > 1,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const newReadme = processReadme(content, projectType, context);
|
|
190
|
+
|
|
191
|
+
if (newReadme === content) {
|
|
192
|
+
console.log("No changes needed");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await octokit.request("PUT /repos/{owner}/{repo}/contents/{path}", {
|
|
197
|
+
owner,
|
|
198
|
+
repo,
|
|
199
|
+
path: "README.md",
|
|
200
|
+
message: "Auto-update README",
|
|
201
|
+
content: Buffer.from(newReadme).toString("base64"),
|
|
202
|
+
sha: data.sha,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
console.log("README updated successfully");
|
|
206
|
+
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error("Bot error:", err.message);
|
|
209
|
+
}
|
|
210
|
+
}
|
package/server/github.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { App } from "@octokit/app";
|
|
2
|
+
|
|
3
|
+
const app = new App({
|
|
4
|
+
appId: process.env.APP_ID,
|
|
5
|
+
privateKey: process.env.PRIVATE_KEY.replace(/\\n/g, '\n'),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export async function getOctokit(installationId) {
|
|
9
|
+
const octokit = await app.getInstallationOctokit(installationId);
|
|
10
|
+
return octokit;
|
|
11
|
+
}
|
package/server/server.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
import { runBot } from "./bot.js";
|
|
4
|
+
import { trackInstall, trackUninstall, trackEvent, getStats } from "./analytics.js";
|
|
5
|
+
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
const app = express();
|
|
9
|
+
app.use(express.json());
|
|
10
|
+
|
|
11
|
+
app.get("/health", (req, res) => {
|
|
12
|
+
res.json({ status: "ok" });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
app.get("/stats", async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const stats = await getStats();
|
|
18
|
+
res.json(stats);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error("Stats error:", err.message);
|
|
21
|
+
res.status(500).json({ error: "Failed to fetch stats" });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
app.post("/webhook", async (req, res) => {
|
|
26
|
+
const event = req.headers["x-github-event"];
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
if (event === "installation") {
|
|
30
|
+
const { action, installation } = req.body;
|
|
31
|
+
|
|
32
|
+
if (action === "created") {
|
|
33
|
+
await trackInstall(
|
|
34
|
+
installation.id,
|
|
35
|
+
installation.account.login,
|
|
36
|
+
installation.account.type
|
|
37
|
+
);
|
|
38
|
+
await trackEvent("installation", { action, installationId: installation.id });
|
|
39
|
+
} else if (action === "deleted") {
|
|
40
|
+
await trackUninstall(installation.id);
|
|
41
|
+
await trackEvent("installation", { action, installationId: installation.id });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (event === "push") {
|
|
46
|
+
console.log("Push event received");
|
|
47
|
+
|
|
48
|
+
const { repository, installation } = req.body;
|
|
49
|
+
await trackEvent("push", {
|
|
50
|
+
repo: repository.full_name,
|
|
51
|
+
installationId: installation.id
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await runBot(req.body);
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error("Webhook error:", err.message);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
res.sendStatus(200);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const PORT = process.env.PORT || 3000;
|
|
64
|
+
|
|
65
|
+
app.listen(PORT, () => {
|
|
66
|
+
console.log(`Server running on port ${PORT}`);
|
|
67
|
+
});
|
package/src/fileTree.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// recursively builds a tree string from a file tree object
|
|
2
|
+
function generateTree(fileTree, prefix = "") {
|
|
3
|
+
if (!fileTree || typeof fileTree !== "object") return "";
|
|
4
|
+
|
|
5
|
+
const entries = Object.entries(fileTree);
|
|
6
|
+
let tree = "";
|
|
7
|
+
|
|
8
|
+
entries.forEach(([name, value], index) => {
|
|
9
|
+
if (name === "node_modules" || name === ".git") return;
|
|
10
|
+
|
|
11
|
+
const isLast = index === entries.length - 1;
|
|
12
|
+
const connector = isLast ? "└── " : "├── ";
|
|
13
|
+
tree += `${prefix}${connector}${name}\n`;
|
|
14
|
+
|
|
15
|
+
// recurse into directories
|
|
16
|
+
if (value && typeof value === "object") {
|
|
17
|
+
tree += generateTree(value, prefix + (isLast ? " " : "│ "));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return tree;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function getProjectStructure(fileTree) {
|
|
25
|
+
return "```\n" + generateTree(fileTree) + "```";
|
|
26
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import getDefaultContent from "./template.js";
|
|
2
|
+
import { detectProjectType } from "./projectReader.js";
|
|
3
|
+
|
|
4
|
+
export function generateReadme(input = {}) {
|
|
5
|
+
const {
|
|
6
|
+
readmeContent = "",
|
|
7
|
+
packageJson = null,
|
|
8
|
+
fileTree = null,
|
|
9
|
+
username = "Unknown",
|
|
10
|
+
projectName = null,
|
|
11
|
+
hasPackageJson = false,
|
|
12
|
+
hasRequirementsTxt = false
|
|
13
|
+
} = input ?? {};
|
|
14
|
+
|
|
15
|
+
const resolvedProjectName = projectName || packageJson?.name || "this project";
|
|
16
|
+
|
|
17
|
+
const context = {
|
|
18
|
+
packageJson,
|
|
19
|
+
fileTree,
|
|
20
|
+
username,
|
|
21
|
+
projectName: resolvedProjectName,
|
|
22
|
+
hasPackageJson,
|
|
23
|
+
hasRequirementsTxt
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const projectType = detectProjectType(context);
|
|
27
|
+
|
|
28
|
+
// parse existing readme into sections
|
|
29
|
+
const sections = (readmeContent || "").split("## ");
|
|
30
|
+
const sectionMap = {};
|
|
31
|
+
const intro = sections[0].trim();
|
|
32
|
+
|
|
33
|
+
sections.slice(1).forEach(section => {
|
|
34
|
+
const lines = section.split("\n");
|
|
35
|
+
const title = lines[0].trim().toLowerCase();
|
|
36
|
+
const content = lines.slice(1).join("\n").trim();
|
|
37
|
+
sectionMap[title] = content;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const requiredSections = ["description", "installation", "usage", "dependencies", "folder structure", "built by"];
|
|
41
|
+
const autoManagedSections = ["installation", "usage", "dependencies", "folder structure"];
|
|
42
|
+
|
|
43
|
+
// fill in missing sections or update auto-managed ones
|
|
44
|
+
requiredSections.forEach(section => {
|
|
45
|
+
const newContent = getDefaultContent(section, projectType, context);
|
|
46
|
+
if (!(section in sectionMap)) {
|
|
47
|
+
sectionMap[section] = newContent;
|
|
48
|
+
} else if (autoManagedSections.includes(section) && sectionMap[section].trim() !== newContent.trim()) {
|
|
49
|
+
sectionMap[section] = newContent;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const formatTitle = title => title.split(" ").map(w => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
54
|
+
|
|
55
|
+
// build the final readme
|
|
56
|
+
let newReadme = intro ? intro + "\n\n" : "";
|
|
57
|
+
requiredSections.forEach(section => {
|
|
58
|
+
newReadme += `## ${formatTitle(section)}\n${sectionMap[section]}\n\n`;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// append any extra sections the user had
|
|
62
|
+
Object.keys(sectionMap).forEach(section => {
|
|
63
|
+
if (!requiredSections.includes(section)) {
|
|
64
|
+
newReadme += `## ${formatTitle(section)}\n\n ${sectionMap[section]}\n\n`;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return { content: newReadme, changed: readmeContent !== newReadme };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { detectProjectType } from "./projectReader.js";
|
|
72
|
+
export { default as getDefaultContent } from "./template.js";
|
|
73
|
+
export { default as getProjectStructure } from "./fileTree.js";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import getDefaultContent from "./template.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
//Core engine: processes README content and returns updated version
|
|
5
|
+
export default function processReadme(content, projectType, context = {}) {
|
|
6
|
+
|
|
7
|
+
const normalizedContent = (content || "").replace(/^##(?=\S)/gm, "## ");
|
|
8
|
+
|
|
9
|
+
// Split into sections
|
|
10
|
+
const sections = normalizedContent.split("## ");
|
|
11
|
+
const sectionMap = {};
|
|
12
|
+
|
|
13
|
+
// Extract intro (title + description)
|
|
14
|
+
const intro = sections[0].trim();
|
|
15
|
+
|
|
16
|
+
// Parse sections
|
|
17
|
+
sections.slice(1).forEach(section => {
|
|
18
|
+
const lines = section.split("\n");
|
|
19
|
+
const title = lines[0].trim().toLowerCase();
|
|
20
|
+
const body = lines.slice(1).join("\n").trim();
|
|
21
|
+
|
|
22
|
+
sectionMap[title] = body;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Required sections
|
|
26
|
+
const requiredSections = [
|
|
27
|
+
"description",
|
|
28
|
+
"installation",
|
|
29
|
+
"usage",
|
|
30
|
+
"dependencies",
|
|
31
|
+
"folder structure",
|
|
32
|
+
"license",
|
|
33
|
+
"built by"
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Auto-managed sections (safe to update)
|
|
37
|
+
const autoManaged = [
|
|
38
|
+
"installation",
|
|
39
|
+
"usage",
|
|
40
|
+
"dependencies",
|
|
41
|
+
"folder structure"
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Diff function
|
|
45
|
+
const isDifferent = (a = "", b = "") => a.trim() !== b.trim();
|
|
46
|
+
|
|
47
|
+
// Add / update sections
|
|
48
|
+
requiredSections.forEach(section => {
|
|
49
|
+
const newContent = getDefaultContent(section, projectType, context);
|
|
50
|
+
const currentContent = (sectionMap[section] || "").trim();
|
|
51
|
+
|
|
52
|
+
if (!currentContent) {
|
|
53
|
+
sectionMap[section] = newContent;
|
|
54
|
+
} else if (autoManaged.includes(section)) {
|
|
55
|
+
if (isDifferent(currentContent, newContent)) {
|
|
56
|
+
sectionMap[section] = newContent;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Format title
|
|
62
|
+
const formatTitle = (title) =>
|
|
63
|
+
title.split(" ")
|
|
64
|
+
.map(word => word[0].toUpperCase() + word.slice(1))
|
|
65
|
+
.join(" ");
|
|
66
|
+
|
|
67
|
+
// Rebuild README
|
|
68
|
+
let newReadme = intro ? intro + "\n\n" : "";
|
|
69
|
+
|
|
70
|
+
// Ordered sections
|
|
71
|
+
requiredSections.forEach(section => {
|
|
72
|
+
newReadme += `## ${formatTitle(section)}\n\n${sectionMap[section]}\n\n`;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Extra sections (preserve user content)
|
|
76
|
+
Object.keys(sectionMap).forEach(section => {
|
|
77
|
+
if (!requiredSections.includes(section)) {
|
|
78
|
+
newReadme += `## ${formatTitle(section)}\n\n${sectionMap[section]}\n\n`;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return newReadme.trim();
|
|
83
|
+
}
|
package/src/template.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import getProjectStructure from "./fileTree.js";
|
|
2
|
+
|
|
3
|
+
export default function getDefaultContent(section, projectType, context = {}) {
|
|
4
|
+
const {
|
|
5
|
+
packages = [],
|
|
6
|
+
dependencies = [],
|
|
7
|
+
scripts = new Map(),
|
|
8
|
+
fileTree = null,
|
|
9
|
+
username = "Unknown",
|
|
10
|
+
projectName = "this project",
|
|
11
|
+
isMonorepo = false,
|
|
12
|
+
} = context ?? {};
|
|
13
|
+
|
|
14
|
+
const safeProjectType = projectType || "unknown";
|
|
15
|
+
const safeSection = (section || "").toLowerCase().trim();
|
|
16
|
+
|
|
17
|
+
switch (safeSection) {
|
|
18
|
+
case "description":
|
|
19
|
+
return getDescriptionContent(safeProjectType, projectName, isMonorepo);
|
|
20
|
+
case "installation":
|
|
21
|
+
return getInstallationContent(safeProjectType, packages, isMonorepo);
|
|
22
|
+
case "usage":
|
|
23
|
+
return getUsageContent(safeProjectType, scripts, isMonorepo);
|
|
24
|
+
case "dependencies":
|
|
25
|
+
return getDependenciesContent(dependencies, packages);
|
|
26
|
+
case "folder structure":
|
|
27
|
+
return getFolderStructureContent(fileTree);
|
|
28
|
+
case "license":
|
|
29
|
+
return "Add your license information here.";
|
|
30
|
+
case "built by":
|
|
31
|
+
return `Built with ❤️ by @${(username || "Unknown").trim()}`;
|
|
32
|
+
default:
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getDescriptionContent(projectType, projectName, isMonorepo) {
|
|
38
|
+
const name = projectName || "this project";
|
|
39
|
+
if (projectType === "node") {
|
|
40
|
+
if (isMonorepo) {
|
|
41
|
+
return `${name} is a Node.js monorepo containing multiple packages. Add a brief description of its purpose and what problem it solves.`;
|
|
42
|
+
}
|
|
43
|
+
return `${name} is a Node.js application. Add a brief description of its purpose and what problem it solves.`;
|
|
44
|
+
}
|
|
45
|
+
if (projectType === "python") return `${name} is a Python project. Add a brief description of its purpose and what problem it solves.`;
|
|
46
|
+
return `${name} - Add a brief description of your project, its purpose, and what problem it solves.`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getInstallationContent(projectType, packages, isMonorepo) {
|
|
50
|
+
if (projectType === "node") {
|
|
51
|
+
if (isMonorepo && packages.length > 1) {
|
|
52
|
+
const packageList = packages
|
|
53
|
+
.map(pkg => {
|
|
54
|
+
const dir = pkg.path === "package.json" ? "(root)" : pkg.path.replace("/package.json", "");
|
|
55
|
+
return `- \`${dir}\``;
|
|
56
|
+
})
|
|
57
|
+
.join("\n");
|
|
58
|
+
|
|
59
|
+
return `This is a monorepo with multiple packages:\n\n${packageList}\n\nTo install all dependencies:\n\n\`\`\`bash\n# Install root dependencies\nnpm install\n\n# Or install dependencies in each package\n${packages.map(pkg => {
|
|
60
|
+
const dir = pkg.path === "package.json" ? "." : pkg.path.replace("/package.json", "");
|
|
61
|
+
return `cd ${dir} && npm install`;
|
|
62
|
+
}).join("\n")}\n\`\`\``;
|
|
63
|
+
}
|
|
64
|
+
return "Follow these steps to install the project:\n\n```bash\nnpm install\n```";
|
|
65
|
+
}
|
|
66
|
+
if (projectType === "python") return "Install dependencies using:\n\n```bash\npip install -r requirements.txt\n```";
|
|
67
|
+
return "Add installation instructions here.";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getUsageContent(projectType, scripts, isMonorepo) {
|
|
71
|
+
if (projectType === "node") {
|
|
72
|
+
if (scripts instanceof Map && scripts.size > 0) {
|
|
73
|
+
const scriptEntries = [];
|
|
74
|
+
|
|
75
|
+
for (const [name, locations] of scripts) {
|
|
76
|
+
if (isMonorepo && locations.length > 1) {
|
|
77
|
+
const packageNames = locations.map(l => l.package).join(", ");
|
|
78
|
+
scriptEntries.push(`- \`npm run ${name}\` (available in: ${packageNames})`);
|
|
79
|
+
} else if (locations.length === 1) {
|
|
80
|
+
const prefix = isMonorepo ? ` (in ${locations[0].package})` : "";
|
|
81
|
+
const cmd = name === "start" ? "npm start" : `npm run ${name}`;
|
|
82
|
+
scriptEntries.push(`- \`${cmd}\`${prefix}`);
|
|
83
|
+
} else {
|
|
84
|
+
scriptEntries.push(`- \`npm run ${name}\``);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return `You can run the following scripts:\n\n${scriptEntries.join("\n")}`;
|
|
89
|
+
}
|
|
90
|
+
return "Run the project using:\n\n```bash\nnpm start\n```";
|
|
91
|
+
}
|
|
92
|
+
if (projectType === "python") return "Run the project using:\n\n```bash\npython main.py\n```";
|
|
93
|
+
return "Add usage instructions here.";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getDependenciesContent(dependencies, packages) {
|
|
97
|
+
if (dependencies && dependencies.length > 0) {
|
|
98
|
+
const isMonorepo = packages && packages.length > 1;
|
|
99
|
+
const header = isMonorepo
|
|
100
|
+
? `This project uses the following dependencies (across ${packages.length} packages):\n\n`
|
|
101
|
+
: "This project uses the following dependencies:\n\n";
|
|
102
|
+
|
|
103
|
+
return header + dependencies.map(d => `- ${d}`).join("\n");
|
|
104
|
+
}
|
|
105
|
+
return "No dependencies found.";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getFolderStructureContent(fileTree) {
|
|
109
|
+
if (!fileTree || typeof fileTree !== "object" || Object.keys(fileTree).length === 0) {
|
|
110
|
+
return "Project structure:\n\n```\n(No file tree provided)\n```";
|
|
111
|
+
}
|
|
112
|
+
return "Project structure:\n\n" + getProjectStructure(fileTree);
|
|
113
|
+
}
|