abapgit-agent 1.13.7 → 1.14.1

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 CHANGED
@@ -97,14 +97,17 @@ abapgit-agent health # Verify ABAP connection
97
97
 
98
98
  ```bash
99
99
  # Install dependencies
100
- cd abapgit-agent
101
100
  npm install
102
101
 
103
- # Run from package directory (auto-detects from git)
104
- node bin/abapgit-agent pull
102
+ # Run unit tests (no ABAP system needed)
103
+ npm test
105
104
 
106
- # Or use npm script
107
- npm run pull -- --url <git-url> --branch main
105
+ # Test a command manually
106
+ node bin/abapgit-agent --help
107
+ node bin/abapgit-agent syntax --files src/zcl_my_class.clas.abap
108
+
109
+ # Run integration tests against a real ABAP system (requires .abapGitAgent)
110
+ npm run test:integration
108
111
  ```
109
112
 
110
113
  ## Documentation
package/abap/CLAUDE.md CHANGED
@@ -471,7 +471,7 @@ Run abaplint as step 4b — after `syntax`, before `git commit`:
471
471
 
472
472
  ```bash
473
473
  # Only if .abaplint.json exists
474
- npx @abaplint/cli .abaplint.json
474
+ abapgit-agent lint
475
475
  ```
476
476
 
477
477
  Fix any reported issues, then commit.
@@ -539,6 +539,21 @@ See **AI Tool Guidelines** below for how to react to each setting.
539
539
  ### Branch Workflow (`"mode": "branch"`)
540
540
 
541
541
  Always work on feature branches. Before every `pull`: rebase to default branch. On completion: create PR with squash merge.
542
+
543
+ ```bash
544
+ git checkout main # or master/develop (auto-detected)
545
+ git pull origin main
546
+ git checkout -b feature/my-change
547
+ # edit your ABAP file (name from objects.local.md)
548
+ abapgit-agent syntax --files src/<name>.clas.abap
549
+ ls .abaplint.json 2>/dev/null && abapgit-agent lint # abaplint (if configured)
550
+ git add . && git commit -m "feat: description"
551
+ git push origin feature/my-change
552
+ git fetch origin main && git rebase origin/main
553
+ git push origin feature/my-change --force-with-lease
554
+ abapgit-agent pull --files src/<name>.clas.abap --sync-xml
555
+ ```
556
+
542
557
  → See `guidelines/branch-workflow.md` — run: `abapgit-agent ref --topic branch-workflow`
543
558
 
544
559
  ### Trunk Workflow (`"mode": "trunk"`)
@@ -550,6 +565,7 @@ git checkout main # or master/develop (auto-detected)
550
565
  git pull origin main
551
566
  # edit your ABAP file (name from objects.local.md)
552
567
  abapgit-agent syntax --files src/<name>.clas.abap
568
+ ls .abaplint.json 2>/dev/null && abapgit-agent lint # abaplint (if configured)
553
569
  git add . && git commit -m "feat: description"
554
570
  git push origin main
555
571
  abapgit-agent pull --files src/<name>.clas.abap --sync-xml
@@ -649,7 +665,7 @@ Modified ABAP files?
649
665
  └─ FUGR and other complex objects?
650
666
  └─ ✅ Use: skip syntax → [abaplint] → commit → push → pull --sync-xml → (if errors: inspect)
651
667
 
652
- [abaplint] = run npx @abaplint/cli .abaplint.json only if .abaplint.json exists in repo root
668
+ [abaplint] = run abapgit-agent lint only if .abaplint.json exists in repo root
653
669
  before applying any quickfix: run abapgit-agent ref --topic abaplint
654
670
  ```
655
671
 
@@ -9,7 +9,9 @@ grand_parent: ABAP Development
9
9
  # abaplint Rule Guidelines
10
10
 
11
11
  **Searchable keywords**: abaplint, prefer_inline, inline declaration, char literal, string truncation,
12
- no_inline_in_optional_branches, fully_type_constants, linting, static analysis
12
+ no_inline_in_optional_branches, fully_type_constants, linting, static analysis,
13
+ run abaplint locally, check changed file, abapgit-agent lint,
14
+ keyword_case, sequential_blank, double_space, use_new, local_variable_names
13
15
 
14
16
  This file covers rules that have **non-obvious or dangerous implications** — cases where applying
15
17
  a rule mechanically (or accepting its quickfix) can introduce subtle bugs.
@@ -166,8 +168,57 @@ on rv_result — no intermediate lv_response variable at all.
166
168
 
167
169
  ---
168
170
 
171
+ ## Running abaplint Locally Against Changed Files
172
+
173
+ Run this before pushing to catch issues early, matching what CI does.
174
+
175
+ ```bash
176
+ abapgit-agent lint
177
+ ```
178
+
179
+ This automatically detects changed `.abap` files (via `git diff`), creates a scoped
180
+ abaplint config for just those files, runs the check, and cleans up.
181
+
182
+ ### Options
183
+
184
+ ```bash
185
+ # Diff against a specific base branch (useful on a feature branch)
186
+ abapgit-agent lint --base main
187
+
188
+ # Check specific files explicitly
189
+ abapgit-agent lint --files src/zcl_foo.clas.abap,src/zcl_foo.clas.testclasses.abap
190
+
191
+ # Use a different abaplint config (default: .abaplint.json)
192
+ abapgit-agent lint --config .abaplint.json
193
+ ```
194
+
195
+ Run repeatedly after each fix until you see:
196
+
197
+ ```
198
+ abaplint: 0 issue(s) found, 1 file(s) analyzed
199
+ ```
200
+
201
+ ---
202
+
203
+ ### Common Issues and Fixes
204
+
205
+ | Rule | Error message | Fix |
206
+ |------|--------------|-----|
207
+ | `keyword_case` | `Keyword should be upper case: "class"` | Uppercase the keyword: `CLASS` |
208
+ | `sequential_blank` | `Remove sequential blank lines` | Max 1 blank line between blocks |
209
+ | `local_variable_names` | `<fs_data> does not match pattern` | Use `l`-prefixed name: `<ls_data>` |
210
+ | `double_space` | `Remove double space` | Single space around `=` in parameters |
211
+ | `use_new` | `Use NEW #() to instantiate` | Replace `CREATE OBJECT mo_foo` → `mo_foo = NEW #( )` |
212
+ | `method_parameter_names` | `Parameter name does not match pattern` | Use `iv_`, `it_`, `is_`, `io_` etc. prefixes |
213
+
214
+ See **abaplint-local.md** for the full naming convention prefix reference.
215
+
216
+ ---
217
+
218
+
169
219
  ## See Also
170
220
 
171
221
  - **common-errors.md** — char-literal truncation listed as a known error pattern
172
222
  - **json.md** — safe patterns for building JSON strings in ABAP
173
223
  - **workflow-detailed.md** — where abaplint fits in the development workflow
224
+ - **abaplint-local.md** — naming convention reference (prefixes for variables, parameters, field-symbols)
@@ -21,6 +21,7 @@ edit src/zcl_auth_handler.clas.abap
21
21
 
22
22
  # 3. Check syntax (CLAS/INTF/PROG/DDLS only, if independent)
23
23
  abapgit-agent syntax --files src/zcl_auth_handler.clas.abap
24
+ ls .abaplint.json 2>/dev/null && abapgit-agent lint # abaplint (if configured)
24
25
 
25
26
  # 4. Commit
26
27
  git add src/zcl_auth_handler.clas.abap
@@ -72,10 +73,47 @@ abapgit-agent pull --files src/zcl_auth_handler.clas.abap
72
73
  abapgit-agent unit --files src/zcl_auth_handler.clas.testclasses.abap
73
74
 
74
75
  # 3. Create PR (squash merge enabled on GitHub/GitLab)
75
- # Go to GitHub and create PR from feature/user-authentication to main
76
- # Select "Squash and merge" option to combine all commits into one
76
+ # See "Creating a PR" section below for transport handling
77
77
  ```
78
78
 
79
+ #### Creating a PR
80
+
81
+ When creating a PR with `gh pr create`:
82
+
83
+ **Check `.abapgit-agent.json` for `transports.hook`.**
84
+
85
+ ```
86
+ transports.hook present?
87
+ ├─ NO → create PR normally, no transport line needed. STOP here.
88
+ └─ YES → continue below
89
+ ```
90
+
91
+ **If hook is configured — resolve which transport to include:**
92
+
93
+ ```bash
94
+ abapgit-agent transport list --scope task --json
95
+ ```
96
+
97
+ - **One result** → use it directly, no need to ask the user
98
+ - **Multiple results** → show the list to the user and ask which one to use for this PR
99
+ - **No results** → ask the user to provide a transport number manually, or confirm to skip
100
+
101
+ **Append `transport: <number>` as the last line of the PR body:**
102
+
103
+ ```
104
+ ## Summary
105
+ - Implemented user authentication handler
106
+
107
+ ## Changed Objects
108
+ - ZCL_AUTH_HANDLER (CLAS)
109
+
110
+ transport: DEVK900001
111
+ ```
112
+
113
+ **Why:** The CI pipeline reads this line from the PR description and uses it for the
114
+ Activate stage, overriding the automatic hook selection. This lets the user control
115
+ which transport request receives the activated changes.
116
+
79
117
  #### Why Rebase Before Pull?
80
118
 
81
119
  ABAP is a **centralized system**. Multiple developers may modify the same files:
@@ -112,6 +150,7 @@ git checkout main && git pull origin main
112
150
  git checkout -b feature/user-authentication
113
151
  edit src/zcl_auth_handler.clas.abap
114
152
  abapgit-agent syntax --files src/zcl_auth_handler.clas.abap
153
+ ls .abaplint.json 2>/dev/null && abapgit-agent lint # abaplint (if configured)
115
154
  git add . && git commit -m "wip: add basic auth logic"
116
155
  git push origin feature/user-authentication
117
156
  git fetch origin main && git rebase origin/main
@@ -123,6 +162,8 @@ git fetch origin main && git rebase origin/main
123
162
  # If conflicts: resolve, git add, git rebase --continue
124
163
  git push origin feature/user-authentication --force-with-lease
125
164
  edit src/zcl_auth_handler.clas.abap
165
+ abapgit-agent syntax --files src/zcl_auth_handler.clas.abap
166
+ ls .abaplint.json 2>/dev/null && abapgit-agent lint # abaplint (if configured)
126
167
  git add . && git commit -m "feat: complete auth logic"
127
168
  git push origin feature/user-authentication
128
169
  git fetch origin main && git rebase origin/main
@@ -133,5 +174,6 @@ abapgit-agent pull --files src/zcl_auth_handler.clas.abap
133
174
  abapgit-agent unit --files src/zcl_auth_handler.clas.testclasses.abap
134
175
  git fetch origin main && git rebase origin/main
135
176
  git push origin feature/user-authentication --force-with-lease
136
- # Create PR on GitHub/GitLab (squash 3 commits into 1)
177
+ # Create PR: check transport hook, resolve transport, include in PR body
178
+ # abapgit-agent transport list --scope task --json → pick transport → append to PR body
137
179
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.13.7",
3
+ "version": "1.14.1",
4
4
  "description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
5
5
  "files": [
6
6
  "bin/",
@@ -29,5 +29,6 @@ module.exports = {
29
29
  init: require('./init'),
30
30
  pull: require('./pull'),
31
31
  upgrade: require('./upgrade'),
32
- transport: require('./transport')
32
+ transport: require('./transport'),
33
+ lint: require('./lint')
33
34
  };
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * lint command - Run abaplint on changed ABAP files
5
+ *
6
+ * Detects files changed relative to a base branch (or HEAD~1),
7
+ * creates a scoped abaplint config for just those files, and runs the check.
8
+ *
9
+ * Usage:
10
+ * abapgit-agent lint
11
+ * abapgit-agent lint --config .abaplint.json
12
+ * abapgit-agent lint --base main
13
+ * abapgit-agent lint --files src/foo.clas.abap,src/foo.clas.testclasses.abap
14
+ * abapgit-agent lint --outformat checkstyle --outfile reports/abaplint-results.xml
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { execSync, spawnSync } = require('child_process');
20
+
21
+ module.exports = {
22
+ name: 'lint',
23
+ description: 'Run abaplint on changed ABAP files',
24
+ requiresAbapConfig: false,
25
+
26
+ execute(args) {
27
+ const configPath = argValue(args, '--config') || '.abaplint.json';
28
+ const baseBranch = argValue(args, '--base');
29
+ const filesArg = argValue(args, '--files');
30
+ const outformat = argValue(args, '--outformat');
31
+ const outfile = argValue(args, '--outfile');
32
+
33
+ // ── Resolve changed files ─────────────────────────────────────────────────
34
+ let abapFiles;
35
+ if (filesArg) {
36
+ abapFiles = filesArg.split(',').map(f => f.trim()).filter(f => f.endsWith('.abap'));
37
+ } else {
38
+ abapFiles = detectChangedAbapFiles(baseBranch);
39
+ }
40
+
41
+ if (abapFiles.length === 0) {
42
+ console.log('No changed .abap files found — nothing to lint.');
43
+ return;
44
+ }
45
+
46
+ if (!outfile) {
47
+ console.log(`\nLinting ${abapFiles.length} file(s):`);
48
+ abapFiles.forEach(f => console.log(` ${f}`));
49
+ console.log('');
50
+ }
51
+
52
+ // ── Load and scope the abaplint config ────────────────────────────────────
53
+ if (!fs.existsSync(configPath)) {
54
+ console.error(`Error: abaplint config not found: ${configPath}`);
55
+ console.error('Run from the project root, or pass --config <path>.');
56
+ process.exit(1);
57
+ }
58
+
59
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
60
+
61
+ // Scope to changed files + their direct dependencies (interfaces, superclasses)
62
+ // so abaplint can resolve cross-references without including the whole repo.
63
+ const abapDir = cfg.global.files.replace(/\/\*\*.*$/, '').replace(/^\//, '') || 'abap';
64
+ const depFiles = resolveDependencies(abapFiles, abapDir);
65
+ const allFiles = [...new Set([...abapFiles, ...depFiles])];
66
+ cfg.global.files = allFiles.map(f => `/${f}`);
67
+
68
+ const scopedConfig = '.abaplint-local.json';
69
+ fs.writeFileSync(scopedConfig, JSON.stringify(cfg, null, 2));
70
+
71
+ // ── Run abaplint ──────────────────────────────────────────────────────────
72
+ try {
73
+ const formatArgs = outformat ? `--outformat ${outformat}` : '';
74
+ const fileArgs = outfile ? `--outfile ${outfile}` : '';
75
+ const result = spawnSync(
76
+ `npx @abaplint/cli@latest ${scopedConfig} ${formatArgs} ${fileArgs}`,
77
+ { stdio: 'inherit', shell: true }
78
+ );
79
+ if (result.status !== 0) {
80
+ process.exitCode = result.status;
81
+ }
82
+ } finally {
83
+ fs.unlinkSync(scopedConfig);
84
+ }
85
+ }
86
+ };
87
+
88
+ // ── Helpers ───────────────────────────────────────────────────────────────────
89
+
90
+ function argValue(args, flag) {
91
+ const idx = args.indexOf(flag);
92
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
93
+ }
94
+
95
+ /**
96
+ * Detect changed .abap files using git diff.
97
+ * - If on a PR branch (CHANGE_TARGET set, e.g. in CI): diffs against that target.
98
+ * - If --base is given: diffs against that branch.
99
+ * - Otherwise: diffs HEAD~1..HEAD (last commit).
100
+ */
101
+ function detectChangedAbapFiles(baseBranch) {
102
+ const base = baseBranch
103
+ || (process.env.CHANGE_TARGET ? `origin/${process.env.CHANGE_TARGET}` : null);
104
+
105
+ let diffCmd;
106
+ if (base) {
107
+ diffCmd = `git diff --name-only ${base}...HEAD -- '*.abap'`;
108
+ } else {
109
+ // Fall back to uncommitted changes first, then last commit
110
+ const uncommitted = runGit('git diff --name-only HEAD -- *.abap').filter(Boolean);
111
+ if (uncommitted.length > 0) return filterAbapFiles(uncommitted);
112
+ diffCmd = `git diff --name-only HEAD~1 HEAD -- '*.abap'`;
113
+ }
114
+
115
+ return filterAbapFiles(runGit(diffCmd));
116
+ }
117
+
118
+ function runGit(cmd) {
119
+ try {
120
+ return execSync(cmd, { encoding: 'utf8' }).trim().split('\n').filter(Boolean);
121
+ } catch {
122
+ return [];
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Resolve direct dependencies of the given ABAP files by scanning their source
128
+ * for interface/superclass/type references and mapping them to local files.
129
+ *
130
+ * Handles:
131
+ * INTERFACES <name>. → <name>.intf.abap + <name>.intf.xml
132
+ * INHERITING FROM <name> → <name>.clas.abap + <name>.clas.xml
133
+ * TYPE REF TO <name> → <name>.intf.abap or <name>.clas.abap (whichever exists)
134
+ *
135
+ * Only resolves one level deep — enough for abaplint to check the changed files.
136
+ * XML companion files are always included alongside their .abap counterpart
137
+ * so xml_consistency checks can run.
138
+ */
139
+ function resolveDependencies(abapFiles, abapDir) {
140
+ const deps = new Set();
141
+ const visited = new Set(abapFiles); // don't re-scan changed files as deps
142
+
143
+ // Patterns to extract referenced object names from ABAP source
144
+ const patterns = [
145
+ /^\s*INTERFACES\s+(\w+)\s*\./gim,
146
+ /INHERITING\s+FROM\s+(\w+)/gim,
147
+ /TYPE\s+REF\s+TO\s+(\w+)/gim,
148
+ ];
149
+
150
+ // BFS queue — seed with the changed files, then expand transitively
151
+ const queue = [...abapFiles];
152
+
153
+ while (queue.length > 0) {
154
+ const file = queue.shift();
155
+
156
+ let source;
157
+ try { source = fs.readFileSync(file, 'utf8'); } catch { continue; }
158
+
159
+ for (const pattern of patterns) {
160
+ pattern.lastIndex = 0;
161
+ let match;
162
+ while ((match = pattern.exec(source)) !== null) {
163
+ const name = match[1].toLowerCase();
164
+ for (const suffix of [`${name}.intf`, `${name}.clas`]) {
165
+ const abapFile = path.join(abapDir, `${suffix}.abap`);
166
+ const xmlFile = path.join(abapDir, `${suffix}.xml`);
167
+ if (fs.existsSync(abapFile)) {
168
+ deps.add(abapFile);
169
+ if (fs.existsSync(xmlFile)) deps.add(xmlFile);
170
+ // Recurse into this dep if not yet visited
171
+ if (!visited.has(abapFile)) {
172
+ visited.add(abapFile);
173
+ queue.push(abapFile);
174
+ }
175
+ break; // intf matched — don't also try clas
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ // Always include the XML companion of each scanned file
182
+ const xmlCompanion = file.replace(/\.abap$/, '.xml');
183
+ if (fs.existsSync(xmlCompanion)) deps.add(xmlCompanion);
184
+ }
185
+
186
+ return [...deps];
187
+ }
188
+
189
+ /**
190
+ * Keep only files that look like ABAP source files
191
+ * (name.type.abap or name.type.subtype.abap).
192
+ */
193
+ function filterAbapFiles(files) {
194
+ return files.filter(f => {
195
+ const parts = path.basename(f).split('.');
196
+ return (parts.length === 3 || parts.length === 4) &&
197
+ parts[parts.length - 1].toLowerCase() === 'abap';
198
+ });
199
+ }