@cspeach/cli 0.9.0 → 1.0.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/README.md +1 -1
- package/dist/agent/intent-system-prompt.js +1 -1
- package/dist/agent/loop.js +209 -20
- package/dist/agent/providers/license-gate.js +44 -0
- package/dist/agent/skill-checkpoint.js +1 -1
- package/dist/agent/tool-dispatch.js +15 -0
- package/dist/approvals/canonical.js +91 -0
- package/dist/approvals/jwt.js +39 -2
- package/dist/auth/org-anthropic-key.js +25 -0
- package/dist/classifier/client.js +18 -3
- package/dist/commands/config-set.js +95 -0
- package/dist/commands/login.js +31 -14
- package/dist/commands/plan-model-tier.js +83 -0
- package/dist/commands/plan-resume.js +148 -21
- package/dist/config/loader.js +95 -1
- package/dist/doctor/checks/_http-probe.js +1 -0
- package/dist/doctor/checks/cert.js +14 -3
- package/dist/doctor/checks/sap.js +30 -8
- package/dist/doctor/checks/zcspeach.js +19 -4
- package/dist/one-shot.js +52 -4
- package/dist/projects/answer-blockers.js +137 -0
- package/dist/projects/extract-cca.js +108 -16
- package/dist/projects/extract-modernize.js +1 -1
- package/dist/projects/extract-plan.js +130 -37
- package/dist/projects/extract-spec-gap.js +34 -7
- package/dist/projects/extract-test-coverage.js +1 -1
- package/dist/projects/extract-upgrade.js +113 -22
- package/dist/projects/index.js +5 -2
- package/dist/projects/merge-cca.js +292 -0
- package/dist/projects/merge-upgrade.js +173 -0
- package/dist/projects/migration.js +103 -1
- package/dist/projects/output-paths.js +27 -0
- package/dist/projects/plan-run.js +159 -25
- package/dist/projects/plan-schema.js +63 -3
- package/dist/projects/promote-command.js +25 -2
- package/dist/projects/promote.js +128 -0
- package/dist/projects/save-command.js +247 -20
- package/dist/projects/status.js +3 -1
- package/dist/projects/validate.js +1 -1
- package/dist/projects/workspace.js +164 -20
- package/dist/renderer/notices.js +64 -0
- package/dist/renderer/progress-chatter.js +8 -0
- package/dist/renderer/tool-widget.js +18 -4
- package/dist/renderer/tty.js +43 -4
- package/dist/renderer/verify-chain.js +77 -0
- package/dist/repl/at-picker.js +60 -7
- package/dist/repl/builtin-commands.js +37 -0
- package/dist/repl/early-line-buffer.js +68 -0
- package/dist/repl/inquirer-guard.js +70 -5
- package/dist/repl/numbered-menu.js +131 -0
- package/dist/repl/post-turn-status.js +2 -2
- package/dist/repl/rule8-detector.js +17 -2
- package/dist/repl/safety-confirm.js +111 -2
- package/dist/repl/safety-mode-state.js +19 -3
- package/dist/repl/slash-picker.js +10 -15
- package/dist/repl.js +301 -35
- package/dist/router/classifier.js +150 -6
- package/dist/sap/capability-matrix.js +20 -0
- package/dist/sap/capability-matrix.json +11236 -0
- package/dist/sap/capability.js +146 -0
- package/dist/sap/connection-manager.js +19 -1
- package/dist/sap/onboarding.js +42 -4
- package/dist/session/pending.js +27 -0
- package/dist/skill-catalog.js +48 -43
- package/dist/skills/bundled-skills.js +279 -1
- package/dist/skills/promotion-dispatch.js +23 -0
- package/dist/tools/_command-shared.js +36 -12
- package/dist/tools/_filesystem-shared.js +139 -4
- package/dist/tools/_flag.js +25 -0
- package/dist/tools/approval.js +64 -21
- package/dist/tools/ask-question.js +96 -4
- package/dist/tools/capability/tool.js +74 -0
- package/dist/tools/dispatch-skill.js +22 -1
- package/dist/tools/extend-model/anchored-insert.js +810 -0
- package/dist/tools/extend-model/tool.js +188 -0
- package/dist/tools/filesystem/extract-document.js +57 -0
- package/dist/tools/filesystem/file-edit.js +12 -2
- package/dist/tools/filesystem/file-read.js +2 -2
- package/dist/tools/filesystem/file-write.js +11 -2
- package/dist/tools/filesystem/glob.js +11 -0
- package/dist/tools/filesystem/grep.js +10 -0
- package/dist/tools/filesystem/read-document.js +107 -0
- package/dist/tools/fiori/apply.js +50 -0
- package/dist/tools/fiori/bin.js +3 -0
- package/dist/tools/fiori/catalog/index.js +27 -0
- package/dist/tools/fiori/catalog/value-help.js +230 -0
- package/dist/tools/fiori/catalog/viz-chart.js +177 -0
- package/dist/tools/fiori/cli.js +71 -0
- package/dist/tools/fiori/deploy-config.js +73 -0
- package/dist/tools/fiori/fe-scaffold.js +45 -0
- package/dist/tools/fiori/i18n.js +39 -0
- package/dist/tools/fiori/manifest.js +70 -0
- package/dist/tools/fiori/render.js +77 -0
- package/dist/tools/fiori/scaffold.js +39 -0
- package/dist/tools/fiori/tools.js +356 -0
- package/dist/tools/fiori/types.js +1 -0
- package/dist/tools/local-build.js +76 -0
- package/dist/tools/local-files.js +31 -0
- package/dist/tools/project/_merge-shared.js +68 -0
- package/dist/tools/project/cca_merge.js +164 -0
- package/dist/tools/project/playbook_get.js +1 -1
- package/dist/tools/project/upgrade_merge_progress.js +206 -0
- package/dist/tools/sap-read.js +53 -9
- package/dist/tools/sap-write.js +530 -21
- package/dist/tools/shell/shell_exec.js +41 -6
- package/dist/tools/snapshot.js +37 -14
- package/dist/tools/subagent/background_run.js +17 -1
- package/dist/tools/transport-resolution.js +86 -0
- package/dist/tools/transport.js +224 -5
- package/dist/tools/write-mode.js +4 -0
- package/dist/ui/app.js +6 -2
- package/dist/ui/body.js +13 -0
- package/dist/ui/footer.js +20 -6
- package/dist/ui/line-resolution.js +17 -6
- package/dist/ui/session-timeline.js +1 -0
- package/dist/ui/text-input.js +150 -0
- package/dist/ui/widgets/ask-question-modal.js +4 -1
- package/package.json +19 -3
- package/bench/README.md +0 -78
- package/bench/prompts/abap-document-cds.md +0 -44
- package/bench/prompts/abap-explain-bdef-handler.md +0 -57
- package/bench/prompts/abap-test-method.md +0 -42
- package/bench/results/abap-document-cds/claude-haiku-4-5.md +0 -189
- package/bench/results/abap-document-cds/claude-opus-4-7.md +0 -120
- package/bench/results/abap-document-cds/claude-sonnet-4-6.md +0 -151
- package/bench/results/abap-explain-bdef-handler/claude-haiku-4-5.md +0 -112
- package/bench/results/abap-explain-bdef-handler/claude-opus-4-7.md +0 -101
- package/bench/results/abap-explain-bdef-handler/claude-sonnet-4-6.md +0 -101
- package/bench/results/abap-test-method/claude-haiku-4-5.md +0 -186
- package/bench/results/abap-test-method/claude-opus-4-7.md +0 -193
- package/bench/results/abap-test-method/claude-sonnet-4-6.md +0 -234
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/fiori/catalog/value-help.ts
|
|
2
|
+
//
|
|
3
|
+
// SAPUI5 ValueHelpDialog (F4 help) entry.
|
|
4
|
+
// Authored against the public sap.ui.comp.valuehelpdialog.ValueHelpDialog API.
|
|
5
|
+
// Source inspiration: SAPUI5 SDK ValueHelpDialog demokit (ui5.sap.com/#/api/sap.ui.comp.valuehelpdialog.ValueHelpDialog).
|
|
6
|
+
// This is an original authoring against the public API — not a copy of any Apache-licensed sample.
|
|
7
|
+
// NOTE: sap.ui.comp is a SAPUI5-only library (not available in OpenUI5).
|
|
8
|
+
export const valueHelp = {
|
|
9
|
+
name: 'value-help',
|
|
10
|
+
title: 'ValueHelpDialog (F4 value help)',
|
|
11
|
+
description: 'A sap.ui.comp.valuehelpdialog.ValueHelpDialog F4 help backed by a second OData entity. ' +
|
|
12
|
+
'Opens a search-and-select dialog, lets the user pick a key+description row, and writes the ' +
|
|
13
|
+
'selected key back to a target field. Single-select by default. Use for any code/key field ' +
|
|
14
|
+
'that needs a search dialog (customer, material, cost centre, etc.).',
|
|
15
|
+
engine: 'freestyle',
|
|
16
|
+
ui5MinVersion: '1.54.0',
|
|
17
|
+
libraries: ['sap.ui.comp', 'sap.m', 'sap.ui.core'],
|
|
18
|
+
categories: ['dialog', 'value-help', 'f4', 'search', 'selection'],
|
|
19
|
+
files: [
|
|
20
|
+
{
|
|
21
|
+
// Fragment: declares the ValueHelpDialog as a dependent; used by the controller mixin.
|
|
22
|
+
// The dialog is created programmatically in the mixin (not in XML) because
|
|
23
|
+
// ValueHelpDialog requires JS-side column and token configuration.
|
|
24
|
+
// This fragment is a thin host container — a Panel that shows the currently
|
|
25
|
+
// selected value token inline (optional), giving the agent a drop-in XML widget.
|
|
26
|
+
path: 'webapp/ext/fragment/<%- ns %>ValueHelp.fragment.xml',
|
|
27
|
+
content: `<core:FragmentDefinition
|
|
28
|
+
xmlns="sap.m"
|
|
29
|
+
xmlns:core="sap.ui.core">
|
|
30
|
+
<!--
|
|
31
|
+
<%- ns %>ValueHelp — F4 value-help trigger widget.
|
|
32
|
+
Drop this fragment into any form field row.
|
|
33
|
+
Bind the Input value to your form model field (e.g. {formModel>/<%- targetField %>}).
|
|
34
|
+
The press handler opens the ValueHelpDialog via the controller mixin.
|
|
35
|
+
-->
|
|
36
|
+
<HBox alignItems="Center">
|
|
37
|
+
<Input
|
|
38
|
+
id="<%- ns %>VHInput"
|
|
39
|
+
value="{formModel>/<%- targetField %>}"
|
|
40
|
+
placeholder="{i18n><%- ns %>.vhPlaceholder}"
|
|
41
|
+
showValueHelp="true"
|
|
42
|
+
valueHelpRequest=".open<%- ns %>ValueHelp"
|
|
43
|
+
width="100%" />
|
|
44
|
+
</HBox>
|
|
45
|
+
</core:FragmentDefinition>`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
// Controller mixin: creates and wires the ValueHelpDialog
|
|
49
|
+
path: 'webapp/controller/ext/<%- ns %>ValueHelp.js',
|
|
50
|
+
content: `sap.ui.define([
|
|
51
|
+
"sap/ui/comp/valuehelpdialog/ValueHelpDialog",
|
|
52
|
+
"sap/m/Token",
|
|
53
|
+
"sap/ui/model/Filter",
|
|
54
|
+
"sap/ui/model/FilterOperator"
|
|
55
|
+
], function (ValueHelpDialog, Token, Filter, FilterOperator) {
|
|
56
|
+
"use strict";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Controller mixin for <%- ns %>ValueHelp.
|
|
60
|
+
*
|
|
61
|
+
* Provides F4 value help for the "<%- targetField %>" field.
|
|
62
|
+
* Help entity: <%- helpEntityPath %> Key: <%- keyField %> Description: <%- descriptionField %>
|
|
63
|
+
*
|
|
64
|
+
* Usage:
|
|
65
|
+
* Object.assign(this, <%- ns %>ValueHelpMixin);
|
|
66
|
+
* // In your XML form bind the Input valueHelpRequest=".open<%- ns %>ValueHelp"
|
|
67
|
+
*/
|
|
68
|
+
var <%- ns %>ValueHelpMixin = {
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Opens the ValueHelpDialog. Wired to valueHelpRequest on the Input control.
|
|
72
|
+
* @param {sap.ui.base.Event} oEvent
|
|
73
|
+
*/
|
|
74
|
+
open<%- ns %>ValueHelp: function (oEvent) {
|
|
75
|
+
var oView = this.getView();
|
|
76
|
+
var oModel = oView.getModel(); // default OData model
|
|
77
|
+
|
|
78
|
+
if (!this._o<%- ns %>VHDialog) {
|
|
79
|
+
this._o<%- ns %>VHDialog = new ValueHelpDialog({
|
|
80
|
+
title: oView.getModel("i18n").getResourceBundle().getText("<%- ns %>.vhTitle"),
|
|
81
|
+
supportMultiselect: false,
|
|
82
|
+
supportRanges: false,
|
|
83
|
+
key: "<%- keyField %>",
|
|
84
|
+
descriptionKey: "<%- descriptionField %>",
|
|
85
|
+
ok: this._on<%- ns %>VHConfirm.bind(this),
|
|
86
|
+
cancel: this._on<%- ns %>VHCancel.bind(this),
|
|
87
|
+
afterClose: function () {
|
|
88
|
+
this._o<%- ns %>VHDialog.destroy();
|
|
89
|
+
this._o<%- ns %>VHDialog = null;
|
|
90
|
+
}.bind(this)
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Device-agnostic column setup via the "columns" named model.
|
|
94
|
+
// ValueHelpDialog.getTable() returns sap.ui.table.Table on desktop/tablet
|
|
95
|
+
// and sap.m.Table on phone. Neither shares the same column/items API,
|
|
96
|
+
// so we use the canonical "columns" model approach that works for both.
|
|
97
|
+
var oTable = this._o<%- ns %>VHDialog.getTable();
|
|
98
|
+
oTable.setModel(oModel);
|
|
99
|
+
|
|
100
|
+
// 1. Describe columns via a named JSON model — VHD reads this model
|
|
101
|
+
// to render grid columns on desktop (sap.ui.table.Table).
|
|
102
|
+
var oColModel = new sap.ui.model.json.JSONModel({
|
|
103
|
+
cols: [
|
|
104
|
+
{ label: "<%- keyField %>", template: "<%- keyField %>" },
|
|
105
|
+
{ label: "<%- descriptionField %>", template: "<%- descriptionField %>" }
|
|
106
|
+
]
|
|
107
|
+
});
|
|
108
|
+
oTable.setModel(oColModel, "columns");
|
|
109
|
+
|
|
110
|
+
// 2. Bind rows/items device-aware: sap.ui.table.Table uses bindRows,
|
|
111
|
+
// sap.m.Table (phone) uses bindItems.
|
|
112
|
+
// Normalise the entity path: strip a leading slash then re-prepend one
|
|
113
|
+
// so we never produce double-slash regardless of what helpEntityPath contains.
|
|
114
|
+
var sEntityPath = "/<%- helpEntityPath %>".replace(/^\\/+/, "/");
|
|
115
|
+
if (oTable.bindRows) {
|
|
116
|
+
// sap.ui.table.Table — desktop / tablet
|
|
117
|
+
oTable.bindRows(sEntityPath);
|
|
118
|
+
} else {
|
|
119
|
+
// sap.m.Table — phone
|
|
120
|
+
oTable.bindItems({
|
|
121
|
+
path: sEntityPath,
|
|
122
|
+
template: new sap.m.ColumnListItem({
|
|
123
|
+
cells: [
|
|
124
|
+
new sap.m.Text({ text: "{<%- keyField %>}" }),
|
|
125
|
+
new sap.m.Text({ text: "{<%- descriptionField %>}" })
|
|
126
|
+
]
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
this._o<%- ns %>VHDialog.update();
|
|
131
|
+
|
|
132
|
+
// Pre-select current value if set
|
|
133
|
+
var sCurrentKey = oView.getModel("formModel") &&
|
|
134
|
+
oView.getModel("formModel").getProperty("/<%- targetField %>");
|
|
135
|
+
if (sCurrentKey) {
|
|
136
|
+
this._o<%- ns %>VHDialog.setTokens([new Token({ key: sCurrentKey, text: sCurrentKey })]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this._o<%- ns %>VHDialog.open();
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* OK handler — transfers the selected token key to the target field.
|
|
145
|
+
* @param {sap.ui.base.Event} oEvent
|
|
146
|
+
*/
|
|
147
|
+
_on<%- ns %>VHConfirm: function (oEvent) {
|
|
148
|
+
var aTokens = oEvent.getParameter("tokens");
|
|
149
|
+
var oFormModel = this.getView().getModel("formModel");
|
|
150
|
+
if (aTokens && aTokens.length > 0 && oFormModel) {
|
|
151
|
+
oFormModel.setProperty("/<%- targetField %>", aTokens[0].getKey());
|
|
152
|
+
}
|
|
153
|
+
if (this._o<%- ns %>VHDialog) {
|
|
154
|
+
this._o<%- ns %>VHDialog.close();
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Cancel handler — closes without changes.
|
|
160
|
+
*/
|
|
161
|
+
_on<%- ns %>VHCancel: function () {
|
|
162
|
+
if (this._o<%- ns %>VHDialog) {
|
|
163
|
+
this._o<%- ns %>VHDialog.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return <%- ns %>ValueHelpMixin;
|
|
169
|
+
});`,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
manifestPatch: {
|
|
173
|
+
'sap.ui5': {
|
|
174
|
+
dependencies: {
|
|
175
|
+
libs: {
|
|
176
|
+
'sap.ui.comp': {},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
i18nKeys: {
|
|
182
|
+
'<%- ns %>.vhTitle': '<%- ns %> Search',
|
|
183
|
+
'<%- ns %>.vhPlaceholder': 'Select <%- ns %>…',
|
|
184
|
+
},
|
|
185
|
+
params: {
|
|
186
|
+
appId: {
|
|
187
|
+
type: 'string',
|
|
188
|
+
required: true,
|
|
189
|
+
from: 'context.app.id',
|
|
190
|
+
description: 'Application namespace ID, e.g. zsales.app',
|
|
191
|
+
},
|
|
192
|
+
ns: {
|
|
193
|
+
type: 'string',
|
|
194
|
+
required: true,
|
|
195
|
+
// SECURITY: ns lands directly in a file path
|
|
196
|
+
// (webapp/ext/fragment/<ns>ValueHelp.fragment.xml). Constrain to a simple
|
|
197
|
+
// UI5-identifier fragment — letters/digits only, leading letter — so no
|
|
198
|
+
// dot, slash, or `..` can ever reach join(appDir, path).
|
|
199
|
+
pattern: '[A-Za-z][A-Za-z0-9]*',
|
|
200
|
+
description: 'PascalCase prefix for IDs and method names, e.g. Customer. Simple identifier only (letters/digits, leading letter).',
|
|
201
|
+
},
|
|
202
|
+
helpEntityPath: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
required: true,
|
|
205
|
+
description: 'Absolute OData binding path for the help entity set, e.g. /Customers',
|
|
206
|
+
},
|
|
207
|
+
keyField: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
required: true,
|
|
210
|
+
description: 'Property name of the key column in the help entity, e.g. CustomerID',
|
|
211
|
+
},
|
|
212
|
+
descriptionField: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
required: true,
|
|
215
|
+
description: 'Property name of the description column in the help entity, e.g. CustomerName',
|
|
216
|
+
},
|
|
217
|
+
targetField: {
|
|
218
|
+
type: 'string',
|
|
219
|
+
required: true,
|
|
220
|
+
description: 'Property path on formModel to write the selected key into, e.g. CustomerID',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
source: {
|
|
224
|
+
url: 'https://ui5.sap.com/#/api/sap.ui.comp.valuehelpdialog.ValueHelpDialog',
|
|
225
|
+
license: 'SAPUI5 — authored against the public ValueHelpDialog API; not copied from Apache-licensed source',
|
|
226
|
+
adaptedFrom: 'Original authoring against sap.ui.comp.valuehelpdialog.ValueHelpDialog public API. ' +
|
|
227
|
+
'sap.ui.comp is a SAPUI5-only library (not OpenUI5); targets SAPUI5 runtime. ' +
|
|
228
|
+
'Pattern adapted from standard F4 help implementation approach documented in SAP ABAP Fiori guidelines.',
|
|
229
|
+
},
|
|
230
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/fiori/catalog/viz-chart.ts
|
|
2
|
+
//
|
|
3
|
+
// SAPUI5 VizFrame column/bar chart entry.
|
|
4
|
+
// Authored against the public sap.viz.ui5.controls.VizFrame API.
|
|
5
|
+
// Source inspiration: SAPUI5 SDK VizFrame demokit samples (ui5.sap.com/#/entity/sap.viz.ui5.controls.VizFrame).
|
|
6
|
+
// This is an original authoring against the public API — not a copy of any Apache-licensed sample.
|
|
7
|
+
export const vizChart = {
|
|
8
|
+
name: 'viz-chart',
|
|
9
|
+
title: 'VizFrame chart (column / bar / line)',
|
|
10
|
+
description: 'A sap.viz.ui5.controls.VizFrame chart bound to an OData list via FlattenedDataset. ' +
|
|
11
|
+
'Supports column, bar, and line chart types. Configures one dimension (category axis) ' +
|
|
12
|
+
'and one or more measures (value axis). Use when you need an analytic chart inline in a page or dialog.',
|
|
13
|
+
engine: 'freestyle',
|
|
14
|
+
ui5MinVersion: '1.54.0',
|
|
15
|
+
libraries: ['sap.viz', 'sap.m', 'sap.ui.core'],
|
|
16
|
+
categories: ['chart', 'analytics', 'visualization'],
|
|
17
|
+
files: [
|
|
18
|
+
{
|
|
19
|
+
// Fragment: VizFrame with inline FlattenedDataset
|
|
20
|
+
path: 'webapp/ext/fragment/<%- ns %>Chart.fragment.xml',
|
|
21
|
+
content: `<core:FragmentDefinition
|
|
22
|
+
xmlns="sap.m"
|
|
23
|
+
xmlns:core="sap.ui.core"
|
|
24
|
+
xmlns:viz="sap.viz.ui5.controls"
|
|
25
|
+
xmlns:viz.data="sap.viz.ui5.data"
|
|
26
|
+
xmlns:viz.feeds="sap.viz.ui5.controls.common.feeds">
|
|
27
|
+
<viz:VizFrame
|
|
28
|
+
id="<%- ns %>VizFrame"
|
|
29
|
+
vizType="<%- locals.vizType || 'column' %>"
|
|
30
|
+
width="100%"
|
|
31
|
+
height="380px"
|
|
32
|
+
uiConfig="{applicationSet:'fiori2'}"
|
|
33
|
+
vizProperties="{ title: { visible: false } }">
|
|
34
|
+
<viz:dataset>
|
|
35
|
+
<viz.data:FlattenedDataset data="{<%- listPath %>}">
|
|
36
|
+
<viz.data:dimensions>
|
|
37
|
+
<viz.data:DimensionDefinition
|
|
38
|
+
name="<%- dimensionField %>"
|
|
39
|
+
value="{<%- dimensionField %>}" />
|
|
40
|
+
</viz.data:dimensions>
|
|
41
|
+
<viz.data:measures>
|
|
42
|
+
<viz.data:MeasureDefinition
|
|
43
|
+
name="<%- measureFields[0] %>"
|
|
44
|
+
value="{<%- measureFields[0] %>}" />
|
|
45
|
+
</viz.data:measures>
|
|
46
|
+
</viz.data:FlattenedDataset>
|
|
47
|
+
</viz:dataset>
|
|
48
|
+
<viz:feeds>
|
|
49
|
+
<viz.feeds:FeedItem
|
|
50
|
+
uid="categoryAxis"
|
|
51
|
+
type="Dimension"
|
|
52
|
+
values="<%- dimensionField %>" />
|
|
53
|
+
<viz.feeds:FeedItem
|
|
54
|
+
uid="valueAxis"
|
|
55
|
+
type="Measure"
|
|
56
|
+
values="<%- measureFields[0] %>" />
|
|
57
|
+
</viz:feeds>
|
|
58
|
+
</viz:VizFrame>
|
|
59
|
+
</core:FragmentDefinition>`,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
// Controller mixin: lazy-loads the fragment and wires additional measures
|
|
63
|
+
path: 'webapp/controller/ext/<%- ns %>Chart.js',
|
|
64
|
+
content: `sap.ui.define([
|
|
65
|
+
"sap/ui/core/Fragment"
|
|
66
|
+
], function (Fragment) {
|
|
67
|
+
"use strict";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Controller mixin for <%- ns %>Chart.
|
|
71
|
+
* Renders a VizFrame column/bar/line chart bound to <%- listPath %>.
|
|
72
|
+
* Dimension: <%- dimensionField %>.
|
|
73
|
+
*
|
|
74
|
+
* Usage: extend your controller with Object.assign(this, <%- ns %>ChartMixin).
|
|
75
|
+
*/
|
|
76
|
+
var <%- ns %>ChartMixin = {
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Open / show the chart fragment as a dependent of the current view.
|
|
80
|
+
* The fragment ID will be <view-id>--<%- ns %>VizFrame.
|
|
81
|
+
*/
|
|
82
|
+
open<%- ns %>Chart: function () {
|
|
83
|
+
var oView = this.getView();
|
|
84
|
+
if (!this._p<%- ns %>Chart) {
|
|
85
|
+
this._p<%- ns %>Chart = Fragment.load({
|
|
86
|
+
id: oView.getId(),
|
|
87
|
+
name: "<%- appId %>.ext.fragment.<%- ns %>Chart",
|
|
88
|
+
controller: this
|
|
89
|
+
}).then(function (oFrag) {
|
|
90
|
+
oView.addDependent(oFrag);
|
|
91
|
+
// Wire additional measures if provided
|
|
92
|
+
var oVizFrame = oView.byId("<%- ns %>VizFrame");
|
|
93
|
+
if (oVizFrame) {
|
|
94
|
+
var oDataset = oVizFrame.getDataset();
|
|
95
|
+
var aExtraMeasures = <%- JSON.stringify(measureFields) %>.slice(1);
|
|
96
|
+
aExtraMeasures.forEach(function (sMeasure) {
|
|
97
|
+
oDataset.addMeasure(
|
|
98
|
+
new sap.viz.ui5.data.MeasureDefinition({ name: sMeasure, value: "{" + sMeasure + "}" })
|
|
99
|
+
);
|
|
100
|
+
oVizFrame.addFeed(
|
|
101
|
+
new sap.viz.ui5.controls.common.feeds.FeedItem({
|
|
102
|
+
uid: "valueAxis", type: "Measure", values: sMeasure
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return oFrag;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
this._p<%- ns %>Chart.then(function (oFrag) {
|
|
111
|
+
if (typeof oFrag.open === "function") { oFrag.open(); }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return <%- ns %>ChartMixin;
|
|
117
|
+
});`,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
manifestPatch: {
|
|
121
|
+
'sap.ui5': {
|
|
122
|
+
dependencies: {
|
|
123
|
+
libs: {
|
|
124
|
+
'sap.viz': {},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
i18nKeys: {
|
|
130
|
+
'<%- ns %>.chartTitle': '<%- ns %> Chart',
|
|
131
|
+
},
|
|
132
|
+
params: {
|
|
133
|
+
appId: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
required: true,
|
|
136
|
+
from: 'context.app.id',
|
|
137
|
+
description: 'Application namespace ID, e.g. zsales.app',
|
|
138
|
+
},
|
|
139
|
+
ns: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
required: true,
|
|
142
|
+
// SECURITY: ns lands directly in a file path
|
|
143
|
+
// (webapp/ext/fragment/<ns>Chart.fragment.xml). Constrain to a simple
|
|
144
|
+
// UI5-identifier fragment — letters/digits only, leading letter — so no
|
|
145
|
+
// dot, slash, or `..` can ever reach join(appDir, path).
|
|
146
|
+
pattern: '[A-Za-z][A-Za-z0-9]*',
|
|
147
|
+
description: 'PascalCase prefix for all generated IDs and method names, e.g. Revenue. Simple identifier only (letters/digits, leading letter).',
|
|
148
|
+
},
|
|
149
|
+
vizType: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
required: false,
|
|
152
|
+
description: 'VizFrame vizType: column (default), bar, line, pie',
|
|
153
|
+
},
|
|
154
|
+
listPath: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
required: true,
|
|
157
|
+
description: 'OData binding path for the list, e.g. /SalesOrderItems',
|
|
158
|
+
},
|
|
159
|
+
dimensionField: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
required: true,
|
|
162
|
+
description: 'Property name used as the chart dimension (category axis), e.g. Material',
|
|
163
|
+
},
|
|
164
|
+
measureFields: {
|
|
165
|
+
type: 'array',
|
|
166
|
+
required: true,
|
|
167
|
+
description: 'Array of property names for value-axis measures, e.g. ["NetAmount","TaxAmount"]',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
source: {
|
|
171
|
+
url: 'https://ui5.sap.com/#/entity/sap.viz.ui5.controls.VizFrame',
|
|
172
|
+
license: 'SAPUI5 — authored against the public VizFrame API; not copied from Apache-licensed source',
|
|
173
|
+
adaptedFrom: 'Original authoring against sap.viz.ui5.controls.VizFrame + sap.viz.ui5.data.FlattenedDataset public API ' +
|
|
174
|
+
'(SAPUI5 SDK demokit samples used as API reference only). ' +
|
|
175
|
+
'sap.viz is a SAPUI5-only library (not OpenUI5); targets SAPUI5 runtime.',
|
|
176
|
+
},
|
|
177
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/fiori/cli.ts
|
|
2
|
+
import { listCatalog } from './catalog/index.js';
|
|
3
|
+
import { scaffoldFreestyle } from './scaffold.js';
|
|
4
|
+
import { applyEntry } from './apply.js';
|
|
5
|
+
import { writeDeployConfig } from './deploy-config.js';
|
|
6
|
+
/** Parse `--key value` / `--key=value` / `--json '<json>'` into a record. */
|
|
7
|
+
function parseFlags(argv) {
|
|
8
|
+
const out = {};
|
|
9
|
+
for (let i = 0; i < argv.length; i++) {
|
|
10
|
+
const a = argv[i];
|
|
11
|
+
if (!a.startsWith('--'))
|
|
12
|
+
continue;
|
|
13
|
+
const eq = a.indexOf('=');
|
|
14
|
+
if (eq !== -1)
|
|
15
|
+
out[a.slice(2, eq)] = a.slice(eq + 1);
|
|
16
|
+
else {
|
|
17
|
+
out[a.slice(2)] = argv[i + 1] ?? '';
|
|
18
|
+
i++;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
/** Engine command surface invoked by the agent via shell_exec. Returns exit code. */
|
|
24
|
+
export async function runFioriCli(argv, log = console.log) {
|
|
25
|
+
const [cmd, ...rest] = argv;
|
|
26
|
+
try {
|
|
27
|
+
switch (cmd) {
|
|
28
|
+
case 'list':
|
|
29
|
+
log(JSON.stringify(listCatalog(), null, 2));
|
|
30
|
+
return 0;
|
|
31
|
+
case 'scaffold': {
|
|
32
|
+
const f = parseFlags(rest);
|
|
33
|
+
await scaffoldFreestyle({
|
|
34
|
+
basePath: f.basePath, appId: f.appId, appTitle: f.appTitle ?? f.appId,
|
|
35
|
+
template: f.template ?? 'basic',
|
|
36
|
+
service: f.serviceUrl ? { url: f.serviceUrl, path: f.servicePath, version: f.serviceVersion ?? '4.0' } : undefined,
|
|
37
|
+
typescript: f.typescript === 'true',
|
|
38
|
+
ui5Version: f.ui5Version,
|
|
39
|
+
});
|
|
40
|
+
log(`scaffolded ${f.appId} at ${f.basePath}`);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
case 'apply': {
|
|
44
|
+
const f = parseFlags(rest);
|
|
45
|
+
applyEntry({ appDir: f.appDir, entryName: f.entry, params: JSON.parse(f.params ?? '{}') });
|
|
46
|
+
log(`applied ${f.entry} into ${f.appDir}`);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
case 'deploy-config': {
|
|
50
|
+
const f = parseFlags(rest);
|
|
51
|
+
writeDeployConfig(f.appDir, {
|
|
52
|
+
url: f.url, client: f.client, appName: f.name,
|
|
53
|
+
description: f.description,
|
|
54
|
+
package: f.package, transport: f.transport ?? '',
|
|
55
|
+
ignoreCertErrors: f.ignoreCertErrors === 'true',
|
|
56
|
+
});
|
|
57
|
+
log(`deploy config written: ${f.appDir}/ui5-deploy.yaml (app ${f.name} -> package ${f.package}` +
|
|
58
|
+
`${f.transport ? `, transport ${f.transport}` : ''}). ` +
|
|
59
|
+
`Run \`npm run deploy-test\` to validate without deploying.`);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
default:
|
|
63
|
+
log(`Unknown fiori subcommand "${cmd}". Use: list | scaffold | apply | deploy-config`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
log(`ERROR: ${e.message}`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/fiori/deploy-config.ts
|
|
2
|
+
//
|
|
3
|
+
// Writes the ABAP-repository deploy configuration for a scaffolded app:
|
|
4
|
+
// ui5-deploy.yaml (deploy-to-abap custom task, shape grounded against the
|
|
5
|
+
// bundled @sap/ux-ui5-tooling `fiori deploy --help` output, 2026-06-10) and
|
|
6
|
+
// the package.json `deploy` / `deploy-test` scripts.
|
|
7
|
+
//
|
|
8
|
+
// Credentials are NEVER written here — `fiori deploy --username/--password`
|
|
9
|
+
// take the NAMES of environment variables at deploy time.
|
|
10
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
export function writeDeployConfig(appDir, opts) {
|
|
13
|
+
const appName = opts.appName;
|
|
14
|
+
if (appName.length > 15) {
|
|
15
|
+
throw new Error(`BSP app name "${appName}" exceeds 15 characters (ABAP repository limit)`);
|
|
16
|
+
}
|
|
17
|
+
if (appName !== appName.toUpperCase()) {
|
|
18
|
+
throw new Error(`BSP app name "${appName}" must be uppercase`);
|
|
19
|
+
}
|
|
20
|
+
const pkg = opts.package.toUpperCase();
|
|
21
|
+
if (!/^(\$TMP|[ZY][A-Z0-9_]*|\/[A-Z0-9_]+\/[A-Z0-9_]+)$/.test(pkg)) {
|
|
22
|
+
throw new Error(`Package "${opts.package}" is not in the customer namespace (Z-/Y-prefixed, $TMP, or /namespace/pkg)`);
|
|
23
|
+
}
|
|
24
|
+
const isTmp = pkg === '$TMP';
|
|
25
|
+
if (!isTmp && !/^[A-Z0-9]{3}K[0-9]{6,}$/.test(opts.transport)) {
|
|
26
|
+
throw new Error(`A valid workbench transport (e.g. S4HK903359) is required unless package is $TMP — got "${opts.transport}"`);
|
|
27
|
+
}
|
|
28
|
+
const pkgJsonPath = join(appDir, 'package.json');
|
|
29
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
30
|
+
const projectName = pkgJson.name ?? appName.toLowerCase();
|
|
31
|
+
// TLS posture (2026-06-14): only disable cert verification when the caller
|
|
32
|
+
// explicitly opted into insecure (wired from the per-alias
|
|
33
|
+
// insecure_skip_tls_verify flag). When a CA is configured / verification is
|
|
34
|
+
// on, leave cert errors enabled — the OS/Node trust + CA handle it. The
|
|
35
|
+
// insecure line carries a loud dev-only comment.
|
|
36
|
+
const insecure = opts.ignoreCertErrors === true;
|
|
37
|
+
const certLine = insecure
|
|
38
|
+
? 'ignoreCertErrors: true # dev only — configure a CA for production'
|
|
39
|
+
: 'ignoreCertErrors: false';
|
|
40
|
+
const yaml = `# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json
|
|
41
|
+
# Deploy configuration — written by the CSPeach fiori engine.
|
|
42
|
+
# Run \`npm run deploy-test\` first: the backend validates WITHOUT deploying.
|
|
43
|
+
specVersion: "4.0"
|
|
44
|
+
metadata:
|
|
45
|
+
name: ${projectName}
|
|
46
|
+
type: application
|
|
47
|
+
builder:
|
|
48
|
+
resources:
|
|
49
|
+
excludes:
|
|
50
|
+
- /test/**
|
|
51
|
+
- /localService/**
|
|
52
|
+
customTasks:
|
|
53
|
+
- name: deploy-to-abap
|
|
54
|
+
afterTask: generateCachebusterInfo
|
|
55
|
+
configuration:
|
|
56
|
+
${certLine}
|
|
57
|
+
target:
|
|
58
|
+
url: ${opts.url}
|
|
59
|
+
client: "${opts.client}"
|
|
60
|
+
app:
|
|
61
|
+
name: ${appName}
|
|
62
|
+
description: ${opts.description ?? projectName}
|
|
63
|
+
package: ${pkg}
|
|
64
|
+
transport: ${isTmp ? '""' : opts.transport}
|
|
65
|
+
exclude:
|
|
66
|
+
- /test/
|
|
67
|
+
`;
|
|
68
|
+
writeFileSync(join(appDir, 'ui5-deploy.yaml'), yaml, 'utf8');
|
|
69
|
+
pkgJson.scripts = pkgJson.scripts ?? {};
|
|
70
|
+
pkgJson.scripts['deploy'] = 'npm run build && fiori deploy --config ui5-deploy.yaml';
|
|
71
|
+
pkgJson.scripts['deploy-test'] = 'npm run build && fiori deploy --config ui5-deploy.yaml --testMode --yes';
|
|
72
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n', 'utf8');
|
|
73
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/fiori/fe-scaffold.ts
|
|
2
|
+
import { generate, TemplateType, OdataVersion } from '@sap-ux/fiori-elements-writer';
|
|
3
|
+
import { create } from 'mem-fs';
|
|
4
|
+
import { create as createEditor } from 'mem-fs-editor';
|
|
5
|
+
const DEFAULT_UI5_VERSION = '1.120.0'; // same rationale as scaffold.ts (avoids the 1.71 blank-page trap)
|
|
6
|
+
/** Scaffold a Fiori Elements shell and FULLY flush it to disk. Relies on the
|
|
7
|
+
* service's backend @UI annotations — authors no local annotation.xml. */
|
|
8
|
+
export async function scaffoldFioriElements(opts) {
|
|
9
|
+
const ui5Version = opts.ui5Version ?? DEFAULT_UI5_VERSION;
|
|
10
|
+
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).
|
|
13
|
+
let template;
|
|
14
|
+
if (opts.template === 'lrop') {
|
|
15
|
+
template = { type: TemplateType.ListReportObjectPage, settings: { entityConfig: { mainEntityName: opts.mainEntity } } };
|
|
16
|
+
}
|
|
17
|
+
else if (opts.template === 'worklist') {
|
|
18
|
+
template = { type: TemplateType.Worklist, settings: { entityConfig: { mainEntityName: opts.mainEntity } } };
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
template = { type: TemplateType.OverviewPage, settings: { filterEntitySet: opts.mainEntity } };
|
|
22
|
+
}
|
|
23
|
+
// Build the service object: version is required; url/path/metadata/client only
|
|
24
|
+
// when provided (metadata is optional — spike §4).
|
|
25
|
+
const service = { version };
|
|
26
|
+
if (opts.service.url)
|
|
27
|
+
service.url = opts.service.url;
|
|
28
|
+
if (opts.service.path)
|
|
29
|
+
service.path = opts.service.path;
|
|
30
|
+
if (opts.service.metadata)
|
|
31
|
+
service.metadata = opts.service.metadata;
|
|
32
|
+
if (opts.service.client)
|
|
33
|
+
service.client = opts.service.client;
|
|
34
|
+
const fs = createEditor(create());
|
|
35
|
+
const config = {
|
|
36
|
+
app: { id: opts.appId, title: opts.appTitle, projectType: 'EDMXBackend' },
|
|
37
|
+
package: { name: opts.appId },
|
|
38
|
+
template,
|
|
39
|
+
service,
|
|
40
|
+
appOptions: { typescript: false },
|
|
41
|
+
ui5: { minUI5Version: ui5Version, version: ui5Version },
|
|
42
|
+
};
|
|
43
|
+
await generate(opts.basePath, config, fs);
|
|
44
|
+
await new Promise((res, rej) => fs.commit((err) => (err ? rej(err) : res())));
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/fiori/i18n.ts
|
|
2
|
+
export class I18nConflictError extends Error {
|
|
3
|
+
constructor(key, a, b) {
|
|
4
|
+
super(`i18n key "${key}" already set to "${a}", refusing to overwrite with "${b}"`);
|
|
5
|
+
this.name = 'I18nConflictError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Parse a .properties body into a key→value map (ignores comments/blanks). */
|
|
9
|
+
function parse(text) {
|
|
10
|
+
const map = new Map();
|
|
11
|
+
for (const line of text.split(/\r?\n/)) {
|
|
12
|
+
const t = line.trim();
|
|
13
|
+
if (!t || t.startsWith('#'))
|
|
14
|
+
continue;
|
|
15
|
+
const eq = t.indexOf('=');
|
|
16
|
+
if (eq === -1)
|
|
17
|
+
continue;
|
|
18
|
+
map.set(t.slice(0, eq).trim(), t.slice(eq + 1));
|
|
19
|
+
}
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
/** Returns the properties text with `keys` appended; dedupes, errors on conflict. */
|
|
23
|
+
export function appendI18n(existing, keys) {
|
|
24
|
+
const map = parse(existing);
|
|
25
|
+
const additions = [];
|
|
26
|
+
for (const [k, v] of Object.entries(keys)) {
|
|
27
|
+
const cur = map.get(k);
|
|
28
|
+
if (cur === undefined) {
|
|
29
|
+
additions.push(`${k}=${v}`);
|
|
30
|
+
map.set(k, v);
|
|
31
|
+
}
|
|
32
|
+
else if (cur !== v)
|
|
33
|
+
throw new I18nConflictError(k, cur, v);
|
|
34
|
+
}
|
|
35
|
+
if (additions.length === 0)
|
|
36
|
+
return existing;
|
|
37
|
+
const sep = existing.endsWith('\n') || existing === '' ? '' : '\n';
|
|
38
|
+
return `${existing}${sep}${additions.join('\n')}\n`;
|
|
39
|
+
}
|