scai 0.1.12 → 0.1.13
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 +42 -42
- package/dist/commands/ChangeLogUpdateCmd.js +39 -0
- package/dist/commands/CommitSuggesterCmd.js +4 -3
- package/dist/commands/SummaryCmd.js +29 -5
- package/dist/config/ModelConfig.js +7 -0
- package/dist/index.js +13 -5
- package/dist/pipeline/modules/changeLogModule.js +30 -0
- package/dist/pipeline/modules/commentModule.js +9 -4
- package/dist/pipeline/modules/summaryModule.js +11 -6
- package/package.json +1 -1
- package/dist/commands/CommitSuggesterCmd.refactored.js +0 -106
- package/dist/commands/EnvCmd.test.js +0 -146
- package/dist/commands/ReadmeCmd.js +0 -61
package/README.md
CHANGED
|
@@ -1,41 +1,34 @@
|
|
|
1
1
|
# ⚙️ scai — Smart Commit AI ✨
|
|
2
2
|
|
|
3
|
-
> AI-powered CLI tools for smart commit messages,
|
|
3
|
+
> AI-powered CLI tools for smart commit messages, code comments, summaries, and changelogs — all powered by local models.
|
|
4
4
|
|
|
5
5
|
**scai** (Smart Commit AI) is a lightweight, privacy-focused CLI tool that uses local AI models (via [Ollama](https://ollama.com)) to help developers work faster and cleaner:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- ✨
|
|
9
|
-
- 🧠
|
|
7
|
+
- 💬 Suggest high-quality Git commit messages
|
|
8
|
+
- ✨ Automatically comment your code
|
|
9
|
+
- 🧠 Summarize code files instantly in the terminal
|
|
10
|
+
- 📝 Generate changelog entries based on Git diffs
|
|
10
11
|
- 🔒 100% local — no API keys, no cloud, no telemetry
|
|
11
12
|
|
|
12
13
|
---
|
|
13
14
|
|
|
14
15
|
## 🚀 Features
|
|
15
16
|
|
|
16
|
-
- ⚡️ Powered by
|
|
17
|
+
- ⚡️ Powered by local Ollama models like `llama3` and `codellama`
|
|
17
18
|
- 🛠️ CLI built with Node.js + TypeScript
|
|
18
19
|
- 🔒 No external services, full privacy by design
|
|
19
|
-
- ✅
|
|
20
|
-
|
|
21
|
-
### 📦 Recent Additions
|
|
22
|
-
|
|
23
|
-
* **Summary Command**: Print a summary of the given file to the terminal (e.g., `scai summ <file>`)
|
|
20
|
+
- ✅ Global options for model and language selection
|
|
24
21
|
|
|
25
22
|
---
|
|
26
23
|
|
|
27
|
-
## ❤️
|
|
24
|
+
## ❤️ Why Local AI?
|
|
28
25
|
|
|
29
26
|
We believe your code — and your workflow — should stay **yours**. scai runs entirely on your machine using open-source models and tools.
|
|
30
27
|
|
|
31
|
-
No internet connection. No vendor lock-in. Just local, private, AI-enhanced developer experience.
|
|
32
|
-
|
|
33
28
|
✅ Works entirely offline
|
|
34
|
-
✅ No API keys
|
|
29
|
+
✅ No API keys or cloud dependencies
|
|
35
30
|
✅ Backed by open-source models
|
|
36
|
-
✅ Designed for CLI-first
|
|
37
|
-
|
|
38
|
-
**scai** follows the Unix philosophy — small, composable tools with powerful output.
|
|
31
|
+
✅ Designed for CLI-first developers
|
|
39
32
|
|
|
40
33
|
---
|
|
41
34
|
|
|
@@ -44,28 +37,27 @@ No internet connection. No vendor lock-in. Just local, private, AI-enhanced deve
|
|
|
44
37
|
1. **Install [Ollama](https://ollama.com)**
|
|
45
38
|
- On **Windows**: [Download Ollama](https://ollama.com/download)
|
|
46
39
|
- On **macOS**:
|
|
47
|
-
-
|
|
40
|
+
- Via Homebrew:
|
|
48
41
|
```bash
|
|
49
42
|
brew install ollama
|
|
50
43
|
```
|
|
51
|
-
- Or download directly from the
|
|
52
|
-
- Ensure it’s added to your system `PATH`
|
|
44
|
+
- Or download directly from the website
|
|
53
45
|
|
|
54
|
-
2. **Install scai globally
|
|
46
|
+
2. **Install scai globally:**
|
|
55
47
|
|
|
56
48
|
```bash
|
|
57
49
|
npm install -g scai
|
|
58
50
|
```
|
|
59
51
|
|
|
60
|
-
3. **
|
|
52
|
+
3. **Initialize the tool and models:**
|
|
61
53
|
|
|
62
54
|
```bash
|
|
63
55
|
scai init
|
|
64
56
|
```
|
|
65
57
|
|
|
66
58
|
This will:
|
|
67
|
-
-
|
|
68
|
-
-
|
|
59
|
+
- Start the Ollama background server
|
|
60
|
+
- Download required models (`llama3`, etc.)
|
|
69
61
|
|
|
70
62
|
---
|
|
71
63
|
|
|
@@ -74,10 +66,7 @@ This will:
|
|
|
74
66
|
### 💬 Suggest a commit message
|
|
75
67
|
|
|
76
68
|
```bash
|
|
77
|
-
# Stage your changes
|
|
78
69
|
git add .
|
|
79
|
-
|
|
80
|
-
# Let scai suggest a commit message
|
|
81
70
|
scai sugg
|
|
82
71
|
```
|
|
83
72
|
|
|
@@ -86,7 +75,7 @@ scai sugg
|
|
|
86
75
|
feat(api): add error handling to user service
|
|
87
76
|
```
|
|
88
77
|
|
|
89
|
-
To automatically
|
|
78
|
+
To commit automatically with the suggestion:
|
|
90
79
|
|
|
91
80
|
```bash
|
|
92
81
|
scai sugg --commit
|
|
@@ -94,41 +83,52 @@ scai sugg --commit
|
|
|
94
83
|
|
|
95
84
|
---
|
|
96
85
|
|
|
97
|
-
###
|
|
86
|
+
### ✨ Comment a code file
|
|
98
87
|
|
|
99
88
|
```bash
|
|
100
|
-
scai
|
|
89
|
+
scai comm <file>
|
|
101
90
|
```
|
|
102
91
|
|
|
103
|
-
|
|
104
|
-
- `-a, --apply`
|
|
92
|
+
Adds clear, helpful comments to the code. Optional flag:
|
|
93
|
+
- `-a, --apply` — Overwrite the original file with the commented version
|
|
105
94
|
|
|
106
95
|
---
|
|
107
96
|
|
|
108
|
-
### 🔍
|
|
97
|
+
### 🔍 Summarize a code file
|
|
109
98
|
|
|
110
99
|
```bash
|
|
111
100
|
scai summ <file>
|
|
112
101
|
```
|
|
113
102
|
|
|
114
|
-
|
|
103
|
+
Prints a summary of what the code does directly to your terminal.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### 📝 Update the changelog
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
scai changelog
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Analyzes the current Git diff and updates (or creates) a `CHANGELOG.md` file with relevant public-facing changes.
|
|
115
114
|
|
|
116
115
|
---
|
|
117
116
|
|
|
118
117
|
## 🔐 License & Fair Use
|
|
119
118
|
|
|
120
|
-
**scai is free to use** for individuals, teams, and companies — including in commercial work.
|
|
119
|
+
**scai is free to use** for individuals, teams, and companies — including in commercial work.
|
|
120
|
+
|
|
121
121
|
You may:
|
|
122
122
|
|
|
123
123
|
- ✅ Use it internally in your projects
|
|
124
|
-
- ✅ Use it at work or in commercial
|
|
125
|
-
- ✅ Share and recommend it
|
|
124
|
+
- ✅ Use it at work or in commercial development
|
|
125
|
+
- ✅ Share and recommend it
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
But you may not:
|
|
128
128
|
|
|
129
|
-
- ❌
|
|
130
|
-
- ❌
|
|
129
|
+
- ❌ Repackage or resell **scai** as a product or SaaS
|
|
130
|
+
- ❌ Claim ownership of the tool
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
See [LICENSE](./LICENSE) for full terms.
|
|
133
133
|
|
|
134
|
-
---
|
|
134
|
+
---
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/commands/handleChangelogUpdate.ts
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { runPromptPipeline } from '../pipeline/runPipeline.js';
|
|
6
|
+
import { changelogModule } from '../pipeline/modules/changeLogModule.js';
|
|
7
|
+
export async function handleChangelogUpdate() {
|
|
8
|
+
try {
|
|
9
|
+
let diff = execSync("git diff", { encoding: "utf-8" }).trim();
|
|
10
|
+
if (!diff) {
|
|
11
|
+
diff = execSync("git diff --cached", { encoding: "utf-8" }).trim();
|
|
12
|
+
}
|
|
13
|
+
if (!diff) {
|
|
14
|
+
console.log("⚠️ No staged or unstaged changes to include in changelog.");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const result = await runPromptPipeline([changelogModule], { code: diff });
|
|
18
|
+
if (!result.code.trim()) {
|
|
19
|
+
console.log("✅ No significant changes for changelog.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const root = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
23
|
+
const changelogPath = path.join(root, "CHANGELOG.md");
|
|
24
|
+
let existing = "";
|
|
25
|
+
try {
|
|
26
|
+
existing = await fs.readFile(changelogPath, "utf-8");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
console.log("📄 Creating new CHANGELOG.md");
|
|
30
|
+
}
|
|
31
|
+
const today = new Date().toISOString().split("T")[0];
|
|
32
|
+
const newEntry = `\n\n## ${today}\n\n${result.code}`;
|
|
33
|
+
await fs.writeFile(changelogPath, existing + newEntry, "utf-8");
|
|
34
|
+
console.log("📝 CHANGELOG.md updated.");
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.error("❌ Failed to update changelog:", err.message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -3,11 +3,12 @@ import readline from 'readline';
|
|
|
3
3
|
function askUserToChoose(suggestions) {
|
|
4
4
|
return new Promise((resolve) => {
|
|
5
5
|
console.log('\n💡 AI-suggested commit messages:\n');
|
|
6
|
-
suggestions.forEach((msg, i) => {
|
|
6
|
+
suggestions.slice(0, 3).forEach((msg, i) => {
|
|
7
7
|
console.log(`${i + 1}) ${msg}`);
|
|
8
8
|
});
|
|
9
|
-
console.log(
|
|
10
|
-
console.log(
|
|
9
|
+
console.log('\n---');
|
|
10
|
+
console.log('4) 🔁 Regenerate suggestions');
|
|
11
|
+
console.log('5) ✍️ Write your own commit message');
|
|
11
12
|
const rl = readline.createInterface({
|
|
12
13
|
input: process.stdin,
|
|
13
14
|
output: process.stdout,
|
|
@@ -1,12 +1,36 @@
|
|
|
1
|
-
// File: src/commands/SummaryCmd.ts
|
|
2
1
|
import fs from 'fs/promises';
|
|
3
2
|
import { summaryModule } from '../pipeline/modules/summaryModule.js';
|
|
3
|
+
import readline from 'readline';
|
|
4
4
|
export async function summarizeFile(filepath) {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
let code = '';
|
|
6
|
+
if (filepath) {
|
|
7
|
+
try {
|
|
8
|
+
code = await fs.readFile(filepath, 'utf-8');
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
console.error(`❌ Could not read or summarize ${filepath}:`, err.message);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// ⬇️ Add this check right here
|
|
16
|
+
else if (process.stdin.isTTY) {
|
|
17
|
+
console.error('❌ No file provided and no piped input.\n👉 Usage: scai summ <file> or cat file | scai summ');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const rl = readline.createInterface({
|
|
22
|
+
input: process.stdin,
|
|
23
|
+
output: process.stdout,
|
|
24
|
+
terminal: false,
|
|
25
|
+
});
|
|
26
|
+
for await (const line of rl) {
|
|
27
|
+
code += line + '\n';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (code.trim()) {
|
|
7
31
|
await summaryModule.run({ code });
|
|
8
32
|
}
|
|
9
|
-
|
|
10
|
-
console.error(
|
|
33
|
+
else {
|
|
34
|
+
console.error('❌ No code provided to summarize.');
|
|
11
35
|
}
|
|
12
36
|
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
export class ModelConfig {
|
|
2
2
|
static setModel(model) {
|
|
3
3
|
this.model = model;
|
|
4
|
+
console.log(`📦 Model set to: ${model}`);
|
|
4
5
|
}
|
|
5
6
|
static getModel() {
|
|
6
7
|
return this.model;
|
|
7
8
|
}
|
|
8
9
|
static setLanguage(lang) {
|
|
9
10
|
this.language = lang;
|
|
11
|
+
console.log(`🗣️ Language set to: ${lang}`);
|
|
10
12
|
}
|
|
11
13
|
static getLanguage() {
|
|
12
14
|
return this.language;
|
|
13
15
|
}
|
|
16
|
+
static logCurrentConfig() {
|
|
17
|
+
console.log(`🔧 Current configuration:`);
|
|
18
|
+
console.log(` Model : ${this.model}`);
|
|
19
|
+
console.log(` Language: ${this.language}`);
|
|
20
|
+
}
|
|
14
21
|
}
|
|
15
22
|
ModelConfig.model = 'codellama:7b';
|
|
16
23
|
ModelConfig.language = 'ts';
|
package/dist/index.js
CHANGED
|
@@ -7,11 +7,11 @@ import { checkEnv } from "./commands/EnvCmd.js";
|
|
|
7
7
|
import { checkGit } from "./commands/GitCmd.js";
|
|
8
8
|
import { suggestCommitMessage } from "./commands/CommitSuggesterCmd.js";
|
|
9
9
|
import { handleRefactor } from "./commands/RefactorCmd.js";
|
|
10
|
-
import { updateReadmeIfNeeded } from "./commands/ReadmeCmd.js";
|
|
11
10
|
import { generateTests } from "./commands/TestGenCmd.js";
|
|
12
11
|
import { bootstrap } from './modelSetup.js';
|
|
13
12
|
import { ModelConfig } from './config/ModelConfig.js';
|
|
14
13
|
import { summarizeFile } from "./commands/SummaryCmd.js";
|
|
14
|
+
import { handleChangelogUpdate } from './commands/ChangeLogUpdateCmd.js';
|
|
15
15
|
// Create the CLI instance
|
|
16
16
|
const cmd = new Command('scai')
|
|
17
17
|
.version(version)
|
|
@@ -36,11 +36,13 @@ cmd
|
|
|
36
36
|
.option('-a, --apply', 'Apply the refactored version to the original file')
|
|
37
37
|
.action((file, options) => handleRefactor(file, options));
|
|
38
38
|
cmd
|
|
39
|
-
.command('
|
|
40
|
-
.description('Update
|
|
41
|
-
.action(
|
|
39
|
+
.command('changelog')
|
|
40
|
+
.description('Update or create the CHANGELOG.md based on current Git diff')
|
|
41
|
+
.action(async () => {
|
|
42
|
+
await handleChangelogUpdate();
|
|
43
|
+
});
|
|
42
44
|
cmd
|
|
43
|
-
.command('summ
|
|
45
|
+
.command('summ [file]')
|
|
44
46
|
.description('Print a summary of the given file to the terminal')
|
|
45
47
|
.action((file) => summarizeFile(file));
|
|
46
48
|
cmd
|
|
@@ -55,6 +57,12 @@ cmd
|
|
|
55
57
|
.command('gen-tests <file>')
|
|
56
58
|
.description('Generate a Jest test file for the specified JS/TS module')
|
|
57
59
|
.action((file) => generateTests(file));
|
|
60
|
+
cmd
|
|
61
|
+
.command('config')
|
|
62
|
+
.description('Show the currently active model and language settings')
|
|
63
|
+
.action(() => {
|
|
64
|
+
ModelConfig.logCurrentConfig();
|
|
65
|
+
});
|
|
58
66
|
// ✅ Now that commands are defined, parse args
|
|
59
67
|
cmd.parse(process.argv);
|
|
60
68
|
// ✅ Apply global options after parsing
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ModelConfig } from '../../config/ModelConfig.js';
|
|
2
|
+
export const changelogModule = {
|
|
3
|
+
name: 'changelogModule',
|
|
4
|
+
description: 'Generates changelog entry based on Git diff',
|
|
5
|
+
async run(input) {
|
|
6
|
+
const model = ModelConfig.getModel();
|
|
7
|
+
const prompt = `
|
|
8
|
+
You're an experienced changelog writer. Based on this Git diff, write a markdown bullet-point entry suitable for CHANGELOG.md:
|
|
9
|
+
|
|
10
|
+
--- DIFF START ---
|
|
11
|
+
${input.code}
|
|
12
|
+
--- DIFF END ---
|
|
13
|
+
|
|
14
|
+
✅ If the changes are significant, return a changelog entry.
|
|
15
|
+
❌ If not, return ONLY: "NO UPDATE".
|
|
16
|
+
`.trim();
|
|
17
|
+
const res = await fetch('http://localhost:11434/api/generate', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
model,
|
|
22
|
+
prompt,
|
|
23
|
+
stream: false,
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
const data = await res.json();
|
|
27
|
+
const output = data.response?.trim() ?? '';
|
|
28
|
+
return { code: output === 'NO UPDATE' ? '' : output };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -6,11 +6,16 @@ export const addCommentsModule = {
|
|
|
6
6
|
const model = ModelConfig.getModel();
|
|
7
7
|
const lang = ModelConfig.getLanguage();
|
|
8
8
|
const prompt = `
|
|
9
|
-
You are a senior ${lang.toUpperCase()} engineer.
|
|
9
|
+
You are a senior ${lang.toUpperCase()} engineer reviewing source code.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Your task is to add clear and helpful single-line // comments to complex or non-obvious parts of the code.
|
|
12
|
+
|
|
13
|
+
⚠️ VERY IMPORTANT RULES:
|
|
14
|
+
- You MUST return the ENTIRE original code.
|
|
15
|
+
- You MUST NOT remove, replace, reformat, or alter ANY code.
|
|
16
|
+
- Only add single-line // comments in appropriate places.
|
|
17
|
+
- Do NOT wrap the code in markdown or code blocks.
|
|
18
|
+
- The code should be valid ${lang.toUpperCase()} after your changes.
|
|
14
19
|
|
|
15
20
|
--- CODE START ---
|
|
16
21
|
${input.code}
|
|
@@ -8,15 +8,20 @@ export const summaryModule = {
|
|
|
8
8
|
const prompt = `
|
|
9
9
|
You are a senior ${lang.toUpperCase()} engineer.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
Take the following source code and do NOT modify it in any way. Your task is:
|
|
12
|
+
|
|
13
|
+
1. Output the original code exactly as it is.
|
|
14
|
+
2. After the code, append a short summary in this format:
|
|
13
15
|
|
|
14
16
|
// Summary of code:
|
|
15
|
-
// - [
|
|
16
|
-
// - [
|
|
17
|
-
// -
|
|
17
|
+
// - [What the code does at a high level]
|
|
18
|
+
// - [Main features or components]
|
|
19
|
+
// - [Any interesting logic or patterns]
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
⚠️ IMPORTANT:
|
|
22
|
+
- Do NOT omit or alter any part of the original code.
|
|
23
|
+
- Do NOT insert comments inside the code.
|
|
24
|
+
- Only append the summary AFTER the full code.
|
|
20
25
|
|
|
21
26
|
--- CODE START ---
|
|
22
27
|
${code}
|
package/package.json
CHANGED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import readline from 'readline';
|
|
3
|
-
async function askUserToChoose(suggestions) {
|
|
4
|
-
return new Promise((resolve) => {
|
|
5
|
-
console.log('\n💡 AI-suggested commit messages:\n'); // Display all suggestions to the user
|
|
6
|
-
suggestions.forEach((msg, i) => {
|
|
7
|
-
console.log(`${i + 1}) ${msg}`);
|
|
8
|
-
});
|
|
9
|
-
console.log(`${suggestions.length + 1}) 🔁 Regenerate suggestions`); // Allow the user to regenerate suggestions if they want
|
|
10
|
-
console.log(`${suggestions.length + 2}) ✍️ Write your own commit message`); // Allow the user to write their own commit message
|
|
11
|
-
const rl = readline.createInterface({
|
|
12
|
-
input: process.stdin,
|
|
13
|
-
output: process.stdout,
|
|
14
|
-
});
|
|
15
|
-
rl.question(`\n👉 Choose a commit message [1-${suggestions.length + 2}]: `, // Ask the user to choose from the suggestions or write their own
|
|
16
|
-
(answer) => {
|
|
17
|
-
rl.close();
|
|
18
|
-
const choice = parseInt(answer, 10); // Parse the user's input as a number
|
|
19
|
-
if (isNaN(choice) || choice < 1 || choice > suggestions.length + 2) { // Check that the user has entered a valid number
|
|
20
|
-
console.log('⚠️ Invalid selection. Using the first suggestion by default.'); // Default to the first suggestion if the input is invalid
|
|
21
|
-
resolve(0); // Resolve the promise with the 0-based index (0 to 3)
|
|
22
|
-
}
|
|
23
|
-
else if (choice === suggestions.length + 2) { // If the user has chosen "Write your own commit message"
|
|
24
|
-
resolve('custom'); // Return 'custom' to indicate that the user wants to write their own commit message
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
resolve(choice - 1); // Return the 0-based index (0 to 3) of the selected suggestion
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
async function generateSuggestions(prompt) {
|
|
33
|
-
const res = await fetch("http://localhost:11434/api/generate", {
|
|
34
|
-
method: "POST",
|
|
35
|
-
headers: { "Content-Type": "application/json" },
|
|
36
|
-
body: JSON.stringify({
|
|
37
|
-
model: "llama3",
|
|
38
|
-
prompt,
|
|
39
|
-
stream: false,
|
|
40
|
-
}),
|
|
41
|
-
});
|
|
42
|
-
const { response } = await res.json(); // Parse the response from the API as JSON
|
|
43
|
-
if (!response || typeof response !== 'string') { // Check that the response is valid
|
|
44
|
-
throw new Error('Invalid LLM response'); // Throw an error if the response is invalid
|
|
45
|
-
}
|
|
46
|
-
const lines = response.trim().split('\n').filter(line => /^\d+\.\s+/.test(line)); // Split the response into individual lines and filter out any lines that don't start with a number followed by a period
|
|
47
|
-
const messages = lines.map(line => // Map each line to its commit message suggestion
|
|
48
|
-
line.replace(/^\d+\.\s+/, '').replace(/^"(.*)"$/, '$1').trim());
|
|
49
|
-
if (messages.length === 0) { // Check that there are any commit message suggestions
|
|
50
|
-
throw new Error('No valid commit messages found in LLM response.'); // Throw an error if there are no valid commit message suggestions
|
|
51
|
-
}
|
|
52
|
-
return messages; // Return the array of commit message suggestions
|
|
53
|
-
}
|
|
54
|
-
function promptCustomMessage() {
|
|
55
|
-
return new Promise((resolve) => {
|
|
56
|
-
const rl = readline.createInterface({
|
|
57
|
-
input: process.stdin,
|
|
58
|
-
output: process.stdout,
|
|
59
|
-
});
|
|
60
|
-
rl.question('\n📝 Enter your custom commit message:\n> ', (input) => {
|
|
61
|
-
rl.close();
|
|
62
|
-
resolve(input.trim()); // Resolve the promise with the trimmed input string
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
export async function suggestCommitMessage(options) {
|
|
67
|
-
try {
|
|
68
|
-
let diff = execSync("git diff", { encoding: "utf-8" }).trim(); // Get the current Git diff
|
|
69
|
-
if (!diff) { // Check that there are any changes to stage
|
|
70
|
-
diff = execSync("git diff --cached", { encoding: "utf-8" }).trim(); // If there are no staged changes, check for unstaged changes
|
|
71
|
-
}
|
|
72
|
-
if (!diff) { // If there are still no changes, display a message and return early
|
|
73
|
-
console.log('⚠️ No staged changes to suggest a message for.');
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
const prompt = `Suggest 3 concise, conventional Git commit message options for this diff. Return ONLY the commit messages, numbered 1 to 3, like so:
|
|
77
|
-
1. ...
|
|
78
|
-
2. ...
|
|
79
|
-
3. ...
|
|
80
|
-
|
|
81
|
-
Here is the diff:
|
|
82
|
-
${diff}`; // Create a prompt string for the user to enter their own commit message suggestions
|
|
83
|
-
let message = null;
|
|
84
|
-
while (message === null) { // Loop until the user has entered a valid commit message or chosen to write their own
|
|
85
|
-
const suggestions = await generateSuggestions(prompt); // Generate commit message suggestions based on the diff output from Git
|
|
86
|
-
const choice = await askUserToChoose(suggestions); // Ask the user to choose from the suggestions or write their own
|
|
87
|
-
if (choice === 'custom') { // If the user has chosen "Write your own commit message"
|
|
88
|
-
message = await promptCustomMessage(); // Prompt the user for their custom commit message
|
|
89
|
-
break; // Break out of the loop and use the custom commit message
|
|
90
|
-
}
|
|
91
|
-
message = suggestions[choice]; // Use the selected suggestion as the commit message
|
|
92
|
-
}
|
|
93
|
-
console.log(`\n✅ Selected commit message:\n${message}\n`); // Display the selected commit message to the user
|
|
94
|
-
if (options.commit) { // If the user has specified that they want to commit the changes
|
|
95
|
-
const commitDiff = execSync("git diff", { encoding: "utf-8" }).trim(); // Get the current Git diff again, in case anything has changed since the last call to `generateSuggestions`
|
|
96
|
-
if (commitDiff) { // Check that there are any staged changes
|
|
97
|
-
execSync("git add .", { encoding: "utf-8" }); // Stage all changes before committing
|
|
98
|
-
}
|
|
99
|
-
execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'inherit' }); // Commit the changes with the selected commit message
|
|
100
|
-
console.log('✅ Committed with selected message.'); // Display a success message to the user
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch (err) { // If there is an error in the code
|
|
104
|
-
console.error('❌ Error in commit message suggestion:', err); // Log the error to the console
|
|
105
|
-
}
|
|
106
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Here;
|
|
3
|
-
's a Jest test file for the `checkEnv` function: `` `typescript
|
|
4
|
-
import { checkEnv } from "../src/utils";
|
|
5
|
-
|
|
6
|
-
describe("checkEnv", () => {
|
|
7
|
-
it("should warn when missing required env vars", () => {
|
|
8
|
-
const processEnv = Object.assign({}, process.env);
|
|
9
|
-
delete processEnv.DB_HOST;
|
|
10
|
-
delete processEnv.API_KEY;
|
|
11
|
-
const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
12
|
-
checkEnv();
|
|
13
|
-
expect(consoleWarnSpy).toHaveBeenCalledWith("❌ Missing env vars: DB_HOST, API_KEY");
|
|
14
|
-
consoleWarnSpy.mockRestore();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("should log a message when all required env vars are set", () => {
|
|
18
|
-
const processEnv = Object.assign({}, process.env);
|
|
19
|
-
checkEnv();
|
|
20
|
-
expect(console.log).toHaveBeenCalledWith("✅ All env vars are set");
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
` ``;
|
|
24
|
-
In;
|
|
25
|
-
this;
|
|
26
|
-
test;
|
|
27
|
-
file, we;
|
|
28
|
-
first;
|
|
29
|
-
var the = ;
|
|
30
|
-
`checkEnv`;
|
|
31
|
-
function from() { }
|
|
32
|
-
the;
|
|
33
|
-
contains;
|
|
34
|
-
it.We;
|
|
35
|
-
then;
|
|
36
|
-
define;
|
|
37
|
-
two;
|
|
38
|
-
test;
|
|
39
|
-
cases: one;
|
|
40
|
-
for (when; required; environment)
|
|
41
|
-
variables;
|
|
42
|
-
are;
|
|
43
|
-
missing;
|
|
44
|
-
and;
|
|
45
|
-
another;
|
|
46
|
-
for (when; all; required)
|
|
47
|
-
environment;
|
|
48
|
-
variables;
|
|
49
|
-
are;
|
|
50
|
-
set.
|
|
51
|
-
;
|
|
52
|
-
In;
|
|
53
|
-
the;
|
|
54
|
-
first;
|
|
55
|
-
test;
|
|
56
|
-
we;
|
|
57
|
-
use `jest.spyOn()`;
|
|
58
|
-
to;
|
|
59
|
-
mock;
|
|
60
|
-
the `console.warn()`;
|
|
61
|
-
method;
|
|
62
|
-
and;
|
|
63
|
-
replace;
|
|
64
|
-
it;
|
|
65
|
-
with (a)
|
|
66
|
-
spy;
|
|
67
|
-
function () { }
|
|
68
|
-
We;
|
|
69
|
-
then;
|
|
70
|
-
delete the `DB_HOST`;
|
|
71
|
-
and `API_KEY`;
|
|
72
|
-
environment;
|
|
73
|
-
variables;
|
|
74
|
-
and;
|
|
75
|
-
call;
|
|
76
|
-
the `checkEnv()`;
|
|
77
|
-
function () { }
|
|
78
|
-
Finally, we;
|
|
79
|
-
check;
|
|
80
|
-
that;
|
|
81
|
-
the `console.warn()`;
|
|
82
|
-
method;
|
|
83
|
-
was;
|
|
84
|
-
called;
|
|
85
|
-
with (the)
|
|
86
|
-
expected;
|
|
87
|
-
message.
|
|
88
|
-
;
|
|
89
|
-
In;
|
|
90
|
-
the;
|
|
91
|
-
second;
|
|
92
|
-
test;
|
|
93
|
-
we;
|
|
94
|
-
simply;
|
|
95
|
-
call;
|
|
96
|
-
the `checkEnv()`;
|
|
97
|
-
function without() { }
|
|
98
|
-
deleting;
|
|
99
|
-
any;
|
|
100
|
-
environment;
|
|
101
|
-
variables;
|
|
102
|
-
and;
|
|
103
|
-
check;
|
|
104
|
-
that;
|
|
105
|
-
the `console.log()`;
|
|
106
|
-
method;
|
|
107
|
-
was;
|
|
108
|
-
called;
|
|
109
|
-
with (the)
|
|
110
|
-
expected;
|
|
111
|
-
message.
|
|
112
|
-
;
|
|
113
|
-
Note;
|
|
114
|
-
that;
|
|
115
|
-
we;
|
|
116
|
-
use;
|
|
117
|
-
the `mockImplementation()`;
|
|
118
|
-
method;
|
|
119
|
-
to;
|
|
120
|
-
mock;
|
|
121
|
-
the `console.warn()`;
|
|
122
|
-
method;
|
|
123
|
-
and;
|
|
124
|
-
the `mockRestore()`;
|
|
125
|
-
method;
|
|
126
|
-
to;
|
|
127
|
-
restore;
|
|
128
|
-
it;
|
|
129
|
-
after;
|
|
130
|
-
the;
|
|
131
|
-
test;
|
|
132
|
-
is;
|
|
133
|
-
done.This;
|
|
134
|
-
is;
|
|
135
|
-
necessary;
|
|
136
|
-
because;
|
|
137
|
-
Jest;
|
|
138
|
-
does;
|
|
139
|
-
not;
|
|
140
|
-
allow;
|
|
141
|
-
us;
|
|
142
|
-
to;
|
|
143
|
-
modify;
|
|
144
|
-
the;
|
|
145
|
-
original `console.warn()`;
|
|
146
|
-
method.;
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
export async function updateReadmeIfNeeded() {
|
|
5
|
-
try {
|
|
6
|
-
let diff = execSync("git diff", { encoding: "utf-8" }).trim();
|
|
7
|
-
if (!diff) {
|
|
8
|
-
diff = execSync("git diff --cached", { encoding: "utf-8" }).trim();
|
|
9
|
-
}
|
|
10
|
-
if (!diff) {
|
|
11
|
-
console.log('⚠️ No staged changes to suggest a message for.');
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
const readmePath = path.resolve("README.md");
|
|
15
|
-
let readme = "";
|
|
16
|
-
try {
|
|
17
|
-
readme = await fs.readFile(readmePath, "utf-8");
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
console.log("📄 No existing README.md found, skipping update.");
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
const prompt = `
|
|
24
|
-
You're an experienced documentation writer. Here's the current README:
|
|
25
|
-
|
|
26
|
-
--- README START ---
|
|
27
|
-
${readme}
|
|
28
|
-
--- README END ---
|
|
29
|
-
|
|
30
|
-
Here is a Git diff of recent code changes:
|
|
31
|
-
|
|
32
|
-
--- DIFF START ---
|
|
33
|
-
${diff}
|
|
34
|
-
--- DIFF END ---
|
|
35
|
-
|
|
36
|
-
✅ If the changes are significant and relevant to the public-facing documentation, return an updated README.
|
|
37
|
-
❌ If they are not, return ONLY: "NO UPDATE".
|
|
38
|
-
`;
|
|
39
|
-
const res = await fetch("http://localhost:11434/api/generate", {
|
|
40
|
-
method: "POST",
|
|
41
|
-
headers: { "Content-Type": "application/json" },
|
|
42
|
-
body: JSON.stringify({
|
|
43
|
-
model: "llama3",
|
|
44
|
-
prompt,
|
|
45
|
-
stream: false,
|
|
46
|
-
}),
|
|
47
|
-
});
|
|
48
|
-
const { response } = await res.json();
|
|
49
|
-
const result = response.trim();
|
|
50
|
-
if (result === "NO UPDATE") {
|
|
51
|
-
console.log("✅ No significant changes for README.");
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
await fs.writeFile(readmePath, result, "utf-8");
|
|
55
|
-
console.log("📝 README.md updated based on significant changes.");
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
catch (err) {
|
|
59
|
-
console.error("❌ Failed to update README:", err.message);
|
|
60
|
-
}
|
|
61
|
-
}
|