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.
@@ -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 };