blytz 1.1.1 → 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 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
- if (shouldShowHelp) {
17
- console.log('Usage: blytz [--update|--init|--force]');
18
- process.exit(0);
19
- }
20
-
21
- if (!hasAction) {
22
- console.log('Usage: blytz [--update|--init|--force]');
23
- process.exit(0);
24
- }
25
-
26
- function buildFileTree(dirPath, depth = 0, maxDepth = 4) {
27
- if (depth >= maxDepth) {
28
- return {};
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 tree;
40
+ return `${codes.join('')}${text}${ANSI.reset}`;
47
41
  }
48
42
 
49
- function collectDependencies(packageJson) {
50
- return Object.keys(packageJson.dependencies || {}).sort();
43
+ function formatLabel(label, color) {
44
+ return colorize(label, ANSI.bold, color);
51
45
  }
52
46
 
53
- function collectPythonDependencies(requirementsContent) {
54
- return requirementsContent
55
- .split(/\r?\n/)
56
- .map(line => line.trim())
57
- .filter(line => line && !line.startsWith('#'))
58
- .filter(line => !line.startsWith('-'))
59
- .map(line => line.split(';')[0].trim())
60
- .map(line => line.split(/[=<>!~]/)[0].trim())
61
- .filter(Boolean)
62
- .sort((left, right) => left.localeCompare(right));
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
- function collectScripts(packageJson) {
66
- const scripts = new Map();
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
- function getLicenseName(licenseContent) {
76
- const firstLine = licenseContent
77
- .split(/\r?\n/)
78
- .map(line => line.trim())
79
- .find(Boolean);
80
-
81
- return firstLine || '';
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('Add title: ')).trim();
92
- const descriptionContent = (await rl.question('Add description: ')).trim();
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("Scanning for project files...");
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("Error: No README.md found in this directory. Try --init.");
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("README.md already exists. Try --force.");
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("Error: No package.json or requirements.txt found in this directory.");
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("Files found. Processing README...");
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 = buildFileTree(targetDir);
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: collectDependencies(packageJson),
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("Success! README.md has been auto-fixed.");
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("An error occurred during processing:", error.message);
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.1.1",
3
+ "version": "1.2.0",
4
4
  "bin": {
5
5
  "blytz": "./bin/cli.js"
6
6
  },
@@ -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 finalIntro = titleContent ? `# ${titleContent}` : intro;
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
- "description",
30
- "installation",
31
- "usage",
32
- "dependencies",
33
- "folder structure",
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,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
+ }