blytz 1.1.0 → 1.2.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/README.md +3 -9
- package/bin/README-NPM.md +3 -9
- package/bin/cli.js +109 -92
- package/package.json +1 -1
- package/src/processReadme.js +20 -15
- package/src/readmeContext.js +264 -0
- package/src/template.js +17 -12
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
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import readline from 'readline/promises';
|
|
5
6
|
|
|
6
7
|
import processReadme from '../src/processReadme.js';
|
|
8
|
+
import {
|
|
9
|
+
buildLocalFileTree,
|
|
10
|
+
collectNodeDependencies,
|
|
11
|
+
collectPythonDependencies,
|
|
12
|
+
collectScripts,
|
|
13
|
+
getLicenseName
|
|
14
|
+
} from '../src/readmeContext.js';
|
|
7
15
|
|
|
8
16
|
const args = process.argv.slice(2);
|
|
9
17
|
const shouldShowHelp = args.includes('--help') || args.includes('-h');
|
|
@@ -12,113 +20,118 @@ const shouldForce = args.includes('--force');
|
|
|
12
20
|
const shouldUpdate = args.length === 0 || args.includes('--update');
|
|
13
21
|
const hasAction = shouldUpdate || shouldInit || shouldForce;
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
|
-
|
|
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;
|
|
40
|
+
return `${codes.join('')}${text}${ANSI.reset}`;
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
function
|
|
49
|
-
return
|
|
43
|
+
function formatLabel(label, color) {
|
|
44
|
+
return colorize(label, ANSI.bold, color);
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
.
|
|
61
|
-
|
|
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'));
|
|
62
59
|
}
|
|
63
60
|
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
if (shouldShowHelp) {
|
|
62
|
+
printHelp();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
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);
|
|
69
73
|
}
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
printHelp();
|
|
75
|
+
process.exit(0);
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
function
|
|
75
|
-
const
|
|
76
|
-
.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
async function promptForTitleAndDescription() {
|
|
79
|
+
const rl = readline.createInterface({
|
|
80
|
+
input: process.stdin,
|
|
81
|
+
output: process.stdout,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
try {
|
|
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();
|
|
87
|
+
return { titleContent, descriptionContent };
|
|
88
|
+
} finally {
|
|
89
|
+
rl.close();
|
|
90
|
+
}
|
|
81
91
|
}
|
|
82
92
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
async function main() {
|
|
94
|
+
console.log(`${formatLabel('Info', ANSI.cyan)} Scanning for project files...`);
|
|
95
|
+
|
|
96
|
+
const targetDir = process.cwd();
|
|
97
|
+
const readmePath = path.join(targetDir, 'README.md');
|
|
98
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
99
|
+
const requirementsPath = path.join(targetDir, 'requirements.txt');
|
|
100
|
+
const licensePath = path.join(targetDir, 'LICENSE');
|
|
101
|
+
const readmeExists = fs.existsSync(readmePath);
|
|
102
|
+
const hasPackageJson = fs.existsSync(packageJsonPath);
|
|
103
|
+
const hasRequirements = fs.existsSync(requirementsPath);
|
|
104
|
+
const hasLicense = fs.existsSync(licensePath);
|
|
105
|
+
|
|
106
|
+
if (!readmeExists && !shouldInit && !shouldForce) {
|
|
107
|
+
console.error(`${formatLabel('Error', ANSI.red)} No README.md found in this directory. Try ${colorize('--init', ANSI.bold, ANSI.yellow)}.`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
99
110
|
|
|
100
|
-
if (readmeExists && shouldInit && !shouldForce) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
111
|
+
if (readmeExists && shouldInit && !shouldForce) {
|
|
112
|
+
console.error(`${formatLabel('Error', ANSI.red)} README.md already exists. Try ${colorize('--force', ANSI.bold, ANSI.red)}.`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
104
115
|
|
|
105
|
-
if (!hasPackageJson && !hasRequirements) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
116
|
+
if (!hasPackageJson && !hasRequirements) {
|
|
117
|
+
console.error(`${formatLabel('Error', ANSI.red)} No ${colorize('package.json', ANSI.bold)} or ${colorize('requirements.txt', ANSI.bold)} found in this directory.`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
109
120
|
|
|
110
|
-
console.log(
|
|
121
|
+
console.log(`${formatLabel('Info', ANSI.cyan)} Files found. Processing README...`);
|
|
111
122
|
|
|
112
|
-
try {
|
|
113
123
|
if (shouldForce && readmeExists) {
|
|
114
124
|
fs.unlinkSync(readmePath);
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
// 1. Read the raw text of both files
|
|
118
127
|
const readmeContent = fs.existsSync(readmePath) ? fs.readFileSync(readmePath, 'utf-8') : '';
|
|
119
|
-
const fileTree =
|
|
128
|
+
const fileTree = buildLocalFileTree(fs, path, targetDir, targetDir);
|
|
120
129
|
const projectName = path.basename(targetDir);
|
|
121
130
|
const licenseName = hasLicense ? getLicenseName(fs.readFileSync(licensePath, 'utf-8')) : '';
|
|
131
|
+
const shouldPromptMetadata = !shouldUpdate;
|
|
132
|
+
const { titleContent, descriptionContent } = shouldPromptMetadata
|
|
133
|
+
? await promptForTitleAndDescription()
|
|
134
|
+
: { titleContent: '', descriptionContent: '' };
|
|
122
135
|
let context;
|
|
123
136
|
let projectType;
|
|
124
137
|
|
|
@@ -129,9 +142,11 @@ try {
|
|
|
129
142
|
context = {
|
|
130
143
|
packageJson,
|
|
131
144
|
packages: [{ path: 'package.json', content: packageJson }],
|
|
132
|
-
dependencies:
|
|
133
|
-
scripts: collectScripts(packageJson),
|
|
145
|
+
dependencies: collectNodeDependencies([{ path: 'package.json', content: packageJson }]),
|
|
146
|
+
scripts: collectScripts([{ path: 'package.json', content: packageJson }]),
|
|
134
147
|
fileTree,
|
|
148
|
+
titleContent,
|
|
149
|
+
descriptionContent,
|
|
135
150
|
licenseName,
|
|
136
151
|
username: packageJson.author || process.env.USERNAME || 'Unknown Author',
|
|
137
152
|
projectName: packageJson.name || projectName,
|
|
@@ -144,9 +159,11 @@ try {
|
|
|
144
159
|
|
|
145
160
|
context = {
|
|
146
161
|
packages: [{ path: 'requirements.txt', content: requirementsContent }],
|
|
147
|
-
dependencies: collectPythonDependencies(requirementsContent),
|
|
162
|
+
dependencies: collectPythonDependencies([{ path: 'requirements.txt', content: requirementsContent }]),
|
|
148
163
|
scripts: new Map(),
|
|
149
164
|
fileTree,
|
|
165
|
+
titleContent,
|
|
166
|
+
descriptionContent,
|
|
150
167
|
licenseName,
|
|
151
168
|
username: process.env.USERNAME || 'Unknown Author',
|
|
152
169
|
projectName,
|
|
@@ -156,15 +173,15 @@ try {
|
|
|
156
173
|
projectType = 'python';
|
|
157
174
|
}
|
|
158
175
|
|
|
159
|
-
// 3. Feed everything into your pure engine
|
|
160
176
|
const updatedReadme = processReadme(readmeContent, projectType, context);
|
|
161
177
|
|
|
162
|
-
// 4. Overwrite the existing README.md with the new content
|
|
163
178
|
fs.writeFileSync(readmePath, updatedReadme, 'utf-8');
|
|
164
179
|
|
|
165
|
-
console.log(
|
|
166
|
-
|
|
167
|
-
} catch (error) {
|
|
168
|
-
console.error("An error occurred during processing:", error.message);
|
|
169
|
-
process.exit(1);
|
|
180
|
+
console.log(`${formatLabel('Success', ANSI.green)} README.md has been auto-fixed.`);
|
|
170
181
|
}
|
|
182
|
+
|
|
183
|
+
(main()
|
|
184
|
+
.catch(error => {
|
|
185
|
+
console.error(`${formatLabel('Error', ANSI.red)} An error occurred during processing: ${error.message}`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}));
|
package/package.json
CHANGED
package/src/processReadme.js
CHANGED
|
@@ -2,16 +2,22 @@ import getDefaultContent from "./template.js";
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
//Core engine: processes README content and returns updated version
|
|
5
|
-
export default function processReadme(content, projectType, context = {}) {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export default function processReadme(content, projectType, context = {}) {
|
|
6
|
+
const { titleContent = "", descriptionContent = "", projectName = "" } = context ?? {};
|
|
7
|
+
|
|
8
|
+
const normalizedContent = (content || "").replace(/^##(?=\S)/gm, "## ");
|
|
8
9
|
|
|
9
10
|
// Split into sections
|
|
10
11
|
const sections = normalizedContent.split("## ");
|
|
11
12
|
const sectionMap = {};
|
|
12
13
|
|
|
13
|
-
// Extract intro (title + description)
|
|
14
|
-
const intro = sections[0].trim();
|
|
14
|
+
// Extract intro (title + description)
|
|
15
|
+
const intro = sections[0].trim();
|
|
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;
|
|
15
21
|
|
|
16
22
|
// Parse sections
|
|
17
23
|
sections.slice(1).forEach(section => {
|
|
@@ -22,14 +28,13 @@ export default function processReadme(content, projectType, context = {}) {
|
|
|
22
28
|
sectionMap[title] = body;
|
|
23
29
|
});
|
|
24
30
|
|
|
25
|
-
// Required sections
|
|
26
|
-
const requiredSections = [
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"license",
|
|
31
|
+
// Required sections
|
|
32
|
+
const requiredSections = [
|
|
33
|
+
"installation",
|
|
34
|
+
"usage",
|
|
35
|
+
"dependencies",
|
|
36
|
+
"folder structure",
|
|
37
|
+
"license",
|
|
33
38
|
"built by"
|
|
34
39
|
];
|
|
35
40
|
|
|
@@ -65,7 +70,7 @@ export default function processReadme(content, projectType, context = {}) {
|
|
|
65
70
|
.join(" ");
|
|
66
71
|
|
|
67
72
|
// Rebuild README
|
|
68
|
-
let newReadme =
|
|
73
|
+
let newReadme = finalIntro ? finalIntro + "\n\n" : "";
|
|
69
74
|
|
|
70
75
|
// Ordered sections
|
|
71
76
|
requiredSections.forEach(section => {
|
|
@@ -80,4 +85,4 @@ export default function processReadme(content, projectType, context = {}) {
|
|
|
80
85
|
});
|
|
81
86
|
|
|
82
87
|
return newReadme.trim();
|
|
83
|
-
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
if (!scripts.has(name)) {
|
|
64
|
+
scripts.set(name, []);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
scripts.get(name).push({ package: packageDir, command });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return scripts;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getLicenseName(licenseContent = "") {
|
|
75
|
+
const firstLine = licenseContent
|
|
76
|
+
.split(/\r?\n/)
|
|
77
|
+
.map(line => line.trim())
|
|
78
|
+
.find(Boolean);
|
|
79
|
+
|
|
80
|
+
return firstLine || "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function escapeRegex(value) {
|
|
84
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function globToRegex(pattern) {
|
|
88
|
+
let regex = "";
|
|
89
|
+
|
|
90
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
91
|
+
const char = pattern[index];
|
|
92
|
+
const nextChar = pattern[index + 1];
|
|
93
|
+
|
|
94
|
+
if (char === "*") {
|
|
95
|
+
if (nextChar === "*") {
|
|
96
|
+
regex += ".*";
|
|
97
|
+
index += 1;
|
|
98
|
+
} else {
|
|
99
|
+
regex += "[^/]*";
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (char === "?") {
|
|
105
|
+
regex += "[^/]";
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
regex += escapeRegex(char);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return regex;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function parseGitignoreContent(content, baseDir = "") {
|
|
116
|
+
return content
|
|
117
|
+
.split(/\r?\n/)
|
|
118
|
+
.map(line => line.trim())
|
|
119
|
+
.filter(line => line && !line.startsWith("#"))
|
|
120
|
+
.map(line => {
|
|
121
|
+
const negated = line.startsWith("!");
|
|
122
|
+
const rawPattern = negated ? line.slice(1).trim() : line;
|
|
123
|
+
|
|
124
|
+
if (!rawPattern) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const directoryOnly = rawPattern.endsWith("/");
|
|
129
|
+
const normalizedPattern = rawPattern.replace(/\/+$/, "").replace(/\\/g, "/");
|
|
130
|
+
|
|
131
|
+
if (!normalizedPattern) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const anchored = normalizedPattern.startsWith("/");
|
|
136
|
+
const matcherPattern = anchored ? normalizedPattern.slice(1) : normalizedPattern;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
baseDir: baseDir.replace(/\\/g, "/"),
|
|
140
|
+
negated,
|
|
141
|
+
directoryOnly,
|
|
142
|
+
hasSlash: matcherPattern.includes("/"),
|
|
143
|
+
regex: new RegExp(`^${globToRegex(matcherPattern)}(?:/.*)?$`),
|
|
144
|
+
basenameRegex: new RegExp(`^${globToRegex(matcherPattern)}$`)
|
|
145
|
+
};
|
|
146
|
+
})
|
|
147
|
+
.filter(Boolean);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function isIgnoredByGitignore(relativePath, isDirectory, gitignoreRules = []) {
|
|
151
|
+
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
152
|
+
let ignored = false;
|
|
153
|
+
|
|
154
|
+
for (const rule of gitignoreRules) {
|
|
155
|
+
if (rule.directoryOnly && !isDirectory) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (rule.baseDir && normalizedPath !== rule.baseDir && !normalizedPath.startsWith(`${rule.baseDir}/`)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const scopedPath = rule.baseDir
|
|
164
|
+
? normalizedPath.slice(rule.baseDir.length).replace(/^\/+/, "")
|
|
165
|
+
: normalizedPath;
|
|
166
|
+
const pathParts = scopedPath.split("/");
|
|
167
|
+
const matches = rule.hasSlash
|
|
168
|
+
? rule.regex.test(scopedPath)
|
|
169
|
+
: pathParts.some(part => rule.basenameRegex.test(part));
|
|
170
|
+
|
|
171
|
+
if (matches) {
|
|
172
|
+
ignored = !rule.negated;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return ignored;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function parseLocalGitignore(fs, gitignorePath, baseDir = "") {
|
|
180
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return parseGitignoreContent(fs.readFileSync(gitignorePath, "utf-8"), baseDir);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function buildLocalFileTree(fs, path, dirPath, rootDir = dirPath, inheritedGitignoreRules = [], depth = 0, maxDepth = 3) {
|
|
188
|
+
if (depth >= maxDepth) {
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const relativeDir = path.relative(rootDir, dirPath).replace(/\\/g, "/");
|
|
193
|
+
const localGitignoreRules = parseLocalGitignore(
|
|
194
|
+
fs,
|
|
195
|
+
path.join(dirPath, ".gitignore"),
|
|
196
|
+
relativeDir === "" ? "" : relativeDir
|
|
197
|
+
);
|
|
198
|
+
const gitignoreRules = [...inheritedGitignoreRules, ...localGitignoreRules];
|
|
199
|
+
const tree = {};
|
|
200
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
201
|
+
.filter(entry => {
|
|
202
|
+
if (DEFAULT_IGNORED_NAMES.has(entry.name)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
207
|
+
const relativePath = path.relative(rootDir, entryPath);
|
|
208
|
+
|
|
209
|
+
return !isIgnoredByGitignore(relativePath, entry.isDirectory(), gitignoreRules);
|
|
210
|
+
})
|
|
211
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
212
|
+
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
215
|
+
|
|
216
|
+
if (entry.isDirectory()) {
|
|
217
|
+
tree[entry.name] = buildLocalFileTree(fs, path, entryPath, rootDir, gitignoreRules, depth + 1, maxDepth);
|
|
218
|
+
} else {
|
|
219
|
+
tree[entry.name] = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return tree;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function buildRemoteFileTree(treeData, gitignoreRules = [], maxDepth = 3) {
|
|
227
|
+
const fileTree = {};
|
|
228
|
+
|
|
229
|
+
if (!treeData?.tree || !Array.isArray(treeData.tree)) {
|
|
230
|
+
return fileTree;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const item of treeData.tree) {
|
|
234
|
+
const parts = item.path.split("/");
|
|
235
|
+
const name = parts[parts.length - 1];
|
|
236
|
+
const isDirectory = item.type === "tree";
|
|
237
|
+
|
|
238
|
+
if (DEFAULT_IGNORED_NAMES.has(name) || isIgnoredByGitignore(item.path, isDirectory, gitignoreRules)) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (parts.length > maxDepth) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let current = fileTree;
|
|
247
|
+
|
|
248
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
249
|
+
const part = parts[index];
|
|
250
|
+
const isFile = index === parts.length - 1 && item.type === "blob";
|
|
251
|
+
|
|
252
|
+
if (isFile) {
|
|
253
|
+
current[part] = null;
|
|
254
|
+
} else {
|
|
255
|
+
if (!current[part]) {
|
|
256
|
+
current[part] = {};
|
|
257
|
+
}
|
|
258
|
+
current = current[part];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return fileTree;
|
|
264
|
+
}
|
package/src/template.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import getProjectStructure from "./fileTree.js";
|
|
2
2
|
|
|
3
3
|
export default function getDefaultContent(section, projectType, context = {}) {
|
|
4
|
-
const {
|
|
5
|
-
packages = [],
|
|
6
|
-
dependencies = [],
|
|
7
|
-
scripts = new Map(),
|
|
4
|
+
const {
|
|
5
|
+
packages = [],
|
|
6
|
+
dependencies = [],
|
|
7
|
+
scripts = new Map(),
|
|
8
8
|
fileTree = null,
|
|
9
|
+
descriptionContent = "",
|
|
9
10
|
licenseName = "",
|
|
10
11
|
username = "Unknown",
|
|
11
|
-
projectName = "this project",
|
|
12
|
-
isMonorepo = false,
|
|
13
|
-
} = context ?? {};
|
|
12
|
+
projectName = "this project",
|
|
13
|
+
isMonorepo = false,
|
|
14
|
+
} = context ?? {};
|
|
14
15
|
|
|
15
16
|
const safeProjectType = projectType || "unknown";
|
|
16
17
|
const safeSection = (section || "").toLowerCase().trim();
|
|
17
18
|
|
|
18
|
-
switch (safeSection) {
|
|
19
|
-
case "description":
|
|
20
|
-
return getDescriptionContent(safeProjectType, projectName, isMonorepo);
|
|
19
|
+
switch (safeSection) {
|
|
20
|
+
case "description":
|
|
21
|
+
return getDescriptionContent(safeProjectType, projectName, isMonorepo, descriptionContent);
|
|
21
22
|
case "installation":
|
|
22
23
|
return getInstallationContent(safeProjectType, packages, isMonorepo);
|
|
23
24
|
case "usage":
|
|
@@ -35,8 +36,12 @@ export default function getDefaultContent(section, projectType, context = {}) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
function getDescriptionContent(projectType, projectName, isMonorepo) {
|
|
39
|
-
|
|
39
|
+
function getDescriptionContent(projectType, projectName, isMonorepo, descriptionContent) {
|
|
40
|
+
if (descriptionContent) {
|
|
41
|
+
return descriptionContent;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const name = projectName || "this project";
|
|
40
45
|
if (projectType === "node") {
|
|
41
46
|
if (isMonorepo) {
|
|
42
47
|
return `${name} is a Node.js monorepo containing multiple packages. Add a brief description of its purpose and what problem it solves.`;
|