git-watchtower 2.3.0 → 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');
@@ -3407,6 +3485,13 @@ async function startWebDashboard(openBrowser) {
3407
3485
  sessionStats: sessionStats.getStats(),
3408
3486
  }),
3409
3487
  onAction: handleWebAction,
3488
+ // Route actions for non-local project tabs through the coordinator so
3489
+ // the targeted worker handles them in its own process. Without this,
3490
+ // every action runs against the coordinator's repo regardless of which
3491
+ // tab the user clicked.
3492
+ sendCommand: (pId, action, payload) => {
3493
+ if (coordinator) coordinator.sendCommand(pId, action, payload);
3494
+ },
3410
3495
  });
3411
3496
  webDashboard.setLocalProjectId(projectId);
3412
3497
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "2.3.0",
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,11 +36,29 @@ 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
42
57
  * @property {import('../state/store').Store} store - State store instance
43
- * @property {function} [onAction] - Callback for web UI actions
58
+ * @property {function} [onAction] - Callback for actions targeting the local project
59
+ * @property {function} [sendCommand] - Routes (projectId, action, payload) to a remote
60
+ * project's worker. When omitted, every action is dispatched locally — preserves
61
+ * single-project behaviour for callers (and tests) that don't run a coordinator.
44
62
  * @property {function} [getExtraState] - Returns additional state to merge
45
63
  */
46
64
 
@@ -56,6 +74,7 @@ class WebDashboardServer {
56
74
  this.port = options.port || DEFAULT_WEB_PORT;
57
75
  this.store = options.store;
58
76
  this.onAction = options.onAction || (() => {});
77
+ this.sendCommand = options.sendCommand || null;
59
78
  this.getExtraState = options.getExtraState || (() => ({}));
60
79
 
61
80
  /** @type {Set<import('http').ServerResponse>} */
@@ -511,27 +530,29 @@ class WebDashboardServer {
511
530
  return;
512
531
  }
513
532
 
514
- // Whitelist allowed actions
515
- const allowedActions = [
516
- 'switchBranch', 'pull', 'fetch', 'undo',
517
- 'toggleSound', 'preview',
518
- 'restartServer', 'reloadBrowsers', 'toggleCasino',
519
- 'openBrowser',
520
- 'stash', 'stashPop', 'deleteBranches', 'checkUpdate',
521
- ];
522
-
523
- if (!allowedActions.includes(action)) {
533
+ if (!ALLOWED_ACTIONS.includes(action)) {
524
534
  res.writeHead(400, { 'Content-Type': 'application/json' });
525
535
  res.end(JSON.stringify({ error: 'Unknown action: ' + action }));
526
536
  return;
527
537
  }
528
538
 
529
- // Include projectId if provided (for multi-project routing)
539
+ // Multi-project routing: when the request targets a project that is
540
+ // not the coordinator's own (the user is on a different tab in the
541
+ // dashboard), forward to that project's worker via sendCommand
542
+ // instead of running the action against the coordinator's repo.
530
543
  const projectId = data.projectId || this.localProjectId;
531
544
  payload._projectId = projectId;
532
545
 
533
- // Dispatch to the main process
534
- this.onAction(action, payload);
546
+ if (
547
+ projectId &&
548
+ this.localProjectId &&
549
+ projectId !== this.localProjectId &&
550
+ typeof this.sendCommand === 'function'
551
+ ) {
552
+ this.sendCommand(projectId, action, payload);
553
+ } else {
554
+ this.onAction(action, payload);
555
+ }
535
556
 
536
557
  res.writeHead(200, { 'Content-Type': 'application/json' });
537
558
  res.end(JSON.stringify({ ok: true }));
@@ -581,4 +602,5 @@ module.exports = {
581
602
  WebDashboardServer,
582
603
  DEFAULT_WEB_PORT,
583
604
  STATE_PUSH_INTERVAL,
605
+ ALLOWED_ACTIONS,
584
606
  };