@mcp-abap-adt/adt-backup 1.0.0 → 1.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"readMetadataXmlForType.d.ts","sourceRoot":"","sources":["../../../src/lib/backup/readMetadataXmlForType.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,aAAa,EACnB,IAAI,EAAE,MAAM,EACZ,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAsHpC"}
1
+ {"version":3,"file":"readMetadataXmlForType.d.ts","sourceRoot":"","sources":["../../../src/lib/backup/readMetadataXmlForType.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,aAAa,EACnB,IAAI,EAAE,MAAM,EACZ,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CA0IpC"}
@@ -4,106 +4,126 @@ exports.readMetadataXmlForType = readMetadataXmlForType;
4
4
  const responseToText_1 = require("../utils/responseToText");
5
5
  async function readMetadataXmlForType(client, type, name, _functionGroupName) {
6
6
  try {
7
+ let result;
7
8
  switch (type) {
8
9
  case 'package': {
9
10
  const state = await client
10
11
  .getPackage()
11
12
  .readMetadata({ packageName: name });
12
- return (0, responseToText_1.responseToText)(state.metadataResult);
13
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
14
+ break;
13
15
  }
14
16
  case 'class': {
15
17
  const state = await client.getClass().readMetadata({ className: name });
16
- return (0, responseToText_1.responseToText)(state.metadataResult);
18
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
19
+ break;
17
20
  }
18
21
  case 'interface': {
19
22
  const state = await client
20
23
  .getInterface()
21
24
  .readMetadata({ interfaceName: name });
22
- return (0, responseToText_1.responseToText)(state.metadataResult);
25
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
26
+ break;
23
27
  }
24
28
  case 'domain': {
25
29
  const state = await client
26
30
  .getDomain()
27
31
  .readMetadata({ domainName: name });
28
- return (0, responseToText_1.responseToText)(state.metadataResult);
32
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
33
+ break;
29
34
  }
30
35
  case 'dataElement': {
31
36
  const state = await client
32
37
  .getDataElement()
33
38
  .readMetadata({ dataElementName: name });
34
- return (0, responseToText_1.responseToText)(state.metadataResult);
39
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
40
+ break;
35
41
  }
36
42
  case 'table': {
37
43
  const state = await client.getTable().readMetadata({ tableName: name });
38
- return (0, responseToText_1.responseToText)(state.metadataResult);
44
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
45
+ break;
39
46
  }
40
47
  case 'tableType': {
41
48
  const state = await client
42
49
  .getTableType()
43
50
  .readMetadata({ tableTypeName: name });
44
- return (0, responseToText_1.responseToText)(state.metadataResult);
51
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
52
+ break;
45
53
  }
46
54
  case 'structure': {
47
55
  const state = await client
48
56
  .getStructure()
49
57
  .readMetadata({ structureName: name });
50
- return (0, responseToText_1.responseToText)(state.metadataResult);
58
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
59
+ break;
51
60
  }
52
61
  case 'view': {
53
62
  const state = await client.getView().readMetadata({ viewName: name });
54
- return (0, responseToText_1.responseToText)(state.metadataResult);
63
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
64
+ break;
55
65
  }
56
66
  case 'behaviorDefinition': {
57
67
  const state = await client
58
68
  .getBehaviorDefinition()
59
69
  .readMetadata({ name });
60
- return (0, responseToText_1.responseToText)(state.metadataResult);
70
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
71
+ break;
61
72
  }
62
73
  case 'behaviorImplementation': {
63
74
  const state = await client
64
75
  .getBehaviorImplementation()
65
76
  .readMetadata({ className: name });
66
- return (0, responseToText_1.responseToText)(state.metadataResult);
77
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
78
+ break;
67
79
  }
68
80
  case 'serviceDefinition': {
69
81
  const state = await client
70
82
  .getServiceDefinition()
71
83
  .readMetadata({ serviceDefinitionName: name });
72
- return (0, responseToText_1.responseToText)(state.metadataResult);
84
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
85
+ break;
73
86
  }
74
87
  case 'serviceBinding': {
75
88
  const state = await client
76
89
  .getServiceBinding()
77
90
  .readMetadata({ bindingName: name });
78
- return (0, responseToText_1.responseToText)(state.metadataResult);
91
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
92
+ break;
79
93
  }
80
94
  case 'metadataExtension': {
81
95
  const state = await client
82
96
  .getMetadataExtension()
83
97
  .readMetadata({ name });
84
- return (0, responseToText_1.responseToText)(state.metadataResult);
98
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
99
+ break;
85
100
  }
86
101
  case 'functionGroup': {
87
102
  const state = await client
88
103
  .getFunctionGroup()
89
104
  .readMetadata({ functionGroupName: name });
90
- return (0, responseToText_1.responseToText)(state.metadataResult);
105
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
106
+ break;
91
107
  }
92
108
  case 'enhancement': {
93
109
  const state = await client
94
110
  .getEnhancement()
95
111
  .readMetadata({ enhancementName: name, enhancementType: 'enhoxh' });
96
- return (0, responseToText_1.responseToText)(state.metadataResult);
112
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
113
+ break;
97
114
  }
98
115
  case 'accessControl': {
99
116
  const state = await client
100
117
  .getAccessControl()
101
118
  .readMetadata({ accessControlName: name });
102
- return (0, responseToText_1.responseToText)(state.metadataResult);
119
+ result = (0, responseToText_1.responseToText)(state.metadataResult);
120
+ break;
103
121
  }
104
122
  default:
105
123
  return undefined;
106
124
  }
125
+ // ADT clients return undefined on 404 — treat as not found
126
+ return result ?? null;
107
127
  }
108
128
  catch (error) {
109
129
  const status = error.status || error.response?.status;
@@ -10,4 +10,10 @@ export interface RestoreGroup {
10
10
  * (e.g. view and behaviorDefinition for the same CDS entity).
11
11
  */
12
12
  export declare function analyzeDependencies(nodes: BackupTreeNode[]): RestoreGroup[];
13
+ /**
14
+ * Analyzes dependencies and merges SCCs at the same dependency level
15
+ * into single groups. Level = max(level of dependencies) + 1.
16
+ * Independent SCCs (same level) are merged into one group.
17
+ */
18
+ export declare function analyzeDependencyLevels(nodes: BackupTreeNode[]): RestoreGroup[];
13
19
  //# sourceMappingURL=analyzeDependencies.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"analyzeDependencies.d.ts","sourceRoot":"","sources":["../../../src/lib/restore/analyzeDependencies.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,UAAU,EAAE,OAAO,CAAC;CACrB;AAMD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,YAAY,EAAE,CAwM3E"}
1
+ {"version":3,"file":"analyzeDependencies.d.ts","sourceRoot":"","sources":["../../../src/lib/restore/analyzeDependencies.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,UAAU,EAAE,OAAO,CAAC;CACrB;AA+MD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,YAAY,EAAE,CA2B3E;AA2BD;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,cAAc,EAAE,GACtB,YAAY,EAAE,CA8DhB"}
@@ -1,30 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.analyzeDependencies = analyzeDependencies;
4
+ exports.analyzeDependencyLevels = analyzeDependencyLevels;
4
5
  const decodeBase64_1 = require("../crypto/decodeBase64");
5
6
  function nodeKey(node) {
6
7
  return `${node.type}:${node.name}`.toUpperCase();
7
8
  }
8
9
  /**
9
- * Robustly analyzes dependencies between objects by scanning both
10
- * source code and XML metadata.
11
- * Uses composite type:name keys to handle objects that share the same name
12
- * (e.g. view and behaviorDefinition for the same CDS entity).
10
+ * Build adjacency map (forward dependencies) by scanning source code and config.
13
11
  */
14
- function analyzeDependencies(nodes) {
15
- const idToNode = new Map();
16
- const nameToIds = new Map();
17
- const allNames = new Set();
18
- for (const node of nodes) {
19
- const id = nodeKey(node);
20
- const upperName = node.name.toUpperCase();
21
- idToNode.set(id, node);
22
- allNames.add(upperName);
23
- const ids = nameToIds.get(upperName) || [];
24
- ids.push(id);
25
- nameToIds.set(upperName, ids);
26
- }
27
- const allIds = new Set(idToNode.keys());
12
+ function buildAdjacency(nodes, allNames, nameToIds, allIds) {
28
13
  const adj = new Map();
29
14
  for (const node of nodes) {
30
15
  const deps = new Set();
@@ -37,13 +22,9 @@ function analyzeDependencies(nodes) {
37
22
  for (const targetName of allNames) {
38
23
  if (targetName === nodeNameUpper)
39
24
  continue;
40
- // Escape name for regex, specifically handling namespaces with /
41
25
  const escapedTarget = targetName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
42
- // Match only if surrounded by boundaries that are NOT part of an ABAP name
43
- // (i.e. not letters, digits, _, or /)
44
26
  const regex = new RegExp(`(?<=[^A-Z0-9_/]|^)${escapedTarget}(?=[^A-Z0-9_/]|$)`, 'g');
45
27
  if (regex.test(contentUpper)) {
46
- // Resolve name to all node IDs with that name
47
28
  const targetIds = nameToIds.get(targetName) || [];
48
29
  for (const tid of targetIds) {
49
30
  if (tid !== id)
@@ -51,9 +32,8 @@ function analyzeDependencies(nodes) {
51
32
  }
52
33
  }
53
34
  }
54
- // 2. Explicit structural dependencies (from config or XML properties)
55
- // Behavior Definition <-> Implementation Class (bidirectional = same SCC group)
56
- // From class source: "FOR BEHAVIOR OF <bdef_name>"
35
+ // 2. Explicit structural dependencies
36
+ // BIML class -> BDEF (bidirectional = same SCC)
57
37
  if (node.type === 'class' || node.type === 'behaviorImplementation') {
58
38
  const bdefMatch = contentUpper.match(/FOR\s+BEHAVIOR\s+OF\s+([A-Z0-9_/]+)/);
59
39
  if (bdefMatch) {
@@ -62,7 +42,7 @@ function analyzeDependencies(nodes) {
62
42
  deps.add(bdefId);
63
43
  }
64
44
  }
65
- // From BDEF source: "IMPLEMENTATION IN CLASS <class_name>"
45
+ // BDEF -> BIML class
66
46
  if (node.type === 'behaviorDefinition') {
67
47
  for (const m of contentUpper.matchAll(/IMPLEMENTATION\s+IN\s+CLASS\s+([A-Z0-9_/]+)/g)) {
68
48
  const className = m[1];
@@ -93,7 +73,12 @@ function analyzeDependencies(nodes) {
93
73
  }
94
74
  adj.set(id, deps);
95
75
  }
96
- // Tarjan's algorithm for SCCs
76
+ return adj;
77
+ }
78
+ /**
79
+ * Tarjan's SCC algorithm.
80
+ */
81
+ function tarjanSCC(allIds, adj) {
97
82
  let index = 0;
98
83
  const stack = [];
99
84
  const onStack = new Set();
@@ -140,7 +125,12 @@ function analyzeDependencies(nodes) {
140
125
  strongConnect(id);
141
126
  }
142
127
  }
143
- // Dependency graph of SCCs
128
+ return sccs;
129
+ }
130
+ /**
131
+ * Build SCC DAG and topological order from SCCs and adjacency map.
132
+ */
133
+ function buildSccDag(sccs, adj) {
144
134
  const sccAdj = new Map();
145
135
  const nodeToSccIndex = new Map();
146
136
  sccs.forEach((scc, i) => {
@@ -177,6 +167,31 @@ function analyzeDependencies(nodes) {
177
167
  for (let i = 0; i < sccs.length; i++) {
178
168
  visit(i);
179
169
  }
170
+ return { sccAdj, order };
171
+ }
172
+ /**
173
+ * Robustly analyzes dependencies between objects by scanning both
174
+ * source code and XML metadata.
175
+ * Uses composite type:name keys to handle objects that share the same name
176
+ * (e.g. view and behaviorDefinition for the same CDS entity).
177
+ */
178
+ function analyzeDependencies(nodes) {
179
+ const idToNode = new Map();
180
+ const nameToIds = new Map();
181
+ const allNames = new Set();
182
+ for (const node of nodes) {
183
+ const id = nodeKey(node);
184
+ const upperName = node.name.toUpperCase();
185
+ idToNode.set(id, node);
186
+ allNames.add(upperName);
187
+ const ids = nameToIds.get(upperName) || [];
188
+ ids.push(id);
189
+ nameToIds.set(upperName, ids);
190
+ }
191
+ const allIds = new Set(idToNode.keys());
192
+ const adj = buildAdjacency(nodes, allNames, nameToIds, allIds);
193
+ const sccs = tarjanSCC(allIds, adj);
194
+ const { order } = buildSccDag(sccs, adj);
180
195
  return order.map((i) => {
181
196
  const ids = sccs[i];
182
197
  return {
@@ -185,3 +200,86 @@ function analyzeDependencies(nodes) {
185
200
  };
186
201
  });
187
202
  }
203
+ /**
204
+ * Creation order priority within a group.
205
+ * Lower = created first. Ensures BDEF exists before BIML class, etc.
206
+ */
207
+ const TYPE_CREATION_ORDER = {
208
+ domain: 0,
209
+ dataElement: 1,
210
+ structure: 2,
211
+ table: 2,
212
+ tableType: 2,
213
+ view: 3,
214
+ behaviorDefinition: 4,
215
+ behaviorImplementation: 5,
216
+ class: 5,
217
+ interface: 5,
218
+ accessControl: 6,
219
+ metadataExtension: 6,
220
+ program: 7,
221
+ functionGroup: 7,
222
+ functionModule: 8,
223
+ serviceDefinition: 9,
224
+ serviceBinding: 10,
225
+ enhancement: 11,
226
+ };
227
+ /**
228
+ * Analyzes dependencies and merges SCCs at the same dependency level
229
+ * into single groups. Level = max(level of dependencies) + 1.
230
+ * Independent SCCs (same level) are merged into one group.
231
+ */
232
+ function analyzeDependencyLevels(nodes) {
233
+ const idToNode = new Map();
234
+ const nameToIds = new Map();
235
+ const allNames = new Set();
236
+ for (const node of nodes) {
237
+ const id = nodeKey(node);
238
+ const upperName = node.name.toUpperCase();
239
+ idToNode.set(id, node);
240
+ allNames.add(upperName);
241
+ const ids = nameToIds.get(upperName) || [];
242
+ ids.push(id);
243
+ nameToIds.set(upperName, ids);
244
+ }
245
+ const allIds = new Set(idToNode.keys());
246
+ const adj = buildAdjacency(nodes, allNames, nameToIds, allIds);
247
+ const sccs = tarjanSCC(allIds, adj);
248
+ const { sccAdj, order } = buildSccDag(sccs, adj);
249
+ // Compute level for each SCC: level = max(level of deps) + 1
250
+ const sccLevel = new Map();
251
+ for (const i of order) {
252
+ let maxDepLevel = -1;
253
+ const deps = sccAdj.get(i) || new Set();
254
+ for (const d of deps) {
255
+ const depLevel = sccLevel.get(d) ?? 0;
256
+ if (depLevel > maxDepLevel)
257
+ maxDepLevel = depLevel;
258
+ }
259
+ sccLevel.set(i, maxDepLevel + 1);
260
+ }
261
+ // Group SCCs by level, merge into RestoreGroups
262
+ const levelMap = new Map();
263
+ for (const i of order) {
264
+ const level = sccLevel.get(i);
265
+ if (!levelMap.has(level)) {
266
+ levelMap.set(level, { nodes: [], hasCircular: false });
267
+ }
268
+ const entry = levelMap.get(level);
269
+ const sccNodes = sccs[i].map((id) => idToNode.get(id));
270
+ entry.nodes.push(...sccNodes);
271
+ if (sccs[i].length > 1)
272
+ entry.hasCircular = true;
273
+ }
274
+ const sortedLevels = [...levelMap.keys()].sort((a, b) => a - b);
275
+ return sortedLevels.map((level) => {
276
+ const entry = levelMap.get(level);
277
+ // Sort nodes within group by creation order (views before BDEFs before classes)
278
+ entry.nodes.sort((a, b) => (TYPE_CREATION_ORDER[a.type || ''] ?? 99) -
279
+ (TYPE_CREATION_ORDER[b.type || ''] ?? 99));
280
+ return {
281
+ nodes: entry.nodes,
282
+ isCircular: entry.hasCircular || entry.nodes.length > 1,
283
+ };
284
+ });
285
+ }
@@ -1,4 +1,4 @@
1
1
  import type { AdtClient } from '@mcp-abap-adt/adt-clients';
2
- import type { BackupTreeNode, RestoreMode } from '../types';
3
- export declare function restoreTreeBackup(client: AdtClient, root: BackupTreeNode, mode: RestoreMode, activate: boolean, transportRequest?: string, restoreIds?: Set<string>, restoreActions?: Map<string, RestoreMode>, activateOnCreate?: boolean, softwareComponent?: string, superPackageOverride?: string, transportLayer?: string): Promise<void>;
2
+ import type { BackupTreeNode, RestoreMode, RestorePlanGroup } from '../types';
3
+ export declare function restoreTreeBackup(client: AdtClient, root: BackupTreeNode, mode: RestoreMode, activate: boolean, transportRequest?: string, restoreIds?: Set<string>, planGroups?: RestorePlanGroup[], activateOnCreate?: boolean, softwareComponent?: string, superPackageOverride?: string, transportLayer?: string): Promise<void>;
4
4
  //# sourceMappingURL=restoreTreeBackup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"restoreTreeBackup.d.ts","sourceRoot":"","sources":["../../../src/lib/restore/restoreTreeBackup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,2BAA2B,CAAC;AAI5E,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAiB,MAAM,UAAU,CAAC;AAwD3E,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,OAAO,EACjB,gBAAgB,CAAC,EAAE,MAAM,EACzB,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,gBAAgB,UAAO,EACvB,iBAAiB,CAAC,EAAE,MAAM,EAC1B,oBAAoB,CAAC,EAAE,MAAM,EAC7B,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CA2Pf"}
1
+ {"version":3,"file":"restoreTreeBackup.d.ts","sourceRoot":"","sources":["../../../src/lib/restore/restoreTreeBackup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,2BAA2B,CAAC;AAK5E,OAAO,KAAK,EACV,cAAc,EACd,WAAW,EACX,gBAAgB,EAEjB,MAAM,UAAU,CAAC;AA8DlB,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,OAAO,EACjB,gBAAgB,CAAC,EAAE,MAAM,EACzB,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,UAAU,CAAC,EAAE,gBAAgB,EAAE,EAC/B,gBAAgB,UAAO,EACvB,iBAAiB,CAAC,EAAE,MAAM,EAC1B,oBAAoB,CAAC,EAAE,MAAM,EAC7B,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAiZf"}
@@ -1,11 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.restoreTreeBackup = restoreTreeBackup;
4
+ const fast_xml_parser_1 = require("fast-xml-parser");
4
5
  const logVerbose_1 = require("../cli/logVerbose");
5
6
  const flattenTree_1 = require("../tree/flattenTree");
6
7
  const getNodeObjectId_1 = require("../tree/getNodeObjectId");
7
8
  const analyzeDependencies_1 = require("./analyzeDependencies");
8
9
  const restoreTreeNode_1 = require("./restoreTreeNode");
10
+ const xmlParser = new fast_xml_parser_1.XMLParser({
11
+ ignoreAttributes: false,
12
+ attributeNamePrefix: '@_',
13
+ parseAttributeValue: false,
14
+ });
9
15
  const RESTORE_PHASES = [
10
16
  { name: 'Domains', types: ['domain'], activation: 'individual' },
11
17
  { name: 'Data Elements', types: ['dataElement'], activation: 'individual' },
@@ -45,7 +51,7 @@ const RESTORE_PHASES = [
45
51
  { name: 'Service Bindings', types: ['serviceBinding'], activation: 'bulk' },
46
52
  { name: 'Enhancements', types: ['enhancement'], activation: 'individual' },
47
53
  ];
48
- async function restoreTreeBackup(client, root, mode, activate, transportRequest, restoreIds, restoreActions, activateOnCreate = true, softwareComponent, superPackageOverride, transportLayer) {
54
+ async function restoreTreeBackup(client, root, mode, activate, transportRequest, restoreIds, planGroups, activateOnCreate = true, softwareComponent, superPackageOverride, transportLayer) {
49
55
  const allNodes = (0, flattenTree_1.flattenTree)(root).filter((node) => node.type && node.restoreStatus === 'ok');
50
56
  const nodes = restoreIds
51
57
  ? allNodes.filter((node) => {
@@ -57,68 +63,94 @@ async function restoreTreeBackup(client, root, mode, activate, transportRequest,
57
63
  const nonPackageNodes = nodes.filter((node) => node.type !== 'package');
58
64
  const backupPackageNames = new Set(packageNodes.map((node) => node.name));
59
65
  const rootPackageName = root.name;
60
- (0, logVerbose_1.logVerbose)(1, `\n>>> STARTING TYPE-PHASE RESTORE: ${nodes.length} objects`);
61
- const failures = [];
62
- // Phase 1: Packages (recursive hierarchy)
63
- if (packageNodes.length > 0) {
64
- (0, logVerbose_1.logVerbose)(1, '[PHASE 1] Restoring package hierarchy...');
65
- const restorePackageRecursive = async (node, parentName) => {
66
- const nodeId = (0, getNodeObjectId_1.getNodeObjectId)(node);
67
- if (node.type === 'package' &&
68
- nodeId &&
69
- (!restoreIds || restoreIds.has(nodeId))) {
70
- const isRootNode = node.name === rootPackageName;
71
- const nodeMode = (restoreActions?.get(nodeId) || mode);
72
- const effectiveMode = isRootNode ? 'update' : nodeMode;
73
- if (effectiveMode === 'skip') {
74
- (0, logVerbose_1.logVerbose)(2, ` [SKIP] package:${node.name}`);
75
- }
76
- else {
77
- (0, logVerbose_1.logVerbose)(2, ` [PACKAGE] ${node.name}`);
78
- try {
79
- await (0, restoreTreeNode_1.restoreTreeNode)(client, node, effectiveMode, false, transportRequest, softwareComponent, backupPackageNames, parentName || superPackageOverride, transportLayer);
80
- }
81
- catch (e) {
82
- if (isRootNode) {
83
- (0, logVerbose_1.logVerbose)(1, ` ! Warning: Root package ${node.name} already exists or update skipped.`);
84
- }
85
- else {
86
- throw e;
87
- }
88
- }
89
- }
90
- }
91
- if (node.children) {
92
- for (const child of node.children) {
93
- await restorePackageRecursive(child, node.type === 'package' ? node.name : parentName);
94
- }
95
- }
96
- };
97
- await restorePackageRecursive(root, undefined);
66
+ // Build restoreActions map from planGroups (if provided) for mode lookups
67
+ const restoreActions = planGroups
68
+ ? new Map(planGroups
69
+ .flatMap((g) => g.actions)
70
+ .map((a) => [a.id, a.action]))
71
+ : undefined;
72
+ // Build nodeMap for quick lookup by objectId (used in plan-driven path)
73
+ const nodeMap = new Map();
74
+ for (const node of allNodes) {
75
+ const id = (0, getNodeObjectId_1.getNodeObjectId)(node);
76
+ if (id)
77
+ nodeMap.set(id, node);
98
78
  }
99
- // Phase 2: Analyze dependencies for processing order
100
- (0, logVerbose_1.logVerbose)(1, `[PHASE 2] Analyzing dependencies for ${nonPackageNodes.length} objects...`);
101
- const restoreGroups = (0, analyzeDependencies_1.analyzeDependencies)(nonPackageNodes);
102
- const orderedNodes = restoreGroups.flatMap((g) => g.nodes);
103
- (0, logVerbose_1.logVerbose)(1, `Dependency analysis complete: ${restoreGroups.length} groups → ${orderedNodes.length} ordered nodes.`);
104
- // Helper to bulk activate a list of refs
79
+ const failures = [];
80
+ // Helper: check which of our refs are still inactive
81
+ const findInactiveRefs = async (refs) => {
82
+ const result = await client.getUtils().getInactiveObjects();
83
+ const inactiveSet = new Set(result.objects.map((o) => `${o.type}:${o.name}`.toUpperCase()));
84
+ return refs.filter((r) => inactiveSet.has(`${r.type}:${r.name}`.toUpperCase()));
85
+ };
86
+ // Helper to bulk activate a list of refs with verification
105
87
  const bulkActivate = async (phaseName, refs) => {
106
88
  if (refs.length === 0)
107
89
  return;
108
- (0, logVerbose_1.logVerbose)(2, ` [*] Bulk activating ${phaseName} (${refs.length} objects)...`);
90
+ // Check which objects are actually inactive
91
+ let toActivate = await findInactiveRefs(refs);
92
+ if (toActivate.length === 0) {
93
+ (0, logVerbose_1.logVerbose)(2, ` [*] ${phaseName}: all ${refs.length} objects already active`);
94
+ return;
95
+ }
96
+ (0, logVerbose_1.logVerbose)(2, ` [*] Bulk activating ${phaseName} (${toActivate.length}/${refs.length} inactive)...`);
97
+ let hasErrors = false;
109
98
  try {
110
- await client.getUtils().activateObjectsGroup(refs, true);
99
+ const result = await client
100
+ .getUtils()
101
+ .activateObjectsGroup(toActivate, true);
102
+ if (result?.data) {
103
+ const parsed = xmlParser.parse(typeof result.data === 'string' ? result.data : String(result.data));
104
+ const msgs = parsed?.['chkl:messages']?.msg;
105
+ if (msgs) {
106
+ const msgArray = Array.isArray(msgs) ? msgs : [msgs];
107
+ for (const msg of msgArray) {
108
+ const type = msg['@_type'] || 'info';
109
+ const text = msg?.shortText?.txt || msg?.shortText || String(msg);
110
+ if (type === 'E')
111
+ hasErrors = true;
112
+ (0, logVerbose_1.logVerbose)(2, ` [${type}] ${text}`);
113
+ }
114
+ }
115
+ }
111
116
  }
112
117
  catch (error) {
118
+ hasErrors = true;
113
119
  const message = error instanceof Error ? error.message : String(error);
114
- (0, logVerbose_1.logVerbose)(1, ` [!] WARNING: ${phaseName} activation failed: ${message}`);
115
- (0, logVerbose_1.logVerbose)(1, ' Objects are created but might be inactive. Continuing...');
120
+ (0, logVerbose_1.logVerbose)(2, ` [*] Activation request completed (${message})`);
121
+ }
122
+ // Verify: poll until our objects are no longer inactive (max 5 retries, 10s apart)
123
+ // Skip polling if activation returned errors
124
+ if (hasErrors) {
125
+ const stillInactive = await findInactiveRefs(refs);
126
+ if (stillInactive.length > 0) {
127
+ (0, logVerbose_1.logVerbose)(1, ` [!] WARNING: ${phaseName}: ${stillInactive.length} object(s) remain inactive:`);
128
+ for (const ref of stillInactive) {
129
+ (0, logVerbose_1.logVerbose)(1, ` - ${ref.type}:${ref.name}`);
130
+ }
131
+ }
132
+ return;
133
+ }
134
+ const maxRetries = 5;
135
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
136
+ const stillInactive = await findInactiveRefs(refs);
137
+ if (stillInactive.length === 0) {
138
+ (0, logVerbose_1.logVerbose)(2, ` [*] ${phaseName}: all objects activated successfully`);
139
+ return;
140
+ }
141
+ if (attempt < maxRetries) {
142
+ (0, logVerbose_1.logVerbose)(2, ` [*] ${stillInactive.length} object(s) still inactive, waiting... (${attempt}/${maxRetries})`);
143
+ await new Promise((resolve) => setTimeout(resolve, 10000));
144
+ }
145
+ else {
146
+ (0, logVerbose_1.logVerbose)(1, ` [!] WARNING: ${phaseName}: ${stillInactive.length} object(s) remain inactive:`);
147
+ for (const ref of stillInactive) {
148
+ (0, logVerbose_1.logVerbose)(1, ` - ${ref.type}:${ref.name}`);
149
+ }
150
+ }
116
151
  }
117
152
  };
118
- // Phase 3: Process by granular type phases with per-type activation strategy
119
- const knownTypes = new Set(RESTORE_PHASES.flatMap((p) => p.types));
120
- const uncategorizedNodes = orderedNodes.filter((n) => n.type && !knownTypes.has(n.type) && n.type !== 'package');
121
- // Helper: process a single node (create/update without activation)
153
+ // Helper: process a single node (create/update)
122
154
  const processNode = async (node, activateFlag) => {
123
155
  const nodeId = (0, getNodeObjectId_1.getNodeObjectId)(node);
124
156
  if (!nodeId)
@@ -148,64 +180,159 @@ async function restoreTreeBackup(client, root, mode, activate, transportRequest,
148
180
  }
149
181
  return null;
150
182
  };
151
- const allProcessedRefs = [];
152
- for (const phase of RESTORE_PHASES) {
153
- const phaseTypeSet = new Set(phase.types);
154
- const phaseNodes = orderedNodes.filter((n) => n.type && phaseTypeSet.has(n.type));
155
- if (phaseNodes.length === 0)
156
- continue;
157
- (0, logVerbose_1.logVerbose)(1, `[${phase.name.toUpperCase()}] Processing ${phaseNodes.length} object(s) (${phase.activation})...`);
158
- if (phase.activation === 'individual') {
159
- // Activate each object at creation/update time
160
- for (const node of phaseNodes) {
161
- const ref = await processNode(node, true);
162
- if (ref)
163
- allProcessedRefs.push(ref);
183
+ // Phase 1: Packages (recursive hierarchy) — shared by both paths
184
+ if (packageNodes.length > 0) {
185
+ (0, logVerbose_1.logVerbose)(1, '[PHASE 1] Restoring package hierarchy...');
186
+ const restorePackageRecursive = async (node, parentName) => {
187
+ const nodeId = (0, getNodeObjectId_1.getNodeObjectId)(node);
188
+ if (node.type === 'package' &&
189
+ nodeId &&
190
+ (!restoreIds || restoreIds.has(nodeId))) {
191
+ const isRootNode = node.name === rootPackageName;
192
+ const nodeMode = (restoreActions?.get(nodeId) || mode);
193
+ const effectiveMode = isRootNode ? 'update' : nodeMode;
194
+ if (effectiveMode === 'skip') {
195
+ (0, logVerbose_1.logVerbose)(2, ` [SKIP] package:${node.name}`);
196
+ }
197
+ else {
198
+ (0, logVerbose_1.logVerbose)(2, ` [PACKAGE] ${node.name}`);
199
+ try {
200
+ await (0, restoreTreeNode_1.restoreTreeNode)(client, node, effectiveMode, false, transportRequest, softwareComponent, backupPackageNames, parentName || superPackageOverride, transportLayer);
201
+ }
202
+ catch (e) {
203
+ if (isRootNode) {
204
+ (0, logVerbose_1.logVerbose)(1, ` ! Warning: Root package ${node.name} already exists or update skipped.`);
205
+ }
206
+ else {
207
+ throw e;
208
+ }
209
+ }
210
+ }
164
211
  }
165
- }
166
- else if (phase.activation === 'bulk') {
167
- // Create/update all without activation, then bulk activate together
168
- const refs = [];
169
- for (const node of phaseNodes) {
212
+ if (node.children) {
213
+ for (const child of node.children) {
214
+ await restorePackageRecursive(child, node.type === 'package' ? node.name : parentName);
215
+ }
216
+ }
217
+ };
218
+ await restorePackageRecursive(root, undefined);
219
+ }
220
+ const allProcessedRefs = [];
221
+ if (planGroups) {
222
+ // ===== Plan-driven restore: follow plan group order =====
223
+ (0, logVerbose_1.logVerbose)(1, `\n>>> PLAN-DRIVEN RESTORE: ${planGroups.length} groups`);
224
+ for (const group of planGroups) {
225
+ const nonPackageActions = group.actions.filter((a) => a.type !== 'package');
226
+ if (nonPackageActions.length === 0)
227
+ continue;
228
+ (0, logVerbose_1.logVerbose)(1, `[GROUP ${group.id}] ${nonPackageActions.length} object(s)${group.isCircular ? ' (circular)' : ''}`);
229
+ const groupRefs = [];
230
+ for (const action of nonPackageActions) {
231
+ if (action.action === 'skip') {
232
+ (0, logVerbose_1.logVerbose)(2, ` [SKIP] ${action.type}:${action.name}`);
233
+ continue;
234
+ }
235
+ const node = nodeMap.get(action.id);
236
+ if (!node) {
237
+ (0, logVerbose_1.logVerbose)(1, ` [WARN] Node not found for ${action.id}, skipping`);
238
+ continue;
239
+ }
170
240
  const ref = await processNode(node, false);
171
241
  if (ref)
172
- refs.push(ref);
242
+ groupRefs.push(ref);
243
+ }
244
+ if (groupRefs.length > 0) {
245
+ await bulkActivate(`Group ${group.id}${group.isCircular ? ' (circular)' : ''}`, groupRefs);
173
246
  }
174
- await bulkActivate(phase.name, refs);
175
- allProcessedRefs.push(...refs);
247
+ allProcessedRefs.push(...groupRefs);
176
248
  }
177
- else if (phase.activation === 'cluster') {
178
- // Cluster by dependencies (SCC groups), bulk activate per cluster
179
- const groups = (0, analyzeDependencies_1.analyzeDependencies)(phaseNodes);
180
- (0, logVerbose_1.logVerbose)(2, ` Dependency clustering: ${groups.length} cluster(s)`);
181
- for (let gi = 0; gi < groups.length; gi++) {
182
- const group = groups[gi];
183
- const clusterRefs = [];
184
- for (const node of group.nodes) {
249
+ }
250
+ else {
251
+ // ===== Fallback: type-phase restore (no plan) =====
252
+ (0, logVerbose_1.logVerbose)(1, `\n>>> STARTING TYPE-PHASE RESTORE: ${nodes.length} objects`);
253
+ (0, logVerbose_1.logVerbose)(1, `[PHASE 2] Analyzing dependencies for ${nonPackageNodes.length} objects...`);
254
+ const restoreGroups = (0, analyzeDependencies_1.analyzeDependencies)(nonPackageNodes);
255
+ const orderedNodes = restoreGroups.flatMap((g) => g.nodes);
256
+ (0, logVerbose_1.logVerbose)(1, `Dependency analysis complete: ${restoreGroups.length} groups → ${orderedNodes.length} ordered nodes.`);
257
+ const knownTypes = new Set(RESTORE_PHASES.flatMap((p) => p.types));
258
+ const uncategorizedNodes = orderedNodes.filter((n) => n.type && !knownTypes.has(n.type) && n.type !== 'package');
259
+ for (const phase of RESTORE_PHASES) {
260
+ const phaseTypeSet = new Set(phase.types);
261
+ const phaseNodes = orderedNodes.filter((n) => n.type && phaseTypeSet.has(n.type));
262
+ if (phaseNodes.length === 0)
263
+ continue;
264
+ (0, logVerbose_1.logVerbose)(1, `[${phase.name.toUpperCase()}] Processing ${phaseNodes.length} object(s) (${phase.activation})...`);
265
+ if (phase.activation === 'individual') {
266
+ for (const node of phaseNodes) {
267
+ const ref = await processNode(node, true);
268
+ if (ref)
269
+ allProcessedRefs.push(ref);
270
+ }
271
+ }
272
+ else if (phase.activation === 'bulk') {
273
+ const refs = [];
274
+ for (const node of phaseNodes) {
185
275
  const ref = await processNode(node, false);
186
276
  if (ref)
187
- clusterRefs.push(ref);
277
+ refs.push(ref);
188
278
  }
189
- if (clusterRefs.length > 0) {
190
- await bulkActivate(`${phase.name} cluster ${gi + 1}/${groups.length}${group.isCircular ? ' (circular)' : ''}`, clusterRefs);
279
+ await bulkActivate(phase.name, refs);
280
+ allProcessedRefs.push(...refs);
281
+ }
282
+ else if (phase.activation === 'cluster') {
283
+ const groups = (0, analyzeDependencies_1.analyzeDependencies)(phaseNodes);
284
+ (0, logVerbose_1.logVerbose)(2, ` Dependency clustering: ${groups.length} cluster(s)`);
285
+ for (let gi = 0; gi < groups.length; gi++) {
286
+ const group = groups[gi];
287
+ const clusterRefs = [];
288
+ for (const node of group.nodes) {
289
+ const ref = await processNode(node, false);
290
+ if (ref)
291
+ clusterRefs.push(ref);
292
+ }
293
+ if (clusterRefs.length > 0) {
294
+ await bulkActivate(`${phase.name} cluster ${gi + 1}/${groups.length}${group.isCircular ? ' (circular)' : ''}`, clusterRefs);
295
+ }
296
+ allProcessedRefs.push(...clusterRefs);
191
297
  }
192
- allProcessedRefs.push(...clusterRefs);
193
298
  }
194
299
  }
195
- }
196
- // Uncategorized types (future types not in RESTORE_PHASES) individual activation
197
- if (uncategorizedNodes.length > 0) {
198
- (0, logVerbose_1.logVerbose)(1, `[OTHER] Processing ${uncategorizedNodes.length} uncategorized object(s) (individual)...`);
199
- for (const node of uncategorizedNodes) {
200
- const ref = await processNode(node, true);
201
- if (ref)
202
- allProcessedRefs.push(ref);
300
+ if (uncategorizedNodes.length > 0) {
301
+ (0, logVerbose_1.logVerbose)(1, `[OTHER] Processing ${uncategorizedNodes.length} uncategorized object(s) (individual)...`);
302
+ for (const node of uncategorizedNodes) {
303
+ const ref = await processNode(node, true);
304
+ if (ref)
305
+ allProcessedRefs.push(ref);
306
+ }
203
307
  }
204
308
  }
205
- // Final activation sweep safety net for objects left inactive
309
+ // Final check: find remaining inactive objects and activate them
206
310
  if (allProcessedRefs.length > 0) {
207
- (0, logVerbose_1.logVerbose)(1, `[FINAL] Activation sweep (${allProcessedRefs.length} objects)...`);
208
- await bulkActivate('Final sweep', allProcessedRefs);
311
+ const stillInactive = await findInactiveRefs(allProcessedRefs);
312
+ if (stillInactive.length > 0) {
313
+ (0, logVerbose_1.logVerbose)(1, `[FINAL] ${stillInactive.length} object(s) still inactive, activating...`);
314
+ try {
315
+ await client.getUtils().activateObjectsGroup(stillInactive, true);
316
+ }
317
+ catch (error) {
318
+ const message = error instanceof Error ? error.message : String(error);
319
+ (0, logVerbose_1.logVerbose)(2, ` [*] Final activation request completed (${message})`);
320
+ }
321
+ // Verify final state
322
+ const remaining = await findInactiveRefs(allProcessedRefs);
323
+ if (remaining.length > 0) {
324
+ (0, logVerbose_1.logVerbose)(1, ` [!] ${remaining.length} object(s) remain inactive:`);
325
+ for (const ref of remaining) {
326
+ (0, logVerbose_1.logVerbose)(1, ` - ${ref.type}:${ref.name}`);
327
+ }
328
+ }
329
+ else {
330
+ (0, logVerbose_1.logVerbose)(1, '[FINAL] All objects activated successfully.');
331
+ }
332
+ }
333
+ else {
334
+ (0, logVerbose_1.logVerbose)(1, '[FINAL] All objects are active.');
335
+ }
209
336
  }
210
337
  if (failures.length > 0) {
211
338
  (0, logVerbose_1.logVerbose)(1, `\n>>> RESTORE COMPLETED WITH ${failures.length} FAILURE(S):`);
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/lib/run.ts"],"names":[],"mappings":"AAiDA,wBAAsB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAsoBzC"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/lib/run.ts"],"names":[],"mappings":"AAyDA,wBAAsB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAytBzC"}
package/dist/lib/run.js CHANGED
@@ -41,6 +41,7 @@ const fs = __importStar(require("node:fs"));
41
41
  const path = __importStar(require("node:path"));
42
42
  const adt_clients_1 = require("@mcp-abap-adt/adt-clients");
43
43
  const connection_1 = require("@mcp-abap-adt/connection");
44
+ const fast_xml_parser_1 = require("fast-xml-parser");
44
45
  const yaml_1 = __importDefault(require("yaml"));
45
46
  const getSapConfigFromBroker_1 = require("./auth/getSapConfigFromBroker");
46
47
  const backupObject_1 = require("./backup/backupObject");
@@ -76,6 +77,11 @@ const parseObjectSpec_1 = require("./utils/parseObjectSpec");
76
77
  const formatVerifyResultsText_1 = require("./verify/formatVerifyResultsText");
77
78
  const verifyBackup_1 = require("./verify/verifyBackup");
78
79
  const extractMetadata_1 = require("./xml/extractMetadata");
80
+ const xmlParser = new fast_xml_parser_1.XMLParser({
81
+ ignoreAttributes: false,
82
+ attributeNamePrefix: '@_',
83
+ parseAttributeValue: false,
84
+ });
79
85
  async function run() {
80
86
  const argv = process.argv.slice(2);
81
87
  const args = (0, parseArgs_1.parseArgs)(argv.slice(1));
@@ -169,7 +175,16 @@ async function run() {
169
175
  ? logger
170
176
  : undefined;
171
177
  const connection = (0, connection_1.createAbapConnection)(sapAuth.config, connectionLogger, undefined, sapAuth.tokenRefresher);
172
- client = new adt_clients_1.AdtClient(connection, adtLogger);
178
+ // Resolve masterSystem/responsible for AdtClient:
179
+ // Cloud (BTP): both from getSystemInformation endpoint
180
+ // On-premise: responsible from connection username, no masterSystem
181
+ const systemInfo = await (0, adt_clients_1.getSystemInformation)(connection);
182
+ const masterSystem = systemInfo?.systemID;
183
+ const responsible = systemInfo?.userName || sapAuth.config.username;
184
+ client = new adt_clients_1.AdtClient(connection, adtLogger, {
185
+ masterSystem,
186
+ responsible,
187
+ });
173
188
  }
174
189
  }
175
190
  if (command === 'tree') {
@@ -263,7 +278,20 @@ async function run() {
263
278
  const allNodes = (0, flattenTree_1.flattenTree)(backup.root).filter((n) => n.type && n.restoreStatus === 'ok');
264
279
  const packageNodes = allNodes.filter((n) => n.type === 'package');
265
280
  const nonPackageNodes = allNodes.filter((n) => n.type !== 'package');
266
- const groups = (0, analyzeDependencies_1.analyzeDependencies)(nonPackageNodes);
281
+ // Dependency-based grouping: SCCs merged by dependency level
282
+ const depGroups = (0, analyzeDependencies_1.analyzeDependencyLevels)(nonPackageNodes);
283
+ const nonPackageGroups = depGroups.map((group, i) => ({
284
+ id: i + 1,
285
+ isCircular: group.isCircular,
286
+ actions: group.nodes.map((node) => ({
287
+ id: (0, getNodeObjectId_1.getNodeObjectId)(node),
288
+ type: node.type,
289
+ name: node.name,
290
+ functionGroupName: node.functionGroupName,
291
+ action: mode,
292
+ adtType: node.adtType,
293
+ })),
294
+ }));
267
295
  const plan = {
268
296
  schemaVersion: 1,
269
297
  generatedAt: new Date().toISOString(),
@@ -281,18 +309,7 @@ async function run() {
281
309
  adtType: node.adtType,
282
310
  })),
283
311
  },
284
- ...groups.map((group, idx) => ({
285
- id: idx + 1,
286
- isCircular: group.isCircular,
287
- actions: group.nodes.map((node) => ({
288
- id: (0, getNodeObjectId_1.getNodeObjectId)(node),
289
- type: node.type,
290
- name: node.name,
291
- functionGroupName: node.functionGroupName,
292
- action: mode,
293
- adtType: node.adtType,
294
- })),
295
- })),
312
+ ...nonPackageGroups,
296
313
  ],
297
314
  };
298
315
  fs.writeFileSync(output, yaml_1.default.stringify(plan, { lineWidth: 0 }), 'utf8');
@@ -373,9 +390,7 @@ async function run() {
373
390
  const transportLayer = typeof args['transport-layer'] === 'string'
374
391
  ? args['transport-layer']
375
392
  : undefined;
376
- await (0, restoreTreeBackup_1.restoreTreeBackup)(client, backup.root, 'upsert', activate, typeof args.transport === 'string' ? args.transport : undefined, undefined, new Map(plan.groups
377
- .flatMap((g) => g.actions)
378
- .map((a) => [a.id, a.action])), activateOnCreate, typeof args['software-component'] === 'string'
393
+ await (0, restoreTreeBackup_1.restoreTreeBackup)(client, backup.root, 'upsert', activate, typeof args.transport === 'string' ? args.transport : undefined, undefined, plan.groups, activateOnCreate, typeof args['software-component'] === 'string'
379
394
  ? args['software-component']
380
395
  : undefined, superPackage, transportLayer);
381
396
  console.log('Restore completed. Running post-restore check on TARGET system...');
@@ -396,54 +411,107 @@ async function run() {
396
411
  throw new Error(`Invalid --filter value: "${filter}". Must be skip, update, or all.`);
397
412
  }
398
413
  const plan = yaml_1.default.parse(fs.readFileSync(planPath, 'utf8'));
399
- const allActions = plan.groups.flatMap((g) => g.actions);
400
- const filtered = allActions.filter((a) => {
401
- if (a.type === 'package')
402
- return false;
403
- if (!a.adtType)
404
- return false;
405
- if (filter === 'all')
406
- return a.action !== 'create';
407
- return a.action === filter;
408
- });
409
- if (filtered.length === 0) {
414
+ // Collect all plan refs (non-package, matching filter)
415
+ const planRefs = [];
416
+ for (const group of plan.groups) {
417
+ for (const action of group.actions) {
418
+ if (action.type === 'package' || !action.adtType)
419
+ continue;
420
+ if (filter !== 'all' && action.action !== filter)
421
+ continue;
422
+ if (filter === 'all' && action.action === 'create')
423
+ continue;
424
+ planRefs.push({ name: action.name, type: action.adtType });
425
+ }
426
+ }
427
+ if (planRefs.length === 0) {
410
428
  console.log('No objects to activate.');
411
429
  return;
412
430
  }
413
- console.log(`Activating ${filtered.length} object(s) (filter: ${filter})...`);
414
- // Group by plan groups to respect dependency order
431
+ // Check which plan objects are actually inactive
432
+ const inactiveResult = await client.getUtils().getInactiveObjects();
433
+ const inactiveSet = new Set(inactiveResult.objects.map((o) => `${o.type}:${o.name}`.toUpperCase()));
434
+ const toActivate = planRefs.filter((r) => inactiveSet.has(`${r.type}:${r.name}`.toUpperCase()));
435
+ const alreadyActive = planRefs.length - toActivate.length;
436
+ if (toActivate.length === 0) {
437
+ console.log(`All ${planRefs.length} object(s) are already active. Nothing to activate.`);
438
+ return;
439
+ }
440
+ console.log(`Found ${toActivate.length} inactive object(s) out of ${planRefs.length} (${alreadyActive} already active). Activating...`);
441
+ // Group inactive objects by plan groups to respect dependency order
415
442
  let activated = 0;
416
- let skipped = 0;
417
443
  let failed = 0;
418
444
  for (const group of plan.groups) {
419
445
  const groupRefs = [];
420
446
  for (const action of group.actions) {
421
447
  if (action.type === 'package' || !action.adtType)
422
448
  continue;
423
- if (filter !== 'all' && action.action !== filter)
424
- continue;
425
- if (filter === 'all' && action.action === 'create')
426
- continue;
427
- groupRefs.push({ name: action.name, type: action.adtType });
449
+ const key = `${action.adtType}:${action.name}`.toUpperCase();
450
+ if (inactiveSet.has(key)) {
451
+ groupRefs.push({ name: action.name, type: action.adtType });
452
+ }
428
453
  }
429
454
  if (groupRefs.length === 0)
430
455
  continue;
431
- (0, logVerbose_1.logVerbose)(2, ` [GROUP ${group.id}] Activating ${groupRefs.length} object(s)...`);
456
+ (0, logVerbose_1.logVerbose)(2, ` [GROUP ${group.id}] Activating ${groupRefs.length} inactive object(s)...`);
457
+ let hasErrors = false;
432
458
  try {
433
- await client.getUtils().activateObjectsGroup(groupRefs, true);
434
- activated += groupRefs.length;
435
- for (const ref of groupRefs) {
436
- (0, logVerbose_1.logVerbose)(2, ` OK ${ref.type} ${ref.name}`);
459
+ const result = await client
460
+ .getUtils()
461
+ .activateObjectsGroup(groupRefs, true);
462
+ // Parse activation result messages
463
+ if (result?.data) {
464
+ const parsed = xmlParser.parse(typeof result.data === 'string' ? result.data : String(result.data));
465
+ const msgs = parsed?.['chkl:messages']?.msg;
466
+ if (msgs) {
467
+ const msgArray = Array.isArray(msgs) ? msgs : [msgs];
468
+ for (const msg of msgArray) {
469
+ const type = msg['@_type'] || 'info';
470
+ const text = msg?.shortText?.txt || msg?.shortText || String(msg);
471
+ if (type === 'E')
472
+ hasErrors = true;
473
+ (0, logVerbose_1.logVerbose)(2, ` [${type}] ${text}`);
474
+ }
475
+ }
437
476
  }
438
477
  }
439
478
  catch (error) {
479
+ hasErrors = true;
440
480
  const message = error instanceof Error ? error.message : String(error);
441
- failed += groupRefs.length;
442
- (0, logVerbose_1.logVerbose)(1, ` [!] Group ${group.id} activation failed: ${message}`);
481
+ (0, logVerbose_1.logVerbose)(2, ` [*] Activation request completed (${message})`);
482
+ }
483
+ // Poll until objects are no longer inactive (max 5 retries, 10s apart)
484
+ // Skip polling if activation returned errors (no point waiting)
485
+ if (!hasErrors) {
486
+ const maxRetries = 5;
487
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
488
+ const postResult = await client.getUtils().getInactiveObjects();
489
+ const postInactiveSet = new Set(postResult.objects.map((o) => `${o.type}:${o.name}`.toUpperCase()));
490
+ const stillInactive = groupRefs.filter((r) => postInactiveSet.has(`${r.type}:${r.name}`.toUpperCase()));
491
+ if (stillInactive.length === 0)
492
+ break;
493
+ if (attempt < maxRetries) {
494
+ (0, logVerbose_1.logVerbose)(2, ` [*] ${stillInactive.length} still inactive, waiting... (${attempt}/${maxRetries})`);
495
+ await new Promise((resolve) => setTimeout(resolve, 10000));
496
+ }
497
+ }
498
+ }
499
+ // Report per-object status
500
+ const finalResult = await client.getUtils().getInactiveObjects();
501
+ const finalInactiveSet = new Set(finalResult.objects.map((o) => `${o.type}:${o.name}`.toUpperCase()));
502
+ for (const ref of groupRefs) {
503
+ const key = `${ref.type}:${ref.name}`.toUpperCase();
504
+ if (finalInactiveSet.has(key)) {
505
+ (0, logVerbose_1.logVerbose)(2, ` INACTIVE ${ref.type} ${ref.name}`);
506
+ failed++;
507
+ }
508
+ else {
509
+ (0, logVerbose_1.logVerbose)(2, ` OK ${ref.type} ${ref.name}`);
510
+ activated++;
511
+ }
443
512
  }
444
513
  }
445
- skipped = allActions.length - activated - failed;
446
- console.log(`Activation complete: ${activated} activated, ${skipped} skipped, ${failed} failed`);
514
+ console.log(`Activation complete: ${activated} activated, ${alreadyActive} already active, ${failed} still inactive`);
447
515
  return;
448
516
  }
449
517
  if (command === 'diff') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/adt-backup",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "ADT backup CLI for SAP ABAP objects (recursive package backups and restores)",
5
5
  "main": "dist/bin/adt-backup.js",
6
6
  "types": "dist/bin/adt-backup.d.ts",
@@ -48,7 +48,7 @@
48
48
  "node": ">=18.0.0"
49
49
  },
50
50
  "dependencies": {
51
- "@mcp-abap-adt/adt-clients": "^1.1.1",
51
+ "@mcp-abap-adt/adt-clients": "^2.2.0",
52
52
  "@mcp-abap-adt/auth-broker": "^1.0.5",
53
53
  "@mcp-abap-adt/auth-providers": "^1.0.5",
54
54
  "@mcp-abap-adt/auth-stores": "^1.0.2",