abapgit-agent 1.17.2 → 1.17.4

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.
@@ -8,5 +8,12 @@
8
8
  "protocol": "https",
9
9
  "gitUsername": "github-username",
10
10
  "gitPassword": "github-token",
11
- "referenceFolder": "~/abap-reference"
11
+ "referenceFolder": "~/abap-reference",
12
+
13
+ "testRepos": {
14
+ "pull": "https://github.com/your-org/abgagt-pull-test.git",
15
+ "drop": "https://github.com/your-org/abgagt-drop-test.git",
16
+ "customize": "https://github.com/your-org/abgagt-customize-test.git",
17
+ "lifecycle": "https://github.com/your-org/abgagt-lifecycle-test.git"
18
+ }
12
19
  }
@@ -0,0 +1,19 @@
1
+ {
2
+ "project": {
3
+ "name": "MY_PACKAGE",
4
+ "description": ""
5
+ },
6
+
7
+ "safeguards": {
8
+ "requireFilesForPull": false,
9
+ "disablePull": false,
10
+ "disableRun": false,
11
+ "disableImport": false,
12
+ "requireImportMessage": false,
13
+ "disableProbeClasses": false
14
+ },
15
+
16
+ "conflictDetection": {
17
+ "mode": "abort"
18
+ }
19
+ }
package/README.md CHANGED
@@ -55,6 +55,7 @@ See [Creating New ABAP Projects](docs/install.md#creating-new-abap-projects) to
55
55
  abapgit-agent init --package ZMY_PACKAGE # Initialize local config
56
56
  abapgit-agent create # Create online repo in ABAP
57
57
  abapgit-agent import # Import objects from ABAP to git
58
+ abapgit-agent import --branch main # Import to specific branch
58
59
  abapgit-agent delete # Delete repo from ABAP
59
60
  ```
60
61
 
@@ -99,17 +100,30 @@ abapgit-agent health # Verify ABAP connection
99
100
  # Install dependencies
100
101
  npm install
101
102
 
102
- # Run unit tests (no ABAP system needed)
103
- npm test
104
-
105
103
  # Test a command manually
106
104
  node bin/abapgit-agent --help
107
105
  node bin/abapgit-agent syntax --files src/zcl_my_class.clas.abap
106
+ ```
108
107
 
109
- # Run integration tests against a real ABAP system (requires .abapGitAgent)
110
- npm run test:integration
108
+ ## Running Tests
109
+
110
+ ```bash
111
+ # Unit tests — no ABAP system needed (fast)
112
+ npm test
113
+
114
+ # Integration tests — requires a configured .abapGitAgent
115
+ npm run test:setup # one-time setup: clone test repos + activate ABAP objects
116
+ npm run test:all # full suite (setup runs automatically on first run)
117
+
118
+ # Run a single suite
119
+ npm run test:cmd # CLI command tests
120
+ npm run test:drop # drop command tests
121
+ npm run test:customize # customize command tests
122
+ npm run test:aunit # ABAP unit tests
111
123
  ```
112
124
 
125
+ > **Full integration test guide:** [docs/integration-tests.md](docs/integration-tests.md)
126
+
113
127
  ## Documentation
114
128
 
115
129
  | Topic | File |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.17.2",
3
+ "version": "1.17.4",
4
4
  "description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
5
5
  "files": [
6
6
  "bin/",
@@ -10,7 +10,8 @@
10
10
  "abap/CLAUDE.slim.md",
11
11
  "abap/.github/copilot-instructions.md",
12
12
  "abap/.github/copilot-instructions.slim.md",
13
- ".abapGitAgent.example"
13
+ ".abapGitAgent.example",
14
+ ".abapgit-agent.example.json"
14
15
  ],
15
16
  "bin": {
16
17
  "abapgit-agent": "bin/abapgit-agent",
@@ -19,6 +20,7 @@
19
20
  "scripts": {
20
21
  "test": "jest",
21
22
  "test:all": "node tests/run-all.js",
23
+ "test:setup": "node tests/run-all.js --setup",
22
24
  "test:unit": "jest",
23
25
  "test:jest": "jest",
24
26
  "test:integration": "node tests/run-all.js --cmd",
@@ -17,6 +17,31 @@ module.exports = {
17
17
  async execute(args, context) {
18
18
  const { loadConfig, AbapHttp, getTransport, getTransportSettings } = context;
19
19
 
20
+ if (args.includes('--help') || args.includes('-h')) {
21
+ console.log(`
22
+ Usage:
23
+ abapgit-agent customize <table> --set <field=value> [<field=value>...]
24
+ [--transport <TRKORR>] [--no-transport] [--json]
25
+
26
+ Description:
27
+ Write a single row to a SAP customizing table (delivery class C or E).
28
+ The row is identified by the key fields you provide via --set.
29
+
30
+ Parameters:
31
+ <table> Name of the customizing table (e.g. ZTABLE_CONFIG).
32
+ --set <field=value> One or more field=value pairs (space-separated after --set).
33
+ --transport <TRKORR> Record the change in this transport request.
34
+ --no-transport Write without a transport request (local change).
35
+ --json Output result as JSON.
36
+
37
+ Examples:
38
+ abapgit-agent customize ZTABLE_CONFIG --set KEY=APP VALUE=active
39
+ abapgit-agent customize ZTABLE_CONFIG --set KEY=APP VALUE=active --transport DEVK900001
40
+ abapgit-agent customize ZTABLE_CONFIG --set KEY=APP VALUE=active --no-transport
41
+ `);
42
+ return;
43
+ }
44
+
20
45
  const jsonOutput = args.includes('--json');
21
46
  const verbose = args.includes('--verbose');
22
47
  const noTransport = args.includes('--no-transport');
@@ -39,11 +39,14 @@ Prerequisites:
39
39
  - Package must have objects to import
40
40
 
41
41
  Options:
42
+ --branch Branch to push to. Auto-detected from current git branch if omitted.
42
43
  --message Commit message (default: "feat: initial import from ABAP package <package>")
43
44
 
44
45
  Examples:
45
46
  abapgit-agent import
46
47
  abapgit-agent import --message "Initial import from SAP"
48
+ abapgit-agent import --branch main
49
+ abapgit-agent import --branch feature/my-branch --message "Import objects"
47
50
  `);
48
51
  return;
49
52
  }
@@ -87,6 +90,9 @@ Examples:
87
90
  commitMessage = args[messageArgIndex + 1];
88
91
  }
89
92
 
93
+ const branchArgIndex = args.indexOf('--branch');
94
+ const branch = branchArgIndex !== -1 ? args[branchArgIndex + 1] : gitUtils.getBranch();
95
+
90
96
  if (safeguards.requireImportMessage && !commitMessage) {
91
97
  console.error('❌ Error: import requires a commit message for this project\n');
92
98
  console.error('Please provide one with:');
@@ -99,6 +105,7 @@ Examples:
99
105
 
100
106
  console.log(`\n📦 Starting import job`);
101
107
  console.log(` URL: ${repoUrl}`);
108
+ console.log(` Branch: ${branch}`);
102
109
  if (commitMessage) {
103
110
  console.log(` Message: ${commitMessage}`);
104
111
  }
@@ -107,7 +114,8 @@ Examples:
107
114
  const csrfToken = await http.fetchCsrfToken();
108
115
 
109
116
  const data = {
110
- url: repoUrl
117
+ url: repoUrl,
118
+ branch
111
119
  };
112
120
 
113
121
  if (commitMessage) {
@@ -565,24 +565,14 @@ Uncomment and edit the rows that differ from the defaults in \`guidelines/object
565
565
  // Create .abapgit-agent.json with default values (team config, checked into git)
566
566
  const projectConfigPath = pathModule.join(process.cwd(), '.abapgit-agent.json');
567
567
  if (!fs.existsSync(projectConfigPath)) {
568
- const projectConfig = {
569
- project: {
570
- name: packageName,
571
- description: ''
572
- },
573
- safeguards: {
574
- requireFilesForPull: false,
575
- disablePull: false,
576
- disableRun: false,
577
- disableImport: false,
578
- requireImportMessage: false,
579
- disableProbeClasses: false
580
- },
581
- conflictDetection: {
582
- mode: 'abort'
583
- }
584
- };
568
+ const projectConfigSamplePath = pathModule.join(__dirname, '..', '..', '.abapgit-agent.example.json');
569
+ if (!fs.existsSync(projectConfigSamplePath)) {
570
+ console.error('Error: .abapgit-agent.example.json not found.');
571
+ process.exit(1);
572
+ }
585
573
  try {
574
+ const projectConfig = JSON.parse(fs.readFileSync(projectConfigSamplePath, 'utf8'));
575
+ projectConfig.project.name = packageName;
586
576
  fs.writeFileSync(projectConfigPath, JSON.stringify(projectConfig, null, 2) + '\n');
587
577
  console.log(`✅ Created .abapgit-agent.json (team config — commit this to git)`);
588
578
  } catch (error) {
@@ -59,13 +59,13 @@ Examples:
59
59
  // ── Resolve changed files ─────────────────────────────────────────────────
60
60
  let abapFiles;
61
61
  if (filesArg) {
62
- abapFiles = filesArg.split(',').map(f => f.trim()).filter(f => f.endsWith('.abap'));
62
+ abapFiles = filesArg.split(',').map(f => f.trim()).filter(f => isLintable(f) && fs.existsSync(f));
63
63
  } else {
64
64
  abapFiles = detectChangedAbapFiles(baseBranch);
65
65
  }
66
66
 
67
67
  if (abapFiles.length === 0) {
68
- console.log('No changed .abap files found — nothing to lint.');
68
+ console.log('No changed .abap/.asddls files found — nothing to lint.');
69
69
  return;
70
70
  }
71
71
 
@@ -102,34 +102,29 @@ Examples:
102
102
  // keep <file> blocks for the originally changed files — suppressing any
103
103
  // pre-existing issues in dependency files that were not part of this change.
104
104
  try {
105
- if (outformat === 'checkstyle') {
106
- // Run to a temp file, filter, then write to the final destination.
107
- const tempOut = '.abaplint-raw.xml';
108
- const abapFilesSet = new Set(abapFiles.map(f => path.resolve(f)));
109
- try {
110
- const result = spawnSync(
111
- `npx @abaplint/cli@latest ${scopedConfig} --outformat checkstyle --outfile ${tempOut}`,
112
- { stdio: 'pipe', shell: true }
113
- );
114
- const raw = fs.existsSync(tempOut) ? fs.readFileSync(tempOut, 'utf8') : '<checkstyle version="8.0"/>';
115
- const filtered = filterCheckstyleToFiles(raw, abapFilesSet);
105
+ const tempOut = '.abaplint-raw.xml';
106
+ const abapFilesSet = new Set(abapFiles.map(f => path.resolve(f)));
107
+ try {
108
+ spawnSync(
109
+ `npx @abaplint/cli@latest ${scopedConfig} --outformat checkstyle --outfile ${tempOut}`,
110
+ { stdio: 'pipe', shell: true }
111
+ );
112
+ const raw = fs.existsSync(tempOut) ? fs.readFileSync(tempOut, 'utf8') : '<checkstyle version="8.0"/>';
113
+ const filtered = filterCheckstyleToFiles(raw, abapFilesSet);
114
+ if (outformat === 'checkstyle') {
116
115
  if (outfile) {
117
116
  fs.writeFileSync(outfile, filtered);
118
117
  } else {
119
118
  process.stdout.write(filtered);
120
119
  }
121
- const issueCount = (filtered.match(/<error /g) || []).length;
122
- if (issueCount > 0) process.exitCode = 1;
123
- } finally {
124
- if (fs.existsSync(tempOut)) fs.unlinkSync(tempOut);
120
+ } else {
121
+ // Interactive: print issues as human-readable text, scoped to changed files only.
122
+ printCheckstyleAsText(filtered);
125
123
  }
126
- } else {
127
- // Interactive: inherit stdio so abaplint's human-readable output flows through.
128
- const result = spawnSync(
129
- `npx @abaplint/cli@latest ${scopedConfig}`,
130
- { stdio: 'inherit', shell: true }
131
- );
132
- if (result.status !== 0) process.exitCode = result.status;
124
+ const issueCount = (filtered.match(/<error /g) || []).length;
125
+ if (issueCount > 0) process.exitCode = 1;
126
+ } finally {
127
+ if (fs.existsSync(tempOut)) fs.unlinkSync(tempOut);
133
128
  }
134
129
  } finally {
135
130
  fs.unlinkSync(scopedConfig);
@@ -156,12 +151,12 @@ function detectChangedAbapFiles(baseBranch) {
156
151
 
157
152
  let diffCmd;
158
153
  if (base) {
159
- diffCmd = `git diff --name-only ${base}...HEAD -- '*.abap'`;
154
+ diffCmd = `git diff --name-only ${base}...HEAD -- '*.abap' '*.asddls'`;
160
155
  } else {
161
156
  // Fall back to uncommitted changes first, then last commit
162
- const uncommitted = runGit('git diff --name-only HEAD -- *.abap').filter(Boolean);
157
+ const uncommitted = runGit(`git diff --name-only HEAD -- '*.abap' '*.asddls'`).filter(Boolean);
163
158
  if (uncommitted.length > 0) return filterAbapFiles(uncommitted);
164
- diffCmd = `git diff --name-only HEAD~1 HEAD -- '*.abap'`;
159
+ diffCmd = `git diff --name-only HEAD~1 HEAD -- '*.abap' '*.asddls'`;
165
160
  }
166
161
 
167
162
  return filterAbapFiles(runGit(diffCmd));
@@ -190,7 +185,7 @@ function buildFileIndex(abapDir) {
190
185
  const full = path.join(dir, entry.name);
191
186
  if (entry.isDirectory()) {
192
187
  walk(full);
193
- } else if (entry.name.endsWith('.abap') || entry.name.endsWith('.xml')) {
188
+ } else if (entry.name.endsWith('.abap') || entry.name.endsWith('.xml') || entry.name.endsWith('.asddls')) {
194
189
  index.set(entry.name.toLowerCase(), full);
195
190
  }
196
191
  }
@@ -304,13 +299,47 @@ function filterCheckstyleToFiles(xml, abapFilesSet) {
304
299
  }
305
300
 
306
301
  /**
307
- * Keep only files that look like ABAP source files
308
- * (name.type.abap or name.type.subtype.abap).
302
+ * Print checkstyle XML as human-readable text, mirroring abaplint's default output format:
303
+ * path/to/file.clas.abap:line - severity - message (rule)
304
+ */
305
+ function printCheckstyleAsText(xml) {
306
+ const fileRe = /<file\s+name="([^"]*)"([\s\S]*?)<\/file>/g;
307
+ const errorRe = /<error\s+[^>]*line="(\d+)"[^>]*severity="([^"]*)"[^>]*message="([^"]*)"[^>]*source="([^"]*)"/g;
308
+ let fileMatch;
309
+ let issueCount = 0;
310
+ while ((fileMatch = fileRe.exec(xml)) !== null) {
311
+ const filePath = fileMatch[1];
312
+ const block = fileMatch[2];
313
+ errorRe.lastIndex = 0;
314
+ let errMatch;
315
+ while ((errMatch = errorRe.exec(block)) !== null) {
316
+ const [, line, severity, message, source] = errMatch;
317
+ const rule = source.split('.').pop();
318
+ const text = message.replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&#39;/g, "'");
319
+ console.log(`${filePath}:${line} - ${severity} - ${text} (${rule})`);
320
+ issueCount++;
321
+ }
322
+ }
323
+ if (issueCount === 0) {
324
+ console.log('No issues found.');
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Returns true if the file is a type abaplint can analyse.
330
+ * - .abap — all ABAP source files (CLAS, INTF, PROG, FUGR, ENHO, etc.)
331
+ * - .asddls — CDS view / view entity sources (DDLS)
332
+ */
333
+ function isLintable(f) {
334
+ const lower = f.toLowerCase();
335
+ return lower.endsWith('.abap') || lower.endsWith('.asddls');
336
+ }
337
+
338
+ /**
339
+ * Keep only lintable files (name.type.abap / name.type.subtype.abap / name.ddls.asddls)
340
+ * that still exist on disk. Deleted files appear in git diff output but must not
341
+ * be passed to abaplint.
309
342
  */
310
343
  function filterAbapFiles(files) {
311
- return files.filter(f => {
312
- const parts = path.basename(f).split('.');
313
- return (parts.length === 3 || parts.length === 4) &&
314
- parts[parts.length - 1].toLowerCase() === 'abap';
315
- });
344
+ return files.filter(f => isLintable(f) && fs.existsSync(f));
316
345
  }