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.
- 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 +456 -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,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
|
|
1422
|
-
console.log("
|
|
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
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2151
|
+
|
|
2152
|
+
TUI.inputMode = 'prompt';
|
|
2153
|
+
|
|
1844
2154
|
const answer = await new Promise(resolve => {
|
|
1845
|
-
|
|
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
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
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
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
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
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
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
|
-
|
|
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}`);
|
|
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
|
-
|
|
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`);
|