gennady 0.3.0 → 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 +95 -16
  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 -43
  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 +30 -175
  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 +4 -2
  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 -12
  34. package/src/prompts/translate-prompt.md +0 -11
package/README.md CHANGED
@@ -3,9 +3,100 @@
3
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> ...
14
+ ```
15
+
16
+ ---
17
+
18
+ ### ✨ Features
19
+
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).
23
+
24
+ ---
25
+
26
+ ## 🔖 Usage Overview
27
+
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
38
+ # Basic usage
39
+ npx gennady
40
+
41
+ # Generate oneline commit message
42
+ npx gennady --mode=oneline
43
+
44
+ # Generate detailed commit message
45
+ npx gennady --mode=detailed
46
+
47
+ # Generate detailed commit message relative to the target branch
48
+ npx gennady --branch=develop
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/
7
94
  ```
8
95
 
96
+ #### Output
97
+ - Shows file contents with headers per file.
98
+ - Hints for copying output without color codes.
99
+
9
100
  ---
10
101
 
11
102
  ## Setup LLM
@@ -22,32 +113,20 @@ ollama serve
22
113
 
23
114
  ### External
24
115
 
25
- Create `~/.gennadyrc` file:
116
+ Create `~/.gennadyrc` configuration file:
26
117
 
27
118
  ```json
28
119
  [
29
120
  {
30
121
  "url": "https://api.openai.com/v1/chat/completions",
31
122
  "key": "...",
32
- "model": "gpt-4o"
123
+ "model": "gpt-3.5-turbo-0125"
33
124
  }
34
125
  ]
35
126
  ```
36
127
 
37
128
  ---
38
129
 
39
- ### Usage
40
-
41
- ```bash
42
- # Basic usage
43
- npx gennady
130
+ ## 🎉 Happy Coding with Gennady!
44
131
 
45
- # Generate oneline commit message
46
- npx gennady --mode=oneline
47
-
48
- # Generate detailed commit message
49
- npx gennady --mode=detailed
50
-
51
- # Generate detailed commit message relative to the target branch
52
- npx gennady --branch=develop
53
- ```
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,48 +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
- model: ['model'],
20
- targetBranch: ['branch', 'b'],
21
- apiUrl: ['api', 'apiUrl'],
22
- });
23
-
24
- const commit = new CommitGen({
25
- ...params,
26
- basePromptTemplate: readFileSync(join(PROMPTS_DIR, 'base-prompt.md')).toString(),
27
- formatOnelinePromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-oneline-prompt.md')).toString(),
28
- formatDetailedPromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-detailed-prompt.md')).toString(),
29
- translatePromptTemplate: readFileSync(join(PROMPTS_DIR, 'translate-prompt.md')).toString(),
30
- });
31
-
32
- console.info(`🤖`, style.whiteBright.bold(`GENNADY`), `(${style.cyan(commit.model)} → ${style.yellow(commit.mode)})`, `🗯️`);
33
- console.info(style.gray(`-`.repeat(30)));
34
- console.info(`- url: ${style.blue(commit.apiUrl)}`);
35
- console.info(style.gray(`-`.repeat(30)));
36
-
37
- const msg = await commit.generate();
38
- if (msg) {
39
- console.info(`-`.repeat(40), '\n');
40
- console.info(style.whiteBright(msg), '\n');
41
- console.info(`^`.repeat(40), '\n');
42
-
43
- const lang = getSysLang();
44
- if (lang !== 'en') {
45
- console.info(await commit.translate(msg, lang), '\n');
46
- console.info(`^`.repeat(40), '\n');
47
- }
18
+ //
19
+ // 🤖 COMMIT-GEN 💬
20
+ //
21
+ default:
22
+ import('./cmd/commit.js');
23
+ break;
48
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.3.0",
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
+