abapgit-agent 1.12.0 ā 1.13.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/README.md +30 -133
- package/abap/.github/copilot-instructions.slim.md +114 -0
- package/abap/CLAUDE.md +90 -25
- package/abap/CLAUDE.slim.md +42 -0
- package/abap/guidelines/abapgit.md +9 -47
- package/abap/guidelines/cds-testing.md +4 -1
- package/abap/guidelines/cds.md +38 -10
- package/abap/guidelines/testing.md +46 -2
- package/bin/abapgit-agent +1 -0
- package/package.json +3 -1
- package/src/commands/guide.js +276 -0
- package/src/commands/help.js +15 -0
- package/src/commands/init.js +132 -65
- package/src/commands/pull.js +69 -20
- package/src/utils/abap-reference.js +254 -118
package/src/commands/init.js
CHANGED
|
@@ -129,7 +129,7 @@ Usage:
|
|
|
129
129
|
|
|
130
130
|
Description:
|
|
131
131
|
Initialize local repository configuration.
|
|
132
|
-
Creates .abapGitAgent, .gitignore, CLAUDE.md, and guidelines folder.
|
|
132
|
+
Creates .abapGitAgent, .abapgit.xml, .gitignore, CLAUDE.md, and guidelines folder.
|
|
133
133
|
|
|
134
134
|
Options:
|
|
135
135
|
--package <PACKAGE> ABAP package name (required)
|
|
@@ -202,34 +202,82 @@ Examples:
|
|
|
202
202
|
console.log(`\nš Updating abapGit Agent files`);
|
|
203
203
|
console.log('');
|
|
204
204
|
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
205
|
+
// copilot-instructions.md: never overwrite ā user may have customised it
|
|
206
|
+
const localCopilotMdPath = pathModule.join(process.cwd(), '.github', 'copilot-instructions.md');
|
|
207
|
+
if (!fs.existsSync(localCopilotMdPath)) {
|
|
208
|
+
await copyFileIfExists(
|
|
209
|
+
pathModule.join(__dirname, '..', '..', 'abap', '.github', 'copilot-instructions.slim.md'),
|
|
210
|
+
localCopilotMdPath,
|
|
211
|
+
'.github/copilot-instructions.md',
|
|
212
|
+
true // create parent dir
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
console.log(`ā ļø .github/copilot-instructions.md already exists, skipped`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// CLAUDE.md: never overwrite ā user may have customised it
|
|
219
|
+
const localClaudeMdPath = pathModule.join(process.cwd(), 'CLAUDE.md');
|
|
220
|
+
if (!fs.existsSync(localClaudeMdPath)) {
|
|
221
|
+
await copyFileIfExists(
|
|
222
|
+
pathModule.join(__dirname, '..', '..', 'abap', 'CLAUDE.slim.md'),
|
|
223
|
+
localClaudeMdPath,
|
|
224
|
+
'CLAUDE.md'
|
|
225
|
+
);
|
|
226
|
+
} else {
|
|
227
|
+
console.log(`ā ļø CLAUDE.md already exists, skipped (use 'abapgit-agent guide --migrate' to replace a full guide with the slim stub)`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// guidelines/: never copy standard files ā they're bundled in the package now.
|
|
231
|
+
// Only ensure objects.local.md stub exists if missing.
|
|
232
|
+
const guidelinesDestPath = pathModule.join(process.cwd(), 'guidelines');
|
|
233
|
+
if (!fs.existsSync(guidelinesDestPath)) {
|
|
234
|
+
fs.mkdirSync(guidelinesDestPath, { recursive: true });
|
|
235
|
+
console.log(`ā
Created guidelines/`);
|
|
236
|
+
}
|
|
237
|
+
const localNamingPath = pathModule.join(guidelinesDestPath, 'objects.local.md');
|
|
238
|
+
if (!fs.existsSync(localNamingPath)) {
|
|
239
|
+
// reuse the same stub content as init
|
|
240
|
+
const localNamingStub = `---
|
|
241
|
+
nav_order: 8
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
# Project Naming Conventions (Override)
|
|
245
|
+
|
|
246
|
+
This file overrides \`guidelines/objects.md\` for this project.
|
|
247
|
+
It is **never overwritten** by \`abapgit-agent init --update\` ā safe to customise.
|
|
248
|
+
|
|
249
|
+
Searched by the \`ref\` command alongside all other guidelines.
|
|
250
|
+
|
|
251
|
+
## Naming Conventions
|
|
252
|
+
|
|
253
|
+
Uncomment and edit the rows that differ from the defaults in \`guidelines/objects.md\`:
|
|
254
|
+
|
|
255
|
+
| Object Type | Prefix | Example |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| Class | ZCL_ | ZCL_MY_CLASS |
|
|
258
|
+
| Interface | ZIF_ | ZIF_MY_INTERFACE |
|
|
259
|
+
| Program | Z | ZMY_PROGRAM |
|
|
260
|
+
| Package | $ | $MY_PACKAGE |
|
|
261
|
+
| Table | Z | ZMY_TABLE |
|
|
262
|
+
| CDS View | ZC_ | ZC_MY_VIEW |
|
|
263
|
+
| CDS Entity | ZE_ | ZE_MY_ENTITY |
|
|
264
|
+
| Data Element | Z | ZMY_ELEMENT |
|
|
265
|
+
| Structure | Z | ZMY_STRUCTURE |
|
|
266
|
+
| Table Type | Z | ZMY_TABLE_TYPE |
|
|
267
|
+
`;
|
|
268
|
+
fs.writeFileSync(localNamingPath, localNamingStub);
|
|
269
|
+
console.log(`ā
Created guidelines/objects.local.md`);
|
|
270
|
+
} else {
|
|
271
|
+
console.log(`ā ļø guidelines/objects.local.md already exists, skipped`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Detect and offer to remove old numbered guideline files (legacy cleanup)
|
|
275
|
+
await cleanupOldGuidelineFiles(guidelinesDestPath);
|
|
229
276
|
|
|
230
277
|
console.log(`
|
|
231
278
|
š Update complete!
|
|
232
|
-
|
|
279
|
+
Standard guidelines are read from the package automatically ā no local copies needed.
|
|
280
|
+
Run 'abapgit-agent guide --migrate' if you still have copied guideline files to remove.
|
|
233
281
|
`);
|
|
234
282
|
return;
|
|
235
283
|
}
|
|
@@ -386,66 +434,55 @@ Examples:
|
|
|
386
434
|
console.log(`ā
.gitignore already up to date`);
|
|
387
435
|
}
|
|
388
436
|
|
|
389
|
-
// Copy CLAUDE.md
|
|
390
|
-
const claudeMdPath = pathModule.join(__dirname, '..', '..', 'abap', 'CLAUDE.md');
|
|
437
|
+
// Copy CLAUDE.md (slim stub ā tells Claude to run 'abapgit-agent guide')
|
|
438
|
+
const claudeMdPath = pathModule.join(__dirname, '..', '..', 'abap', 'CLAUDE.slim.md');
|
|
391
439
|
const localClaudeMdPath = pathModule.join(process.cwd(), 'CLAUDE.md');
|
|
392
440
|
try {
|
|
393
|
-
if (fs.existsSync(
|
|
441
|
+
if (fs.existsSync(localClaudeMdPath)) {
|
|
442
|
+
console.log(`ā ļø CLAUDE.md already exists, skipped`);
|
|
443
|
+
} else if (fs.existsSync(claudeMdPath)) {
|
|
394
444
|
fs.copyFileSync(claudeMdPath, localClaudeMdPath);
|
|
395
445
|
console.log(`ā
Created CLAUDE.md`);
|
|
396
446
|
} else {
|
|
397
|
-
console.log(`ā ļø CLAUDE.md not found in abap/ directory`);
|
|
447
|
+
console.log(`ā ļø CLAUDE.slim.md not found in abap/ directory`);
|
|
398
448
|
}
|
|
399
449
|
} catch (error) {
|
|
400
450
|
console.error(`Error copying CLAUDE.md: ${error.message}`);
|
|
401
451
|
}
|
|
402
452
|
|
|
403
|
-
// Copy copilot-instructions.md for GitHub Copilot
|
|
404
|
-
const copilotMdPath = pathModule.join(__dirname, '..', '..', 'abap', '.github', 'copilot-instructions.md');
|
|
453
|
+
// Copy copilot-instructions.md for GitHub Copilot (slim stub)
|
|
454
|
+
const copilotMdPath = pathModule.join(__dirname, '..', '..', 'abap', '.github', 'copilot-instructions.slim.md');
|
|
405
455
|
const githubDir = pathModule.join(process.cwd(), '.github');
|
|
406
456
|
const localCopilotMdPath = pathModule.join(githubDir, 'copilot-instructions.md');
|
|
407
457
|
try {
|
|
408
|
-
if (fs.existsSync(
|
|
409
|
-
|
|
458
|
+
if (fs.existsSync(localCopilotMdPath)) {
|
|
459
|
+
console.log(`ā ļø .github/copilot-instructions.md already exists, skipped`);
|
|
460
|
+
} else if (fs.existsSync(copilotMdPath)) {
|
|
410
461
|
if (!fs.existsSync(githubDir)) {
|
|
411
462
|
fs.mkdirSync(githubDir, { recursive: true });
|
|
412
463
|
}
|
|
413
464
|
fs.copyFileSync(copilotMdPath, localCopilotMdPath);
|
|
414
465
|
console.log(`ā
Created .github/copilot-instructions.md`);
|
|
415
466
|
} else {
|
|
416
|
-
console.log(`ā ļø copilot-instructions.md not found in abap/ directory`);
|
|
467
|
+
console.log(`ā ļø copilot-instructions.slim.md not found in abap/.github/ directory`);
|
|
417
468
|
}
|
|
418
469
|
} catch (error) {
|
|
419
470
|
console.error(`Error copying copilot-instructions.md: ${error.message}`);
|
|
420
471
|
}
|
|
421
472
|
|
|
422
|
-
//
|
|
423
|
-
|
|
473
|
+
// Create guidelines/ directory and objects.local.md stub
|
|
474
|
+
// (Standard guidelines are bundled in the package ā no need to copy them)
|
|
424
475
|
const guidelinesDestPath = pathModule.join(process.cwd(), 'guidelines');
|
|
425
476
|
try {
|
|
426
|
-
if (fs.existsSync(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
// Copy all files from guidelines folder
|
|
431
|
-
const files = fs.readdirSync(guidelinesSrcPath);
|
|
432
|
-
for (const file of files) {
|
|
433
|
-
if (file.endsWith('.md')) {
|
|
434
|
-
fs.copyFileSync(
|
|
435
|
-
pathModule.join(guidelinesSrcPath, file),
|
|
436
|
-
pathModule.join(guidelinesDestPath, file)
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
console.log(`ā
Created guidelines/ (${files.filter(f => f.endsWith('.md')).length} files)`);
|
|
441
|
-
} else {
|
|
442
|
-
console.log(`ā ļø guidelines/ already exists, skipped`);
|
|
443
|
-
}
|
|
477
|
+
if (!fs.existsSync(guidelinesDestPath)) {
|
|
478
|
+
fs.mkdirSync(guidelinesDestPath, { recursive: true });
|
|
479
|
+
console.log(`ā
Created guidelines/`);
|
|
480
|
+
}
|
|
444
481
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
482
|
+
// Create objects.local.md stub if not already present
|
|
483
|
+
const localNamingPath = pathModule.join(guidelinesDestPath, 'objects.local.md');
|
|
484
|
+
if (!fs.existsSync(localNamingPath)) {
|
|
485
|
+
const localNamingStub = `---
|
|
449
486
|
nav_order: 8
|
|
450
487
|
---
|
|
451
488
|
|
|
@@ -473,14 +510,11 @@ Uncomment and edit the rows that differ from the defaults in \`guidelines/object
|
|
|
473
510
|
| Structure | Z | ZMY_STRUCTURE |
|
|
474
511
|
| Table Type | Z | ZMY_TABLE_TYPE |
|
|
475
512
|
`;
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
} else {
|
|
480
|
-
console.log(`ā ļø guidelines folder not found in abap/ directory`);
|
|
513
|
+
fs.writeFileSync(localNamingPath, localNamingStub);
|
|
514
|
+
console.log(`ā
Created guidelines/objects.local.md (project naming conventions)`);
|
|
481
515
|
}
|
|
482
516
|
} catch (error) {
|
|
483
|
-
console.error(`Error
|
|
517
|
+
console.error(`Error creating guidelines: ${error.message}`);
|
|
484
518
|
}
|
|
485
519
|
|
|
486
520
|
// Create folder
|
|
@@ -500,11 +534,44 @@ Uncomment and edit the rows that differ from the defaults in \`guidelines/object
|
|
|
500
534
|
console.error(`Error creating folder: ${error.message}`);
|
|
501
535
|
}
|
|
502
536
|
|
|
537
|
+
// Create .abapgit.xml with correct STARTING_FOLDER so abapGit's remove_ignored_files()
|
|
538
|
+
// keeps files inside the source folder. Without this file the stored starting_folder in
|
|
539
|
+
// the ABAP persistence may not match the actual folder, causing all remote files to be
|
|
540
|
+
// silently ignored and pull to return ACTIVATED_COUNT=0 with an empty log.
|
|
541
|
+
const abapgitXmlPath = pathModule.join(process.cwd(), '.abapgit.xml');
|
|
542
|
+
if (!fs.existsSync(abapgitXmlPath)) {
|
|
543
|
+
const language = (config.language || 'E').toUpperCase().charAt(0);
|
|
544
|
+
const abapgitXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
545
|
+
<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
|
|
546
|
+
<asx:values>
|
|
547
|
+
<DATA>
|
|
548
|
+
<MASTER_LANGUAGE>${language}</MASTER_LANGUAGE>
|
|
549
|
+
<STARTING_FOLDER>${folder}</STARTING_FOLDER>
|
|
550
|
+
<FOLDER_LOGIC>${folderLogic}</FOLDER_LOGIC>
|
|
551
|
+
</DATA>
|
|
552
|
+
</asx:values>
|
|
553
|
+
</asx:abap>
|
|
554
|
+
`;
|
|
555
|
+
try {
|
|
556
|
+
fs.writeFileSync(abapgitXmlPath, abapgitXml);
|
|
557
|
+
console.log(`ā
Created .abapgit.xml (STARTING_FOLDER=${folder}, FOLDER_LOGIC=${folderLogic})`);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
console.error(`Error creating .abapgit.xml: ${error.message}`);
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
console.log(`ā
.abapgit.xml already exists, skipped`);
|
|
563
|
+
}
|
|
564
|
+
|
|
503
565
|
console.log(`
|
|
504
566
|
š Next steps:
|
|
505
567
|
1. Edit .abapGitAgent with your ABAP credentials (host, user, password)
|
|
506
568
|
2. Run 'abapgit-agent create --import' to create online repository
|
|
507
569
|
3. Run 'abapgit-agent pull' to activate objects
|
|
570
|
+
|
|
571
|
+
š” Tips:
|
|
572
|
+
⢠Only guidelines/objects.local.md needs to live in your repo.
|
|
573
|
+
Standard guidelines are read from the package automatically via 'ref'.
|
|
574
|
+
⢠Run 'abapgit-agent guide' to read the full ABAP development guide.
|
|
508
575
|
`);
|
|
509
576
|
}
|
|
510
577
|
};
|
package/src/commands/pull.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { printHttpError } = require('../utils/format-error');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const pathModule = require('path');
|
|
6
8
|
|
|
7
9
|
module.exports = {
|
|
8
10
|
name: 'pull',
|
|
@@ -84,6 +86,20 @@ module.exports = {
|
|
|
84
86
|
console.error(' Example: --files src/zcl_my_class.clas.abap');
|
|
85
87
|
process.exit(1);
|
|
86
88
|
}
|
|
89
|
+
|
|
90
|
+
// Validate that every specified file exists on disk.
|
|
91
|
+
// Skip when --url is explicit: the files belong to a different repository
|
|
92
|
+
// and won't be present in the current working directory.
|
|
93
|
+
if (urlArgIndex === -1) {
|
|
94
|
+
const fs = require('fs');
|
|
95
|
+
const missingFiles = files.filter(f => !fs.existsSync(f));
|
|
96
|
+
if (missingFiles.length > 0) {
|
|
97
|
+
console.error('ā Error: the following file(s) do not exist:');
|
|
98
|
+
missingFiles.forEach(f => console.error(` ${f}`));
|
|
99
|
+
console.error(' Check the path and try again.');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
87
103
|
}
|
|
88
104
|
|
|
89
105
|
// SAFEGUARD 2: Check if files are required but not provided
|
|
@@ -102,22 +118,30 @@ module.exports = {
|
|
|
102
118
|
if (!transportRequest && !jsonOutput) {
|
|
103
119
|
const { selectTransport, isNonInteractive, _getTransportHookConfig } = require('../utils/transport-selector');
|
|
104
120
|
|
|
105
|
-
// Check
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
let transportRequired =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
// Check repository status ā confirms the repo is registered and tells us
|
|
122
|
+
// whether a transport is required. If the check fails, abort: pulling into
|
|
123
|
+
// an uninitialised repository does not make sense.
|
|
124
|
+
let transportRequired = false;
|
|
125
|
+
let statusResult;
|
|
126
|
+
try {
|
|
127
|
+
const config = loadConfig();
|
|
128
|
+
const http = new AbapHttp(config);
|
|
129
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
130
|
+
statusResult = await http.post('/sap/bc/z_abapgit_agent/status', { url: gitUrl }, { csrfToken });
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.error(`ā Repository status check failed: ${e.message}`);
|
|
133
|
+
console.error(' Make sure the repository is registered with abapgit-agent (run "abapgit-agent create").');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!statusResult || statusResult.status === 'Not found') {
|
|
138
|
+
console.error(`ā Repository not found in ABAP system: ${gitUrl}`);
|
|
139
|
+
console.error(' Run "abapgit-agent create" to register it first.');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (statusResult.transport_required === true || statusResult.transport_required === 'true') {
|
|
144
|
+
transportRequired = true;
|
|
121
145
|
}
|
|
122
146
|
|
|
123
147
|
if (transportRequired) {
|
|
@@ -147,10 +171,10 @@ module.exports = {
|
|
|
147
171
|
}
|
|
148
172
|
}
|
|
149
173
|
|
|
150
|
-
await this.pull(gitUrl, branch, files, transportRequest, loadConfig, AbapHttp, jsonOutput, undefined, conflictMode);
|
|
174
|
+
await this.pull(gitUrl, branch, files, transportRequest, loadConfig, AbapHttp, jsonOutput, undefined, conflictMode, verbose);
|
|
151
175
|
},
|
|
152
176
|
|
|
153
|
-
async pull(gitUrl, branch = 'main', files = null, transportRequest = null, loadConfig, AbapHttp, jsonOutput = false, gitCredentials = undefined, conflictMode = 'abort') {
|
|
177
|
+
async pull(gitUrl, branch = 'main', files = null, transportRequest = null, loadConfig, AbapHttp, jsonOutput = false, gitCredentials = undefined, conflictMode = 'abort', verbose = false) {
|
|
154
178
|
const TERM_WIDTH = process.stdout.columns || 80;
|
|
155
179
|
|
|
156
180
|
if (!jsonOutput) {
|
|
@@ -196,12 +220,26 @@ module.exports = {
|
|
|
196
220
|
|
|
197
221
|
const result = await http.post('/sap/bc/z_abapgit_agent/pull', data, { csrfToken });
|
|
198
222
|
|
|
223
|
+
// Detect missing .abapgit.xml ā without it abapGit's stored starting_folder
|
|
224
|
+
// may not match the actual source folder, causing pull to silently return ACTIVATED_COUNT=0
|
|
225
|
+
const missingAbapgitXml = fs.existsSync(pathModule.join(process.cwd(), '.git')) &&
|
|
226
|
+
!fs.existsSync(pathModule.join(process.cwd(), '.abapgit.xml'));
|
|
227
|
+
|
|
199
228
|
// JSON output mode
|
|
200
229
|
if (jsonOutput) {
|
|
230
|
+
if (missingAbapgitXml) {
|
|
231
|
+
result.missing_abapgit_xml = true;
|
|
232
|
+
}
|
|
201
233
|
console.log(JSON.stringify(result, null, 2));
|
|
202
234
|
return result;
|
|
203
235
|
}
|
|
204
236
|
|
|
237
|
+
if (missingAbapgitXml) {
|
|
238
|
+
console.warn('ā ļø .abapgit.xml not found in repository root.');
|
|
239
|
+
console.warn(' Pull may return ACTIVATED_COUNT=0 if the ABAP-side starting_folder is wrong.');
|
|
240
|
+
console.warn(' Run: abapgit-agent init --package <PACKAGE> to create it.\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
205
243
|
console.log('\n');
|
|
206
244
|
|
|
207
245
|
// Display raw result for debugging
|
|
@@ -285,6 +323,13 @@ module.exports = {
|
|
|
285
323
|
if (success === 'X' || success === true) {
|
|
286
324
|
console.log(`ā
Pull completed successfully!`);
|
|
287
325
|
console.log(` Message: ${message || 'N/A'}`);
|
|
326
|
+
} else if (failedCount === 0 && failedObjects.length === 0 &&
|
|
327
|
+
activatedCount === 0 && logMessages.length === 0 &&
|
|
328
|
+
(!message || /activation cancelled|nothing to activate|already active/i.test(message))) {
|
|
329
|
+
// abapGit returns SUCCESS='' with "Activation cancelled" when there are
|
|
330
|
+
// no inactive objects to activate ā the object is already active and consistent.
|
|
331
|
+
console.log(`ā
Pull completed ā object already active, nothing to activate.`);
|
|
332
|
+
console.log(` Message: ${message || 'N/A'}`);
|
|
288
333
|
} else {
|
|
289
334
|
console.error(`ā Pull completed with errors!`);
|
|
290
335
|
console.error(` Message: ${message || 'N/A'}`);
|
|
@@ -393,8 +438,12 @@ module.exports = {
|
|
|
393
438
|
console.log(`\nā Failed Objects Log (${failedCount})`);
|
|
394
439
|
}
|
|
395
440
|
|
|
396
|
-
// Throw if pull was not successful so callers (e.g. upgrade) can detect failure
|
|
397
|
-
|
|
441
|
+
// Throw if pull was not successful so callers (e.g. upgrade) can detect failure.
|
|
442
|
+
// Exception: SUCCESS='' with no failures and no log = object already active, not a real error.
|
|
443
|
+
const alreadyActive = failedCount === 0 && failedObjects.length === 0 &&
|
|
444
|
+
activatedCount === 0 && logMessages.length === 0 &&
|
|
445
|
+
(!message || /activation cancelled|nothing to activate|already active/i.test(message));
|
|
446
|
+
if (success !== 'X' && success !== true && !alreadyActive) {
|
|
398
447
|
const err = new Error(message || 'Pull completed with errors');
|
|
399
448
|
err._isPullError = true;
|
|
400
449
|
throw err;
|