git-watchtower 1.4.0 → 1.5.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.
@@ -1190,6 +1190,11 @@ function render() {
1190
1190
  if (state.errorToast) {
1191
1191
  renderer.renderErrorToast(state, write);
1192
1192
  }
1193
+
1194
+ // Stash confirmation dialog renders on top of everything
1195
+ if (state.stashConfirmMode) {
1196
+ renderer.renderStashConfirm(state, write);
1197
+ }
1193
1198
  }
1194
1199
 
1195
1200
  function showFlash(message) {
@@ -1224,7 +1229,6 @@ function showErrorToast(title, message, hint = null, duration = 8000) {
1224
1229
 
1225
1230
  errorToastTimeout = setTimeout(() => {
1226
1231
  store.setState({ errorToast: null });
1227
- pendingDirtyOperation = null;
1228
1232
  render();
1229
1233
  }, duration);
1230
1234
  }
@@ -1240,6 +1244,26 @@ function hideErrorToast() {
1240
1244
  }
1241
1245
  }
1242
1246
 
1247
+ function showStashConfirm(operationLabel) {
1248
+ store.setState({
1249
+ stashConfirmMode: true,
1250
+ stashConfirmSelectedIndex: 0,
1251
+ pendingDirtyOperationLabel: operationLabel,
1252
+ });
1253
+ render();
1254
+ }
1255
+
1256
+ function hideStashConfirm() {
1257
+ if (store.get('stashConfirmMode')) {
1258
+ store.setState({
1259
+ stashConfirmMode: false,
1260
+ stashConfirmSelectedIndex: 0,
1261
+ pendingDirtyOperationLabel: null,
1262
+ });
1263
+ render();
1264
+ }
1265
+ }
1266
+
1243
1267
  // ============================================================================
1244
1268
  // Git Functions
1245
1269
  // ============================================================================
@@ -1365,13 +1389,8 @@ async function switchToBranch(branchName, recordHistory = true) {
1365
1389
  const isDirty = await hasUncommittedChanges();
1366
1390
  if (isDirty) {
1367
1391
  addLog(`Cannot switch: uncommitted changes in working directory`, 'error');
1368
- addLog(`Press S to stash changes, or commit manually`, 'warning');
1369
1392
  pendingDirtyOperation = { type: 'switch', branch: branchName };
1370
- showErrorToast(
1371
- 'Cannot Switch Branch',
1372
- 'You have uncommitted changes in your working directory that would be lost.',
1373
- 'Press S to stash changes'
1374
- );
1393
+ showStashConfirm(`switch to ${branchName}`);
1375
1394
  return { success: false, reason: 'dirty' };
1376
1395
  }
1377
1396
 
@@ -1404,6 +1423,7 @@ async function switchToBranch(branchName, recordHistory = true) {
1404
1423
  }
1405
1424
 
1406
1425
  addLog(`Switched to ${safeBranchName}`, 'success');
1426
+ pendingDirtyOperation = null;
1407
1427
 
1408
1428
  // Restart server if configured (command mode)
1409
1429
  if (SERVER_MODE === 'command' && RESTART_ON_SWITCH && serverProcess) {
@@ -1423,13 +1443,8 @@ async function switchToBranch(branchName, recordHistory = true) {
1423
1443
  );
1424
1444
  } else if (errMsg.includes('local changes') || errMsg.includes('overwritten')) {
1425
1445
  addLog(`Cannot switch: local changes would be overwritten`, 'error');
1426
- addLog(`Press S to stash changes, or commit manually`, 'warning');
1427
1446
  pendingDirtyOperation = { type: 'switch', branch: branchName };
1428
- showErrorToast(
1429
- 'Cannot Switch Branch',
1430
- 'Your local changes would be overwritten by checkout.',
1431
- 'Press S to stash changes'
1432
- );
1447
+ showStashConfirm(`switch to ${branchName}`);
1433
1448
  } else {
1434
1449
  addLog(`Failed to switch: ${errMsg}`, 'error');
1435
1450
  showErrorToast(
@@ -1481,6 +1496,7 @@ async function pullCurrentBranch() {
1481
1496
 
1482
1497
  await execAsync(`git pull "${REMOTE_NAME}" "${branch}"`);
1483
1498
  addLog('Pulled successfully', 'success');
1499
+ pendingDirtyOperation = null;
1484
1500
  notifyClients();
1485
1501
  return { success: true };
1486
1502
  } catch (e) {
@@ -1488,13 +1504,8 @@ async function pullCurrentBranch() {
1488
1504
  addLog(`Pull failed: ${errMsg}`, 'error');
1489
1505
 
1490
1506
  if (errMsg.includes('local changes') || errMsg.includes('overwritten') || errMsg.includes('uncommitted changes')) {
1491
- addLog(`Press S to stash changes, or commit manually`, 'warning');
1492
1507
  pendingDirtyOperation = { type: 'pull' };
1493
- showErrorToast(
1494
- 'Pull Failed',
1495
- 'Your local changes would be overwritten by pull.',
1496
- 'Press S to stash changes'
1497
- );
1508
+ showStashConfirm('pull');
1498
1509
  } else if (isMergeConflict(errMsg)) {
1499
1510
  store.setState({ hasMergeConflict: true });
1500
1511
  showErrorToast(
@@ -1535,6 +1546,7 @@ async function stashAndRetry() {
1535
1546
 
1536
1547
  pendingDirtyOperation = null;
1537
1548
  hideErrorToast();
1549
+ hideStashConfirm();
1538
1550
 
1539
1551
  addLog('Stashing uncommitted changes...', 'update');
1540
1552
  render();
@@ -1556,9 +1568,13 @@ async function stashAndRetry() {
1556
1568
  const popResult = await gitStashPop();
1557
1569
  if (popResult.success) {
1558
1570
  addLog('Stashed changes restored', 'info');
1571
+ showFlash('Stashed changes restored (switch failed)');
1559
1572
  } else {
1560
1573
  addLog('Warning: could not restore stashed changes. Run: git stash pop', 'error');
1574
+ showErrorToast('Stash Pop Failed', 'Could not restore stashed changes.', 'Run: git stash pop');
1561
1575
  }
1576
+ } else {
1577
+ showFlash(`Stashed & switched to ${operation.branch}`);
1562
1578
  }
1563
1579
  await pollGitChanges();
1564
1580
  } else if (operation.type === 'pull') {
@@ -1568,9 +1584,13 @@ async function stashAndRetry() {
1568
1584
  const popResult = await gitStashPop();
1569
1585
  if (popResult.success) {
1570
1586
  addLog('Stashed changes restored', 'info');
1587
+ showFlash('Stashed changes restored (pull failed)');
1571
1588
  } else {
1572
1589
  addLog('Warning: could not restore stashed changes. Run: git stash pop', 'error');
1590
+ showErrorToast('Stash Pop Failed', 'Could not restore stashed changes.', 'Run: git stash pop');
1573
1591
  }
1592
+ } else {
1593
+ showFlash('Stashed & pulled successfully');
1574
1594
  }
1575
1595
  await pollGitChanges();
1576
1596
  }
@@ -2309,6 +2329,52 @@ function setupKeyboardInput() {
2309
2329
  return; // Ignore other keys in action mode
2310
2330
  }
2311
2331
 
2332
+ // Handle stash confirmation dialog
2333
+ if (store.get('stashConfirmMode')) {
2334
+ if (key === '\u001b[A' || key === 'k') { // Up
2335
+ const idx = store.get('stashConfirmSelectedIndex');
2336
+ if (idx > 0) {
2337
+ store.setState({ stashConfirmSelectedIndex: idx - 1 });
2338
+ render();
2339
+ }
2340
+ return;
2341
+ }
2342
+ if (key === '\u001b[B' || key === 'j') { // Down
2343
+ const idx = store.get('stashConfirmSelectedIndex');
2344
+ if (idx < 1) {
2345
+ store.setState({ stashConfirmSelectedIndex: idx + 1 });
2346
+ render();
2347
+ }
2348
+ return;
2349
+ }
2350
+ if (key === '\r' || key === '\n') { // Enter — execute selected option
2351
+ const idx = store.get('stashConfirmSelectedIndex');
2352
+ hideStashConfirm();
2353
+ if (idx === 0 && pendingDirtyOperation) {
2354
+ await stashAndRetry();
2355
+ } else {
2356
+ addLog('Stash cancelled — handle changes manually', 'info');
2357
+ pendingDirtyOperation = null;
2358
+ }
2359
+ return;
2360
+ }
2361
+ if (key === 'S') { // S shortcut — stash directly
2362
+ hideStashConfirm();
2363
+ if (pendingDirtyOperation) {
2364
+ await stashAndRetry();
2365
+ }
2366
+ return;
2367
+ }
2368
+ if (key === '\u001b') { // Escape — cancel
2369
+ hideStashConfirm();
2370
+ addLog('Stash cancelled — handle changes manually', 'info');
2371
+ pendingDirtyOperation = null;
2372
+ render();
2373
+ return;
2374
+ }
2375
+ return; // Ignore other keys in stash confirm mode
2376
+ }
2377
+
2312
2378
  // Dismiss flash on any key
2313
2379
  if (store.get('flashMessage')) {
2314
2380
  hideFlash();
@@ -2324,7 +2390,6 @@ function setupKeyboardInput() {
2324
2390
  return;
2325
2391
  }
2326
2392
  hideErrorToast();
2327
- pendingDirtyOperation = null;
2328
2393
  if (key !== '\u001b[A' && key !== '\u001b[B' && key !== '\r' && key !== 'q') {
2329
2394
  return;
2330
2395
  }
@@ -2475,9 +2540,14 @@ function setupKeyboardInput() {
2475
2540
  break;
2476
2541
  }
2477
2542
 
2478
- case 'S': // Stash changes (only active with pending dirty operation)
2543
+ case 'S': // Stash changes open confirm dialog or show hint
2479
2544
  if (pendingDirtyOperation) {
2480
- await stashAndRetry();
2545
+ const label = pendingDirtyOperation.type === 'switch'
2546
+ ? `switch to ${pendingDirtyOperation.branch}`
2547
+ : 'pull';
2548
+ showStashConfirm(label);
2549
+ } else {
2550
+ showFlash('No pending operation — stash with S after a failed switch or pull');
2481
2551
  }
2482
2552
  break;
2483
2553
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {