abapgit-agent 1.3.0 → 1.5.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,298 @@
1
+ /**
2
+ * Release script - Creates release for github.com
3
+ *
4
+ * Usage: npm run release [--dry-run]
5
+ *
6
+ * Options:
7
+ * --dry-run Test the release flow without pushing to github.com
8
+ *
9
+ * This script:
10
+ * 1. Reads version from package.json
11
+ * 2. Updates the ABAP health resource with the new version
12
+ * 3. Uses Claude CLI to generate release notes from commits
13
+ * 4. Updates RELEASE_NOTES.md with new version notes
14
+ * 5. Pushes to github.com to trigger GitHub Actions (unless --dry-run)
15
+ * 6. GitHub Actions will publish to npm and create GitHub release
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { execSync } = require('child_process');
21
+
22
+ const packageJsonPath = path.join(__dirname, '..', 'package.json');
23
+ const abapHealthPath = path.join(__dirname, '..', 'abap', 'zcl_abgagt_resource_health.clas.abap');
24
+ const releaseNotesPath = path.join(__dirname, '..', 'RELEASE_NOTES.md');
25
+ const repoRoot = path.join(__dirname, '..');
26
+
27
+ // Check for --dry-run flag
28
+ const args = process.argv.slice(2);
29
+ const dryRun = args.includes('--dry-run');
30
+
31
+ if (dryRun) {
32
+ console.log('🔹 DRY RUN MODE - No actual release will be created\n');
33
+ }
34
+
35
+ // Read version from package.json
36
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
37
+ const version = pkg.version;
38
+
39
+ console.log(`Current version: ${version}`);
40
+ console.log('');
41
+
42
+ // Check if version has been bumped - version must be greater than or equal to latest tag
43
+ const allTags = execSync('git tag --sort=-v:refname', { cwd: repoRoot, encoding: 'utf8' });
44
+ const tagList = allTags.trim().split('\n').filter(t => t.startsWith('v'));
45
+ const latestTag = tagList[0] ? tagList[0].replace('v', '') : '';
46
+
47
+ const versionTag = `v${version}`;
48
+
49
+ if (version === latestTag) {
50
+ // Check if the tag points to HEAD (new release we just created)
51
+ try {
52
+ const tagRef = execSync(`git rev-parse ${versionTag}^{commit}`, { cwd: repoRoot, encoding: 'utf8' }).trim();
53
+ const headRef = execSync('git rev-parse HEAD', { cwd: repoRoot, encoding: 'utf8' }).trim();
54
+
55
+ if (tagRef !== headRef) {
56
+ console.log(`Version ${version} has already been released (tag v${version} exists on different commit)`);
57
+ console.log('');
58
+ console.log('To bump version, run one of:');
59
+ console.log(' npm version patch # e.g., 1.4.0 -> 1.4.1');
60
+ console.log(' npm version minor # e.g., 1.4.0 -> 1.5.0');
61
+ console.log(' npm version major # e.g., 1.4.0 -> 2.0.0');
62
+ console.log('');
63
+ process.exit(1);
64
+ }
65
+ // If tag points to HEAD, this is the new release we just created - continue
66
+ } catch (e) {
67
+ // Tag doesn't exist - this is a new version
68
+ }
69
+ }
70
+
71
+ console.log(`Current version: ${version} (tag: ${versionTag})`);
72
+ console.log('');
73
+
74
+ // Check if there's a remote for github.com
75
+ let remoteName = 'origin';
76
+ try {
77
+ const remotes = execSync('git remote -v', { cwd: repoRoot, encoding: 'utf8' });
78
+ if (remotes.includes('github.com') && !remotes.includes('github.tools.sap')) {
79
+ remoteName = 'origin';
80
+ } else if (remotes.includes('public') && remotes.includes('github.com')) {
81
+ remoteName = 'public';
82
+ }
83
+ console.log(`Using remote: ${remoteName}`);
84
+ } catch (e) {
85
+ console.log('Could not determine remote, using origin');
86
+ }
87
+
88
+ console.log('');
89
+
90
+ // Read ABAP health resource file
91
+ let abapContent = fs.readFileSync(abapHealthPath, 'utf8');
92
+
93
+ // Update version in ABAP file (replace existing version)
94
+ const oldVersionMatch = abapContent.match(/version":"(\d+\.\d+\.\d+)"/);
95
+ if (oldVersionMatch) {
96
+ const oldVersion = oldVersionMatch[1];
97
+ abapContent = abapContent.replace(
98
+ `version":"${oldVersion}"`,
99
+ `version":"${version}"`
100
+ );
101
+
102
+ // Write updated content
103
+ fs.writeFileSync(abapHealthPath, abapContent);
104
+ console.log(`📦 ABAP version: ${oldVersion} -> ${version}`);
105
+ if (dryRun) {
106
+ console.log(' (file modified, not committed)');
107
+ }
108
+ console.log('');
109
+ } else {
110
+ console.error('Could not find version in ABAP file');
111
+ process.exit(1);
112
+ }
113
+
114
+ // Get commits since last tag for release notes
115
+ console.log('Generating release notes with Claude...');
116
+ console.log('');
117
+
118
+ let releaseNotesContent = '';
119
+
120
+ try {
121
+ // Find previous tag
122
+ const allTags = execSync('git tag --sort=-v:refname', { cwd: repoRoot, encoding: 'utf8' });
123
+ const tagList = allTags.trim().split('\n').filter(t => t.startsWith('v'));
124
+ const previousTag = tagList[1] || 'HEAD~10';
125
+
126
+ // Get commits since last release
127
+ const commits = execSync(`git log ${previousTag}..HEAD --oneline`, { cwd: repoRoot, encoding: 'utf8' });
128
+
129
+ if (commits.trim()) {
130
+ console.log(`Found ${commits.trim().split('\n').length} commits since last release`);
131
+ console.log('');
132
+
133
+ // Create Claude prompt - escape for shell
134
+ const commitsEscaped = commits.replace(/"/g, '\\"').replace(/\n/g, '\\n');
135
+ const prompt = `Generate concise release notes for version ${version} of a Node.js CLI tool called abapgit-agent.
136
+
137
+ Commits since last release:
138
+ ${commitsEscaped}
139
+
140
+ Instructions:
141
+ 1. IGNORE commits that revert, undo, or remove previous changes
142
+ 2. IGNORE commits that fix/improve the release process itself
143
+ 3. Focus on actual USER-FACING features and fixes
144
+ 4. Use 2-4 bullet points MAX per category
145
+ 5. Keep each bullet brief (under 10 words)
146
+ 6. START your response with "## v${version}" and END with "---"
147
+ 7. OUTPUT ONLY the release notes - do NOT add any intro text, explanation, or commentary
148
+ 8. Use this exact format with blank lines between categories:
149
+ ## v${version}
150
+
151
+ ### New Features
152
+
153
+ - Brief feature description
154
+
155
+ ### Bug Fixes
156
+
157
+ - Brief fix description
158
+
159
+ ### Improvements
160
+
161
+ - Brief improvement
162
+
163
+ ### Documentation
164
+
165
+ - Brief doc update
166
+
167
+ ---
168
+
169
+ OMIT any category that has no items.`;
170
+
171
+ // Call Claude CLI
172
+ try {
173
+ releaseNotesContent = execSync(`claude --print "${prompt}"`, { cwd: repoRoot, encoding: 'utf8', timeout: 60000 });
174
+ releaseNotesContent = releaseNotesContent.trim();
175
+ console.log('Generated release notes:');
176
+ console.log(releaseNotesContent);
177
+ console.log('');
178
+ } catch (e) {
179
+ console.log('Claude CLI not available, using fallback');
180
+ releaseNotesContent = `## v${version}\n\nSee commit history for changes.`;
181
+ }
182
+ } else {
183
+ console.log('No commits since last release');
184
+ releaseNotesContent = `## v${version}\n\nSee commit history for changes.`;
185
+ }
186
+ } catch (e) {
187
+ console.log('Could not generate release notes:', e.message);
188
+ releaseNotesContent = `## v${version}\n\nSee commit history for changes.`;
189
+ }
190
+
191
+ // Show release notes in dry-run mode
192
+ if (dryRun) {
193
+ console.log('📝 Generated Release Notes:');
194
+ console.log('─'.repeat(50));
195
+ console.log(releaseNotesContent);
196
+ console.log('─'.repeat(50));
197
+ console.log('');
198
+ }
199
+
200
+ // Update RELEASE_NOTES.md
201
+ if (fs.existsSync(releaseNotesPath)) {
202
+ const existingContent = fs.readFileSync(releaseNotesPath, 'utf8');
203
+
204
+ // Check if version already exists
205
+ if (existingContent.includes(`## v${version}`)) {
206
+ console.log(`Release notes for v${version} already exist`);
207
+ } else {
208
+ let newContent;
209
+
210
+ // Check if there's a "# Release Notes" header - insert after it if present
211
+ if (existingContent.startsWith('# Release Notes')) {
212
+ // Find the position after "# Release Notes" and any following content
213
+ const lines = existingContent.split('\n');
214
+ let insertIndex = 0;
215
+ for (let i = 0; i < lines.length; i++) {
216
+ if (lines[i].match(/^## v\d+\.\d+\.\d+/)) {
217
+ insertIndex = i;
218
+ break;
219
+ }
220
+ }
221
+ // Insert new content before the first version header
222
+ const before = lines.slice(0, insertIndex).join('\n');
223
+ const after = lines.slice(insertIndex).join('\n');
224
+ newContent = before + '\n\n' + releaseNotesContent + '\n\n---\n\n' + after;
225
+ } else {
226
+ // Add new version at the top
227
+ newContent = releaseNotesContent + '\n\n---\n\n' + existingContent;
228
+ }
229
+
230
+ fs.writeFileSync(releaseNotesPath, newContent);
231
+ if (dryRun) {
232
+ console.log('📄 RELEASE_NOTES.md: would add new version at top');
233
+ } else {
234
+ console.log(`Updated RELEASE_NOTES.md with v${version}`);
235
+ }
236
+ }
237
+ } else {
238
+ // Create new RELEASE_NOTES.md
239
+ fs.writeFileSync(releaseNotesPath, releaseNotesContent);
240
+ console.log(`Created RELEASE_NOTES.md with v${version}`);
241
+ }
242
+ console.log('');
243
+
244
+ // Check git status and commit (skip in dry-run)
245
+ const status = execSync('git status --porcelain', { cwd: repoRoot, encoding: 'utf8' });
246
+
247
+ if (status.trim()) {
248
+ if (dryRun) {
249
+ console.log('🔹 DRY RUN - Would create commit with changes:');
250
+ console.log(status);
251
+ console.log('');
252
+ } else {
253
+ // Stage and commit
254
+ try {
255
+ execSync('git add abap/zcl_abgagt_resource_health.clas.abap package.json RELEASE_NOTES.md', { cwd: repoRoot });
256
+ execSync(`git commit -m "chore: release v${version}"`, { cwd: repoRoot });
257
+ console.log('Created git commit for version update');
258
+ console.log('');
259
+ } catch (e) {
260
+ console.log('No changes to commit or commit failed');
261
+ }
262
+ }
263
+ } else {
264
+ console.log('No changes to commit (version already up to date)');
265
+ console.log('');
266
+ }
267
+
268
+ // Push to trigger GitHub Actions
269
+ if (dryRun) {
270
+ console.log('🔹 DRY RUN - Skipping push to github.com');
271
+ console.log('');
272
+ console.log('To actually release, run:');
273
+ console.log(` git push ${remoteName} master --follow-tags`);
274
+ console.log('');
275
+ } else {
276
+ console.log('Pushing to github.com to trigger release...');
277
+ console.log('');
278
+
279
+ try {
280
+ // Push master and tags to github.com
281
+ execSync(`git push ${remoteName} master --follow-tags`, { cwd: repoRoot });
282
+ console.log('Pushed to github.com successfully!');
283
+ console.log('');
284
+ } catch (e) {
285
+ console.log('Push failed, please push manually');
286
+ console.log('');
287
+ }
288
+
289
+ console.log('Release workflow triggered!');
290
+ console.log('');
291
+ console.log('The GitHub Actions workflow will:');
292
+ console.log('1. Run tests');
293
+ console.log('2. Publish to npm');
294
+ console.log('3. Create GitHub release with Claude-generated notes');
295
+ console.log('');
296
+ console.log('Next step for ABAP system:');
297
+ console.log(' abapgit-agent pull --files abap/zcl_abgagt_resource_health.clas.abap');
298
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Unrelease script - Remove a release
3
+ *
4
+ * Usage: npm run unrelease [version] [--dry-run]
5
+ *
6
+ * Options:
7
+ * --dry-run Test the unrelease flow without making actual changes
8
+ *
9
+ * This script:
10
+ * 1. Takes a version as argument (default: current version from package.json)
11
+ * 2. Deletes the GitHub release
12
+ * 3. Deletes the git tag
13
+ * 4. Removes release notes from RELEASE_NOTES.md
14
+ * 5. Restores version in local files (or shows what would be restored in dry-run)
15
+ * 6. Removes release commits from git history (top 2 commits if they match release pattern)
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { execSync } = require('child_process');
21
+
22
+ const packageJsonPath = path.join(__dirname, '..', 'package.json');
23
+ const releaseNotesPath = path.join(__dirname, '..', 'RELEASE_NOTES.md');
24
+ const repoRoot = path.join(__dirname, '..');
25
+
26
+ // Get version from argument or package.json
27
+ let version = process.argv[2];
28
+
29
+ // Check for --dry-run flag
30
+ const args = process.argv.slice(2);
31
+ const dryRun = args.includes('--dry-run');
32
+
33
+ // Remove version and dry-run from args to get clean list
34
+ const remainingArgs = args.filter(arg => arg !== '--dry-run' && !arg.startsWith('--'));
35
+ if (!version && remainingArgs.length > 0) {
36
+ version = remainingArgs[0];
37
+ }
38
+
39
+ if (!version) {
40
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
41
+ version = pkg.version;
42
+ }
43
+
44
+ // Ensure version has 'v' prefix for tag
45
+ const versionTag = version.startsWith('v') ? version : `v${version}`;
46
+ const versionNoV = version.startsWith('v') ? version.slice(1) : version;
47
+
48
+ if (dryRun) {
49
+ console.log('🔹 DRY RUN MODE - No actual changes will be made\n');
50
+ }
51
+
52
+ console.log(`Unreleasing version: ${versionNoV} (tag: ${versionTag})`);
53
+ console.log('');
54
+
55
+ // Check if there's a remote for github.com
56
+ let remoteName = 'origin';
57
+ try {
58
+ const remotes = execSync('git remote -v', { cwd: repoRoot, encoding: 'utf8' });
59
+ if (remotes.includes('public') && remotes.includes('github.com')) {
60
+ remoteName = 'public';
61
+ }
62
+ console.log(`Using remote: ${remoteName}`);
63
+ } catch (e) {
64
+ console.log('Could not determine remote, using origin');
65
+ }
66
+
67
+ console.log('');
68
+
69
+ // Step 1: Delete GitHub release
70
+ if (dryRun) {
71
+ console.log(`🔹 DRY RUN - Would delete GitHub release: ${versionTag}`);
72
+ } else {
73
+ console.log('Deleting GitHub release...');
74
+ try {
75
+ execSync(`gh release delete ${versionTag} --repo SylvosCai/abapgit-agent --yes`, { cwd: repoRoot, encoding: 'utf8' });
76
+ console.log('✅ GitHub release deleted');
77
+ } catch (e) {
78
+ console.log('⚠️ GitHub release not found or already deleted');
79
+ }
80
+ }
81
+ console.log('');
82
+
83
+ // Step 2: Delete git tag locally and remotely
84
+ if (dryRun) {
85
+ console.log(`🔹 DRY RUN - Would delete git tag: ${versionTag}`);
86
+ } else {
87
+ console.log('Deleting git tag...');
88
+ try {
89
+ execSync(`git tag -d ${versionTag}`, { cwd: repoRoot, encoding: 'utf8' });
90
+ console.log('✅ Local tag deleted');
91
+ } catch (e) {
92
+ console.log('⚠️ Local tag not found');
93
+ }
94
+
95
+ try {
96
+ execSync(`git push ${remoteName} --delete ${versionTag}`, { cwd: repoRoot, encoding: 'utf8' });
97
+ console.log('✅ Remote tag deleted');
98
+ } catch (e) {
99
+ console.log('⚠️ Remote tag not found or already deleted');
100
+ }
101
+ }
102
+ console.log('');
103
+
104
+ // Step 3: Remove release notes from RELEASE_NOTES.md
105
+ if (dryRun) {
106
+ console.log(`🔹 DRY RUN - Would remove release notes for v${versionNoV} from RELEASE_NOTES.md`);
107
+ } else {
108
+ console.log('Removing release notes from RELEASE_NOTES.md...');
109
+ if (fs.existsSync(releaseNotesPath)) {
110
+ let content = fs.readFileSync(releaseNotesPath, 'utf8');
111
+
112
+ // Check if version exists in release notes
113
+ const versionHeader = `## v${versionNoV}`;
114
+ if (content.includes(versionHeader)) {
115
+ // Remove the version section (from ## vX.X.X to next --- or end)
116
+ const lines = content.split('\n');
117
+ const newLines = [];
118
+ let inVersionSection = false;
119
+ let foundVersion = false;
120
+
121
+ for (let i = 0; i < lines.length; i++) {
122
+ const line = lines[i];
123
+
124
+ // Match exactly the version header (e.g., ## v1.4.1)
125
+ if (line.trim() === versionHeader) {
126
+ inVersionSection = true;
127
+ foundVersion = true;
128
+ continue;
129
+ }
130
+
131
+ if (inVersionSection) {
132
+ // Stop at next --- separator or exact version header
133
+ if (line.startsWith('---')) {
134
+ inVersionSection = false;
135
+ newLines.push(line);
136
+ } else if (line.trim().startsWith('## v')) {
137
+ // Check if it's another exact version header (e.g., ## v1.4.0)
138
+ const trimmed = line.trim();
139
+ if (trimmed.match(/^## v\d+\.\d+\.\d+$/)) {
140
+ inVersionSection = false;
141
+ newLines.push(line);
142
+ }
143
+ }
144
+ // Otherwise skip this line (part of the version section)
145
+ } else {
146
+ newLines.push(line);
147
+ }
148
+ }
149
+
150
+ if (foundVersion) {
151
+ fs.writeFileSync(releaseNotesPath, newLines.join('\n').trim() + '\n');
152
+ console.log('✅ Release notes removed');
153
+ } else {
154
+ console.log('⚠️ Version not found in RELEASE_NOTES.md');
155
+ }
156
+ } else {
157
+ console.log('⚠️ Version not found in RELEASE_NOTES.md');
158
+ }
159
+ } else {
160
+ console.log('⚠️ RELEASE_NOTES.md not found');
161
+ }
162
+ }
163
+ console.log('');
164
+
165
+ // Step 4: Restore version in package.json and abap health resource
166
+ console.log('Restoring version in local files...');
167
+
168
+ // Restore package.json to previous version (find previous tag)
169
+ const allTags = execSync('git tag --sort=-v:refname', { cwd: repoRoot, encoding: 'utf8' });
170
+ const tagList = allTags.trim().split('\n').filter(t => t.startsWith('v') && t !== versionTag);
171
+ const previousTag = tagList[0];
172
+
173
+ if (previousTag) {
174
+ // In dry-run mode, still restore files but don't commit
175
+ if (dryRun) {
176
+ try {
177
+ execSync(`git show ${previousTag}:package.json > ${packageJsonPath}`, { cwd: repoRoot, encoding: 'utf8' });
178
+ console.log(`🔹 DRY RUN - Restored package.json from ${previousTag}`);
179
+ } catch (e) {
180
+ console.log('⚠️ Could not restore package.json');
181
+ }
182
+
183
+ try {
184
+ execSync(`git show ${previousTag}:abap/zcl_abgagt_resource_health.clas.abap > abap/zcl_abgagt_resource_health.clas.abap`, { cwd: repoRoot, encoding: 'utf8' });
185
+ console.log(`🔹 DRY RUN - Restored ABAP health resource from ${previousTag}`);
186
+ } catch (e) {
187
+ console.log('⚠️ Could not restore ABAP health resource');
188
+ }
189
+ } else {
190
+ try {
191
+ execSync(`git show ${previousTag}:package.json > ${packageJsonPath}`, { cwd: repoRoot, encoding: 'utf8' });
192
+ console.log('✅ package.json restored to previous version');
193
+ } catch (e) {
194
+ console.log('⚠️ Could not restore package.json');
195
+ }
196
+
197
+ try {
198
+ execSync(`git show ${previousTag}:abap/zcl_abgagt_resource_health.clas.abap > abap/zcl_abgagt_resource_health.clas.abap`, { cwd: repoRoot, encoding: 'utf8' });
199
+ console.log('✅ ABAP health resource restored to previous version');
200
+ } catch (e) {
201
+ console.log('⚠️ Could not restore ABAP health resource');
202
+ }
203
+ }
204
+ } else {
205
+ console.log('⚠️ No previous tag found, cannot restore version');
206
+ }
207
+ console.log('');
208
+
209
+ // Step 5: Remove release commits from remote if they exist
210
+ // Pattern: "chore: release vX.X.X" followed by "X.X.X"
211
+ console.log('Checking for release commits on remote...');
212
+ try {
213
+ // Get top 2 commit messages from remote
214
+ let remoteRef;
215
+ try {
216
+ remoteRef = execSync(`git rev-parse ${remoteName}/master 2>/dev/null`, { cwd: repoRoot, encoding: 'utf8' }).trim();
217
+ } catch (e) {
218
+ try {
219
+ remoteRef = execSync(`git rev-parse ${remoteName}/main 2>/dev/null`, { cwd: repoRoot, encoding: 'utf8' }).trim();
220
+ } catch (e2) {
221
+ remoteRef = '';
222
+ }
223
+ }
224
+
225
+ if (!remoteRef) {
226
+ console.log('⚠️ Could not determine remote ref');
227
+ } else {
228
+ // Get top 2 commits from remote
229
+ const remoteTopCommits = execSync(`git log ${remoteRef} -2 --format="%s"`, { cwd: repoRoot, encoding: 'utf8' }).trim().split('\n');
230
+
231
+ if (remoteTopCommits.length >= 2) {
232
+ const firstCommit = remoteTopCommits[0]; // Most recent on remote
233
+ const secondCommit = remoteTopCommits[1]; // Second most recent on remote
234
+
235
+ // Check if they match the expected pattern
236
+ // firstCommit (HEAD) = "chore: release vX.X.X"
237
+ // secondCommit (HEAD~1) = "X.X.X" (just version number)
238
+ const isReleaseCommit = firstCommit && firstCommit.match(/^chore: release v\d+\.\d+\.\d+$/);
239
+ const isVersionBump = secondCommit && secondCommit.match(/^\d+\.\d+\.\d+$/);
240
+
241
+ if (isReleaseCommit && isVersionBump) {
242
+ // Found release commits on remote - need to force push to remove them
243
+ // Find the commit to reset to (before the version bump)
244
+ const resetToRef = execSync(`git rev-parse ${remoteRef}~2`, { cwd: repoRoot, encoding: 'utf8' }).trim();
245
+
246
+ if (dryRun) {
247
+ console.log(`🔹 DRY RUN - Would force push to remove release commits from remote`);
248
+ console.log(` Would remove: "${firstCommit}" and "${secondCommit}"`);
249
+ console.log(` Would reset remote to: ${resetToRef.slice(0, 7)}`);
250
+ } else {
251
+ // Reset to the commit before release, then force push
252
+ execSync(`git reset --hard ${resetToRef}`, { cwd: repoRoot });
253
+ console.log('✅ Reset to commit before release');
254
+
255
+ // Force push to remove from remote
256
+ console.log('Force pushing to remote...');
257
+ execSync(`git push ${remoteName} +HEAD --force`, { cwd: repoRoot });
258
+ console.log('✅ Force pushed to remote - release commits removed from history');
259
+ }
260
+ } else {
261
+ console.log('⚠️ Remote top commits do not match release pattern, skipping');
262
+ console.log(` Found: "${firstCommit}" and "${secondCommit}"`);
263
+ }
264
+ } else {
265
+ console.log('⚠️ Not enough commits on remote to check');
266
+ }
267
+ }
268
+ } catch (e) {
269
+ console.log('⚠️ Could not check/remove release commits:', e.message);
270
+ }
271
+ console.log('');
272
+
273
+ if (dryRun) {
274
+ console.log('🔹 DRY RUN COMPLETE - No actual changes made');
275
+ } else {
276
+ console.log('Done!');
277
+ }
@@ -378,6 +378,24 @@ class ABAPClient {
378
378
 
379
379
  return await this.request('POST', '/tree', data, { csrfToken: this.csrfToken });
380
380
  }
381
+
382
+ async preview(objects, type = null, limit = 10) {
383
+ // Fetch CSRF token first
384
+ await this.fetchCsrfToken();
385
+
386
+ const data = {
387
+ objects: objects,
388
+ limit: Math.min(Math.max(1, limit), 100)
389
+ };
390
+
391
+ if (type) {
392
+ data.type = type;
393
+ }
394
+
395
+ logger.info('Previewing data', { objects, type, limit: data.limit, service: 'abapgit-agent' });
396
+
397
+ return await this.request('POST', '/preview', data, { csrfToken: this.csrfToken });
398
+ }
381
399
  }
382
400
 
383
401
  // Singleton instance
package/src/agent.js CHANGED
@@ -53,7 +53,7 @@ class ABAPGitAgent {
53
53
  return {
54
54
  status: 'healthy',
55
55
  abap: 'connected',
56
- version: result.version || '1.0.0'
56
+ version: result.version || '1.4.0'
57
57
  };
58
58
  } catch (error) {
59
59
  return {
@@ -191,6 +191,25 @@ class ABAPGitAgent {
191
191
  throw new Error(`Tree command failed: ${error.message}`);
192
192
  }
193
193
  }
194
+
195
+ async preview(objects, type = null, limit = 10) {
196
+ logger.info('Previewing data', { objects, type, limit });
197
+
198
+ try {
199
+ const result = await this.abap.preview(objects, type, limit);
200
+ return {
201
+ success: result.SUCCESS === 'X' || result.success === 'X' || result.success === true,
202
+ command: result.COMMAND || result.command || 'PREVIEW',
203
+ message: result.MESSAGE || result.message || '',
204
+ objects: result.OBJECTS || result.objects || [],
205
+ summary: result.SUMMARY || result.summary || null,
206
+ error: result.ERROR || result.error || null
207
+ };
208
+ } catch (error) {
209
+ logger.error('Preview command failed', { error: error.message });
210
+ throw new Error(`Preview command failed: ${error.message}`);
211
+ }
212
+ }
194
213
  }
195
214
 
196
215
  module.exports = {
package/src/config.js CHANGED
@@ -33,7 +33,8 @@ function loadConfig() {
33
33
  password: process.env.ABAP_PASSWORD,
34
34
  language: process.env.ABAP_LANGUAGE || 'EN',
35
35
  gitUsername: process.env.GIT_USERNAME,
36
- gitPassword: process.env.GIT_PASSWORD
36
+ gitPassword: process.env.GIT_PASSWORD,
37
+ transport: process.env.ABAP_TRANSPORT
37
38
  };
38
39
  }
39
40
 
@@ -59,8 +60,14 @@ function getAgentConfig() {
59
60
  return cfg.agent;
60
61
  }
61
62
 
63
+ function getTransport() {
64
+ const cfg = loadConfig();
65
+ return cfg.transport;
66
+ }
67
+
62
68
  module.exports = {
63
69
  loadConfig,
64
70
  getAbapConfig,
65
- getAgentConfig
71
+ getAgentConfig,
72
+ getTransport
66
73
  };