zencommit 0.1.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 +421 -0
- package/bin/zencommit.js +37 -0
- package/package.json +68 -0
- package/scripts/install.mjs +146 -0
- package/scripts/platform.mjs +34 -0
- package/src/auth/secrets.ts +234 -0
- package/src/commands/auth.ts +138 -0
- package/src/commands/config.ts +83 -0
- package/src/commands/default.ts +322 -0
- package/src/commands/models.ts +67 -0
- package/src/config/load.test.ts +47 -0
- package/src/config/load.ts +118 -0
- package/src/config/merge.test.ts +25 -0
- package/src/config/merge.ts +30 -0
- package/src/config/types.ts +119 -0
- package/src/config/validate.ts +139 -0
- package/src/git/commit.ts +17 -0
- package/src/git/diff.ts +89 -0
- package/src/git/repo.ts +10 -0
- package/src/index.ts +207 -0
- package/src/llm/generate.ts +188 -0
- package/src/llm/prompt-template.ts +44 -0
- package/src/llm/prompt.ts +83 -0
- package/src/llm/prompts/base.md +119 -0
- package/src/llm/prompts/conventional.md +123 -0
- package/src/llm/prompts/gitmoji.md +212 -0
- package/src/llm/prompts/system.md +21 -0
- package/src/llm/providers.ts +102 -0
- package/src/llm/tokens.test.ts +22 -0
- package/src/llm/tokens.ts +46 -0
- package/src/llm/truncate.test.ts +60 -0
- package/src/llm/truncate.ts +552 -0
- package/src/metadata/cache.ts +28 -0
- package/src/metadata/index.ts +94 -0
- package/src/metadata/providers/local.ts +66 -0
- package/src/metadata/providers/modelsdev.ts +145 -0
- package/src/metadata/types.ts +20 -0
- package/src/ui/editor.ts +33 -0
- package/src/ui/prompts.ts +99 -0
- package/src/util/exec.ts +57 -0
- package/src/util/fs.ts +46 -0
- package/src/util/logger.ts +50 -0
- package/src/util/redact.ts +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# zencommit
|
|
2
|
+
|
|
3
|
+
AI-powered git commit message generator. Analyzes your staged changes and generates meaningful, conventional commit messages using LLMs.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Smart Diff Analysis** - Automatically parses and prioritizes code changes for optimal context
|
|
8
|
+
- **Auto Token Capping** - Intelligently truncates diffs based on model token limits
|
|
9
|
+
- **Multiple Providers** - Supports 20+ AI providers via Vercel AI SDK
|
|
10
|
+
- **Conventional Commits** - Generates messages following conventional commit standards
|
|
11
|
+
- **Interactive Workflow** - Preview, edit, or confirm before committing
|
|
12
|
+
- **Flexible Configuration** - Global, project, and CLI-level configuration options
|
|
13
|
+
- **Secure Credential Storage** - API keys stored securely via Bun's Secrets API
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### Using npm
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g zencommit
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Using Bun
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bun install -g zencommit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### From Source
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/mboisvertdupras/zencommit.git
|
|
33
|
+
cd zencommit
|
|
34
|
+
bun install
|
|
35
|
+
bun run build:exe
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
1. **Set up your API key:**
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
zencommit auth login
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. **Stage your changes and run:**
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git add .
|
|
50
|
+
zencommit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
3. **Review, edit, or confirm the generated commit message.**
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Basic Commands
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Generate commit message for staged changes
|
|
61
|
+
zencommit
|
|
62
|
+
|
|
63
|
+
# Auto-commit without confirmation
|
|
64
|
+
zencommit --yes
|
|
65
|
+
|
|
66
|
+
# Preview without committing
|
|
67
|
+
zencommit --dry-run
|
|
68
|
+
|
|
69
|
+
# Stage all changes and generate message
|
|
70
|
+
zencommit --all
|
|
71
|
+
|
|
72
|
+
# Generate message for unstaged changes (preview only)
|
|
73
|
+
zencommit --unstaged
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Command-Line Options
|
|
77
|
+
|
|
78
|
+
| Flag | Description |
|
|
79
|
+
| ------------------ | --------------------------------------------------- |
|
|
80
|
+
| `--yes` | Skip confirmation and commit immediately |
|
|
81
|
+
| `--dry-run` | Preview output without committing |
|
|
82
|
+
| `--all` | Stage all changes (`git add -A`) before generating |
|
|
83
|
+
| `--unstaged` | Use unstaged diff (never commits unless `--commit`) |
|
|
84
|
+
| `--commit` | Allow committing with `--unstaged` |
|
|
85
|
+
| `--model <id>` | Override model (e.g., `openai/gpt-4o`) |
|
|
86
|
+
| `--format <style>` | Commit style: `conventional` or `freeform` |
|
|
87
|
+
| `--lang <code>` | Language code (e.g., `en`, `fr`, `es`) |
|
|
88
|
+
| `--no-body` | Generate subject line only |
|
|
89
|
+
| `-v, -vv, -vvv` | Increase verbosity level |
|
|
90
|
+
| `--` | Pass additional arguments to `git commit` |
|
|
91
|
+
|
|
92
|
+
### Examples
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Use a specific model
|
|
96
|
+
zencommit --model anthropic/claude-sonnet-4-20250514
|
|
97
|
+
|
|
98
|
+
# Generate in French
|
|
99
|
+
zencommit --lang fr
|
|
100
|
+
|
|
101
|
+
# Freeform style without body
|
|
102
|
+
zencommit --format freeform --no-body
|
|
103
|
+
|
|
104
|
+
# Pass args to git commit
|
|
105
|
+
zencommit --yes -- --no-verify
|
|
106
|
+
|
|
107
|
+
# Verbose output for debugging
|
|
108
|
+
zencommit -vv --dry-run
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Authentication
|
|
112
|
+
|
|
113
|
+
Manage API keys with the `auth` command. Keys are stored securely via Bun's Secrets API and never written to config files.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Interactive login
|
|
117
|
+
zencommit auth login
|
|
118
|
+
|
|
119
|
+
# Non-interactive login
|
|
120
|
+
zencommit auth login --env-key OPENAI_API_KEY --token sk-...
|
|
121
|
+
|
|
122
|
+
# Remove stored key
|
|
123
|
+
zencommit auth logout --env-key OPENAI_API_KEY
|
|
124
|
+
|
|
125
|
+
# Check authentication status
|
|
126
|
+
zencommit auth status
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Supported Environment Variables
|
|
130
|
+
|
|
131
|
+
| Provider | Environment Variable |
|
|
132
|
+
| ----------- | -------------------------------------------------- |
|
|
133
|
+
| OpenAI | `OPENAI_API_KEY` |
|
|
134
|
+
| Anthropic | `ANTHROPIC_API_KEY` |
|
|
135
|
+
| Google | `GOOGLE_GENERATIVE_AI_API_KEY` or `GEMINI_API_KEY` |
|
|
136
|
+
| Azure | `AZURE_API_KEY` |
|
|
137
|
+
| AWS Bedrock | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` |
|
|
138
|
+
| Groq | `GROQ_API_KEY` |
|
|
139
|
+
| Mistral | `MISTRAL_API_KEY` |
|
|
140
|
+
| xAI | `XAI_API_KEY` |
|
|
141
|
+
| OpenRouter | `OPENROUTER_API_KEY` |
|
|
142
|
+
| Together AI | `TOGETHER_AI_API_KEY` |
|
|
143
|
+
| Perplexity | `PERPLEXITY_API_KEY` |
|
|
144
|
+
| Cohere | `COHERE_API_KEY` |
|
|
145
|
+
| Cerebras | `CEREBRAS_API_KEY` |
|
|
146
|
+
| DeepInfra | `DEEPINFRA_API_KEY` |
|
|
147
|
+
| GitLab | `GITLAB_API_KEY` |
|
|
148
|
+
| Vercel | `VERCEL_API_KEY` |
|
|
149
|
+
|
|
150
|
+
## Configuration
|
|
151
|
+
|
|
152
|
+
Configuration is loaded from multiple sources and merged in order:
|
|
153
|
+
|
|
154
|
+
1. **Global** - `~/.config/zencommit/config.json`
|
|
155
|
+
2. **Custom path** - `$ZENCOMMIT_CONFIG`
|
|
156
|
+
3. **Project** - `zencommit.json` in repository root
|
|
157
|
+
4. **Inline** - `$ZENCOMMIT_CONFIG_CONTENT` (JSON string)
|
|
158
|
+
|
|
159
|
+
### Initialize Config
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
zencommit config init
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### View Resolved Config
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
zencommit config print
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Validate Config
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
zencommit config validate
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Full Configuration Schema
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"$schema": "https://zencommit.dev/config.json",
|
|
182
|
+
"ai": {
|
|
183
|
+
"model": "openai/gpt-4o",
|
|
184
|
+
"temperature": 0.2,
|
|
185
|
+
"maxOutputTokens": 4096,
|
|
186
|
+
"timeoutMs": 20000,
|
|
187
|
+
"openaiCompatible": {
|
|
188
|
+
"baseUrl": "https://example.com/v1",
|
|
189
|
+
"name": "my-provider"
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"commit": {
|
|
193
|
+
"style": "conventional",
|
|
194
|
+
"language": "en",
|
|
195
|
+
"includeBody": true,
|
|
196
|
+
"emoji": false
|
|
197
|
+
},
|
|
198
|
+
"git": {
|
|
199
|
+
"diffMode": "staged",
|
|
200
|
+
"autoStage": false,
|
|
201
|
+
"confirmBeforeCommit": true
|
|
202
|
+
},
|
|
203
|
+
"diff": {
|
|
204
|
+
"truncateStrategy": "smart",
|
|
205
|
+
"includeFileList": true,
|
|
206
|
+
"excludeGitignoreFiles": true,
|
|
207
|
+
"maxFiles": 200,
|
|
208
|
+
"smart": {
|
|
209
|
+
"maxAddedLinesPerHunk": 12,
|
|
210
|
+
"maxRemovedLinesPerHunk": 12
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"metadata": {
|
|
214
|
+
"provider": "auto",
|
|
215
|
+
"fallbackOrder": ["modelsdev", "local"],
|
|
216
|
+
"providers": {
|
|
217
|
+
"modelsdev": {
|
|
218
|
+
"url": "https://models.dev/api.json",
|
|
219
|
+
"cacheTtlHours": 24
|
|
220
|
+
},
|
|
221
|
+
"local": {
|
|
222
|
+
"path": "./models.metadata.json"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Configuration Options
|
|
230
|
+
|
|
231
|
+
#### AI Settings (`ai`)
|
|
232
|
+
|
|
233
|
+
| Option | Type | Default | Description |
|
|
234
|
+
| ------------------ | ------ | ----------------- | ---------------------------------------- |
|
|
235
|
+
| `model` | string | `"openai/gpt-4o"` | Model ID in `provider/model` format |
|
|
236
|
+
| `temperature` | number | `0.2` | Sampling temperature (0-1) |
|
|
237
|
+
| `maxOutputTokens` | number | `4096` | Maximum tokens in response |
|
|
238
|
+
| `timeoutMs` | number | `20000` | Request timeout in milliseconds |
|
|
239
|
+
| `openaiCompatible` | object | - | Settings for OpenAI-compatible providers |
|
|
240
|
+
|
|
241
|
+
#### Commit Settings (`commit`)
|
|
242
|
+
|
|
243
|
+
| Option | Type | Default | Description |
|
|
244
|
+
| ------------- | ------- | ---------------- | -------------------------------- |
|
|
245
|
+
| `style` | string | `"conventional"` | `"conventional"` or `"freeform"` |
|
|
246
|
+
| `language` | string | `"en"` | Language code for commit message |
|
|
247
|
+
| `includeBody` | boolean | `true` | Include detailed body in message |
|
|
248
|
+
| `emoji` | boolean | `false` | Include emoji in commit subject |
|
|
249
|
+
|
|
250
|
+
#### Git Settings (`git`)
|
|
251
|
+
|
|
252
|
+
| Option | Type | Default | Description |
|
|
253
|
+
| --------------------- | ------- | ---------- | ------------------------------------ |
|
|
254
|
+
| `diffMode` | string | `"staged"` | `"staged"`, `"unstaged"`, or `"all"` |
|
|
255
|
+
| `autoStage` | boolean | `false` | Auto-stage changes before generating |
|
|
256
|
+
| `confirmBeforeCommit` | boolean | `true` | Prompt before committing |
|
|
257
|
+
|
|
258
|
+
#### Diff Settings (`diff`)
|
|
259
|
+
|
|
260
|
+
| Option | Type | Default | Description |
|
|
261
|
+
| ------------------------------ | ------- | --------- | ----------------------------- |
|
|
262
|
+
| `truncateStrategy` | string | `"smart"` | `"smart"` or `"byFile"` |
|
|
263
|
+
| `includeFileList` | boolean | `true` | Include list of changed files |
|
|
264
|
+
| `excludeGitignoreFiles` | boolean | `true` | Respect .gitignore |
|
|
265
|
+
| `maxFiles` | number | `200` | Maximum files to include |
|
|
266
|
+
| `smart.maxAddedLinesPerHunk` | number | `12` | Max added lines per hunk |
|
|
267
|
+
| `smart.maxRemovedLinesPerHunk` | number | `12` | Max removed lines per hunk |
|
|
268
|
+
|
|
269
|
+
## Supported Providers
|
|
270
|
+
|
|
271
|
+
zencommit supports any provider compatible with the Vercel AI SDK:
|
|
272
|
+
|
|
273
|
+
| Provider | Model Format | Example |
|
|
274
|
+
| ----------------- | --------------------------- | ------------------------------------------- |
|
|
275
|
+
| OpenAI | `openai/<model>` | `openai/gpt-4o` |
|
|
276
|
+
| Anthropic | `anthropic/<model>` | `anthropic/claude-sonnet-4-20250514` |
|
|
277
|
+
| Google | `google/<model>` | `google/gemini-2.5-pro` |
|
|
278
|
+
| Azure OpenAI | `azure/<deployment>` | `azure/gpt-4o-deployment` |
|
|
279
|
+
| AWS Bedrock | `bedrock/<model>` | `bedrock/anthropic.claude-3-5-sonnet` |
|
|
280
|
+
| Google Vertex | `vertex/<model>` | `vertex/gemini-2.0-flash` |
|
|
281
|
+
| Vertex Anthropic | `vertex-anthropic/<model>` | `vertex-anthropic/claude-sonnet-4-20250514` |
|
|
282
|
+
| Groq | `groq/<model>` | `groq/llama-3.3-70b-versatile` |
|
|
283
|
+
| Mistral | `mistral/<model>` | `mistral/mistral-large-latest` |
|
|
284
|
+
| xAI | `xai/<model>` | `xai/grok-2` |
|
|
285
|
+
| OpenRouter | `openrouter/<model>` | `openrouter/anthropic/claude-3-opus` |
|
|
286
|
+
| Together AI | `togetherai/<model>` | `togetherai/meta-llama/Meta-Llama-3-70B` |
|
|
287
|
+
| Perplexity | `perplexity/<model>` | `perplexity/llama-3.1-sonar-large-128k` |
|
|
288
|
+
| Cohere | `cohere/<model>` | `cohere/command-r-plus` |
|
|
289
|
+
| Cerebras | `cerebras/<model>` | `cerebras/llama3.1-70b` |
|
|
290
|
+
| DeepInfra | `deepinfra/<model>` | `deepinfra/meta-llama/Llama-2-70b` |
|
|
291
|
+
| GitLab | `gitlab/<model>` | `gitlab/claude-3-5-sonnet` |
|
|
292
|
+
| Vercel | `vercel/<model>` | `vercel/v0-1.0-md` |
|
|
293
|
+
| AI Gateway | `gateway/<model>` | `gateway/openai/gpt-4o` |
|
|
294
|
+
| OpenAI Compatible | `openai-compatible/<model>` | `openai-compatible/my-model` |
|
|
295
|
+
|
|
296
|
+
### Provider Aliases
|
|
297
|
+
|
|
298
|
+
For convenience, these aliases are also supported:
|
|
299
|
+
|
|
300
|
+
- `gemini` → `google`
|
|
301
|
+
- `amazon-bedrock`, `aws-bedrock` → `bedrock`
|
|
302
|
+
- `google-vertex`, `google-vertex-ai` → `vertex`
|
|
303
|
+
- `azure-openai` → `azure`
|
|
304
|
+
- `together.ai` → `togetherai`
|
|
305
|
+
- `xai-grok` → `xai`
|
|
306
|
+
- `open-router` → `openrouter`
|
|
307
|
+
- `vercel-ai-gateway`, `ai-gateway` → `gateway`
|
|
308
|
+
|
|
309
|
+
## Model Discovery
|
|
310
|
+
|
|
311
|
+
Explore available models using the `models` command. Model metadata is fetched from [models.dev](https://models.dev) and cached locally.
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# Search for models
|
|
315
|
+
zencommit models search gpt-4
|
|
316
|
+
|
|
317
|
+
# Get detailed model info
|
|
318
|
+
zencommit models info openai/gpt-4o
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Smart Diff Truncation
|
|
322
|
+
|
|
323
|
+
zencommit automatically caps diff content to fit within model token limits:
|
|
324
|
+
|
|
325
|
+
### `smart` Strategy (default)
|
|
326
|
+
|
|
327
|
+
1. **File Summary** - Includes compact `name-status` and `numstat` output
|
|
328
|
+
2. **Compact Diff** - Uses `--unified=0` to minimize context lines
|
|
329
|
+
3. **Hunk Prioritization** - Scores and selects most informative hunks:
|
|
330
|
+
- Prioritizes source code over generated files
|
|
331
|
+
- Boosts hunks with definitions (functions, classes, types)
|
|
332
|
+
- Prefers smaller, more focused hunks
|
|
333
|
+
4. **Graceful Degradation** - Falls back to summary-only when needed
|
|
334
|
+
|
|
335
|
+
### `byFile` Strategy
|
|
336
|
+
|
|
337
|
+
Distributes token budget proportionally across files, ensuring each file gets a minimum allocation before distributing remaining tokens by size.
|
|
338
|
+
|
|
339
|
+
## Exit Codes
|
|
340
|
+
|
|
341
|
+
| Code | Meaning |
|
|
342
|
+
| ---- | ------------------------------------- |
|
|
343
|
+
| `0` | Success |
|
|
344
|
+
| `2` | Configuration or authentication error |
|
|
345
|
+
| `3` | Git error or no diff available |
|
|
346
|
+
| `4` | Model/LLM call error |
|
|
347
|
+
|
|
348
|
+
## Development
|
|
349
|
+
|
|
350
|
+
### Prerequisites
|
|
351
|
+
|
|
352
|
+
- [Bun](https://bun.sh/) >= 1.0
|
|
353
|
+
- Node.js >= 18 (for npm compatibility)
|
|
354
|
+
|
|
355
|
+
### Commands
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
# Install dependencies
|
|
359
|
+
bun install
|
|
360
|
+
|
|
361
|
+
# Run directly
|
|
362
|
+
bun src/index.ts
|
|
363
|
+
|
|
364
|
+
# Lint
|
|
365
|
+
bun run lint
|
|
366
|
+
bun run lint:fix
|
|
367
|
+
|
|
368
|
+
# Format
|
|
369
|
+
bun run format
|
|
370
|
+
bun run format:check
|
|
371
|
+
|
|
372
|
+
# Run tests
|
|
373
|
+
bun run test
|
|
374
|
+
|
|
375
|
+
# Build standalone executable
|
|
376
|
+
bun run build:exe
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Project Structure
|
|
380
|
+
|
|
381
|
+
```
|
|
382
|
+
zencommit/
|
|
383
|
+
├── src/
|
|
384
|
+
│ ├── index.ts # CLI entry point (yargs)
|
|
385
|
+
│ ├── commands/ # Command implementations
|
|
386
|
+
│ │ ├── default.ts # Main commit generation
|
|
387
|
+
│ │ ├── auth.ts # Authentication commands
|
|
388
|
+
│ │ ├── config.ts # Configuration commands
|
|
389
|
+
│ │ └── models.ts # Model discovery commands
|
|
390
|
+
│ ├── config/ # Configuration loading/merging
|
|
391
|
+
│ ├── auth/ # Secrets management
|
|
392
|
+
│ ├── metadata/ # Model metadata providers
|
|
393
|
+
│ ├── git/ # Git operations
|
|
394
|
+
│ ├── llm/ # LLM interactions & tokenization
|
|
395
|
+
│ ├── ui/ # Interactive prompts
|
|
396
|
+
│ └── util/ # Utility functions
|
|
397
|
+
├── tests/ # Test files
|
|
398
|
+
├── docs/ # Documentation
|
|
399
|
+
└── bin/ # Executable scripts
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Contributing
|
|
403
|
+
|
|
404
|
+
1. Fork the repository
|
|
405
|
+
2. Create a feature branch: `git checkout -b feat/my-feature`
|
|
406
|
+
3. Make your changes following the coding style
|
|
407
|
+
4. Run linting and tests: `bun run lint && bun run test`
|
|
408
|
+
5. Commit using conventional commits: `feat: add my feature`
|
|
409
|
+
6. Push and open a pull request
|
|
410
|
+
|
|
411
|
+
## License
|
|
412
|
+
|
|
413
|
+
MIT
|
|
414
|
+
|
|
415
|
+
## Acknowledgments
|
|
416
|
+
|
|
417
|
+
- [Vercel AI SDK](https://sdk.vercel.ai/) for unified LLM access
|
|
418
|
+
- [models.dev](https://models.dev) for model metadata
|
|
419
|
+
- [Bun](https://bun.sh/) for fast JavaScript runtime
|
|
420
|
+
- [yargs](https://yargs.js.org/) for CLI parsing
|
|
421
|
+
- [@clack/prompts](https://github.com/bombshell-dev/clack) for beautiful prompts
|
package/bin/zencommit.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { getInstallPath } from '../scripts/platform.mjs';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const pkgRoot = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
let binaryPath;
|
|
12
|
+
try {
|
|
13
|
+
binaryPath = getInstallPath(pkgRoot);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16
|
+
console.error(`zencommit: ${message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(binaryPath)) {
|
|
21
|
+
console.error('zencommit: binary not found.');
|
|
22
|
+
console.error('zencommit: reinstall the package to download the executable.');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
|
|
27
|
+
child.on('error', (error) => {
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
console.error(`zencommit: failed to launch: ${message}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
child.on('exit', (code, signal) => {
|
|
33
|
+
if (signal) {
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
process.exit(code ?? 1);
|
|
37
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zencommit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"module": "./src/index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"bin": {
|
|
8
|
+
"zencommit": "bin/zencommit.js"
|
|
9
|
+
},
|
|
10
|
+
"files": ["src", "bin", "scripts"],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"lint": "eslint .",
|
|
13
|
+
"lint:fix": "eslint . --fix",
|
|
14
|
+
"format": "prettier --write .",
|
|
15
|
+
"format:check": "prettier --check .",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"build:exe": "bun build --compile src/index.ts --outfile dist/zencommit",
|
|
18
|
+
"postinstall": "node scripts/install.mjs"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.0.0"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@ai-sdk/amazon-bedrock": "^4.0.39",
|
|
29
|
+
"@ai-sdk/anthropic": "^3.0.30",
|
|
30
|
+
"@ai-sdk/azure": "^3.0.23",
|
|
31
|
+
"@ai-sdk/cerebras": "^2.0.26",
|
|
32
|
+
"@ai-sdk/cohere": "^3.0.14",
|
|
33
|
+
"@ai-sdk/deepinfra": "^2.0.25",
|
|
34
|
+
"@ai-sdk/gateway": "^3.0.28",
|
|
35
|
+
"@ai-sdk/google": "^3.0.17",
|
|
36
|
+
"@ai-sdk/google-vertex": "^4.0.36",
|
|
37
|
+
"@ai-sdk/groq": "^3.0.18",
|
|
38
|
+
"@ai-sdk/mistral": "^3.0.15",
|
|
39
|
+
"@ai-sdk/openai": "^3.0.22",
|
|
40
|
+
"@ai-sdk/openai-compatible": "^2.0.23",
|
|
41
|
+
"@ai-sdk/perplexity": "^3.0.14",
|
|
42
|
+
"@ai-sdk/togetherai": "^2.0.26",
|
|
43
|
+
"@ai-sdk/vercel": "^2.0.25",
|
|
44
|
+
"@ai-sdk/xai": "^3.0.42",
|
|
45
|
+
"@clack/prompts": "^1.0.0",
|
|
46
|
+
"@dqbd/tiktoken": "^1.0.22",
|
|
47
|
+
"@gitlab/gitlab-ai-provider": "^3.3.1",
|
|
48
|
+
"@openrouter/ai-sdk-provider": "^2.1.1",
|
|
49
|
+
"ai": "^6.0.61",
|
|
50
|
+
"yargs": "^18.0.0",
|
|
51
|
+
"yocto-spinner": "^1.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/bun": "latest",
|
|
55
|
+
"@types/node": "^25.1.0",
|
|
56
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
57
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
58
|
+
"eslint": "^9.39.2",
|
|
59
|
+
"eslint-config-prettier": "^10.1.8",
|
|
60
|
+
"globals": "^17.2.0",
|
|
61
|
+
"prettier": "^3.8.1",
|
|
62
|
+
"typescript": "^5.9.3",
|
|
63
|
+
"vitest": "^4.0.18"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"typescript": "^5.9.3"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import https from 'node:https';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import { Transform } from 'node:stream';
|
|
8
|
+
import { pipeline } from 'node:stream/promises';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { getAssetName, getInstallPath } from './platform.mjs';
|
|
11
|
+
|
|
12
|
+
const REPO = 'mboisvertdupras/zencommit';
|
|
13
|
+
const USER_AGENT = 'zencommit-installer';
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const pkgRoot = path.resolve(__dirname, '..');
|
|
17
|
+
|
|
18
|
+
const skipChecksum =
|
|
19
|
+
process.env.ZENCOMMIT_SKIP_CHECKSUM === '1' || process.env.ZENCOMMIT_SKIP_CHECKSUM === 'true';
|
|
20
|
+
|
|
21
|
+
function getReleaseBase(version) {
|
|
22
|
+
const base =
|
|
23
|
+
process.env.ZENCOMMIT_RELEASES_BASE_URL ??
|
|
24
|
+
`https://github.com/${REPO}/releases/download`;
|
|
25
|
+
const normalized = base.replace(/\/$/, '');
|
|
26
|
+
return `${normalized}/v${version}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function request(url, redirects = 5) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const client = url.startsWith('https:') ? https : http;
|
|
32
|
+
const req = client.get(
|
|
33
|
+
url,
|
|
34
|
+
{
|
|
35
|
+
headers: { 'User-Agent': USER_AGENT },
|
|
36
|
+
},
|
|
37
|
+
(res) => {
|
|
38
|
+
const status = res.statusCode ?? 0;
|
|
39
|
+
const location = res.headers.location;
|
|
40
|
+
const isRedirect = [301, 302, 303, 307, 308].includes(status);
|
|
41
|
+
if (isRedirect && location) {
|
|
42
|
+
if (redirects <= 0) {
|
|
43
|
+
res.resume();
|
|
44
|
+
reject(new Error(`Too many redirects for ${url}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
res.resume();
|
|
48
|
+
const nextUrl = new URL(location, url).toString();
|
|
49
|
+
request(nextUrl, redirects - 1).then(resolve, reject);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (status < 200 || status >= 300) {
|
|
53
|
+
res.resume();
|
|
54
|
+
reject(new Error(`Request failed: ${status} ${res.statusMessage ?? ''} (${url})`));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
resolve(res);
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
req.on('error', reject);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function downloadText(url) {
|
|
65
|
+
const res = await request(url);
|
|
66
|
+
const chunks = [];
|
|
67
|
+
for await (const chunk of res) {
|
|
68
|
+
chunks.push(chunk);
|
|
69
|
+
}
|
|
70
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function downloadFile(url, destPath) {
|
|
74
|
+
const res = await request(url);
|
|
75
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
76
|
+
const hash = crypto.createHash('sha256');
|
|
77
|
+
const hashStream = new Transform({
|
|
78
|
+
transform(chunk, _encoding, callback) {
|
|
79
|
+
hash.update(chunk);
|
|
80
|
+
callback(null, chunk);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
await pipeline(res, hashStream, createWriteStream(destPath));
|
|
84
|
+
return hash.digest('hex');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseChecksum(text, assetName) {
|
|
88
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim());
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
if (!line) continue;
|
|
91
|
+
const [hash, file] = line.split(/\s+/);
|
|
92
|
+
if (!hash || !file) continue;
|
|
93
|
+
const normalized = file.replace(/^\*/, '');
|
|
94
|
+
if (normalized === assetName) {
|
|
95
|
+
return hash;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function main() {
|
|
102
|
+
const pkgRaw = await fs.readFile(path.join(pkgRoot, 'package.json'), 'utf8');
|
|
103
|
+
const pkg = JSON.parse(pkgRaw);
|
|
104
|
+
const version = pkg.version;
|
|
105
|
+
if (!version) {
|
|
106
|
+
throw new Error('package.json version is missing');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const assetName = getAssetName(version);
|
|
110
|
+
const installPath = getInstallPath(pkgRoot);
|
|
111
|
+
const tmpPath = `${installPath}.tmp`;
|
|
112
|
+
const releaseBase = getReleaseBase(version);
|
|
113
|
+
const assetUrl = `${releaseBase}/${assetName}`;
|
|
114
|
+
const checksumsUrl = `${releaseBase}/checksums.txt`;
|
|
115
|
+
|
|
116
|
+
let expectedHash = null;
|
|
117
|
+
if (!skipChecksum) {
|
|
118
|
+
const checksums = await downloadText(checksumsUrl);
|
|
119
|
+
expectedHash = parseChecksum(checksums, assetName);
|
|
120
|
+
if (!expectedHash) {
|
|
121
|
+
throw new Error(`Missing checksum for ${assetName}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await fs.rm(installPath, { force: true });
|
|
126
|
+
await fs.rm(tmpPath, { force: true });
|
|
127
|
+
const actualHash = await downloadFile(assetUrl, tmpPath);
|
|
128
|
+
|
|
129
|
+
if (!skipChecksum && expectedHash && actualHash !== expectedHash) {
|
|
130
|
+
await fs.rm(tmpPath, { force: true });
|
|
131
|
+
throw new Error(`Checksum mismatch for ${assetName}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await fs.rename(tmpPath, installPath);
|
|
135
|
+
if (process.platform !== 'win32') {
|
|
136
|
+
await fs.chmod(installPath, 0o755);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(`Installed ${assetName}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main().catch((error) => {
|
|
143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
144
|
+
console.error(`zencommit install failed: ${message}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
});
|