s9n-devops-agent 2.0.18-dev.1 → 2.0.18-dev.11
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 +1 -0
- package/bin/cs-devops-agent +16 -20
- package/docs/RELEASE_NOTES.md +15 -0
- package/package.json +1 -1
- package/scripts/deploy-local.sh +100 -0
- package/src/agent-chat.js +299 -36
- package/src/credentials-manager.js +28 -6
- package/src/cs-devops-agent-worker.js +446 -87
- package/src/kora-skills.json +47 -0
- package/src/session-coordinator.js +499 -70
- package/src/setup-cs-devops-agent.js +298 -42
- package/src/ui-utils.js +1 -1
- package/start-devops-session.sh +4 -27
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
832
|
-
|
|
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.
|
|
853
|
+
console.error(`✗ PUSH FAILED: Check logs for details`);
|
|
838
854
|
}
|
|
839
855
|
}
|
|
840
856
|
} finally {
|
|
@@ -953,6 +969,109 @@ 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
|
+
const stashRes = await run('git', ['stash', 'push', '-m', `Auto-stash before rebase ${new Date().toISOString()}`]);
|
|
1012
|
+
if (stashRes.ok) stashed = true;
|
|
1013
|
+
else {
|
|
1014
|
+
console.error('\x1b[31m✗ Failed to stash changes. Aborting rebase.\x1b[0m');
|
|
1015
|
+
busy = false;
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Fetch latest
|
|
1021
|
+
log(`Fetching latest changes from origin...`);
|
|
1022
|
+
await run('git', ['fetch', 'origin', BASE_BRANCH]);
|
|
1023
|
+
|
|
1024
|
+
// Rebase
|
|
1025
|
+
log(`Rebasing onto origin/${BASE_BRANCH}...`);
|
|
1026
|
+
const rebaseRes = await run('git', ['pull', '--rebase', 'origin', BASE_BRANCH]);
|
|
1027
|
+
|
|
1028
|
+
if (rebaseRes.ok) {
|
|
1029
|
+
console.log(`\x1b[32m✓ Rebase successful!\x1b[0m`);
|
|
1030
|
+
|
|
1031
|
+
// Pop stash if needed
|
|
1032
|
+
if (stashed) {
|
|
1033
|
+
log('Restoring stashed changes...');
|
|
1034
|
+
const popRes = await run('git', ['stash', 'pop']);
|
|
1035
|
+
if (!popRes.ok) {
|
|
1036
|
+
console.error('\x1b[31m⚠️ Conflict during stash pop. Manual intervention required.\x1b[0m');
|
|
1037
|
+
console.log('\x1b[33mPlease resolve conflicts and continue.\x1b[0m');
|
|
1038
|
+
} else {
|
|
1039
|
+
console.log('\x1b[32m✓ Stash restored.\x1b[0m');
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
global.lastRebaseTime = Date.now();
|
|
1044
|
+
} else {
|
|
1045
|
+
console.error(`\x1b[31m✗ REBASE CONFLICT DETECTED\x1b[0m`);
|
|
1046
|
+
console.error(rebaseRes.stdout);
|
|
1047
|
+
|
|
1048
|
+
// Abort rebase
|
|
1049
|
+
log('Aborting rebase...');
|
|
1050
|
+
await run('git', ['rebase', '--abort']);
|
|
1051
|
+
|
|
1052
|
+
// Restore stash if we stashed
|
|
1053
|
+
if (stashed) {
|
|
1054
|
+
await run('git', ['stash', 'pop']);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
console.log('\x1b[33mRebase aborted. Please manually rebase your branch.\x1b[0m');
|
|
1058
|
+
// Disable auto-rebase for this session to avoid loop?
|
|
1059
|
+
// Or just wait for next interval.
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
console.log(`\x1b[32m✓ RESUMING AGENT OPERATION\x1b[0m\n`);
|
|
1063
|
+
return true;
|
|
1064
|
+
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
console.error(`Rebase error: ${err.message}`);
|
|
1067
|
+
// Try to recover state
|
|
1068
|
+
try { await run('git', ['rebase', '--abort']); } catch (e) {}
|
|
1069
|
+
return false;
|
|
1070
|
+
} finally {
|
|
1071
|
+
busy = false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
956
1075
|
/**
|
|
957
1076
|
* Update infrastructure documentation
|
|
958
1077
|
* @param {object} infraChanges - Infrastructure change details
|
|
@@ -1418,8 +1537,8 @@ function saveProjectSettings(settings, settingsPath) {
|
|
|
1418
1537
|
// Display copyright and license information immediately
|
|
1419
1538
|
console.log("\n" + "=".repeat(70));
|
|
1420
1539
|
console.log(" CS_DevOpsAgent - Intelligent Git Automation System");
|
|
1421
|
-
console.log(" Version
|
|
1422
|
-
console.log("
|
|
1540
|
+
console.log(" Version 2.0.18-dev.11 | Build 20260107");
|
|
1541
|
+
console.log(" Copyright (c) 2026 SeKondBrain AI Labs Limited");
|
|
1423
1542
|
console.log(" Author: Sachin Dev Duggal");
|
|
1424
1543
|
console.log(" \n Licensed under the MIT License");
|
|
1425
1544
|
console.log(" This software is provided 'as-is' without any warranty.");
|
|
@@ -1500,6 +1619,19 @@ console.log();
|
|
|
1500
1619
|
log("watching…");
|
|
1501
1620
|
}
|
|
1502
1621
|
|
|
1622
|
+
// Schedule auto-rebase if configured
|
|
1623
|
+
if (REBASE_INTERVAL_HOURS > 0) {
|
|
1624
|
+
log(`Auto-rebase scheduled every ${REBASE_INTERVAL_HOURS} hours (checking every 5m)`);
|
|
1625
|
+
// Initial check (in case we started overdue)
|
|
1626
|
+
// Don't await this so we don't block startup
|
|
1627
|
+
checkAndPerformRebase(repoRoot).catch(err => console.error(err));
|
|
1628
|
+
|
|
1629
|
+
// Periodic check
|
|
1630
|
+
setInterval(async () => {
|
|
1631
|
+
await checkAndPerformRebase(repoRoot);
|
|
1632
|
+
}, 5 * 60 * 1000); // Check every 5 minutes
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1503
1635
|
// ============================================================================
|
|
1504
1636
|
// FILE WATCHER SETUP - Monitor for changes and trigger commits
|
|
1505
1637
|
// ============================================================================
|
|
@@ -1625,25 +1757,30 @@ console.log();
|
|
|
1625
1757
|
],
|
|
1626
1758
|
})
|
|
1627
1759
|
.on("all", async (evt, p) => {
|
|
1760
|
+
// Define 'now' at the very top of the callback to avoid ReferenceError
|
|
1761
|
+
const now = Date.now();
|
|
1762
|
+
|
|
1628
1763
|
// Check if file is in local_deploy or .file-coordination (internal agent files)
|
|
1629
1764
|
if (p.includes('local_deploy/') || p.includes('.file-coordination/')) {
|
|
1630
1765
|
return;
|
|
1631
1766
|
}
|
|
1632
1767
|
|
|
1768
|
+
const isMsg = samePath(p, relMsg);
|
|
1769
|
+
|
|
1633
1770
|
// Check if file is ignored by git (respect .gitignore)
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1771
|
+
// IMPORTANT: Do NOT ignore the message file, even if it's in .gitignore
|
|
1772
|
+
if (!isMsg) {
|
|
1773
|
+
try {
|
|
1774
|
+
// git check-ignore returns 0 if ignored, 1 if not ignored
|
|
1775
|
+
// We use quiet mode (-q) and ignore stdio
|
|
1776
|
+
execSync(`git check-ignore -q "${p}"`, { stdio: 'ignore' });
|
|
1777
|
+
// If we get here, exit code was 0, so file IS ignored
|
|
1778
|
+
if (DEBUG) console.log(`[debug] Ignoring gitignored file: ${p}`);
|
|
1779
|
+
return;
|
|
1780
|
+
} catch (e) {
|
|
1781
|
+
// Exit code 1 means NOT ignored (or other error). Proceed.
|
|
1782
|
+
}
|
|
1643
1783
|
}
|
|
1644
|
-
|
|
1645
|
-
const now = Date.now();
|
|
1646
|
-
const isMsg = samePath(p, relMsg);
|
|
1647
1784
|
|
|
1648
1785
|
// Track timing of non-message changes
|
|
1649
1786
|
if (!isMsg) {
|
|
@@ -1731,12 +1868,161 @@ console.log();
|
|
|
1731
1868
|
const rl = readline.createInterface({
|
|
1732
1869
|
input: input,
|
|
1733
1870
|
output: output,
|
|
1734
|
-
prompt: '
|
|
1871
|
+
prompt: '', // We'll manage the prompt manually
|
|
1735
1872
|
terminal: true
|
|
1736
1873
|
});
|
|
1737
|
-
|
|
1874
|
+
|
|
1875
|
+
// TUI STATE
|
|
1876
|
+
const TUI = {
|
|
1877
|
+
leftLogs: [],
|
|
1878
|
+
rightLogs: [],
|
|
1879
|
+
maxLogs: 200,
|
|
1880
|
+
status: 'ACTIVE',
|
|
1881
|
+
inputBuffer: '',
|
|
1882
|
+
sessionId: sessionId,
|
|
1883
|
+
branch: await currentBranch(),
|
|
1884
|
+
inputMode: 'command' // 'command' or 'prompt'
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
// Helper to strip ANSI codes
|
|
1888
|
+
const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
|
1889
|
+
|
|
1890
|
+
// Override log output to split into two panels
|
|
1891
|
+
// Right Panel: [cs-devops-agent], [debug], [cmd], [err] (Background Activity)
|
|
1892
|
+
// Left Panel: Interactive output (status, help, etc)
|
|
1893
|
+
const originalConsoleLog = console.log;
|
|
1894
|
+
const originalConsoleError = console.error;
|
|
1895
|
+
|
|
1896
|
+
const addToBuffers = (msg, isError = false) => {
|
|
1897
|
+
const cleanMsg = stripAnsi(msg);
|
|
1898
|
+
const isBackground = cleanMsg.startsWith('[cs-devops-agent]') ||
|
|
1899
|
+
cleanMsg.startsWith('[debug]') ||
|
|
1900
|
+
cleanMsg.startsWith('[cmd]') ||
|
|
1901
|
+
cleanMsg.startsWith('[err]') ||
|
|
1902
|
+
cleanMsg.startsWith('ignore') ||
|
|
1903
|
+
cleanMsg.startsWith('watcher:');
|
|
1904
|
+
|
|
1905
|
+
const targetBuffer = isBackground ? TUI.rightLogs : TUI.leftLogs;
|
|
1906
|
+
|
|
1907
|
+
// Add timestamp for background logs only
|
|
1908
|
+
let line = msg;
|
|
1909
|
+
if (isBackground) {
|
|
1910
|
+
const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit' });
|
|
1911
|
+
line = `\x1b[2m[${time}]\x1b[0m ${msg}`;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// Split newlines and push
|
|
1915
|
+
const lines = line.split('\n');
|
|
1916
|
+
lines.forEach(l => {
|
|
1917
|
+
targetBuffer.push(l);
|
|
1918
|
+
if (targetBuffer.length > TUI.maxLogs) targetBuffer.shift();
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
renderTUI();
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1924
|
+
console.log = (...args) => {
|
|
1925
|
+
const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
|
|
1926
|
+
// Filter TUI artifacts
|
|
1927
|
+
if (msg.includes('\x1b[2J') || msg.includes('\x1b[0f')) return;
|
|
1928
|
+
addToBuffers(msg, false);
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1931
|
+
console.error = (...args) => {
|
|
1932
|
+
const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
|
|
1933
|
+
const colored = msg.startsWith('\x1b') ? msg : `\x1b[31m${msg}\x1b[0m`;
|
|
1934
|
+
addToBuffers(colored, true);
|
|
1935
|
+
};
|
|
1936
|
+
|
|
1937
|
+
// Vertical Split Renderer
|
|
1938
|
+
function renderTUI() {
|
|
1939
|
+
const width = process.stdout.columns || 80;
|
|
1940
|
+
const height = process.stdout.rows || 24;
|
|
1941
|
+
const contentHeight = height - 4; // Header, Divider, Prompt, Margin
|
|
1942
|
+
|
|
1943
|
+
// Split: Left (Interactive) 40%, Right (Logs) 60%
|
|
1944
|
+
// Ensure minimum widths
|
|
1945
|
+
const leftPercent = 0.4;
|
|
1946
|
+
const leftWidth = Math.floor(width * leftPercent) - 2;
|
|
1947
|
+
const rightWidth = width - leftWidth - 3; // -3 for separator " | "
|
|
1948
|
+
|
|
1949
|
+
if (leftWidth < 10 || rightWidth < 10) {
|
|
1950
|
+
// Fallback for very narrow screens? Just clear and print prompt?
|
|
1951
|
+
// Or simple stacked? Let's just proceed, it will look messy but functional.
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// Prepare views (Bottom-Anchored)
|
|
1955
|
+
const leftView = TUI.leftLogs.slice(-contentHeight);
|
|
1956
|
+
const rightView = TUI.rightLogs.slice(-contentHeight);
|
|
1957
|
+
|
|
1958
|
+
const leftEmpty = contentHeight - leftView.length;
|
|
1959
|
+
const rightEmpty = contentHeight - rightView.length;
|
|
1960
|
+
|
|
1961
|
+
// Draw Frame
|
|
1962
|
+
process.stdout.write('\x1b[2J'); // Clear
|
|
1963
|
+
process.stdout.write('\x1b[H'); // Home (standard ANSI)
|
|
1964
|
+
|
|
1965
|
+
// 1. Header
|
|
1966
|
+
const headerText = ` DEVOPS AGENT | Session: ${TUI.sessionId || 'N/A'} | Branch: ${TUI.branch} `;
|
|
1967
|
+
const headPad = Math.max(0, width - headerText.length);
|
|
1968
|
+
process.stdout.write(`\x1b[44m\x1b[37m${headerText}${' '.repeat(headPad)}\x1b[0m\n`);
|
|
1969
|
+
|
|
1970
|
+
// 2. Columns
|
|
1971
|
+
for (let i = 0; i < contentHeight; i++) {
|
|
1972
|
+
let lLine = (i >= leftEmpty) ? leftView[i - leftEmpty] : '';
|
|
1973
|
+
let rLine = (i >= rightEmpty) ? rightView[i - rightEmpty] : '';
|
|
1974
|
+
|
|
1975
|
+
// Removing ANSI for length calculation is essential for alignment
|
|
1976
|
+
const lPlain = stripAnsi(lLine);
|
|
1977
|
+
const rPlain = stripAnsi(rLine);
|
|
1978
|
+
|
|
1979
|
+
// Truncate to prevent wrapping and breaking layout
|
|
1980
|
+
let lDisp = lLine;
|
|
1981
|
+
let rDisp = rLine;
|
|
1982
|
+
|
|
1983
|
+
if (lPlain.length > leftWidth) {
|
|
1984
|
+
// Simple truncation (note: might cut ansi codes, but prevents layout break)
|
|
1985
|
+
// We use the plain length to determine cut point
|
|
1986
|
+
// For safety, we just display the plain truncated text to avoid hanging colors
|
|
1987
|
+
lDisp = lPlain.substring(0, leftWidth - 3) + '...';
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
if (rPlain.length > rightWidth) {
|
|
1991
|
+
rDisp = rPlain.substring(0, rightWidth - 3) + '...';
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// Pad using the length of the *displayed* string (re-strip to be sure)
|
|
1995
|
+
const lDispLen = stripAnsi(lDisp).length;
|
|
1996
|
+
const rDispLen = stripAnsi(rDisp).length;
|
|
1997
|
+
|
|
1998
|
+
const lPad = Math.max(0, leftWidth - lDispLen);
|
|
1999
|
+
const rPad = Math.max(0, rightWidth - rDispLen);
|
|
2000
|
+
|
|
2001
|
+
process.stdout.write(lDisp + ' '.repeat(lPad));
|
|
2002
|
+
process.stdout.write('\x1b[90m │ \x1b[0m');
|
|
2003
|
+
process.stdout.write(rDisp + ' '.repeat(rPad));
|
|
2004
|
+
process.stdout.write('\n');
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// 3. Divider
|
|
2008
|
+
const lDiv = '─'.repeat(leftWidth);
|
|
2009
|
+
const rDiv = '─'.repeat(rightWidth);
|
|
2010
|
+
process.stdout.write(`\x1b[90m${lDiv}─┴─${rDiv}\x1b[0m\n`);
|
|
2011
|
+
|
|
2012
|
+
// 4. Prompt
|
|
2013
|
+
process.stdout.write(`\x1b[36m[agent] >\x1b[0m ${TUI.inputBuffer}`);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// Initial render
|
|
2017
|
+
renderTUI();
|
|
2018
|
+
|
|
1738
2019
|
// Command handler
|
|
1739
2020
|
rl.on('line', async (line) => {
|
|
2021
|
+
// If in prompt mode, ignore this handler (let the prompt handler take it)
|
|
2022
|
+
if (TUI.inputMode !== 'command') return;
|
|
2023
|
+
|
|
2024
|
+
TUI.inputBuffer = '';
|
|
2025
|
+
addToBuffers(`> ${line}`); // Echo command to left panel
|
|
1740
2026
|
const cmd = line.trim().toLowerCase();
|
|
1741
2027
|
|
|
1742
2028
|
switch (cmd) {
|
|
@@ -1802,10 +2088,22 @@ console.log();
|
|
|
1802
2088
|
let commitMsg = readMsgFile(msgPath);
|
|
1803
2089
|
if (!commitMsg) {
|
|
1804
2090
|
console.log("No commit message found. Enter message (or 'cancel' to abort):");
|
|
1805
|
-
|
|
2091
|
+
|
|
2092
|
+
// Switch to prompt mode to prevent main loop interference
|
|
2093
|
+
TUI.inputMode = 'prompt';
|
|
2094
|
+
|
|
2095
|
+
// Use a temporary prompt interface
|
|
1806
2096
|
const msgInput = await new Promise(resolve => {
|
|
1807
|
-
|
|
2097
|
+
// We need to use a one-time listener that doesn't conflict
|
|
2098
|
+
const handler = (input) => {
|
|
2099
|
+
resolve(input);
|
|
2100
|
+
};
|
|
2101
|
+
rl.once('line', handler);
|
|
1808
2102
|
});
|
|
2103
|
+
|
|
2104
|
+
// Switch back to command mode
|
|
2105
|
+
TUI.inputMode = 'command';
|
|
2106
|
+
|
|
1809
2107
|
if (msgInput.trim().toLowerCase() === 'cancel') {
|
|
1810
2108
|
console.log("Commit cancelled.");
|
|
1811
2109
|
break;
|
|
@@ -1818,7 +2116,7 @@ console.log();
|
|
|
1818
2116
|
console.log("No changes to commit.");
|
|
1819
2117
|
}
|
|
1820
2118
|
break;
|
|
1821
|
-
|
|
2119
|
+
|
|
1822
2120
|
case 'push':
|
|
1823
2121
|
case 'p':
|
|
1824
2122
|
const branchName = await currentBranch();
|
|
@@ -1840,11 +2138,18 @@ console.log();
|
|
|
1840
2138
|
const uncommitted = await hasUncommittedChanges();
|
|
1841
2139
|
if (uncommitted) {
|
|
1842
2140
|
console.log("You have uncommitted changes. Commit them before exit? (y/n)");
|
|
1843
|
-
|
|
2141
|
+
|
|
2142
|
+
TUI.inputMode = 'prompt';
|
|
2143
|
+
|
|
1844
2144
|
const answer = await new Promise(resolve => {
|
|
1845
|
-
|
|
2145
|
+
const handler = (input) => {
|
|
2146
|
+
resolve(input);
|
|
2147
|
+
};
|
|
2148
|
+
rl.once('line', handler);
|
|
1846
2149
|
});
|
|
1847
2150
|
|
|
2151
|
+
TUI.inputMode = 'command';
|
|
2152
|
+
|
|
1848
2153
|
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
1849
2154
|
console.log("Committing changes...");
|
|
1850
2155
|
const exitMsg = "chore: session cleanup - final commit before exit";
|
|
@@ -1941,74 +2246,128 @@ console.log();
|
|
|
1941
2246
|
let mergeCompleted = false;
|
|
1942
2247
|
if (shouldMerge) {
|
|
1943
2248
|
try {
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
let branchExists = false;
|
|
2249
|
+
// Check if repoRoot is clean before attempting merge operations
|
|
2250
|
+
// We use --porcelain to check for modifications
|
|
2251
|
+
let isDirty = false;
|
|
1948
2252
|
try {
|
|
1949
|
-
execSync(`git
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
}).trim();
|
|
2253
|
+
const status = execSync(`git status --porcelain`, { cwd: repoRoot, encoding: 'utf8' });
|
|
2254
|
+
if (status && status.trim().length > 0) {
|
|
2255
|
+
isDirty = true;
|
|
2256
|
+
console.log(`\n\x1b[33m⚠️ The main repository has uncommitted changes:\x1b[0m`);
|
|
2257
|
+
const lines = status.trim().split('\n').slice(0, 5);
|
|
2258
|
+
lines.forEach(l => console.log(` ${l}`));
|
|
2259
|
+
if (status.trim().split('\n').length > 5) console.log(' ...');
|
|
2260
|
+
|
|
2261
|
+
console.log(`\nMerge cannot proceed with a dirty working tree.`);
|
|
2262
|
+
console.log(`You can stash these changes to proceed with the merge, or skip the merge.`);
|
|
2263
|
+
console.log(` y/yes - Stash changes and merge`);
|
|
2264
|
+
console.log(` n/no - Skip merge`);
|
|
1962
2265
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2266
|
+
rl.prompt();
|
|
2267
|
+
const stashAnswer = await new Promise(resolve => {
|
|
2268
|
+
rl.once('line', resolve);
|
|
2269
|
+
});
|
|
2270
|
+
|
|
2271
|
+
if (stashAnswer.toLowerCase() === 'y' || stashAnswer.toLowerCase() === 'yes') {
|
|
2272
|
+
console.log(`\nStashing changes in main repo...`);
|
|
2273
|
+
execSync(`git stash push -m "Auto-stash before session merge ${sessionId}"`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2274
|
+
isDirty = false;
|
|
1967
2275
|
} else {
|
|
1968
|
-
|
|
1969
|
-
|
|
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}`);
|
|
2276
|
+
console.log(`\nSkipping merge due to dirty working tree.`);
|
|
2277
|
+
shouldMerge = false;
|
|
1973
2278
|
}
|
|
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
2279
|
}
|
|
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
2280
|
} catch (err) {
|
|
2008
|
-
|
|
2281
|
+
console.log(`\x1b[31mError checking git status: ${err.message}\x1b[0m`);
|
|
2282
|
+
shouldMerge = false; // Err on side of caution
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
if (shouldMerge && !isDirty) {
|
|
2286
|
+
console.log(`\n\x1b[34mMerging ${currentBranchName} into ${targetBranch}...\x1b[0m`);
|
|
2287
|
+
|
|
2288
|
+
// Check if target branch exists locally
|
|
2289
|
+
let branchExists = false;
|
|
2290
|
+
try {
|
|
2291
|
+
execSync(`git rev-parse --verify ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2292
|
+
branchExists = true;
|
|
2293
|
+
} catch (err) {
|
|
2294
|
+
// Branch doesn't exist locally
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
if (!branchExists) {
|
|
2298
|
+
// Check if branch exists on remote
|
|
2299
|
+
try {
|
|
2300
|
+
const remoteCheck = execSync(`git ls-remote --heads origin ${targetBranch}`, {
|
|
2301
|
+
cwd: repoRoot,
|
|
2302
|
+
encoding: 'utf8'
|
|
2303
|
+
}).trim();
|
|
2304
|
+
|
|
2305
|
+
if (remoteCheck) {
|
|
2306
|
+
// Branch exists on remote, fetch it
|
|
2307
|
+
console.log(`\x1b[2mTarget branch doesn't exist locally, fetching from remote...\x1b[0m`);
|
|
2308
|
+
execSync(`git fetch origin ${targetBranch}:${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2309
|
+
} else {
|
|
2310
|
+
// Branch doesn't exist on remote either, create it
|
|
2311
|
+
console.log(`\x1b[33mTarget branch '${targetBranch}' doesn't exist. Creating it...\x1b[0m`);
|
|
2312
|
+
execSync(`git checkout -b ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2313
|
+
execSync(`git push -u origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2314
|
+
console.log(`\x1b[32m✓\x1b[0m Created new branch ${targetBranch}`);
|
|
2315
|
+
}
|
|
2316
|
+
} catch (err) {
|
|
2317
|
+
console.error(`\x1b[31m✗ Error checking/creating remote branch: ${err.message}\x1b[0m`);
|
|
2318
|
+
throw err;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
// Switch to target branch
|
|
2323
|
+
execSync(`git checkout ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2324
|
+
|
|
2325
|
+
// Pull latest (if branch already existed)
|
|
2326
|
+
if (branchExists) {
|
|
2327
|
+
try {
|
|
2328
|
+
execSync(`git pull origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2329
|
+
} catch (err) {
|
|
2330
|
+
console.log(`\x1b[2mCould not pull latest changes (may be new branch)\x1b[0m`);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// Rebase session branch onto target branch
|
|
2335
|
+
try {
|
|
2336
|
+
console.log(`\x1b[34mRebasing ${currentBranchName} onto ${targetBranch}...\x1b[0m`);
|
|
2337
|
+
execSync(`git checkout ${currentBranchName}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2338
|
+
execSync(`git rebase ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2339
|
+
console.log(`\x1b[32m✓\x1b[0m Rebase successful`);
|
|
2340
|
+
|
|
2341
|
+
// Switch back to target for merge
|
|
2342
|
+
execSync(`git checkout ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2343
|
+
} catch (err) {
|
|
2344
|
+
console.error(`\x1b[31m✗ Rebase failed: ${err.message}\x1b[0m`);
|
|
2345
|
+
console.log(`\x1b[33mAborting rebase and trying direct merge...\x1b[0m`);
|
|
2346
|
+
try { execSync(`git rebase --abort`, { cwd: repoRoot, stdio: 'pipe' }); } catch (e) {}
|
|
2347
|
+
execSync(`git checkout ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// Merge the session branch
|
|
2351
|
+
execSync(`git merge --no-ff ${currentBranchName} -m "Merge session ${sessionId}: session work"`, {
|
|
2352
|
+
cwd: repoRoot,
|
|
2353
|
+
stdio: 'pipe'
|
|
2354
|
+
});
|
|
2355
|
+
|
|
2356
|
+
// Push merged changes
|
|
2357
|
+
execSync(`git push origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2358
|
+
|
|
2359
|
+
console.log(`\x1b[32m✓\x1b[0m Successfully merged to ${targetBranch}`);
|
|
2360
|
+
|
|
2361
|
+
// Delete remote branch after successful merge
|
|
2362
|
+
try {
|
|
2363
|
+
execSync(`git push origin --delete ${currentBranchName}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
2364
|
+
console.log(`\x1b[32m✓\x1b[0m Deleted remote branch ${currentBranchName}`);
|
|
2365
|
+
} catch (err) {
|
|
2366
|
+
console.log(`\x1b[2mCould not delete remote branch\x1b[0m`);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
mergeCompleted = true;
|
|
2009
2370
|
}
|
|
2010
|
-
|
|
2011
|
-
mergeCompleted = true;
|
|
2012
2371
|
} catch (err) {
|
|
2013
2372
|
console.error(`\x1b[31m✗ Merge failed: ${err.message}\x1b[0m`);
|
|
2014
2373
|
console.log(`\x1b[33mYou may need to resolve conflicts manually\x1b[0m`);
|