patchwise 1.0.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 +291 -0
- package/dist/patchwise.js +865 -0
- package/dist/patchwise.js.map +1 -0
- package/package.json +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Patchwise
|
|
2
|
+
|
|
3
|
+
AI-assisted Git commits, with you still in charge.
|
|
4
|
+
|
|
5
|
+
Patchwise is a CLI tool that helps developers turn raw Git changes into clean, structured, and meaningful commits using AI assistance.
|
|
6
|
+
|
|
7
|
+
It analyzes your diff, suggests commit messages, and guides you through staging and committing while keeping you fully in control.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
* Analyze staged or unstaged changes
|
|
14
|
+
* Generate commit messages using AI
|
|
15
|
+
* Supports Conventional Commits
|
|
16
|
+
* Interactive file selection
|
|
17
|
+
* Edit before committing
|
|
18
|
+
* Optional push after commit
|
|
19
|
+
* Provider-agnostic architecture, Groq by default
|
|
20
|
+
* Multi-language support (EN / FR)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g patchwise
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
or
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm add -g patchwise
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
### 1. Set your API key
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export GROQ_API_KEY=your_api_key_here
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. Stage your changes
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git add .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3. Generate and commit
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
patchwise commit
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Example
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
patchwise commit
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```txt
|
|
67
|
+
Summary:
|
|
68
|
+
Adds retry logic for failed payment transactions
|
|
69
|
+
|
|
70
|
+
Suggestions:
|
|
71
|
+
1. feat(payment): add retry mechanism for failed transactions
|
|
72
|
+
2. fix(payment): handle transient provider failures
|
|
73
|
+
3. refactor(payment): improve error handling
|
|
74
|
+
|
|
75
|
+
Select a commit message:
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Commands
|
|
81
|
+
|
|
82
|
+
### patchwise commit
|
|
83
|
+
|
|
84
|
+
Generate and create a commit from staged changes.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
patchwise commit
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Options:
|
|
91
|
+
|
|
92
|
+
* `--all` stage all changes before commit
|
|
93
|
+
* `--select` interactively select files
|
|
94
|
+
* `--push` push after commit
|
|
95
|
+
* `--yes` skip confirmations
|
|
96
|
+
* `--lang <en|fr>` commit language
|
|
97
|
+
* `--provider <name>` AI provider
|
|
98
|
+
* `--model <model>` AI model
|
|
99
|
+
* `--scope <scope>` set commit scope
|
|
100
|
+
* `--no-scope` disable scope
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### patchwise suggest
|
|
105
|
+
|
|
106
|
+
Generate commit suggestions without committing.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
patchwise suggest
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### patchwise stage
|
|
115
|
+
|
|
116
|
+
Interactively select files to stage.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
patchwise stage
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### patchwise config init
|
|
125
|
+
|
|
126
|
+
Create a config file.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
patchwise config init
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
### Environment variables
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
GROQ_API_KEY=xxx
|
|
140
|
+
PATCHWISE_PROVIDER=groq
|
|
141
|
+
PATCHWISE_MODEL=llama-3.3-70b-versatile
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### Config file
|
|
147
|
+
|
|
148
|
+
Create `patchwise.config.json`:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"provider": "groq",
|
|
153
|
+
"model": "llama-3.3-70b-versatile",
|
|
154
|
+
"commitConvention": "conventional",
|
|
155
|
+
"language": "en",
|
|
156
|
+
"maxSubjectLength": 72,
|
|
157
|
+
"confirmBeforeCommit": true,
|
|
158
|
+
"confirmBeforePush": true
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Conventional Commits
|
|
165
|
+
|
|
166
|
+
Patchwise generates commits following this format:
|
|
167
|
+
|
|
168
|
+
```txt
|
|
169
|
+
type(scope): subject
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Examples:
|
|
173
|
+
|
|
174
|
+
```txt
|
|
175
|
+
feat(auth): add email verification flow
|
|
176
|
+
fix(api): handle null company id
|
|
177
|
+
refactor(ui): simplify sidebar logic
|
|
178
|
+
docs(readme): update installation guide
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## AI Providers
|
|
184
|
+
|
|
185
|
+
Supported:
|
|
186
|
+
|
|
187
|
+
* Groq
|
|
188
|
+
|
|
189
|
+
Planned:
|
|
190
|
+
|
|
191
|
+
* OpenAI
|
|
192
|
+
* Ollama
|
|
193
|
+
* Anthropic
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Architecture
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
CLI
|
|
201
|
+
├── Git Layer
|
|
202
|
+
├── AI Layer
|
|
203
|
+
├── Commit Engine
|
|
204
|
+
└── UI Layer
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Safety
|
|
210
|
+
|
|
211
|
+
* No commit without user validation
|
|
212
|
+
* No push without confirmation unless explicitly requested
|
|
213
|
+
* API keys are not stored in the project
|
|
214
|
+
* Only staged changes are used by default
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
Install dependencies:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
pnpm install
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Run in development:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
pnpm dev
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Build:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
pnpm build
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Roadmap
|
|
241
|
+
|
|
242
|
+
V1:
|
|
243
|
+
|
|
244
|
+
* commit suggestions
|
|
245
|
+
* commit execution
|
|
246
|
+
* Groq integration
|
|
247
|
+
* file selection
|
|
248
|
+
|
|
249
|
+
V1.1:
|
|
250
|
+
|
|
251
|
+
* push support
|
|
252
|
+
* commit body generation
|
|
253
|
+
* language support
|
|
254
|
+
|
|
255
|
+
V2:
|
|
256
|
+
|
|
257
|
+
* hunk selection
|
|
258
|
+
* multi-commit split
|
|
259
|
+
* multi-provider support
|
|
260
|
+
* local AI
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Contributing
|
|
265
|
+
|
|
266
|
+
Contributions are welcome.
|
|
267
|
+
|
|
268
|
+
* open an issue
|
|
269
|
+
* submit a pull request
|
|
270
|
+
* improve documentation
|
|
271
|
+
* propose features
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Philosophy
|
|
282
|
+
|
|
283
|
+
Patchwise does not replace Git.
|
|
284
|
+
|
|
285
|
+
It helps you write better commits, keep history clean, and move faster without losing control.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Support
|
|
290
|
+
|
|
291
|
+
If you find the project useful, consider starring the repository.
|
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/program.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/core/ai/prompt.ts
|
|
7
|
+
function buildPrompt(input2) {
|
|
8
|
+
const categories = categorizeFiles(input2.fileNames);
|
|
9
|
+
const categoryList = Array.from(categories).join(", ");
|
|
10
|
+
return [
|
|
11
|
+
"You generate Conventional Commit messages from a Git diff.",
|
|
12
|
+
"Return strict JSON only.",
|
|
13
|
+
"The JSON shape must be:",
|
|
14
|
+
'{"summary":"string","suggestions":[{"type":"feat","scope":"optional","subject":"string","body":"optional string"}]}',
|
|
15
|
+
"Rules:",
|
|
16
|
+
"- EVERY suggestion must cover ALL changes in the diff, never just a subset.",
|
|
17
|
+
"- The body should list each change group as bullet points.",
|
|
18
|
+
`- This diff touches these areas: ${categoryList}.`,
|
|
19
|
+
"",
|
|
20
|
+
"Suggestion 1 \u2014 Primary: the most accurate type/scope for the full change.",
|
|
21
|
+
"Suggestion 2 \u2014 Alternative angle: use a different type or scope that also fits.",
|
|
22
|
+
"Suggestion 3 \u2014 Broad: a higher-level framing that encompasses everything.",
|
|
23
|
+
"",
|
|
24
|
+
"Each suggestion must have a DISTINCT type, scope, or subject. No rephrasing.",
|
|
25
|
+
"- Subjects must be imperative, concise, and lower-case.",
|
|
26
|
+
`- Subject max length: ${input2.maxSubjectLength}.`,
|
|
27
|
+
`- Language: ${input2.language}.`,
|
|
28
|
+
`- Scope strategy: ${input2.scopeStrategy}.`,
|
|
29
|
+
input2.scope ? `- Preferred scope: ${input2.scope}.` : "- Preferred scope: infer only if obvious.",
|
|
30
|
+
`- Files involved: ${input2.fileNames.join(", ") || "unknown"}.`,
|
|
31
|
+
"Diff:",
|
|
32
|
+
input2.diff,
|
|
33
|
+
input2.diff.includes("[diff truncated") ? "\nNote: the diff was truncated to fit token limits. Use the file list and stat summary to infer the full scope." : ""
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
36
|
+
function categorizeFiles(files) {
|
|
37
|
+
const categories = /* @__PURE__ */ new Set();
|
|
38
|
+
for (const f of files) {
|
|
39
|
+
if (f.startsWith("src/") || f.startsWith("lib/")) categories.add("code");
|
|
40
|
+
else if (f.startsWith("test/") || f.startsWith("tests/") || f.startsWith("spec/")) categories.add("test");
|
|
41
|
+
else if (f.startsWith("docs/") || f.endsWith(".md")) categories.add("docs");
|
|
42
|
+
else if (f.includes(".config.") || f.endsWith("rc") || f.endsWith(".json")) categories.add("config");
|
|
43
|
+
else if (f.startsWith(".github/") || f.endsWith(".yml") || f.endsWith(".yaml")) categories.add("ci");
|
|
44
|
+
else categories.add("other");
|
|
45
|
+
}
|
|
46
|
+
return categories;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/core/ai/schemas.ts
|
|
50
|
+
import { z } from "zod";
|
|
51
|
+
var VALID_COMMIT_TYPES = [
|
|
52
|
+
"feat",
|
|
53
|
+
"fix",
|
|
54
|
+
"refactor",
|
|
55
|
+
"docs",
|
|
56
|
+
"test",
|
|
57
|
+
"chore",
|
|
58
|
+
"perf",
|
|
59
|
+
"build",
|
|
60
|
+
"ci"
|
|
61
|
+
];
|
|
62
|
+
function coerceCommitType(value) {
|
|
63
|
+
if (VALID_COMMIT_TYPES.includes(value)) {
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
const aliasMap = {
|
|
67
|
+
bugfix: "fix",
|
|
68
|
+
"bug-fix": "fix",
|
|
69
|
+
bug: "fix",
|
|
70
|
+
feature: "feat",
|
|
71
|
+
"new-feature": "feat",
|
|
72
|
+
improvement: "refactor",
|
|
73
|
+
improve: "refactor",
|
|
74
|
+
cleanup: "chore",
|
|
75
|
+
maintenance: "chore",
|
|
76
|
+
style: "refactor",
|
|
77
|
+
formatting: "chore",
|
|
78
|
+
config: "chore",
|
|
79
|
+
configuration: "chore",
|
|
80
|
+
build: "build",
|
|
81
|
+
ci: "ci",
|
|
82
|
+
test: "test",
|
|
83
|
+
tests: "test",
|
|
84
|
+
testing: "test",
|
|
85
|
+
docs: "docs",
|
|
86
|
+
doc: "docs",
|
|
87
|
+
documentation: "docs",
|
|
88
|
+
perf: "perf",
|
|
89
|
+
performance: "perf",
|
|
90
|
+
refactor: "refactor",
|
|
91
|
+
feat: "feat",
|
|
92
|
+
fix: "fix",
|
|
93
|
+
chore: "chore"
|
|
94
|
+
};
|
|
95
|
+
return aliasMap[value.toLowerCase()] ?? "feat";
|
|
96
|
+
}
|
|
97
|
+
var suggestionSchema = z.object({
|
|
98
|
+
type: z.string().transform(coerceCommitType),
|
|
99
|
+
scope: z.string().optional().transform((v) => v && v.trim().length > 0 ? v.trim() : void 0),
|
|
100
|
+
subject: z.string().min(1),
|
|
101
|
+
body: z.string().optional()
|
|
102
|
+
});
|
|
103
|
+
var providerResponseSchema = z.object({
|
|
104
|
+
summary: z.string().min(1),
|
|
105
|
+
suggestions: z.array(suggestionSchema).min(1).max(10)
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// src/core/ai/providers/groq.ts
|
|
109
|
+
var GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions";
|
|
110
|
+
var GroqAIProvider = class {
|
|
111
|
+
constructor(apiKey, model) {
|
|
112
|
+
this.apiKey = apiKey;
|
|
113
|
+
this.model = model;
|
|
114
|
+
}
|
|
115
|
+
apiKey;
|
|
116
|
+
model;
|
|
117
|
+
async generateCommitSuggestions(input2) {
|
|
118
|
+
const response = await fetch(GROQ_API_URL, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
model: this.model,
|
|
126
|
+
temperature: 0.2,
|
|
127
|
+
response_format: { type: "json_object" },
|
|
128
|
+
messages: [
|
|
129
|
+
{
|
|
130
|
+
role: "system",
|
|
131
|
+
content: "You are a precise commit assistant. Return valid JSON only."
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
role: "user",
|
|
135
|
+
content: buildPrompt(input2)
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
})
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const body = await response.text();
|
|
142
|
+
throw new Error(`Groq API error (${response.status}): ${body}`);
|
|
143
|
+
}
|
|
144
|
+
const payload = await response.json();
|
|
145
|
+
const rawContent = payload.choices?.[0]?.message?.content;
|
|
146
|
+
if (!rawContent) {
|
|
147
|
+
throw new Error("Groq API returned an empty response.");
|
|
148
|
+
}
|
|
149
|
+
const parsed = providerResponseSchema.parse(JSON.parse(rawContent));
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// src/core/ai/create-provider.ts
|
|
155
|
+
function createAIProvider(config) {
|
|
156
|
+
if (config.provider === "groq") {
|
|
157
|
+
const apiKey = config.groqApiKey;
|
|
158
|
+
if (!apiKey) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"Missing Groq API key. Run `patchwise setup` to configure it."
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return new GroqAIProvider(apiKey, config.model);
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`Unsupported provider: ${config.provider}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/core/commit/diff.ts
|
|
169
|
+
function truncateDiff(diff, maxChars = 8e3) {
|
|
170
|
+
if (diff.length <= maxChars) {
|
|
171
|
+
return diff;
|
|
172
|
+
}
|
|
173
|
+
const files = parseDiffFiles(diff);
|
|
174
|
+
if (files.length === 0) {
|
|
175
|
+
return diff.slice(0, maxChars) + "\n\n[diff truncated]";
|
|
176
|
+
}
|
|
177
|
+
const statLines = files.map((f) => `${f.path} (+${f.added}/-${f.removed})`);
|
|
178
|
+
let result = `Files changed: ${files.length}
|
|
179
|
+
${statLines.join("\n")}
|
|
180
|
+
|
|
181
|
+
`;
|
|
182
|
+
const maxLinesPerFile = Math.max(10, Math.floor((maxChars - result.length) / files.length / 6));
|
|
183
|
+
for (const file of files) {
|
|
184
|
+
const header = `--- ${file.path} ---`;
|
|
185
|
+
const lines = file.diffLines.slice(0, maxLinesPerFile);
|
|
186
|
+
const truncated = file.diffLines.length > maxLinesPerFile ? `
|
|
187
|
+
[... ${file.diffLines.length - maxLinesPerFile} more lines]` : "";
|
|
188
|
+
const fileBlock = `${header}
|
|
189
|
+
${lines.join("\n")}${truncated}
|
|
190
|
+
`;
|
|
191
|
+
if (result.length + fileBlock.length > maxChars) {
|
|
192
|
+
result += "\n[diff truncated \u2014 remaining files omitted]";
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
result += fileBlock;
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
function parseDiffFiles(diff) {
|
|
200
|
+
const files = [];
|
|
201
|
+
const lines = diff.split("\n");
|
|
202
|
+
let current = null;
|
|
203
|
+
for (const line of lines) {
|
|
204
|
+
if (line.startsWith("diff --git ")) {
|
|
205
|
+
if (current) files.push(current);
|
|
206
|
+
const parts = line.split(" ");
|
|
207
|
+
const path2 = parts[2]?.replace(/^a\//, "") ?? "unknown";
|
|
208
|
+
current = { path: path2, diffLines: [], added: 0, removed: 0 };
|
|
209
|
+
} else if (current) {
|
|
210
|
+
current.diffLines.push(line);
|
|
211
|
+
if (line.startsWith("+")) current.added++;
|
|
212
|
+
else if (line.startsWith("-")) current.removed++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (current) files.push(current);
|
|
216
|
+
return files;
|
|
217
|
+
}
|
|
218
|
+
function extractFileNamesFromDiff(diff) {
|
|
219
|
+
const fileNames = /* @__PURE__ */ new Set();
|
|
220
|
+
for (const line of diff.split("\n")) {
|
|
221
|
+
if (!line.startsWith("diff --git ")) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const parts = line.split(" ");
|
|
225
|
+
const filePath = parts[2]?.replace(/^a\//, "");
|
|
226
|
+
if (filePath) {
|
|
227
|
+
fileNames.add(filePath);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return [...fileNames];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/core/commit/format.ts
|
|
234
|
+
function formatCommitMessage(suggestion) {
|
|
235
|
+
const scope = suggestion.scope ? `(${suggestion.scope})` : "";
|
|
236
|
+
return `${suggestion.type}${scope}: ${suggestion.subject}`;
|
|
237
|
+
}
|
|
238
|
+
function formatCommitMessageWithBody(suggestion) {
|
|
239
|
+
const header = formatCommitMessage(suggestion);
|
|
240
|
+
if (!suggestion.body) return header;
|
|
241
|
+
return `${header}
|
|
242
|
+
|
|
243
|
+
${suggestion.body}`;
|
|
244
|
+
}
|
|
245
|
+
function applyScopeOverride(suggestion, scope, disableScope) {
|
|
246
|
+
if (disableScope) {
|
|
247
|
+
return {
|
|
248
|
+
...suggestion,
|
|
249
|
+
scope: void 0
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (!scope) {
|
|
253
|
+
return suggestion;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
...suggestion,
|
|
257
|
+
scope
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function truncateSubject(subject, maxLength) {
|
|
261
|
+
if (subject.length <= maxLength) {
|
|
262
|
+
return subject;
|
|
263
|
+
}
|
|
264
|
+
return subject.slice(0, maxLength - 1).trimEnd();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/cli/services.ts
|
|
268
|
+
async function generateSuggestionsFromDiff(diff, config, options) {
|
|
269
|
+
const provider = createAIProvider(config);
|
|
270
|
+
const input2 = {
|
|
271
|
+
diff: truncateDiff(diff),
|
|
272
|
+
fileNames: extractFileNamesFromDiff(diff),
|
|
273
|
+
language: options?.language ?? config.language,
|
|
274
|
+
scopeStrategy: options?.noScope ? "none" : options?.scope ? "manual" : config.scopeStrategy,
|
|
275
|
+
scope: options?.scope,
|
|
276
|
+
maxSubjectLength: config.maxSubjectLength
|
|
277
|
+
};
|
|
278
|
+
const result = await provider.generateCommitSuggestions(input2);
|
|
279
|
+
return {
|
|
280
|
+
...result,
|
|
281
|
+
suggestions: result.suggestions.map(
|
|
282
|
+
(suggestion) => normalizeSuggestion(
|
|
283
|
+
suggestion,
|
|
284
|
+
config.maxSubjectLength,
|
|
285
|
+
options?.scope,
|
|
286
|
+
options?.noScope
|
|
287
|
+
)
|
|
288
|
+
).slice(0, 3)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function normalizeSuggestion(suggestion, maxSubjectLength, scope, noScope) {
|
|
292
|
+
const withScope = applyScopeOverride(suggestion, scope, noScope);
|
|
293
|
+
return {
|
|
294
|
+
...withScope,
|
|
295
|
+
subject: truncateSubject(withScope.subject, maxSubjectLength)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/core/git/client.ts
|
|
300
|
+
import { execFile } from "child_process";
|
|
301
|
+
import { promisify } from "util";
|
|
302
|
+
var execFileAsync = promisify(execFile);
|
|
303
|
+
async function assertGitRepository(cwd = process.cwd()) {
|
|
304
|
+
try {
|
|
305
|
+
await execGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
306
|
+
} catch {
|
|
307
|
+
throw new Error("Current directory is not a Git repository.");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function getStagedDiff(cwd = process.cwd()) {
|
|
311
|
+
return execGit(["diff", "--cached", "--no-ext-diff"], cwd);
|
|
312
|
+
}
|
|
313
|
+
async function getModifiedFiles(cwd = process.cwd()) {
|
|
314
|
+
const output = await execGit(["status", "--short"], cwd);
|
|
315
|
+
return output.split("\n").filter(Boolean).map((line) => ({
|
|
316
|
+
indexStatus: line[0] ?? " ",
|
|
317
|
+
workingTreeStatus: line[1] ?? " ",
|
|
318
|
+
path: line.slice(3).trim()
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
async function stageFiles(files, cwd = process.cwd()) {
|
|
322
|
+
if (files.length === 0) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await execGit(["add", "--", ...files], cwd);
|
|
326
|
+
}
|
|
327
|
+
async function stageAll(cwd = process.cwd()) {
|
|
328
|
+
await execGit(["add", "-A"], cwd);
|
|
329
|
+
}
|
|
330
|
+
async function createCommit(message, cwd = process.cwd()) {
|
|
331
|
+
await execGit(["commit", "-m", message], cwd);
|
|
332
|
+
}
|
|
333
|
+
async function pushCurrentBranch(cwd = process.cwd()) {
|
|
334
|
+
await execGit(["push"], cwd);
|
|
335
|
+
}
|
|
336
|
+
async function getCurrentBranch(cwd = process.cwd()) {
|
|
337
|
+
return execGit(["branch", "--show-current"], cwd);
|
|
338
|
+
}
|
|
339
|
+
async function execGit(args, cwd) {
|
|
340
|
+
try {
|
|
341
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
342
|
+
cwd,
|
|
343
|
+
encoding: "utf8",
|
|
344
|
+
maxBuffer: 1024 * 1024 * 10
|
|
345
|
+
});
|
|
346
|
+
return stdout.trim();
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error instanceof Error && "stderr" in error) {
|
|
349
|
+
const stderr = String(error.stderr ?? "").trim();
|
|
350
|
+
throw new Error(stderr || "Git command failed.", { cause: error });
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/core/ui/output.ts
|
|
357
|
+
import chalk from "chalk";
|
|
358
|
+
var BORDER = "\u2500";
|
|
359
|
+
var WIDTH = 56;
|
|
360
|
+
function divider(label) {
|
|
361
|
+
const line = chalk.dim(BORDER.repeat(WIDTH));
|
|
362
|
+
if (!label) return line;
|
|
363
|
+
const padded = ` ${label} `;
|
|
364
|
+
const remaining = Math.max(0, WIDTH - padded.length);
|
|
365
|
+
const half = Math.floor(remaining / 2);
|
|
366
|
+
return chalk.dim(BORDER.repeat(half)) + chalk.dim(padded) + chalk.dim(BORDER.repeat(remaining - half));
|
|
367
|
+
}
|
|
368
|
+
function typeBadge(type) {
|
|
369
|
+
const colors = {
|
|
370
|
+
feat: chalk.cyan,
|
|
371
|
+
fix: chalk.yellow,
|
|
372
|
+
refactor: chalk.magenta,
|
|
373
|
+
docs: chalk.blue,
|
|
374
|
+
test: chalk.green,
|
|
375
|
+
chore: chalk.gray,
|
|
376
|
+
perf: chalk.red,
|
|
377
|
+
build: chalk.yellow,
|
|
378
|
+
ci: chalk.blueBright
|
|
379
|
+
};
|
|
380
|
+
const color = colors[type] ?? chalk.white;
|
|
381
|
+
return color(` ${type.toUpperCase()} `);
|
|
382
|
+
}
|
|
383
|
+
function formatSuggestionLine(suggestion, index) {
|
|
384
|
+
const header = formatCommitMessage(suggestion);
|
|
385
|
+
const badge = typeBadge(suggestion.type);
|
|
386
|
+
const number = chalk.bold(`${index + 1}.`);
|
|
387
|
+
const line = ` ${number} ${badge} ${chalk.white(header)}`;
|
|
388
|
+
if (suggestion.body) {
|
|
389
|
+
const bodyLines = suggestion.body.split("\n");
|
|
390
|
+
const preview = bodyLines.slice(0, 3).join("\n");
|
|
391
|
+
const more = bodyLines.length > 3 ? chalk.dim(` ... +${bodyLines.length - 3} more lines`) : "";
|
|
392
|
+
return `${line}
|
|
393
|
+
${chalk.dim(` ${preview}`)}${more ? `
|
|
394
|
+
${more}` : ""}`;
|
|
395
|
+
}
|
|
396
|
+
return line;
|
|
397
|
+
}
|
|
398
|
+
function printSuggestionResult(result) {
|
|
399
|
+
console.log();
|
|
400
|
+
console.log(divider());
|
|
401
|
+
console.log(` ${chalk.bold(chalk.white("\u{1F4DD} Commit Suggestions"))}`);
|
|
402
|
+
console.log(divider());
|
|
403
|
+
console.log();
|
|
404
|
+
console.log(` ${chalk.dim("Summary:")}`);
|
|
405
|
+
console.log(` ${chalk.italic(chalk.white(result.summary))}`);
|
|
406
|
+
console.log();
|
|
407
|
+
console.log(` ${chalk.dim("Suggestions:")}`);
|
|
408
|
+
console.log();
|
|
409
|
+
result.suggestions.forEach((suggestion, index) => {
|
|
410
|
+
console.log(formatSuggestionLine(suggestion, index));
|
|
411
|
+
});
|
|
412
|
+
console.log();
|
|
413
|
+
console.log(divider());
|
|
414
|
+
console.log();
|
|
415
|
+
}
|
|
416
|
+
function printCommitCreated(message, branch) {
|
|
417
|
+
console.log();
|
|
418
|
+
console.log(divider());
|
|
419
|
+
console.log(` ${chalk.green("\u2714")} ${chalk.green("Commit created:")}`);
|
|
420
|
+
console.log(` ${chalk.white(message)}`);
|
|
421
|
+
if (branch) {
|
|
422
|
+
console.log(` ${chalk.dim(`on branch ${branch}`)}`);
|
|
423
|
+
}
|
|
424
|
+
console.log(divider());
|
|
425
|
+
console.log();
|
|
426
|
+
}
|
|
427
|
+
function printPushed(branch) {
|
|
428
|
+
console.log();
|
|
429
|
+
console.log(divider());
|
|
430
|
+
console.log(
|
|
431
|
+
` ${chalk.green("\u2714")} ${chalk.green(`Pushed to origin/${branch}`)}`
|
|
432
|
+
);
|
|
433
|
+
console.log(divider());
|
|
434
|
+
console.log();
|
|
435
|
+
}
|
|
436
|
+
function printCancelled() {
|
|
437
|
+
console.log(` ${chalk.yellow("\u21A9")} ${chalk.yellow("Cancelled.")}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/core/ui/prompts.ts
|
|
441
|
+
import { checkbox, confirm, input, password, select } from "@inquirer/prompts";
|
|
442
|
+
import chalk2 from "chalk";
|
|
443
|
+
|
|
444
|
+
// src/core/ai/models.ts
|
|
445
|
+
var GROQ_MODELS_URL = "https://api.groq.com/openai/v1/models";
|
|
446
|
+
async function fetchGroqModels(apiKey) {
|
|
447
|
+
const response = await fetch(GROQ_MODELS_URL, {
|
|
448
|
+
headers: {
|
|
449
|
+
Authorization: `Bearer ${apiKey}`
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
if (!response.ok) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Failed to fetch models (${response.status}). Check your API key.`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const data = await response.json();
|
|
458
|
+
return (data.data ?? []).filter((m) => m.active !== false).map((m) => ({
|
|
459
|
+
id: m.id,
|
|
460
|
+
name: formatModelName(m.id),
|
|
461
|
+
active: m.active ?? true
|
|
462
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
463
|
+
}
|
|
464
|
+
function formatModelName(id) {
|
|
465
|
+
return id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/core/ui/prompts.ts
|
|
469
|
+
async function promptForFiles(files) {
|
|
470
|
+
return checkbox({
|
|
471
|
+
message: chalk2.bold("Select files to stage"),
|
|
472
|
+
choices: files.map((file) => ({
|
|
473
|
+
name: `${getStatusIcon(file.indexStatus, file.workingTreeStatus)} ${file.path}`,
|
|
474
|
+
value: file.path
|
|
475
|
+
})),
|
|
476
|
+
required: false
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function getStatusIcon(index, working) {
|
|
480
|
+
if (index !== " ") return chalk2.green(`[${index}]`);
|
|
481
|
+
if (working !== " ") return chalk2.yellow(`[${working}]`);
|
|
482
|
+
return chalk2.dim(`[\xB7]`);
|
|
483
|
+
}
|
|
484
|
+
async function promptForSuggestion(suggestions) {
|
|
485
|
+
const selected = await select({
|
|
486
|
+
message: chalk2.bold("Select a commit message"),
|
|
487
|
+
choices: [
|
|
488
|
+
...suggestions.map((suggestion, index) => ({
|
|
489
|
+
name: `${chalk2.bold(`${index + 1}.`)} ${formatCommitMessageWithBody(suggestion)}`,
|
|
490
|
+
value: formatCommitMessageWithBody(suggestion)
|
|
491
|
+
})),
|
|
492
|
+
{
|
|
493
|
+
name: chalk2.italic("\u270F\uFE0F Write a custom message"),
|
|
494
|
+
value: "__custom__"
|
|
495
|
+
}
|
|
496
|
+
]
|
|
497
|
+
});
|
|
498
|
+
if (selected !== "__custom__") {
|
|
499
|
+
return selected;
|
|
500
|
+
}
|
|
501
|
+
return input({
|
|
502
|
+
message: chalk2.bold("Commit message"),
|
|
503
|
+
validate(value) {
|
|
504
|
+
return value.trim().length > 0 || "Commit message cannot be empty.";
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
async function confirmAction(message, defaultValue = true) {
|
|
509
|
+
return confirm({
|
|
510
|
+
message: chalk2.yellow(`? ${message}`),
|
|
511
|
+
default: defaultValue
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
async function promptForSetup(defaults) {
|
|
515
|
+
const provider = await select({
|
|
516
|
+
message: chalk2.bold("Select your AI provider"),
|
|
517
|
+
choices: [{ name: "Groq", value: "groq" }],
|
|
518
|
+
default: defaults.provider ?? "groq"
|
|
519
|
+
});
|
|
520
|
+
const groqApiKey = await password({
|
|
521
|
+
message: chalk2.bold("Groq API key") + chalk2.dim(" (https://console.groq.com/keys)"),
|
|
522
|
+
mask: "*",
|
|
523
|
+
validate(value) {
|
|
524
|
+
return value.trim().length > 0 || "API key is required.";
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
const trimmedApiKey = groqApiKey.trim();
|
|
528
|
+
let modelChoices = [];
|
|
529
|
+
try {
|
|
530
|
+
const models = await fetchGroqModels(trimmedApiKey);
|
|
531
|
+
modelChoices = models.map((m) => ({
|
|
532
|
+
name: `${m.name}`,
|
|
533
|
+
value: m.id
|
|
534
|
+
}));
|
|
535
|
+
} catch {
|
|
536
|
+
}
|
|
537
|
+
let model;
|
|
538
|
+
if (modelChoices.length > 0) {
|
|
539
|
+
const defaultModel = defaults.model ?? "llama-3.3-70b-versatile";
|
|
540
|
+
const defaultChoice = modelChoices.find((c) => c.value === defaultModel);
|
|
541
|
+
model = await select({
|
|
542
|
+
message: chalk2.bold("Select a Groq model"),
|
|
543
|
+
choices: modelChoices,
|
|
544
|
+
default: defaultChoice?.value
|
|
545
|
+
});
|
|
546
|
+
} else {
|
|
547
|
+
model = await input({
|
|
548
|
+
message: chalk2.bold("Groq model") + chalk2.dim(" (enter manually)"),
|
|
549
|
+
default: defaults.model ?? "llama-3.3-70b-versatile",
|
|
550
|
+
validate(value) {
|
|
551
|
+
return value.trim().length > 0 || "Model is required.";
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
const language = await select({
|
|
556
|
+
message: chalk2.bold("Default commit language"),
|
|
557
|
+
choices: [
|
|
558
|
+
{ name: "\u{1F1EC}\u{1F1E7} English", value: "en" },
|
|
559
|
+
{ name: "\u{1F1EB}\u{1F1F7} French", value: "fr" }
|
|
560
|
+
],
|
|
561
|
+
default: defaults.language ?? "en"
|
|
562
|
+
});
|
|
563
|
+
return {
|
|
564
|
+
provider,
|
|
565
|
+
model: model.trim(),
|
|
566
|
+
language,
|
|
567
|
+
groqApiKey: trimmedApiKey
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/cli/commands/commit.ts
|
|
572
|
+
async function runCommitCommand(context, options) {
|
|
573
|
+
await assertGitRepository(context.cwd);
|
|
574
|
+
if (options.all) {
|
|
575
|
+
await stageAll(context.cwd);
|
|
576
|
+
}
|
|
577
|
+
if (options.select) {
|
|
578
|
+
const files = await getModifiedFiles(context.cwd);
|
|
579
|
+
if (files.length === 0) {
|
|
580
|
+
throw new Error("No modified files found.");
|
|
581
|
+
}
|
|
582
|
+
const selected = await promptForFiles(files);
|
|
583
|
+
if (selected.length === 0) {
|
|
584
|
+
throw new Error("No files selected for staging.");
|
|
585
|
+
}
|
|
586
|
+
await stageFiles(selected, context.cwd);
|
|
587
|
+
}
|
|
588
|
+
const diff = await getStagedDiff(context.cwd);
|
|
589
|
+
if (!diff) {
|
|
590
|
+
throw new Error(
|
|
591
|
+
"No staged changes found. Stage files before running commit."
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const config = {
|
|
595
|
+
...context.config,
|
|
596
|
+
language: options.lang ?? context.config.language,
|
|
597
|
+
provider: options.provider ?? context.config.provider,
|
|
598
|
+
model: options.model ?? context.config.model
|
|
599
|
+
};
|
|
600
|
+
const result = await generateSuggestionsFromDiff(diff, config, {
|
|
601
|
+
language: options.lang,
|
|
602
|
+
scope: options.scope,
|
|
603
|
+
noScope: options.noScope
|
|
604
|
+
});
|
|
605
|
+
printSuggestionResult(result);
|
|
606
|
+
const message = await promptForSuggestion(result.suggestions);
|
|
607
|
+
const shouldCommit = options.yes || !config.confirmBeforeCommit || await confirmAction(`Commit with "${message}"?`);
|
|
608
|
+
if (!shouldCommit) {
|
|
609
|
+
printCancelled();
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const branch = await getCurrentBranch(context.cwd);
|
|
613
|
+
await createCommit(message, context.cwd);
|
|
614
|
+
printCommitCreated(message, branch);
|
|
615
|
+
if (!options.push) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const shouldPush = options.yes || !config.confirmBeforePush || await confirmAction(`Push to origin/${branch}?`);
|
|
619
|
+
if (!shouldPush) {
|
|
620
|
+
printCancelled();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
await pushCurrentBranch(context.cwd);
|
|
624
|
+
printPushed(branch);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/core/config/load-config.ts
|
|
628
|
+
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
629
|
+
import os from "os";
|
|
630
|
+
import path from "path";
|
|
631
|
+
import { z as z2 } from "zod";
|
|
632
|
+
|
|
633
|
+
// src/core/config/defaults.ts
|
|
634
|
+
var DEFAULT_CONFIG = {
|
|
635
|
+
provider: "groq",
|
|
636
|
+
model: "llama-3.3-70b-versatile",
|
|
637
|
+
commitConvention: "conventional",
|
|
638
|
+
language: "en",
|
|
639
|
+
maxSubjectLength: 72,
|
|
640
|
+
confirmBeforeCommit: true,
|
|
641
|
+
confirmBeforePush: true,
|
|
642
|
+
scopeStrategy: "auto",
|
|
643
|
+
onboardingComplete: false
|
|
644
|
+
};
|
|
645
|
+
var CONFIG_FILE_NAME = "patchwise.config.json";
|
|
646
|
+
|
|
647
|
+
// src/core/config/load-config.ts
|
|
648
|
+
var configSchema = z2.object({
|
|
649
|
+
provider: z2.literal("groq").optional(),
|
|
650
|
+
model: z2.string().min(1).optional(),
|
|
651
|
+
commitConvention: z2.literal("conventional").optional(),
|
|
652
|
+
language: z2.enum(["en", "fr"]).optional(),
|
|
653
|
+
maxSubjectLength: z2.number().int().positive().optional(),
|
|
654
|
+
confirmBeforeCommit: z2.boolean().optional(),
|
|
655
|
+
confirmBeforePush: z2.boolean().optional(),
|
|
656
|
+
scopeStrategy: z2.enum(["auto", "manual", "none"]).optional(),
|
|
657
|
+
groqApiKey: z2.string().min(1).optional(),
|
|
658
|
+
onboardingComplete: z2.boolean().optional()
|
|
659
|
+
});
|
|
660
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
661
|
+
const fileConfig = await loadProjectConfig(cwd);
|
|
662
|
+
const userConfig = await loadUserConfig();
|
|
663
|
+
return {
|
|
664
|
+
...DEFAULT_CONFIG,
|
|
665
|
+
...userConfig,
|
|
666
|
+
...fileConfig,
|
|
667
|
+
provider: readEnv("PATCHWISE_PROVIDER", DEFAULT_CONFIG.provider),
|
|
668
|
+
model: process.env.PATCHWISE_MODEL ?? fileConfig.model ?? userConfig.model ?? DEFAULT_CONFIG.model,
|
|
669
|
+
language: readEnv(
|
|
670
|
+
"PATCHWISE_LANGUAGE",
|
|
671
|
+
fileConfig.language ?? userConfig.language ?? DEFAULT_CONFIG.language
|
|
672
|
+
),
|
|
673
|
+
groqApiKey: process.env.GROQ_API_KEY ?? fileConfig.groqApiKey ?? userConfig.groqApiKey,
|
|
674
|
+
onboardingComplete: fileConfig.onboardingComplete ?? userConfig.onboardingComplete ?? DEFAULT_CONFIG.onboardingComplete
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
async function initConfigFile(cwd = process.cwd()) {
|
|
678
|
+
const configPath = path.join(cwd, CONFIG_FILE_NAME);
|
|
679
|
+
try {
|
|
680
|
+
await access(configPath);
|
|
681
|
+
return configPath;
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (!isMissingFile(error)) {
|
|
684
|
+
throw error;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
await writeFile(
|
|
688
|
+
`${configPath}`,
|
|
689
|
+
`${JSON.stringify(DEFAULT_CONFIG, null, 2)}
|
|
690
|
+
`,
|
|
691
|
+
"utf8"
|
|
692
|
+
);
|
|
693
|
+
return configPath;
|
|
694
|
+
}
|
|
695
|
+
async function loadUserConfig() {
|
|
696
|
+
const configPath = getUserConfigPath();
|
|
697
|
+
try {
|
|
698
|
+
const raw = await readFile(configPath, "utf8");
|
|
699
|
+
return configSchema.parse(JSON.parse(raw));
|
|
700
|
+
} catch (error) {
|
|
701
|
+
if (!isMissingFile(error)) {
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return {};
|
|
706
|
+
}
|
|
707
|
+
async function saveUserConfig(config) {
|
|
708
|
+
const configPath = getUserConfigPath();
|
|
709
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
710
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
711
|
+
`, "utf8");
|
|
712
|
+
return configPath;
|
|
713
|
+
}
|
|
714
|
+
function getUserConfigPath() {
|
|
715
|
+
if (process.platform === "win32") {
|
|
716
|
+
const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
|
|
717
|
+
return path.join(appData, "patchwise", "config.json");
|
|
718
|
+
}
|
|
719
|
+
if (process.platform === "darwin") {
|
|
720
|
+
return path.join(
|
|
721
|
+
os.homedir(),
|
|
722
|
+
"Library",
|
|
723
|
+
"Application Support",
|
|
724
|
+
"patchwise",
|
|
725
|
+
"config.json"
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
729
|
+
return path.join(xdgConfigHome, "patchwise", "config.json");
|
|
730
|
+
}
|
|
731
|
+
async function loadProjectConfig(cwd) {
|
|
732
|
+
const configPath = path.join(cwd, CONFIG_FILE_NAME);
|
|
733
|
+
try {
|
|
734
|
+
const raw = await readFile(configPath, "utf8");
|
|
735
|
+
return configSchema.parse(JSON.parse(raw));
|
|
736
|
+
} catch (error) {
|
|
737
|
+
if (!isMissingFile(error)) {
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return {};
|
|
742
|
+
}
|
|
743
|
+
function isMissingFile(error) {
|
|
744
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
745
|
+
}
|
|
746
|
+
function readEnv(name, fallback) {
|
|
747
|
+
const value = process.env[name];
|
|
748
|
+
if (!value) {
|
|
749
|
+
return fallback;
|
|
750
|
+
}
|
|
751
|
+
return value;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/cli/commands/config.ts
|
|
755
|
+
async function runConfigInitCommand(context) {
|
|
756
|
+
const configPath = await initConfigFile(context.cwd);
|
|
757
|
+
console.log(`Config ready at ${configPath}`);
|
|
758
|
+
}
|
|
759
|
+
async function runSetupCommand(context, options) {
|
|
760
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
761
|
+
if (options?.silentWhenNonInteractive) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
throw new Error(
|
|
765
|
+
"Interactive setup requires a TTY. Run `patchwise setup` in a terminal."
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
const currentConfig = await loadConfig(context.cwd);
|
|
769
|
+
const answers = await promptForSetup(currentConfig);
|
|
770
|
+
const configPath = await saveUserConfig({
|
|
771
|
+
provider: answers.provider,
|
|
772
|
+
model: answers.model,
|
|
773
|
+
language: answers.language,
|
|
774
|
+
groqApiKey: answers.groqApiKey,
|
|
775
|
+
onboardingComplete: true
|
|
776
|
+
});
|
|
777
|
+
context.config = await loadConfig(context.cwd);
|
|
778
|
+
console.log(`User config saved to ${configPath}`);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/cli/commands/stage.ts
|
|
782
|
+
async function runStageCommand(context) {
|
|
783
|
+
await assertGitRepository(context.cwd);
|
|
784
|
+
const files = await getModifiedFiles(context.cwd);
|
|
785
|
+
if (files.length === 0) {
|
|
786
|
+
throw new Error("No modified files found.");
|
|
787
|
+
}
|
|
788
|
+
const selected = await promptForFiles(files);
|
|
789
|
+
if (selected.length === 0) {
|
|
790
|
+
console.log("No files selected.");
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
await stageFiles(selected, context.cwd);
|
|
794
|
+
console.log(`Staged ${selected.length} file(s).`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/cli/commands/suggest.ts
|
|
798
|
+
async function runSuggestCommand(context) {
|
|
799
|
+
await assertGitRepository(context.cwd);
|
|
800
|
+
const diff = await getStagedDiff(context.cwd);
|
|
801
|
+
if (!diff) {
|
|
802
|
+
throw new Error(
|
|
803
|
+
"No staged changes found. Stage files before running suggest."
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
const result = await generateSuggestionsFromDiff(diff, context.config);
|
|
807
|
+
printSuggestionResult(result);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/cli/program.ts
|
|
811
|
+
async function createProgram(cwd = process.cwd()) {
|
|
812
|
+
const context = { cwd, config: await loadConfig(cwd) };
|
|
813
|
+
const program2 = new Command();
|
|
814
|
+
program2.name("patchwise").description("AI-assisted Git commits with explicit human validation.").version("0.1.0");
|
|
815
|
+
program2.command("suggest").description("Generate commit suggestions from staged changes.").action(async () => handleCommand(() => runSuggestCommand(context)));
|
|
816
|
+
program2.command("stage").description("Interactively select files to stage.").action(async () => handleCommand(() => runStageCommand(context)));
|
|
817
|
+
program2.command("commit").description(
|
|
818
|
+
"Generate suggestions and create a commit from staged changes."
|
|
819
|
+
).option("--all", "Stage all changes before generating a commit.").option(
|
|
820
|
+
"--select",
|
|
821
|
+
"Select files interactively before generating a commit."
|
|
822
|
+
).option("--push", "Push after creating the commit.").option("--yes", "Skip commit and push confirmations.").option("--lang <lang>", "Commit message language (en|fr).").option("--provider <provider>", "AI provider to use.").option("--model <model>", "Model name to use.").option("--scope <scope>", "Force a commit scope.").option("--no-scope", "Disable commit scope.").action(
|
|
823
|
+
async (options) => handleCommand(
|
|
824
|
+
() => runCommitCommand(context, sanitizeCommitOptions(options))
|
|
825
|
+
)
|
|
826
|
+
);
|
|
827
|
+
program2.command("setup").description(
|
|
828
|
+
"Run the one-time interactive setup and store user configuration."
|
|
829
|
+
).action(async () => handleCommand(async () => runSetupCommand(context)));
|
|
830
|
+
const configCommand = program2.command("config").description("Manage project configuration.");
|
|
831
|
+
configCommand.command("init").description("Create patchwise.config.json if it does not exist.").action(async () => handleCommand(() => runConfigInitCommand(context)));
|
|
832
|
+
program2.hook("preAction", async (_, actionCommand) => {
|
|
833
|
+
if (actionCommand.name() === "setup" || actionCommand.name() === "init") {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
context.config = await loadConfig(cwd);
|
|
837
|
+
if (context.config.onboardingComplete && context.config.groqApiKey) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
await runSetupCommand(context, { silentWhenNonInteractive: true });
|
|
841
|
+
context.config = await loadConfig(cwd);
|
|
842
|
+
});
|
|
843
|
+
return program2;
|
|
844
|
+
}
|
|
845
|
+
function sanitizeCommitOptions(options) {
|
|
846
|
+
return {
|
|
847
|
+
...options,
|
|
848
|
+
lang: options.lang === "fr" ? "fr" : "en",
|
|
849
|
+
provider: options.provider ?? "groq"
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
async function handleCommand(action) {
|
|
853
|
+
try {
|
|
854
|
+
await action();
|
|
855
|
+
} catch (error) {
|
|
856
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
857
|
+
console.error(`patchwise: ${message}`);
|
|
858
|
+
process.exitCode = 1;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// src/bin/patchwise.ts
|
|
863
|
+
var program = await createProgram();
|
|
864
|
+
await program.parseAsync(process.argv);
|
|
865
|
+
//# sourceMappingURL=patchwise.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/program.ts","../src/core/ai/prompt.ts","../src/core/ai/schemas.ts","../src/core/ai/providers/groq.ts","../src/core/ai/create-provider.ts","../src/core/commit/diff.ts","../src/core/commit/format.ts","../src/cli/services.ts","../src/core/git/client.ts","../src/core/ui/output.ts","../src/core/ui/prompts.ts","../src/core/ai/models.ts","../src/cli/commands/commit.ts","../src/core/config/load-config.ts","../src/core/config/defaults.ts","../src/cli/commands/config.ts","../src/cli/commands/stage.ts","../src/cli/commands/suggest.ts","../src/bin/patchwise.ts"],"sourcesContent":["import { Command } from \"commander\";\n\nimport { runCommitCommand } from \"@/cli/commands/commit\";\nimport { runConfigInitCommand, runSetupCommand } from \"@/cli/commands/config\";\nimport { runStageCommand } from \"@/cli/commands/stage\";\nimport { runSuggestCommand } from \"@/cli/commands/suggest\";\nimport type { CommandContext } from \"@/cli/context\";\nimport { loadConfig } from \"@/core/config/load-config\";\nimport type { CommitCommandOptions, ProviderName } from \"@/types\";\n\nexport async function createProgram(cwd = process.cwd()): Promise<Command> {\n const context: CommandContext = { cwd, config: await loadConfig(cwd) };\n\n const program = new Command();\n\n program\n .name(\"patchwise\")\n .description(\"AI-assisted Git commits with explicit human validation.\")\n .version(\"0.1.0\");\n\n program\n .command(\"suggest\")\n .description(\"Generate commit suggestions from staged changes.\")\n .action(async () => handleCommand(() => runSuggestCommand(context)));\n\n program\n .command(\"stage\")\n .description(\"Interactively select files to stage.\")\n .action(async () => handleCommand(() => runStageCommand(context)));\n\n program\n .command(\"commit\")\n .description(\n \"Generate suggestions and create a commit from staged changes.\",\n )\n .option(\"--all\", \"Stage all changes before generating a commit.\")\n .option(\n \"--select\",\n \"Select files interactively before generating a commit.\",\n )\n .option(\"--push\", \"Push after creating the commit.\")\n .option(\"--yes\", \"Skip commit and push confirmations.\")\n .option(\"--lang <lang>\", \"Commit message language (en|fr).\")\n .option(\"--provider <provider>\", \"AI provider to use.\")\n .option(\"--model <model>\", \"Model name to use.\")\n .option(\"--scope <scope>\", \"Force a commit scope.\")\n .option(\"--no-scope\", \"Disable commit scope.\")\n .action(async (options: CommitCommandOptions) =>\n handleCommand(() =>\n runCommitCommand(context, sanitizeCommitOptions(options)),\n ),\n );\n\n program\n .command(\"setup\")\n .description(\n \"Run the one-time interactive setup and store user configuration.\",\n )\n .action(async () => handleCommand(async () => runSetupCommand(context)));\n\n const configCommand = program\n .command(\"config\")\n .description(\"Manage project configuration.\");\n configCommand\n .command(\"init\")\n .description(\"Create patchwise.config.json if it does not exist.\")\n .action(async () => handleCommand(() => runConfigInitCommand(context)));\n\n program.hook(\"preAction\", async (_, actionCommand) => {\n if (actionCommand.name() === \"setup\" || actionCommand.name() === \"init\") {\n return;\n }\n\n context.config = await loadConfig(cwd);\n\n if (context.config.onboardingComplete && context.config.groqApiKey) {\n return;\n }\n\n await runSetupCommand(context, { silentWhenNonInteractive: true });\n context.config = await loadConfig(cwd);\n });\n\n return program;\n}\n\nfunction sanitizeCommitOptions(\n options: CommitCommandOptions,\n): CommitCommandOptions {\n return {\n ...options,\n lang: options.lang === \"fr\" ? \"fr\" : \"en\",\n provider: (options.provider ?? \"groq\") as ProviderName,\n };\n}\n\nasync function handleCommand(action: () => Promise<void>): Promise<void> {\n try {\n await action();\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n console.error(`patchwise: ${message}`);\n process.exitCode = 1;\n }\n}\n","import type { SuggestCommitInput } from \"@/types\";\n\nexport function buildPrompt(input: SuggestCommitInput): string {\n const categories = categorizeFiles(input.fileNames);\n const categoryList = Array.from(categories).join(\", \");\n\n return [\n \"You generate Conventional Commit messages from a Git diff.\",\n \"Return strict JSON only.\",\n \"The JSON shape must be:\",\n '{\"summary\":\"string\",\"suggestions\":[{\"type\":\"feat\",\"scope\":\"optional\",\"subject\":\"string\",\"body\":\"optional string\"}]}',\n \"Rules:\",\n \"- EVERY suggestion must cover ALL changes in the diff, never just a subset.\",\n \"- The body should list each change group as bullet points.\",\n `- This diff touches these areas: ${categoryList}.`,\n \"\",\n \"Suggestion 1 — Primary: the most accurate type/scope for the full change.\",\n \"Suggestion 2 — Alternative angle: use a different type or scope that also fits.\",\n \"Suggestion 3 — Broad: a higher-level framing that encompasses everything.\",\n \"\",\n \"Each suggestion must have a DISTINCT type, scope, or subject. No rephrasing.\",\n \"- Subjects must be imperative, concise, and lower-case.\",\n `- Subject max length: ${input.maxSubjectLength}.`,\n `- Language: ${input.language}.`,\n `- Scope strategy: ${input.scopeStrategy}.`,\n input.scope\n ? `- Preferred scope: ${input.scope}.`\n : \"- Preferred scope: infer only if obvious.\",\n `- Files involved: ${input.fileNames.join(\", \") || \"unknown\"}.`,\n \"Diff:\",\n input.diff,\n input.diff.includes(\"[diff truncated\")\n ? \"\\nNote: the diff was truncated to fit token limits. Use the file list and stat summary to infer the full scope.\"\n : \"\",\n ].join(\"\\n\");\n}\n\nfunction categorizeFiles(files: string[]): Set<string> {\n const categories = new Set<string>();\n\n for (const f of files) {\n if (f.startsWith(\"src/\") || f.startsWith(\"lib/\")) categories.add(\"code\");\n else if (f.startsWith(\"test/\") || f.startsWith(\"tests/\") || f.startsWith(\"spec/\")) categories.add(\"test\");\n else if (f.startsWith(\"docs/\") || f.endsWith(\".md\")) categories.add(\"docs\");\n else if (f.includes(\".config.\") || f.endsWith(\"rc\") || f.endsWith(\".json\")) categories.add(\"config\");\n else if (f.startsWith(\".github/\") || f.endsWith(\".yml\") || f.endsWith(\".yaml\")) categories.add(\"ci\");\n else categories.add(\"other\");\n }\n\n return categories;\n}\n","import { z } from \"zod\";\n\nimport type { CommitType } from \"@/types\";\n\nconst VALID_COMMIT_TYPES: CommitType[] = [\n \"feat\",\n \"fix\",\n \"refactor\",\n \"docs\",\n \"test\",\n \"chore\",\n \"perf\",\n \"build\",\n \"ci\",\n];\n\n// Accept any string for type, then coerce to the closest valid type.\n// This prevents crashes when the AI returns \"bugfix\", \"feature\", etc.\nfunction coerceCommitType(value: string): CommitType {\n if (VALID_COMMIT_TYPES.includes(value as CommitType)) {\n return value as CommitType;\n }\n\n // Common AI hallucinations → real types\n const aliasMap: Record<string, CommitType> = {\n bugfix: \"fix\",\n \"bug-fix\": \"fix\",\n bug: \"fix\",\n feature: \"feat\",\n \"new-feature\": \"feat\",\n improvement: \"refactor\",\n improve: \"refactor\",\n cleanup: \"chore\",\n maintenance: \"chore\",\n style: \"refactor\",\n formatting: \"chore\",\n config: \"chore\",\n configuration: \"chore\",\n build: \"build\",\n ci: \"ci\",\n test: \"test\",\n tests: \"test\",\n testing: \"test\",\n docs: \"docs\",\n doc: \"docs\",\n documentation: \"docs\",\n perf: \"perf\",\n performance: \"perf\",\n refactor: \"refactor\",\n feat: \"feat\",\n fix: \"fix\",\n chore: \"chore\",\n };\n\n return aliasMap[value.toLowerCase()] ?? \"feat\";\n}\n\nexport const suggestionSchema = z.object({\n type: z.string().transform(coerceCommitType),\n scope: z\n .string()\n .optional()\n .transform((v) => (v && v.trim().length > 0 ? v.trim() : undefined)),\n subject: z.string().min(1),\n body: z.string().optional(),\n});\n\nexport const providerResponseSchema = z.object({\n summary: z.string().min(1),\n suggestions: z.array(suggestionSchema).min(1).max(10),\n});\n","import { buildPrompt } from \"@/core/ai/prompt\";\nimport { providerResponseSchema } from \"@/core/ai/schemas\";\nimport type { AIProvider, SuggestCommitInput, SuggestionResult } from \"@/types\";\n\nconst GROQ_API_URL = \"https://api.groq.com/openai/v1/chat/completions\";\n\ninterface GroqResponse {\n choices?: Array<{\n message?: {\n content?: string;\n };\n }>;\n}\n\nexport class GroqAIProvider implements AIProvider {\n constructor(\n private readonly apiKey: string,\n private readonly model: string,\n ) {}\n\n async generateCommitSuggestions(\n input: SuggestCommitInput,\n ): Promise<SuggestionResult> {\n const response = await fetch(GROQ_API_URL, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n model: this.model,\n temperature: 0.2,\n response_format: { type: \"json_object\" },\n messages: [\n {\n role: \"system\",\n content:\n \"You are a precise commit assistant. Return valid JSON only.\",\n },\n {\n role: \"user\",\n content: buildPrompt(input),\n },\n ],\n }),\n });\n\n if (!response.ok) {\n const body = await response.text();\n throw new Error(`Groq API error (${response.status}): ${body}`);\n }\n\n const payload = (await response.json()) as GroqResponse;\n const rawContent = payload.choices?.[0]?.message?.content;\n\n if (!rawContent) {\n throw new Error(\"Groq API returned an empty response.\");\n }\n\n const parsed = providerResponseSchema.parse(JSON.parse(rawContent));\n\n return parsed;\n }\n}\n","import { GroqAIProvider } from \"@/core/ai/providers/groq\";\nimport type { AIProvider, AppConfig } from \"@/types\";\n\nexport function createAIProvider(config: AppConfig): AIProvider {\n if (config.provider === \"groq\") {\n const apiKey = config.groqApiKey;\n\n if (!apiKey) {\n throw new Error(\n \"Missing Groq API key. Run `patchwise setup` to configure it.\",\n );\n }\n\n return new GroqAIProvider(apiKey, config.model);\n }\n\n throw new Error(`Unsupported provider: ${config.provider}`);\n}\n","export function truncateDiff(diff: string, maxChars = 8_000): string {\n if (diff.length <= maxChars) {\n return diff;\n }\n\n // Smart truncation: keep file headers and first N lines per file\n const files = parseDiffFiles(diff);\n\n if (files.length === 0) {\n return diff.slice(0, maxChars) + \"\\n\\n[diff truncated]\";\n }\n\n // Start with a stat summary\n const statLines = files.map((f) => `${f.path} (+${f.added}/-${f.removed})`);\n let result = `Files changed: ${files.length}\\n${statLines.join(\"\\n\")}\\n\\n`;\n\n // Add truncated diffs, limiting lines per file\n const maxLinesPerFile = Math.max(10, Math.floor((maxChars - result.length) / files.length / 6));\n\n for (const file of files) {\n const header = `--- ${file.path} ---`;\n const lines = file.diffLines.slice(0, maxLinesPerFile);\n const truncated = file.diffLines.length > maxLinesPerFile ? `\\n[... ${file.diffLines.length - maxLinesPerFile} more lines]` : \"\";\n const fileBlock = `${header}\\n${lines.join(\"\\n\")}${truncated}\\n`;\n\n if (result.length + fileBlock.length > maxChars) {\n result += \"\\n[diff truncated — remaining files omitted]\";\n break;\n }\n\n result += fileBlock;\n }\n\n return result;\n}\n\ninterface ParsedFile {\n path: string;\n diffLines: string[];\n added: number;\n removed: number;\n}\n\nfunction parseDiffFiles(diff: string): ParsedFile[] {\n const files: ParsedFile[] = [];\n const lines = diff.split(\"\\n\");\n let current: ParsedFile | null = null;\n\n for (const line of lines) {\n if (line.startsWith(\"diff --git \")) {\n if (current) files.push(current);\n const parts = line.split(\" \");\n const path = parts[2]?.replace(/^a\\//, \"\") ?? \"unknown\";\n current = { path, diffLines: [], added: 0, removed: 0 };\n } else if (current) {\n current.diffLines.push(line);\n if (line.startsWith(\"+\")) current.added++;\n else if (line.startsWith(\"-\")) current.removed++;\n }\n }\n\n if (current) files.push(current);\n return files;\n}\n\nexport function extractFileNamesFromDiff(diff: string): string[] {\n const fileNames = new Set<string>();\n\n for (const line of diff.split(\"\\n\")) {\n if (!line.startsWith(\"diff --git \")) {\n continue;\n }\n\n const parts = line.split(\" \");\n const filePath = parts[2]?.replace(/^a\\//, \"\");\n\n if (filePath) {\n fileNames.add(filePath);\n }\n }\n\n return [...fileNames];\n}\n","import type { CommitSuggestion } from \"@/types\";\n\nexport function formatCommitMessage(suggestion: CommitSuggestion): string {\n const scope = suggestion.scope ? `(${suggestion.scope})` : \"\";\n return `${suggestion.type}${scope}: ${suggestion.subject}`;\n}\n\nexport function formatCommitMessageWithBody(suggestion: CommitSuggestion): string {\n const header = formatCommitMessage(suggestion);\n if (!suggestion.body) return header;\n return `${header}\\n\\n${suggestion.body}`;\n}\n\nexport function applyScopeOverride(\n suggestion: CommitSuggestion,\n scope: string | undefined,\n disableScope: boolean | undefined,\n): CommitSuggestion {\n if (disableScope) {\n return {\n ...suggestion,\n scope: undefined,\n };\n }\n\n if (!scope) {\n return suggestion;\n }\n\n return {\n ...suggestion,\n scope,\n };\n}\n\nexport function truncateSubject(subject: string, maxLength: number): string {\n if (subject.length <= maxLength) {\n return subject;\n }\n\n return subject.slice(0, maxLength - 1).trimEnd();\n}\n","import { createAIProvider } from \"@/core/ai/create-provider\";\nimport { extractFileNamesFromDiff, truncateDiff } from \"@/core/commit/diff\";\nimport { applyScopeOverride, truncateSubject } from \"@/core/commit/format\";\nimport type { AppConfig, CommitSuggestion, SuggestionResult } from \"@/types\";\n\nexport async function generateSuggestionsFromDiff(\n diff: string,\n config: AppConfig,\n options?: {\n language?: AppConfig[\"language\"];\n scope?: string;\n noScope?: boolean;\n },\n): Promise<SuggestionResult> {\n const provider = createAIProvider(config);\n const input = {\n diff: truncateDiff(diff),\n fileNames: extractFileNamesFromDiff(diff),\n language: options?.language ?? config.language,\n scopeStrategy: options?.noScope\n ? \"none\"\n : options?.scope\n ? \"manual\"\n : config.scopeStrategy,\n scope: options?.scope,\n maxSubjectLength: config.maxSubjectLength,\n };\n\n const result = await provider.generateCommitSuggestions(input);\n\n return {\n ...result,\n suggestions: result.suggestions\n .map((suggestion) =>\n normalizeSuggestion(\n suggestion,\n config.maxSubjectLength,\n options?.scope,\n options?.noScope,\n ),\n )\n .slice(0, 3),\n };\n}\n\nfunction normalizeSuggestion(\n suggestion: CommitSuggestion,\n maxSubjectLength: number,\n scope?: string,\n noScope?: boolean,\n): CommitSuggestion {\n const withScope = applyScopeOverride(suggestion, scope, noScope);\n\n return {\n ...withScope,\n subject: truncateSubject(withScope.subject, maxSubjectLength),\n };\n}\n","import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\nexport interface FileStatus {\n path: string;\n indexStatus: string;\n workingTreeStatus: string;\n}\n\nexport async function assertGitRepository(cwd = process.cwd()): Promise<void> {\n try {\n await execGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n throw new Error(\"Current directory is not a Git repository.\");\n }\n}\n\nexport async function getStagedDiff(cwd = process.cwd()): Promise<string> {\n return execGit([\"diff\", \"--cached\", \"--no-ext-diff\"], cwd);\n}\n\nexport async function getModifiedFiles(\n cwd = process.cwd(),\n): Promise<FileStatus[]> {\n const output = await execGit([\"status\", \"--short\"], cwd);\n\n return output\n .split(\"\\n\")\n .filter(Boolean)\n .map((line) => ({\n indexStatus: line[0] ?? \" \",\n workingTreeStatus: line[1] ?? \" \",\n path: line.slice(3).trim(),\n }));\n}\n\nexport async function stageFiles(\n files: string[],\n cwd = process.cwd(),\n): Promise<void> {\n if (files.length === 0) {\n return;\n }\n\n await execGit([\"add\", \"--\", ...files], cwd);\n}\n\nexport async function stageAll(cwd = process.cwd()): Promise<void> {\n await execGit([\"add\", \"-A\"], cwd);\n}\n\nexport async function createCommit(\n message: string,\n cwd = process.cwd(),\n): Promise<void> {\n await execGit([\"commit\", \"-m\", message], cwd);\n}\n\nexport async function pushCurrentBranch(cwd = process.cwd()): Promise<void> {\n await execGit([\"push\"], cwd);\n}\n\nexport async function getCurrentBranch(cwd = process.cwd()): Promise<string> {\n return execGit([\"branch\", \"--show-current\"], cwd);\n}\n\nasync function execGit(args: string[], cwd: string): Promise<string> {\n try {\n const { stdout } = await execFileAsync(\"git\", args, {\n cwd,\n encoding: \"utf8\",\n maxBuffer: 1024 * 1024 * 10,\n });\n\n return stdout.trim();\n } catch (error) {\n if (error instanceof Error && \"stderr\" in error) {\n const stderr = String(error.stderr ?? \"\").trim();\n\n throw new Error(stderr || \"Git command failed.\", { cause: error });\n }\n\n throw error;\n }\n}\n","import chalk from \"chalk\";\n\nimport { formatCommitMessage } from \"@/core/commit/format\";\nimport type { CommitSuggestion, SuggestionResult } from \"@/types\";\n\nconst BORDER = \"─\";\nconst WIDTH = 56;\n\nfunction divider(label?: string): string {\n const line = chalk.dim(BORDER.repeat(WIDTH));\n if (!label) return line;\n const padded = ` ${label} `;\n const remaining = Math.max(0, WIDTH - padded.length);\n const half = Math.floor(remaining / 2);\n return (\n chalk.dim(BORDER.repeat(half)) +\n chalk.dim(padded) +\n chalk.dim(BORDER.repeat(remaining - half))\n );\n}\n\nfunction typeBadge(type: string): string {\n const colors: Record<string, typeof chalk.green> = {\n feat: chalk.cyan,\n fix: chalk.yellow,\n refactor: chalk.magenta,\n docs: chalk.blue,\n test: chalk.green,\n chore: chalk.gray,\n perf: chalk.red,\n build: chalk.yellow,\n ci: chalk.blueBright,\n };\n const color = colors[type] ?? chalk.white;\n return color(` ${type.toUpperCase()} `);\n}\n\nfunction formatSuggestionLine(\n suggestion: CommitSuggestion,\n index: number,\n): string {\n const header = formatCommitMessage(suggestion);\n const badge = typeBadge(suggestion.type);\n const number = chalk.bold(`${index + 1}.`);\n const line = ` ${number} ${badge} ${chalk.white(header)}`;\n\n if (suggestion.body) {\n const bodyLines = suggestion.body.split(\"\\n\");\n const preview = bodyLines.slice(0, 3).join(\"\\n\");\n const more = bodyLines.length > 3 ? chalk.dim(` ... +${bodyLines.length - 3} more lines`) : \"\";\n return `${line}\\n${chalk.dim(` ${preview}`)}${more ? `\\n${more}` : \"\"}`;\n }\n\n return line;\n}\n\nexport function printSuggestionResult(result: SuggestionResult): void {\n console.log();\n console.log(divider());\n console.log(` ${chalk.bold(chalk.white(\"📝 Commit Suggestions\"))}`);\n console.log(divider());\n console.log();\n console.log(` ${chalk.dim(\"Summary:\")}`);\n console.log(` ${chalk.italic(chalk.white(result.summary))}`);\n console.log();\n console.log(` ${chalk.dim(\"Suggestions:\")}`);\n console.log();\n\n result.suggestions.forEach((suggestion, index) => {\n console.log(formatSuggestionLine(suggestion, index));\n });\n\n console.log();\n console.log(divider());\n console.log();\n}\n\nexport function printSuccess(message: string): void {\n console.log(` ${chalk.green(\"✔\")} ${chalk.green(message)}`);\n}\n\nexport function printInfo(message: string): void {\n console.log(` ${chalk.blue(\"ℹ\")} ${chalk.blue(message)}`);\n}\n\nexport function printWarning(message: string): void {\n console.log(` ${chalk.yellow(\"⚠\")} ${chalk.yellow(message)}`);\n}\n\nexport function printError(message: string): void {\n console.error(` ${chalk.red(\"✖\")} ${chalk.red(message)}`);\n}\n\nexport function printCommitCreated(message: string, branch?: string): void {\n console.log();\n console.log(divider());\n console.log(` ${chalk.green(\"✔\")} ${chalk.green(\"Commit created:\")}`);\n console.log(` ${chalk.white(message)}`);\n if (branch) {\n console.log(` ${chalk.dim(`on branch ${branch}`)}`);\n }\n console.log(divider());\n console.log();\n}\n\nexport function printPushed(branch: string): void {\n console.log();\n console.log(divider());\n console.log(\n ` ${chalk.green(\"✔\")} ${chalk.green(`Pushed to origin/${branch}`)}`,\n );\n console.log(divider());\n console.log();\n}\n\nexport function printCancelled(): void {\n console.log(` ${chalk.yellow(\"↩\")} ${chalk.yellow(\"Cancelled.\")}`);\n}\n","import { checkbox, confirm, input, password, select } from \"@inquirer/prompts\";\nimport chalk from \"chalk\";\n\nimport { fetchGroqModels } from \"@/core/ai/models\";\nimport { formatCommitMessageWithBody } from \"@/core/commit/format\";\nimport type { FileStatus } from \"@/core/git/client\";\nimport type {\n AppConfig,\n CommitSuggestion,\n Language,\n ProviderName,\n} from \"@/types\";\n\nexport async function promptForFiles(files: FileStatus[]): Promise<string[]> {\n return checkbox({\n message: chalk.bold(\"Select files to stage\"),\n choices: files.map((file) => ({\n name: `${getStatusIcon(file.indexStatus, file.workingTreeStatus)} ${file.path}`,\n value: file.path,\n })),\n required: false,\n });\n}\n\nfunction getStatusIcon(index: string, working: string): string {\n if (index !== \" \") return chalk.green(`[${index}]`);\n if (working !== \" \") return chalk.yellow(`[${working}]`);\n return chalk.dim(`[·]`);\n}\n\nexport async function promptForSuggestion(\n suggestions: CommitSuggestion[],\n): Promise<string> {\n const selected = await select({\n message: chalk.bold(\"Select a commit message\"),\n choices: [\n ...suggestions.map((suggestion, index) => ({\n name: `${chalk.bold(`${index + 1}.`)} ${formatCommitMessageWithBody(suggestion)}`,\n value: formatCommitMessageWithBody(suggestion),\n })),\n {\n name: chalk.italic(\"✏️ Write a custom message\"),\n value: \"__custom__\",\n },\n ],\n });\n\n if (selected !== \"__custom__\") {\n return selected;\n }\n\n return input({\n message: chalk.bold(\"Commit message\"),\n validate(value) {\n return value.trim().length > 0 || \"Commit message cannot be empty.\";\n },\n });\n}\n\nexport async function confirmAction(\n message: string,\n defaultValue = true,\n): Promise<boolean> {\n return confirm({\n message: chalk.yellow(`? ${message}`),\n default: defaultValue,\n });\n}\n\nexport interface SetupAnswers {\n provider: ProviderName;\n model: string;\n language: Language;\n groqApiKey: string;\n}\n\nexport async function promptForSetup(\n defaults: Partial<AppConfig>,\n): Promise<SetupAnswers> {\n const provider = await select<ProviderName>({\n message: chalk.bold(\"Select your AI provider\"),\n choices: [{ name: \"Groq\", value: \"groq\" }],\n default: defaults.provider ?? \"groq\",\n });\n\n const groqApiKey = await password({\n message:\n chalk.bold(\"Groq API key\") +\n chalk.dim(\" (https://console.groq.com/keys)\"),\n mask: \"*\",\n validate(value) {\n return value.trim().length > 0 || \"API key is required.\";\n },\n });\n\n const trimmedApiKey = groqApiKey.trim();\n\n // Fetch available models from Groq\n let modelChoices: Array<{ name: string; value: string }> = [];\n\n try {\n const models = await fetchGroqModels(trimmedApiKey);\n modelChoices = models.map((m) => ({\n name: `${m.name}`,\n value: m.id,\n }));\n } catch {\n // Fallback to manual input if fetch fails\n }\n\n let model: string;\n\n if (modelChoices.length > 0) {\n const defaultModel = defaults.model ?? \"llama-3.3-70b-versatile\";\n const defaultChoice = modelChoices.find((c) => c.value === defaultModel);\n\n model = await select({\n message: chalk.bold(\"Select a Groq model\"),\n choices: modelChoices,\n default: defaultChoice?.value,\n });\n } else {\n model = await input({\n message: chalk.bold(\"Groq model\") + chalk.dim(\" (enter manually)\"),\n default: defaults.model ?? \"llama-3.3-70b-versatile\",\n validate(value) {\n return value.trim().length > 0 || \"Model is required.\";\n },\n });\n }\n\n const language = await select<Language>({\n message: chalk.bold(\"Default commit language\"),\n choices: [\n { name: \"🇬🇧 English\", value: \"en\" },\n { name: \"🇫🇷 French\", value: \"fr\" },\n ],\n default: defaults.language ?? \"en\",\n });\n\n return {\n provider,\n model: model.trim(),\n language,\n groqApiKey: trimmedApiKey,\n };\n}\n","const GROQ_MODELS_URL = \"https://api.groq.com/openai/v1/models\";\n\nexport interface GroqModel {\n id: string;\n name: string;\n active: boolean;\n}\n\nexport async function fetchGroqModels(apiKey: string): Promise<GroqModel[]> {\n const response = await fetch(GROQ_MODELS_URL, {\n headers: {\n Authorization: `Bearer ${apiKey}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch models (${response.status}). Check your API key.`,\n );\n }\n\n const data = (await response.json()) as {\n data?: Array<{ id: string; active?: boolean }>;\n };\n\n return (data.data ?? [])\n .filter((m) => m.active !== false)\n .map((m) => ({\n id: m.id,\n name: formatModelName(m.id),\n active: m.active ?? true,\n }))\n .sort((a, b) => a.name.localeCompare(b.name));\n}\n\nfunction formatModelName(id: string): string {\n return id.replace(/-/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n","import type { CommandContext } from \"@/cli/context\";\nimport { generateSuggestionsFromDiff } from \"@/cli/services\";\nimport {\n assertGitRepository,\n createCommit,\n getCurrentBranch,\n getModifiedFiles,\n getStagedDiff,\n pushCurrentBranch,\n stageAll,\n stageFiles,\n} from \"@/core/git/client\";\nimport {\n printCancelled,\n printCommitCreated,\n printPushed,\n printSuggestionResult,\n} from \"@/core/ui/output\";\nimport {\n confirmAction,\n promptForFiles,\n promptForSuggestion,\n} from \"@/core/ui/prompts\";\nimport type { CommitCommandOptions } from \"@/types\";\n\nexport async function runCommitCommand(\n context: CommandContext,\n options: CommitCommandOptions,\n): Promise<void> {\n await assertGitRepository(context.cwd);\n\n if (options.all) {\n await stageAll(context.cwd);\n }\n\n if (options.select) {\n const files = await getModifiedFiles(context.cwd);\n\n if (files.length === 0) {\n throw new Error(\"No modified files found.\");\n }\n\n const selected = await promptForFiles(files);\n\n if (selected.length === 0) {\n throw new Error(\"No files selected for staging.\");\n }\n\n await stageFiles(selected, context.cwd);\n }\n\n const diff = await getStagedDiff(context.cwd);\n\n if (!diff) {\n throw new Error(\n \"No staged changes found. Stage files before running commit.\",\n );\n }\n\n const config = {\n ...context.config,\n language: options.lang ?? context.config.language,\n provider: options.provider ?? context.config.provider,\n model: options.model ?? context.config.model,\n };\n\n const result = await generateSuggestionsFromDiff(diff, config, {\n language: options.lang,\n scope: options.scope,\n noScope: options.noScope,\n });\n\n printSuggestionResult(result);\n\n const message = await promptForSuggestion(result.suggestions);\n const shouldCommit =\n options.yes ||\n !config.confirmBeforeCommit ||\n (await confirmAction(`Commit with \"${message}\"?`));\n\n if (!shouldCommit) {\n printCancelled();\n return;\n }\n\n const branch = await getCurrentBranch(context.cwd);\n await createCommit(message, context.cwd);\n printCommitCreated(message, branch);\n\n if (!options.push) {\n return;\n }\n\n const shouldPush =\n options.yes ||\n !config.confirmBeforePush ||\n (await confirmAction(`Push to origin/${branch}?`));\n\n if (!shouldPush) {\n printCancelled();\n return;\n }\n\n await pushCurrentBranch(context.cwd);\n printPushed(branch);\n}\n","import { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { z } from \"zod\";\n\nimport type { AppConfig, Language, ProviderName, ScopeStrategy } from \"@/types\";\nimport { CONFIG_FILE_NAME, DEFAULT_CONFIG } from \"@/core/config/defaults\";\n\nconst configSchema = z.object({\n provider: z.literal(\"groq\").optional(),\n model: z.string().min(1).optional(),\n commitConvention: z.literal(\"conventional\").optional(),\n language: z.enum([\"en\", \"fr\"]).optional(),\n maxSubjectLength: z.number().int().positive().optional(),\n confirmBeforeCommit: z.boolean().optional(),\n confirmBeforePush: z.boolean().optional(),\n scopeStrategy: z.enum([\"auto\", \"manual\", \"none\"]).optional(),\n groqApiKey: z.string().min(1).optional(),\n onboardingComplete: z.boolean().optional(),\n});\n\nexport async function loadConfig(cwd = process.cwd()): Promise<AppConfig> {\n const fileConfig = await loadProjectConfig(cwd);\n const userConfig = await loadUserConfig();\n\n return {\n ...DEFAULT_CONFIG,\n ...userConfig,\n ...fileConfig,\n provider: readEnv(\"PATCHWISE_PROVIDER\", DEFAULT_CONFIG.provider),\n model:\n process.env.PATCHWISE_MODEL ??\n fileConfig.model ??\n userConfig.model ??\n DEFAULT_CONFIG.model,\n language: readEnv(\n \"PATCHWISE_LANGUAGE\",\n fileConfig.language ?? userConfig.language ?? DEFAULT_CONFIG.language,\n ),\n groqApiKey:\n process.env.GROQ_API_KEY ??\n fileConfig.groqApiKey ??\n userConfig.groqApiKey,\n onboardingComplete:\n fileConfig.onboardingComplete ??\n userConfig.onboardingComplete ??\n DEFAULT_CONFIG.onboardingComplete,\n };\n}\n\nexport async function initConfigFile(cwd = process.cwd()): Promise<string> {\n const configPath = path.join(cwd, CONFIG_FILE_NAME);\n\n try {\n await access(configPath);\n return configPath;\n } catch (error) {\n if (!isMissingFile(error)) {\n throw error;\n }\n }\n\n await writeFile(\n `${configPath}`,\n `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\\n`,\n \"utf8\",\n );\n\n return configPath;\n}\n\nexport async function loadUserConfig(): Promise<Partial<AppConfig>> {\n const configPath = getUserConfigPath();\n\n try {\n const raw = await readFile(configPath, \"utf8\");\n return configSchema.parse(JSON.parse(raw));\n } catch (error) {\n if (!isMissingFile(error)) {\n throw error;\n }\n }\n\n return {};\n}\n\nexport async function saveUserConfig(\n config: Partial<AppConfig>,\n): Promise<string> {\n const configPath = getUserConfigPath();\n await mkdir(path.dirname(configPath), { recursive: true });\n await writeFile(configPath, `${JSON.stringify(config, null, 2)}\\n`, \"utf8\");\n return configPath;\n}\n\nexport function getUserConfigPath(): string {\n if (process.platform === \"win32\") {\n const appData =\n process.env.APPDATA ?? path.join(os.homedir(), \"AppData\", \"Roaming\");\n return path.join(appData, \"patchwise\", \"config.json\");\n }\n\n if (process.platform === \"darwin\") {\n return path.join(\n os.homedir(),\n \"Library\",\n \"Application Support\",\n \"patchwise\",\n \"config.json\",\n );\n }\n\n const xdgConfigHome =\n process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), \".config\");\n return path.join(xdgConfigHome, \"patchwise\", \"config.json\");\n}\n\nasync function loadProjectConfig(cwd: string): Promise<Partial<AppConfig>> {\n const configPath = path.join(cwd, CONFIG_FILE_NAME);\n\n try {\n const raw = await readFile(configPath, \"utf8\");\n return configSchema.parse(JSON.parse(raw));\n } catch (error) {\n if (!isMissingFile(error)) {\n throw error;\n }\n }\n\n return {};\n}\n\nfunction isMissingFile(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n}\n\nfunction readEnv<T extends Language | ProviderName | ScopeStrategy>(\n name: string,\n fallback: T,\n): T {\n const value = process.env[name];\n\n if (!value) {\n return fallback;\n }\n\n return value as T;\n}\n","import type { AppConfig } from \"@/types\";\n\nexport const DEFAULT_CONFIG: AppConfig = {\n provider: \"groq\",\n model: \"llama-3.3-70b-versatile\",\n commitConvention: \"conventional\",\n language: \"en\",\n maxSubjectLength: 72,\n confirmBeforeCommit: true,\n confirmBeforePush: true,\n scopeStrategy: \"auto\",\n onboardingComplete: false,\n};\n\nexport const CONFIG_FILE_NAME = \"patchwise.config.json\";\n","import type { CommandContext } from \"@/cli/context\";\nimport {\n initConfigFile,\n loadConfig,\n saveUserConfig,\n} from \"@/core/config/load-config\";\nimport { promptForSetup } from \"@/core/ui/prompts\";\n\nexport async function runConfigInitCommand(\n context: CommandContext,\n): Promise<void> {\n const configPath = await initConfigFile(context.cwd);\n console.log(`Config ready at ${configPath}`);\n}\n\nexport async function runSetupCommand(\n context: CommandContext,\n options?: { silentWhenNonInteractive?: boolean },\n): Promise<void> {\n if (!process.stdin.isTTY || !process.stdout.isTTY) {\n if (options?.silentWhenNonInteractive) {\n return;\n }\n\n throw new Error(\n \"Interactive setup requires a TTY. Run `patchwise setup` in a terminal.\",\n );\n }\n\n const currentConfig = await loadConfig(context.cwd);\n const answers = await promptForSetup(currentConfig);\n const configPath = await saveUserConfig({\n provider: answers.provider,\n model: answers.model,\n language: answers.language,\n groqApiKey: answers.groqApiKey,\n onboardingComplete: true,\n });\n\n context.config = await loadConfig(context.cwd);\n console.log(`User config saved to ${configPath}`);\n}\n","import type { CommandContext } from \"@/cli/context\";\nimport {\n assertGitRepository,\n getModifiedFiles,\n stageFiles,\n} from \"@/core/git/client\";\nimport { promptForFiles } from \"@/core/ui/prompts\";\n\nexport async function runStageCommand(context: CommandContext): Promise<void> {\n await assertGitRepository(context.cwd);\n\n const files = await getModifiedFiles(context.cwd);\n\n if (files.length === 0) {\n throw new Error(\"No modified files found.\");\n }\n\n const selected = await promptForFiles(files);\n\n if (selected.length === 0) {\n console.log(\"No files selected.\");\n return;\n }\n\n await stageFiles(selected, context.cwd);\n console.log(`Staged ${selected.length} file(s).`);\n}\n","import type { CommandContext } from \"@/cli/context\";\nimport { generateSuggestionsFromDiff } from \"@/cli/services\";\nimport { assertGitRepository, getStagedDiff } from \"@/core/git/client\";\nimport { printSuggestionResult } from \"@/core/ui/output\";\n\nexport async function runSuggestCommand(\n context: CommandContext,\n): Promise<void> {\n await assertGitRepository(context.cwd);\n\n const diff = await getStagedDiff(context.cwd);\n\n if (!diff) {\n throw new Error(\n \"No staged changes found. Stage files before running suggest.\",\n );\n }\n\n const result = await generateSuggestionsFromDiff(diff, context.config);\n printSuggestionResult(result);\n}\n","#!/usr/bin/env node\n\nimport { createProgram } from \"@/cli/program\";\n\nconst program = await createProgram();\nawait program.parseAsync(process.argv);\n"],"mappings":"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","names":["input","input","path","input","chalk","chalk","z","z","program"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "patchwise",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-assisted Git commits, with the developer still in charge.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"patchwise": "./dist/patchwise.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"cli",
|
|
20
|
+
"git",
|
|
21
|
+
"commit",
|
|
22
|
+
"ai",
|
|
23
|
+
"conventional-commits"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/jiordiviera/patchwise.git"
|
|
28
|
+
},
|
|
29
|
+
"author": "",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@inquirer/prompts": "^8.3.2",
|
|
33
|
+
"chalk": "^5.6.2",
|
|
34
|
+
"commander": "^14.0.3",
|
|
35
|
+
"vite-tsconfig-paths": "^6.1.1",
|
|
36
|
+
"zod": "^4.3.6"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@changesets/cli": "^2.30.0",
|
|
40
|
+
"@eslint/js": "^10.0.1",
|
|
41
|
+
"@eslint/json": "^1.2.0",
|
|
42
|
+
"@eslint/markdown": "^8.0.1",
|
|
43
|
+
"@types/node": "^25.5.2",
|
|
44
|
+
"eslint": "^10.2.0",
|
|
45
|
+
"globals": "^17.4.0",
|
|
46
|
+
"jiti": "^2.6.1",
|
|
47
|
+
"prettier": "3.8.1",
|
|
48
|
+
"rimraf": "^6.1.3",
|
|
49
|
+
"tsup": "^8.5.1",
|
|
50
|
+
"tsx": "^4.20.6",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"typescript-eslint": "^8.58.0",
|
|
53
|
+
"vitest": "^4.1.2"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup",
|
|
57
|
+
"check:ci": "pnpm run lint && pnpm run typecheck",
|
|
58
|
+
"clean": "rimraf dist",
|
|
59
|
+
"dev": "tsup --watch",
|
|
60
|
+
"check": "pnpm run format:write && eslint --fix",
|
|
61
|
+
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
|
|
62
|
+
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
|
|
63
|
+
"lint": "eslint",
|
|
64
|
+
"pub:beta": "pnpm build && pnpm publish --no-git-checks --access public --tag beta",
|
|
65
|
+
"pub:next": "pnpm build && pnpm publish --no-git-checks --access public --tag next",
|
|
66
|
+
"pub:release": "pnpm build && pnpm publish --access public",
|
|
67
|
+
"changeset": "changeset",
|
|
68
|
+
"version": "changeset version",
|
|
69
|
+
"release": "changeset publish",
|
|
70
|
+
"start": "node dist/index.js",
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"test:coverage": "vitest run --coverage",
|
|
73
|
+
"test:watch": "vitest",
|
|
74
|
+
"typecheck": "tsc --noEmit"
|
|
75
|
+
}
|
|
76
|
+
}
|