abapgit-agent 1.17.8 → 1.18.0
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/abap/CLAUDE.md +150 -25
- package/abap/CLAUDE.slim.md +5 -4
- package/abap/guidelines/abaplint.md +2 -0
- package/abap/guidelines/cds-testing.md +12 -0
- package/abap/guidelines/cds.md +7 -0
- package/abap/guidelines/debug-dump.md +4 -0
- package/abap/guidelines/debug-session.md +27 -2
- package/abap/guidelines/run-probe-classes.md +43 -0
- package/abap/guidelines/string-template.md +66 -1
- package/bin/abapgit-agent +3 -2
- package/package.json +10 -6
- package/src/commands/debug.js +156 -119
- package/src/commands/guide.js +17 -0
- package/src/commands/inspect.js +7 -4
- package/src/commands/pull.js +32 -14
- package/src/commands/unit.js +2 -1
- package/src/commands/view.js +1 -1
- package/src/config.js +13 -1
- package/src/utils/abap-http.js +136 -252
- package/src/utils/adt-http.js +134 -216
- package/src/utils/debug-daemon.js +57 -48
- package/src/utils/debug-session.js +126 -25
package/src/commands/debug.js
CHANGED
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
* - Local state persisted in tmp so stateless CLI calls share the breakpoint list.
|
|
20
20
|
*
|
|
21
21
|
* Session management for AI/scripting mode (--json):
|
|
22
|
-
* attach --json
|
|
23
|
-
*
|
|
22
|
+
* attach --json runs the IPC socket server in-process after emitting the
|
|
23
|
+
* session JSON line. This keeps the ADT TCP connection alive in the same
|
|
24
|
+
* process that called attach() — SAP pins the debug session to the
|
|
25
|
+
* originating TCP connection. step/vars/stack/terminate are thin IPC
|
|
24
26
|
* clients that connect to the daemon's Unix socket and exchange one JSON command
|
|
25
27
|
* per invocation. The daemon exits when terminate is called or after 30 min idle.
|
|
26
28
|
*
|
|
@@ -30,7 +32,6 @@
|
|
|
30
32
|
|
|
31
33
|
const net = require('net');
|
|
32
34
|
const path = require('path');
|
|
33
|
-
const { spawn } = require('child_process');
|
|
34
35
|
const { AdtHttp } = require('../utils/adt-http');
|
|
35
36
|
const { DebugSession } = require('../utils/debug-session');
|
|
36
37
|
const debugStateModule = require('../utils/debug-state');
|
|
@@ -47,7 +48,13 @@ const _loadBpState = debugStateModule.loadBreakpointState;
|
|
|
47
48
|
// Daemon socket path — may be absent in unit-test mocks
|
|
48
49
|
const _getDaemonSocketPath = debugStateModule.getDaemonSocketPath;
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
// Allow callers to override the terminalId/ideId used for listener and
|
|
52
|
+
// breakpoint registration via env var. This lets CI jobs use a unique ID
|
|
53
|
+
// per run to avoid stale listeners from crashed previous runs blocking
|
|
54
|
+
// the listener POST (SAP ICM returns 400 if another listener with the
|
|
55
|
+
// same terminalId is still registered and DELETE is not available on the
|
|
56
|
+
// system).
|
|
57
|
+
const ADT_CLIENT_ID = process.env.ABAPGIT_DEBUG_TERMINAL_ID || 'ABAPGIT-AGENT-CLI';
|
|
51
58
|
|
|
52
59
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
53
60
|
|
|
@@ -632,8 +639,8 @@ async function cmdDelete(args, config, adt) {
|
|
|
632
639
|
async function cmdAttach(args, config, adt) {
|
|
633
640
|
const sessionIdOverride = val(args, '--session');
|
|
634
641
|
const jsonOutput = hasFlag(args, '--json');
|
|
635
|
-
// Per-poll timeout in seconds sent to ADT (ADT blocks the POST for this long)
|
|
636
642
|
const pollTimeout = parseInt(val(args, '--timeout') || '30', 10);
|
|
643
|
+
const maxListenSeconds = parseInt(val(args, '--max-listen') || '240', 10);
|
|
637
644
|
// Shorter timeout used in takeover mode — keeps the connection alive but
|
|
638
645
|
// lets ADT process stepContinue quickly and lets session 2 exit within
|
|
639
646
|
// ~5 seconds of clearActiveSession being written.
|
|
@@ -646,8 +653,42 @@ async function cmdAttach(args, config, adt) {
|
|
|
646
653
|
process.stderr.write('\n Waiting for breakpoint hit... (run your ABAP program in a separate window)\n');
|
|
647
654
|
}
|
|
648
655
|
|
|
656
|
+
// Fresh session for attach flow. Don't set stateful here — CSRF fetch
|
|
657
|
+
// without stateful avoids tying up a dialog WP. Stateful is added per-call
|
|
658
|
+
// by debug-session.js STATEFUL_HEADER on attach/getStack/step/vars.
|
|
659
|
+
adt.clearSession();
|
|
660
|
+
if (adt._axios) {
|
|
661
|
+
const agent = adt._axios.defaults.httpsAgent || adt._axios.defaults.httpAgent;
|
|
662
|
+
if (agent) agent.destroy();
|
|
663
|
+
if (adt._axios.defaults.httpsAgent) {
|
|
664
|
+
adt._axios.defaults.httpsAgent = new (require('https').Agent)({ rejectUnauthorized: false, keepAlive: true });
|
|
665
|
+
} else if (adt._axios.defaults.httpAgent) {
|
|
666
|
+
adt._axios.defaults.httpAgent = new (require('http').Agent)({ keepAlive: true });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
649
669
|
await adt.fetchCsrfToken();
|
|
650
670
|
|
|
671
|
+
// Delete any stale listener registered under our terminalId from a previous
|
|
672
|
+
// (possibly crashed) run. A frozen ABAP work process holding a listener for
|
|
673
|
+
// the same terminalId causes SAP ICM to return HTTP 400 "Service cannot be
|
|
674
|
+
// reached" for every new listener POST. Deleting first clears that state.
|
|
675
|
+
// Ignore errors — there may be no stale listener to delete.
|
|
676
|
+
if (!sessionIdOverride) {
|
|
677
|
+
try {
|
|
678
|
+
const delUrl = `/sap/bc/adt/debugger/listeners` +
|
|
679
|
+
`?debuggingMode=user` +
|
|
680
|
+
`&requestUser=${encodeURIComponent((config.user || '').toUpperCase())}` +
|
|
681
|
+
`&terminalId=${encodeURIComponent(ADT_CLIENT_ID)}` +
|
|
682
|
+
`&ideId=${encodeURIComponent(ADT_CLIENT_ID)}` +
|
|
683
|
+
`&checkConflict=false` +
|
|
684
|
+
`¬ifyConflict=true`;
|
|
685
|
+
await adt.delete(delUrl);
|
|
686
|
+
if (jsonOutput) process.stderr.write(`[debug-attach] deleted stale listener\n`);
|
|
687
|
+
} catch (e) {
|
|
688
|
+
if (jsonOutput) process.stderr.write(`[debug-attach] delete listener (cleanup): ${e.statusCode || e.message}\n`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
651
692
|
// Re-POST local breakpoints to refresh them on the server before listening.
|
|
652
693
|
// Breakpoints expire when the SAP session or work process is restarted.
|
|
653
694
|
// This ensures they are active regardless of when they were originally set.
|
|
@@ -672,6 +713,7 @@ async function cmdAttach(args, config, adt) {
|
|
|
672
713
|
}
|
|
673
714
|
let sessionId = sessionIdOverride;
|
|
674
715
|
let positionResult = null;
|
|
716
|
+
let session = null;
|
|
675
717
|
|
|
676
718
|
if (!sessionId) {
|
|
677
719
|
// ADT listeners: POST long-polls until a breakpoint is hit (or timeout).
|
|
@@ -683,9 +725,9 @@ async function cmdAttach(args, config, adt) {
|
|
|
683
725
|
`&terminalId=${encodeURIComponent(listenTerminalId)}` +
|
|
684
726
|
`&ideId=${encodeURIComponent(listenTerminalId)}`;
|
|
685
727
|
|
|
686
|
-
const MAX_POLLS = Math.ceil(
|
|
728
|
+
const MAX_POLLS = Math.ceil(maxListenSeconds / pollTimeout);
|
|
687
729
|
// In takeover mode we switch to takeoverPollTimeout — recalculate the limit then.
|
|
688
|
-
const MAX_TAKEOVER_POLLS = Math.ceil(
|
|
730
|
+
const MAX_TAKEOVER_POLLS = Math.ceil(maxListenSeconds / takeoverPollTimeout);
|
|
689
731
|
let dots = 0;
|
|
690
732
|
const attachStartedAt = Date.now(); // used to detect if another session takes over
|
|
691
733
|
let takenOver = false;
|
|
@@ -723,6 +765,12 @@ async function cmdAttach(args, config, adt) {
|
|
|
723
765
|
await new Promise(r => setTimeout(r, 2000));
|
|
724
766
|
continue;
|
|
725
767
|
}
|
|
768
|
+
if (err && err.statusCode === 400 &&
|
|
769
|
+
err.body && err.body.includes('Service cannot be reached')) {
|
|
770
|
+
if (jsonOutput) process.stderr.write(`[debug-attach] listener POST returned 400 (Service cannot be reached), retrying in 5s...\n`);
|
|
771
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
726
774
|
if (err && err.statusCode === 404) {
|
|
727
775
|
console.error(
|
|
728
776
|
'\n Debug session commands require ADT Debugger service (listeners).' +
|
|
@@ -742,6 +790,7 @@ async function cmdAttach(args, config, adt) {
|
|
|
742
790
|
const debuggeeIdMatch = resp.body.match(/<DEBUGGEE_ID>([^<]+)<\/DEBUGGEE_ID>/i) ||
|
|
743
791
|
resp.body.match(/DEBUGGEE_ID="([^"]+)"/i);
|
|
744
792
|
sessionId = debuggeeIdMatch ? debuggeeIdMatch[1].trim() : null;
|
|
793
|
+
if (jsonOutput && sessionId) process.stderr.write(`[debug-attach] listener returned debuggeeId=${sessionId}\n`);
|
|
745
794
|
|
|
746
795
|
if (!sessionId) {
|
|
747
796
|
// Fallback: some ADT versions put session info in different attributes
|
|
@@ -773,6 +822,34 @@ async function cmdAttach(args, config, adt) {
|
|
|
773
822
|
}
|
|
774
823
|
continue;
|
|
775
824
|
}
|
|
825
|
+
|
|
826
|
+
// Attach to the WP and verify it's alive before committing.
|
|
827
|
+
// In --json mode: if the WP is a stale session from a previous run,
|
|
828
|
+
// attach() may succeed but getPosition() returns 400. When that happens,
|
|
829
|
+
// reset the SAP session fully and continue listening for a fresh BP hit.
|
|
830
|
+
{
|
|
831
|
+
if (!jsonOutput) process.stderr.write('\n Attaching to debug session...\n');
|
|
832
|
+
const candidateSession = new DebugSession(adt, sessionId);
|
|
833
|
+
try {
|
|
834
|
+
await candidateSession.attach(sessionId, (config.user || '').toUpperCase());
|
|
835
|
+
} catch (e) {
|
|
836
|
+
if (jsonOutput) {
|
|
837
|
+
process.stderr.write(`[debug-attach] attach() failed: ${e.message || e} — resetting session\n`);
|
|
838
|
+
adt.clearSession();
|
|
839
|
+
await adt.fetchCsrfToken();
|
|
840
|
+
sessionId = null;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
printHttpError(e, { prefix: ' Error during attach' });
|
|
844
|
+
if (e.body) console.error(' Response body:', e.body.substring(0, 400));
|
|
845
|
+
process.exit(1);
|
|
846
|
+
}
|
|
847
|
+
// attach() succeeded — skip getPosition() validation.
|
|
848
|
+
// With keepAlive, the connection is shared and getStack via the
|
|
849
|
+
// in-process daemon will use the same AdtHttp + same TCP socket.
|
|
850
|
+
positionResult = { position: {}, source: [] };
|
|
851
|
+
session = candidateSession;
|
|
852
|
+
}
|
|
776
853
|
break;
|
|
777
854
|
}
|
|
778
855
|
|
|
@@ -799,93 +876,53 @@ async function cmdAttach(args, config, adt) {
|
|
|
799
876
|
if (!jsonOutput) process.stderr.write(' Listener timed out waiting for other session to finish.\n\n');
|
|
800
877
|
process.exit(0);
|
|
801
878
|
}
|
|
802
|
-
console.error(
|
|
879
|
+
console.error(`\n Timeout: No breakpoint was hit within ${maxListenSeconds}s.\n`);
|
|
803
880
|
process.exit(1);
|
|
804
881
|
}
|
|
805
882
|
}
|
|
806
883
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
// debugger for that work process. Without this step, all subsequent calls
|
|
812
|
-
// (getStack, getVariables, step) return "noSessionAttached" (T100-530).
|
|
813
|
-
if (!sessionIdOverride) {
|
|
814
|
-
if (!jsonOutput) {
|
|
815
|
-
process.stderr.write('\n Attaching to debug session...\n');
|
|
816
|
-
}
|
|
884
|
+
// When using --session override (no listener), do attach+getPosition here.
|
|
885
|
+
if (sessionIdOverride) {
|
|
886
|
+
session = new DebugSession(adt, sessionId);
|
|
887
|
+
// getPosition is best-effort for --session override (e.g. human REPL recovery)
|
|
817
888
|
try {
|
|
818
|
-
await session.
|
|
889
|
+
positionResult = await session.getPosition();
|
|
819
890
|
} catch (e) {
|
|
820
|
-
|
|
821
|
-
if (e.body) console.error(' Response body:', e.body.substring(0, 400));
|
|
822
|
-
process.exit(1);
|
|
891
|
+
positionResult = { position: {}, source: [] };
|
|
823
892
|
}
|
|
824
893
|
}
|
|
825
894
|
|
|
826
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
positionResult = { position: {}, source: [] };
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// Dummy loop body retained for structural compatibility (never executes)
|
|
835
|
-
if (false) {
|
|
836
|
-
const MAX_POLLS = 0;
|
|
837
|
-
const POLL_INTERVAL = 0;
|
|
838
|
-
for (let i = 0; i < MAX_POLLS; i++) {
|
|
839
|
-
try {
|
|
840
|
-
const frames = await session.getStack();
|
|
841
|
-
if (frames && frames.length > 0 && frames[0].line > 0) {
|
|
842
|
-
positionResult = await session.getPosition();
|
|
843
|
-
break;
|
|
844
|
-
}
|
|
845
|
-
} catch (e) {
|
|
846
|
-
// Stack not ready yet — keep polling
|
|
847
|
-
}
|
|
848
|
-
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
849
|
-
}
|
|
895
|
+
// session and positionResult are now set (either via listener loop or --session override)
|
|
896
|
+
if (!session) {
|
|
897
|
+
// Should not reach here — listener loop either sets session or exits
|
|
898
|
+
console.error('\n Internal error: session not initialized after attach.\n');
|
|
899
|
+
process.exit(1);
|
|
850
900
|
}
|
|
901
|
+
if (!positionResult) positionResult = { position: {}, source: [] };
|
|
851
902
|
|
|
852
903
|
const { position, source } = positionResult;
|
|
853
904
|
saveActiveSession(config, { sessionId, position });
|
|
854
905
|
|
|
855
906
|
if (jsonOutput) {
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
//
|
|
907
|
+
// Run the socket server in-process so all ADT requests reuse the same TCP
|
|
908
|
+
// connection that called attach(). SAP pins the debug session to the
|
|
909
|
+
// originating TCP connection — a new connection returns HTTP 400.
|
|
859
910
|
const socketPath = _getDaemonSocketPath ? _getDaemonSocketPath(config) : null;
|
|
860
911
|
if (socketPath) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
...process.env,
|
|
864
|
-
DEBUG_DAEMON_MODE: '1',
|
|
865
|
-
DEBUG_DAEMON_CONFIG: JSON.stringify(config),
|
|
866
|
-
DEBUG_DAEMON_SESSION_ID: sessionId,
|
|
867
|
-
DEBUG_DAEMON_SOCK_PATH: socketPath,
|
|
868
|
-
DEBUG_DAEMON_SESSION_SNAPSHOT: JSON.stringify(snapshot)
|
|
869
|
-
};
|
|
870
|
-
const daemonScript = path.resolve(__dirname, '../utils/debug-daemon.js');
|
|
871
|
-
const child = spawn(process.execPath, [daemonScript], {
|
|
872
|
-
detached: true,
|
|
873
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
874
|
-
env: daemonEnv
|
|
875
|
-
});
|
|
876
|
-
child.unref();
|
|
877
|
-
|
|
878
|
-
// Wait for daemon socket to appear (up to 5 s) before returning JSON
|
|
879
|
-
try {
|
|
880
|
-
await waitForSocket(socketPath, 5000);
|
|
881
|
-
// Persist socket path in session state so step/vars/stack/terminate find it
|
|
882
|
-
saveActiveSession(config, { sessionId, position, socketPath });
|
|
883
|
-
} catch (e) {
|
|
884
|
-
// Non-fatal: fall back to stateless direct-ADT mode
|
|
885
|
-
}
|
|
912
|
+
// Save cookies so terminate can release the frozen WP even if the daemon is gone
|
|
913
|
+
saveActiveSession(config, { sessionId, position, socketPath, cookies: adt.cookies });
|
|
886
914
|
}
|
|
887
915
|
|
|
916
|
+
// Emit session JSON now so the caller (script / AI) knows the BP was hit.
|
|
888
917
|
console.log(JSON.stringify({ session: sessionId, position, source }));
|
|
918
|
+
|
|
919
|
+
if (socketPath) {
|
|
920
|
+
const { startDaemon } = require('../utils/debug-daemon');
|
|
921
|
+
// Blocks until terminate is called or idle timeout — intentional.
|
|
922
|
+
// The process stays alive as the IPC socket server, holding the ADT
|
|
923
|
+
// TCP connection open. The caller runs this command in the background (&).
|
|
924
|
+
await startDaemon(session, socketPath);
|
|
925
|
+
}
|
|
889
926
|
return;
|
|
890
927
|
}
|
|
891
928
|
|
|
@@ -1050,8 +1087,8 @@ async function cmdVars(args, config, adt) {
|
|
|
1050
1087
|
* LO_FACTORY->MT_COMMAND_MAP (object attr → table)
|
|
1051
1088
|
* LS_DATA->COMPONENT (structure field)
|
|
1052
1089
|
*
|
|
1053
|
-
* When a daemon socket is active, uses daemon IPC for single-segment
|
|
1054
|
-
*
|
|
1090
|
+
* When a daemon socket is active, uses daemon IPC for both single-segment and
|
|
1091
|
+
* multi-segment paths so the stateful ADT session is always reused.
|
|
1055
1092
|
*/
|
|
1056
1093
|
async function cmdExpand(expandName, sessionId, socketPath, config, adt, jsonOutput) {
|
|
1057
1094
|
// Split on -> to detect path notation. Also handle --> typos by stripping stray dashes.
|
|
@@ -1059,20 +1096,37 @@ async function cmdExpand(expandName, sessionId, socketPath, config, adt, jsonOut
|
|
|
1059
1096
|
// [N]-FIELD → [N]->FIELD (array row then struct field)
|
|
1060
1097
|
// *-FIELD → *->FIELD (dereference then struct field: lr_request->*-files)
|
|
1061
1098
|
const normalizedName = expandName
|
|
1099
|
+
.replace(/^([A-Za-z0-9_@]+)\[(\d+)\]$/, '$1->[$2]') // VAR[N] → VAR->[N] (exact match)
|
|
1100
|
+
.replace(/([A-Za-z0-9_@]+)\[(\d+)\]->/g, '$1->[$2]->') // VAR[N]->X → VAR->[N]->X (mid-path)
|
|
1062
1101
|
.replace(/\](-(?!>))/g, ']->') // [N]-FIELD → [N]->FIELD
|
|
1063
1102
|
.replace(/\*(-(?!>))/g, '*->'); // *-FIELD → *->FIELD
|
|
1064
1103
|
const pathParts = normalizedName.split('->').map(s => s.replace(/^-+|-+$/g, '').trim()).filter(Boolean);
|
|
1065
1104
|
|
|
1066
|
-
// Multi-segment path:
|
|
1105
|
+
// Multi-segment path: route through daemon when available (daemon holds stateful ADT session)
|
|
1067
1106
|
if (pathParts.length > 1) {
|
|
1068
|
-
if (!adt.csrfToken) await adt.fetchCsrfToken();
|
|
1069
|
-
const session = new DebugSession(adt, sessionId);
|
|
1070
1107
|
let result;
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1108
|
+
if (socketPath) {
|
|
1109
|
+
let resp;
|
|
1110
|
+
try {
|
|
1111
|
+
resp = await sendDaemonCommand(socketPath, { cmd: 'expandPath', pathParts }, 60000);
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
printHttpError(err, { prefix: ' Error' });
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
}
|
|
1116
|
+
if (!resp.ok) {
|
|
1117
|
+
console.error(`\n Error: ${resp.error}\n`);
|
|
1118
|
+
process.exit(1);
|
|
1119
|
+
}
|
|
1120
|
+
result = { variable: resp.variable, children: resp.children };
|
|
1121
|
+
} else {
|
|
1122
|
+
if (!adt.csrfToken) await adt.fetchCsrfToken();
|
|
1123
|
+
const session = new DebugSession(adt, sessionId);
|
|
1124
|
+
try {
|
|
1125
|
+
result = await session.expandPath(pathParts);
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
printHttpError(err, { prefix: ' Error' });
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1076
1130
|
}
|
|
1077
1131
|
const { variable: target, children } = result;
|
|
1078
1132
|
return _printExpandResult(expandName, target, children, jsonOutput);
|
|
@@ -1243,20 +1297,29 @@ async function cmdTerminate(args, config, adt) {
|
|
|
1243
1297
|
|
|
1244
1298
|
// Prefer daemon IPC — daemon calls session.terminate() then exits itself
|
|
1245
1299
|
if (socketPath) {
|
|
1246
|
-
let
|
|
1300
|
+
let ipcSucceeded = false;
|
|
1247
1301
|
try {
|
|
1248
|
-
resp = await sendDaemonCommand(socketPath, { cmd: 'terminate' }, 30000);
|
|
1302
|
+
const resp = await sendDaemonCommand(socketPath, { cmd: 'terminate' }, 30000);
|
|
1303
|
+
if (resp && resp.ok) ipcSucceeded = true;
|
|
1249
1304
|
} catch (err) {
|
|
1250
|
-
// Socket
|
|
1251
|
-
resp = { ok: true, terminated: true };
|
|
1305
|
+
// Socket is gone (daemon crashed or was killed) — fall through to direct ADT call
|
|
1252
1306
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1307
|
+
if (ipcSucceeded) {
|
|
1308
|
+
clearActiveSession(config);
|
|
1309
|
+
if (jsonOutput) {
|
|
1310
|
+
console.log(JSON.stringify({ terminated: true }));
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
console.log('\n Debug session terminated.\n');
|
|
1256
1314
|
return;
|
|
1257
1315
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1316
|
+
// IPC failed — daemon is gone but ABAP WP may still be frozen.
|
|
1317
|
+
// Use stored session cookies to release the WP via direct ADT call.
|
|
1318
|
+
const savedState = loadActiveSession(config);
|
|
1319
|
+
if (savedState && savedState.cookies && sessionId) {
|
|
1320
|
+
adt.cookies = savedState.cookies;
|
|
1321
|
+
}
|
|
1322
|
+
// Fall through to direct ADT terminate below
|
|
1260
1323
|
}
|
|
1261
1324
|
|
|
1262
1325
|
// Fallback: direct ADT call
|
|
@@ -1289,32 +1352,6 @@ async function cmdTerminate(args, config, adt) {
|
|
|
1289
1352
|
|
|
1290
1353
|
// ─── Daemon IPC helpers ───────────────────────────────────────────────────────
|
|
1291
1354
|
|
|
1292
|
-
/**
|
|
1293
|
-
* Wait for the daemon's Unix socket to appear (after spawn).
|
|
1294
|
-
* @param {string} socketPath
|
|
1295
|
-
* @param {number} timeoutMs
|
|
1296
|
-
*/
|
|
1297
|
-
function waitForSocket(socketPath, timeoutMs) {
|
|
1298
|
-
return new Promise((resolve, reject) => {
|
|
1299
|
-
const deadline = Date.now() + timeoutMs;
|
|
1300
|
-
function check() {
|
|
1301
|
-
const client = net.createConnection(socketPath);
|
|
1302
|
-
client.on('connect', () => {
|
|
1303
|
-
client.destroy();
|
|
1304
|
-
resolve();
|
|
1305
|
-
});
|
|
1306
|
-
client.on('error', () => {
|
|
1307
|
-
if (Date.now() >= deadline) {
|
|
1308
|
-
reject(new Error('Timeout waiting for debug daemon to start'));
|
|
1309
|
-
} else {
|
|
1310
|
-
setTimeout(check, 100);
|
|
1311
|
-
}
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
check();
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
1355
|
/**
|
|
1319
1356
|
* Send one JSON command to the daemon and return the parsed JSON response.
|
|
1320
1357
|
* @param {string} socketPath
|
package/src/commands/guide.js
CHANGED
|
@@ -178,6 +178,13 @@ module.exports = {
|
|
|
178
178
|
console.log('');
|
|
179
179
|
},
|
|
180
180
|
|
|
181
|
+
_extractAiContent(content) {
|
|
182
|
+
const marker = '<!-- AI-CONDENSED-START -->';
|
|
183
|
+
const markerIndex = content.indexOf(marker);
|
|
184
|
+
if (markerIndex === -1) return content;
|
|
185
|
+
return content.slice(markerIndex + marker.length).trimStart();
|
|
186
|
+
},
|
|
187
|
+
|
|
181
188
|
async execute(args) {
|
|
182
189
|
if (args.includes('--migrate')) {
|
|
183
190
|
return this._runMigrate(args);
|
|
@@ -197,6 +204,16 @@ module.exports = {
|
|
|
197
204
|
|
|
198
205
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
199
206
|
|
|
207
|
+
if (args.includes('--ai')) {
|
|
208
|
+
const aiContent = this._extractAiContent(content);
|
|
209
|
+
if (args.includes('--json')) {
|
|
210
|
+
console.log(JSON.stringify({ path: filePath, content: aiContent }));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
console.log(aiContent);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
200
217
|
if (args.includes('--json')) {
|
|
201
218
|
console.log(JSON.stringify({ path: filePath, content }));
|
|
202
219
|
return;
|
package/src/commands/inspect.js
CHANGED
|
@@ -248,7 +248,7 @@ module.exports = {
|
|
|
248
248
|
requiresVersionCheck: true,
|
|
249
249
|
|
|
250
250
|
async execute(args, context) {
|
|
251
|
-
const { loadConfig, AbapHttp } = context;
|
|
251
|
+
const { loadConfig, AbapHttp, getInspectConfig } = context;
|
|
252
252
|
|
|
253
253
|
if (args.includes('--help') || args.includes('-h')) {
|
|
254
254
|
console.log(`
|
|
@@ -287,9 +287,11 @@ Examples:
|
|
|
287
287
|
|
|
288
288
|
const filesSyntaxCheck = args[filesArgIndex + 1].split(',').map(f => f.trim());
|
|
289
289
|
|
|
290
|
-
// Parse optional --variant parameter
|
|
290
|
+
// Parse optional --variant parameter; fall back to project config
|
|
291
291
|
const variantArgIndex = args.indexOf('--variant');
|
|
292
|
-
const
|
|
292
|
+
const variantArg = variantArgIndex !== -1 ? args[variantArgIndex + 1] : null;
|
|
293
|
+
const inspectConfig = getInspectConfig();
|
|
294
|
+
const variant = variantArg || inspectConfig.variant || null;
|
|
293
295
|
|
|
294
296
|
// Parse optional --junit-output parameter
|
|
295
297
|
const junitArgIndex = args.indexOf('--junit-output');
|
|
@@ -298,7 +300,8 @@ Examples:
|
|
|
298
300
|
if (!jsonOutput) {
|
|
299
301
|
console.log(`\n Inspect for ${filesSyntaxCheck.length} file(s)`);
|
|
300
302
|
if (variant) {
|
|
301
|
-
|
|
303
|
+
const source = variantArg ? '' : ' (from project config)';
|
|
304
|
+
console.log(` Using variant: ${variant}${source}`);
|
|
302
305
|
}
|
|
303
306
|
if (junitOutput) {
|
|
304
307
|
console.log(` JUnit output: ${junitOutput}`);
|
package/src/commands/pull.js
CHANGED
|
@@ -329,7 +329,8 @@ Examples:
|
|
|
329
329
|
const jobId = result.JOB_ID || result.job_id;
|
|
330
330
|
const message = result.MESSAGE || result.message;
|
|
331
331
|
const errorDetail = result.ERROR_DETAIL || result.error_detail;
|
|
332
|
-
const
|
|
332
|
+
const activatedCountRaw = result.ACTIVATED_COUNT ?? result.activated_count ?? null;
|
|
333
|
+
const activatedCount = activatedCountRaw || 0;
|
|
333
334
|
const failedCount = result.FAILED_COUNT || result.failed_count || 0;
|
|
334
335
|
const logMessages = result.LOG_MESSAGES || result.log_messages || [];
|
|
335
336
|
const activatedObjects = result.ACTIVATED_OBJECTS || result.activated_objects || [];
|
|
@@ -362,6 +363,9 @@ Examples:
|
|
|
362
363
|
if (success === 'X' || success === true) {
|
|
363
364
|
console.log(`✅ Pull completed successfully!`);
|
|
364
365
|
console.log(` Message: ${message || 'N/A'}`);
|
|
366
|
+
if (activatedCountRaw !== null && activatedCount === 0 && activatedObjects.length === 0 && files && !isRepull) {
|
|
367
|
+
console.warn(`⚠️ ACTIVATED_COUNT: 0 — no objects were activated. Check for unpushed commits: git log origin/<branch>..HEAD`);
|
|
368
|
+
}
|
|
365
369
|
} else if (failedCount === 0 && failedObjects.length === 0 &&
|
|
366
370
|
activatedCount === 0 && logMessages.length === 0 &&
|
|
367
371
|
(!message || /activation cancelled|nothing to activate|already active/i.test(message))) {
|
|
@@ -522,20 +526,40 @@ Examples:
|
|
|
522
526
|
const quotedPaths = diffFiles.map(f => `"${f.relPath}"`).join(' ');
|
|
523
527
|
execSync(`git add ${quotedPaths}`, { cwd: process.cwd() });
|
|
524
528
|
|
|
525
|
-
// 3.
|
|
529
|
+
// 3. Capture pre-amend SHA before amending (used for --force-with-lease below)
|
|
530
|
+
const preAmendSha = execSync('git rev-parse HEAD', { cwd: process.cwd(), stdio: 'pipe' }).toString().trim();
|
|
531
|
+
|
|
532
|
+
// 3b. Check whether origin/<branch> already exists on the remote.
|
|
533
|
+
// Use git ls-remote rather than rev-parse origin/<branch> because the local
|
|
534
|
+
// remote-tracking ref may not exist even when the branch is on the remote
|
|
535
|
+
// (e.g. after a push that only wrote branch config but not refs/remotes/).
|
|
536
|
+
let remoteRefExists = false;
|
|
537
|
+
try {
|
|
538
|
+
const lsOut = execSync(`git ls-remote origin "refs/heads/${branch}"`, { cwd: process.cwd(), stdio: 'pipe' }).toString().trim();
|
|
539
|
+
remoteRefExists = lsOut.length > 0;
|
|
540
|
+
} catch (_) { /* cannot reach remote — treat as new branch */ }
|
|
541
|
+
|
|
542
|
+
// 4. Amend last commit
|
|
526
543
|
execSync('git commit --amend --no-edit', { cwd: process.cwd() });
|
|
527
544
|
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
545
|
+
// 5. Push with force-with-lease using the pre-amend SHA as the expected remote value.
|
|
546
|
+
// Using --force-with-lease=<refname>:<sha> tells git: "the remote must have exactly
|
|
547
|
+
// <preAmendSha> — if it does, replace it with our amended commit".
|
|
548
|
+
// Plain --force-with-lease (no sha) re-checks the tracking ref which was just updated
|
|
549
|
+
// by git fetch, making it always fail after an amend.
|
|
531
550
|
let pushed = false;
|
|
532
551
|
try {
|
|
533
|
-
try { execSync('git fetch origin', { cwd: process.cwd(), stdio: 'pipe' }); } catch (_) { /* no remote is fine */ }
|
|
534
552
|
// Retry the push up to 3 times on transient server errors (e.g. GitHub Enterprise 500)
|
|
535
553
|
let pushErr;
|
|
536
554
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
537
555
|
try {
|
|
538
|
-
|
|
556
|
+
// If origin/<branch> exists: use --force-with-lease=branch:preAmendSha so we only
|
|
557
|
+
// overwrite if the remote still has the pre-amend commit (safe concurrent guard).
|
|
558
|
+
// If origin/<branch> doesn't exist yet: plain push with --set-upstream (no lease needed).
|
|
559
|
+
const pushCmd = remoteRefExists
|
|
560
|
+
? `git push --set-upstream origin ${branch} --force-with-lease=${branch}:${preAmendSha}`
|
|
561
|
+
: `git push --set-upstream origin ${branch}`;
|
|
562
|
+
execSync(pushCmd, { cwd: process.cwd(), stdio: 'pipe' });
|
|
539
563
|
pushed = true;
|
|
540
564
|
break;
|
|
541
565
|
} catch (err) {
|
|
@@ -550,13 +574,7 @@ Examples:
|
|
|
550
574
|
}
|
|
551
575
|
}
|
|
552
576
|
} catch (pushErr) {
|
|
553
|
-
|
|
554
|
-
if (msg.includes('no upstream branch') || msg.includes('has no upstream')) {
|
|
555
|
-
// Branch not yet pushed — set upstream and force push (amend requires force)
|
|
556
|
-
execSync(`git push --force-with-lease --set-upstream origin ${branch}`, { cwd: process.cwd(), stdio: 'pipe' });
|
|
557
|
-
pushed = true;
|
|
558
|
-
}
|
|
559
|
-
// Any other push error (no remote at all, auth failure, etc.) → skip silently
|
|
577
|
+
// Any push error (no remote, auth failure, etc.) → skip silently
|
|
560
578
|
}
|
|
561
579
|
|
|
562
580
|
if (pushed) {
|
package/src/commands/unit.js
CHANGED
|
@@ -166,8 +166,9 @@ async function runUnitTestForFile(sourceFile, csrfToken, config, coverage, http,
|
|
|
166
166
|
// Handle uppercase keys from ABAP
|
|
167
167
|
const success = result.SUCCESS || result.success;
|
|
168
168
|
const testCount = result.TEST_COUNT || result.test_count || 0;
|
|
169
|
-
const passedCount = result.PASSED_COUNT || result.passed_count || 0;
|
|
170
169
|
const failedCount = result.FAILED_COUNT || result.failed_count || 0;
|
|
170
|
+
// ABAP AUnit API does not return individual passing method names — derive passed count
|
|
171
|
+
const passedCount = testCount - failedCount;
|
|
171
172
|
const message = result.MESSAGE || result.message || '';
|
|
172
173
|
const errors = result.ERRORS || result.errors || [];
|
|
173
174
|
|
package/src/commands/view.js
CHANGED
|
@@ -151,7 +151,7 @@ async function computeGlobalStarts(objName, sections, config) {
|
|
|
151
151
|
* Returns 0 if no better line is found (falls back to METHOD statement).
|
|
152
152
|
*/
|
|
153
153
|
function findFirstExecutableLine(lines) {
|
|
154
|
-
const declPattern = /^\s*(data|final|types|constants|class-data)[\s:(]/i;
|
|
154
|
+
const declPattern = /^\s*(data|final|types|constants|class-data|field-symbols)[\s:(</\[]/i;
|
|
155
155
|
const methodPattern = /^\s*method\s+/i;
|
|
156
156
|
const commentPattern = /^\s*[*"]/;
|
|
157
157
|
// Program-level header/declaration keywords that are not executable statements
|
package/src/config.js
CHANGED
|
@@ -247,6 +247,17 @@ function getCoverageConfig() {
|
|
|
247
247
|
};
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Get inspect settings from project-level config (.abapgit-agent.json)
|
|
252
|
+
* @returns {{ variant: string|null }}
|
|
253
|
+
*/
|
|
254
|
+
function getInspectConfig() {
|
|
255
|
+
const projectConfig = loadProjectConfig();
|
|
256
|
+
return {
|
|
257
|
+
variant: projectConfig?.inspect?.variant || null,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
250
261
|
module.exports = {
|
|
251
262
|
loadConfig,
|
|
252
263
|
getAbapConfig,
|
|
@@ -261,5 +272,6 @@ module.exports = {
|
|
|
261
272
|
getTransportHookConfig,
|
|
262
273
|
getTransportSettings,
|
|
263
274
|
getScratchWorkspace,
|
|
264
|
-
getCoverageConfig
|
|
275
|
+
getCoverageConfig,
|
|
276
|
+
getInspectConfig
|
|
265
277
|
};
|