arc-1 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/dist/adt/client.d.ts +9 -1
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +39 -1
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/config.d.ts +1 -0
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js +1 -0
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +2 -1
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/devtools.d.ts +15 -0
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +162 -67
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +105 -184
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/features.d.ts.map +1 -1
- package/dist/adt/features.js +3 -0
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +83 -2
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/transport.d.ts +6 -0
- package/dist/adt/transport.d.ts.map +1 -1
- package/dist/adt/transport.js +35 -22
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +49 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/ui5-repository.d.ts +19 -0
- package/dist/adt/ui5-repository.d.ts.map +1 -0
- package/dist/adt/ui5-repository.js +43 -0
- package/dist/adt/ui5-repository.js.map +1 -0
- package/dist/adt/xml-parser.d.ts +30 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +138 -2
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/handlers/intent.d.ts +16 -0
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +377 -61
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +15 -0
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +11 -1
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +34 -9
- package/dist/handlers/tools.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +1 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +2 -1
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +1 -0
- package/dist/server/types.js.map +1 -1
- package/package.json +12 -3
package/dist/handlers/intent.js
CHANGED
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { findDefinition, findReferences, findWhereUsed, getCompletion, } from '../adt/codeintel.js';
|
|
13
13
|
import { createObject, deleteObject, lockObject, safeUpdateSource, unlockObject } from '../adt/crud.js';
|
|
14
|
-
import { activate, activateBatch, runAtcCheck, runUnitTests, syntaxCheck } from '../adt/devtools.js';
|
|
14
|
+
import { activate, activateBatch, publishServiceBinding, runAtcCheck, runUnitTests, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
|
|
15
15
|
import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
|
|
16
16
|
import { AdtApiError, AdtNetworkError, AdtSafetyError, isNotFoundError } from '../adt/errors.js';
|
|
17
17
|
import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
|
|
18
18
|
import { checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
|
|
19
19
|
import { createTransport, getTransport, listTransports, releaseTransport } from '../adt/transport.js';
|
|
20
|
+
import { getAppInfo } from '../adt/ui5-repository.js';
|
|
20
21
|
import { validateAffHeader } from '../aff/validator.js';
|
|
21
22
|
import { extractCdsElements } from '../context/cds-deps.js';
|
|
22
23
|
import { compressCdsContext, compressContext } from '../context/compressor.js';
|
|
@@ -74,6 +75,46 @@ function textResult(text) {
|
|
|
74
75
|
function errorResult(message) {
|
|
75
76
|
return { content: [{ type: 'text', text: message }], isError: true };
|
|
76
77
|
}
|
|
78
|
+
// ─── Search Helpers ─────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Transliterate non-ASCII characters in search queries.
|
|
81
|
+
* SAP object names are ASCII-only, so umlauts and accented characters
|
|
82
|
+
* never appear in object names. This prevents wasted searches with
|
|
83
|
+
* German terms like "*Schätzung*" that silently return empty results.
|
|
84
|
+
*/
|
|
85
|
+
export function transliterateQuery(query) {
|
|
86
|
+
// Explicit German umlaut replacements (must come before NFD decomposition)
|
|
87
|
+
let result = query
|
|
88
|
+
.replace(/ä/g, 'AE')
|
|
89
|
+
.replace(/Ä/g, 'AE')
|
|
90
|
+
.replace(/ö/g, 'OE')
|
|
91
|
+
.replace(/Ö/g, 'OE')
|
|
92
|
+
.replace(/ü/g, 'UE')
|
|
93
|
+
.replace(/Ü/g, 'UE')
|
|
94
|
+
.replace(/ß/g, 'SS');
|
|
95
|
+
// General fallback: strip remaining diacritics (é→e, ñ→n, etc.)
|
|
96
|
+
result = result.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
97
|
+
return { normalized: result, changed: result !== query };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Detect if a search query looks like a field/column name rather than
|
|
101
|
+
* an object name. Field names are short, uppercase, and typically don't
|
|
102
|
+
* start with Z/Y (which are custom object prefixes).
|
|
103
|
+
*/
|
|
104
|
+
export function looksLikeFieldName(query) {
|
|
105
|
+
// Wildcard patterns are object searches, not field names
|
|
106
|
+
if (query.includes('*'))
|
|
107
|
+
return false;
|
|
108
|
+
if (query.length === 0 || query.length > 15)
|
|
109
|
+
return false;
|
|
110
|
+
// Must be uppercase letters, digits, underscores only
|
|
111
|
+
if (!/^[A-Z0-9_]+$/.test(query))
|
|
112
|
+
return false;
|
|
113
|
+
// Z/Y prefix → more likely an object name
|
|
114
|
+
if (/^[ZY]/.test(query))
|
|
115
|
+
return false;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
77
118
|
/** Classify error type for audit logging */
|
|
78
119
|
/** Format error messages with LLM-friendly remediation hints */
|
|
79
120
|
function formatErrorForLLM(err, message, _tool, args) {
|
|
@@ -86,12 +127,49 @@ function formatErrorForLLM(err, message, _tool, args) {
|
|
|
86
127
|
if (err.isUnauthorized || err.isForbidden) {
|
|
87
128
|
return `${message}\n\nHint: Authorization error. Check SAP_CLIENT (default: '100'), SAP_USER, and SAP_PASSWORD. The configured SAP user may lack permissions for this object.`;
|
|
88
129
|
}
|
|
130
|
+
// Transport / corrNr specific hints
|
|
131
|
+
const transportHint = getTransportHint(err);
|
|
132
|
+
if (transportHint) {
|
|
133
|
+
return `${message}\n\nHint: ${transportHint}`;
|
|
134
|
+
}
|
|
89
135
|
}
|
|
90
136
|
if (err instanceof AdtNetworkError) {
|
|
91
137
|
return `${message}\n\nHint: Cannot reach the SAP system. This is a connectivity issue, not a usage error.`;
|
|
92
138
|
}
|
|
93
139
|
return message;
|
|
94
140
|
}
|
|
141
|
+
/** Detect transport/corrNr failure signatures and return a remediation hint, or undefined if not transport-related. */
|
|
142
|
+
function getTransportHint(err) {
|
|
143
|
+
const body = (err.responseBody ?? '').toLowerCase();
|
|
144
|
+
const msg = err.message.toLowerCase();
|
|
145
|
+
const combined = `${msg} ${body}`;
|
|
146
|
+
// Missing or invalid transport/correction number
|
|
147
|
+
if (combined.includes('correction number') ||
|
|
148
|
+
combined.includes('corrnr') ||
|
|
149
|
+
(combined.includes('transport request') &&
|
|
150
|
+
(combined.includes('missing') || combined.includes('required') || combined.includes('invalid')))) {
|
|
151
|
+
return 'A transport/correction number is required but was not provided or is invalid. Provide an explicit "transport" parameter with a valid transport request ID, or check SE09 in SAP GUI that an open transport exists for your user and target package.';
|
|
152
|
+
}
|
|
153
|
+
// Transport not found or not modifiable
|
|
154
|
+
if (combined.includes('e070') ||
|
|
155
|
+
(combined.includes('transport') &&
|
|
156
|
+
(combined.includes('not found') || combined.includes('does not exist') || combined.includes('not modifiable')))) {
|
|
157
|
+
return 'The specified transport request was not found or is not modifiable. Verify the transport ID in SE09, ensure it is not yet released, and that it belongs to the correct user and target package.';
|
|
158
|
+
}
|
|
159
|
+
// Package / transport layer mismatch
|
|
160
|
+
if (combined.includes('transport layer') ||
|
|
161
|
+
(combined.includes('package') &&
|
|
162
|
+
combined.includes('transport') &&
|
|
163
|
+
(combined.includes('mismatch') || combined.includes('not assigned') || combined.includes('no transport layer')))) {
|
|
164
|
+
return 'The target package has no transport layer or a transport layer mismatch. Check that the package is configured for transport in SE80/TDEVC, or use a local package ($TMP) if no transport is needed.';
|
|
165
|
+
}
|
|
166
|
+
// Authorization for transport operations
|
|
167
|
+
if (combined.includes('s_transprt') ||
|
|
168
|
+
(combined.includes('transport') && (combined.includes('no authorization') || combined.includes('not authorized')))) {
|
|
169
|
+
return 'The SAP user lacks transport authorization (S_TRANSPRT). Contact your SAP basis administrator to grant the required transport permissions.';
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
95
173
|
function classifyError(err) {
|
|
96
174
|
if (err instanceof AdtApiError)
|
|
97
175
|
return 'AdtApiError';
|
|
@@ -292,20 +370,26 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
292
370
|
if (isBtpSystem() && BTP_HINTS[type]) {
|
|
293
371
|
return errorResult(BTP_HINTS[type]);
|
|
294
372
|
}
|
|
295
|
-
// Helper: get source with cache support
|
|
373
|
+
// Helper: get source with cache support, returns cache hit status
|
|
296
374
|
const cachedGet = async (objType, objName, fetcher) => {
|
|
297
375
|
if (!cachingLayer)
|
|
298
|
-
return fetcher();
|
|
299
|
-
const { source } = await cachingLayer.getSource(objType, objName, fetcher);
|
|
300
|
-
return source;
|
|
376
|
+
return { source: await fetcher(), cacheHit: false };
|
|
377
|
+
const { source, hit } = await cachingLayer.getSource(objType, objName, fetcher);
|
|
378
|
+
return { source, cacheHit: hit };
|
|
379
|
+
};
|
|
380
|
+
/** Prepend [cached] indicator when result came from cache */
|
|
381
|
+
const cachedTextResult = (source, cacheHit) => {
|
|
382
|
+
return textResult(cacheHit ? `[cached]\n${source}` : source);
|
|
301
383
|
};
|
|
302
384
|
// Structured format is only supported for CLAS type
|
|
303
385
|
if (args.format === 'structured' && type !== 'CLAS') {
|
|
304
386
|
return errorResult('The "structured" format is only supported for CLAS type. Other types return text format.');
|
|
305
387
|
}
|
|
306
388
|
switch (type) {
|
|
307
|
-
case 'PROG':
|
|
308
|
-
|
|
389
|
+
case 'PROG': {
|
|
390
|
+
const { source, cacheHit } = await cachedGet('PROG', name, () => client.getProgram(name));
|
|
391
|
+
return cachedTextResult(source, cacheHit);
|
|
392
|
+
}
|
|
309
393
|
case 'CLAS': {
|
|
310
394
|
// Structured format: return JSON with metadata + decomposed source
|
|
311
395
|
if (args.format === 'structured') {
|
|
@@ -314,8 +398,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
314
398
|
}
|
|
315
399
|
const methodParam = args.method;
|
|
316
400
|
if (methodParam && !args.include) {
|
|
317
|
-
// Method-level read — fetch full source then extract
|
|
318
|
-
const fullSource = await cachedGet('CLAS', name, () => client.getClass(name));
|
|
401
|
+
// Method-level read — fetch full source then extract (no cache indicator for derived results)
|
|
402
|
+
const { source: fullSource } = await cachedGet('CLAS', name, () => client.getClass(name));
|
|
319
403
|
const abaplintVer = cachedFeatures?.abapRelease
|
|
320
404
|
? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
|
|
321
405
|
: undefined;
|
|
@@ -331,12 +415,15 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
331
415
|
}
|
|
332
416
|
// Only cache the full merged source (no include param), not individual includes
|
|
333
417
|
if (!args.include) {
|
|
334
|
-
|
|
418
|
+
const { source, cacheHit } = await cachedGet('CLAS', name, () => client.getClass(name));
|
|
419
|
+
return cachedTextResult(source, cacheHit);
|
|
335
420
|
}
|
|
336
421
|
return textResult(await client.getClass(name, args.include));
|
|
337
422
|
}
|
|
338
|
-
case 'INTF':
|
|
339
|
-
|
|
423
|
+
case 'INTF': {
|
|
424
|
+
const { source, cacheHit } = await cachedGet('INTF', name, () => client.getInterface(name));
|
|
425
|
+
return cachedTextResult(source, cacheHit);
|
|
426
|
+
}
|
|
340
427
|
case 'FUNC': {
|
|
341
428
|
let group = String(args.group ?? '');
|
|
342
429
|
if (!group) {
|
|
@@ -349,7 +436,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
349
436
|
}
|
|
350
437
|
group = resolved;
|
|
351
438
|
}
|
|
352
|
-
|
|
439
|
+
const { source, cacheHit } = await cachedGet('FUNC', name, () => client.getFunction(group, name));
|
|
440
|
+
return cachedTextResult(source, cacheHit);
|
|
353
441
|
}
|
|
354
442
|
case 'FUGR': {
|
|
355
443
|
const expand = Boolean(args.expand_includes);
|
|
@@ -374,22 +462,30 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
374
462
|
const fg = await client.getFunctionGroup(name);
|
|
375
463
|
return textResult(JSON.stringify(fg, null, 2));
|
|
376
464
|
}
|
|
377
|
-
case 'INCL':
|
|
378
|
-
|
|
465
|
+
case 'INCL': {
|
|
466
|
+
const { source, cacheHit } = await cachedGet('INCL', name, () => client.getInclude(name));
|
|
467
|
+
return cachedTextResult(source, cacheHit);
|
|
468
|
+
}
|
|
379
469
|
case 'DDLS': {
|
|
380
|
-
const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
|
|
470
|
+
const { source: ddlSource, cacheHit } = await cachedGet('DDLS', name, () => client.getDdls(name));
|
|
381
471
|
if (args.include?.toLowerCase() === 'elements') {
|
|
472
|
+
// Elements extraction is derived from source — no cache indicator
|
|
382
473
|
return textResult(extractCdsElements(ddlSource, name));
|
|
383
474
|
}
|
|
384
|
-
return
|
|
475
|
+
return cachedTextResult(ddlSource, cacheHit);
|
|
476
|
+
}
|
|
477
|
+
case 'BDEF': {
|
|
478
|
+
const { source, cacheHit } = await cachedGet('BDEF', name, () => client.getBdef(name));
|
|
479
|
+
return cachedTextResult(source, cacheHit);
|
|
480
|
+
}
|
|
481
|
+
case 'SRVD': {
|
|
482
|
+
const { source, cacheHit } = await cachedGet('SRVD', name, () => client.getSrvd(name));
|
|
483
|
+
return cachedTextResult(source, cacheHit);
|
|
385
484
|
}
|
|
386
|
-
case 'BDEF':
|
|
387
|
-
return textResult(await cachedGet('BDEF', name, () => client.getBdef(name)));
|
|
388
|
-
case 'SRVD':
|
|
389
|
-
return textResult(await cachedGet('SRVD', name, () => client.getSrvd(name)));
|
|
390
485
|
case 'DDLX': {
|
|
391
486
|
try {
|
|
392
|
-
|
|
487
|
+
const { source, cacheHit } = await cachedGet('DDLX', name, () => client.getDdlx(name));
|
|
488
|
+
return cachedTextResult(source, cacheHit);
|
|
393
489
|
}
|
|
394
490
|
catch (err) {
|
|
395
491
|
if (isNotFoundError(err)) {
|
|
@@ -398,14 +494,22 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
398
494
|
throw err;
|
|
399
495
|
}
|
|
400
496
|
}
|
|
401
|
-
case 'SRVB':
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
case '
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
497
|
+
case 'SRVB': {
|
|
498
|
+
const { source, cacheHit } = await cachedGet('SRVB', name, () => client.getSrvb(name));
|
|
499
|
+
return cachedTextResult(source, cacheHit);
|
|
500
|
+
}
|
|
501
|
+
case 'TABL': {
|
|
502
|
+
const { source, cacheHit } = await cachedGet('TABL', name, () => client.getTable(name));
|
|
503
|
+
return cachedTextResult(source, cacheHit);
|
|
504
|
+
}
|
|
505
|
+
case 'VIEW': {
|
|
506
|
+
const { source, cacheHit } = await cachedGet('VIEW', name, () => client.getView(name));
|
|
507
|
+
return cachedTextResult(source, cacheHit);
|
|
508
|
+
}
|
|
509
|
+
case 'STRU': {
|
|
510
|
+
const { source, cacheHit } = await cachedGet('STRU', name, () => client.getStructure(name));
|
|
511
|
+
return cachedTextResult(source, cacheHit);
|
|
512
|
+
}
|
|
409
513
|
case 'DOMA': {
|
|
410
514
|
const domain = await client.getDomain(name);
|
|
411
515
|
return textResult(JSON.stringify(domain, null, 2));
|
|
@@ -431,6 +535,18 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
431
535
|
}
|
|
432
536
|
return textResult(JSON.stringify(tran, null, 2));
|
|
433
537
|
}
|
|
538
|
+
case 'API_STATE': {
|
|
539
|
+
// Determine object type for URL construction — use explicit objectType, infer from name, or error
|
|
540
|
+
const explicitType = String(args.objectType ?? '').toUpperCase();
|
|
541
|
+
const inferredType = explicitType || inferObjectType(name);
|
|
542
|
+
if (!inferredType) {
|
|
543
|
+
return errorResult(`Cannot infer object type from name "${name}". Please specify objectType explicitly (e.g., objectType="CLAS", "INTF", "PROG", "TABL", "DDLS", "FUGR", "DOMA", "DTEL", "SRVD", "SRVB", "BDEF").`);
|
|
544
|
+
}
|
|
545
|
+
// Use raw URI (no name encoding) — getApiReleaseState encodes the full URI as a single path segment
|
|
546
|
+
const objectUri = objectUrlForTypeRaw(inferredType, name);
|
|
547
|
+
const releaseState = await client.getApiReleaseState(objectUri);
|
|
548
|
+
return textResult(JSON.stringify(releaseState, null, 2));
|
|
549
|
+
}
|
|
434
550
|
case 'TABLE_CONTENTS': {
|
|
435
551
|
const maxRows = Number(args.maxRows ?? 100);
|
|
436
552
|
const data = await client.getTableContents(name, maxRows, args.sqlFilter);
|
|
@@ -480,18 +596,53 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
480
596
|
return textResult(await client.getTextElements(name));
|
|
481
597
|
case 'VARIANTS':
|
|
482
598
|
return textResult(await client.getVariants(name));
|
|
599
|
+
case 'BSP': {
|
|
600
|
+
if (cachedFeatures?.ui5 && !cachedFeatures.ui5.available) {
|
|
601
|
+
return errorResult('UI5/Fiori BSP Filestore is not available on this SAP system. ' +
|
|
602
|
+
'Run SAPManage(action="probe") to verify feature availability.');
|
|
603
|
+
}
|
|
604
|
+
const include = args.include;
|
|
605
|
+
if (!name) {
|
|
606
|
+
// List all BSP apps (optional search via query param not used here since name is empty)
|
|
607
|
+
const apps = await client.listBspApps();
|
|
608
|
+
return textResult(JSON.stringify(apps, null, 2));
|
|
609
|
+
}
|
|
610
|
+
if (!include) {
|
|
611
|
+
// Browse root structure of the app
|
|
612
|
+
return textResult(JSON.stringify(await client.getBspAppStructure(name), null, 2));
|
|
613
|
+
}
|
|
614
|
+
// If include contains a dot, treat as file read; otherwise browse subfolder
|
|
615
|
+
if (include.includes('.')) {
|
|
616
|
+
return textResult(await client.getBspFileContent(name, include));
|
|
617
|
+
}
|
|
618
|
+
return textResult(JSON.stringify(await client.getBspAppStructure(name, `/${include}`), null, 2));
|
|
619
|
+
}
|
|
620
|
+
case 'BSP_DEPLOY': {
|
|
621
|
+
if (cachedFeatures?.ui5repo && !cachedFeatures.ui5repo.available) {
|
|
622
|
+
return errorResult('ABAP Repository OData Service is not available on this SAP system. ' +
|
|
623
|
+
'Run SAPManage(action="probe") to verify feature availability.');
|
|
624
|
+
}
|
|
625
|
+
if (!name) {
|
|
626
|
+
return errorResult('BSP_DEPLOY requires a name parameter (e.g., name="ZAPP_BOOKING").');
|
|
627
|
+
}
|
|
628
|
+
const info = await getAppInfo(client.http, client.safety, name);
|
|
629
|
+
if (!info) {
|
|
630
|
+
return textResult(`App "${name}" not found in ABAP Repository.`);
|
|
631
|
+
}
|
|
632
|
+
return textResult(JSON.stringify(info, null, 2));
|
|
633
|
+
}
|
|
483
634
|
default:
|
|
484
|
-
return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS. ` +
|
|
635
|
+
return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS, BSP, BSP_DEPLOY, API_STATE. ` +
|
|
485
636
|
'Tip: Map objectType from SAPSearch results by dropping the slash suffix (e.g., DDLS/DF → type="DDLS", CLAS/OC → type="CLAS", PROG/P → type="PROG"). ' +
|
|
486
637
|
'Do not pass a URI — use the "type" and "name" parameters instead.');
|
|
487
638
|
}
|
|
488
639
|
}
|
|
489
640
|
async function handleSAPSearch(client, args) {
|
|
490
|
-
const
|
|
641
|
+
const rawQuery = String(args.query ?? '');
|
|
491
642
|
const maxResults = Number(args.maxResults ?? 100);
|
|
492
643
|
const searchType = String(args.searchType ?? 'object');
|
|
493
644
|
if (searchType === 'source_code') {
|
|
494
|
-
//
|
|
645
|
+
// Source code search: do NOT transliterate — source can contain umlauts in strings/comments
|
|
495
646
|
if (cachedFeatures?.textSearch && !cachedFeatures.textSearch.available) {
|
|
496
647
|
return errorResult(`Source code search is not available on this SAP system. ${cachedFeatures.textSearch.reason ?? ''}` +
|
|
497
648
|
`\nUse SAPSearch with searchType="object" to search by object name instead, or use SAPQuery to search metadata tables.`);
|
|
@@ -499,7 +650,7 @@ async function handleSAPSearch(client, args) {
|
|
|
499
650
|
const objectType = args.objectType;
|
|
500
651
|
const packageName = args.packageName;
|
|
501
652
|
try {
|
|
502
|
-
const results = await client.searchSource(
|
|
653
|
+
const results = await client.searchSource(rawQuery, maxResults, objectType, packageName);
|
|
503
654
|
return textResult(JSON.stringify(results, null, 2));
|
|
504
655
|
}
|
|
505
656
|
catch (err) {
|
|
@@ -514,8 +665,25 @@ async function handleSAPSearch(client, args) {
|
|
|
514
665
|
throw err;
|
|
515
666
|
}
|
|
516
667
|
}
|
|
668
|
+
// Object search: transliterate non-ASCII (SAP object names are ASCII-only)
|
|
669
|
+
const { normalized: query, changed: wasTransliterated } = transliterateQuery(rawQuery);
|
|
670
|
+
const transliterationNote = wasTransliterated
|
|
671
|
+
? `Note: Query contained non-ASCII characters. Transliterated "${rawQuery}" → "${query}" (SAP object names are ASCII-only).\n\n`
|
|
672
|
+
: '';
|
|
517
673
|
const results = await client.searchObject(query, maxResults);
|
|
518
|
-
|
|
674
|
+
if (Array.isArray(results) && results.length === 0) {
|
|
675
|
+
let hint = '[]' +
|
|
676
|
+
'\n\n' +
|
|
677
|
+
transliterationNote +
|
|
678
|
+
'No objects found. If searching for custom objects, try Z* or Y* prefixes (e.g., "Z*ESTIM*"). ' +
|
|
679
|
+
'If you already found objects in a package, use SAPRead with type=DEVC to list all package contents instead of more searches.';
|
|
680
|
+
if (looksLikeFieldName(query)) {
|
|
681
|
+
const stripped = query.replace(/\*/g, '');
|
|
682
|
+
hint += `\nThis looks like a field/column name. Use SAPQuery("SELECT fieldname, rollname, domname FROM dd03l WHERE fieldname = '${stripped}'") or SAPRead(type='DDLS', include='elements') to find fields.`;
|
|
683
|
+
}
|
|
684
|
+
return textResult(hint);
|
|
685
|
+
}
|
|
686
|
+
return textResult(transliterationNote + JSON.stringify(results, null, 2));
|
|
519
687
|
}
|
|
520
688
|
async function handleSAPQuery(client, args) {
|
|
521
689
|
const sql = String(args.sql ?? '');
|
|
@@ -739,46 +907,67 @@ function escapeXml(s) {
|
|
|
739
907
|
.replace(/>/g, '>');
|
|
740
908
|
}
|
|
741
909
|
// ─── Object URL Mapping ──────────────────────────────────────────────
|
|
742
|
-
/**
|
|
743
|
-
function
|
|
744
|
-
const encoded = encodeURIComponent(name);
|
|
910
|
+
/** Base path for an object type. Returns path prefix without trailing name segment. */
|
|
911
|
+
function objectBasePath(type) {
|
|
745
912
|
switch (type) {
|
|
746
913
|
case 'PROG':
|
|
747
|
-
return
|
|
914
|
+
return '/sap/bc/adt/programs/programs/';
|
|
748
915
|
case 'CLAS':
|
|
749
|
-
return
|
|
916
|
+
return '/sap/bc/adt/oo/classes/';
|
|
750
917
|
case 'INTF':
|
|
751
|
-
return
|
|
918
|
+
return '/sap/bc/adt/oo/interfaces/';
|
|
752
919
|
case 'FUNC':
|
|
753
|
-
return
|
|
920
|
+
return '/sap/bc/adt/functions/groups/';
|
|
754
921
|
case 'INCL':
|
|
755
|
-
return
|
|
922
|
+
return '/sap/bc/adt/programs/includes/';
|
|
756
923
|
case 'FUGR':
|
|
757
|
-
return
|
|
924
|
+
return '/sap/bc/adt/functions/groups/';
|
|
758
925
|
case 'DDLS':
|
|
759
|
-
return
|
|
926
|
+
return '/sap/bc/adt/ddic/ddl/sources/';
|
|
760
927
|
case 'BDEF':
|
|
761
|
-
return
|
|
928
|
+
return '/sap/bc/adt/bo/behaviordefinitions/';
|
|
762
929
|
case 'SRVD':
|
|
763
|
-
return
|
|
930
|
+
return '/sap/bc/adt/ddic/srvd/sources/';
|
|
764
931
|
case 'DDLX':
|
|
765
|
-
return
|
|
932
|
+
return '/sap/bc/adt/ddic/ddlx/sources/';
|
|
766
933
|
case 'SRVB':
|
|
767
|
-
return
|
|
934
|
+
return '/sap/bc/adt/businessservices/bindings/';
|
|
768
935
|
case 'TABL':
|
|
769
|
-
return
|
|
936
|
+
return '/sap/bc/adt/ddic/tables/';
|
|
770
937
|
case 'STRU':
|
|
771
|
-
return
|
|
938
|
+
return '/sap/bc/adt/ddic/structures/';
|
|
772
939
|
case 'DOMA':
|
|
773
|
-
return
|
|
940
|
+
return '/sap/bc/adt/ddic/domains/';
|
|
774
941
|
case 'DTEL':
|
|
775
|
-
return
|
|
942
|
+
return '/sap/bc/adt/ddic/dataelements/';
|
|
776
943
|
case 'TRAN':
|
|
777
|
-
return
|
|
944
|
+
return '/sap/bc/adt/vit/wb/object_type/trant/object_name/';
|
|
778
945
|
default:
|
|
779
|
-
return
|
|
946
|
+
return '/sap/bc/adt/programs/programs/';
|
|
780
947
|
}
|
|
781
948
|
}
|
|
949
|
+
/** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. Name is URI-encoded. */
|
|
950
|
+
function objectUrlForType(type, name) {
|
|
951
|
+
return `${objectBasePath(type)}${encodeURIComponent(name)}`;
|
|
952
|
+
}
|
|
953
|
+
/** Infer SAP object type from naming conventions. Returns empty string if type cannot be determined. */
|
|
954
|
+
function inferObjectType(name) {
|
|
955
|
+
const upper = name.toUpperCase();
|
|
956
|
+
if (upper.startsWith('IF_') || upper.startsWith('ZIF_') || upper.startsWith('YIF_'))
|
|
957
|
+
return 'INTF';
|
|
958
|
+
if (upper.startsWith('CL_') || upper.startsWith('ZCL_') || upper.startsWith('YCL_'))
|
|
959
|
+
return 'CLAS';
|
|
960
|
+
if (upper.startsWith('CX_') || upper.startsWith('ZCX_') || upper.startsWith('YCX_'))
|
|
961
|
+
return 'CLAS';
|
|
962
|
+
return '';
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Map object type + name to the ADT object URL WITHOUT encoding the name.
|
|
966
|
+
* Used for API release state where the full URI is encoded as a single path segment by the caller.
|
|
967
|
+
*/
|
|
968
|
+
function objectUrlForTypeRaw(type, name) {
|
|
969
|
+
return `${objectBasePath(type)}${name}`;
|
|
970
|
+
}
|
|
782
971
|
/** Get the source URL for an object (appends /source/main) */
|
|
783
972
|
function sourceUrlForType(type, name) {
|
|
784
973
|
return `${objectUrlForType(type, name)}/source/main`;
|
|
@@ -811,7 +1000,6 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
811
1000
|
const pkg = String(args.package ?? '$TMP');
|
|
812
1001
|
checkPackage(client.safety, pkg);
|
|
813
1002
|
const description = String(args.description ?? name);
|
|
814
|
-
checkPackage(client.safety, pkg);
|
|
815
1003
|
// AFF header validation (if schema available for this type)
|
|
816
1004
|
const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
|
|
817
1005
|
if (!affResult.valid) {
|
|
@@ -870,11 +1058,12 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
870
1058
|
return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
|
|
871
1059
|
}
|
|
872
1060
|
case 'delete': {
|
|
873
|
-
// Lock, delete, unlock pattern
|
|
1061
|
+
// Lock, delete, unlock pattern — auto-propagate lock corrNr if no explicit transport
|
|
874
1062
|
await client.http.withStatefulSession(async (session) => {
|
|
875
1063
|
const lock = await lockObject(session, client.safety, objectUrl);
|
|
1064
|
+
const effectiveTransport = transport ?? (lock.corrNr || undefined);
|
|
876
1065
|
try {
|
|
877
|
-
await deleteObject(session, client.safety, objectUrl, lock.lockHandle,
|
|
1066
|
+
await deleteObject(session, client.safety, objectUrl, lock.lockHandle, effectiveTransport);
|
|
878
1067
|
}
|
|
879
1068
|
finally {
|
|
880
1069
|
try {
|
|
@@ -1039,8 +1228,78 @@ function runPreWriteLint(source, type, name, config) {
|
|
|
1039
1228
|
}
|
|
1040
1229
|
// ─── SAPActivate Handler ─────────────────────────────────────────────
|
|
1041
1230
|
async function handleSAPActivate(client, args) {
|
|
1042
|
-
const
|
|
1231
|
+
const action = String(args.action ?? 'activate');
|
|
1232
|
+
const name = String(args.name ?? '');
|
|
1233
|
+
const version = String(args.version ?? '0001');
|
|
1234
|
+
// Publish service binding
|
|
1235
|
+
if (action === 'publish_srvb') {
|
|
1236
|
+
if (!name) {
|
|
1237
|
+
return errorResult('Missing required "name" parameter for publish_srvb action.');
|
|
1238
|
+
}
|
|
1239
|
+
const result = await publishServiceBinding(client.http, client.safety, name, version);
|
|
1240
|
+
if (result.severity === 'ERROR') {
|
|
1241
|
+
return errorResult(`Failed to publish service binding ${name}: ${result.shortText}${result.longText ? ` — ${result.longText}` : ''}`);
|
|
1242
|
+
}
|
|
1243
|
+
let srvbInfo;
|
|
1244
|
+
try {
|
|
1245
|
+
srvbInfo = await client.getSrvb(name);
|
|
1246
|
+
}
|
|
1247
|
+
catch {
|
|
1248
|
+
if (result.severity === 'UNKNOWN') {
|
|
1249
|
+
return errorResult(`Publish response for ${name} could not be parsed and readback failed — use SAPRead to verify publish status.`);
|
|
1250
|
+
}
|
|
1251
|
+
return textResult(`Successfully published service binding ${name} (readback of binding metadata failed — use SAPRead to verify)`);
|
|
1252
|
+
}
|
|
1253
|
+
// Verify the published flag from the SRVB readback
|
|
1254
|
+
try {
|
|
1255
|
+
const srvbData = JSON.parse(srvbInfo);
|
|
1256
|
+
if (srvbData.published === false) {
|
|
1257
|
+
return errorResult(`Publish of service binding ${name} may have failed — binding is still unpublished.\n\n${srvbInfo}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
// If we can't parse the readback JSON, fall through — better to return what we have
|
|
1262
|
+
}
|
|
1263
|
+
if (result.severity === 'UNKNOWN') {
|
|
1264
|
+
return textResult(`Publish request for ${name} completed but response could not be fully parsed. Verify status below:\n\n${srvbInfo}`);
|
|
1265
|
+
}
|
|
1266
|
+
return textResult(`Successfully published service binding ${name}.\n\n${srvbInfo}`);
|
|
1267
|
+
}
|
|
1268
|
+
// Unpublish service binding
|
|
1269
|
+
if (action === 'unpublish_srvb') {
|
|
1270
|
+
if (!name) {
|
|
1271
|
+
return errorResult('Missing required "name" parameter for unpublish_srvb action.');
|
|
1272
|
+
}
|
|
1273
|
+
const result = await unpublishServiceBinding(client.http, client.safety, name, version);
|
|
1274
|
+
if (result.severity === 'ERROR') {
|
|
1275
|
+
return errorResult(`Failed to unpublish service binding ${name}: ${result.shortText}${result.longText ? ` — ${result.longText}` : ''}`);
|
|
1276
|
+
}
|
|
1277
|
+
let srvbInfo;
|
|
1278
|
+
try {
|
|
1279
|
+
srvbInfo = await client.getSrvb(name);
|
|
1280
|
+
}
|
|
1281
|
+
catch {
|
|
1282
|
+
// Readback failed — fall through with what we have
|
|
1283
|
+
}
|
|
1284
|
+
// Verify the published flag from the SRVB readback
|
|
1285
|
+
if (srvbInfo) {
|
|
1286
|
+
try {
|
|
1287
|
+
const srvbData = JSON.parse(srvbInfo);
|
|
1288
|
+
if (srvbData.published === true) {
|
|
1289
|
+
return errorResult(`Unpublish of service binding ${name} may have failed — binding is still published.\n\n${srvbInfo}`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
catch {
|
|
1293
|
+
// If we can't parse the readback JSON, fall through
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (result.severity === 'UNKNOWN') {
|
|
1297
|
+
return textResult(`Unpublish request for ${name} completed but response could not be fully parsed.${srvbInfo ? ` Verify status below:\n\n${srvbInfo}` : ' Use SAPRead to verify status.'}`);
|
|
1298
|
+
}
|
|
1299
|
+
return textResult(`Successfully unpublished service binding ${name}.${srvbInfo ? `\n\n${srvbInfo}` : ''}`);
|
|
1300
|
+
}
|
|
1043
1301
|
// Batch activation: multiple objects at once (for RAP stacks etc.)
|
|
1302
|
+
const type = String(args.type ?? '');
|
|
1044
1303
|
if (args.objects && Array.isArray(args.objects)) {
|
|
1045
1304
|
const objects = args.objects.map((o) => {
|
|
1046
1305
|
const objType = String(o.type ?? type);
|
|
@@ -1055,7 +1314,6 @@ async function handleSAPActivate(client, args) {
|
|
|
1055
1314
|
return errorResult(`Batch activation failed for: ${names}.\nErrors: ${result.messages.join('; ')}`);
|
|
1056
1315
|
}
|
|
1057
1316
|
// Single activation (existing behavior)
|
|
1058
|
-
const name = String(args.name ?? '');
|
|
1059
1317
|
const objectUrl = objectUrlForType(type, name);
|
|
1060
1318
|
const result = await activate(client.http, client.safety, objectUrl);
|
|
1061
1319
|
if (result.success) {
|
|
@@ -1137,8 +1395,63 @@ async function handleSAPNavigate(client, args) {
|
|
|
1137
1395
|
const proposals = await getCompletion(client.http, client.safety, uri, line, column, source);
|
|
1138
1396
|
return textResult(JSON.stringify(proposals, null, 2));
|
|
1139
1397
|
}
|
|
1398
|
+
case 'hierarchy': {
|
|
1399
|
+
const className = String(args.name ?? '').toUpperCase();
|
|
1400
|
+
if (!className) {
|
|
1401
|
+
return errorResult('Provide name (class name) for hierarchy lookup.');
|
|
1402
|
+
}
|
|
1403
|
+
// Sanitize to prevent SQL injection — class names are alphanumeric + underscore + namespace slash
|
|
1404
|
+
const safeName = className.replace(/[^A-Z0-9_/]/g, '');
|
|
1405
|
+
if (safeName !== className) {
|
|
1406
|
+
return errorResult(`Invalid class name: "${className}". Only alphanumeric characters, underscores, and slashes are allowed.`);
|
|
1407
|
+
}
|
|
1408
|
+
const canFreeSQL = isOperationAllowed(client.safety, OperationType.FreeSQL);
|
|
1409
|
+
const canQuery = isOperationAllowed(client.safety, OperationType.Query);
|
|
1410
|
+
if (!canFreeSQL && !canQuery) {
|
|
1411
|
+
return errorResult('Class hierarchy requires data access permissions. ' +
|
|
1412
|
+
'Enable free SQL (--block-free-sql=false) or table preview (--block-data=false).');
|
|
1413
|
+
}
|
|
1414
|
+
try {
|
|
1415
|
+
let ownRels;
|
|
1416
|
+
let subRels;
|
|
1417
|
+
if (canFreeSQL) {
|
|
1418
|
+
ownRels = await client.runQuery(`SELECT CLSNAME, REFCLSNAME, RELTYPE FROM SEOMETAREL WHERE CLSNAME = '${safeName}'`, 100);
|
|
1419
|
+
subRels = await client.runQuery(`SELECT CLSNAME FROM SEOMETAREL WHERE REFCLSNAME = '${safeName}' AND RELTYPE = '2'`, 100);
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
// Fall back to named table preview (Query op type)
|
|
1423
|
+
ownRels = await client.getTableContents('SEOMETAREL', 100, `CLSNAME = '${safeName}'`);
|
|
1424
|
+
subRels = await client.getTableContents('SEOMETAREL', 100, `REFCLSNAME = '${safeName}' AND RELTYPE = '2'`);
|
|
1425
|
+
}
|
|
1426
|
+
let superclass = null;
|
|
1427
|
+
const interfaces = [];
|
|
1428
|
+
for (let i = 0; i < ownRels.rows.length; i++) {
|
|
1429
|
+
const row = ownRels.rows[i];
|
|
1430
|
+
const reltype = String(row.RELTYPE ?? '').trim();
|
|
1431
|
+
const refName = String(row.REFCLSNAME ?? '').trim();
|
|
1432
|
+
if (reltype === '2') {
|
|
1433
|
+
superclass = refName;
|
|
1434
|
+
}
|
|
1435
|
+
else if (reltype === '1') {
|
|
1436
|
+
interfaces.push(refName);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const subclasses = [];
|
|
1440
|
+
for (let i = 0; i < subRels.rows.length; i++) {
|
|
1441
|
+
subclasses.push(String(subRels.rows[i].CLSNAME ?? '').trim());
|
|
1442
|
+
}
|
|
1443
|
+
const result = { className: safeName, superclass, interfaces, subclasses };
|
|
1444
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1445
|
+
}
|
|
1446
|
+
catch (err) {
|
|
1447
|
+
if (err instanceof AdtApiError && err.statusCode === 404) {
|
|
1448
|
+
return errorResult('Cannot query SEOMETAREL — table may not be accessible on this system.');
|
|
1449
|
+
}
|
|
1450
|
+
throw err;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1140
1453
|
default:
|
|
1141
|
-
return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion`);
|
|
1454
|
+
return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion, hierarchy`);
|
|
1142
1455
|
}
|
|
1143
1456
|
}
|
|
1144
1457
|
// ─── SAPDiagnose Handler ─────────────────────────────────────────────
|
|
@@ -1229,6 +1542,8 @@ async function handleSAPTransport(client, args) {
|
|
|
1229
1542
|
if (!description)
|
|
1230
1543
|
return errorResult('Description is required for "create" action.');
|
|
1231
1544
|
const id = await createTransport(client.http, client.safety, description);
|
|
1545
|
+
if (!id)
|
|
1546
|
+
return errorResult('Transport creation succeeded but no transport ID was returned. Check the SAP system manually.');
|
|
1232
1547
|
return textResult(`Created transport request: ${id}`);
|
|
1233
1548
|
}
|
|
1234
1549
|
case 'release': {
|
|
@@ -1382,6 +1697,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
1382
1697
|
featureConfig.amdp = config.featureAmdp;
|
|
1383
1698
|
featureConfig.ui5 = config.featureUi5;
|
|
1384
1699
|
featureConfig.transport = config.featureTransport;
|
|
1700
|
+
featureConfig.ui5repo = config.featureUi5Repo;
|
|
1385
1701
|
const probed = await probeFeatures(client.http, featureConfig, config.systemType);
|
|
1386
1702
|
// In PP mode with a per-user client, auth-sensitive results (401/403 on any
|
|
1387
1703
|
// feature) must not poison the global cache — another user may have different
|