abapgit-agent 1.9.0 → 1.10.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 +22 -0
- package/abap/CLAUDE.md +104 -72
- package/bin/abapgit-agent +1 -0
- package/package.json +6 -1
- package/src/commands/debug.js +1390 -0
- package/src/utils/adt-http.js +344 -0
- package/src/utils/debug-daemon.js +207 -0
- package/src/utils/debug-render.js +69 -0
- package/src/utils/debug-repl.js +256 -0
- package/src/utils/debug-session.js +845 -0
- package/src/utils/debug-state.js +124 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interactive readline REPL for human debug mode.
|
|
5
|
+
* Entered when `debug attach` is called without --json.
|
|
6
|
+
*/
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const { printVarList } = require('./debug-render');
|
|
9
|
+
|
|
10
|
+
const HELP = `
|
|
11
|
+
Commands:
|
|
12
|
+
s / step — Step into
|
|
13
|
+
n / next — Step over
|
|
14
|
+
o / out — Step out
|
|
15
|
+
c / continue — Continue execution
|
|
16
|
+
v / vars — Show variables
|
|
17
|
+
x / expand <var> — Drill into a complex variable (table / structure)
|
|
18
|
+
bt / stack — Show call stack
|
|
19
|
+
q / quit — Detach debugger (program continues running)
|
|
20
|
+
kill — Terminate the running program (hard abort)
|
|
21
|
+
h / help — Show this help
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render the current debugger state to the terminal.
|
|
26
|
+
* @param {object} position - { class, method, include, line, ... }
|
|
27
|
+
* @param {Array} source - [{ lineNumber, text, current }]
|
|
28
|
+
* @param {Array} variables - [{ name, type, value }]
|
|
29
|
+
*/
|
|
30
|
+
function renderState(position, source, variables) {
|
|
31
|
+
const where = position.class
|
|
32
|
+
? `${position.class}->${position.method}`
|
|
33
|
+
: (position.method || position.program || '?');
|
|
34
|
+
const lineRef = position.line ? ` (line ${position.line})` : '';
|
|
35
|
+
|
|
36
|
+
console.log(`\n ABAP Debugger — ${where}${lineRef}`);
|
|
37
|
+
console.log(' ' + '─'.repeat(55));
|
|
38
|
+
|
|
39
|
+
if (source && source.length > 0) {
|
|
40
|
+
source.forEach(({ lineNumber, text, current }) => {
|
|
41
|
+
const marker = current ? '>' : ' ';
|
|
42
|
+
console.log(` ${marker}${String(lineNumber).padStart(4)} ${text}`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (variables && variables.length > 0) {
|
|
47
|
+
printVarList(variables);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('\n [s]tep [n]ext [o]ut [c]ontinue [v]ars [x]pand [bt] [q]uit(detach) kill');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Start the interactive REPL.
|
|
55
|
+
* @param {import('./debug-session').DebugSession} session
|
|
56
|
+
* @param {{ position: object, source: string[] }} initialState
|
|
57
|
+
* @param {Function} [onBeforeExit] Optional async cleanup called before process.exit(0)
|
|
58
|
+
*/
|
|
59
|
+
async function startRepl(session, initialState, onBeforeExit) {
|
|
60
|
+
let { position, source } = initialState;
|
|
61
|
+
let variables = [];
|
|
62
|
+
let exitCleanupDone = false;
|
|
63
|
+
async function runExitCleanup() {
|
|
64
|
+
if (exitCleanupDone) return;
|
|
65
|
+
exitCleanupDone = true;
|
|
66
|
+
if (onBeforeExit) {
|
|
67
|
+
try { await onBeforeExit(); } catch (e) { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
variables = await session.getVariables();
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// Best-effort
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
renderState(position, source, variables);
|
|
78
|
+
|
|
79
|
+
const rl = readline.createInterface({
|
|
80
|
+
input: process.stdin,
|
|
81
|
+
output: process.stdout,
|
|
82
|
+
prompt: '\n debug> '
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
rl.prompt();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns true when the step result contains a meaningful program position.
|
|
89
|
+
* After `continue` (or a step that runs off the end of a method), ADT returns
|
|
90
|
+
* an empty stack, leaving position.class / .method / .program all undefined.
|
|
91
|
+
* That signals the debuggee has completed — no further stepping is possible.
|
|
92
|
+
*/
|
|
93
|
+
function _hasPosition(pos) {
|
|
94
|
+
return pos && (pos.class || pos.method || pos.program);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Common handler for step results.
|
|
99
|
+
* Returns true if the session ended (caller should close the REPL).
|
|
100
|
+
*/
|
|
101
|
+
async function _handleStepResult(result) {
|
|
102
|
+
position = result.position;
|
|
103
|
+
source = result.source;
|
|
104
|
+
if (!_hasPosition(position)) {
|
|
105
|
+
console.log('\n Execution completed — no active breakpoint. Debug session ended.\n');
|
|
106
|
+
// Program already finished — nothing to terminate or detach; just close.
|
|
107
|
+
rl.close();
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
variables = await session.getVariables().catch(() => []);
|
|
111
|
+
renderState(position, source, variables);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
rl.on('line', async (line) => {
|
|
116
|
+
const cmd = line.trim().toLowerCase();
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
if (cmd === 's' || cmd === 'step') {
|
|
120
|
+
if (await _handleStepResult(await session.step('stepInto'))) return;
|
|
121
|
+
|
|
122
|
+
} else if (cmd === 'n' || cmd === 'next') {
|
|
123
|
+
if (await _handleStepResult(await session.step('stepOver'))) return;
|
|
124
|
+
|
|
125
|
+
} else if (cmd === 'o' || cmd === 'out') {
|
|
126
|
+
if (await _handleStepResult(await session.step('stepOut'))) return;
|
|
127
|
+
|
|
128
|
+
} else if (cmd === 'c' || cmd === 'continue') {
|
|
129
|
+
console.log('\n Continuing execution...');
|
|
130
|
+
if (await _handleStepResult(await session.step('continue'))) return;
|
|
131
|
+
|
|
132
|
+
} else if (cmd === 'v' || cmd === 'vars') {
|
|
133
|
+
variables = await session.getVariables();
|
|
134
|
+
printVarList(variables);
|
|
135
|
+
|
|
136
|
+
} else if (cmd.startsWith('x ') || cmd.startsWith('expand ')) {
|
|
137
|
+
const varExpr = cmd.replace(/^(x|expand)\s+/i, '').trim().toUpperCase();
|
|
138
|
+
if (!varExpr) {
|
|
139
|
+
console.log(' Usage: x <variable-name> or x <parent>-><child>');
|
|
140
|
+
} else {
|
|
141
|
+
// Normalize ABAP-style field accessors to use -> separator.
|
|
142
|
+
const normalizedExpr = varExpr
|
|
143
|
+
.replace(/\](-(?!>))/g, ']->') // [N]-FIELD → [N]->FIELD
|
|
144
|
+
.replace(/\*(-(?!>))/g, '*->'); // *-FIELD → *->FIELD
|
|
145
|
+
const pathParts = normalizedExpr.split('->').map(s => s.replace(/^-+|-+$/g, '').trim()).filter(Boolean);
|
|
146
|
+
|
|
147
|
+
if (pathParts.length > 1) {
|
|
148
|
+
// Multi-segment path — use session.expandPath
|
|
149
|
+
try {
|
|
150
|
+
const { variable: target, children } = await session.expandPath(pathParts);
|
|
151
|
+
_renderChildren(varExpr, target, children);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.log(`\n Error: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
// Single segment — find in last-fetched vars list
|
|
157
|
+
const target = variables.find(v => v.name.toUpperCase() === varExpr);
|
|
158
|
+
if (!target) {
|
|
159
|
+
console.log(`\n Variable '${varExpr}' not found. Run 'v' to list variables.`);
|
|
160
|
+
} else if (!target.id) {
|
|
161
|
+
console.log(`\n Variable '${varExpr}' has no ADT ID — cannot expand.`);
|
|
162
|
+
} else {
|
|
163
|
+
const meta = { metaType: target.metaType || '', tableLines: target.tableLines || 0 };
|
|
164
|
+
const children = await session.getVariableChildren(target.id, meta);
|
|
165
|
+
_renderChildren(varExpr, target, children);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
} else if (cmd === 'bt' || cmd === 'stack') {
|
|
172
|
+
const frames = await session.getStack();
|
|
173
|
+
console.log('\n Call Stack:');
|
|
174
|
+
frames.forEach(({ frame, class: cls, method, line }) => {
|
|
175
|
+
const loc = cls ? `${cls}->${method}` : method;
|
|
176
|
+
console.log(` ${String(frame).padStart(3)} ${loc} (line ${line})`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
} else if (cmd === 'q' || cmd === 'quit') {
|
|
180
|
+
console.log('\n Detaching debugger — program will continue running...');
|
|
181
|
+
try {
|
|
182
|
+
await session.detach();
|
|
183
|
+
} catch (e) {
|
|
184
|
+
// Ignore detach errors
|
|
185
|
+
}
|
|
186
|
+
// Clear state AFTER detach so session 2 (takeover) exits via state-file check.
|
|
187
|
+
await runExitCleanup();
|
|
188
|
+
rl.close();
|
|
189
|
+
return;
|
|
190
|
+
|
|
191
|
+
} else if (cmd === 'kill') {
|
|
192
|
+
console.log('\n Terminating program (hard abort)...');
|
|
193
|
+
try {
|
|
194
|
+
await session.terminate();
|
|
195
|
+
} catch (e) {
|
|
196
|
+
// Ignore terminate errors
|
|
197
|
+
}
|
|
198
|
+
await runExitCleanup();
|
|
199
|
+
rl.close();
|
|
200
|
+
return;
|
|
201
|
+
|
|
202
|
+
} else if (cmd === 'h' || cmd === 'help' || cmd === '?') {
|
|
203
|
+
console.log(HELP);
|
|
204
|
+
|
|
205
|
+
} else if (cmd !== '') {
|
|
206
|
+
console.log(` Unknown command: ${cmd}. Type 'h' for help.`);
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.error(`\n Error: ${err.message || JSON.stringify(err)}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
rl.prompt();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
rl.on('close', async () => {
|
|
216
|
+
await runExitCleanup();
|
|
217
|
+
process.exit(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
rl.on('close', resolve);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Render the children of an expand operation.
|
|
227
|
+
* Tables show a row-count hint and a path hint for further expansion.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} expr - The expression the user typed (e.g. 'LO_FACTORY->MT_COMMAND_MAP')
|
|
230
|
+
* @param {object} variable - The expanded variable metadata
|
|
231
|
+
* @param {Array} children - The children returned by getVariableChildren / expandPath
|
|
232
|
+
*/
|
|
233
|
+
function _renderChildren(expr, variable, children) {
|
|
234
|
+
if (children.length === 0) {
|
|
235
|
+
console.log(`\n ${expr} — no children (scalar or empty).`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Compute column widths from actual data (min 4/4, capped at 50/25).
|
|
240
|
+
const nameW = Math.min(50, Math.max(4, ...children.map(c => (c.name || '').length)));
|
|
241
|
+
const typeW = Math.min(25, Math.max(4, ...children.map(c => (c.type || c.metaType || '').length)));
|
|
242
|
+
|
|
243
|
+
console.log(`\n ${expr} (${(variable.type || variable.metaType || '?').toLowerCase()}):`);
|
|
244
|
+
console.log(' ' + 'Name'.padEnd(nameW + 2) + 'Type'.padEnd(typeW + 2) + 'Value');
|
|
245
|
+
console.log(' ' + '-'.repeat(nameW + typeW + 24));
|
|
246
|
+
children.forEach(({ name, type, value, metaType, tableLines }) => {
|
|
247
|
+
const displayType = (type || metaType || '').toLowerCase().slice(0, typeW);
|
|
248
|
+
const displayName = name.toLowerCase();
|
|
249
|
+
const displayValue = metaType === 'table'
|
|
250
|
+
? `[${tableLines} rows] — use 'x ${expr}->${displayName}' to expand`
|
|
251
|
+
: value;
|
|
252
|
+
console.log(' ' + displayName.padEnd(nameW + 2) + displayType.padEnd(typeW + 2) + displayValue);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = { startRepl, renderState };
|