@cspeach/cli 1.1.41 → 1.1.42
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/.cspeach/project.json +4 -0
- package/dist/approvals/op-labels.js +18 -1
- package/dist/approvals/render.js +27 -7
- package/dist/approvals/risk-floor.js +16 -0
- package/dist/renderer/tool-labels.js +4 -1
- package/dist/sap/profile-features.js +5 -0
- package/dist/tools/approval.js +8 -1
- package/dist/tools/sap-read.js +79 -1
- package/dist/tools/sap-write.js +159 -1
- package/dist/tools/write-pipeline.js +5 -0
- package/dist/ui/approval-modal.js +16 -4
- package/package.json +2 -2
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { typeCode } from './risk-floor.js';
|
|
2
|
+
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
1
3
|
/**
|
|
2
4
|
* Human-readable operation + object labels for the approval surfaces.
|
|
3
5
|
*
|
|
@@ -36,7 +38,8 @@ const TYPE_WORDS = {
|
|
|
36
38
|
TTYP: 'table type',
|
|
37
39
|
MSAG: 'message class',
|
|
38
40
|
DEVC: 'package',
|
|
39
|
-
ENHO: '
|
|
41
|
+
ENHO: 'BAdI implementation',
|
|
42
|
+
ENHS: 'BAdI enhancement spot',
|
|
40
43
|
XSLT: 'transformation',
|
|
41
44
|
SHLP: 'search help',
|
|
42
45
|
};
|
|
@@ -49,6 +52,11 @@ export function humanType(type) {
|
|
|
49
52
|
const key = type.trim().toUpperCase();
|
|
50
53
|
if (TYPE_WORDS[key])
|
|
51
54
|
return TYPE_WORDS[key];
|
|
55
|
+
// Part 2b D7 — the BAdI types read the same with a subtype ('ENHO/XHB').
|
|
56
|
+
// Every other type keeps today's label.
|
|
57
|
+
const code = typeCode(type);
|
|
58
|
+
if (code === 'ENHO' || code === 'ENHS')
|
|
59
|
+
return TYPE_WORDS[code];
|
|
52
60
|
// Already a word (has a lowercase letter or a space)? use it verbatim.
|
|
53
61
|
if (/[a-z ]/.test(type))
|
|
54
62
|
return type;
|
|
@@ -60,6 +68,15 @@ export function verbLine(change) {
|
|
|
60
68
|
.filter(Boolean)
|
|
61
69
|
.join(' ');
|
|
62
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Part 2b D7 — "Affects: <process>" for a change that names the standard SAP
|
|
73
|
+
* process it runs in (a BAdI implementation); null when it names none. The
|
|
74
|
+
* text comes from the model: control characters are removed.
|
|
75
|
+
*/
|
|
76
|
+
export function affectsLine(change) {
|
|
77
|
+
const t = sanitizeControlChars(String(change.affects ?? '')).trim();
|
|
78
|
+
return t ? `Affects: ${t}` : null;
|
|
79
|
+
}
|
|
63
80
|
/**
|
|
64
81
|
* The transport to DISPLAY on the approval surface for one change — mirrors
|
|
65
82
|
* how the write tools actually resolve it (traced in sap-write.ts, 2026-07-05
|
package/dist/approvals/render.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { select, input } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { riskReasonText, shownRiskReasons } from './risk-floor.js';
|
|
4
|
-
import { verbLine, viewAffordance, toSourceLines } from './op-labels.js';
|
|
4
|
+
import { verbLine, viewAffordance, toSourceLines, affectsLine } from './op-labels.js';
|
|
5
5
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
6
6
|
import { approvalEmitter } from '../ui/approval-emitter.js';
|
|
7
7
|
import { withInquirer } from '../repl/inquirer-guard.js';
|
|
@@ -114,15 +114,29 @@ export async function renderPlanGate(summary, changes, transport, effectiveRisk,
|
|
|
114
114
|
if (shownReasons.length > 0) {
|
|
115
115
|
facts.push(`Risk raised to ${effectiveRisk.level}: ${shownReasons.map(riskReasonText).join('; ')}`);
|
|
116
116
|
}
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
// Part 2b D7 — a step that names the process it runs in (a BAdI
|
|
118
|
+
// implementation) carries its "Affects:" row right under it. The card's
|
|
119
|
+
// step budget counts rows, and a step is never shown without its row.
|
|
120
|
+
// No affects anywhere: one row per step, exactly as before.
|
|
121
|
+
const steps = visibleChanges.map((c, i) => {
|
|
122
|
+
const affects = affectsLine(c);
|
|
123
|
+
return [`${i + 1}. ${verbLine(c)}`, ...(affects ? [` ${affects}`] : [])];
|
|
124
|
+
});
|
|
125
|
+
const totalRows = steps.reduce((n, s) => n + s.length, 0);
|
|
126
|
+
if (totalRows > MAX_STEPS) {
|
|
119
127
|
// Object names are model-supplied: neutralise control characters
|
|
120
128
|
// before they reach scrollback (the card sanitises its own rows).
|
|
121
|
-
emitTrace(sanitizeControlChars(['Plan', ...steps.map((s) => ` ${s}`)].join('\n')));
|
|
122
|
-
|
|
129
|
+
emitTrace(sanitizeControlChars(['Plan', ...steps.flat().map((s) => ` ${s}`)].join('\n')));
|
|
130
|
+
let shown = 0;
|
|
131
|
+
let rows = 0;
|
|
132
|
+
while (shown < steps.length && (shown === 0 || rows + steps[shown].length <= MAX_STEPS)) {
|
|
133
|
+
rows += steps[shown].length;
|
|
134
|
+
shown += 1;
|
|
135
|
+
}
|
|
136
|
+
facts.push(...steps.slice(0, shown).flat(), `${getGlyphs().more} ${steps.length - shown} more steps, listed above`);
|
|
123
137
|
}
|
|
124
138
|
else {
|
|
125
|
-
facts.push(...steps);
|
|
139
|
+
facts.push(...steps.flat());
|
|
126
140
|
}
|
|
127
141
|
const res = await askConfirm({
|
|
128
142
|
id: 'plan-gate',
|
|
@@ -162,6 +176,10 @@ async function classicPlanGate(summary, visibleChanges, transport, effectiveRisk
|
|
|
162
176
|
}
|
|
163
177
|
visibleChanges.forEach((c, i) => {
|
|
164
178
|
console.log(` ${chalk.dim(`${i + 1}.`)} ${verbLine(c)}`);
|
|
179
|
+
// Part 2b D7 — the process a BAdI implementation runs in, under its line.
|
|
180
|
+
const affects = affectsLine(c);
|
|
181
|
+
if (affects)
|
|
182
|
+
console.log(` ${chalk.dim(affects)}`);
|
|
165
183
|
});
|
|
166
184
|
console.log('');
|
|
167
185
|
const choice = await withInquirer(() => select({
|
|
@@ -193,8 +211,10 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
193
211
|
if (aff)
|
|
194
212
|
choices.push({ value: 'diff', name: aff.label });
|
|
195
213
|
choices.push({ value: 'skip', name: 'Skip (tell model why)' }, { value: 'cancel', name: 'Cancel turn' });
|
|
214
|
+
// Part 2b D7 — the process a BAdI implementation runs in, under the verb line.
|
|
215
|
+
const affects = affectsLine(change);
|
|
196
216
|
const choice = await withInquirer(() => select({
|
|
197
|
-
message: `${objLabel} — apply
|
|
217
|
+
message: `${objLabel} — apply?${affects ? `\n ${chalk.dim(affects)}` : ''}`,
|
|
198
218
|
default: 'apply',
|
|
199
219
|
theme: inquirerTheme,
|
|
200
220
|
choices,
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Part 2b D7 — the type code of a change or id: trimmed, upper case, without a
|
|
3
|
+
* `/subtype` ('enho/xhb' → 'ENHO'). The ENHO risk floor, the affects_required
|
|
4
|
+
* refusal and the write tool's type binding all use it, so they always agree.
|
|
5
|
+
*/
|
|
6
|
+
export function typeCode(type) {
|
|
7
|
+
return String(type ?? '').trim().toUpperCase().split('/')[0].trim();
|
|
8
|
+
}
|
|
1
9
|
export function effectiveRisk(declared, changes, ctx) {
|
|
2
10
|
let floor = declared;
|
|
3
11
|
const raisedBy = [];
|
|
@@ -28,6 +36,13 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
28
36
|
raisedBy.push('syntax_errors');
|
|
29
37
|
}
|
|
30
38
|
}
|
|
39
|
+
// Part 2b D7 — a BAdI implementation runs inside a standard SAP process:
|
|
40
|
+
// high (never auto-approved), and the reason is always shown.
|
|
41
|
+
if (changes.some((c) => typeCode(c.type) === 'ENHO')) {
|
|
42
|
+
raise('high', 'standard_process');
|
|
43
|
+
if (!raisedBy.includes('standard_process'))
|
|
44
|
+
raisedBy.push('standard_process');
|
|
45
|
+
}
|
|
31
46
|
if (changes.some((c) => c.op === 'delete'))
|
|
32
47
|
raise('medium', 'delete_op');
|
|
33
48
|
// Round 4 item 8a — these were both labelled 'shared_transport', so a plan
|
|
@@ -58,6 +73,7 @@ const RISK_REASON_TEXT = {
|
|
|
58
73
|
escalated_in_session: 'more than 5 changes in one plan',
|
|
59
74
|
prod_namespace: 'the object is in a production namespace',
|
|
60
75
|
syntax_errors: 'the last syntax check of this object found errors',
|
|
76
|
+
standard_process: 'this code runs inside a standard SAP process (a BAdI implementation)',
|
|
61
77
|
};
|
|
62
78
|
/** Round 8 item 4 — every code the floor can raise (each has plain words above). */
|
|
63
79
|
export const RISK_REASON_CODES = Object.keys(RISK_REASON_TEXT);
|
|
@@ -167,6 +167,9 @@ const RULES = {
|
|
|
167
167
|
rules_propose: () => ({ verb: 'Check a proposed decision rule', object: 'nothing is saved' }),
|
|
168
168
|
team_estate: (a) => ({ verb: 'Read estate inventory', object: str(a['packages']) ?? 'all packages' }),
|
|
169
169
|
sap_badi_list: (a) => ({ verb: 'Searched BAdIs', object: shortRight(`"${str(a['query']) ?? ''}"`) }),
|
|
170
|
+
sap_badi_read: (a) => ({ verb: 'Read BAdI', object: shortRight([str(a['kind']), str(a['name'])].filter(Boolean).join(' ')) }),
|
|
171
|
+
sap_enhancement_options: (a) => ({ verb: 'Read enhancement options', object: shortRight([str(a['type']), str(a['name'])].filter(Boolean).join(' ')) }),
|
|
172
|
+
sap_create_badi_implementation: sapObject('Created BAdI implementation'),
|
|
170
173
|
sap_snapshot_list: (a) => ({ verb: 'Listed snapshots', object: shortRight(str(a['name']) ?? '') }),
|
|
171
174
|
sap_snapshot_read: sapObject('Read snapshot'),
|
|
172
175
|
sap_snapshot_cleanup: () => ({ verb: 'Cleaned snapshots', object: '' }),
|
|
@@ -265,7 +268,7 @@ const PRESENT = {
|
|
|
265
268
|
'Read snapshot': 'Reading snapshot of', 'Cleaned snapshots': 'Cleaning snapshots',
|
|
266
269
|
'Created transport': 'Creating transport', 'Transport for': 'Finding transport for',
|
|
267
270
|
'Listed transports': 'Listing transports', Released: 'Releasing', 'Created domain': 'Creating domain',
|
|
268
|
-
'Created data element': 'Creating data element', Locked: 'Locking', Unlocked: 'Unlocking',
|
|
271
|
+
'Created data element': 'Creating data element', 'Read BAdI': 'Reading BAdI', 'Read enhancement options': 'Reading enhancement options of', 'Created BAdI implementation': 'Creating BAdI implementation', Locked: 'Locking', Unlocked: 'Unlocking',
|
|
269
272
|
Published: 'Publishing', Unpublished: 'Unpublishing', Messages: 'Maintaining messages in',
|
|
270
273
|
'Number range': 'Maintaining number range', Question: 'Asking', 'Updated todos': 'Updating todos',
|
|
271
274
|
'Ran skill': 'Running skill', 'Read document': 'Reading document', Fetched: 'Fetching',
|
|
@@ -26,6 +26,10 @@ export const FEATURES = Object.freeze({
|
|
|
26
26
|
'datapreview.freestyle': { label: 'SQL data preview', collection: `${A}/datapreview/freestyle`, provenOnOurBox: true },
|
|
27
27
|
'msag': { label: 'Message classes', collection: `${A}/messageclass`, provenOnOurBox: true },
|
|
28
28
|
'nrob': { label: 'Number range objects', collection: `${A}/numberranges/objects`, provenOnOurBox: true },
|
|
29
|
+
// Part 2b: no `since`. The gap table's "enhancement editor 1809, BAdI editor
|
|
30
|
+
// 2020" has no cited source (Part 1 I1 rule): listed = yes, else unknown.
|
|
31
|
+
'enh.badiImplementations': { label: 'BAdI implementations', collection: `${A}/enhancements/enhoxhb`, provenOnOurBox: true },
|
|
32
|
+
'enh.badiSpots': { label: 'BAdI enhancement spots', collection: `${A}/enhancements/enhsxsb`, provenOnOurBox: true },
|
|
29
33
|
});
|
|
30
34
|
export const FEATURE_IDS = Object.freeze(Object.keys(FEATURES));
|
|
31
35
|
export function isFeatureId(s) {
|
|
@@ -37,6 +41,7 @@ export const OBJECT_TYPE_FEATURE = Object.freeze({
|
|
|
37
41
|
DDLS: 'cds.ddls', CDS: 'cds.ddls', DDLX: 'cds.ddlx', DCLS: 'cds.dcls',
|
|
38
42
|
TABL: 'ddic.tables', STRU: 'ddic.structures', DOMA: 'ddic.domains', DTEL: 'ddic.dataelements',
|
|
39
43
|
MSAG: 'msag', NROB: 'nrob',
|
|
44
|
+
ENHO: 'enh.badiImplementations', ENHS: 'enh.badiSpots',
|
|
40
45
|
});
|
|
41
46
|
/** The RAP/CDS subset: the only types a READ is ever refused for (R6). */
|
|
42
47
|
export const READ_GATED_TYPES = new Set(['BDEF', 'SRVD', 'SRVB', 'DDLS', 'CDS', 'DDLX', 'DCLS']);
|
package/dist/tools/approval.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { registerTool } from './index.js';
|
|
3
3
|
import { isHeadless, shouldUseInk } from '../renderer/tty.js';
|
|
4
|
-
import { effectiveRisk, riskReasonText } from '../approvals/risk-floor.js';
|
|
4
|
+
import { effectiveRisk, riskReasonText, typeCode } from '../approvals/risk-floor.js';
|
|
5
5
|
import { objectsWithSyntaxErrors } from './syntax-state.js';
|
|
6
6
|
import { noteWriteTransport } from '../repl/current-transport.js';
|
|
7
7
|
/**
|
|
@@ -140,6 +140,7 @@ registerTool({
|
|
|
140
140
|
type: { type: 'string' },
|
|
141
141
|
diff: { type: 'string', description: 'For a CREATE op this MUST be the exact, complete source that will be written — verbatim, character-for-character, NOT a description, summary, or paraphrase. It is shown to the developer in the approval box as the exact thing they are approving, so a one-line summary here means they approve a write they cannot inspect. For a MODIFY op, the real unified diff of the change (again verbatim, not a description).' },
|
|
142
142
|
package: { type: 'string', description: 'Target package (development class) for the object, if known — shown on the approval box' },
|
|
143
|
+
affects: { type: 'string', description: 'The standard SAP process this change runs inside, in plain words — REQUIRED for type ENHO (a BAdI implementation): name the BAdI, its spot and what calls it, e.g. "purchase order check before save (BAdI ME_PROCESS_PO_CUST, spot ES_MM_PO)". Shown on the approval card.' },
|
|
143
144
|
},
|
|
144
145
|
required: ['op', 'object', 'type'],
|
|
145
146
|
},
|
|
@@ -196,6 +197,12 @@ registerTool({
|
|
|
196
197
|
};
|
|
197
198
|
}
|
|
198
199
|
}
|
|
200
|
+
// Part 2b D7 — an enhancement change never reaches a card without naming the process it runs in.
|
|
201
|
+
const unnamed = (args.changes ?? []).filter((c) => typeCode(c.type) === 'ENHO' && !String(c.affects ?? '').trim());
|
|
202
|
+
if (unnamed.length > 0) {
|
|
203
|
+
return { content: JSON.stringify({ error: 'affects_required', approved: false, changes: unnamed.map((c) => c.object),
|
|
204
|
+
message: 'A BAdI implementation changes a standard SAP process. Say which one in changes[].affects (the BAdI, its spot and what calls it), then ask again.' }), is_error: true };
|
|
205
|
+
}
|
|
199
206
|
let cfg = await loadConfig();
|
|
200
207
|
// Task 2 (ux-wave1) — the approval flow reads the EFFECTIVE write mode:
|
|
201
208
|
// config overlaid by the session override, clamped to the active alias's
|
package/dist/tools/sap-read.js
CHANGED
|
@@ -20,12 +20,15 @@
|
|
|
20
20
|
* runUnitTest(type, name)
|
|
21
21
|
* odataTest(servicePath, entity?, method?)
|
|
22
22
|
* badiList(query, enhancementSpot?, maxResults?)
|
|
23
|
+
* readBadiSpot(name) / readBadiImplementation(name) (Part 2b)
|
|
24
|
+
* enhancementOptions(type, name) (Part 2b)
|
|
23
25
|
*/
|
|
24
26
|
import { registerTool } from './index.js';
|
|
25
27
|
import { byObjectTypeForRead } from '../sap/profile-gate.js';
|
|
26
28
|
import { normaliseSyntaxResult, checkSyntax } from './verify.js';
|
|
27
29
|
import { recordSyntaxCheck } from './syntax-state.js';
|
|
28
30
|
import { spillToolResult } from './result-spill.js';
|
|
31
|
+
import { SapError } from '@cspeach/sap-client';
|
|
29
32
|
// -----------------------------------------------------------------------
|
|
30
33
|
// Task 17 (spec D12, ruling F18) — model-facing formats.
|
|
31
34
|
// -----------------------------------------------------------------------
|
|
@@ -746,7 +749,7 @@ registerTool({
|
|
|
746
749
|
// -----------------------------------------------------------------------
|
|
747
750
|
registerTool({
|
|
748
751
|
name: 'sap_badi_list',
|
|
749
|
-
description: 'Search for BAdI definitions and enhancement spots by name pattern (supports * wildcard), with optional enhancement-spot drill-down.',
|
|
752
|
+
description: 'Search for BAdI definitions and enhancement spots by name pattern (supports * wildcard), with optional enhancement-spot drill-down. Classic SE19 implementations are not available through ADT.',
|
|
750
753
|
isMutating: false,
|
|
751
754
|
requiresSap: true,
|
|
752
755
|
input_schema: {
|
|
@@ -772,3 +775,78 @@ registerTool({
|
|
|
772
775
|
return { content: JSON.stringify(result, null, 2) };
|
|
773
776
|
},
|
|
774
777
|
});
|
|
778
|
+
// -----------------------------------------------------------------------
|
|
779
|
+
// 19. sap_badi_read (Part 2b) — one new-style spot or implementation
|
|
780
|
+
// -----------------------------------------------------------------------
|
|
781
|
+
const isSapNotFound = (e) => e instanceof SapError && e.httpStatus === 404;
|
|
782
|
+
const notFound = (detail) => ({ content: JSON.stringify({ error: 'not_found', detail }), is_error: true });
|
|
783
|
+
registerTool({
|
|
784
|
+
name: 'sap_badi_read',
|
|
785
|
+
description: 'Read one new-style BAdI enhancement spot (its BAdI definitions and their interfaces) or one BAdI implementation '
|
|
786
|
+
+ '(its spot, BAdI definition and implementing class). Classic SE18/SE19 objects are not available through ADT.',
|
|
787
|
+
isMutating: false,
|
|
788
|
+
requiresSap: true,
|
|
789
|
+
input_schema: {
|
|
790
|
+
type: 'object',
|
|
791
|
+
properties: {
|
|
792
|
+
kind: { type: 'string', enum: ['spot', 'implementation'], description: 'spot = an enhancement spot; implementation = a BAdI implementation' },
|
|
793
|
+
name: { type: 'string', description: 'Name of the spot or implementation' },
|
|
794
|
+
},
|
|
795
|
+
required: ['kind', 'name'],
|
|
796
|
+
},
|
|
797
|
+
handler: async (args, ctx) => {
|
|
798
|
+
const name = String(args.name ?? '').trim().toUpperCase();
|
|
799
|
+
const spot = args.kind === 'spot';
|
|
800
|
+
try {
|
|
801
|
+
const info = spot ? await ctx.adt.readBadiSpot(args.name) : await ctx.adt.readBadiImplementation(args.name);
|
|
802
|
+
return { content: JSON.stringify(info, null, 2) };
|
|
803
|
+
}
|
|
804
|
+
catch (e) {
|
|
805
|
+
if (!isSapNotFound(e))
|
|
806
|
+
throw e;
|
|
807
|
+
return notFound(spot
|
|
808
|
+
? `${name} is not a new-style BAdI enhancement spot. Classic BAdI definitions (SE18 without an enhancement spot) are not available through ADT.`
|
|
809
|
+
: `${name} is not a new-style BAdI implementation. A classic SE19 implementation is not available through ADT; CSPeach does not create or change those.`);
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
// -----------------------------------------------------------------------
|
|
814
|
+
// 20. sap_enhancement_options (Part 2b) — implicit enhancement options, read only
|
|
815
|
+
// -----------------------------------------------------------------------
|
|
816
|
+
registerTool({
|
|
817
|
+
name: 'sap_enhancement_options',
|
|
818
|
+
description: 'List the implicit enhancement options of a program, class or function group (full name, description, mode). '
|
|
819
|
+
+ 'Read only: CSPeach does not write implicit enhancements.',
|
|
820
|
+
isMutating: false,
|
|
821
|
+
requiresSap: true,
|
|
822
|
+
input_schema: {
|
|
823
|
+
type: 'object',
|
|
824
|
+
properties: {
|
|
825
|
+
type: { type: 'string', enum: ['PROG', 'CLAS', 'FUGR'], description: 'Object type: PROG, CLAS or FUGR' },
|
|
826
|
+
name: { type: 'string', description: 'Object name' },
|
|
827
|
+
max: { type: 'number', description: 'Most options to return (default 200)' },
|
|
828
|
+
},
|
|
829
|
+
required: ['type', 'name'],
|
|
830
|
+
},
|
|
831
|
+
handler: async (args, ctx) => {
|
|
832
|
+
const type = String(args.type ?? '').trim().toUpperCase();
|
|
833
|
+
const name = String(args.name ?? '').trim().toUpperCase();
|
|
834
|
+
const max = Number.isFinite(args.max) && args.max >= 1 ? Math.floor(args.max) : 200;
|
|
835
|
+
let all;
|
|
836
|
+
try {
|
|
837
|
+
all = await ctx.adt.enhancementOptions(type, args.name);
|
|
838
|
+
}
|
|
839
|
+
catch (e) {
|
|
840
|
+
if (!isSapNotFound(e))
|
|
841
|
+
throw e;
|
|
842
|
+
return notFound(`${type} ${name} does not exist.`);
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
content: JSON.stringify({
|
|
846
|
+
object: name, type, count: all.length,
|
|
847
|
+
options: all.slice(0, max).map((o) => ({ full_name: o.fullName, description: o.description, mode: o.mode })),
|
|
848
|
+
...(all.length > max ? { truncated: true } : {}),
|
|
849
|
+
}),
|
|
850
|
+
};
|
|
851
|
+
},
|
|
852
|
+
});
|
package/dist/tools/sap-write.js
CHANGED
|
@@ -32,7 +32,7 @@ import { hasRecordedSyntaxErrorsByName, clearSyntaxErrorsAfterActivation } from
|
|
|
32
32
|
import { teamFixOffers, cspeachWritesThisSession } from '../register/offer.js';
|
|
33
33
|
import { appendPending, finalizeToolCall } from '../session/pending.js';
|
|
34
34
|
import { checkTransportFit, reusedRequestRefusal } from './transport-fit-check.js';
|
|
35
|
-
import { auditLog } from '@cspeach/sap-client';
|
|
35
|
+
import { auditLog, SapError } from '@cspeach/sap-client';
|
|
36
36
|
import { ERR } from '../errors/codes.js';
|
|
37
37
|
import { interceptUpdateMethod, CancelledByUserError, } from './update-method-intercept.js';
|
|
38
38
|
// F-L2-1: statusline-only wire — records the transport a write ACTUALLY
|
|
@@ -1253,3 +1253,161 @@ registerTool({
|
|
|
1253
1253
|
};
|
|
1254
1254
|
},
|
|
1255
1255
|
});
|
|
1256
|
+
// ── sap_create_badi_implementation (Part 2b, D1/D5) ─────────────────────────
|
|
1257
|
+
//
|
|
1258
|
+
// One new-style BAdI implementation (ENHO) for a BAdI definition in an
|
|
1259
|
+
// enhancement spot, on the shared write pipeline. sap-client composes shell
|
|
1260
|
+
// POST → locked PUT → activate → read back in one call. The implementing
|
|
1261
|
+
// class must already exist, be ACTIVE and list the BAdI interface: SAP
|
|
1262
|
+
// activates an implementation even when its class is broken, and the standard
|
|
1263
|
+
// process that calls it would then fail (D5). Those checks run in preGate —
|
|
1264
|
+
// after the approval check, before the WAL and before anything changes in
|
|
1265
|
+
// SAP. A create approval is reusable for its object, so a refusal here never
|
|
1266
|
+
// uses it up (sap-badi-tools.test.ts proves it with a real id).
|
|
1267
|
+
/** SAP's own message in plain words: no "Object not found:" prefix, no internal method name, never a stack. */
|
|
1268
|
+
export function plainSapText(e) {
|
|
1269
|
+
if (!(e instanceof SapError))
|
|
1270
|
+
return e instanceof Error ? e.message : String(e);
|
|
1271
|
+
const t = e.message
|
|
1272
|
+
.replace(/^(?:Object not found|Object locked by another user|Authentication failed|CSRF rejected): /, '')
|
|
1273
|
+
.replace(/^SAP HTTP \d+: /, '')
|
|
1274
|
+
.replace(/^[a-z][A-Za-z]*\([^)]*\)(?:\s+—\s+)?/, '')
|
|
1275
|
+
.trim();
|
|
1276
|
+
return t || `SAP answered HTTP ${e.httpStatus}`;
|
|
1277
|
+
}
|
|
1278
|
+
/** The first error text of an activation, or a plain default. */
|
|
1279
|
+
export function firstError(a) {
|
|
1280
|
+
return a?.messages?.find((m) => m.severity === 'error' && m.shortText)?.shortText ?? 'SAP did not activate it';
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Does the class definition list the interface in an INTERFACES statement
|
|
1284
|
+
* (single, or chained with a colon; case-insensitive, whole name)? Comment
|
|
1285
|
+
* lines and end-of-line comments are ignored.
|
|
1286
|
+
*/
|
|
1287
|
+
export function classListsInterface(source, interfaceName) {
|
|
1288
|
+
const want = interfaceName.trim().toUpperCase();
|
|
1289
|
+
const code = source.split(/\r?\n/)
|
|
1290
|
+
.filter((l) => !l.startsWith('*'))
|
|
1291
|
+
.map((l) => l.replace(/".*$/, ''))
|
|
1292
|
+
.join('\n');
|
|
1293
|
+
for (const m of code.matchAll(/(?<![A-Za-z0-9_/~-])INTERFACES\s*:?\s*([^.]*)\./gi)) {
|
|
1294
|
+
for (const part of m[1].split(',')) {
|
|
1295
|
+
if ((part.trim().split(/\s+/)[0] ?? '').toUpperCase() === want)
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
const upperOf = (v) => String(v ?? '').trim().toUpperCase();
|
|
1302
|
+
const CREATE_BADI_IMPL_SPEC = {
|
|
1303
|
+
tool: 'sap_create_badi_implementation',
|
|
1304
|
+
approval: {
|
|
1305
|
+
object: (a) => a.name, op: 'create',
|
|
1306
|
+
// D7: only an id from the ENHO card (high risk, Affects line) writes a BAdI implementation.
|
|
1307
|
+
boundType: 'ENHO',
|
|
1308
|
+
claims: (a, ctx) => ({ type: 'ENHO', package: a.package, transport: a.transport ?? ctx.currentTransport?.get() ?? null }),
|
|
1309
|
+
},
|
|
1310
|
+
transport: { mode: 'three-tier' },
|
|
1311
|
+
snapshot: { kind: 'none', reason: 'a new enhancement implementation has no prior document' },
|
|
1312
|
+
// D5 — refuse before the WAL: spot readable, definition in it, class active and implementing the interface.
|
|
1313
|
+
preGate: async (a, ctx) => {
|
|
1314
|
+
let spot;
|
|
1315
|
+
try {
|
|
1316
|
+
spot = await ctx.adt.readBadiSpot(a.enhancement_spot);
|
|
1317
|
+
}
|
|
1318
|
+
catch (e) {
|
|
1319
|
+
return { error: { error: 'spot_not_found', detail: plainSapText(e), next: 'Check the enhancement spot name with sap_badi_list. Classic BAdI definitions (SE18 without an enhancement spot) are not available through ADT.' } };
|
|
1320
|
+
}
|
|
1321
|
+
const def = spot.definitions.find((d) => d.name === upperOf(a.badi_definition));
|
|
1322
|
+
if (!def) {
|
|
1323
|
+
return { error: { error: 'badi_definition_not_in_spot', detail: `Spot ${spot.name} has no BAdI definition ${upperOf(a.badi_definition)}. It has: ${spot.definitions.map((d) => d.name).join(', ') || 'none'}.` } };
|
|
1324
|
+
}
|
|
1325
|
+
if (!def.interfaceName?.trim()) {
|
|
1326
|
+
return { error: { error: 'badi_definition_without_interface', detail: `BAdI ${def.name} in spot ${spot.name} names no interface, so the implementing class cannot be checked. Check the BAdI definition in ADT.` } };
|
|
1327
|
+
}
|
|
1328
|
+
const cls = upperOf(a.implementing_class);
|
|
1329
|
+
const state = await verifyActive(ctx.adt, cls, 'CLAS'); // the read-only check (inactive list by uri)
|
|
1330
|
+
if (state.verifyError !== undefined)
|
|
1331
|
+
return { error: { error: 'class_state_unknown', detail: state.verifyError } };
|
|
1332
|
+
let source;
|
|
1333
|
+
try {
|
|
1334
|
+
source = await ctx.adt.getSource('CLAS', cls);
|
|
1335
|
+
}
|
|
1336
|
+
catch (e) {
|
|
1337
|
+
const why = e instanceof SapError && e.httpStatus === 404 ? `Class ${cls} does not exist.` : `Class ${cls} could not be read: ${plainSapText(e)}.`;
|
|
1338
|
+
return { error: { error: 'class_not_found', detail: `${why} Create and activate the implementing class first.` } };
|
|
1339
|
+
}
|
|
1340
|
+
if (!state.active) {
|
|
1341
|
+
return { error: { error: 'class_not_active', detail: `Class ${cls} is not active. Activate it first; a BAdI implementation that points at an inactive class would fail inside the standard process that calls it.` } };
|
|
1342
|
+
}
|
|
1343
|
+
if (!classListsInterface(source, def.interfaceName)) {
|
|
1344
|
+
return { error: { error: 'class_lacks_interface', detail: `Class ${cls} does not list INTERFACES ${def.interfaceName} (the interface of BAdI ${def.name}). Add it, activate the class, then try again.` } };
|
|
1345
|
+
}
|
|
1346
|
+
return { ok: true, extra: { interface_name: def.interfaceName } };
|
|
1347
|
+
},
|
|
1348
|
+
walArgs: (a) => ({ name: a.name, type: 'ENHO', package: a.package, enhancement_spot: a.enhancement_spot, badi_definition: a.badi_definition, implementing_class: a.implementing_class }),
|
|
1349
|
+
call: (a, ctx, pre) => ctx.adt.createBadiImplementation({
|
|
1350
|
+
name: a.name, description: a.description, packageName: a.package,
|
|
1351
|
+
enhancementSpot: a.enhancement_spot, badiDefinition: a.badi_definition, implementingClass: a.implementing_class,
|
|
1352
|
+
}, pre.transport),
|
|
1353
|
+
errorCode: 'create_failed',
|
|
1354
|
+
// SAP's own answers decide "landed"; whether it is really ACTIVE is Gate 4's
|
|
1355
|
+
// (verify.activation) — one rule for every write (preflight F-05).
|
|
1356
|
+
landed: (r) => (r.status === 'created' && r.written && r.activated
|
|
1357
|
+
? { ok: true }
|
|
1358
|
+
: {
|
|
1359
|
+
ok: false,
|
|
1360
|
+
detail: r.error ?? (!r.created ? 'SAP did not create the implementation'
|
|
1361
|
+
: !r.written ? 'the implementation document was not written'
|
|
1362
|
+
: `activation failed: ${firstError(r.activationResult)}`),
|
|
1363
|
+
extra: {
|
|
1364
|
+
created: r.created, written: r.written, activated: r.activated,
|
|
1365
|
+
...(r.activationResult?.messages?.length ? { activation_messages: r.activationResult.messages.map((m) => m.shortText) } : {}),
|
|
1366
|
+
},
|
|
1367
|
+
audit: { status: 'error', reason: r.error ?? 'not activated' },
|
|
1368
|
+
}),
|
|
1369
|
+
audit: {
|
|
1370
|
+
method: 'createBadiImplementation', type: () => 'ENHO', name: (a) => a.name,
|
|
1371
|
+
detail: (a) => `badi=${upperOf(a.badi_definition)} spot=${upperOf(a.enhancement_spot)} class=${upperOf(a.implementing_class)}`,
|
|
1372
|
+
},
|
|
1373
|
+
// verifyWriteActivation reads r.activationResult, then the inactive list by uri (ENHO row → toObjectUrl).
|
|
1374
|
+
verify: { syntax: false, activation: true },
|
|
1375
|
+
walResult: (_a, r) => ({ created: true, activated: r.activated }),
|
|
1376
|
+
chain: (a, pre, _syntax, act) => ({
|
|
1377
|
+
snapshot: 'new_object', written: true, syntax: 'not_checked', activation: chainActivation(act),
|
|
1378
|
+
card: { object: upperOf(a.name), what: `BAdI implementation of ${upperOf(a.badi_definition)} (spot ${upperOf(a.enhancement_spot)})`, transport: pre.transport },
|
|
1379
|
+
}),
|
|
1380
|
+
envelope: (a, r, pre, _syntax, act) => ({
|
|
1381
|
+
created: true, object: r.objectName, type: 'ENHO', package: r.packageName,
|
|
1382
|
+
badi_definition: upperOf(a.badi_definition), enhancement_spot: upperOf(a.enhancement_spot), implementing_class: upperOf(a.implementing_class),
|
|
1383
|
+
...activatedByWrite(act), ...(pre.transport ? { transport: pre.transport } : {}),
|
|
1384
|
+
next_step: act?.success && act.verified
|
|
1385
|
+
? 'the implementation is active: the standard process now calls it on this system — test it there'
|
|
1386
|
+
: 'activation is not confirmed: check sap_inactive_objects and activate before reporting it active',
|
|
1387
|
+
}),
|
|
1388
|
+
};
|
|
1389
|
+
registerTool({
|
|
1390
|
+
name: 'sap_create_badi_implementation',
|
|
1391
|
+
requires: 'enh.badiImplementations',
|
|
1392
|
+
description: 'Create and activate a new-style BAdI implementation (enhancement implementation, ENHO) for a BAdI definition in an enhancement spot. '
|
|
1393
|
+
+ 'The implementing class must already exist, be ACTIVE and list the BAdI interface (create it with sap_create_object CLAS + sap_set_source and activate it first). '
|
|
1394
|
+
+ 'Requires approval_id from a request_approval whose change names type ENHO, the BAdI definition, its enhancement spot and the standard process it affects. '
|
|
1395
|
+
+ 'Classic SE19 implementations are not available through ADT.',
|
|
1396
|
+
isMutating: true,
|
|
1397
|
+
requiresSap: true,
|
|
1398
|
+
input_schema: {
|
|
1399
|
+
type: 'object',
|
|
1400
|
+
properties: {
|
|
1401
|
+
name: { type: 'string', description: 'Name of the new BAdI implementation (Z*/Y*)' },
|
|
1402
|
+
description: { type: 'string', description: 'Short description' },
|
|
1403
|
+
package: { type: 'string', description: 'Development package (Z*/Y* or $TMP)' },
|
|
1404
|
+
enhancement_spot: { type: 'string', description: 'The enhancement spot that holds the BAdI definition' },
|
|
1405
|
+
badi_definition: { type: 'string', description: 'The BAdI definition to implement (inside that spot)' },
|
|
1406
|
+
implementing_class: { type: 'string', description: 'The active class that implements the BAdI interface' },
|
|
1407
|
+
transport: { type: 'string', description: 'Target transport (optional — required for workbench packages)' },
|
|
1408
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
1409
|
+
},
|
|
1410
|
+
required: ['name', 'description', 'package', 'enhancement_spot', 'badi_definition', 'implementing_class', 'approval_id'],
|
|
1411
|
+
},
|
|
1412
|
+
handler: writeToolHandler(CREATE_BADI_IMPL_SPEC),
|
|
1413
|
+
});
|
|
@@ -27,6 +27,7 @@ import { readFileSync } from 'node:fs';
|
|
|
27
27
|
import { auditLog, SapError } from '@cspeach/sap-client';
|
|
28
28
|
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
29
29
|
import { grantClaimMismatch, grantOwningTransportMismatch, grantOwningTransportDetail } from '../approvals/grant-claims.js';
|
|
30
|
+
import { typeCode } from '../approvals/risk-floor.js';
|
|
30
31
|
import { autoSnapshot, autoSnapshotClassInclude, autoSnapshotMessageClass } from './snapshot.js';
|
|
31
32
|
import { includeSnapshotType } from './include-snapshot.js';
|
|
32
33
|
import { buildWriteChainLine } from '../renderer/verify-chain.js';
|
|
@@ -297,6 +298,10 @@ export async function runWritePipeline(args, ctx, spec) {
|
|
|
297
298
|
const verify = await verifyAndSpendApprovalId(raw.approval_id, spec.approval.object(args), spec.approval.op);
|
|
298
299
|
if (!verify.ok)
|
|
299
300
|
return refuse({ error: ERR.APPROVAL_INVALID, reason: verify.reason });
|
|
301
|
+
// Part 2b D7 — the id must come from a card for this exact type.
|
|
302
|
+
if (spec.approval.boundType && typeCode(verify.payload.type) !== spec.approval.boundType) {
|
|
303
|
+
return refuse({ error: ERR.APPROVAL_INVALID, reason: 'type_mismatch' });
|
|
304
|
+
}
|
|
300
305
|
// 2026-10-04 — a plan-grant id holds the write to its claims (type, package, transport).
|
|
301
306
|
if (spec.approval.claims) {
|
|
302
307
|
const claim = grantClaimMismatch(verify.payload, spec.approval.claims(args, ctx));
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useRef, useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
4
|
import { riskReasonText, shownRiskReasons } from '../approvals/risk-floor.js';
|
|
5
|
-
import { verbLine, viewAffordance, toSourceLines } from '../approvals/op-labels.js';
|
|
5
|
+
import { verbLine, viewAffordance, toSourceLines, affectsLine } from '../approvals/op-labels.js';
|
|
6
6
|
// BUG-1 (2026-07-05 live smoke): the interactive approval modal mounted
|
|
7
7
|
// DEAD — every keypress ignored — when it appeared after a guarded
|
|
8
8
|
// /abap-plan turn. Root cause is identical to the one AskCard
|
|
@@ -34,6 +34,16 @@ function riskText(effectiveRisk) {
|
|
|
34
34
|
export function riskRows(effectiveRisk, width) {
|
|
35
35
|
return wrapRows(riskText(effectiveRisk), Math.max(1, width - MODAL_CHROME_COLS), RISK_MAX_ROWS);
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Part 2b D7 — the standard SAP process a BAdI implementation runs in is a
|
|
39
|
+
* decision input too: its "Affects:" text wraps onto up to this many rows.
|
|
40
|
+
*/
|
|
41
|
+
export const AFFECTS_MAX_ROWS = 2;
|
|
42
|
+
/** The "Affects: …" rows under the verb line; none when the change names no process. */
|
|
43
|
+
export function affectsRows(change, width) {
|
|
44
|
+
const line = affectsLine(change);
|
|
45
|
+
return line ? wrapRows(line, Math.max(1, width - MODAL_CHROME_COLS), AFFECTS_MAX_ROWS) : [];
|
|
46
|
+
}
|
|
37
47
|
/**
|
|
38
48
|
* The collapsed card's rendered height at `width` (the live-area budget; the
|
|
39
49
|
* App adds its own one-row top margin). Panels: border 2 + paddingY 2 + verb
|
|
@@ -42,7 +52,7 @@ export function riskRows(effectiveRisk, width) {
|
|
|
42
52
|
*/
|
|
43
53
|
export function estimateApprovalModalRows(p, width = uiWidth()) {
|
|
44
54
|
const meta = p.change.package || p.transport ? 1 : 0;
|
|
45
|
-
const body = 1 + meta + riskRows(p.effectiveRisk, width).length + 2;
|
|
55
|
+
const body = 1 + affectsRows(p.change, width).length + meta + riskRows(p.effectiveRisk, width).length + 2;
|
|
46
56
|
return 4 + body + (getLook() === 'panels' ? 0 : 2);
|
|
47
57
|
}
|
|
48
58
|
// Legacy look: today's literal names (bytes unchanged while colour is on).
|
|
@@ -180,6 +190,8 @@ export function ApprovalModal({ change, effectiveRisk, transport, onApply, onSki
|
|
|
180
190
|
}
|
|
181
191
|
// Pre-wrapped rows, never Ink soft-wrap (Wave-1 rule): every row is counted.
|
|
182
192
|
const risks = riskRows(effectiveRisk, cardWidth);
|
|
193
|
+
// Part 2b D7 — "Affects: …" under the verb line (none for other changes).
|
|
194
|
+
const affects = affectsRows(change, cardWidth);
|
|
183
195
|
// Meta line: Package · Transport — each segment only when genuinely known.
|
|
184
196
|
// The handler resolves `transport` per op (see op-labels displayTransport:
|
|
185
197
|
// create-family may fall back to the session transport because the create
|
|
@@ -206,7 +218,7 @@ export function ApprovalModal({ change, effectiveRisk, transport, onApply, onSki
|
|
|
206
218
|
if (getLook() === 'panels') {
|
|
207
219
|
const risk = roleProps(riskRole(effectiveRisk.level));
|
|
208
220
|
const tag = inkChip('APPROVAL');
|
|
209
|
-
return (_jsxs(Box, { flexDirection: "column", width: cardWidth, borderStyle: "round", borderColor: risk.color, paddingX: 2, paddingY: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...tag.props, children: tag.text }), _jsx(Text, { children: ' ' }), _jsx(Text, { ...(change.op === 'delete' ? roleProps('bad') : {}), bold: true, children: verb }), change.op === 'create' ? _jsx(Text, { ...roleProps('dim'), children: " (new object)" }) : null] }), metaSegs.length > 0 && _jsx(Text, { ...roleProps('dim'), wrap: "truncate-end", children: metaSegs.join(' · ') }), risks.map((row, i) => _jsx(Text, { ...risk, wrap: "truncate-end", children: row }, `risk-${i}`)), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...roleProps('sap', { bold: true }), children: keys[0] }), _jsx(Text, { children: ' ' + keys.slice(1).join(' ') })] }) })] }));
|
|
221
|
+
return (_jsxs(Box, { flexDirection: "column", width: cardWidth, borderStyle: "round", borderColor: risk.color, paddingX: 2, paddingY: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...tag.props, children: tag.text }), _jsx(Text, { children: ' ' }), _jsx(Text, { ...(change.op === 'delete' ? roleProps('bad') : {}), bold: true, children: verb }), change.op === 'create' ? _jsx(Text, { ...roleProps('dim'), children: " (new object)" }) : null] }), affects.map((row, i) => _jsx(Text, { ...roleProps('dim'), wrap: "truncate-end", children: row }, `affects-${i}`)), metaSegs.length > 0 && _jsx(Text, { ...roleProps('dim'), wrap: "truncate-end", children: metaSegs.join(' · ') }), risks.map((row, i) => _jsx(Text, { ...risk, wrap: "truncate-end", children: row }, `risk-${i}`)), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...roleProps('sap', { bold: true }), children: keys[0] }), _jsx(Text, { children: ' ' + keys.slice(1).join(' ') })] }) })] }));
|
|
210
222
|
}
|
|
211
|
-
return (_jsxs(Box, { flexDirection: "column", width: cardWidth, borderStyle: "round", borderColor: legacyInkColor(riskColor(effectiveRisk.level)), paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Write approval" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: verbColor, children: verb }), change.op === 'create' ? _jsx(Text, { dimColor: true, children: " (new object)" }) : null] }) }), metaSegs.length > 0 && (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: metaSegs.join(' · ') })), risks.map((row, i) => (_jsx(Text, { color: legacyInkColor(riskColor(effectiveRisk.level)), wrap: "truncate-end", children: row }, `risk-${i}`))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { wrap: "truncate-end", children: keys.join(' ') }) })] }));
|
|
223
|
+
return (_jsxs(Box, { flexDirection: "column", width: cardWidth, borderStyle: "round", borderColor: legacyInkColor(riskColor(effectiveRisk.level)), paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Write approval" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: verbColor, children: verb }), change.op === 'create' ? _jsx(Text, { dimColor: true, children: " (new object)" }) : null] }) }), affects.map((row, i) => (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: row }, `affects-${i}`))), metaSegs.length > 0 && (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: metaSegs.join(' · ') })), risks.map((row, i) => (_jsx(Text, { color: legacyInkColor(riskColor(effectiveRisk.level)), wrap: "truncate-end", children: row }, `risk-${i}`))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { wrap: "truncate-end", children: keys.join(' ') }) })] }));
|
|
212
224
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cspeach/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.42",
|
|
4
4
|
"description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, signed skills, safety gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abap",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@anthropic-ai/sdk": "^0.90.0",
|
|
55
55
|
"@azure/msal-node": "6.0.1",
|
|
56
56
|
"@cspeach/register-core": "0.4.0",
|
|
57
|
-
"@cspeach/sap-client": "0.1.
|
|
57
|
+
"@cspeach/sap-client": "0.1.27",
|
|
58
58
|
"@iarna/toml": "^2.2.5",
|
|
59
59
|
"@inkjs/ui": "^2.0.0",
|
|
60
60
|
"@inquirer/prompts": "^7.0.0",
|