blytz 1.1.1 → 1.2.1
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 +3 -9
- package/bin/README-NPM.md +3 -9
- package/bin/cli.js +63 -69
- package/package.json +2 -2
- package/src/processReadme.js +14 -11
- package/src/readmeContext.js +269 -0
- package/src/index.js +0 -73
- package/src/projectReader.js +0 -5
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
- Updates an existing `README.md`
|
|
8
8
|
- Creates a new `README.md` with `--init`
|
|
9
9
|
- Replaces an existing `README.md` with `--force`
|
|
10
|
-
- Reads `package.json` to build README sections automatically
|
|
10
|
+
- Reads `package.json` / `requirements.txt` to build README sections automatically
|
|
11
11
|
- Generates a basic folder tree for the project structure section
|
|
12
12
|
|
|
13
13
|
## Install
|
|
@@ -16,12 +16,6 @@
|
|
|
16
16
|
npm install -g blytz
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
If you are developing locally from this repository, you can link it instead:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm link
|
|
23
|
-
```
|
|
24
|
-
|
|
25
19
|
## Usage
|
|
26
20
|
|
|
27
21
|
Run the CLI from the root of the project you want to document:
|
|
@@ -96,9 +90,9 @@ blytz
|
|
|
96
90
|
|
|
97
91
|
## Notes
|
|
98
92
|
|
|
99
|
-
- The CLI expects to run in a folder that contains `package.json`
|
|
93
|
+
- The CLI expects to run in a folder that contains `package.json` or `requirements.txt`
|
|
100
94
|
- The CLI reads the local `README.md` in the current directory
|
|
101
|
-
- The folder tree excludes `node_modules` and `.git`
|
|
95
|
+
- The folder tree excludes `node_modules` and `.git` and any folder that might be in `.gitignore`
|
|
102
96
|
|
|
103
97
|
## License
|
|
104
98
|
|
package/bin/README-NPM.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
- Updates an existing `README.md`
|
|
8
8
|
- Creates a new `README.md` with `--init`
|
|
9
9
|
- Replaces an existing `README.md` with `--force`
|
|
10
|
-
- Reads `package.json` to build README sections automatically
|
|
10
|
+
- Reads `package.json` / `requirements.txt` to build README sections automatically
|
|
11
11
|
- Generates a basic folder tree for the project structure section
|
|
12
12
|
|
|
13
13
|
## Install
|
|
@@ -16,12 +16,6 @@
|
|
|
16
16
|
npm install -g blytz
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
If you are developing locally from this repository, you can link it instead:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm link
|
|
23
|
-
```
|
|
24
|
-
|
|
25
19
|
## Usage
|
|
26
20
|
|
|
27
21
|
Run the CLI from the root of the project you want to document:
|
|
@@ -96,9 +90,9 @@ blytz
|
|
|
96
90
|
|
|
97
91
|
## Notes
|
|
98
92
|
|
|
99
|
-
- The CLI expects to run in a folder that contains `package.json`
|
|
93
|
+
- The CLI expects to run in a folder that contains `package.json` or `requirements.txt`
|
|
100
94
|
- The CLI reads the local `README.md` in the current directory
|
|
101
|
-
- The folder tree excludes `node_modules` and `.git`
|
|
95
|
+
- The folder tree excludes `node_modules` and `.git` and any folder that might be in `.gitignore`
|
|
102
96
|
|
|
103
97
|
## License
|
|
104
98
|
|
package/bin/cli.js
CHANGED
|
@@ -5,6 +5,13 @@ import path from 'path';
|
|
|
5
5
|
import readline from 'readline/promises';
|
|
6
6
|
|
|
7
7
|
import processReadme from '../src/processReadme.js';
|
|
8
|
+
import {
|
|
9
|
+
buildLocalFileTree,
|
|
10
|
+
collectNodeDependencies,
|
|
11
|
+
collectPythonDependencies,
|
|
12
|
+
collectScripts,
|
|
13
|
+
getLicenseName
|
|
14
|
+
} from '../src/readmeContext.js';
|
|
8
15
|
|
|
9
16
|
const args = process.argv.slice(2);
|
|
10
17
|
const shouldShowHelp = args.includes('--help') || args.includes('-h');
|
|
@@ -13,72 +20,59 @@ const shouldForce = args.includes('--force');
|
|
|
13
20
|
const shouldUpdate = args.length === 0 || args.includes('--update');
|
|
14
21
|
const hasAction = shouldUpdate || shouldInit || shouldForce;
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const tree = {};
|
|
32
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
33
|
-
.filter(entry => entry.name !== 'node_modules' && entry.name !== '.git')
|
|
34
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
35
|
-
|
|
36
|
-
for (const entry of entries) {
|
|
37
|
-
const entryPath = path.join(dirPath, entry.name);
|
|
38
|
-
|
|
39
|
-
if (entry.isDirectory()) {
|
|
40
|
-
tree[entry.name] = buildFileTree(entryPath, depth + 1, maxDepth);
|
|
41
|
-
} else {
|
|
42
|
-
tree[entry.name] = null;
|
|
43
|
-
}
|
|
23
|
+
const supportsColor = process.stdout.isTTY && process.env.NO_COLOR !== '1';
|
|
24
|
+
const ANSI = {
|
|
25
|
+
reset: '\x1b[0m',
|
|
26
|
+
bold: '\x1b[1m',
|
|
27
|
+
dim: '\x1b[2m',
|
|
28
|
+
cyan: '\x1b[36m',
|
|
29
|
+
green: '\x1b[32m',
|
|
30
|
+
yellow: '\x1b[33m',
|
|
31
|
+
red: '\x1b[31m',
|
|
32
|
+
blue: '\x1b[34m'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function colorize(text, ...codes) {
|
|
36
|
+
if (!supportsColor) {
|
|
37
|
+
return text;
|
|
44
38
|
}
|
|
45
39
|
|
|
46
|
-
return
|
|
40
|
+
return `${codes.join('')}${text}${ANSI.reset}`;
|
|
47
41
|
}
|
|
48
42
|
|
|
49
|
-
function
|
|
50
|
-
return
|
|
43
|
+
function formatLabel(label, color) {
|
|
44
|
+
return colorize(label, ANSI.bold, color);
|
|
51
45
|
}
|
|
52
46
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.
|
|
60
|
-
.
|
|
61
|
-
.
|
|
62
|
-
|
|
47
|
+
function printHelp(output = console.log) {
|
|
48
|
+
output([
|
|
49
|
+
formatLabel('Usage', ANSI.blue),
|
|
50
|
+
` ${colorize('blytz', ANSI.bold, ANSI.green)} ${colorize('[command]', ANSI.dim)}`,
|
|
51
|
+
'',
|
|
52
|
+
formatLabel('Commands', ANSI.blue),
|
|
53
|
+
` ${colorize('--update', ANSI.yellow)} ${colorize('Update the existing README.md using project metadata.', ANSI.dim)}`,
|
|
54
|
+
` ${colorize('--init', ANSI.yellow)} ${colorize('Create a new README.md and prompt for title and description.', ANSI.dim)}`,
|
|
55
|
+
` ${colorize('--force', ANSI.yellow)} ${colorize('Replace the existing README.md with a newly generated one.', ANSI.dim)}`,
|
|
56
|
+
` ${colorize('--help, -h', ANSI.yellow)} ${colorize('Show this help message.', ANSI.dim)}`,
|
|
57
|
+
'',
|
|
58
|
+
].join('\n'));
|
|
63
59
|
}
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (const [name, command] of Object.entries(packageJson.scripts || {})) {
|
|
69
|
-
scripts.set(name, [{ package: '(root)', command }]);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return scripts;
|
|
61
|
+
if (shouldShowHelp) {
|
|
62
|
+
printHelp();
|
|
63
|
+
process.exit(0);
|
|
73
64
|
}
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.
|
|
78
|
-
.
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
66
|
+
if (!hasAction) {
|
|
67
|
+
if (args.length > 0) {
|
|
68
|
+
const invalidCommand = args.find(arg => arg.startsWith('-')) || args[0];
|
|
69
|
+
console.error(`${formatLabel('Error', ANSI.red)} Command not available: ${colorize(invalidCommand, ANSI.bold, ANSI.red)}`);
|
|
70
|
+
console.error('');
|
|
71
|
+
printHelp(console.error);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
printHelp();
|
|
75
|
+
process.exit(0);
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
async function promptForTitleAndDescription() {
|
|
@@ -88,8 +82,8 @@ async function promptForTitleAndDescription() {
|
|
|
88
82
|
});
|
|
89
83
|
|
|
90
84
|
try {
|
|
91
|
-
const titleContent = (await rl.question('
|
|
92
|
-
const descriptionContent = (await rl.question('
|
|
85
|
+
const titleContent = (await rl.question(`${formatLabel('Title', ANSI.yellow)} ${colorize('(leave blank to use project name)', ANSI.dim)}: `)).trim();
|
|
86
|
+
const descriptionContent = (await rl.question(`${formatLabel('Description', ANSI.yellow)} ${colorize('(leave blank to use default intro)', ANSI.dim)}: `)).trim();
|
|
93
87
|
return { titleContent, descriptionContent };
|
|
94
88
|
} finally {
|
|
95
89
|
rl.close();
|
|
@@ -97,7 +91,7 @@ async function promptForTitleAndDescription() {
|
|
|
97
91
|
}
|
|
98
92
|
|
|
99
93
|
async function main() {
|
|
100
|
-
console.log(
|
|
94
|
+
console.log(`${formatLabel('Info', ANSI.cyan)} Scanning for project files...`);
|
|
101
95
|
|
|
102
96
|
const targetDir = process.cwd();
|
|
103
97
|
const readmePath = path.join(targetDir, 'README.md');
|
|
@@ -110,28 +104,28 @@ async function main() {
|
|
|
110
104
|
const hasLicense = fs.existsSync(licensePath);
|
|
111
105
|
|
|
112
106
|
if (!readmeExists && !shouldInit && !shouldForce) {
|
|
113
|
-
console.error(
|
|
107
|
+
console.error(`${formatLabel('Error', ANSI.red)} No README.md found in this directory. Try ${colorize('--init', ANSI.bold, ANSI.yellow)}.`);
|
|
114
108
|
process.exit(1);
|
|
115
109
|
}
|
|
116
110
|
|
|
117
111
|
if (readmeExists && shouldInit && !shouldForce) {
|
|
118
|
-
console.error(
|
|
112
|
+
console.error(`${formatLabel('Error', ANSI.red)} README.md already exists. Try ${colorize('--force', ANSI.bold, ANSI.red)}.`);
|
|
119
113
|
process.exit(1);
|
|
120
114
|
}
|
|
121
115
|
|
|
122
116
|
if (!hasPackageJson && !hasRequirements) {
|
|
123
|
-
console.error(
|
|
117
|
+
console.error(`${formatLabel('Error', ANSI.red)} No ${colorize('package.json', ANSI.bold)} or ${colorize('requirements.txt', ANSI.bold)} found in this directory.`);
|
|
124
118
|
process.exit(1);
|
|
125
119
|
}
|
|
126
120
|
|
|
127
|
-
console.log(
|
|
121
|
+
console.log(`${formatLabel('Info', ANSI.cyan)} Files found. Processing README...`);
|
|
128
122
|
|
|
129
123
|
if (shouldForce && readmeExists) {
|
|
130
124
|
fs.unlinkSync(readmePath);
|
|
131
125
|
}
|
|
132
126
|
|
|
133
127
|
const readmeContent = fs.existsSync(readmePath) ? fs.readFileSync(readmePath, 'utf-8') : '';
|
|
134
|
-
const fileTree =
|
|
128
|
+
const fileTree = buildLocalFileTree(fs, path, targetDir, targetDir);
|
|
135
129
|
const projectName = path.basename(targetDir);
|
|
136
130
|
const licenseName = hasLicense ? getLicenseName(fs.readFileSync(licensePath, 'utf-8')) : '';
|
|
137
131
|
const shouldPromptMetadata = !shouldUpdate;
|
|
@@ -148,8 +142,8 @@ async function main() {
|
|
|
148
142
|
context = {
|
|
149
143
|
packageJson,
|
|
150
144
|
packages: [{ path: 'package.json', content: packageJson }],
|
|
151
|
-
dependencies:
|
|
152
|
-
scripts: collectScripts(packageJson),
|
|
145
|
+
dependencies: collectNodeDependencies([{ path: 'package.json', content: packageJson }]),
|
|
146
|
+
scripts: collectScripts([{ path: 'package.json', content: packageJson }]),
|
|
153
147
|
fileTree,
|
|
154
148
|
titleContent,
|
|
155
149
|
descriptionContent,
|
|
@@ -165,7 +159,7 @@ async function main() {
|
|
|
165
159
|
|
|
166
160
|
context = {
|
|
167
161
|
packages: [{ path: 'requirements.txt', content: requirementsContent }],
|
|
168
|
-
dependencies: collectPythonDependencies(requirementsContent),
|
|
162
|
+
dependencies: collectPythonDependencies([{ path: 'requirements.txt', content: requirementsContent }]),
|
|
169
163
|
scripts: new Map(),
|
|
170
164
|
fileTree,
|
|
171
165
|
titleContent,
|
|
@@ -183,11 +177,11 @@ async function main() {
|
|
|
183
177
|
|
|
184
178
|
fs.writeFileSync(readmePath, updatedReadme, 'utf-8');
|
|
185
179
|
|
|
186
|
-
console.log(
|
|
180
|
+
console.log(`${formatLabel('Success', ANSI.green)} README.md has been auto-fixed.`);
|
|
187
181
|
}
|
|
188
182
|
|
|
189
183
|
(main()
|
|
190
184
|
.catch(error => {
|
|
191
|
-
console.error(
|
|
185
|
+
console.error(`${formatLabel('Error', ANSI.red)} An error occurred during processing: ${error.message}`);
|
|
192
186
|
process.exit(1);
|
|
193
187
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blytz",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"bin": {
|
|
5
5
|
"blytz": "./bin/cli.js"
|
|
6
6
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"description": "An automated CLI tool to fix and maintain project READMEs",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"scripts": {
|
|
16
|
-
"start": "node
|
|
16
|
+
"start": "node server/server.js",
|
|
17
17
|
"prepublishOnly": "node scripts/sync-readme.js prepublish",
|
|
18
18
|
"postpublish": "node scripts/sync-readme.js postpublish"
|
|
19
19
|
},
|
package/src/processReadme.js
CHANGED
|
@@ -3,7 +3,7 @@ import getDefaultContent from "./template.js";
|
|
|
3
3
|
|
|
4
4
|
//Core engine: processes README content and returns updated version
|
|
5
5
|
export default function processReadme(content, projectType, context = {}) {
|
|
6
|
-
const { titleContent = "" } = context ?? {};
|
|
6
|
+
const { titleContent = "", descriptionContent = "", projectName = "" } = context ?? {};
|
|
7
7
|
|
|
8
8
|
const normalizedContent = (content || "").replace(/^##(?=\S)/gm, "## ");
|
|
9
9
|
|
|
@@ -11,9 +11,13 @@ export default function processReadme(content, projectType, context = {}) {
|
|
|
11
11
|
const sections = normalizedContent.split("## ");
|
|
12
12
|
const sectionMap = {};
|
|
13
13
|
|
|
14
|
-
// Extract intro (title + description)
|
|
14
|
+
// Extract intro (title + description)
|
|
15
15
|
const intro = sections[0].trim();
|
|
16
|
-
const
|
|
16
|
+
const resolvedTitle = titleContent || projectName || "";
|
|
17
|
+
const resolvedDescription = descriptionContent || getDefaultContent("description", projectType, context);
|
|
18
|
+
const finalIntro = resolvedTitle
|
|
19
|
+
? [`# ${resolvedTitle}`, resolvedDescription].filter(Boolean).join("\n\n")
|
|
20
|
+
: intro;
|
|
17
21
|
|
|
18
22
|
// Parse sections
|
|
19
23
|
sections.slice(1).forEach(section => {
|
|
@@ -24,14 +28,13 @@ export default function processReadme(content, projectType, context = {}) {
|
|
|
24
28
|
sectionMap[title] = body;
|
|
25
29
|
});
|
|
26
30
|
|
|
27
|
-
// Required sections
|
|
28
|
-
const requiredSections = [
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"license",
|
|
31
|
+
// Required sections
|
|
32
|
+
const requiredSections = [
|
|
33
|
+
"installation",
|
|
34
|
+
"usage",
|
|
35
|
+
"dependencies",
|
|
36
|
+
"folder structure",
|
|
37
|
+
"license",
|
|
35
38
|
"built by"
|
|
36
39
|
];
|
|
37
40
|
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
export const DEFAULT_IGNORED_NAMES = new Set([
|
|
2
|
+
"node_modules",
|
|
3
|
+
".git",
|
|
4
|
+
".agent",
|
|
5
|
+
"dist",
|
|
6
|
+
"build",
|
|
7
|
+
"out",
|
|
8
|
+
"target",
|
|
9
|
+
"venv",
|
|
10
|
+
".venv",
|
|
11
|
+
"__pycache__",
|
|
12
|
+
".idea",
|
|
13
|
+
".vscode",
|
|
14
|
+
".DS_Store",
|
|
15
|
+
"Thumbs.db",
|
|
16
|
+
".env",
|
|
17
|
+
".env.local",
|
|
18
|
+
".env.development",
|
|
19
|
+
".env.production"
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function collectNodeDependencies(packages = []) {
|
|
23
|
+
const dependencies = new Set();
|
|
24
|
+
|
|
25
|
+
for (const pkg of packages) {
|
|
26
|
+
for (const dependency of Object.keys(pkg?.content?.dependencies || {})) {
|
|
27
|
+
dependencies.add(dependency);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Array.from(dependencies).sort((left, right) => left.localeCompare(right));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function collectPythonDependencies(files = []) {
|
|
35
|
+
const dependencies = new Set();
|
|
36
|
+
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
if (!file?.content) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
file.content
|
|
43
|
+
.split(/\r?\n/)
|
|
44
|
+
.map(line => line.trim())
|
|
45
|
+
.filter(line => line && !line.startsWith("#"))
|
|
46
|
+
.filter(line => !line.startsWith("-"))
|
|
47
|
+
.map(line => line.split(";")[0].trim())
|
|
48
|
+
.map(line => line.split(/[=<>!~]/)[0].trim())
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.forEach(dependency => dependencies.add(dependency));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Array.from(dependencies).sort((left, right) => left.localeCompare(right));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function collectScripts(packages = []) {
|
|
57
|
+
const scripts = new Map();
|
|
58
|
+
|
|
59
|
+
for (const pkg of packages) {
|
|
60
|
+
const packageDir = pkg.path === "package.json" ? "(root)" : pkg.path.replace("/package.json", "");
|
|
61
|
+
|
|
62
|
+
for (const [name, command] of Object.entries(pkg?.content?.scripts || {})) {
|
|
63
|
+
|
|
64
|
+
if(name !=="start"){
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!scripts.has(name)) {
|
|
69
|
+
scripts.set(name, []);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
scripts.get(name).push({ package: packageDir, command });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return scripts;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getLicenseName(licenseContent = "") {
|
|
80
|
+
const firstLine = licenseContent
|
|
81
|
+
.split(/\r?\n/)
|
|
82
|
+
.map(line => line.trim())
|
|
83
|
+
.find(Boolean);
|
|
84
|
+
|
|
85
|
+
return firstLine || "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function escapeRegex(value) {
|
|
89
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function globToRegex(pattern) {
|
|
93
|
+
let regex = "";
|
|
94
|
+
|
|
95
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
96
|
+
const char = pattern[index];
|
|
97
|
+
const nextChar = pattern[index + 1];
|
|
98
|
+
|
|
99
|
+
if (char === "*") {
|
|
100
|
+
if (nextChar === "*") {
|
|
101
|
+
regex += ".*";
|
|
102
|
+
index += 1;
|
|
103
|
+
} else {
|
|
104
|
+
regex += "[^/]*";
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (char === "?") {
|
|
110
|
+
regex += "[^/]";
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
regex += escapeRegex(char);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return regex;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function parseGitignoreContent(content, baseDir = "") {
|
|
121
|
+
return content
|
|
122
|
+
.split(/\r?\n/)
|
|
123
|
+
.map(line => line.trim())
|
|
124
|
+
.filter(line => line && !line.startsWith("#"))
|
|
125
|
+
.map(line => {
|
|
126
|
+
const negated = line.startsWith("!");
|
|
127
|
+
const rawPattern = negated ? line.slice(1).trim() : line;
|
|
128
|
+
|
|
129
|
+
if (!rawPattern) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const directoryOnly = rawPattern.endsWith("/");
|
|
134
|
+
const normalizedPattern = rawPattern.replace(/\/+$/, "").replace(/\\/g, "/");
|
|
135
|
+
|
|
136
|
+
if (!normalizedPattern) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const anchored = normalizedPattern.startsWith("/");
|
|
141
|
+
const matcherPattern = anchored ? normalizedPattern.slice(1) : normalizedPattern;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
baseDir: baseDir.replace(/\\/g, "/"),
|
|
145
|
+
negated,
|
|
146
|
+
directoryOnly,
|
|
147
|
+
hasSlash: matcherPattern.includes("/"),
|
|
148
|
+
regex: new RegExp(`^${globToRegex(matcherPattern)}(?:/.*)?$`),
|
|
149
|
+
basenameRegex: new RegExp(`^${globToRegex(matcherPattern)}$`)
|
|
150
|
+
};
|
|
151
|
+
})
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function isIgnoredByGitignore(relativePath, isDirectory, gitignoreRules = []) {
|
|
156
|
+
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
157
|
+
let ignored = false;
|
|
158
|
+
|
|
159
|
+
for (const rule of gitignoreRules) {
|
|
160
|
+
if (rule.directoryOnly && !isDirectory) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (rule.baseDir && normalizedPath !== rule.baseDir && !normalizedPath.startsWith(`${rule.baseDir}/`)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const scopedPath = rule.baseDir
|
|
169
|
+
? normalizedPath.slice(rule.baseDir.length).replace(/^\/+/, "")
|
|
170
|
+
: normalizedPath;
|
|
171
|
+
const pathParts = scopedPath.split("/");
|
|
172
|
+
const matches = rule.hasSlash
|
|
173
|
+
? rule.regex.test(scopedPath)
|
|
174
|
+
: pathParts.some(part => rule.basenameRegex.test(part));
|
|
175
|
+
|
|
176
|
+
if (matches) {
|
|
177
|
+
ignored = !rule.negated;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return ignored;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function parseLocalGitignore(fs, gitignorePath, baseDir = "") {
|
|
185
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return parseGitignoreContent(fs.readFileSync(gitignorePath, "utf-8"), baseDir);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function buildLocalFileTree(fs, path, dirPath, rootDir = dirPath, inheritedGitignoreRules = [], depth = 0, maxDepth = 3) {
|
|
193
|
+
if (depth >= maxDepth) {
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const relativeDir = path.relative(rootDir, dirPath).replace(/\\/g, "/");
|
|
198
|
+
const localGitignoreRules = parseLocalGitignore(
|
|
199
|
+
fs,
|
|
200
|
+
path.join(dirPath, ".gitignore"),
|
|
201
|
+
relativeDir === "" ? "" : relativeDir
|
|
202
|
+
);
|
|
203
|
+
const gitignoreRules = [...inheritedGitignoreRules, ...localGitignoreRules];
|
|
204
|
+
const tree = {};
|
|
205
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
206
|
+
.filter(entry => {
|
|
207
|
+
if (DEFAULT_IGNORED_NAMES.has(entry.name)) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
212
|
+
const relativePath = path.relative(rootDir, entryPath);
|
|
213
|
+
|
|
214
|
+
return !isIgnoredByGitignore(relativePath, entry.isDirectory(), gitignoreRules);
|
|
215
|
+
})
|
|
216
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
217
|
+
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
220
|
+
|
|
221
|
+
if (entry.isDirectory()) {
|
|
222
|
+
tree[entry.name] = buildLocalFileTree(fs, path, entryPath, rootDir, gitignoreRules, depth + 1, maxDepth);
|
|
223
|
+
} else {
|
|
224
|
+
tree[entry.name] = null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return tree;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function buildRemoteFileTree(treeData, gitignoreRules = [], maxDepth = 3) {
|
|
232
|
+
const fileTree = {};
|
|
233
|
+
|
|
234
|
+
if (!treeData?.tree || !Array.isArray(treeData.tree)) {
|
|
235
|
+
return fileTree;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const item of treeData.tree) {
|
|
239
|
+
const parts = item.path.split("/");
|
|
240
|
+
const name = parts[parts.length - 1];
|
|
241
|
+
const isDirectory = item.type === "tree";
|
|
242
|
+
|
|
243
|
+
if (DEFAULT_IGNORED_NAMES.has(name) || isIgnoredByGitignore(item.path, isDirectory, gitignoreRules)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (parts.length > maxDepth) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let current = fileTree;
|
|
252
|
+
|
|
253
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
254
|
+
const part = parts[index];
|
|
255
|
+
const isFile = index === parts.length - 1 && item.type === "blob";
|
|
256
|
+
|
|
257
|
+
if (isFile) {
|
|
258
|
+
current[part] = null;
|
|
259
|
+
} else {
|
|
260
|
+
if (!current[part]) {
|
|
261
|
+
current[part] = {};
|
|
262
|
+
}
|
|
263
|
+
current = current[part];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return fileTree;
|
|
269
|
+
}
|
package/src/index.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
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";
|