zapdev 0.9.0 → 0.10.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 +85 -57
- package/dist/cli.js +124 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,55 @@
|
|
|
1
1
|
# zapdev
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Project Introduction
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**zapdev** is a lightweight TypeScript CLI that makes small, repetitive Git chores fast and precise.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
It stages changes, scans them for secrets, generates Conventional Commit messages with Ollama, and streamlines repository cleanup.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Project Architecture
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
flowchart LR
|
|
13
|
+
Entry["src/index.ts"] --> CLI["src/cli.ts"]
|
|
14
|
+
CLI --> Commands["src/commands"]
|
|
15
|
+
Commands --> Lib["src/lib"]
|
|
16
|
+
Commands --> Types["src/types"]
|
|
17
|
+
Lib --> Prompts["src/prompts"]
|
|
18
|
+
Lib --> Types
|
|
19
|
+
Lib --> Tools["Git, Gitleaks, Ollama"]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `src/index.ts`: bin launcher; enables the V8 compile cache, then loads `cli.js`.
|
|
23
|
+
- `src/cli.ts`: CLI entry; registers subcommands and opens the interactive menu.
|
|
24
|
+
- `src/commands/`: command UI and orchestration.
|
|
25
|
+
- `src/lib/`: pure logic and isolated Git, Gitleaks, and Ollama side effects.
|
|
26
|
+
- `src/prompts/`: LLM prompts inlined into the bundle at build time.
|
|
27
|
+
- `src/types/`: shared type declarations.
|
|
28
|
+
|
|
29
|
+
## Environment Variables
|
|
30
|
+
|
|
31
|
+
| Variable | Default | Required | Description |
|
|
32
|
+
| --- | --- | --- | --- |
|
|
33
|
+
| `OLLAMA_URL` | `http://localhost:11434` | No | Ollama base URL |
|
|
34
|
+
| `OLLAMA_MODEL` | `deepseek-v4-flash:cloud` | No | Model used to generate commit messages |
|
|
35
|
+
| `OLLAMA_BACKUP_MODEL` | - | No | Model used when generation with the primary model fails |
|
|
36
|
+
| `OLLAMA_EFFORT` | - | No | Ollama thinking effort (`low`, `medium`, `high`, or `max`) |
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
### Requirements
|
|
41
|
+
|
|
42
|
+
- **Node.js >= 20 (required):** runs the CLI.
|
|
43
|
+
- **Git (required):** provides the repository operations.
|
|
44
|
+
- **Ollama (required for `commit`):** generates Conventional Commit messages.
|
|
45
|
+
- **Gitleaks (recommended):** scans staged changes before message generation when available on `PATH`.
|
|
46
|
+
|
|
47
|
+
### Install
|
|
48
|
+
|
|
49
|
+
Install zapdev globally for daily use:
|
|
10
50
|
|
|
11
51
|
```bash
|
|
12
|
-
npm install -g zapdev
|
|
52
|
+
npm install -g zapdev
|
|
13
53
|
```
|
|
14
54
|
|
|
15
55
|
Or run it once without installing:
|
|
@@ -18,15 +58,28 @@ Or run it once without installing:
|
|
|
18
58
|
npx zapdev commit
|
|
19
59
|
```
|
|
20
60
|
|
|
21
|
-
|
|
61
|
+
### Development Setup
|
|
62
|
+
|
|
63
|
+
From a clone:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install
|
|
67
|
+
npm run zapdev # build then run the CLI in dev
|
|
68
|
+
npm run typecheck # tsc --noEmit
|
|
69
|
+
npm run lint # eslint
|
|
70
|
+
npm run test # vitest
|
|
71
|
+
npm run build # bundle to dist/ with esbuild
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`npm link` exposes the local `zapdev` binary after `npm run build`.
|
|
22
75
|
|
|
23
76
|
## Usage
|
|
24
77
|
|
|
25
|
-
Run `zapdev` with no command to pick one from an interactive menu
|
|
78
|
+
Run `zapdev` with no command to pick one from an interactive menu. Without a TTY, zapdev displays its usage instead.
|
|
26
79
|
|
|
27
80
|
### `zapdev commit`
|
|
28
81
|
|
|
29
|
-
Stages all changes
|
|
82
|
+
Stages all changes, scans them with Gitleaks when installed, generates a Conventional Commit message, and optionally pushes the commit.
|
|
30
83
|
|
|
31
84
|
```bash
|
|
32
85
|
zapdev commit
|
|
@@ -34,77 +87,52 @@ zapdev commit
|
|
|
34
87
|
|
|
35
88
|
| Flag | Description |
|
|
36
89
|
| --- | --- |
|
|
37
|
-
|
|
|
38
|
-
| `-t, --type <type>` | Force the Conventional
|
|
90
|
+
| `--model <model>` | Override the Ollama model |
|
|
91
|
+
| `-t, --type <type>` | Force the Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
|
|
39
92
|
| `-p, --push` | Push after committing without asking |
|
|
93
|
+
| `-s, --staged` | Commit only changes that are already staged |
|
|
94
|
+
| `-r, --rebase` | Rebase on upstream if the push is rejected |
|
|
95
|
+
| `-m, --merge` | Merge upstream if the push is rejected |
|
|
40
96
|
| `-y, --yes` | Skip prompts and commit directly |
|
|
41
97
|
|
|
42
98
|
```bash
|
|
43
|
-
zapdev commit -t feat # force the type
|
|
99
|
+
zapdev commit -t feat # force the type
|
|
100
|
+
zapdev commit --staged # leave unstaged changes untouched
|
|
44
101
|
```
|
|
45
102
|
|
|
46
|
-
|
|
103
|
+
Before contacting Ollama, zapdev runs `gitleaks git --staged` when Gitleaks is installed. A failed scan stops the commit; when Gitleaks is absent, the scan is skipped.
|
|
104
|
+
|
|
105
|
+
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.
|
|
47
106
|
|
|
48
|
-
Without a TTY
|
|
107
|
+
Without a TTY, zapdev commits automatically and only pushes when `--push` is set.
|
|
49
108
|
|
|
50
109
|
### `zapdev reset`
|
|
51
110
|
|
|
52
|
-
Operates on a
|
|
111
|
+
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.
|
|
53
112
|
|
|
54
113
|
```bash
|
|
55
|
-
zapdev reset # the current repo
|
|
56
|
-
zapdev reset ~/dev #
|
|
57
|
-
zapdev reset -p # switch
|
|
58
|
-
zapdev reset -t dev # switch
|
|
114
|
+
zapdev reset # reset the current repo or direct child repos
|
|
115
|
+
zapdev reset ~/dev # reset repos under a directory
|
|
116
|
+
zapdev reset -p # switch to the principal branch without prompting
|
|
117
|
+
zapdev reset -t dev # switch to dev or fall back to the principal branch
|
|
59
118
|
```
|
|
60
119
|
|
|
61
120
|
| Flag | Description |
|
|
62
121
|
| --- | --- |
|
|
63
122
|
| `-p, --principal` | Switch every repo to its resolved principal branch (`origin/HEAD`) |
|
|
64
|
-
| `-t, --target <branch>` | Switch
|
|
123
|
+
| `-t, --target <branch>` | Switch to a target branch, falling back to the principal branch |
|
|
65
124
|
| `--pull` | Pull the checked-out branch after reset without asking |
|
|
66
|
-
| `-y, --yes` |
|
|
125
|
+
| `-y, --yes` | Switch and delete without confirmation |
|
|
67
126
|
|
|
68
|
-
Deletion is permanent
|
|
127
|
+
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.
|
|
69
128
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`--yes` skips every prompt, which is exactly what you want behind a short alias. Two that pay off daily — add them to `~/.zshrc` or `~/.bashrc`:
|
|
129
|
+
### Shell Aliases
|
|
73
130
|
|
|
74
131
|
```bash
|
|
75
|
-
alias commit="zapdev commit --yes"
|
|
76
|
-
alias git-reset="zapdev reset --yes --principal --pull"
|
|
132
|
+
alias commit="zapdev commit --yes"
|
|
133
|
+
alias git-reset="zapdev reset --yes --principal --pull"
|
|
77
134
|
```
|
|
78
135
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
## Configuration
|
|
82
|
-
|
|
83
|
-
| Variable | Default | Description |
|
|
84
|
-
| --- | --- | --- |
|
|
85
|
-
| `OLLAMA_URL` | `http://localhost:11434` | Ollama base URL |
|
|
86
|
-
| `OLLAMA_MODEL` | `deepseek-v4-flash:cloud` | Model used to generate messages |
|
|
87
|
-
|
|
88
|
-
## Development
|
|
89
|
-
|
|
90
|
-
From a clone:
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
npm install
|
|
94
|
-
npm run zapdev # build then run the CLI in dev
|
|
95
|
-
npm run typecheck # tsc --noEmit
|
|
96
|
-
npm run lint # eslint
|
|
97
|
-
npm run test # vitest
|
|
98
|
-
npm run build # bundle to dist/ (esbuild)
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
`npm link` (after `npm run build`) exposes the local `zapdev` binary on your PATH.
|
|
102
|
-
|
|
103
|
-
## Project structure
|
|
136
|
+
## Other
|
|
104
137
|
|
|
105
|
-
|
|
106
|
-
- `src/cli.ts` — CLI entry (Citty); registers subcommands, defaults to `commit`.
|
|
107
|
-
- `src/commands/` — one file per command.
|
|
108
|
-
- `src/lib/` — pure logic (`config`, `commit-message`, `branches`, unit-tested) and side effects (`git`, `ollama`); shared helpers (`errors`).
|
|
109
|
-
- `src/prompts/` — LLM prompts as `.md` files, imported as text (esbuild `.md` text loader) and inlined into the bundle at build time.
|
|
110
|
-
- `src/types/` — shared type declarations (one file per domain) and ambient module declarations.
|
|
138
|
+
zapdev is available under the MIT license.
|
package/dist/cli.js
CHANGED
|
@@ -1718,7 +1718,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
1718
1718
|
// package.json
|
|
1719
1719
|
var package_default = {
|
|
1720
1720
|
name: "zapdev",
|
|
1721
|
-
version: "0.
|
|
1721
|
+
version: "0.10.0",
|
|
1722
1722
|
description: "Fast, performant and lightweight git chores and dev tools using your terminal and your own local LLM.",
|
|
1723
1723
|
type: "module",
|
|
1724
1724
|
bin: {
|
|
@@ -1817,9 +1817,14 @@ var DEFAULT_MODEL = "deepseek-v4-flash:cloud";
|
|
|
1817
1817
|
function resolveConfig(env = process.env, overrides = {}) {
|
|
1818
1818
|
return {
|
|
1819
1819
|
ollamaUrl: normalizeBaseUrl(overrides.ollamaUrl ?? env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL),
|
|
1820
|
-
model: overrides.model ?? env.OLLAMA_MODEL ?? DEFAULT_MODEL
|
|
1820
|
+
model: overrides.model ?? env.OLLAMA_MODEL ?? DEFAULT_MODEL,
|
|
1821
|
+
backupModel: overrides.backupModel ?? optionalValue(env.OLLAMA_BACKUP_MODEL),
|
|
1822
|
+
effort: overrides.effort ?? optionalValue(env.OLLAMA_EFFORT)
|
|
1821
1823
|
};
|
|
1822
1824
|
}
|
|
1825
|
+
function optionalValue(value) {
|
|
1826
|
+
return value?.trim() || void 0;
|
|
1827
|
+
}
|
|
1823
1828
|
function normalizeBaseUrl(raw) {
|
|
1824
1829
|
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
1825
1830
|
return /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
@@ -2225,6 +2230,9 @@ async function pushSetUpstream(branch) {
|
|
|
2225
2230
|
async function pullRebase() {
|
|
2226
2231
|
await git(["pull", "--rebase"]);
|
|
2227
2232
|
}
|
|
2233
|
+
async function pullMerge() {
|
|
2234
|
+
await git(["pull", "--no-rebase", "--no-edit"]);
|
|
2235
|
+
}
|
|
2228
2236
|
async function fetchRemote() {
|
|
2229
2237
|
await git(["fetch"]);
|
|
2230
2238
|
}
|
|
@@ -2296,6 +2304,25 @@ function errorMessage(error) {
|
|
|
2296
2304
|
return error instanceof Error ? error.message : String(error);
|
|
2297
2305
|
}
|
|
2298
2306
|
|
|
2307
|
+
// src/lib/gitleaks.ts
|
|
2308
|
+
async function hasGitleaks() {
|
|
2309
|
+
try {
|
|
2310
|
+
await R2("gitleaks", ["version"], { nodePath: false });
|
|
2311
|
+
return true;
|
|
2312
|
+
} catch (error) {
|
|
2313
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
2314
|
+
return false;
|
|
2315
|
+
}
|
|
2316
|
+
throw error;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
async function scanStagedChanges() {
|
|
2320
|
+
const result = await R2("gitleaks", ["git", "--staged"], { nodePath: false });
|
|
2321
|
+
if (result.exitCode === 0) return;
|
|
2322
|
+
const output = result.stderr.trim() || result.stdout.trim();
|
|
2323
|
+
throw new Error(output || "gitleaks git --staged failed");
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2299
2326
|
// src/prompts/commit-message.md
|
|
2300
2327
|
var commit_message_default = "You are zapdev, a CLI tool that creates commits quickly and reliably.\n\nGenerate ONE commit message in Conventional Commits format.\nA single line: imperative, English, \u226472 chars.\n\nStrictly follow this format:\n\n```text\ntype(scope): description\n```\n\nReply ONLY with the message, no backticks, no quotes, no explanation.\n";
|
|
2301
2328
|
|
|
@@ -2307,13 +2334,28 @@ var REQUEST_TIMEOUT_MS = 25e3;
|
|
|
2307
2334
|
var KEEP_ALIVE = "30m";
|
|
2308
2335
|
async function generateCommitMessage(diff, config, type) {
|
|
2309
2336
|
const systemPrompt = type ? applyCommitType(COMMIT_SYSTEM_PROMPT, type) : COMMIT_SYSTEM_PROMPT;
|
|
2310
|
-
const
|
|
2337
|
+
const messages = [
|
|
2311
2338
|
{ role: "system", content: systemPrompt },
|
|
2312
2339
|
{ role: "user", content: truncateDiff(diff) }
|
|
2313
|
-
]
|
|
2340
|
+
];
|
|
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
|
+
}
|
|
2314
2356
|
return sanitizeCommitMessage(content);
|
|
2315
2357
|
}
|
|
2316
|
-
async function chat(config, messages) {
|
|
2358
|
+
async function chat(config, model, messages) {
|
|
2317
2359
|
const url = `${config.ollamaUrl}/api/chat`;
|
|
2318
2360
|
let response;
|
|
2319
2361
|
try {
|
|
@@ -2321,9 +2363,10 @@ async function chat(config, messages) {
|
|
|
2321
2363
|
method: "POST",
|
|
2322
2364
|
headers: { "content-type": "application/json" },
|
|
2323
2365
|
body: JSON.stringify({
|
|
2324
|
-
model
|
|
2366
|
+
model,
|
|
2325
2367
|
stream: false,
|
|
2326
2368
|
keep_alive: KEEP_ALIVE,
|
|
2369
|
+
think: config.effort,
|
|
2327
2370
|
options: { temperature: 0.2 },
|
|
2328
2371
|
messages
|
|
2329
2372
|
}),
|
|
@@ -2374,7 +2417,6 @@ var commitCommand = defineCommand({
|
|
|
2374
2417
|
args: {
|
|
2375
2418
|
model: {
|
|
2376
2419
|
type: "string",
|
|
2377
|
-
alias: "m",
|
|
2378
2420
|
description: "Override the Ollama model (defaults to $OLLAMA_MODEL)."
|
|
2379
2421
|
},
|
|
2380
2422
|
type: {
|
|
@@ -2387,6 +2429,21 @@ var commitCommand = defineCommand({
|
|
|
2387
2429
|
alias: "p",
|
|
2388
2430
|
description: "Push after committing without asking."
|
|
2389
2431
|
},
|
|
2432
|
+
staged: {
|
|
2433
|
+
type: "boolean",
|
|
2434
|
+
alias: "s",
|
|
2435
|
+
description: "Commit only changes that are already staged."
|
|
2436
|
+
},
|
|
2437
|
+
rebase: {
|
|
2438
|
+
type: "boolean",
|
|
2439
|
+
alias: "r",
|
|
2440
|
+
description: "Rebase on the upstream branch if the push is rejected."
|
|
2441
|
+
},
|
|
2442
|
+
merge: {
|
|
2443
|
+
type: "boolean",
|
|
2444
|
+
alias: "m",
|
|
2445
|
+
description: "Merge the upstream branch if the push is rejected."
|
|
2446
|
+
},
|
|
2390
2447
|
yes: {
|
|
2391
2448
|
type: "boolean",
|
|
2392
2449
|
alias: "y",
|
|
@@ -2399,6 +2456,12 @@ var commitCommand = defineCommand({
|
|
|
2399
2456
|
process.env,
|
|
2400
2457
|
args.model ? { model: args.model } : {}
|
|
2401
2458
|
);
|
|
2459
|
+
if (args.rebase && args.merge) {
|
|
2460
|
+
log.error("Choose either --rebase or --merge, not both.");
|
|
2461
|
+
process.exitCode = 1;
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
const syncStrategy = args.rebase ? "rebase" : args.merge ? "merge" : void 0;
|
|
2402
2465
|
const type = args.type ? normalizeCommitType(args.type) : void 0;
|
|
2403
2466
|
if (type === null) {
|
|
2404
2467
|
log.error(
|
|
@@ -2408,13 +2471,32 @@ var commitCommand = defineCommand({
|
|
|
2408
2471
|
return;
|
|
2409
2472
|
}
|
|
2410
2473
|
if (interactive) intro("zapdev commit");
|
|
2411
|
-
await stageAll();
|
|
2474
|
+
if (!args.staged) await stageAll();
|
|
2412
2475
|
const diff = await getStagedDiff();
|
|
2413
2476
|
if (!diff.trim()) {
|
|
2414
2477
|
log.warn("Nothing to commit.");
|
|
2415
2478
|
if (interactive) outro("Nothing to do.");
|
|
2416
2479
|
return;
|
|
2417
2480
|
}
|
|
2481
|
+
try {
|
|
2482
|
+
if (await hasGitleaks()) {
|
|
2483
|
+
const scanLoader = interactive ? spinner() : void 0;
|
|
2484
|
+
scanLoader?.start("Scanning staged changes with Gitleaks");
|
|
2485
|
+
try {
|
|
2486
|
+
await scanStagedChanges();
|
|
2487
|
+
scanLoader?.stop("No leaks found");
|
|
2488
|
+
} catch (error) {
|
|
2489
|
+
scanLoader?.error("Gitleaks check failed");
|
|
2490
|
+
throw error;
|
|
2491
|
+
}
|
|
2492
|
+
} else {
|
|
2493
|
+
log.info("Gitleaks not found, skipping secret scan.");
|
|
2494
|
+
}
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
log.error(`Gitleaks check failed: ${errorMessage(error)}`);
|
|
2497
|
+
process.exitCode = 1;
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2418
2500
|
const loader = interactive ? spinner() : void 0;
|
|
2419
2501
|
loader?.start("Generating commit message");
|
|
2420
2502
|
let message;
|
|
@@ -2477,7 +2559,11 @@ var commitCommand = defineCommand({
|
|
|
2477
2559
|
shouldPush = answer;
|
|
2478
2560
|
}
|
|
2479
2561
|
if (shouldPush) {
|
|
2480
|
-
const pushed = await pushOptimistic(
|
|
2562
|
+
const pushed = await pushOptimistic(
|
|
2563
|
+
interactive,
|
|
2564
|
+
interactive && !args.yes,
|
|
2565
|
+
syncStrategy
|
|
2566
|
+
);
|
|
2481
2567
|
if (!pushed) {
|
|
2482
2568
|
process.exitCode = 1;
|
|
2483
2569
|
return;
|
|
@@ -2486,26 +2572,28 @@ var commitCommand = defineCommand({
|
|
|
2486
2572
|
if (interactive) outro("Done.");
|
|
2487
2573
|
}
|
|
2488
2574
|
});
|
|
2489
|
-
async function
|
|
2575
|
+
async function syncWithUpstream(strategy, interactive) {
|
|
2490
2576
|
const loader = interactive ? spinner() : void 0;
|
|
2491
|
-
|
|
2577
|
+
const label = strategy === "rebase" ? "Rebase" : "Merge";
|
|
2578
|
+
loader?.start(`Pulling --${strategy === "rebase" ? "rebase" : "no-rebase"}`);
|
|
2492
2579
|
try {
|
|
2493
|
-
await pullRebase();
|
|
2494
|
-
loader?.stop("\u2713 Rebased on upstream");
|
|
2580
|
+
await (strategy === "rebase" ? pullRebase() : pullMerge());
|
|
2581
|
+
loader?.stop(strategy === "rebase" ? "\u2713 Rebased on upstream" : "\u2713 Merged upstream");
|
|
2495
2582
|
return true;
|
|
2496
2583
|
} catch (error) {
|
|
2497
|
-
loader?.error(
|
|
2498
|
-
log.error(
|
|
2584
|
+
loader?.error(`${label} failed`);
|
|
2585
|
+
log.error(`${label} failed (resolve conflicts, then push): ${errorMessage(error)}`);
|
|
2499
2586
|
return false;
|
|
2500
2587
|
}
|
|
2501
2588
|
}
|
|
2502
|
-
async function pushOptimistic(interactive) {
|
|
2589
|
+
async function pushOptimistic(interactive, canPrompt, strategy) {
|
|
2503
2590
|
const [upstream, branch] = await Promise.all([hasUpstream(), currentBranch()]);
|
|
2504
2591
|
const doPush = () => upstream ? push() : pushSetUpstream(branch);
|
|
2505
2592
|
const first = await tryPush(interactive, doPush);
|
|
2506
2593
|
if (first.ok) return true;
|
|
2507
2594
|
if (upstream && await isBehind(interactive)) {
|
|
2508
|
-
|
|
2595
|
+
const syncStrategy = strategy ?? await chooseSyncStrategy(canPrompt);
|
|
2596
|
+
if (!syncStrategy || !await syncWithUpstream(syncStrategy, interactive)) return false;
|
|
2509
2597
|
const retry = await tryPush(interactive, doPush);
|
|
2510
2598
|
if (retry.ok) return true;
|
|
2511
2599
|
log.error(`Push failed: ${errorMessage(retry.error)}`);
|
|
@@ -2514,6 +2602,25 @@ async function pushOptimistic(interactive) {
|
|
|
2514
2602
|
log.error(`Push failed: ${errorMessage(first.error)}`);
|
|
2515
2603
|
return false;
|
|
2516
2604
|
}
|
|
2605
|
+
async function chooseSyncStrategy(interactive) {
|
|
2606
|
+
if (!interactive) {
|
|
2607
|
+
log.error("Branch is behind upstream. Re-run with --rebase or --merge.");
|
|
2608
|
+
return null;
|
|
2609
|
+
}
|
|
2610
|
+
const action = await select({
|
|
2611
|
+
message: "Branch is behind upstream. How should zapdev sync it?",
|
|
2612
|
+
options: [
|
|
2613
|
+
{ value: "rebase", label: "Rebase" },
|
|
2614
|
+
{ value: "merge", label: "Merge" },
|
|
2615
|
+
{ value: "quit", label: "Quit" }
|
|
2616
|
+
]
|
|
2617
|
+
});
|
|
2618
|
+
if (isCancel(action) || action === "quit") {
|
|
2619
|
+
log.warn("Push cancelled. Commit remains local.");
|
|
2620
|
+
return null;
|
|
2621
|
+
}
|
|
2622
|
+
return action;
|
|
2623
|
+
}
|
|
2517
2624
|
async function tryPush(interactive, doPush) {
|
|
2518
2625
|
const loader = interactive ? spinner() : void 0;
|
|
2519
2626
|
loader?.start("Pushing");
|