zapdev 0.8.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 -47
- package/dist/cli.js +124 -38
- 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,67 +87,52 @@ zapdev commit
|
|
|
34
87
|
|
|
35
88
|
| Flag | Description |
|
|
36
89
|
| --- | --- |
|
|
37
|
-
|
|
|
38
|
-
| `-t, --type <type>` | Force the Conventional
|
|
39
|
-
| `--pull` | Rebase onto upstream (`git pull --rebase`) after committing, before pushing |
|
|
90
|
+
| `--model <model>` | Override the Ollama model |
|
|
91
|
+
| `-t, --type <type>` | Force the Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
|
|
40
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 |
|
|
41
96
|
| `-y, --yes` | Skip prompts and commit directly |
|
|
42
97
|
|
|
43
98
|
```bash
|
|
44
|
-
zapdev commit -t feat # force the type
|
|
99
|
+
zapdev commit -t feat # force the type
|
|
100
|
+
zapdev commit --staged # leave unstaged changes untouched
|
|
45
101
|
```
|
|
46
102
|
|
|
47
|
-
|
|
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.
|
|
48
106
|
|
|
49
|
-
Without a TTY
|
|
107
|
+
Without a TTY, zapdev commits automatically and only pushes when `--push` is set.
|
|
50
108
|
|
|
51
109
|
### `zapdev reset`
|
|
52
110
|
|
|
53
|
-
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.
|
|
54
112
|
|
|
55
113
|
```bash
|
|
56
|
-
zapdev reset # the current repo
|
|
57
|
-
zapdev reset ~/dev #
|
|
58
|
-
zapdev reset -p # switch
|
|
59
|
-
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
|
|
60
118
|
```
|
|
61
119
|
|
|
62
120
|
| Flag | Description |
|
|
63
121
|
| --- | --- |
|
|
64
122
|
| `-p, --principal` | Switch every repo to its resolved principal branch (`origin/HEAD`) |
|
|
65
|
-
| `-t, --target <branch>` | Switch
|
|
123
|
+
| `-t, --target <branch>` | Switch to a target branch, falling back to the principal branch |
|
|
66
124
|
| `--pull` | Pull the checked-out branch after reset without asking |
|
|
67
|
-
| `-y, --yes` |
|
|
68
|
-
|
|
69
|
-
Deletion is permanent: branches via `git branch -D` (force), worktrees via `git worktree remove --force`. Worktrees are removed first so the branches they held become deletable. Without a TTY, pass `--yes` — otherwise the destructive step is refused. `node_modules` is never scanned.
|
|
70
|
-
|
|
71
|
-
## Configuration
|
|
125
|
+
| `-y, --yes` | Switch and delete without confirmation |
|
|
72
126
|
|
|
73
|
-
|
|
74
|
-
| --- | --- | --- |
|
|
75
|
-
| `OLLAMA_URL` | `http://localhost:11434` | Ollama base URL |
|
|
76
|
-
| `OLLAMA_MODEL` | `deepseek-v4-flash:cloud` | Model used to generate messages |
|
|
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.
|
|
77
128
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
From a clone:
|
|
129
|
+
### Shell Aliases
|
|
81
130
|
|
|
82
131
|
```bash
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
npm run typecheck # tsc --noEmit
|
|
86
|
-
npm run lint # eslint
|
|
87
|
-
npm run test # vitest
|
|
88
|
-
npm run build # bundle to dist/ (esbuild)
|
|
132
|
+
alias commit="zapdev commit --yes"
|
|
133
|
+
alias git-reset="zapdev reset --yes --principal --pull"
|
|
89
134
|
```
|
|
90
135
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
## Project structure
|
|
136
|
+
## Other
|
|
94
137
|
|
|
95
|
-
|
|
96
|
-
- `src/cli.ts` — CLI entry (Citty); registers subcommands, defaults to `commit`.
|
|
97
|
-
- `src/commands/` — one file per command.
|
|
98
|
-
- `src/lib/` — pure logic (`config`, `commit-message`, `branches`, unit-tested) and side effects (`git`, `ollama`); shared helpers (`errors`).
|
|
99
|
-
- `src/prompts/` — LLM prompts as `.md` files, imported as text (esbuild `.md` text loader) and inlined into the bundle at build time.
|
|
100
|
-
- `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: {
|
|
@@ -2382,15 +2424,26 @@ var commitCommand = defineCommand({
|
|
|
2382
2424
|
alias: "t",
|
|
2383
2425
|
description: `Force the Conventional Commits type (${COMMIT_TYPES.join(", ")}).`
|
|
2384
2426
|
},
|
|
2385
|
-
pull: {
|
|
2386
|
-
type: "boolean",
|
|
2387
|
-
description: "Rebase onto upstream (git pull --rebase) after committing, before pushing."
|
|
2388
|
-
},
|
|
2389
2427
|
push: {
|
|
2390
2428
|
type: "boolean",
|
|
2391
2429
|
alias: "p",
|
|
2392
2430
|
description: "Push after committing without asking."
|
|
2393
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
|
+
},
|
|
2394
2447
|
yes: {
|
|
2395
2448
|
type: "boolean",
|
|
2396
2449
|
alias: "y",
|
|
@@ -2403,6 +2456,12 @@ var commitCommand = defineCommand({
|
|
|
2403
2456
|
process.env,
|
|
2404
2457
|
args.model ? { model: args.model } : {}
|
|
2405
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;
|
|
2406
2465
|
const type = args.type ? normalizeCommitType(args.type) : void 0;
|
|
2407
2466
|
if (type === null) {
|
|
2408
2467
|
log.error(
|
|
@@ -2412,13 +2471,32 @@ var commitCommand = defineCommand({
|
|
|
2412
2471
|
return;
|
|
2413
2472
|
}
|
|
2414
2473
|
if (interactive) intro("zapdev commit");
|
|
2415
|
-
await stageAll();
|
|
2474
|
+
if (!args.staged) await stageAll();
|
|
2416
2475
|
const diff = await getStagedDiff();
|
|
2417
2476
|
if (!diff.trim()) {
|
|
2418
2477
|
log.warn("Nothing to commit.");
|
|
2419
2478
|
if (interactive) outro("Nothing to do.");
|
|
2420
2479
|
return;
|
|
2421
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
|
+
}
|
|
2422
2500
|
const loader = interactive ? spinner() : void 0;
|
|
2423
2501
|
loader?.start("Generating commit message");
|
|
2424
2502
|
let message;
|
|
@@ -2471,10 +2549,6 @@ var commitCommand = defineCommand({
|
|
|
2471
2549
|
}
|
|
2472
2550
|
await commit(finalMessage);
|
|
2473
2551
|
if (interactive) log.success(`Committed: ${finalMessage}`);
|
|
2474
|
-
if (args.pull && !await rebaseOnUpstream(interactive)) {
|
|
2475
|
-
process.exitCode = 1;
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
2552
|
let shouldPush = Boolean(args.push);
|
|
2479
2553
|
if (!shouldPush && interactive && !args.yes) {
|
|
2480
2554
|
const answer = await confirm({ message: "Push?", initialValue: false });
|
|
@@ -2485,7 +2559,11 @@ var commitCommand = defineCommand({
|
|
|
2485
2559
|
shouldPush = answer;
|
|
2486
2560
|
}
|
|
2487
2561
|
if (shouldPush) {
|
|
2488
|
-
const pushed = await pushOptimistic(
|
|
2562
|
+
const pushed = await pushOptimistic(
|
|
2563
|
+
interactive,
|
|
2564
|
+
interactive && !args.yes,
|
|
2565
|
+
syncStrategy
|
|
2566
|
+
);
|
|
2489
2567
|
if (!pushed) {
|
|
2490
2568
|
process.exitCode = 1;
|
|
2491
2569
|
return;
|
|
@@ -2494,39 +2572,28 @@ var commitCommand = defineCommand({
|
|
|
2494
2572
|
if (interactive) outro("Done.");
|
|
2495
2573
|
}
|
|
2496
2574
|
});
|
|
2497
|
-
async function
|
|
2498
|
-
if (!await hasUpstream()) {
|
|
2499
|
-
log.info("No upstream to pull from; skipping --pull.");
|
|
2500
|
-
return true;
|
|
2501
|
-
}
|
|
2575
|
+
async function syncWithUpstream(strategy, interactive) {
|
|
2502
2576
|
const loader = interactive ? spinner() : void 0;
|
|
2503
|
-
|
|
2577
|
+
const label = strategy === "rebase" ? "Rebase" : "Merge";
|
|
2578
|
+
loader?.start(`Pulling --${strategy === "rebase" ? "rebase" : "no-rebase"}`);
|
|
2504
2579
|
try {
|
|
2505
|
-
await pullRebase();
|
|
2506
|
-
loader?.stop("\u2713 Rebased on upstream");
|
|
2580
|
+
await (strategy === "rebase" ? pullRebase() : pullMerge());
|
|
2581
|
+
loader?.stop(strategy === "rebase" ? "\u2713 Rebased on upstream" : "\u2713 Merged upstream");
|
|
2507
2582
|
return true;
|
|
2508
2583
|
} catch (error) {
|
|
2509
|
-
loader?.error(
|
|
2510
|
-
log.error(
|
|
2584
|
+
loader?.error(`${label} failed`);
|
|
2585
|
+
log.error(`${label} failed (resolve conflicts, then push): ${errorMessage(error)}`);
|
|
2511
2586
|
return false;
|
|
2512
2587
|
}
|
|
2513
2588
|
}
|
|
2514
|
-
async function pushOptimistic(interactive,
|
|
2589
|
+
async function pushOptimistic(interactive, canPrompt, strategy) {
|
|
2515
2590
|
const [upstream, branch] = await Promise.all([hasUpstream(), currentBranch()]);
|
|
2516
2591
|
const doPush = () => upstream ? push() : pushSetUpstream(branch);
|
|
2517
2592
|
const first = await tryPush(interactive, doPush);
|
|
2518
2593
|
if (first.ok) return true;
|
|
2519
2594
|
if (upstream && await isBehind(interactive)) {
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
return false;
|
|
2523
|
-
}
|
|
2524
|
-
const answer = await confirm({ message: "Pull --rebase and retry push?", initialValue: true });
|
|
2525
|
-
if (isCancel(answer) || !answer) {
|
|
2526
|
-
log.info("Not pushed (still behind).");
|
|
2527
|
-
return true;
|
|
2528
|
-
}
|
|
2529
|
-
if (!await rebaseOnUpstream(interactive)) return false;
|
|
2595
|
+
const syncStrategy = strategy ?? await chooseSyncStrategy(canPrompt);
|
|
2596
|
+
if (!syncStrategy || !await syncWithUpstream(syncStrategy, interactive)) return false;
|
|
2530
2597
|
const retry = await tryPush(interactive, doPush);
|
|
2531
2598
|
if (retry.ok) return true;
|
|
2532
2599
|
log.error(`Push failed: ${errorMessage(retry.error)}`);
|
|
@@ -2535,6 +2602,25 @@ async function pushOptimistic(interactive, assumeYes) {
|
|
|
2535
2602
|
log.error(`Push failed: ${errorMessage(first.error)}`);
|
|
2536
2603
|
return false;
|
|
2537
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
|
+
}
|
|
2538
2624
|
async function tryPush(interactive, doPush) {
|
|
2539
2625
|
const loader = interactive ? spinner() : void 0;
|
|
2540
2626
|
loader?.start("Pushing");
|