@cspeach/cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/loop.js +22 -9
- package/dist/approvals/op-labels.js +124 -0
- package/dist/approvals/render.js +42 -36
- package/dist/cli.js +15 -0
- package/dist/commands/compact.js +28 -2
- package/dist/commands/config-set.js +189 -0
- package/dist/commands/config-show.js +20 -0
- package/dist/commands/export-audit.js +43 -0
- package/dist/commands/help.js +5 -0
- package/dist/commands/plan-audit-evidence.js +266 -0
- package/dist/commands/plan-audit.js +692 -0
- package/dist/commands/plan-chain.js +671 -0
- package/dist/commands/plan-continue.js +179 -0
- package/dist/commands/plan-gate.js +154 -0
- package/dist/commands/plan-resume.js +588 -33
- package/dist/config/loader.js +128 -4
- package/dist/config/model-defaults.js +14 -0
- package/dist/cost/pricing.js +27 -1
- package/dist/doctor/checks/system-roles.js +41 -0
- package/dist/doctor/run.js +2 -0
- package/dist/models/resolve.js +61 -0
- package/dist/models/server-config.js +155 -0
- package/dist/one-shot.js +25 -3
- package/dist/projects/extract-cca.js +3 -1
- package/dist/projects/extract-modernize.js +3 -1
- package/dist/projects/extract-plan.js +60 -6
- package/dist/projects/extract-test-coverage.js +3 -1
- package/dist/projects/extract-upgrade.js +3 -1
- package/dist/projects/handover-md.js +195 -0
- package/dist/projects/index.js +1 -1
- package/dist/projects/plan-run.js +137 -13
- package/dist/projects/plan-schema.js +73 -0
- package/dist/projects/run-lease.js +157 -0
- package/dist/projects/save-command.js +26 -15
- package/dist/renderer/status-footer.js +22 -12
- package/dist/renderer/thinking-heartbeat.js +64 -8
- package/dist/renderer/todo-block.js +51 -0
- package/dist/renderer/tool-widget.js +37 -0
- package/dist/repl/bracketed-paste.js +28 -19
- package/dist/repl/builtin-commands.js +5 -0
- package/dist/repl/current-transport.js +10 -0
- package/dist/repl/history.js +86 -0
- package/dist/repl/ink-stdin-guard.js +64 -0
- package/dist/repl/mode-ceiling.js +16 -0
- package/dist/repl/mode-cycle.js +104 -0
- package/dist/repl/post-turn-status.js +24 -4
- package/dist/repl/slash-completer.js +5 -0
- package/dist/repl.js +954 -83
- package/dist/rewind/candidates.js +194 -0
- package/dist/rewind/cli.js +137 -0
- package/dist/rewind/format.js +27 -0
- package/dist/rewind/restore.js +245 -0
- package/dist/session/audit-export.js +459 -0
- package/dist/session/context-report.js +163 -0
- package/dist/session/recap.js +160 -0
- package/dist/skill-catalog.js +9 -3
- package/dist/skills/bundled-skills.js +59 -66
- package/dist/tools/approval.js +115 -7
- package/dist/tools/ask-question.js +304 -3
- package/dist/tools/extend-model/anchored-insert.js +604 -0
- package/dist/tools/extend-model/tool.js +162 -10
- package/dist/tools/fiori/fe-extend.js +76 -0
- package/dist/tools/fiori/fe-scaffold.js +29 -3
- package/dist/tools/fiori/floorplan-map.js +19 -0
- package/dist/tools/fiori/samples/data/index.json +13602 -0
- package/dist/tools/fiori/samples/data/sources.generated.js +808 -0
- package/dist/tools/fiori/samples/loader.js +248 -0
- package/dist/tools/fiori/samples/search.js +63 -0
- package/dist/tools/fiori/samples/types.js +2 -0
- package/dist/tools/fiori/smoke/assertions.js +74 -0
- package/dist/tools/fiori/smoke/browser.js +52 -0
- package/dist/tools/fiori/smoke/driver.js +89 -0
- package/dist/tools/fiori/smoke/freestyle-spec.js +317 -0
- package/dist/tools/fiori/smoke/run-smoke.js +149 -0
- package/dist/tools/fiori/tools.js +328 -3
- package/dist/tools/local-build.js +11 -1
- package/dist/tools/sap-read.js +79 -11
- package/dist/tools/sap-write.js +24 -4
- package/dist/tools/snapshot.js +27 -1
- package/dist/tools/subagent/agent_run.js +27 -3
- package/dist/tools/todo.js +144 -0
- package/dist/ui/app.js +372 -19
- package/dist/ui/approval-modal.js +49 -16
- package/dist/ui/ask-question-emitter.js +14 -0
- package/dist/ui/context-grid.js +108 -0
- package/dist/ui/footer.js +109 -30
- package/dist/ui/header.js +7 -0
- package/dist/ui/line-resolution.js +18 -2
- package/dist/ui/rewind-emitter.js +10 -0
- package/dist/ui/rewind-panel.js +81 -0
- package/dist/ui/sap-state-store.js +1 -0
- package/dist/ui/status-line.js +43 -0
- package/dist/ui/text-input.js +72 -8
- package/dist/ui/todo-emitter.js +25 -0
- package/dist/ui/todo-panel.js +64 -0
- package/dist/ui/turn-status-emitter.js +50 -4
- package/dist/ui/turn-status.js +18 -3
- package/dist/ui/widgets/ask-form.js +242 -0
- package/dist/ui/widgets/ask-question-modal.js +17 -7
- package/package.json +4 -1
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* See docs/superpowers/specs/2026-06-24-revision-aware-cspeach-design.md §5.2.
|
|
14
14
|
*/
|
|
15
15
|
import { registerTool } from '../index.js';
|
|
16
|
-
import { insertCdsField, insertDdlxLineItem, promoteCdsFieldToLineItem, addTableField, insertBdefField, buildClauseLine, buildHandlerMethod, insertBdefClause, insertHandlerMethod, createHandlerClassSkeleton, classBlockSpan, resolveBehaviorAlias, insertProjectionUse, insertMdeActionButton, } from './anchored-insert.js';
|
|
16
|
+
import { insertCdsField, insertDdlxLineItem, promoteCdsFieldToLineItem, addTableField, insertBdefField, buildClauseLine, buildHandlerMethod, insertBdefClause, insertHandlerMethod, createHandlerClassSkeleton, classBlockSpan, resolveBehaviorAlias, insertProjectionUse, insertMdeActionButton, insertDdlxHeaderInfo, insertDdlxSelectionField, insertDdlxIdentification, insertDdlxFacet, insertDdlxDataPoint, insertDdlxChart, insertCdsValueHelp, insertBdefSideEffects, } from './anchored-insert.js';
|
|
17
17
|
async function extendModelInsertHandler(args, _ctx) {
|
|
18
18
|
const { kind } = args;
|
|
19
19
|
// bdef-stub is branched BEFORE the source/field guard (which does not apply to this kind).
|
|
@@ -47,6 +47,105 @@ async function extendModelInsertHandler(args, _ctx) {
|
|
|
47
47
|
return { content: `error: mde-action-button failed — ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
// ddlx-headerinfo uses `source` + typeName/typeNamePlural/titleField (NOT `field`) — branch BEFORE the field guard.
|
|
51
|
+
if (kind === 'ddlx-headerinfo') {
|
|
52
|
+
if (typeof args.source !== 'string'
|
|
53
|
+
|| typeof args.typeName !== 'string' || args.typeName.trim() === ''
|
|
54
|
+
|| typeof args.typeNamePlural !== 'string' || args.typeNamePlural.trim() === ''
|
|
55
|
+
|| typeof args.titleField !== 'string' || args.titleField.trim() === '') {
|
|
56
|
+
return { content: 'error: ddlx-headerinfo requires source, typeName, typeNamePlural and titleField', is_error: true };
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const modified = insertDdlxHeaderInfo(args.source, {
|
|
60
|
+
typeName: args.typeName, typeNamePlural: args.typeNamePlural,
|
|
61
|
+
titleField: args.titleField, descriptionField: args.descriptionField,
|
|
62
|
+
});
|
|
63
|
+
return { content: JSON.stringify({ modified, summary: `Added a @UI.headerInfo block (typeName '${args.typeName}', title ${args.titleField}) above the annotate view.` }) };
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return { content: `error: ddlx-headerinfo failed — ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ddlx-identification uses `source`; `field` is optional (FOR_ACTION button needs none) — branch BEFORE the field guard.
|
|
70
|
+
if (kind === 'ddlx-identification') {
|
|
71
|
+
if (typeof args.source !== 'string') {
|
|
72
|
+
return { content: 'error: ddlx-identification requires source', is_error: true };
|
|
73
|
+
}
|
|
74
|
+
const forAction = (typeof args.action === 'string' && args.action.trim() !== '')
|
|
75
|
+
? { dataAction: args.action, label: args.label ?? args.action }
|
|
76
|
+
: undefined;
|
|
77
|
+
if (!forAction && (typeof args.field !== 'string' || args.field.trim() === '')) {
|
|
78
|
+
return { content: 'error: ddlx-identification requires either field (plain identification) or action (FOR_ACTION button)', is_error: true };
|
|
79
|
+
}
|
|
80
|
+
const position = typeof args.position === 'number' ? args.position : 10;
|
|
81
|
+
try {
|
|
82
|
+
const modified = insertDdlxIdentification(args.source, { field: args.field, position, forAction });
|
|
83
|
+
const summary = forAction
|
|
84
|
+
? `Added a FOR_ACTION @UI.identification button for '${forAction.dataAction}' (Object Page).`
|
|
85
|
+
: `Added @UI.identification (position ${position}) on ${args.field}.`;
|
|
86
|
+
return { content: JSON.stringify({ modified, summary }) };
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return { content: `error: ddlx-identification failed — ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ddlx-facet uses `source` + facetId/label/fieldGroupQualifier/fields (NOT a single `field`) — branch BEFORE the field guard.
|
|
93
|
+
if (kind === 'ddlx-facet') {
|
|
94
|
+
if (typeof args.source !== 'string'
|
|
95
|
+
|| typeof args.facetId !== 'string' || args.facetId.trim() === ''
|
|
96
|
+
|| typeof args.label !== 'string' || args.label.trim() === ''
|
|
97
|
+
|| typeof args.fieldGroupQualifier !== 'string' || args.fieldGroupQualifier.trim() === ''
|
|
98
|
+
|| !Array.isArray(args.fields)) {
|
|
99
|
+
return { content: 'error: ddlx-facet requires source, facetId, label, fieldGroupQualifier and a fields array', is_error: true };
|
|
100
|
+
}
|
|
101
|
+
const position = typeof args.position === 'number' ? args.position : 10;
|
|
102
|
+
try {
|
|
103
|
+
const modified = insertDdlxFacet(args.source, {
|
|
104
|
+
facetId: args.facetId, label: args.label, fieldGroupQualifier: args.fieldGroupQualifier,
|
|
105
|
+
position, fields: args.fields,
|
|
106
|
+
});
|
|
107
|
+
return { content: JSON.stringify({ modified, summary: `Added facet '${args.facetId}' (field group '${args.fieldGroupQualifier}') + @UI.fieldGroup on ${args.fields.length} field(s).` }) };
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
return { content: `error: ddlx-facet failed — ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ddlx-chart uses `source` + chartType/dimensions/measures (NOT a single `field`) — branch BEFORE the field guard.
|
|
114
|
+
if (kind === 'ddlx-chart') {
|
|
115
|
+
if (typeof args.source !== 'string'
|
|
116
|
+
|| typeof args.chartType !== 'string'
|
|
117
|
+
|| !Array.isArray(args.dimensions) || args.dimensions.length === 0
|
|
118
|
+
|| !Array.isArray(args.measures) || args.measures.length === 0) {
|
|
119
|
+
return { content: 'error: ddlx-chart requires source, chartType and non-empty dimensions and measures arrays', is_error: true };
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const modified = insertDdlxChart(args.source, {
|
|
123
|
+
qualifier: args.qualifier, chartType: args.chartType,
|
|
124
|
+
dimensions: args.dimensions, measures: args.measures,
|
|
125
|
+
});
|
|
126
|
+
return { content: JSON.stringify({ modified, summary: `Added a #${args.chartType} @UI.chart + paired @UI.presentationVariant (#AS_CHART)${args.qualifier ? ` (qualifier ${args.qualifier})` : ''} above the annotate view.` }) };
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
return { content: `error: ddlx-chart failed — ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// bdef-sideeffects uses `source` + sourceFields/targetFields (NOT `field`) — branch BEFORE the field guard.
|
|
133
|
+
if (kind === 'bdef-sideeffects') {
|
|
134
|
+
if (typeof args.source !== 'string'
|
|
135
|
+
|| !Array.isArray(args.sourceFields) || args.sourceFields.length === 0
|
|
136
|
+
|| !Array.isArray(args.targetFields) || args.targetFields.length === 0) {
|
|
137
|
+
return { content: 'error: bdef-sideeffects requires source and non-empty sourceFields and targetFields arrays', is_error: true };
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const modified = insertBdefSideEffects(args.source, {
|
|
141
|
+
entityAlias: args.entityAlias, sourceFields: args.sourceFields, targetFields: args.targetFields,
|
|
142
|
+
});
|
|
143
|
+
return { content: JSON.stringify({ modified, summary: `Added a side effects clause (field ${args.sourceFields.join(', ')} affects field ${args.targetFields.join(', ')}) to the managed BDEF${args.entityAlias ? ` (alias ${args.entityAlias})` : ''}.` }) };
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
return { content: `error: bdef-sideeffects failed — ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
50
149
|
const { source, field } = args;
|
|
51
150
|
if (typeof source !== 'string' || typeof field !== 'string' || field.trim() === '') {
|
|
52
151
|
return { content: 'error: source (string) and field (non-empty string) are required', is_error: true };
|
|
@@ -77,9 +176,30 @@ async function extendModelInsertHandler(args, _ctx) {
|
|
|
77
176
|
modified = insertBdefField(source, { alias: args.alias, target, clause: field });
|
|
78
177
|
summary = `Added ${target} '${field}' to BDEF${args.alias ? ` (alias ${args.alias})` : ''}.`;
|
|
79
178
|
}
|
|
179
|
+
else if (kind === 'ddlx-selectionfield') {
|
|
180
|
+
const position = typeof args.position === 'number' ? args.position : 10;
|
|
181
|
+
modified = insertDdlxSelectionField(source, { field, position });
|
|
182
|
+
summary = `Added "${field}" as an @UI.selectionField filter at position ${position} in the DDLX metadata extension.`;
|
|
183
|
+
}
|
|
184
|
+
else if (kind === 'ddlx-datapoint') {
|
|
185
|
+
if (typeof args.title !== 'string' || args.title.trim() === '') {
|
|
186
|
+
return { content: 'error: ddlx-datapoint requires a non-empty title', is_error: true };
|
|
187
|
+
}
|
|
188
|
+
modified = insertDdlxDataPoint(source, { field, title: args.title, criticalityField: args.criticalityField });
|
|
189
|
+
summary = `Added a @UI.dataPoint (title '${args.title}'${args.criticalityField ? `, criticality ${args.criticalityField}` : ''}) on ${field}.`;
|
|
190
|
+
}
|
|
191
|
+
else if (kind === 'cds-valuehelp') {
|
|
192
|
+
if (typeof args.entity !== 'string' || args.entity.trim() === '' || typeof args.element !== 'string' || args.element.trim() === '') {
|
|
193
|
+
return { content: 'error: cds-valuehelp requires a non-empty entity and element', is_error: true };
|
|
194
|
+
}
|
|
195
|
+
modified = insertCdsValueHelp(source, {
|
|
196
|
+
field, entity: args.entity, element: args.element, additionalBinding: args.additionalBinding,
|
|
197
|
+
});
|
|
198
|
+
summary = `Added a @Consumption.valueHelpDefinition (entity '${args.entity}', element ${args.element}) above ${field} in the CDS view.`;
|
|
199
|
+
}
|
|
80
200
|
else {
|
|
81
201
|
return {
|
|
82
|
-
content: `error: unknown kind "${String(kind)}" — expected "cds-field", "ddlx-lineitem", "cds-lineitem", "table-field", "bdef-field" or "bdef-
|
|
202
|
+
content: `error: unknown kind "${String(kind)}" — expected "cds-field", "ddlx-lineitem", "cds-lineitem", "table-field", "bdef-field", "bdef-stub", "projection-use", "mde-action-button", "ddlx-headerinfo", "ddlx-selectionfield", "ddlx-identification", "ddlx-facet", "ddlx-datapoint", "ddlx-chart", "cds-valuehelp" or "bdef-sideeffects"`,
|
|
83
203
|
is_error: true,
|
|
84
204
|
};
|
|
85
205
|
}
|
|
@@ -107,8 +227,21 @@ registerTool({
|
|
|
107
227
|
+ 'MANAGED BDEF and the corresponding handler method skeleton to the CCIMP include; '
|
|
108
228
|
+ 'kind="projection-use" exposes a `use action/function/association` line inside a PROJECTION BDEF '
|
|
109
229
|
+ 'entity body (non-projection BDEFs refused); kind="mde-action-button" inserts a @UI.lineItem '
|
|
110
|
-
+ 'FOR_ACTION button annotation above an anchor field in a metadata extension
|
|
111
|
-
+ '
|
|
230
|
+
+ 'FOR_ACTION button annotation above an anchor field in a metadata extension; '
|
|
231
|
+
+ 'kind="ddlx-headerinfo" inserts a view-level @UI.headerInfo block above the `annotate view` line '
|
|
232
|
+
+ '(Object Page header — idempotent, refuses a second block); kind="ddlx-selectionfield" adds an '
|
|
233
|
+
+ '@UI.selectionField filter to a DDLX metadata extension (in place above an exposed field, or '
|
|
234
|
+
+ 'appends the field + annotation when absent); kind="ddlx-identification" adds a @UI.identification '
|
|
235
|
+
+ 'annotation on a field, or a #FOR_ACTION Object-Page action button (via `action`) — refuses a '
|
|
236
|
+
+ 'duplicate dataAction; kind="ddlx-facet" adds a view-level @UI.facet (#FIELDGROUP_REFERENCE) above '
|
|
237
|
+
+ 'the `annotate view` line (merged into an existing @UI.facet array, idempotent on facetId) plus a '
|
|
238
|
+
+ 'per-field @UI.fieldGroup on each named field; kind="ddlx-datapoint" adds a @UI.dataPoint (title, '
|
|
239
|
+
+ 'optional criticality field) above an existing field; kind="ddlx-chart" adds a view-level @UI.chart '
|
|
240
|
+
+ 'and its paired @UI.presentationVariant (#AS_CHART) above the `annotate view` line, qualifier-'
|
|
241
|
+
+ 'idempotent as a unit; kind="cds-valuehelp" inserts a @Consumption.valueHelpDefinition above an '
|
|
242
|
+
+ 'EXISTING exposed CDS-view field (DDLS source; refuses multi-line/computed elements, idempotent); '
|
|
243
|
+
+ 'kind="bdef-sideeffects" inserts a `side effects { field … affects field …; }` block into a MANAGED '
|
|
244
|
+
+ 'BDEF entity body (refuses unmanaged). Pure transform — does not touch SAP.',
|
|
112
245
|
isMutating: false,
|
|
113
246
|
category: 'fiori',
|
|
114
247
|
flagGated: true,
|
|
@@ -117,15 +250,15 @@ registerTool({
|
|
|
117
250
|
properties: {
|
|
118
251
|
kind: {
|
|
119
252
|
type: 'string',
|
|
120
|
-
enum: ['cds-field', 'ddlx-lineitem', 'cds-lineitem', 'table-field', 'bdef-field', 'bdef-stub', 'projection-use', 'mde-action-button'],
|
|
121
|
-
description: 'cds-field: add an element to a CDS view body. ddlx-lineitem: add an @UI.lineItem column to a DDLX metadata extension. cds-lineitem: promote an existing exposed CDS-view field to a list column (insert @UI.lineItem above its declaration). table-field: append a NON-KEY field to a CDS `define table` body (key fields refused). bdef-field: add a field/mapping line to a MANAGED BDEF entity (unmanaged refused). bdef-stub: add a validation/determination/action clause to a MANAGED BDEF and its handler skeleton to the CCIMP include. projection-use: expose a `use action/function/association` line inside a PROJECTION BDEF entity body (non-projection BDEFs refused). mde-action-button: insert a @UI.lineItem FOR_ACTION button annotation above an anchor field in a metadata extension.',
|
|
253
|
+
enum: ['cds-field', 'ddlx-lineitem', 'cds-lineitem', 'table-field', 'bdef-field', 'bdef-stub', 'projection-use', 'mde-action-button', 'ddlx-headerinfo', 'ddlx-selectionfield', 'ddlx-identification', 'ddlx-facet', 'ddlx-datapoint', 'ddlx-chart', 'cds-valuehelp', 'bdef-sideeffects'],
|
|
254
|
+
description: 'cds-field: add an element to a CDS view body. ddlx-lineitem: add an @UI.lineItem column to a DDLX metadata extension. cds-lineitem: promote an existing exposed CDS-view field to a list column (insert @UI.lineItem above its declaration). table-field: append a NON-KEY field to a CDS `define table` body (key fields refused). bdef-field: add a field/mapping line to a MANAGED BDEF entity (unmanaged refused). bdef-stub: add a validation/determination/action clause to a MANAGED BDEF and its handler skeleton to the CCIMP include. projection-use: expose a `use action/function/association` line inside a PROJECTION BDEF entity body (non-projection BDEFs refused). mde-action-button: insert a @UI.lineItem FOR_ACTION button annotation above an anchor field in a metadata extension. ddlx-headerinfo: insert a view-level @UI.headerInfo block above the `annotate view` line (Object Page header; idempotent). ddlx-selectionfield: add an @UI.selectionField filter to a DDLX metadata extension (in place above an exposed field, or append the field + annotation when absent). ddlx-identification: add an @UI.identification annotation on a field, or a #FOR_ACTION Object-Page action button (set `action`); refuses a duplicate dataAction. ddlx-facet: add a view-level @UI.facet (#FIELDGROUP_REFERENCE) above the `annotate view` line (merged into an existing @UI.facet array, idempotent on facetId) plus a per-field @UI.fieldGroup on each named field. ddlx-datapoint: add a @UI.dataPoint (title, optional criticality field) above an existing field. ddlx-chart: add a view-level @UI.chart and its paired @UI.presentationVariant (#AS_CHART) above the `annotate view` line (qualifier-idempotent as a unit). cds-valuehelp: insert a @Consumption.valueHelpDefinition above an existing exposed CDS-view field (DDLS source; refuses multi-line/computed elements, idempotent). bdef-sideeffects: insert a `side effects { field … affects field …; }` block into a MANAGED BDEF entity body (unmanaged refused).',
|
|
122
255
|
},
|
|
123
256
|
source: { type: 'string', description: 'Full current source of the CDS view, DDLX metadata extension, CDS table, or managed BDEF (from sap_get_source). Required for all kinds except bdef-stub.' },
|
|
124
257
|
field: {
|
|
125
258
|
type: 'string',
|
|
126
|
-
description: 'cds-field: the element to add, e.g. "Priority" or "priority as Priority". ddlx-lineitem / cds-lineitem: the field name to expose as a column. table-field: the field clause WITHOUT trailing ";", e.g. "reference : zde_reference". bdef-field: target="field" → a full field declaration "field ( readonly ) Reference"; target="mapping" → an assignment "Reference = reference" (no trailing ";"). Required for all kinds except bdef-stub.',
|
|
259
|
+
description: 'cds-field: the element to add, e.g. "Priority" or "priority as Priority". ddlx-lineitem / cds-lineitem: the field name to expose as a column. table-field: the field clause WITHOUT trailing ";", e.g. "reference : zde_reference". bdef-field: target="field" → a full field declaration "field ( readonly ) Reference"; target="mapping" → an assignment "Reference = reference" (no trailing ";"). ddlx-selectionfield: the field to expose as a filter. ddlx-identification: the field to annotate (required for a plain identification; optional anchor for a #FOR_ACTION button). Required for all kinds except bdef-stub, ddlx-headerinfo, and a FOR_ACTION ddlx-identification.',
|
|
127
260
|
},
|
|
128
|
-
position: { type: 'number', description: 'kind=ddlx-lineitem and kind=cds-lineitem: the @UI.lineItem column position (default 10). kind=mde-action-button: optional column order for the inserted #FOR_ACTION entry. Ignored for other kinds.' },
|
|
261
|
+
position: { type: 'number', description: 'kind=ddlx-lineitem and kind=cds-lineitem: the @UI.lineItem column position (default 10). kind=ddlx-selectionfield: the @UI.selectionField filter order (default 10). kind=ddlx-identification: the @UI.identification position (default 10). kind=mde-action-button: optional column order for the inserted #FOR_ACTION entry. Ignored for other kinds.' },
|
|
129
262
|
alias: { type: 'string', description: 'kind=bdef-field: the behavior entity alias to extend (`alias <ALIAS>`). Optional only when the BDEF defines exactly one behavior; required when several exist. kind=bdef-stub: optional when the BDEF defines exactly one behavior; required when several exist (mirrors bdef-field behavior). kind=projection-use: the projection behavior entity alias; required when the projection BDEF defines more than one behavior.' },
|
|
130
263
|
target: { type: 'string', enum: ['field', 'mapping'], description: 'kind=bdef-field: "field" (default) adds a `field ( … )` line to the entity body; "mapping" adds an assignment line to the entity\'s mapping block.' },
|
|
131
264
|
bdefSource: { type: 'string', description: 'kind=bdef-stub: full managed BDEF source (from sap_get_source).' },
|
|
@@ -140,9 +273,28 @@ registerTool({
|
|
|
140
273
|
static: { type: 'boolean', description: 'kind=bdef-stub static (factory) action.' },
|
|
141
274
|
features: { type: 'boolean', description: 'kind=bdef-stub feature-controlled action (features : instance).' },
|
|
142
275
|
useClause: { type: 'string', description: 'kind=projection-use: the use clause to expose, e.g. "action Approve" or "association _Customer". Do NOT include a leading "use " or trailing ";". Required for projection-use.' },
|
|
143
|
-
action: { type: 'string', description: 'kind=mde-action-button: the action name to wire, e.g. "Approve".
|
|
276
|
+
action: { type: 'string', description: 'kind=mde-action-button: the action name to wire, e.g. "Approve" (required). kind=ddlx-identification: set this to emit a #FOR_ACTION Object-Page button for the named dataAction (omit for a plain field identification).' },
|
|
144
277
|
anchorField: { type: 'string', description: 'kind=mde-action-button: the field name above which to insert the action button annotation. Required for mde-action-button.' },
|
|
145
|
-
label: { type: 'string', description: 'kind=mde-action-button: optional button label text. Defaults to the action name.' },
|
|
278
|
+
label: { type: 'string', description: 'kind=mde-action-button / kind=ddlx-identification (#FOR_ACTION): optional button label text. Defaults to the action name.' },
|
|
279
|
+
typeName: { type: 'string', description: 'kind=ddlx-headerinfo: singular Object-Page type name, e.g. "Maintenance Request". Required for ddlx-headerinfo.' },
|
|
280
|
+
typeNamePlural: { type: 'string', description: 'kind=ddlx-headerinfo: plural Object-Page type name, e.g. "Maintenance Requests". Required for ddlx-headerinfo.' },
|
|
281
|
+
titleField: { type: 'string', description: 'kind=ddlx-headerinfo: the field whose value renders as the header title. Required for ddlx-headerinfo.' },
|
|
282
|
+
descriptionField: { type: 'string', description: 'kind=ddlx-headerinfo: optional field for the header description line.' },
|
|
283
|
+
facetId: { type: 'string', description: 'kind=ddlx-facet: the facet id (idempotency key). Required for ddlx-facet.' },
|
|
284
|
+
fieldGroupQualifier: { type: 'string', description: 'kind=ddlx-facet: the field-group qualifier the facet targets and each per-field @UI.fieldGroup binds to. Required for ddlx-facet.' },
|
|
285
|
+
fields: { type: 'array', description: 'kind=ddlx-facet: the fields to place in the field group, each { field, position } — a @UI.fieldGroup annotation is added on each. Required for ddlx-facet.', items: { type: 'object', properties: { field: { type: 'string' }, position: { type: 'number' } }, required: ['field', 'position'] } },
|
|
286
|
+
title: { type: 'string', description: 'kind=ddlx-datapoint: the @UI.dataPoint title text. Required for ddlx-datapoint.' },
|
|
287
|
+
criticalityField: { type: 'string', description: 'kind=ddlx-datapoint: optional field name driving the data point criticality colour (the transform does NOT create this field).' },
|
|
288
|
+
entity: { type: 'string', description: 'kind=cds-valuehelp: the value-help provider CDS entity, e.g. "ZI_Customer". Required for cds-valuehelp.' },
|
|
289
|
+
element: { type: 'string', description: 'kind=cds-valuehelp: the provider element to bind to, e.g. "CustomerID". Required for cds-valuehelp.' },
|
|
290
|
+
additionalBinding: { type: 'object', description: 'kind=cds-valuehelp: optional extra value-help binding { localElement, element }.', properties: { localElement: { type: 'string' }, element: { type: 'string' } }, required: ['localElement', 'element'] },
|
|
291
|
+
entityAlias: { type: 'string', description: 'kind=bdef-sideeffects: the behavior entity alias to extend. Optional only when the BDEF defines exactly one behavior; required when several exist.' },
|
|
292
|
+
sourceFields: { type: 'array', description: 'kind=bdef-sideeffects: the trigger fields (`field <…>`), non-empty. Required for bdef-sideeffects.', items: { type: 'string' } },
|
|
293
|
+
targetFields: { type: 'array', description: 'kind=bdef-sideeffects: the affected fields (`affects field <…>`), non-empty. Required for bdef-sideeffects.', items: { type: 'string' } },
|
|
294
|
+
qualifier: { type: 'string', description: 'kind=ddlx-chart: optional qualifier binding the @UI.chart to its @UI.presentationVariant (idempotency key). Omit for an anonymous chart.' },
|
|
295
|
+
chartType: { type: 'string', enum: ['COLUMN', 'BAR', 'LINE', 'DONUT'], description: 'kind=ddlx-chart: the chart type, mapped to the CDS enum #COLUMN/#BAR/#LINE/#DONUT. Required for ddlx-chart.' },
|
|
296
|
+
dimensions: { type: 'array', description: 'kind=ddlx-chart: the chart dimension field names (non-empty). Required for ddlx-chart.', items: { type: 'string' } },
|
|
297
|
+
measures: { type: 'array', description: 'kind=ddlx-chart: the chart measure field names (non-empty). Required for ddlx-chart.', items: { type: 'string' } },
|
|
146
298
|
},
|
|
147
299
|
required: ['kind'],
|
|
148
300
|
},
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/fiori/fe-extend.ts
|
|
2
|
+
/**
|
|
3
|
+
* Add a Fiori Elements extension point (custom column / action / section /
|
|
4
|
+
* controller-extension) to an EXISTING V4 FE app on disk, via
|
|
5
|
+
* @sap-ux/fe-fpm-writer. Mirrors scaffold.ts's generate()+commit() pattern:
|
|
6
|
+
* run the matching generator into a mem-fs editor, then flush with commit().
|
|
7
|
+
*
|
|
8
|
+
* SCOPE LIMIT (V4 FE only)
|
|
9
|
+
* ------------------------
|
|
10
|
+
* fe-fpm-writer's validateBasePath requires a `sap.fe.templates` dependency in
|
|
11
|
+
* the manifest — it works ONLY on V4 Fiori Elements apps (LROP / Worklist /
|
|
12
|
+
* ALP / FEOP) and REFUSES OVP (sap.ovp) and every OData V2 app with a
|
|
13
|
+
* "Dependency sap.fe.templates is missing …" library error. We catch that and
|
|
14
|
+
* rethrow the clean typed message so callers get a single, actionable line
|
|
15
|
+
* instead of the writer's internal wording.
|
|
16
|
+
*/
|
|
17
|
+
import { create } from 'mem-fs';
|
|
18
|
+
import { create as createEditor } from 'mem-fs-editor';
|
|
19
|
+
/** The four supported extension operations. Closed union → schema enum. */
|
|
20
|
+
export const FE_EXTEND_OPS = ['custom-column', 'custom-action', 'custom-section', 'controller-extension'];
|
|
21
|
+
/** Thrown when the target app is not a V4 Fiori Elements app (no sap.fe.templates). */
|
|
22
|
+
export class NotV4FeAppError extends Error {
|
|
23
|
+
constructor() {
|
|
24
|
+
super('fiori_fe_extend requires a V4 Fiori Elements app');
|
|
25
|
+
this.name = 'NotV4FeAppError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// fe-fpm-writer's validateBasePath library error — both the single-dependency
|
|
29
|
+
// and (defensively) the shared trailing sentence are matched so a wording
|
|
30
|
+
// tweak on either side still maps to the typed V4-only error.
|
|
31
|
+
function isMissingFeLibError(msg) {
|
|
32
|
+
return /sap\.fe\.templates is missing/i.test(msg) || /Fiori elements FPM requires the SAP FE libraries/i.test(msg);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Run the requested FE extension generator against an existing V4 FE app and
|
|
36
|
+
* flush the result to disk. Throws NotV4FeAppError when the target is not a V4
|
|
37
|
+
* FE app; rethrows any other writer error unchanged.
|
|
38
|
+
*/
|
|
39
|
+
export async function feExtend(opts) {
|
|
40
|
+
// Dynamic import: keep the heavy writer out of the cold-start path (mirrors
|
|
41
|
+
// the engine's lazy-load convention; only paid when the tool actually runs).
|
|
42
|
+
const fpm = await import('@sap-ux/fe-fpm-writer');
|
|
43
|
+
const editor = createEditor(create());
|
|
44
|
+
let result;
|
|
45
|
+
try {
|
|
46
|
+
// params is passed through verbatim (1:1 with the writer's Custom*/Controller
|
|
47
|
+
// config types) — the tool is a thin, deterministic pass-through.
|
|
48
|
+
switch (opts.op) {
|
|
49
|
+
case 'custom-column':
|
|
50
|
+
result = await fpm.generateCustomColumn(opts.basePath, opts.params, editor);
|
|
51
|
+
break;
|
|
52
|
+
case 'custom-action':
|
|
53
|
+
result = await fpm.generateCustomAction(opts.basePath, opts.params, editor);
|
|
54
|
+
break;
|
|
55
|
+
case 'custom-section':
|
|
56
|
+
result = await fpm.generateCustomSection(opts.basePath, opts.params, editor);
|
|
57
|
+
break;
|
|
58
|
+
case 'controller-extension':
|
|
59
|
+
result = await fpm.generateControllerExtension(opts.basePath, opts.params, editor);
|
|
60
|
+
break;
|
|
61
|
+
default: {
|
|
62
|
+
// Exhaustiveness guard: the union is closed. A future op added without a
|
|
63
|
+
// branch here fails loudly instead of silently no-op'ing.
|
|
64
|
+
const never = opts.op;
|
|
65
|
+
throw new Error(`unsupported fe_extend op: ${String(never)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71
|
+
if (isMissingFeLibError(msg))
|
|
72
|
+
throw new NotV4FeAppError();
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
await new Promise((res, rej) => result.commit((e) => (e ? rej(e) : res())));
|
|
76
|
+
}
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
import { generate, TemplateType, OdataVersion } from '@sap-ux/fiori-elements-writer';
|
|
3
3
|
import { create } from 'mem-fs';
|
|
4
4
|
import { create as createEditor } from 'mem-fs-editor';
|
|
5
|
+
import { join } from 'node:path';
|
|
5
6
|
const DEFAULT_UI5_VERSION = '1.120.0'; // same rationale as scaffold.ts (avoids the 1.71 blank-page trap)
|
|
6
7
|
/** Scaffold a Fiori Elements shell and FULLY flush it to disk. Relies on the
|
|
7
8
|
* service's backend @UI annotations — authors no local annotation.xml. */
|
|
8
9
|
export async function scaffoldFioriElements(opts) {
|
|
9
10
|
const ui5Version = opts.ui5Version ?? DEFAULT_UI5_VERSION;
|
|
10
11
|
const version = opts.service.version === '2.0' ? OdataVersion.v2 : OdataVersion.v4;
|
|
11
|
-
// Per-template settings (spike §3): LROP/Worklist use
|
|
12
|
-
//
|
|
12
|
+
// Per-template settings (spike §3): LROP/Worklist/ALP use
|
|
13
|
+
// entityConfig.mainEntityName (ALP is a LROP-based analytical variant — same
|
|
14
|
+
// settings shape); OVP uses filterEntitySet (the modern replacement for the
|
|
15
|
+
// deprecated filterEntityType). Each arm is explicit so adding a branch never
|
|
16
|
+
// silently changes another floorplan's behaviour.
|
|
13
17
|
let template;
|
|
14
18
|
if (opts.template === 'lrop') {
|
|
15
19
|
template = { type: TemplateType.ListReportObjectPage, settings: { entityConfig: { mainEntityName: opts.mainEntity } } };
|
|
@@ -17,9 +21,19 @@ export async function scaffoldFioriElements(opts) {
|
|
|
17
21
|
else if (opts.template === 'worklist') {
|
|
18
22
|
template = { type: TemplateType.Worklist, settings: { entityConfig: { mainEntityName: opts.mainEntity } } };
|
|
19
23
|
}
|
|
20
|
-
else {
|
|
24
|
+
else if (opts.template === 'ovp') {
|
|
21
25
|
template = { type: TemplateType.OverviewPage, settings: { filterEntitySet: opts.mainEntity } };
|
|
22
26
|
}
|
|
27
|
+
else if (opts.template === 'alp') {
|
|
28
|
+
template = { type: TemplateType.AnalyticalListPage, settings: { entityConfig: { mainEntityName: opts.mainEntity } } };
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// Exhaustiveness guard: the union is closed, so this is unreachable — but a
|
|
32
|
+
// future template added to the union without a branch here fails loudly
|
|
33
|
+
// instead of silently misrouting to another floorplan.
|
|
34
|
+
const never = opts.template;
|
|
35
|
+
throw new Error(`unsupported FE template: ${String(never)}`);
|
|
36
|
+
}
|
|
23
37
|
// Build the service object: version is required; url/path/metadata/client only
|
|
24
38
|
// when provided (metadata is optional — spike §4).
|
|
25
39
|
const service = { version };
|
|
@@ -31,6 +45,13 @@ export async function scaffoldFioriElements(opts) {
|
|
|
31
45
|
service.metadata = opts.service.metadata;
|
|
32
46
|
if (opts.service.client)
|
|
33
47
|
service.client = opts.service.client;
|
|
48
|
+
// Foreign-service local annotations: thread ONLY localAnnotationsName so the
|
|
49
|
+
// writer produces a LOCAL ODataAnnotation dataSource (uri === localUri ===
|
|
50
|
+
// annotations/<name>.xml). The real EDMX is written over the skeleton below,
|
|
51
|
+
// after generate() and before commit. When absent, the writer's own default
|
|
52
|
+
// ('annotation') is left untouched — zero regression.
|
|
53
|
+
if (opts.localAnnotations)
|
|
54
|
+
service.localAnnotationsName = opts.localAnnotations.technicalName;
|
|
34
55
|
const fs = createEditor(create());
|
|
35
56
|
const config = {
|
|
36
57
|
app: { id: opts.appId, title: opts.appTitle, projectType: 'EDMXBackend' },
|
|
@@ -41,5 +62,10 @@ export async function scaffoldFioriElements(opts) {
|
|
|
41
62
|
ui5: { minUI5Version: ui5Version, version: ui5Version },
|
|
42
63
|
};
|
|
43
64
|
await generate(opts.basePath, config, fs);
|
|
65
|
+
// Overwrite the writer's empty EDMX skeleton with the caller's real annotations
|
|
66
|
+
// EDMX, in the mem-fs editor, BEFORE commit so the flushed file is the caller's.
|
|
67
|
+
if (opts.localAnnotations) {
|
|
68
|
+
fs.write(join(opts.basePath, 'webapp', 'annotations', `${opts.localAnnotations.technicalName}.xml`), opts.localAnnotations.xml);
|
|
69
|
+
}
|
|
44
70
|
await new Promise((res, rej) => fs.commit((err) => (err ? rej(err) : res())));
|
|
45
71
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/fiori/floorplan-map.ts
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for the plan-name ↔ tool-template spelling
|
|
4
|
+
// divergence: the plan schema (PlanPhaseUi.floorplan) names the List Report
|
|
5
|
+
// floorplan 'listReport', but the FE writer / fiori_scaffold_fe tool names
|
|
6
|
+
// the same template 'lrop'. Every other floorplan spells identically.
|
|
7
|
+
//
|
|
8
|
+
// Deliberate leaf module (cycle-1 M7): it imports NOTHING from the writer, so
|
|
9
|
+
// plan-resume.ts can consume the map without pulling
|
|
10
|
+
// @sap-ux/fiori-elements-writer into plan-resume's test graph. Both
|
|
11
|
+
// fe-scaffold's consumers and plan-resume import FLOORPLAN_TO_TEMPLATE here.
|
|
12
|
+
/** Deterministic plan-floorplan → tool-template mapping. Only 'listReport'
|
|
13
|
+
* diverges ('lrop'); the rest map to their identical spelling. */
|
|
14
|
+
export const FLOORPLAN_TO_TEMPLATE = {
|
|
15
|
+
listReport: 'lrop',
|
|
16
|
+
worklist: 'worklist',
|
|
17
|
+
ovp: 'ovp',
|
|
18
|
+
alp: 'alp',
|
|
19
|
+
};
|