git-watchtower 2.3.1 → 2.3.2

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.
@@ -3340,6 +3340,84 @@ async function handleWebAction(action, payload) {
3340
3340
  });
3341
3341
  }
3342
3342
  break;
3343
+ case 'stash': {
3344
+ // If a switch/pull is queued behind a dirty working tree, run the
3345
+ // existing stash-and-retry flow so the original operation completes
3346
+ // after the stash. Otherwise just stash standalone.
3347
+ if (pendingDirtyOperation) {
3348
+ const op = pendingDirtyOperation;
3349
+ await stashAndRetry();
3350
+ const label = op.type === 'switch' ? `stashed and switched to ${op.branch}` : 'stashed and pulled';
3351
+ sendResult(true, label);
3352
+ } else {
3353
+ const stashResult = await gitStash({ message: 'git-watchtower: stashed from web dashboard' });
3354
+ if (stashResult.success) {
3355
+ addLog('Changes stashed (from web)', 'success');
3356
+ telemetry.capture('stash_performed');
3357
+ await pollGitChanges();
3358
+ sendResult(true, 'Changes stashed');
3359
+ } else {
3360
+ const msg = stashResult.error ? stashResult.error.message : 'Could not stash';
3361
+ addLog(`Stash failed (from web): ${msg}`, 'error');
3362
+ sendResult(false, msg);
3363
+ }
3364
+ render();
3365
+ }
3366
+ break;
3367
+ }
3368
+ case 'stashPop': {
3369
+ const popResult = await gitStashPop();
3370
+ if (popResult.success) {
3371
+ addLog('Stash popped (from web)', 'success');
3372
+ await pollGitChanges();
3373
+ sendResult(true, 'Stash popped');
3374
+ } else {
3375
+ const msg = popResult.error ? popResult.error.message : 'Could not pop stash';
3376
+ addLog(`Stash pop failed (from web): ${msg}`, 'error');
3377
+ sendResult(false, msg);
3378
+ }
3379
+ render();
3380
+ break;
3381
+ }
3382
+ case 'deleteBranches': {
3383
+ const branches = Array.isArray(payload.branches) ? payload.branches : [];
3384
+ const force = payload.force === true;
3385
+ if (branches.length === 0) {
3386
+ sendResult(false, 'No branches specified');
3387
+ break;
3388
+ }
3389
+ addLog(
3390
+ `Cleaning up ${branches.length} branch${branches.length === 1 ? '' : 'es'}${force ? ' (force)' : ''} (from web)...`,
3391
+ 'update'
3392
+ );
3393
+ render();
3394
+ const cleanupResult = await deleteGoneBranches(branches, { force });
3395
+ for (const name of cleanupResult.deleted) {
3396
+ addLog(`Deleted branch: ${name}`, 'success');
3397
+ }
3398
+ for (const f of cleanupResult.failed) {
3399
+ addLog(`Failed to delete ${f.name}: ${f.error}`, 'error');
3400
+ }
3401
+ if (cleanupResult.deleted.length > 0) {
3402
+ telemetry.capture('cleanup_branches_deleted', { count: cleanupResult.deleted.length });
3403
+ await pollGitChanges();
3404
+ }
3405
+ if (cleanupResult.failed.length === 0 && cleanupResult.deleted.length > 0) {
3406
+ sendResult(true, `Deleted ${cleanupResult.deleted.length} branch${cleanupResult.deleted.length === 1 ? '' : 'es'}`);
3407
+ } else if (cleanupResult.deleted.length > 0) {
3408
+ sendResult(
3409
+ false,
3410
+ `Deleted ${cleanupResult.deleted.length}, failed ${cleanupResult.failed.length}`
3411
+ );
3412
+ } else {
3413
+ sendResult(
3414
+ false,
3415
+ `Failed to delete ${cleanupResult.failed.length} branch${cleanupResult.failed.length === 1 ? '' : 'es'}`
3416
+ );
3417
+ }
3418
+ render();
3419
+ break;
3420
+ }
3343
3421
  }
3344
3422
  } catch (err) {
3345
3423
  addLog(`Web action error: ${err.message}`, 'error');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
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": {
package/src/server/web.js CHANGED
@@ -36,6 +36,21 @@ const MAX_PORT_RETRIES = 20;
36
36
  */
37
37
  const SSE_KEEPALIVE_INTERVAL = 15000;
38
38
 
39
+ /**
40
+ * Actions the web dashboard is allowed to POST to /api/action. Every entry
41
+ * here MUST be matched by a `case` in `handleWebAction` in bin/git-watchtower.js
42
+ * — `tests/unit/server/web.test.js` enforces that link so a future addition
43
+ * to the whitelist can't silently no-op the way `stash` / `stashPop` /
44
+ * `deleteBranches` did before they were implemented.
45
+ */
46
+ const ALLOWED_ACTIONS = Object.freeze([
47
+ 'switchBranch', 'pull', 'fetch', 'undo',
48
+ 'toggleSound', 'preview',
49
+ 'restartServer', 'reloadBrowsers', 'toggleCasino',
50
+ 'openBrowser',
51
+ 'stash', 'stashPop', 'deleteBranches', 'checkUpdate',
52
+ ]);
53
+
39
54
  /**
40
55
  * @typedef {Object} WebDashboardOptions
41
56
  * @property {number} [port=4000] - Port to listen on
@@ -515,16 +530,7 @@ class WebDashboardServer {
515
530
  return;
516
531
  }
517
532
 
518
- // Whitelist allowed actions
519
- const allowedActions = [
520
- 'switchBranch', 'pull', 'fetch', 'undo',
521
- 'toggleSound', 'preview',
522
- 'restartServer', 'reloadBrowsers', 'toggleCasino',
523
- 'openBrowser',
524
- 'stash', 'stashPop', 'deleteBranches', 'checkUpdate',
525
- ];
526
-
527
- if (!allowedActions.includes(action)) {
533
+ if (!ALLOWED_ACTIONS.includes(action)) {
528
534
  res.writeHead(400, { 'Content-Type': 'application/json' });
529
535
  res.end(JSON.stringify({ error: 'Unknown action: ' + action }));
530
536
  return;
@@ -596,4 +602,5 @@ module.exports = {
596
602
  WebDashboardServer,
597
603
  DEFAULT_WEB_PORT,
598
604
  STATE_PUSH_INTERVAL,
605
+ ALLOWED_ACTIONS,
599
606
  };