gennady 0.2.2 β†’ 0.5.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.
Files changed (34) hide show
  1. package/README.md +107 -10
  2. package/cli/cmd/cat.js +26 -0
  3. package/cli/cmd/commit.js +83 -0
  4. package/cli/cmd/review.js +58 -0
  5. package/cli/gennady.js +19 -40
  6. package/index.js +2 -0
  7. package/package.json +5 -3
  8. package/src/ai/ai-core.js +187 -0
  9. package/src/cat-gen/cat-gen.js +53 -0
  10. package/src/commit-gen/commit-gen.js +38 -71
  11. package/src/git/git-core.js +69 -0
  12. package/src/git/git-diff.js +36 -13
  13. package/src/prompts/commit/commit-base-prompt.md +38 -0
  14. package/src/prompts/commit/commit-format-detailed-prompt.md +29 -0
  15. package/src/prompts/commit/commit-format-oneline-prompt.md +19 -0
  16. package/src/prompts/commit/commit-translate-prompt.md +15 -0
  17. package/src/prompts/index.js +10 -0
  18. package/src/prompts/review/review-base-prompt.md +46 -0
  19. package/src/review-gen/__fixture__/review-gen-fixture.md +110 -0
  20. package/src/review-gen/review-gen.js +119 -0
  21. package/src/review-gen/review-gen.test.js +79 -0
  22. package/src/review-gen/specs/js/Function.prototype.json +162 -0
  23. package/src/review-gen/specs/js/Global.json +219 -0
  24. package/src/review-gen/specs/js/JSON.json +85 -0
  25. package/src/review-gen/specs/js/Object.json +626 -0
  26. package/src/review-gen/specs/js/Object.prototype.json +337 -0
  27. package/src/review-gen/specs/js/Storage.json +97 -0
  28. package/src/utils/parse-args.js +7 -3
  29. package/src/utils/style.js +12 -3
  30. package/src/git/git-cmd.js +0 -32
  31. package/src/prompts/base-prompt.md +0 -27
  32. package/src/prompts/format-detailed-prompt.md +0 -16
  33. package/src/prompts/format-oneline-prompt.md +0 -11
  34. package/src/prompts/translate-prompt.md +0 -11
package/README.md CHANGED
@@ -1,26 +1,40 @@
1
1
  πŸ€– Gennadyᡇᡉᡗᡃ πŸ—―οΈ
2
2
  -----------------
3
- **Gen**erate **N**ext-level **A**utomated **D**escription **Y**ntelligence.
3
+ **GEN**erate **N**ext-level **A**utomated **D**escription **Y**ntelligence.
4
4
 
5
5
  ```bash
6
+ # Commit message
6
7
  npx gennady
8
+
9
+ # Code review for critical issues
10
+ npx gennady review
11
+
12
+ # Quickly display the contents
13
+ npx gennady cat <path1> <path2> ...
7
14
  ```
8
15
 
9
16
  ---
10
17
 
11
- ### Setup Local LLM
18
+ ### ✨ Features
12
19
 
13
- ```sh
14
- brew install ollama
15
- ollama pull llama3:8b
16
- ollama serve
17
- ```
20
+ - πŸ€– [**Commit Message**](#-commit-messages): Automatically generate clear, descriptive git commit messages from your staged changes.
21
+ - πŸ“ [**review**](#-review): Instantly review your staged git changes for critical issues (logic, runtime, security).
22
+ - 🐱 [**cat**](#-cat): Quickly display the contents of multiple files or directories at once, filtered by allowed extensions (default: .js, .ts, .tsx).
18
23
 
19
24
  ---
20
25
 
21
- ### Usage
26
+ ## πŸ”– Usage Overview
22
27
 
23
- ```bash
28
+ Gennady provides several main CLI commands:
29
+ - `npx gennady` β€” Generate commit messages from your staged git changes.
30
+ - `npx gennady cat <path1> <path2> ...` β€” Display the contents of one or more files or directories, filtered by allowed extensions.
31
+ - `npx gennady review` β€” Review your staged git changes for critical issues.
32
+
33
+ ---
34
+
35
+ ## πŸ€– Commit Messages
36
+
37
+ ```sh
24
38
  # Basic usage
25
39
  npx gennady
26
40
 
@@ -32,4 +46,87 @@ npx gennady --mode=detailed
32
46
 
33
47
  # Generate detailed commit message relative to the target branch
34
48
  npx gennady --branch=develop
35
- ```
49
+ ```
50
+
51
+ ### Options
52
+ | Option | Alias(es) | Description |
53
+ |-------------------|------------------|----------------------------------------------|
54
+ | `--mode` | `-m` | Set the mode (`auto`, `oneline`, `detailed`) |
55
+ | `--oneline` | `--short`, `-o` | Generate a one-line commit message |
56
+ | `--model` | | Specify the AI model |
57
+ | `--branch` | `-b` | Target branch for diff |
58
+ | `--apply` | | Immediately apply the generated commit message to git |
59
+
60
+
61
+ #### What Happens?
62
+ - Gennady analyzes your staged changes.
63
+ - It generates a commit message.
64
+ - If your system language isn't English, it translates the message for you.
65
+
66
+ ---
67
+
68
+
69
+ ## πŸ“ review
70
+
71
+ Review your staged git changes for critical issues.
72
+
73
+ ```sh
74
+ npx gennady review
75
+
76
+ # Review changes relative to a specific branch
77
+ npx gennady review --branch=develop
78
+ ```
79
+
80
+ #### What Happens?
81
+ - Gennady analyzes your staged changes.
82
+ - It checks only the lines added or modified in your diff for critical issues (logic, runtime, and security errors).
83
+ - If no critical issues are found, it outputs `GOOD`.
84
+ - If issues are found, they are listed in a clear, structured format.
85
+
86
+ ---
87
+
88
+ ## 🐱 cat
89
+
90
+ Display the contents of files or directories (with filtering for allowed extensions).
91
+
92
+ ```sh
93
+ npx gennady cat ./src/
94
+ ```
95
+
96
+ #### Output
97
+ - Shows file contents with headers per file.
98
+ - Hints for copying output without color codes.
99
+
100
+ ---
101
+
102
+ ## Setup LLM
103
+
104
+ ### Local
105
+
106
+ ```sh
107
+ brew install ollama
108
+ ollama pull llama3:8b
109
+ ollama serve
110
+ ```
111
+
112
+ ---
113
+
114
+ ### External
115
+
116
+ Create `~/.gennadyrc` configuration file:
117
+
118
+ ```json
119
+ [
120
+ {
121
+ "url": "https://api.openai.com/v1/chat/completions",
122
+ "key": "...",
123
+ "model": "gpt-3.5-turbo-0125"
124
+ }
125
+ ]
126
+ ```
127
+
128
+ ---
129
+
130
+ ## πŸŽ‰ Happy Coding with Gennady!
131
+
132
+ > Made with πŸ€– by Konstantin Lebedev
package/cli/cmd/cat.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'path';
4
+ import { catGen } from '../../src/cat-gen/cat-gen.js';
5
+ import { style } from '../../src/utils/style.js';
6
+ import { parseArgs } from '../../src/utils/parse-args.js';
7
+
8
+ //
9
+ // 🐱 CAT-GEN
10
+ //
11
+ const args = parseArgs(process.argv);
12
+
13
+ if (args._.length === 0) {
14
+ console.error(style.yellow('Usage: npx gennady cat <path1> <path2> ...'));
15
+ process.exit(1);
16
+ }
17
+
18
+ catGen(args._).forEach(({ relativePath, content }) => {
19
+ console.log(style.blue(`#### ${relativePath}`));
20
+ console.log(content);
21
+ console.log('');
22
+ });
23
+
24
+ console.log(style.green(`^`.repeat(40)));
25
+ console.log(style.italic.gray(`Hint: npx gennady cat ${process.argv.slice(3).join(' ')} --plain | pbcopy`));
26
+ console.log('');
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { execSync } from 'node:child_process';
7
+ import { CommitGen } from '../../src/commit-gen/commit-gen.js';
8
+ import { parseArgs } from '../../src/utils/parse-args.js';
9
+ import { style } from '../../src/utils/style.js';
10
+ import { getSysLang } from '../../src/utils/language.js';
11
+
12
+ //
13
+ // πŸ€– COMMIT-GEN πŸ’¬
14
+ //
15
+
16
+ const CACHE_PATH = path.join(os.homedir(), '.gennady_commit_cache.json');
17
+ const PROJECT_KEY = process.cwd();
18
+ const COMMIT_CACHE = readCache();
19
+
20
+ const params = parseArgs(process.argv, {
21
+ apply: ['apply'],
22
+ mode: ['mode', 'm'],
23
+ oneline: ['short', 'one', 'o'],
24
+ model: ['model'],
25
+ targetBranch: ['branch', 'b'],
26
+ apiUrl: ['api', 'apiUrl'],
27
+ });
28
+
29
+ const commitGen = new CommitGen(params);
30
+
31
+ console.info(
32
+ 'πŸ€–',
33
+ style.whiteBright.bold('GENNADY'),
34
+ `(${style.cyan(commitGen.model)} β†’ ${style.yellow(commitGen.mode)})`,
35
+ 'πŸ—―οΈ',
36
+ );
37
+
38
+ console.info(style.gray('-'.repeat(40)));
39
+ console.info(`- url: ${style.blue(commitGen.apiUrl)}`);
40
+ console.info(style.gray('-'.repeat(40)));
41
+
42
+ const commitMessage = params.apply && COMMIT_CACHE[PROJECT_KEY] || await commitGen.generate();
43
+ if (!commitMessage) {
44
+ process.exit(0);
45
+ }
46
+
47
+ if (params.apply) {
48
+ // APPLY COMMIT
49
+ execSync(
50
+ `git commit -am "${commitMessage.replace(/"/g, '\"')}"`,
51
+ {stdio: 'inherit'},
52
+ );
53
+ saveCache({ [PROJECT_KEY]: undefined });
54
+ } else {
55
+ // GENERATE COMMIT
56
+ console.info('-'.repeat(40), '\n');
57
+ console.info(style.whiteBright(commitMessage), '\n');
58
+ console.info('^'.repeat(40), '\n');
59
+
60
+ const lang = getSysLang();
61
+ if (lang !== 'en') {
62
+ console.info(await commitGen.translate(commitMessage, lang), '\n');
63
+ console.info('^'.repeat(40), '\n');
64
+ }
65
+
66
+ console.log(style.italic.gray(`Hint: npx gennady ${process.argv.slice(3).join(' ')} --apply`));
67
+ console.log('');
68
+
69
+ saveCache({ [PROJECT_KEY]: commitMessage });
70
+ }
71
+
72
+ function readCache() {
73
+ try {
74
+ return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
75
+ } catch {
76
+ return {};
77
+ }
78
+ }
79
+
80
+ function saveCache(patch) {
81
+ const next = { ...COMMIT_CACHE, ...patch };
82
+ fs.writeFileSync(CACHE_PATH, JSON.stringify(next, null, 2));
83
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { getGitDiffInfo } from '../../src/git/git-core.js';
4
+ import { ReviewGen } from '../../src/review-gen/review-gen.js';
5
+ import { parseArgs } from '../../src/utils/parse-args.js';
6
+ import { style } from '../../src/utils/style.js';
7
+
8
+ //
9
+ // πŸ“ REVIEW-GEN
10
+ //
11
+
12
+ const params = parseArgs(process.argv, {
13
+ branch: ['branch', 'b'],
14
+ });
15
+
16
+ const review = new ReviewGen();
17
+
18
+ console.info(
19
+ `πŸ€–`,
20
+ style.whiteBright.bold(`GENNADY`),
21
+ `(${style.cyan(review.ai.model)})`,
22
+ `πŸ“`,
23
+ );
24
+
25
+ console.info(style.gray(`-`.repeat(40)));
26
+
27
+ const {
28
+ parsedCodeDiff,
29
+ parsedCodeTokens,
30
+ programmingLanguages,
31
+ parsedCodeChunkMaxTokens,
32
+ } = getGitDiffInfo(params.branch);
33
+
34
+ console.info(`- Tokens: ${style.bold.cyanBright(parsedCodeTokens)} ${style.gray(`(max per file: ${parsedCodeChunkMaxTokens})`)}`);
35
+ console.info(`- Languages: ${style.yellow(programmingLanguages)}`);
36
+
37
+ if (parsedCodeDiff.length === 0) {
38
+ console.info(`No changes detected, skipping review.`);
39
+ console.info(style.italic.gray(`Hint: git add`));
40
+ process.exit(0);
41
+ }
42
+
43
+ const batches = review.ai.createPromptsBatchesByDiff(parsedCodeDiff);
44
+
45
+ console.info(`- Queue: ${style.bold.cyan(batches.length)}`);
46
+ console.info(style.gray(`-`.repeat(40)));
47
+
48
+ const startGenTime = performance.now();
49
+ const results = await Promise.all(batches.map(async (batch) => {
50
+ console.info(`- Task:`, batch.tokens, batch.languages);
51
+ return await review.generate(batch.diff, batch.languages);
52
+ }));
53
+
54
+ console.info(style.gray(`-`.repeat(40)));
55
+ console.info(`- Generation time: ${style.blueBright(((performance.now() - startGenTime) / 1000).toFixed(2))}s`);
56
+ console.info(style.gray(`-`.repeat(40)));
57
+
58
+ console.info(results.join('\n\n'));
package/cli/gennady.js CHANGED
@@ -1,45 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync } from 'fs';
4
- import { fileURLToPath } from 'url';
5
- import { dirname, join } from 'path';
6
- import { CommitGen } from '../src/commit-gen/commit-gen.js';
7
- import { parseArgs } from '../src/utils/parse-args.js';
8
- import { style } from '../src/utils/style.js';
9
- import { getSysLang } from '../src/utils/language.js';
3
+ switch(process.argv[2]) {
4
+ //
5
+ // 🐱 CAT-GEN
6
+ //
7
+ case 'cat':
8
+ import('./cmd/cat.js');
9
+ break;
10
10
 
11
- const PROMPTS_DIR = join(
12
- typeof __dirname !== 'string' ? dirname(fileURLToPath(import.meta.url)) : __dirname,
13
- '../src/prompts',
14
- );
11
+ //
12
+ // πŸ“ REVIEW-GEN
13
+ //
14
+ case 'review':
15
+ import('./cmd/review.js');
16
+ break;
15
17
 
16
- const params = parseArgs(process.argv, {
17
- mode: ['mode', 'm'],
18
- oneline: ['short', 'one', 'o'],
19
- reviewerModel: ['model'],
20
- targetBranch: ['branch', 'b'],
21
- });
22
-
23
- const commit = new CommitGen({
24
- ...params,
25
- basePromptTemplate: readFileSync(join(PROMPTS_DIR, 'base-prompt.md')).toString(),
26
- formatOnelinePromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-oneline-prompt.md')).toString(),
27
- formatDetailedPromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-detailed-prompt.md')).toString(),
28
- translatePromptTemplate: readFileSync(join(PROMPTS_DIR, 'translate-prompt.md')).toString(),
29
- });
30
-
31
- console.info(`πŸ€–`, style.whiteBright.bold(`GENNADY`), `(${style.cyan(commit.reviewerModel)} β†’ ${style.yellow(commit.mode)})`, `πŸ—―οΈ`);
32
- console.info(style.gray(`-`.repeat(30)));
33
-
34
- const msg = await commit.generate();
35
- if (msg) {
36
- console.info(`-`.repeat(40), '\n');
37
- console.info(style.whiteBright(msg), '\n');
38
- console.info(`^`.repeat(40), '\n');
39
-
40
- const lang = getSysLang();
41
- if (lang !== 'en') {
42
- console.info(await commit.translate(msg, lang), '\n');
43
- console.info(`^`.repeat(40), '\n');
44
- }
18
+ //
19
+ // πŸ€– COMMIT-GEN πŸ’¬
20
+ //
21
+ default:
22
+ import('./cmd/commit.js');
23
+ break;
45
24
  }
package/index.js CHANGED
@@ -1 +1,3 @@
1
1
  export * from './src/commit-gen/commit-gen.js';
2
+ export * from './src/git/git-diff.js';
3
+ export * from './src/utils/language.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gennady",
3
- "version": "0.2.2",
3
+ "version": "0.5.0",
4
4
  "author": "Konstantin Lebedev <ibnrubaxa@gmail.com>",
5
5
  "description": "Gennady β€” Generate Next-level Automated Description Yntelligence",
6
6
  "keywords": [
@@ -10,7 +10,9 @@
10
10
  "local",
11
11
  "commit",
12
12
  "generator",
13
- "gennady"
13
+ "gennady",
14
+ "review",
15
+ "code-review"
14
16
  ],
15
17
  "license": "MIT",
16
18
  "repository": "https://github.com/rubaxa/gennady",
@@ -18,6 +20,6 @@
18
20
  "bin": "./cli/gennady.js",
19
21
  "main": "index.js",
20
22
  "scripts": {
21
- "test": "echo \"Error: no test specified\" && exit 1"
23
+ "test": "node --test"
22
24
  }
23
25
  }
@@ -0,0 +1,187 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const DEFAULT_MODEL = 'llama3:8b';
5
+ const DEFAULT_API_URL = 'http://127.0.0.1:11434/api/generate';
6
+
7
+ const GENNADY_RC_FILENAME = '.gennadyrc';
8
+
9
+ export class AiCore {
10
+ api = undefined;
11
+ apiList = [];
12
+
13
+ constructor(init) {
14
+ this.init = {
15
+ logger: console,
16
+ timeout: 120,
17
+ maxInputTokens: init.maxInputTokens || 4000,
18
+ ...init,
19
+ };
20
+
21
+ // Try parse rc files
22
+ [
23
+ join(process.cwd(), GENNADY_RC_FILENAME),
24
+ join(process.env.HOME, GENNADY_RC_FILENAME),
25
+ ].find((file) => {
26
+ try {
27
+ if (existsSync(file)) {
28
+ const items = JSON.parse(readFileSync(file).toString());
29
+ if (Array.isArray(items)) {
30
+ this.apiList.push(...items);
31
+ } else {
32
+ this.logger.warn(`Invalid "${file}" config:`, items);
33
+ }
34
+ }
35
+ } catch (err) {
36
+ this.logger.error(`Parse "${file}" error:`, err);
37
+ }
38
+ });
39
+
40
+ // Default API
41
+ this.apiList[this.init.apiUrl ? 'unshift' : 'push']({
42
+ url: this.init.url || DEFAULT_API_URL,
43
+ key: this.init.key,
44
+ model: this.init.model || DEFAULT_MODEL,
45
+ });
46
+ }
47
+
48
+ get model() {
49
+ return this.api?.model || this.apiList[0].model;
50
+ }
51
+
52
+ get apiUrl() {
53
+ return this.api?.url || this.apiList[0].url;
54
+ }
55
+
56
+ get maxInputTokens() {
57
+ return this.init.maxInputTokens;
58
+ }
59
+
60
+ get logger() {
61
+ return this.init.logger;
62
+ }
63
+
64
+ createPromptsBatchesByDiff(parseDiff) {
65
+ const maxChunkTokens = parseDiff.at(-1)?.tokens || 0
66
+
67
+ if (maxChunkTokens > this.maxInputTokens) {
68
+ this.logger.error(`TODO: Diff is too large`);
69
+ process.exit(1);
70
+ }
71
+
72
+ const batches = parseDiff.reduce((acc, file) => {
73
+ if (!acc[0] || acc[0].tokens + file.tokens > this.maxInputTokens) {
74
+ acc.unshift({tokens: 0, diff: '', languages: []});
75
+ }
76
+
77
+ const fileDiff = file.diff.hunks.flatMap(h => h.changes).join('\n');
78
+ if (fileDiff.trim()) {
79
+ acc[0].tokens += file.tokens;
80
+ acc[0].diff += `### File **${file.filename}**:\n${fileDiff}\n\n`;
81
+
82
+ if (!acc[0].languages.includes(file.programmingLanguage)) {
83
+ acc[0].languages.push(file.programmingLanguage);
84
+ }
85
+ }
86
+
87
+ return acc;
88
+ }, []);
89
+
90
+ return batches;
91
+ }
92
+
93
+ async generate(prompt, context) {
94
+ try {
95
+ const api = await this._getApi();
96
+ if (api.url.includes('completions')) {
97
+ return await this._callCompletionsApi(api, prompt, context);
98
+ }
99
+
100
+ return await this._callGenerateApi(api, prompt, context);
101
+ } catch (e) {
102
+ this.logger.error(`Failed to generate LLM response:`, e);
103
+ return '';
104
+ }
105
+ }
106
+
107
+ async _getApi() {
108
+ if (!this.api) {
109
+ // By default
110
+ this.api = {url: DEFAULT_API_URL, model: DEFAULT_MODEL};
111
+
112
+ for (const api of this.apiList) {
113
+ try {
114
+ const ctrl = new AbortController();
115
+
116
+ setTimeout(() => ctrl.abort(new Error('Module timeout')), 500);
117
+
118
+ const resp = await fetch(api.url, {method: 'HEAD', signal: ctrl.signal});
119
+ if (resp.status >= 200 && resp.status < 500) {
120
+ this.api = api;
121
+ return api;
122
+ }
123
+ } catch {}
124
+ }
125
+ }
126
+
127
+ return this.api;
128
+ }
129
+
130
+ async _callGenerateApi(api, prompt, context) {
131
+ const req = await fetch(api.url, {
132
+ method: 'POST',
133
+ headers: {'Content-Type': 'application/json'},
134
+ body: JSON.stringify({
135
+ model: api.model,
136
+ stream: false,
137
+ prompt,
138
+ context,
139
+ }),
140
+ });
141
+ const data = await req.json();
142
+ return data.response || '';
143
+ }
144
+
145
+ async _callCompletionsApi(api, prompt, context) {
146
+ const messages = [];
147
+
148
+ if (context) {
149
+ messages.push({ role: 'system', content: context });
150
+ }
151
+
152
+ messages.push({ role: 'user', content: prompt });
153
+
154
+ const req = await fetch(api.url, {
155
+ method: 'POST',
156
+ headers: {
157
+ 'Content-Type': 'application/json',
158
+ 'Authorization': `Bearer ${api.key}`,
159
+ },
160
+ body: JSON.stringify({
161
+ model: api.model,
162
+ messages: messages,
163
+ temperature: 0.1,
164
+ stream: false,
165
+ timeout: this.init.timeout,
166
+ }),
167
+ });
168
+
169
+ if (!req.ok) {
170
+ let errorBody = '';
171
+ try {
172
+ errorBody = await req.text();
173
+ } catch (e) { /* ignore */ }
174
+
175
+ throw new Error(`LLM completions request failed with status ${req.status}: ${errorBody}`);
176
+ }
177
+
178
+ const data = await req.json();
179
+
180
+ if (data.choices && data.choices.length > 0 && data.choices[0].message) {
181
+ return data.choices[0].message.content || '';
182
+ } else {
183
+ this.logger.warn(style.yellow('LLM completions response structure unexpected:'), data);
184
+ return '';
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,53 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export const DEFAULT_EXTENSIONS = ['.js', '.ts', '.tsx'];
5
+
6
+ export const catGen = (paths, extensions = DEFAULT_EXTENSIONS) => {
7
+ const extSet = new Set(extensions);
8
+ const results = [];
9
+
10
+ const process = (inputPath) => {
11
+ const absoluteInputPath = path.resolve(inputPath);
12
+ if (!fs.existsSync(absoluteInputPath)) {
13
+ return;
14
+ }
15
+
16
+ const initialStats = fs.statSync(absoluteInputPath);
17
+ const basePath = initialStats.isDirectory()
18
+ ? absoluteInputPath
19
+ : path.dirname(absoluteInputPath);
20
+
21
+ const walk = (currentPath) => {
22
+ const stats = fs.statSync(currentPath);
23
+
24
+ if (stats.isFile()) {
25
+ const ext = path.extname(currentPath).toLowerCase();
26
+ if (extSet.has(ext)) {
27
+ const relativePath = path.relative(basePath, currentPath);
28
+
29
+ const content = fs.readFileSync(currentPath, 'utf8');
30
+ results.push({
31
+ basePath,
32
+ currentPath,
33
+ relativePath,
34
+ content,
35
+ });
36
+ }
37
+ } else if (stats.isDirectory()) {
38
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
39
+ for (const entry of entries) {
40
+ const fullEntryPath = path.join(currentPath, entry.name);
41
+ walk(fullEntryPath);
42
+ }
43
+ }
44
+ };
45
+
46
+ walk(absoluteInputPath);
47
+ };
48
+
49
+ (paths || []).forEach(process);
50
+
51
+ return results;
52
+ };
53
+