abapgit-agent 1.10.0 → 1.11.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/abap/CLAUDE.md CHANGED
@@ -308,32 +308,26 @@ abapgit-agent debug list # confirm it was registered
308
308
 
309
309
  **Step 2 — attach and trigger**
310
310
 
311
- Two modes depending on context:
311
+ Best practice: individual sequential calls. Once the daemon is running and
312
+ the session is saved to the state file, each `vars/stack/step` command is a
313
+ plain standalone call — no `--session` flag needed.
312
314
 
313
- *Interactive (human in a terminal):*
314
315
  ```bash
315
- # Terminal 1 attach (blocks waiting for breakpoint, opens REPL on hit)
316
- abapgit-agent debug attach
317
-
318
- # Terminal 2 — trigger (any command that calls the backend)
319
- abapgit-agent unit --files src/zcl_my_class.clas.testclasses.abap
320
- abapgit-agent inspect --files src/zcl_my_class.clas.abap
321
- ```
322
-
323
- *Scripted (AI / automation) — best practice: individual sequential calls:*
324
-
325
- Once the daemon is running and the session is saved to the state file, each
326
- `vars/stack/step` command is a plain standalone call — no bash script needed.
327
-
328
- ```bash
329
- # Step 1: start attach listener in background (spawns a daemon, saves session to state file)
316
+ # Start attach listener in background (spawns a daemon, saves session to state file)
330
317
  abapgit-agent debug attach --json > /tmp/attach.json 2>&1 &
331
- sleep 2
332
318
 
333
- # Step 2: trigger in background MUST stay alive for the whole session
319
+ # Rule 1: wait for "Listener active" in the output, THEN fire the trigger.
320
+ # attach --json prints "Listener active" to stderr (captured in attach.json) the
321
+ # moment the long-poll POST is about to be sent to ADT. Waiting for this marker
322
+ # is reliable under any system load; a blind sleep may fire the trigger before
323
+ # ADT has a registered listener, causing the breakpoint hit to be missed.
324
+ until grep -q "Listener active" /tmp/attach.json 2>/dev/null; do sleep 0.3; done
325
+ sleep 1 # brief extra window for the POST to reach ADT
326
+
327
+ # Trigger in background — MUST stay alive for the whole session
334
328
  abapgit-agent unit --files src/zcl_my_class.clas.testclasses.abap > /tmp/trigger.json 2>&1 &
335
329
 
336
- # Step 3: poll until breakpoint fires and session JSON appears in attach output
330
+ # Poll until breakpoint fires and session JSON appears in attach output
337
331
  SESSION=""
338
332
  for i in $(seq 1 30); do
339
333
  sleep 0.5
@@ -341,40 +335,29 @@ for i in $(seq 1 30); do
341
335
  [ -n "$SESSION" ] && break
342
336
  done
343
337
 
344
- # Step 4: inspect and step — each is an individual call, no --session needed
338
+ # Inspect and step — each is an individual call, no --session needed
345
339
  abapgit-agent debug stack --json
346
340
  abapgit-agent debug vars --json
347
341
  abapgit-agent debug vars --expand LS_OBJECT --json
348
342
  abapgit-agent debug step --type over --json
349
343
  abapgit-agent debug vars --json
350
344
 
351
- # Step 5: ALWAYS release the ABAP work process before finishing
345
+ # ALWAYS release the ABAP work process before finishing
352
346
  abapgit-agent debug step --type continue --json
353
347
 
354
- # Step 6: check trigger result
348
+ # Check trigger result
355
349
  cat /tmp/trigger.json
356
350
  rm -f /tmp/attach.json /tmp/trigger.json
357
351
  ```
358
352
 
359
353
  > **Four rules for scripted mode:**
360
- > 1. `sleep 2` after starting attach — the listener must register on the server before the trigger fires
354
+ > 1. Wait for `"Listener active"` in the attach output before firing the trigger this message is printed to stderr (captured in `attach.json`) the moment the listener POST is about to reach ADT. A blind `sleep` is not reliable under system load.
361
355
  > 2. Keep the trigger process alive in the background for the entire session — if it exits, the ABAP work process is released and the session ends
362
356
  > 3. Always finish with `step --type continue` — this releases the frozen work process so the trigger can complete normally
363
357
  > 4. **Never pass `--session` to `step/vars/stack`** — it bypasses the daemon IPC and causes `noSessionAttached`. Omit it and let commands auto-load from the saved state file.
364
358
 
365
359
  **Step 3 — step through and inspect**
366
360
 
367
- *Interactive REPL commands (after `attach` without `--json`):*
368
- ```
369
- debug> v — show all variables
370
- debug> x LT_DATA — expand a table or structure
371
- debug> n — step over
372
- debug> s — step into
373
- debug> bt — call stack
374
- debug> q — detach (program continues); kill — hard abort
375
- ```
376
-
377
- *Scripted commands (after `attach --json`) — omit `--session`, commands auto-load from state file:*
378
361
  ```bash
379
362
  abapgit-agent debug vars --json # all variables
380
363
  abapgit-agent debug vars --name LV_RESULT --json # one variable
@@ -396,6 +379,24 @@ abapgit-agent debug delete --all
396
379
 
397
380
  This project's workflow mode is configured in `.abapGitAgent` under `workflow.mode`.
398
381
 
382
+ ### Project-Level Config (`.abapgit-agent.json`)
383
+
384
+ Checked into the repository — applies to all developers. **Read this file at the start of every session.**
385
+
386
+ | Setting | Values | Default | Effect |
387
+ |---------|--------|---------|--------|
388
+ | `safeguards.requireFilesForPull` | `true`/`false` | `false` | Requires `--files` on every pull |
389
+ | `safeguards.disablePull` | `true`/`false` | `false` | Disables pull entirely (CI/CD-only projects) |
390
+ | `conflictDetection.mode` | `"abort"`/`"ignore"` | `"abort"` | Whether to abort pull on conflict |
391
+ | `transports.hook.path` | string | `null` | Path to JS module that auto-selects a transport for pull |
392
+ | `transports.hook.description` | string | `null` | Optional label shown when the hook runs |
393
+ | `transports.allowCreate` | `true`/`false` | `true` | When `false`, `transport create` is blocked |
394
+ | `transports.allowRelease` | `true`/`false` | `true` | When `false`, `transport release` is blocked |
395
+
396
+ CLI `--conflict-mode` always overrides the project config for a single run.
397
+
398
+ See **AI Tool Guidelines** below for how to react to each setting.
399
+
399
400
  ### Workflow Modes
400
401
 
401
402
  | Mode | Branch Strategy | Rebase Before Pull | Create PR |
@@ -571,6 +572,37 @@ abapgit-agent pull --files src/zcl_auth_handler.clas.abap
571
572
  3. ✓ `git pull origin <default>` before push
572
573
  4. ✗ Don't create feature branches
573
574
 
575
+ **Read `.abapgit-agent.json` to determine project safeguards and conflict detection:**
576
+
577
+ **When `safeguards.requireFilesForPull = true`:**
578
+ 1. ✓ Always include `--files` in every `pull` command
579
+ 2. ✓ Never run `abapgit-agent pull` without `--files`
580
+ 3. ✗ Don't suggest or run a full pull without specifying files
581
+
582
+ **When `safeguards.requireFilesForPull = false` or not set:**
583
+ 1. ✓ `--files` is optional — use it for speed, omit for full pull
584
+
585
+ **When `safeguards.disablePull = true`:**
586
+ 1. ✗ Do not run `abapgit-agent pull` at all
587
+ 2. ✓ Inform the user that pull is disabled for this project (CI/CD only)
588
+
589
+ **When `conflictDetection.mode = "ignore"` or not set:**
590
+ 1. ✓ Run `pull` normally — no conflict flags needed
591
+ 2. ✗ Don't add `--conflict-mode` unless user explicitly asks
592
+
593
+ **When `conflictDetection.mode = "abort"`:**
594
+ 1. ✓ Conflict detection is active — pull aborts if ABAP system was edited since last pull
595
+ 2. ✓ If pull is aborted with conflict error, inform user and suggest `--conflict-mode ignore` to override for that run
596
+ 3. ✗ Don't silently add `--conflict-mode ignore` — always tell the user about the conflict
597
+
598
+ **When `transports.allowCreate = false`:**
599
+ 1. ✗ Do not run `abapgit-agent transport create`
600
+ 2. ✓ Inform the user that transport creation is disabled for this project
601
+
602
+ **When `transports.allowRelease = false`:**
603
+ 1. ✗ Do not run `abapgit-agent transport release`
604
+ 2. ✓ Inform the user that transport release is disabled for this project
605
+
574
606
  ---
575
607
 
576
608
  ## Development Workflow (Detailed)
@@ -600,6 +632,7 @@ abapgit-agent pull --files src/zcl_auth_handler.clas.abap
600
632
 
601
633
 
602
634
  6. Activate → abapgit-agent pull --files src/file.clas.abap
635
+ │ (behaviour depends on .abapgit-agent.json — see AI Tool Guidelines)
603
636
 
604
637
 
605
638
  7. Verify → Check pull output
@@ -791,6 +824,9 @@ abapgit-agent syntax --files src/zcl_class1.clas.abap,src/zif_intf1.intf.abap,sr
791
824
  # 2. Pull/activate AFTER pushing to git
792
825
  abapgit-agent pull --files src/zcl_class1.clas.abap,src/zcl_class2.clas.abap
793
826
 
827
+ # Override conflict detection for a single pull (e.g. deliberate branch switch)
828
+ abapgit-agent pull --files src/zcl_class1.clas.abap --conflict-mode ignore
829
+
794
830
  # 3. Inspect AFTER pull (for errors or unsupported types)
795
831
  abapgit-agent inspect --files src/zcl_class1.clas.abap
796
832
 
package/bin/abapgit-agent CHANGED
@@ -24,7 +24,7 @@ const gitUtils = require('../src/utils/git-utils');
24
24
  const versionCheck = require('../src/utils/version-check');
25
25
  const validators = require('../src/utils/validators');
26
26
  const { AbapHttp } = require('../src/utils/abap-http');
27
- const { loadConfig, getTransport, isAbapIntegrationEnabled, getSafeguards } = require('../src/config');
27
+ const { loadConfig, getTransport, isAbapIntegrationEnabled, getSafeguards, getConflictSettings, getTransportSettings } = require('../src/config');
28
28
 
29
29
  // Get terminal width for responsive table
30
30
  const getTermWidth = () => process.stdout.columns || 80;
@@ -55,7 +55,8 @@ async function main() {
55
55
  ref: require('../src/commands/ref'),
56
56
  init: require('../src/commands/init'),
57
57
  pull: require('../src/commands/pull'),
58
- upgrade: require('../src/commands/upgrade')
58
+ upgrade: require('../src/commands/upgrade'),
59
+ transport: require('../src/commands/transport')
59
60
  };
60
61
 
61
62
  // Check if this is a modular command
@@ -102,7 +103,9 @@ To enable integration:
102
103
  loadConfig,
103
104
  AbapHttp,
104
105
  getTransport,
105
- getSafeguards
106
+ getSafeguards,
107
+ getConflictSettings,
108
+ getTransportSettings
106
109
  };
107
110
 
108
111
  // Execute command
@@ -121,4 +124,11 @@ To enable integration:
121
124
  process.exit(1);
122
125
  }
123
126
 
124
- main();
127
+ main().catch((err) => {
128
+ // Known CLI errors (e.g. pull conflict) already printed their own output — skip re-printing.
129
+ // For all other unexpected errors, print the message without the stack trace.
130
+ if (!err._isPullError && !err._isTransportError) {
131
+ console.error(err.message || err);
132
+ }
133
+ process.exit(1);
134
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -42,6 +42,8 @@
42
42
  "test:cmd:upgrade": "node tests/run-all.js --cmd --command=upgrade",
43
43
  "test:lifecycle": "node tests/run-all.js --lifecycle",
44
44
  "test:pull": "node tests/run-all.js --pull",
45
+ "test:full-pull": "node tests/run-all.js --full-pull",
46
+ "test:conflict": "node tests/run-all.js --conflict",
45
47
  "pull": "node bin/abapgit-agent",
46
48
  "release": "node scripts/release.js",
47
49
  "unrelease": "node scripts/unrelease.js"
@@ -583,7 +583,7 @@ async function cmdAttach(args, config, adt) {
583
583
  let firstPoll = true;
584
584
 
585
585
  for (let i = 0; i < (takenOver ? MAX_TAKEOVER_POLLS : MAX_POLLS); i++) {
586
- if (firstPoll && !jsonOutput) {
586
+ if (firstPoll) {
587
587
  process.stderr.write(' Listener active — trigger your ABAP program now.\n');
588
588
  firstPoll = false;
589
589
  }
@@ -806,7 +806,7 @@ async function cmdStep(args, config, adt) {
806
806
  process.exit(1);
807
807
  }
808
808
  if (!resp.ok) {
809
- console.error(`\n Error: ${resp.error}${resp.statusCode ? ` (HTTP ${resp.statusCode})` : ''}\n`);
809
+ console.error(`\n Error: ${resp.error}${resp.statusCode ? ` (HTTP ${resp.statusCode})` : ''}${resp.body ? '\n Body: ' + resp.body.substring(0, 400) : ''}\n`);
810
810
  process.exit(1);
811
811
  }
812
812
  // continued+finished (or empty position) means the program ran to completion
@@ -1074,7 +1074,7 @@ async function cmdStack(args, config, adt) {
1074
1074
  process.exit(1);
1075
1075
  }
1076
1076
  if (!resp.ok) {
1077
- console.error(`\n Error: ${resp.error}${resp.statusCode ? ` (HTTP ${resp.statusCode})` : ''}\n`);
1077
+ console.error(`\n Error: ${resp.error}${resp.statusCode ? ` (HTTP ${resp.statusCode})` : ''}${resp.body ? '\n Body: ' + resp.body.substring(0, 400) : ''}\n`);
1078
1078
  process.exit(1);
1079
1079
  }
1080
1080
  const frames = resp.frames;
@@ -9,7 +9,7 @@ module.exports = {
9
9
  requiresVersionCheck: true,
10
10
 
11
11
  async execute(args, context) {
12
- const { loadConfig, AbapHttp, gitUtils, getTransport, getSafeguards } = context;
12
+ const { loadConfig, AbapHttp, gitUtils, getTransport, getSafeguards, getConflictSettings, getTransportSettings } = context;
13
13
 
14
14
  // Check project-level safeguards
15
15
  const safeguards = getSafeguards();
@@ -29,14 +29,16 @@ module.exports = {
29
29
  const branchArgIndex = args.indexOf('--branch');
30
30
  const filesArgIndex = args.indexOf('--files');
31
31
  const transportArgIndex = args.indexOf('--transport');
32
+ const conflictModeArgIndex = args.indexOf('--conflict-mode');
32
33
  const jsonOutput = args.includes('--json');
33
34
 
34
35
  // Auto-detect git remote URL if not provided
35
36
  let gitUrl = urlArgIndex !== -1 ? args[urlArgIndex + 1] : null;
36
37
  let branch = branchArgIndex !== -1 ? args[branchArgIndex + 1] : gitUtils.getBranch();
37
38
  let files = null;
39
+ let conflictMode = conflictModeArgIndex !== -1 ? args[conflictModeArgIndex + 1] : getConflictSettings().mode;
38
40
 
39
- // Transport: CLI arg takes priority, then config/environment, then null
41
+ // Transport: CLI arg takes priority, then config/environment, then selector
40
42
  let transportRequest = null;
41
43
  if (transportArgIndex !== -1 && transportArgIndex + 1 < args.length) {
42
44
  // Explicit --transport argument
@@ -46,8 +48,39 @@ module.exports = {
46
48
  transportRequest = getTransport();
47
49
  }
48
50
 
51
+ // Auto-detect URL early (needed for transport_required check)
52
+ if (!gitUrl) {
53
+ gitUrl = gitUtils.getRemoteUrl();
54
+ if (!gitUrl) {
55
+ console.error('Error: Not in a git repository or no remote configured.');
56
+ console.error('Either run from a git repo, or specify --url <git-url>');
57
+ process.exit(1);
58
+ }
59
+ if (!jsonOutput) {
60
+ console.log(`📌 Auto-detected git remote: ${gitUrl}`);
61
+ }
62
+ }
63
+
49
64
  if (filesArgIndex !== -1 && filesArgIndex + 1 < args.length) {
50
65
  files = args[filesArgIndex + 1].split(',').map(f => f.trim());
66
+
67
+ // Validate that every file has a recognised ABAP source extension
68
+ // (.abap or .asddls) — XML metadata files must NOT be passed here
69
+ const ABAP_SOURCE_EXTS = new Set(['abap', 'asddls']);
70
+ const nonSourceFiles = files.filter(f => {
71
+ const base = f.split('/').pop(); // strip directory
72
+ const parts = base.split('.');
73
+ const ext = parts[parts.length - 1].toLowerCase();
74
+ return parts.length < 3 || !ABAP_SOURCE_EXTS.has(ext);
75
+ });
76
+ if (nonSourceFiles.length > 0) {
77
+ console.error('❌ Error: --files only accepts ABAP source files (.abap, .asddls).');
78
+ console.error(' The following file(s) are not ABAP source files:');
79
+ nonSourceFiles.forEach(f => console.error(` ${f}`));
80
+ console.error(' Tip: pass the source file, not the XML metadata file.');
81
+ console.error(' Example: --files src/zcl_my_class.clas.abap');
82
+ process.exit(1);
83
+ }
51
84
  }
52
85
 
53
86
  // SAFEGUARD 2: Check if files are required but not provided
@@ -62,22 +95,59 @@ module.exports = {
62
95
  process.exit(1);
63
96
  }
64
97
 
65
- if (!gitUrl) {
66
- gitUrl = gitUtils.getRemoteUrl();
67
- if (!gitUrl) {
68
- console.error('Error: Not in a git repository or no remote configured.');
69
- console.error('Either run from a git repo, or specify --url <git-url>');
70
- process.exit(1);
98
+ // Auto-select transport when none configured and not in JSON mode
99
+ if (!transportRequest && !jsonOutput) {
100
+ const { selectTransport, isNonInteractive, _getTransportHookConfig } = require('../utils/transport-selector');
101
+
102
+ // Check if this package requires a transport before showing the interactive
103
+ // picker. Skip the status round-trip in non-interactive mode — selectTransport
104
+ // already returns null there without prompting, so the extra HTTP call is wasted.
105
+ let transportRequired = true; // Safe default: assume transport needed
106
+ if (!isNonInteractive()) {
107
+ try {
108
+ const config = loadConfig();
109
+ const http = new AbapHttp(config);
110
+ const csrfToken = await http.fetchCsrfToken();
111
+ const statusResult = await http.post('/sap/bc/z_abapgit_agent/status', { url: gitUrl }, { csrfToken });
112
+ if (statusResult.transport_required === false || statusResult.transport_required === 'false') {
113
+ transportRequired = false;
114
+ }
115
+ } catch (e) {
116
+ // Status check failed — proceed with selector as safe default
117
+ }
71
118
  }
72
- if (!jsonOutput) {
73
- console.log(`📌 Auto-detected git remote: ${gitUrl}`);
119
+
120
+ if (transportRequired) {
121
+ const config = loadConfig();
122
+ const http = new AbapHttp(config);
123
+ transportRequest = await selectTransport(config, http, loadConfig, AbapHttp, getTransportSettings);
124
+
125
+ // If a hook was configured but returned no transport, handle based on context
126
+ if (transportRequest === null) {
127
+ const hookConfig = _getTransportHookConfig();
128
+ if (hookConfig && hookConfig.hook) {
129
+ if (isNonInteractive()) {
130
+ // Non-interactive (AI/CI): fail — a configured hook must return a transport
131
+ console.error('❌ Error: transport hook returned no transport request.');
132
+ console.error(` Hook: ${hookConfig.hook}`);
133
+ if (hookConfig.description) console.error(` ${hookConfig.description}`);
134
+ process.exit(1);
135
+ } else {
136
+ // Interactive (TTY): warn and fall through to the picker
137
+ process.stderr.write(`⚠️ Transport hook returned no transport request (${hookConfig.hook}).\n`);
138
+ process.stderr.write(' Please select one manually:\n');
139
+ const { interactivePicker } = require('../utils/transport-selector');
140
+ transportRequest = await interactivePicker(http);
141
+ }
142
+ }
143
+ }
74
144
  }
75
145
  }
76
146
 
77
- await this.pull(gitUrl, branch, files, transportRequest, loadConfig, AbapHttp, jsonOutput);
147
+ await this.pull(gitUrl, branch, files, transportRequest, loadConfig, AbapHttp, jsonOutput, undefined, conflictMode);
78
148
  },
79
149
 
80
- async pull(gitUrl, branch = 'main', files = null, transportRequest = null, loadConfig, AbapHttp, jsonOutput = false, gitCredentials = undefined) {
150
+ async pull(gitUrl, branch = 'main', files = null, transportRequest = null, loadConfig, AbapHttp, jsonOutput = false, gitCredentials = undefined, conflictMode = 'abort') {
81
151
  const TERM_WIDTH = process.stdout.columns || 80;
82
152
 
83
153
  if (!jsonOutput) {
@@ -107,6 +177,7 @@ module.exports = {
107
177
  const data = {
108
178
  url: gitUrl,
109
179
  branch: branch,
180
+ conflict_mode: conflictMode,
110
181
  ...(resolvedCredentials ? { username: resolvedCredentials.username, password: resolvedCredentials.password } : {})
111
182
  };
112
183
 
@@ -140,12 +211,23 @@ module.exports = {
140
211
  const jobId = result.JOB_ID || result.job_id;
141
212
  const message = result.MESSAGE || result.message;
142
213
  const errorDetail = result.ERROR_DETAIL || result.error_detail;
143
- const transportRequestUsed = result.TRANSPORT_REQUEST || result.transport_request;
144
214
  const activatedCount = result.ACTIVATED_COUNT || result.activated_count || 0;
145
215
  const failedCount = result.FAILED_COUNT || result.failed_count || 0;
146
216
  const logMessages = result.LOG_MESSAGES || result.log_messages || [];
147
217
  const activatedObjects = result.ACTIVATED_OBJECTS || result.activated_objects || [];
148
218
  const failedObjects = result.FAILED_OBJECTS || result.failed_objects || [];
219
+ const conflictReport = result.CONFLICT_REPORT || result.conflict_report || '';
220
+ const conflictCount = result.CONFLICT_COUNT || result.conflict_count || 0;
221
+
222
+ // --- Conflict report (pull was aborted) ---
223
+ if (conflictCount > 0 && conflictReport) {
224
+ console.error(`⚠️ Pull aborted — ${conflictCount} conflict(s) detected\n`);
225
+ console.error('─'.repeat(TERM_WIDTH));
226
+ console.error(conflictReport.replace(/\\n/g, '\n'));
227
+ const err = new Error(message || `Pull aborted — ${conflictCount} conflict(s) detected`);
228
+ err._isPullError = true;
229
+ throw err;
230
+ }
149
231
 
150
232
  // Icon mapping for message types
151
233
  const getIcon = (type) => {
@@ -32,7 +32,9 @@ module.exports = {
32
32
  console.log(' Repository: ✅ Created');
33
33
  const pkg = result.package || result.PACKAGE || 'N/A';
34
34
  const key = result.key || result.KEY || result.REPO_KEY || result.repo_key || 'N/A';
35
+ const transportRequired = result.transport_required === true || result.transport_required === 'true';
35
36
  console.log(` Package: ${pkg}`);
37
+ console.log(` Transport required: ${transportRequired ? 'Yes' : 'No'}`);
36
38
  console.log(` URL: ${repoUrl}`);
37
39
  console.log(` Key: ${key}`);
38
40
  } else {