zapdev 0.10.0 → 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 +70 -25
- package/dist/cli.js +66 -60
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**zapdev** is a lightweight TypeScript CLI that makes small, repetitive Git chores fast and precise.
|
|
6
6
|
|
|
7
|
-
It stages changes, scans them for secrets, generates Conventional Commit messages with
|
|
7
|
+
It stages changes, scans them for secrets, generates Conventional Commit messages with an OpenAI-compatible LLM endpoint, and streamlines repository cleanup.
|
|
8
8
|
|
|
9
9
|
## Project Architecture
|
|
10
10
|
|
|
@@ -16,24 +16,27 @@ flowchart LR
|
|
|
16
16
|
Commands --> Types["src/types"]
|
|
17
17
|
Lib --> Prompts["src/prompts"]
|
|
18
18
|
Lib --> Types
|
|
19
|
-
Lib --> Tools["Git, Gitleaks,
|
|
19
|
+
Lib --> Tools["Git, Gitleaks, LLM API"]
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
- `src/index.ts`: bin launcher; enables the V8 compile cache, then loads `cli.js`.
|
|
23
23
|
- `src/cli.ts`: CLI entry; registers subcommands and opens the interactive menu.
|
|
24
24
|
- `src/commands/`: command UI and orchestration.
|
|
25
|
-
- `src/lib/`: pure logic and isolated Git, Gitleaks, and
|
|
25
|
+
- `src/lib/`: pure logic and isolated Git, Gitleaks, and LLM side effects.
|
|
26
26
|
- `src/prompts/`: LLM prompts inlined into the bundle at build time.
|
|
27
27
|
- `src/types/`: shared type declarations.
|
|
28
28
|
|
|
29
29
|
## Environment Variables
|
|
30
30
|
|
|
31
|
-
| Variable |
|
|
32
|
-
|
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
|
|
31
|
+
| Variable | Required | Description |
|
|
32
|
+
| -------- | -------- | ----------- |
|
|
33
|
+
| `ZD_URL` | For `commit` | Complete HTTP(S) Chat Completions endpoint, including its path |
|
|
34
|
+
| `ZD_MODEL` | For `commit` | Model identifier supported by the endpoint |
|
|
35
|
+
| `ZD_EFFORT` | For `commit` | Sent as `reasoning_effort`; use a value supported by your model, such as `low`, `medium`, or `high` |
|
|
36
|
+
|
|
37
|
+
There are no defaults, automatic provider detection, or backup models. CLI flags override these variables. Legacy `OLLAMA_*` variables are no longer read. Other commands do not require LLM configuration.
|
|
38
|
+
|
|
39
|
+
Requests use the OpenAI Chat Completions format over plain `fetch`, without a provider SDK. No authentication headers are sent; use an endpoint that does not require them. The endpoint and model must support `reasoning_effort`.
|
|
37
40
|
|
|
38
41
|
## Setup
|
|
39
42
|
|
|
@@ -41,7 +44,7 @@ flowchart LR
|
|
|
41
44
|
|
|
42
45
|
- **Node.js >= 20 (required):** runs the CLI.
|
|
43
46
|
- **Git (required):** provides the repository operations.
|
|
44
|
-
- **
|
|
47
|
+
- **OpenAI-compatible Chat Completions endpoint (required for `commit`):** generates Conventional Commit messages.
|
|
45
48
|
- **Gitleaks (recommended):** scans staged changes before message generation when available on `PATH`.
|
|
46
49
|
|
|
47
50
|
### Install
|
|
@@ -52,6 +55,14 @@ Install zapdev globally for daily use:
|
|
|
52
55
|
npm install -g zapdev
|
|
53
56
|
```
|
|
54
57
|
|
|
58
|
+
Configure your endpoint and model before running `commit` (replace these example values):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export ZD_URL="http://localhost:1234/v1/chat/completions"
|
|
62
|
+
export ZD_MODEL="your-model-id"
|
|
63
|
+
export ZD_EFFORT="low"
|
|
64
|
+
```
|
|
65
|
+
|
|
55
66
|
Or run it once without installing:
|
|
56
67
|
|
|
57
68
|
```bash
|
|
@@ -85,27 +96,61 @@ Stages all changes, scans them with Gitleaks when installed, generates a Convent
|
|
|
85
96
|
zapdev commit
|
|
86
97
|
```
|
|
87
98
|
|
|
88
|
-
| Flag
|
|
89
|
-
|
|
|
90
|
-
| `--
|
|
99
|
+
| Flag | Description |
|
|
100
|
+
| ------------------- | ----------------------------------------------------------------- |
|
|
101
|
+
| `--url <url>` | Override the complete Chat Completions endpoint |
|
|
102
|
+
| `--model <model>` | Override the model |
|
|
103
|
+
| `--effort <effort>` | Override the reasoning effort |
|
|
91
104
|
| `-t, --type <type>` | Force the Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
|
|
92
|
-
| `-p, --push`
|
|
93
|
-
| `-s, --staged`
|
|
94
|
-
| `-r, --rebase`
|
|
95
|
-
| `-m, --merge`
|
|
96
|
-
| `-y, --yes`
|
|
105
|
+
| `-p, --push` | Push after committing without asking |
|
|
106
|
+
| `-s, --staged` | Commit only changes that are already staged |
|
|
107
|
+
| `-r, --rebase` | Rebase on upstream if the push is rejected |
|
|
108
|
+
| `-m, --merge` | Merge upstream if the push is rejected |
|
|
109
|
+
| `-y, --yes` | Skip prompts and commit directly |
|
|
97
110
|
|
|
98
111
|
```bash
|
|
99
112
|
zapdev commit -t feat # force the type
|
|
100
113
|
zapdev commit --staged # leave unstaged changes untouched
|
|
101
114
|
```
|
|
102
115
|
|
|
103
|
-
Before contacting
|
|
116
|
+
Before contacting the LLM endpoint, zapdev runs `gitleaks git --staged` when Gitleaks is installed. A failed scan stops the commit; when Gitleaks is absent, the scan is skipped. The staged diff is sent to the configured endpoint, which may be remote.
|
|
104
117
|
|
|
105
118
|
Pushing is optimistic, with no preliminary fetch. If the branch is behind upstream, `--rebase` runs `git pull --rebase`, while `--merge` runs `git pull --no-rebase --no-edit`; zapdev then retries once. Without either flag, interactive runs ask whether to rebase, merge, or quit. Runs using `--yes` or without a TTY must provide one of the flags.
|
|
106
119
|
|
|
107
120
|
Without a TTY, zapdev commits automatically and only pushes when `--push` is set.
|
|
108
121
|
|
|
122
|
+
### Zed IDE
|
|
123
|
+
|
|
124
|
+
For a faster review and commit workflow, review and stage changes from Zed's Git panel, then run `zapdev commit -syp` from a task. The command commits only staged changes, skips prompts, and pushes the commit.
|
|
125
|
+
|
|
126
|
+
Add the following tasks to `.zed/tasks.json`:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
[
|
|
130
|
+
{
|
|
131
|
+
"label": "Safely commit staged changes.",
|
|
132
|
+
"command": "zapdev commit -s",
|
|
133
|
+
"reveal": "always",
|
|
134
|
+
"hide": "on_success",
|
|
135
|
+
"reveal_target": "center"
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Add this entry to Zed's `keymap.json` to run the commit task with `ctrl-cmd-enter`:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"context": "Pane",
|
|
145
|
+
"bindings": {
|
|
146
|
+
"ctrl-cmd-enter": [
|
|
147
|
+
"task::Spawn",
|
|
148
|
+
{ "task_name": "Safely commit staged changes." }
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
109
154
|
### `zapdev reset`
|
|
110
155
|
|
|
111
156
|
Operates on a Git repository or the direct child repositories of a directory. It fetches and prunes, switches branch, then permanently removes other local branches and linked worktrees.
|
|
@@ -117,12 +162,12 @@ zapdev reset -p # switch to the principal branch without prompting
|
|
|
117
162
|
zapdev reset -t dev # switch to dev or fall back to the principal branch
|
|
118
163
|
```
|
|
119
164
|
|
|
120
|
-
| Flag
|
|
121
|
-
|
|
|
122
|
-
| `-p, --principal`
|
|
123
|
-
| `-t, --target <branch>` | Switch to a target branch, falling back to the principal branch
|
|
124
|
-
| `--pull`
|
|
125
|
-
| `-y, --yes`
|
|
165
|
+
| Flag | Description |
|
|
166
|
+
| ----------------------- | ------------------------------------------------------------------ |
|
|
167
|
+
| `-p, --principal` | Switch every repo to its resolved principal branch (`origin/HEAD`) |
|
|
168
|
+
| `-t, --target <branch>` | Switch to a target branch, falling back to the principal branch |
|
|
169
|
+
| `--pull` | Pull the checked-out branch after reset without asking |
|
|
170
|
+
| `-y, --yes` | Switch and delete without confirmation |
|
|
126
171
|
|
|
127
172
|
Deletion is permanent. Branches are removed with `git branch -D`; worktrees are removed with `git worktree remove --force`. Without a TTY, pass `--yes` or the destructive step is refused. `node_modules` is never scanned.
|
|
128
173
|
|
package/dist/cli.js
CHANGED
|
@@ -1718,8 +1718,8 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
1718
1718
|
// package.json
|
|
1719
1719
|
var package_default = {
|
|
1720
1720
|
name: "zapdev",
|
|
1721
|
-
version: "0.
|
|
1722
|
-
description: "Fast, performant and lightweight git chores and dev tools using your terminal and
|
|
1721
|
+
version: "1.0.0",
|
|
1722
|
+
description: "Fast, performant and lightweight git chores and dev tools using your terminal and an OpenAI-compatible LLM endpoint.",
|
|
1723
1723
|
type: "module",
|
|
1724
1724
|
bin: {
|
|
1725
1725
|
zapdev: "./dist/index.js"
|
|
@@ -1735,7 +1735,6 @@ var package_default = {
|
|
|
1735
1735
|
"cli",
|
|
1736
1736
|
"commit",
|
|
1737
1737
|
"conventional-commits",
|
|
1738
|
-
"ollama",
|
|
1739
1738
|
"llm",
|
|
1740
1739
|
"ai"
|
|
1741
1740
|
],
|
|
@@ -1812,22 +1811,25 @@ function sanitizeCommitMessage(raw) {
|
|
|
1812
1811
|
}
|
|
1813
1812
|
|
|
1814
1813
|
// src/lib/config.ts
|
|
1815
|
-
var DEFAULT_OLLAMA_URL = "http://localhost:11434";
|
|
1816
|
-
var DEFAULT_MODEL = "deepseek-v4-flash:cloud";
|
|
1817
1814
|
function resolveConfig(env = process.env, overrides = {}) {
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
model: overrides.model ?? env.
|
|
1821
|
-
|
|
1822
|
-
effort: overrides.effort ?? optionalValue(env.OLLAMA_EFFORT)
|
|
1815
|
+
const config = {
|
|
1816
|
+
url: (overrides.url ?? env.ZD_URL)?.trim() ?? "",
|
|
1817
|
+
model: (overrides.model ?? env.ZD_MODEL)?.trim() ?? "",
|
|
1818
|
+
effort: (overrides.effort ?? env.ZD_EFFORT)?.trim() ?? ""
|
|
1823
1819
|
};
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1820
|
+
for (const [key, value] of Object.entries(config)) {
|
|
1821
|
+
if (!value) throw new Error(`Set ZD_${key.toUpperCase()} or --${key} before generating a commit message.`);
|
|
1822
|
+
}
|
|
1823
|
+
let url;
|
|
1824
|
+
try {
|
|
1825
|
+
url = new URL(config.url);
|
|
1826
|
+
} catch {
|
|
1827
|
+
throw new Error("URL must be a complete HTTP(S) Chat Completions endpoint.");
|
|
1828
|
+
}
|
|
1829
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
1830
|
+
throw new Error("URL must be a complete HTTP(S) Chat Completions endpoint.");
|
|
1831
|
+
}
|
|
1832
|
+
return config;
|
|
1831
1833
|
}
|
|
1832
1834
|
|
|
1833
1835
|
// src/lib/git.ts
|
|
@@ -2329,65 +2331,49 @@ var commit_message_default = "You are zapdev, a CLI tool that creates commits qu
|
|
|
2329
2331
|
// src/prompts/index.ts
|
|
2330
2332
|
var COMMIT_SYSTEM_PROMPT = commit_message_default.trim();
|
|
2331
2333
|
|
|
2332
|
-
// src/lib/
|
|
2334
|
+
// src/lib/llm.ts
|
|
2333
2335
|
var REQUEST_TIMEOUT_MS = 25e3;
|
|
2334
|
-
var KEEP_ALIVE = "30m";
|
|
2335
2336
|
async function generateCommitMessage(diff, config, type) {
|
|
2336
2337
|
const systemPrompt = type ? applyCommitType(COMMIT_SYSTEM_PROMPT, type) : COMMIT_SYSTEM_PROMPT;
|
|
2337
2338
|
const messages = [
|
|
2338
2339
|
{ role: "system", content: systemPrompt },
|
|
2339
2340
|
{ role: "user", content: truncateDiff(diff) }
|
|
2340
2341
|
];
|
|
2341
|
-
let content;
|
|
2342
|
-
try {
|
|
2343
|
-
content = await chat(config, config.model, messages);
|
|
2344
|
-
} catch (error) {
|
|
2345
|
-
if (!config.backupModel || config.backupModel === config.model) throw error;
|
|
2346
|
-
try {
|
|
2347
|
-
content = await chat(config, config.backupModel, messages);
|
|
2348
|
-
} catch (backupError) {
|
|
2349
|
-
throw new AggregateError(
|
|
2350
|
-
[error, backupError],
|
|
2351
|
-
`Primary model failed: ${errorMessage(error)} Backup model failed: ${errorMessage(backupError)}`,
|
|
2352
|
-
{ cause: backupError }
|
|
2353
|
-
);
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
return sanitizeCommitMessage(content);
|
|
2357
|
-
}
|
|
2358
|
-
async function chat(config, model, messages) {
|
|
2359
|
-
const url = `${config.ollamaUrl}/api/chat`;
|
|
2360
2342
|
let response;
|
|
2343
|
+
let body;
|
|
2361
2344
|
try {
|
|
2362
|
-
response = await fetch(url, {
|
|
2345
|
+
response = await fetch(config.url, {
|
|
2363
2346
|
method: "POST",
|
|
2364
2347
|
headers: { "content-type": "application/json" },
|
|
2365
2348
|
body: JSON.stringify({
|
|
2366
|
-
model,
|
|
2349
|
+
model: config.model,
|
|
2367
2350
|
stream: false,
|
|
2368
|
-
|
|
2369
|
-
think: config.effort,
|
|
2370
|
-
options: { temperature: 0.2 },
|
|
2351
|
+
reasoning_effort: config.effort,
|
|
2371
2352
|
messages
|
|
2372
2353
|
}),
|
|
2373
2354
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
2374
2355
|
});
|
|
2356
|
+
const text2 = await response.text();
|
|
2357
|
+
try {
|
|
2358
|
+
body = JSON.parse(text2);
|
|
2359
|
+
} catch {
|
|
2360
|
+
body = null;
|
|
2361
|
+
}
|
|
2375
2362
|
} catch (error) {
|
|
2376
|
-
throw new Error(describeRequestError(error
|
|
2363
|
+
throw new Error(describeRequestError(error), { cause: error });
|
|
2377
2364
|
}
|
|
2378
|
-
const body = await response.json().catch(() => null);
|
|
2379
2365
|
if (!response.ok) {
|
|
2380
|
-
throw new Error(`
|
|
2366
|
+
throw new Error(`LLM error: ${serverError(body) ?? `HTTP ${response.status}`}`);
|
|
2381
2367
|
}
|
|
2382
2368
|
const content = messageContent(body);
|
|
2383
|
-
if (content === null) throw new Error("
|
|
2384
|
-
return content;
|
|
2369
|
+
if (content === null) throw new Error("LLM returned an unexpected response shape.");
|
|
2370
|
+
return sanitizeCommitMessage(content);
|
|
2385
2371
|
}
|
|
2386
|
-
function describeRequestError(error
|
|
2372
|
+
function describeRequestError(error) {
|
|
2387
2373
|
if (errorName(error) === "TimeoutError") {
|
|
2388
|
-
return `
|
|
2374
|
+
return `LLM did not answer within ${REQUEST_TIMEOUT_MS / 1e3}s.`;
|
|
2389
2375
|
}
|
|
2390
|
-
return
|
|
2376
|
+
return "Could not complete the LLM request. Check URL and endpoint availability.";
|
|
2391
2377
|
}
|
|
2392
2378
|
function errorName(error) {
|
|
2393
2379
|
if (error && typeof error === "object" && "name" in error && typeof error.name === "string") {
|
|
@@ -2396,14 +2382,18 @@ function errorName(error) {
|
|
|
2396
2382
|
return null;
|
|
2397
2383
|
}
|
|
2398
2384
|
function serverError(body) {
|
|
2399
|
-
if (body
|
|
2400
|
-
|
|
2385
|
+
if (!body || typeof body !== "object" || !("error" in body)) return null;
|
|
2386
|
+
const error = body.error;
|
|
2387
|
+
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
|
|
2388
|
+
return error.message;
|
|
2401
2389
|
}
|
|
2402
2390
|
return null;
|
|
2403
2391
|
}
|
|
2404
2392
|
function messageContent(body) {
|
|
2405
|
-
if (!body || typeof body !== "object" || !("
|
|
2406
|
-
const
|
|
2393
|
+
if (!body || typeof body !== "object" || !("choices" in body) || !Array.isArray(body.choices)) return null;
|
|
2394
|
+
const choice = body.choices[0];
|
|
2395
|
+
if (!choice || typeof choice !== "object" || !("message" in choice)) return null;
|
|
2396
|
+
const message = choice.message;
|
|
2407
2397
|
if (!message || typeof message !== "object" || !("content" in message)) return null;
|
|
2408
2398
|
return typeof message.content === "string" ? message.content : null;
|
|
2409
2399
|
}
|
|
@@ -2415,9 +2405,17 @@ var commitCommand = defineCommand({
|
|
|
2415
2405
|
description: "Stage all changes and commit with an LLM-generated Conventional Commits message."
|
|
2416
2406
|
},
|
|
2417
2407
|
args: {
|
|
2408
|
+
url: {
|
|
2409
|
+
type: "string",
|
|
2410
|
+
description: "Override $ZD_URL, the complete Chat Completions endpoint."
|
|
2411
|
+
},
|
|
2418
2412
|
model: {
|
|
2419
2413
|
type: "string",
|
|
2420
|
-
description: "Override the
|
|
2414
|
+
description: "Override the model configured with $ZD_MODEL."
|
|
2415
|
+
},
|
|
2416
|
+
effort: {
|
|
2417
|
+
type: "string",
|
|
2418
|
+
description: "Override $ZD_EFFORT, sent as reasoning_effort."
|
|
2421
2419
|
},
|
|
2422
2420
|
type: {
|
|
2423
2421
|
type: "string",
|
|
@@ -2452,10 +2450,6 @@ var commitCommand = defineCommand({
|
|
|
2452
2450
|
},
|
|
2453
2451
|
async run({ args }) {
|
|
2454
2452
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
2455
|
-
const config = resolveConfig(
|
|
2456
|
-
process.env,
|
|
2457
|
-
args.model ? { model: args.model } : {}
|
|
2458
|
-
);
|
|
2459
2453
|
if (args.rebase && args.merge) {
|
|
2460
2454
|
log.error("Choose either --rebase or --merge, not both.");
|
|
2461
2455
|
process.exitCode = 1;
|
|
@@ -2470,6 +2464,18 @@ var commitCommand = defineCommand({
|
|
|
2470
2464
|
process.exitCode = 1;
|
|
2471
2465
|
return;
|
|
2472
2466
|
}
|
|
2467
|
+
let config;
|
|
2468
|
+
try {
|
|
2469
|
+
config = resolveConfig(process.env, {
|
|
2470
|
+
url: args.url,
|
|
2471
|
+
model: args.model,
|
|
2472
|
+
effort: args.effort
|
|
2473
|
+
});
|
|
2474
|
+
} catch (error) {
|
|
2475
|
+
log.error(errorMessage(error));
|
|
2476
|
+
process.exitCode = 1;
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2473
2479
|
if (interactive) intro("zapdev commit");
|
|
2474
2480
|
if (!args.staged) await stageAll();
|
|
2475
2481
|
const diff = await getStagedDiff();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zapdev",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Fast, performant and lightweight git chores and dev tools using your terminal and
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fast, performant and lightweight git chores and dev tools using your terminal and an OpenAI-compatible LLM endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"zapdev": "./dist/index.js"
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"cli",
|
|
18
18
|
"commit",
|
|
19
19
|
"conventional-commits",
|
|
20
|
-
"ollama",
|
|
21
20
|
"llm",
|
|
22
21
|
"ai"
|
|
23
22
|
],
|