zapdev 0.11.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 -61
- 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,23 +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
|
-
var DEFAULT_EFFORT = "low";
|
|
1818
1814
|
function resolveConfig(env = process.env, overrides = {}) {
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
model: overrides.model ?? env.
|
|
1822
|
-
|
|
1823
|
-
effort: overrides.effort ?? optionalValue(env.OLLAMA_EFFORT) ?? DEFAULT_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() ?? ""
|
|
1824
1819
|
};
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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;
|
|
1832
1833
|
}
|
|
1833
1834
|
|
|
1834
1835
|
// src/lib/git.ts
|
|
@@ -2330,65 +2331,49 @@ var commit_message_default = "You are zapdev, a CLI tool that creates commits qu
|
|
|
2330
2331
|
// src/prompts/index.ts
|
|
2331
2332
|
var COMMIT_SYSTEM_PROMPT = commit_message_default.trim();
|
|
2332
2333
|
|
|
2333
|
-
// src/lib/
|
|
2334
|
+
// src/lib/llm.ts
|
|
2334
2335
|
var REQUEST_TIMEOUT_MS = 25e3;
|
|
2335
|
-
var KEEP_ALIVE = "30m";
|
|
2336
2336
|
async function generateCommitMessage(diff, config, type) {
|
|
2337
2337
|
const systemPrompt = type ? applyCommitType(COMMIT_SYSTEM_PROMPT, type) : COMMIT_SYSTEM_PROMPT;
|
|
2338
2338
|
const messages = [
|
|
2339
2339
|
{ role: "system", content: systemPrompt },
|
|
2340
2340
|
{ role: "user", content: truncateDiff(diff) }
|
|
2341
2341
|
];
|
|
2342
|
-
let content;
|
|
2343
|
-
try {
|
|
2344
|
-
content = await chat(config, config.model, messages);
|
|
2345
|
-
} catch (error) {
|
|
2346
|
-
if (!config.backupModel || config.backupModel === config.model) throw error;
|
|
2347
|
-
try {
|
|
2348
|
-
content = await chat(config, config.backupModel, messages);
|
|
2349
|
-
} catch (backupError) {
|
|
2350
|
-
throw new AggregateError(
|
|
2351
|
-
[error, backupError],
|
|
2352
|
-
`Primary model failed: ${errorMessage(error)} Backup model failed: ${errorMessage(backupError)}`,
|
|
2353
|
-
{ cause: backupError }
|
|
2354
|
-
);
|
|
2355
|
-
}
|
|
2356
|
-
}
|
|
2357
|
-
return sanitizeCommitMessage(content);
|
|
2358
|
-
}
|
|
2359
|
-
async function chat(config, model, messages) {
|
|
2360
|
-
const url = `${config.ollamaUrl}/api/chat`;
|
|
2361
2342
|
let response;
|
|
2343
|
+
let body;
|
|
2362
2344
|
try {
|
|
2363
|
-
response = await fetch(url, {
|
|
2345
|
+
response = await fetch(config.url, {
|
|
2364
2346
|
method: "POST",
|
|
2365
2347
|
headers: { "content-type": "application/json" },
|
|
2366
2348
|
body: JSON.stringify({
|
|
2367
|
-
model,
|
|
2349
|
+
model: config.model,
|
|
2368
2350
|
stream: false,
|
|
2369
|
-
|
|
2370
|
-
think: config.effort,
|
|
2371
|
-
options: { temperature: 0.2 },
|
|
2351
|
+
reasoning_effort: config.effort,
|
|
2372
2352
|
messages
|
|
2373
2353
|
}),
|
|
2374
2354
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
2375
2355
|
});
|
|
2356
|
+
const text2 = await response.text();
|
|
2357
|
+
try {
|
|
2358
|
+
body = JSON.parse(text2);
|
|
2359
|
+
} catch {
|
|
2360
|
+
body = null;
|
|
2361
|
+
}
|
|
2376
2362
|
} catch (error) {
|
|
2377
|
-
throw new Error(describeRequestError(error
|
|
2363
|
+
throw new Error(describeRequestError(error), { cause: error });
|
|
2378
2364
|
}
|
|
2379
|
-
const body = await response.json().catch(() => null);
|
|
2380
2365
|
if (!response.ok) {
|
|
2381
|
-
throw new Error(`
|
|
2366
|
+
throw new Error(`LLM error: ${serverError(body) ?? `HTTP ${response.status}`}`);
|
|
2382
2367
|
}
|
|
2383
2368
|
const content = messageContent(body);
|
|
2384
|
-
if (content === null) throw new Error("
|
|
2385
|
-
return content;
|
|
2369
|
+
if (content === null) throw new Error("LLM returned an unexpected response shape.");
|
|
2370
|
+
return sanitizeCommitMessage(content);
|
|
2386
2371
|
}
|
|
2387
|
-
function describeRequestError(error
|
|
2372
|
+
function describeRequestError(error) {
|
|
2388
2373
|
if (errorName(error) === "TimeoutError") {
|
|
2389
|
-
return `
|
|
2374
|
+
return `LLM did not answer within ${REQUEST_TIMEOUT_MS / 1e3}s.`;
|
|
2390
2375
|
}
|
|
2391
|
-
return
|
|
2376
|
+
return "Could not complete the LLM request. Check URL and endpoint availability.";
|
|
2392
2377
|
}
|
|
2393
2378
|
function errorName(error) {
|
|
2394
2379
|
if (error && typeof error === "object" && "name" in error && typeof error.name === "string") {
|
|
@@ -2397,14 +2382,18 @@ function errorName(error) {
|
|
|
2397
2382
|
return null;
|
|
2398
2383
|
}
|
|
2399
2384
|
function serverError(body) {
|
|
2400
|
-
if (body
|
|
2401
|
-
|
|
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;
|
|
2402
2389
|
}
|
|
2403
2390
|
return null;
|
|
2404
2391
|
}
|
|
2405
2392
|
function messageContent(body) {
|
|
2406
|
-
if (!body || typeof body !== "object" || !("
|
|
2407
|
-
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;
|
|
2408
2397
|
if (!message || typeof message !== "object" || !("content" in message)) return null;
|
|
2409
2398
|
return typeof message.content === "string" ? message.content : null;
|
|
2410
2399
|
}
|
|
@@ -2416,9 +2405,17 @@ var commitCommand = defineCommand({
|
|
|
2416
2405
|
description: "Stage all changes and commit with an LLM-generated Conventional Commits message."
|
|
2417
2406
|
},
|
|
2418
2407
|
args: {
|
|
2408
|
+
url: {
|
|
2409
|
+
type: "string",
|
|
2410
|
+
description: "Override $ZD_URL, the complete Chat Completions endpoint."
|
|
2411
|
+
},
|
|
2419
2412
|
model: {
|
|
2420
2413
|
type: "string",
|
|
2421
|
-
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."
|
|
2422
2419
|
},
|
|
2423
2420
|
type: {
|
|
2424
2421
|
type: "string",
|
|
@@ -2453,10 +2450,6 @@ var commitCommand = defineCommand({
|
|
|
2453
2450
|
},
|
|
2454
2451
|
async run({ args }) {
|
|
2455
2452
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
2456
|
-
const config = resolveConfig(
|
|
2457
|
-
process.env,
|
|
2458
|
-
args.model ? { model: args.model } : {}
|
|
2459
|
-
);
|
|
2460
2453
|
if (args.rebase && args.merge) {
|
|
2461
2454
|
log.error("Choose either --rebase or --merge, not both.");
|
|
2462
2455
|
process.exitCode = 1;
|
|
@@ -2471,6 +2464,18 @@ var commitCommand = defineCommand({
|
|
|
2471
2464
|
process.exitCode = 1;
|
|
2472
2465
|
return;
|
|
2473
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
|
+
}
|
|
2474
2479
|
if (interactive) intro("zapdev commit");
|
|
2475
2480
|
if (!args.staged) await stageAll();
|
|
2476
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
|
],
|