s9n-devops-agent 2.0.18-dev.1 → 2.0.18-dev.12

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.
@@ -165,6 +165,10 @@ const FORCE_ROLLOVER = (process.env.AC_FORCE_ROLLOVER || "false").toLowerCase(
165
165
  const VERSION_PREFIX = process.env.AC_VERSION_PREFIX || "v0.";
166
166
  const VERSION_START_MINOR = Number(process.env.AC_VERSION_START_MINOR || "20"); // Start at v0.20 for micro-revisions
167
167
  const VERSION_BASE_REF = process.env.AC_VERSION_BASE_REF || "origin/main"; // where new version branches start
168
+
169
+ // Rebase configuration
170
+ const REBASE_INTERVAL_HOURS = Number(process.env.AC_REBASE_INTERVAL || 0);
171
+ const BASE_BRANCH = process.env.AC_BASE_BRANCH || 'HEAD';
168
172
  // ------------------------------------------------
169
173
 
170
174
  const log = (...a) => console.log("[cs-devops-agent]", ...a);
@@ -287,7 +291,16 @@ async function stagedCount() {
287
291
  return r.ok ? r.stdout.split("\n").filter(Boolean).length : 0;
288
292
  }
289
293
  async function unstageIfStaged(file) {
290
- await run("git", ["restore", "--staged", file]);
294
+ try {
295
+ // Check if file is actually staged first to avoid errors
296
+ // Use diff --cached to check without erroring on untracked files
297
+ const { ok, stdout } = await run("git", ["diff", "--name-only", "--cached", file]);
298
+ if (ok && stdout.trim().length > 0) {
299
+ await run("git", ["restore", "--staged", file]);
300
+ }
301
+ } catch (e) {
302
+ // Ignore errors
303
+ }
291
304
  }
292
305
  async function defaultRemote() {
293
306
  const r = await run("git", ["remote"]);
@@ -824,17 +837,20 @@ const REQUIRE_AI_COMMIT = (process.env.AC_AI_COMMIT || "false").toLowerCase() ==
824
837
 
825
838
  if (PUSH) {
826
839
  const ok = await pushBranch(BRANCH);
827
- log(`push ${ok ? "ok" : "failed"}`);
840
+ // Ensure this log goes to the right panel
841
+ log(`[push] pushing branch ${BRANCH} result: ${ok ? "ok" : "failed"}`);
842
+
828
843
  if (ok) {
829
844
  if (CLEAR_MSG_WHEN === "push") clearMsgFile(msgPath);
830
845
 
831
- console.log(`\x1b[32m✓ PUSHED:\x1b[0m ${BRANCH} -> remote`);
832
- console.log(""); // Empty line for spacing
846
+ // This log was going to stdout directly, bypassing TUI buffers
847
+ // Redirect to TUI log
848
+ log(`✓ PUSHED: ${BRANCH} -> remote`);
833
849
 
834
850
  // Handle Docker restart if configured
835
851
  await handleDockerRestart();
836
852
  } else {
837
- console.log(`\x1b[31m✗ PUSH FAILED:\x1b[0m Check logs for details`);
853
+ console.error(`✗ PUSH FAILED: Check logs for details`);
838
854
  }
839
855
  }
840
856
  } finally {
@@ -953,6 +969,119 @@ function detectInfrastructureChanges(changedFiles) {
953
969
  return detected;
954
970
  }
955
971
 
972
+ // ============================================================================
973
+ // AUTO-REBASE FUNCTIONALITY
974
+ // ============================================================================
975
+
976
+ /**
977
+ * Check if it's time to rebase and perform the operation
978
+ * @param {string} repoRoot - Repository root
979
+ * @returns {Promise<boolean>} - True if rebase occurred
980
+ */
981
+ async function checkAndPerformRebase(repoRoot) {
982
+ if (REBASE_INTERVAL_HOURS <= 0) return false;
983
+ if (BASE_BRANCH === 'HEAD') return false; // Can't rebase from HEAD (relative)
984
+
985
+ // Calculate interval in ms
986
+ const intervalMs = REBASE_INTERVAL_HOURS * 60 * 60 * 1000;
987
+ const now = Date.now();
988
+
989
+ // Check if enough time has passed
990
+ if (!global.lastRebaseTime) global.lastRebaseTime = now; // Initialize on first run
991
+ if (now - global.lastRebaseTime < intervalMs) return false;
992
+
993
+ // Don't rebase if we are busy
994
+ if (busy) return false;
995
+
996
+ try {
997
+ busy = true;
998
+
999
+ console.log('\n' + '━'.repeat(60));
1000
+ console.log(`\x1b[33m⚠️ AUTO-REBASE IN PROGRESS - PAUSING AGENT...\x1b[0m`);
1001
+ console.log(`Interval: ${REBASE_INTERVAL_HOURS} hours reached.`);
1002
+ console.log(`Base: ${BASE_BRANCH}`);
1003
+ console.log('━'.repeat(60) + '\n');
1004
+
1005
+ // Check for uncommitted changes
1006
+ const dirty = await hasUncommittedChanges();
1007
+ let stashed = false;
1008
+
1009
+ if (dirty) {
1010
+ log('Stashing uncommitted changes before rebase...');
1011
+ // Use -u to include untracked files, ensuring we capture everything status --porcelain sees
1012
+ const stashRes = await run('git', ['stash', 'push', '-u', '-m', `Auto-stash before rebase ${new Date().toISOString()}`]);
1013
+
1014
+ // Only mark as stashed if we actually saved something
1015
+ // git stash returns 0 even if nothing to save, so we must check stdout
1016
+ if (stashRes.ok && stashRes.stdout && stashRes.stdout.includes('Saved working directory')) {
1017
+ stashed = true;
1018
+ log('Changes stashed successfully.');
1019
+ } else if (stashRes.ok && stashRes.stdout && stashRes.stdout.includes('No local changes to save')) {
1020
+ stashed = false;
1021
+ log('No changes needed stashing (git reported no local changes).');
1022
+ } else {
1023
+ console.error('\\x1b[31m✗ Failed to stash changes. Aborting rebase.\\x1b[0m');
1024
+ if (stashRes.stdout) console.error(stashRes.stdout);
1025
+ busy = false;
1026
+ return false;
1027
+ }
1028
+ }
1029
+
1030
+ // Fetch latest
1031
+ log(`Fetching latest changes from origin...`);
1032
+ await run('git', ['fetch', 'origin', BASE_BRANCH]);
1033
+
1034
+ // Rebase
1035
+ log(`Rebasing onto origin/${BASE_BRANCH}...`);
1036
+ const rebaseRes = await run('git', ['pull', '--rebase', 'origin', BASE_BRANCH]);
1037
+
1038
+ if (rebaseRes.ok) {
1039
+ console.log(`\x1b[32m✓ Rebase successful!\x1b[0m`);
1040
+
1041
+ // Pop stash if needed
1042
+ if (stashed) {
1043
+ log('Restoring stashed changes...');
1044
+ const popRes = await run('git', ['stash', 'pop']);
1045
+ if (!popRes.ok) {
1046
+ console.error('\x1b[31m⚠️ Conflict during stash pop. Manual intervention required.\x1b[0m');
1047
+ console.log('\x1b[33mPlease resolve conflicts and continue.\x1b[0m');
1048
+ } else {
1049
+ console.log('\x1b[32m✓ Stash restored.\x1b[0m');
1050
+ }
1051
+ }
1052
+
1053
+ global.lastRebaseTime = Date.now();
1054
+ } else {
1055
+ console.error(`\x1b[31m✗ REBASE CONFLICT DETECTED\x1b[0m`);
1056
+ console.error(rebaseRes.stdout);
1057
+
1058
+ // Abort rebase
1059
+ log('Aborting rebase...');
1060
+ await run('git', ['rebase', '--abort']);
1061
+
1062
+ // Restore stash if we stashed
1063
+ if (stashed) {
1064
+ await run('git', ['stash', 'pop']);
1065
+ }
1066
+
1067
+ console.log('\x1b[33mRebase aborted. Please manually rebase your branch.\x1b[0m');
1068
+ // Disable auto-rebase for this session to avoid loop?
1069
+ // Or just wait for next interval.
1070
+ }
1071
+
1072
+ console.log(`\x1b[32m✓ RESUMING AGENT OPERATION\x1b[0m\n`);
1073
+ return true;
1074
+
1075
+ } catch (err) {
1076
+ console.error(`Rebase error: ${err.message}`);
1077
+ // Try to recover state
1078
+ try { await run('git', ['rebase', '--abort']); } catch (e) {}
1079
+ return false;
1080
+ } finally {
1081
+ busy = false;
1082
+ }
1083
+ }
1084
+
956
1085
  /**
957
1086
  * Update infrastructure documentation
958
1087
  * @param {object} infraChanges - Infrastructure change details
@@ -1418,8 +1547,8 @@ function saveProjectSettings(settings, settingsPath) {
1418
1547
  // Display copyright and license information immediately
1419
1548
  console.log("\n" + "=".repeat(70));
1420
1549
  console.log(" CS_DevOpsAgent - Intelligent Git Automation System");
1421
- console.log(" Version 1.4.8 | Build 20251008.1");
1422
- console.log(" \n Copyright (c) 2024 SecondBrain Labs");
1550
+ console.log(" Version 2.0.18-dev.12 | Build 20260107");
1551
+ console.log(" Copyright (c) 2026 SeKondBrain AI Labs Limited");
1423
1552
  console.log(" Author: Sachin Dev Duggal");
1424
1553
  console.log(" \n Licensed under the MIT License");
1425
1554
  console.log(" This software is provided 'as-is' without any warranty.");
@@ -1500,6 +1629,19 @@ console.log();
1500
1629
  log("watching…");
1501
1630
  }
1502
1631
 
1632
+ // Schedule auto-rebase if configured
1633
+ if (REBASE_INTERVAL_HOURS > 0) {
1634
+ log(`Auto-rebase scheduled every ${REBASE_INTERVAL_HOURS} hours (checking every 5m)`);
1635
+ // Initial check (in case we started overdue)
1636
+ // Don't await this so we don't block startup
1637
+ checkAndPerformRebase(repoRoot).catch(err => console.error(err));
1638
+
1639
+ // Periodic check
1640
+ setInterval(async () => {
1641
+ await checkAndPerformRebase(repoRoot);
1642
+ }, 5 * 60 * 1000); // Check every 5 minutes
1643
+ }
1644
+
1503
1645
  // ============================================================================
1504
1646
  // FILE WATCHER SETUP - Monitor for changes and trigger commits
1505
1647
  // ============================================================================
@@ -1625,25 +1767,30 @@ console.log();
1625
1767
  ],
1626
1768
  })
1627
1769
  .on("all", async (evt, p) => {
1770
+ // Define 'now' at the very top of the callback to avoid ReferenceError
1771
+ const now = Date.now();
1772
+
1628
1773
  // Check if file is in local_deploy or .file-coordination (internal agent files)
1629
1774
  if (p.includes('local_deploy/') || p.includes('.file-coordination/')) {
1630
1775
  return;
1631
1776
  }
1632
1777
 
1778
+ const isMsg = samePath(p, relMsg);
1779
+
1633
1780
  // Check if file is ignored by git (respect .gitignore)
1634
- try {
1635
- // git check-ignore returns 0 if ignored, 1 if not ignored
1636
- // We use quiet mode (-q) and ignore stdio
1637
- execSync(`git check-ignore -q "${p}"`, { stdio: 'ignore' });
1638
- // If we get here, exit code was 0, so file IS ignored
1639
- if (DEBUG) console.log(`[debug] Ignoring gitignored file: ${p}`);
1640
- return;
1641
- } catch (e) {
1642
- // Exit code 1 means NOT ignored (or other error). Proceed.
1781
+ // IMPORTANT: Do NOT ignore the message file, even if it's in .gitignore
1782
+ if (!isMsg) {
1783
+ try {
1784
+ // git check-ignore returns 0 if ignored, 1 if not ignored
1785
+ // We use quiet mode (-q) and ignore stdio
1786
+ execSync(`git check-ignore -q "${p}"`, { stdio: 'ignore' });
1787
+ // If we get here, exit code was 0, so file IS ignored
1788
+ if (DEBUG) console.log(`[debug] Ignoring gitignored file: ${p}`);
1789
+ return;
1790
+ } catch (e) {
1791
+ // Exit code 1 means NOT ignored (or other error). Proceed.
1792
+ }
1643
1793
  }
1644
-
1645
- const now = Date.now();
1646
- const isMsg = samePath(p, relMsg);
1647
1794
 
1648
1795
  // Track timing of non-message changes
1649
1796
  if (!isMsg) {
@@ -1731,12 +1878,161 @@ console.log();
1731
1878
  const rl = readline.createInterface({
1732
1879
  input: input,
1733
1880
  output: output,
1734
- prompt: '\\x1b[36m[agent] >\\x1b[0m ',
1881
+ prompt: '', // We'll manage the prompt manually
1735
1882
  terminal: true
1736
1883
  });
1737
-
1884
+
1885
+ // TUI STATE
1886
+ const TUI = {
1887
+ leftLogs: [],
1888
+ rightLogs: [],
1889
+ maxLogs: 200,
1890
+ status: 'ACTIVE',
1891
+ inputBuffer: '',
1892
+ sessionId: sessionId,
1893
+ branch: await currentBranch(),
1894
+ inputMode: 'command' // 'command' or 'prompt'
1895
+ };
1896
+
1897
+ // Helper to strip ANSI codes
1898
+ const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
1899
+
1900
+ // Override log output to split into two panels
1901
+ // Right Panel: [cs-devops-agent], [debug], [cmd], [err] (Background Activity)
1902
+ // Left Panel: Interactive output (status, help, etc)
1903
+ const originalConsoleLog = console.log;
1904
+ const originalConsoleError = console.error;
1905
+
1906
+ const addToBuffers = (msg, isError = false) => {
1907
+ const cleanMsg = stripAnsi(msg);
1908
+ const isBackground = cleanMsg.startsWith('[cs-devops-agent]') ||
1909
+ cleanMsg.startsWith('[debug]') ||
1910
+ cleanMsg.startsWith('[cmd]') ||
1911
+ cleanMsg.startsWith('[err]') ||
1912
+ cleanMsg.startsWith('ignore') ||
1913
+ cleanMsg.startsWith('watcher:');
1914
+
1915
+ const targetBuffer = isBackground ? TUI.rightLogs : TUI.leftLogs;
1916
+
1917
+ // Add timestamp for background logs only
1918
+ let line = msg;
1919
+ if (isBackground) {
1920
+ const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit' });
1921
+ line = `\x1b[2m[${time}]\x1b[0m ${msg}`;
1922
+ }
1923
+
1924
+ // Split newlines and push
1925
+ const lines = line.split('\n');
1926
+ lines.forEach(l => {
1927
+ targetBuffer.push(l);
1928
+ if (targetBuffer.length > TUI.maxLogs) targetBuffer.shift();
1929
+ });
1930
+
1931
+ renderTUI();
1932
+ };
1933
+
1934
+ console.log = (...args) => {
1935
+ const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
1936
+ // Filter TUI artifacts
1937
+ if (msg.includes('\x1b[2J') || msg.includes('\x1b[0f')) return;
1938
+ addToBuffers(msg, false);
1939
+ };
1940
+
1941
+ console.error = (...args) => {
1942
+ const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
1943
+ const colored = msg.startsWith('\x1b') ? msg : `\x1b[31m${msg}\x1b[0m`;
1944
+ addToBuffers(colored, true);
1945
+ };
1946
+
1947
+ // Vertical Split Renderer
1948
+ function renderTUI() {
1949
+ const width = process.stdout.columns || 80;
1950
+ const height = process.stdout.rows || 24;
1951
+ const contentHeight = height - 4; // Header, Divider, Prompt, Margin
1952
+
1953
+ // Split: Left (Interactive) 40%, Right (Logs) 60%
1954
+ // Ensure minimum widths
1955
+ const leftPercent = 0.4;
1956
+ const leftWidth = Math.floor(width * leftPercent) - 2;
1957
+ const rightWidth = width - leftWidth - 3; // -3 for separator " | "
1958
+
1959
+ if (leftWidth < 10 || rightWidth < 10) {
1960
+ // Fallback for very narrow screens? Just clear and print prompt?
1961
+ // Or simple stacked? Let's just proceed, it will look messy but functional.
1962
+ }
1963
+
1964
+ // Prepare views (Bottom-Anchored)
1965
+ const leftView = TUI.leftLogs.slice(-contentHeight);
1966
+ const rightView = TUI.rightLogs.slice(-contentHeight);
1967
+
1968
+ const leftEmpty = contentHeight - leftView.length;
1969
+ const rightEmpty = contentHeight - rightView.length;
1970
+
1971
+ // Draw Frame
1972
+ process.stdout.write('\x1b[2J'); // Clear
1973
+ process.stdout.write('\x1b[H'); // Home (standard ANSI)
1974
+
1975
+ // 1. Header
1976
+ const headerText = ` DEVOPS AGENT | Session: ${TUI.sessionId || 'N/A'} | Branch: ${TUI.branch} `;
1977
+ const headPad = Math.max(0, width - headerText.length);
1978
+ process.stdout.write(`\x1b[44m\x1b[37m${headerText}${' '.repeat(headPad)}\x1b[0m\n`);
1979
+
1980
+ // 2. Columns
1981
+ for (let i = 0; i < contentHeight; i++) {
1982
+ let lLine = (i >= leftEmpty) ? leftView[i - leftEmpty] : '';
1983
+ let rLine = (i >= rightEmpty) ? rightView[i - rightEmpty] : '';
1984
+
1985
+ // Removing ANSI for length calculation is essential for alignment
1986
+ const lPlain = stripAnsi(lLine);
1987
+ const rPlain = stripAnsi(rLine);
1988
+
1989
+ // Truncate to prevent wrapping and breaking layout
1990
+ let lDisp = lLine;
1991
+ let rDisp = rLine;
1992
+
1993
+ if (lPlain.length > leftWidth) {
1994
+ // Simple truncation (note: might cut ansi codes, but prevents layout break)
1995
+ // We use the plain length to determine cut point
1996
+ // For safety, we just display the plain truncated text to avoid hanging colors
1997
+ lDisp = lPlain.substring(0, leftWidth - 3) + '...';
1998
+ }
1999
+
2000
+ if (rPlain.length > rightWidth) {
2001
+ rDisp = rPlain.substring(0, rightWidth - 3) + '...';
2002
+ }
2003
+
2004
+ // Pad using the length of the *displayed* string (re-strip to be sure)
2005
+ const lDispLen = stripAnsi(lDisp).length;
2006
+ const rDispLen = stripAnsi(rDisp).length;
2007
+
2008
+ const lPad = Math.max(0, leftWidth - lDispLen);
2009
+ const rPad = Math.max(0, rightWidth - rDispLen);
2010
+
2011
+ process.stdout.write(lDisp + ' '.repeat(lPad));
2012
+ process.stdout.write('\x1b[90m │ \x1b[0m');
2013
+ process.stdout.write(rDisp + ' '.repeat(rPad));
2014
+ process.stdout.write('\n');
2015
+ }
2016
+
2017
+ // 3. Divider
2018
+ const lDiv = '─'.repeat(leftWidth);
2019
+ const rDiv = '─'.repeat(rightWidth);
2020
+ process.stdout.write(`\x1b[90m${lDiv}─┴─${rDiv}\x1b[0m\n`);
2021
+
2022
+ // 4. Prompt
2023
+ process.stdout.write(`\x1b[36m[agent] >\x1b[0m ${TUI.inputBuffer}`);
2024
+ }
2025
+
2026
+ // Initial render
2027
+ renderTUI();
2028
+
1738
2029
  // Command handler
1739
2030
  rl.on('line', async (line) => {
2031
+ // If in prompt mode, ignore this handler (let the prompt handler take it)
2032
+ if (TUI.inputMode !== 'command') return;
2033
+
2034
+ TUI.inputBuffer = '';
2035
+ addToBuffers(`> ${line}`); // Echo command to left panel
1740
2036
  const cmd = line.trim().toLowerCase();
1741
2037
 
1742
2038
  switch (cmd) {
@@ -1802,10 +2098,22 @@ console.log();
1802
2098
  let commitMsg = readMsgFile(msgPath);
1803
2099
  if (!commitMsg) {
1804
2100
  console.log("No commit message found. Enter message (or 'cancel' to abort):");
1805
- rl.prompt();
2101
+
2102
+ // Switch to prompt mode to prevent main loop interference
2103
+ TUI.inputMode = 'prompt';
2104
+
2105
+ // Use a temporary prompt interface
1806
2106
  const msgInput = await new Promise(resolve => {
1807
- rl.once('line', resolve);
2107
+ // We need to use a one-time listener that doesn't conflict
2108
+ const handler = (input) => {
2109
+ resolve(input);
2110
+ };
2111
+ rl.once('line', handler);
1808
2112
  });
2113
+
2114
+ // Switch back to command mode
2115
+ TUI.inputMode = 'command';
2116
+
1809
2117
  if (msgInput.trim().toLowerCase() === 'cancel') {
1810
2118
  console.log("Commit cancelled.");
1811
2119
  break;
@@ -1818,7 +2126,7 @@ console.log();
1818
2126
  console.log("No changes to commit.");
1819
2127
  }
1820
2128
  break;
1821
-
2129
+
1822
2130
  case 'push':
1823
2131
  case 'p':
1824
2132
  const branchName = await currentBranch();
@@ -1840,11 +2148,18 @@ console.log();
1840
2148
  const uncommitted = await hasUncommittedChanges();
1841
2149
  if (uncommitted) {
1842
2150
  console.log("You have uncommitted changes. Commit them before exit? (y/n)");
1843
- rl.prompt();
2151
+
2152
+ TUI.inputMode = 'prompt';
2153
+
1844
2154
  const answer = await new Promise(resolve => {
1845
- rl.once('line', resolve);
2155
+ const handler = (input) => {
2156
+ resolve(input);
2157
+ };
2158
+ rl.once('line', handler);
1846
2159
  });
1847
2160
 
2161
+ TUI.inputMode = 'command';
2162
+
1848
2163
  if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
1849
2164
  console.log("Committing changes...");
1850
2165
  const exitMsg = "chore: session cleanup - final commit before exit";
@@ -1941,74 +2256,128 @@ console.log();
1941
2256
  let mergeCompleted = false;
1942
2257
  if (shouldMerge) {
1943
2258
  try {
1944
- console.log(`\n\x1b[34mMerging ${currentBranchName} into ${targetBranch}...\x1b[0m`);
1945
-
1946
- // Check if target branch exists locally
1947
- let branchExists = false;
2259
+ // Check if repoRoot is clean before attempting merge operations
2260
+ // We use --porcelain to check for modifications
2261
+ let isDirty = false;
1948
2262
  try {
1949
- execSync(`git rev-parse --verify ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
1950
- branchExists = true;
1951
- } catch (err) {
1952
- // Branch doesn't exist locally
1953
- }
1954
-
1955
- if (!branchExists) {
1956
- // Check if branch exists on remote
1957
- try {
1958
- const remoteCheck = execSync(`git ls-remote --heads origin ${targetBranch}`, {
1959
- cwd: repoRoot,
1960
- encoding: 'utf8'
1961
- }).trim();
2263
+ const status = execSync(`git status --porcelain`, { cwd: repoRoot, encoding: 'utf8' });
2264
+ if (status && status.trim().length > 0) {
2265
+ isDirty = true;
2266
+ console.log(`\n\x1b[33m⚠️ The main repository has uncommitted changes:\x1b[0m`);
2267
+ const lines = status.trim().split('\n').slice(0, 5);
2268
+ lines.forEach(l => console.log(` ${l}`));
2269
+ if (status.trim().split('\n').length > 5) console.log(' ...');
2270
+
2271
+ console.log(`\nMerge cannot proceed with a dirty working tree.`);
2272
+ console.log(`You can stash these changes to proceed with the merge, or skip the merge.`);
2273
+ console.log(` y/yes - Stash changes and merge`);
2274
+ console.log(` n/no - Skip merge`);
1962
2275
 
1963
- if (remoteCheck) {
1964
- // Branch exists on remote, fetch it
1965
- console.log(`\x1b[2mTarget branch doesn't exist locally, fetching from remote...\x1b[0m`);
1966
- execSync(`git fetch origin ${targetBranch}:${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2276
+ rl.prompt();
2277
+ const stashAnswer = await new Promise(resolve => {
2278
+ rl.once('line', resolve);
2279
+ });
2280
+
2281
+ if (stashAnswer.toLowerCase() === 'y' || stashAnswer.toLowerCase() === 'yes') {
2282
+ console.log(`\nStashing changes in main repo...`);
2283
+ execSync(`git stash push -m "Auto-stash before session merge ${sessionId}"`, { cwd: repoRoot, stdio: 'pipe' });
2284
+ isDirty = false;
1967
2285
  } else {
1968
- // Branch doesn't exist on remote either, create it
1969
- console.log(`\x1b[33mTarget branch '${targetBranch}' doesn't exist. Creating it...\x1b[0m`);
1970
- execSync(`git checkout -b ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
1971
- execSync(`git push -u origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
1972
- console.log(`\x1b[32m✓\x1b[0m Created new branch ${targetBranch}`);
2286
+ console.log(`\nSkipping merge due to dirty working tree.`);
2287
+ shouldMerge = false;
1973
2288
  }
1974
- } catch (err) {
1975
- console.error(`\x1b[31m✗ Error checking/creating remote branch: ${err.message}\x1b[0m`);
1976
- throw err;
1977
- }
1978
- }
1979
-
1980
- // Switch to target branch
1981
- execSync(`git checkout ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
1982
-
1983
- // Pull latest (if branch already existed)
1984
- if (branchExists) {
1985
- try {
1986
- execSync(`git pull origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
1987
- } catch (err) {
1988
- console.log(`\x1b[2mCould not pull latest changes (may be new branch)\x1b[0m`);
1989
2289
  }
1990
- }
1991
-
1992
- // Merge the session branch
1993
- execSync(`git merge --no-ff ${currentBranchName} -m "Merge session ${sessionId}: session work"`, {
1994
- cwd: repoRoot,
1995
- stdio: 'pipe'
1996
- });
1997
-
1998
- // Push merged changes
1999
- execSync(`git push origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2000
-
2001
- console.log(`\x1b[32m✓\x1b[0m Successfully merged to ${targetBranch}`);
2002
-
2003
- // Delete remote branch after successful merge
2004
- try {
2005
- execSync(`git push origin --delete ${currentBranchName}`, { cwd: repoRoot, stdio: 'pipe' });
2006
- console.log(`\x1b[32m✓\x1b[0m Deleted remote branch ${currentBranchName}`);
2007
2290
  } catch (err) {
2008
- console.log(`\x1b[2mCould not delete remote branch\x1b[0m`);
2291
+ console.log(`\x1b[31mError checking git status: ${err.message}\x1b[0m`);
2292
+ shouldMerge = false; // Err on side of caution
2293
+ }
2294
+
2295
+ if (shouldMerge && !isDirty) {
2296
+ console.log(`\n\x1b[34mMerging ${currentBranchName} into ${targetBranch}...\x1b[0m`);
2297
+
2298
+ // Check if target branch exists locally
2299
+ let branchExists = false;
2300
+ try {
2301
+ execSync(`git rev-parse --verify ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2302
+ branchExists = true;
2303
+ } catch (err) {
2304
+ // Branch doesn't exist locally
2305
+ }
2306
+
2307
+ if (!branchExists) {
2308
+ // Check if branch exists on remote
2309
+ try {
2310
+ const remoteCheck = execSync(`git ls-remote --heads origin ${targetBranch}`, {
2311
+ cwd: repoRoot,
2312
+ encoding: 'utf8'
2313
+ }).trim();
2314
+
2315
+ if (remoteCheck) {
2316
+ // Branch exists on remote, fetch it
2317
+ console.log(`\x1b[2mTarget branch doesn't exist locally, fetching from remote...\x1b[0m`);
2318
+ execSync(`git fetch origin ${targetBranch}:${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2319
+ } else {
2320
+ // Branch doesn't exist on remote either, create it
2321
+ console.log(`\x1b[33mTarget branch '${targetBranch}' doesn't exist. Creating it...\x1b[0m`);
2322
+ execSync(`git checkout -b ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2323
+ execSync(`git push -u origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2324
+ console.log(`\x1b[32m✓\x1b[0m Created new branch ${targetBranch}`);
2325
+ }
2326
+ } catch (err) {
2327
+ console.error(`\x1b[31m✗ Error checking/creating remote branch: ${err.message}\x1b[0m`);
2328
+ throw err;
2329
+ }
2330
+ }
2331
+
2332
+ // Switch to target branch
2333
+ execSync(`git checkout ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2334
+
2335
+ // Pull latest (if branch already existed)
2336
+ if (branchExists) {
2337
+ try {
2338
+ execSync(`git pull origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2339
+ } catch (err) {
2340
+ console.log(`\x1b[2mCould not pull latest changes (may be new branch)\x1b[0m`);
2341
+ }
2342
+ }
2343
+
2344
+ // Rebase session branch onto target branch
2345
+ try {
2346
+ console.log(`\x1b[34mRebasing ${currentBranchName} onto ${targetBranch}...\x1b[0m`);
2347
+ execSync(`git checkout ${currentBranchName}`, { cwd: repoRoot, stdio: 'pipe' });
2348
+ execSync(`git rebase ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2349
+ console.log(`\x1b[32m✓\x1b[0m Rebase successful`);
2350
+
2351
+ // Switch back to target for merge
2352
+ execSync(`git checkout ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2353
+ } catch (err) {
2354
+ console.error(`\x1b[31m✗ Rebase failed: ${err.message}\x1b[0m`);
2355
+ console.log(`\x1b[33mAborting rebase and trying direct merge...\x1b[0m`);
2356
+ try { execSync(`git rebase --abort`, { cwd: repoRoot, stdio: 'pipe' }); } catch (e) {}
2357
+ execSync(`git checkout ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2358
+ }
2359
+
2360
+ // Merge the session branch
2361
+ execSync(`git merge --no-ff ${currentBranchName} -m "Merge session ${sessionId}: session work"`, {
2362
+ cwd: repoRoot,
2363
+ stdio: 'pipe'
2364
+ });
2365
+
2366
+ // Push merged changes
2367
+ execSync(`git push origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2368
+
2369
+ console.log(`\x1b[32m✓\x1b[0m Successfully merged to ${targetBranch}`);
2370
+
2371
+ // Delete remote branch after successful merge
2372
+ try {
2373
+ execSync(`git push origin --delete ${currentBranchName}`, { cwd: repoRoot, stdio: 'pipe' });
2374
+ console.log(`\x1b[32m✓\x1b[0m Deleted remote branch ${currentBranchName}`);
2375
+ } catch (err) {
2376
+ console.log(`\x1b[2mCould not delete remote branch\x1b[0m`);
2377
+ }
2378
+
2379
+ mergeCompleted = true;
2009
2380
  }
2010
-
2011
- mergeCompleted = true;
2012
2381
  } catch (err) {
2013
2382
  console.error(`\x1b[31m✗ Merge failed: ${err.message}\x1b[0m`);
2014
2383
  console.log(`\x1b[33mYou may need to resolve conflicts manually\x1b[0m`);