@cspeach/cli 1.0.0 → 1.1.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.
Files changed (100) hide show
  1. package/dist/agent/loop.js +22 -9
  2. package/dist/approvals/op-labels.js +124 -0
  3. package/dist/approvals/render.js +42 -36
  4. package/dist/cli.js +15 -0
  5. package/dist/commands/compact.js +28 -2
  6. package/dist/commands/config-set.js +189 -0
  7. package/dist/commands/config-show.js +20 -0
  8. package/dist/commands/export-audit.js +43 -0
  9. package/dist/commands/help.js +5 -0
  10. package/dist/commands/plan-audit-evidence.js +266 -0
  11. package/dist/commands/plan-audit.js +692 -0
  12. package/dist/commands/plan-chain.js +671 -0
  13. package/dist/commands/plan-continue.js +179 -0
  14. package/dist/commands/plan-gate.js +154 -0
  15. package/dist/commands/plan-resume.js +588 -33
  16. package/dist/config/loader.js +128 -4
  17. package/dist/config/model-defaults.js +14 -0
  18. package/dist/cost/pricing.js +27 -1
  19. package/dist/doctor/checks/system-roles.js +41 -0
  20. package/dist/doctor/run.js +2 -0
  21. package/dist/models/resolve.js +61 -0
  22. package/dist/models/server-config.js +155 -0
  23. package/dist/one-shot.js +25 -3
  24. package/dist/projects/extract-cca.js +3 -1
  25. package/dist/projects/extract-modernize.js +3 -1
  26. package/dist/projects/extract-plan.js +60 -6
  27. package/dist/projects/extract-test-coverage.js +3 -1
  28. package/dist/projects/extract-upgrade.js +3 -1
  29. package/dist/projects/handover-md.js +195 -0
  30. package/dist/projects/index.js +1 -1
  31. package/dist/projects/plan-run.js +137 -13
  32. package/dist/projects/plan-schema.js +73 -0
  33. package/dist/projects/run-lease.js +157 -0
  34. package/dist/projects/save-command.js +26 -15
  35. package/dist/renderer/status-footer.js +22 -12
  36. package/dist/renderer/thinking-heartbeat.js +64 -8
  37. package/dist/renderer/todo-block.js +51 -0
  38. package/dist/renderer/tool-widget.js +37 -0
  39. package/dist/repl/bracketed-paste.js +28 -19
  40. package/dist/repl/builtin-commands.js +5 -0
  41. package/dist/repl/current-transport.js +10 -0
  42. package/dist/repl/history.js +86 -0
  43. package/dist/repl/ink-stdin-guard.js +64 -0
  44. package/dist/repl/mode-ceiling.js +16 -0
  45. package/dist/repl/mode-cycle.js +104 -0
  46. package/dist/repl/post-turn-status.js +24 -4
  47. package/dist/repl/slash-completer.js +5 -0
  48. package/dist/repl.js +954 -83
  49. package/dist/rewind/candidates.js +194 -0
  50. package/dist/rewind/cli.js +137 -0
  51. package/dist/rewind/format.js +27 -0
  52. package/dist/rewind/restore.js +245 -0
  53. package/dist/session/audit-export.js +459 -0
  54. package/dist/session/context-report.js +163 -0
  55. package/dist/session/recap.js +160 -0
  56. package/dist/skill-catalog.js +9 -3
  57. package/dist/skills/bundled-skills.js +71 -78
  58. package/dist/tools/approval.js +115 -7
  59. package/dist/tools/ask-question.js +304 -3
  60. package/dist/tools/extend-model/anchored-insert.js +604 -0
  61. package/dist/tools/extend-model/tool.js +162 -10
  62. package/dist/tools/fiori/fe-extend.js +76 -0
  63. package/dist/tools/fiori/fe-scaffold.js +29 -3
  64. package/dist/tools/fiori/floorplan-map.js +19 -0
  65. package/dist/tools/fiori/samples/data/index.json +13602 -0
  66. package/dist/tools/fiori/samples/data/sources.generated.js +808 -0
  67. package/dist/tools/fiori/samples/loader.js +248 -0
  68. package/dist/tools/fiori/samples/search.js +63 -0
  69. package/dist/tools/fiori/samples/types.js +2 -0
  70. package/dist/tools/fiori/smoke/assertions.js +74 -0
  71. package/dist/tools/fiori/smoke/browser.js +52 -0
  72. package/dist/tools/fiori/smoke/driver.js +89 -0
  73. package/dist/tools/fiori/smoke/freestyle-spec.js +317 -0
  74. package/dist/tools/fiori/smoke/run-smoke.js +149 -0
  75. package/dist/tools/fiori/tools.js +328 -3
  76. package/dist/tools/local-build.js +11 -1
  77. package/dist/tools/sap-read.js +79 -11
  78. package/dist/tools/sap-write.js +24 -4
  79. package/dist/tools/snapshot.js +27 -1
  80. package/dist/tools/subagent/agent_run.js +27 -3
  81. package/dist/tools/todo.js +144 -0
  82. package/dist/ui/app.js +372 -19
  83. package/dist/ui/approval-modal.js +49 -16
  84. package/dist/ui/ask-question-emitter.js +14 -0
  85. package/dist/ui/context-grid.js +108 -0
  86. package/dist/ui/footer.js +109 -30
  87. package/dist/ui/header.js +7 -0
  88. package/dist/ui/line-resolution.js +18 -2
  89. package/dist/ui/rewind-emitter.js +10 -0
  90. package/dist/ui/rewind-panel.js +81 -0
  91. package/dist/ui/sap-state-store.js +1 -0
  92. package/dist/ui/status-line.js +43 -0
  93. package/dist/ui/text-input.js +72 -8
  94. package/dist/ui/todo-emitter.js +25 -0
  95. package/dist/ui/todo-panel.js +64 -0
  96. package/dist/ui/turn-status-emitter.js +50 -4
  97. package/dist/ui/turn-status.js +18 -3
  98. package/dist/ui/widgets/ask-form.js +242 -0
  99. package/dist/ui/widgets/ask-question-modal.js +17 -7
  100. 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-stub"`,
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. Pure transform — '
111
- + 'does not touch SAP.',
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". Required for mde-action-button.' },
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 entityConfig.mainEntityName;
12
- // OVP uses filterEntitySet (the modern replacement for the deprecated filterEntityType).
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
+ };