@cspeach/cli 0.9.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.
Files changed (195) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/intent-system-prompt.js +1 -1
  3. package/dist/agent/loop.js +228 -26
  4. package/dist/agent/providers/license-gate.js +44 -0
  5. package/dist/agent/skill-checkpoint.js +1 -1
  6. package/dist/agent/tool-dispatch.js +15 -0
  7. package/dist/approvals/canonical.js +91 -0
  8. package/dist/approvals/jwt.js +39 -2
  9. package/dist/approvals/op-labels.js +124 -0
  10. package/dist/approvals/render.js +42 -36
  11. package/dist/auth/org-anthropic-key.js +25 -0
  12. package/dist/classifier/client.js +18 -3
  13. package/dist/cli.js +15 -0
  14. package/dist/commands/compact.js +28 -2
  15. package/dist/commands/config-set.js +284 -0
  16. package/dist/commands/config-show.js +20 -0
  17. package/dist/commands/export-audit.js +43 -0
  18. package/dist/commands/help.js +5 -0
  19. package/dist/commands/login.js +31 -14
  20. package/dist/commands/plan-audit-evidence.js +266 -0
  21. package/dist/commands/plan-audit.js +692 -0
  22. package/dist/commands/plan-chain.js +671 -0
  23. package/dist/commands/plan-continue.js +179 -0
  24. package/dist/commands/plan-gate.js +154 -0
  25. package/dist/commands/plan-model-tier.js +83 -0
  26. package/dist/commands/plan-resume.js +728 -46
  27. package/dist/config/loader.js +223 -5
  28. package/dist/config/model-defaults.js +14 -0
  29. package/dist/cost/pricing.js +27 -1
  30. package/dist/doctor/checks/_http-probe.js +1 -0
  31. package/dist/doctor/checks/cert.js +14 -3
  32. package/dist/doctor/checks/sap.js +30 -8
  33. package/dist/doctor/checks/system-roles.js +41 -0
  34. package/dist/doctor/checks/zcspeach.js +19 -4
  35. package/dist/doctor/run.js +2 -0
  36. package/dist/models/resolve.js +61 -0
  37. package/dist/models/server-config.js +155 -0
  38. package/dist/one-shot.js +76 -6
  39. package/dist/projects/answer-blockers.js +137 -0
  40. package/dist/projects/extract-cca.js +111 -17
  41. package/dist/projects/extract-modernize.js +4 -2
  42. package/dist/projects/extract-plan.js +184 -37
  43. package/dist/projects/extract-spec-gap.js +34 -7
  44. package/dist/projects/extract-test-coverage.js +4 -2
  45. package/dist/projects/extract-upgrade.js +116 -23
  46. package/dist/projects/handover-md.js +195 -0
  47. package/dist/projects/index.js +5 -2
  48. package/dist/projects/merge-cca.js +292 -0
  49. package/dist/projects/merge-upgrade.js +173 -0
  50. package/dist/projects/migration.js +103 -1
  51. package/dist/projects/output-paths.js +27 -0
  52. package/dist/projects/plan-run.js +285 -27
  53. package/dist/projects/plan-schema.js +136 -3
  54. package/dist/projects/promote-command.js +25 -2
  55. package/dist/projects/promote.js +128 -0
  56. package/dist/projects/run-lease.js +157 -0
  57. package/dist/projects/save-command.js +259 -21
  58. package/dist/projects/status.js +3 -1
  59. package/dist/projects/validate.js +1 -1
  60. package/dist/projects/workspace.js +164 -20
  61. package/dist/renderer/notices.js +64 -0
  62. package/dist/renderer/progress-chatter.js +8 -0
  63. package/dist/renderer/status-footer.js +22 -12
  64. package/dist/renderer/thinking-heartbeat.js +64 -8
  65. package/dist/renderer/todo-block.js +51 -0
  66. package/dist/renderer/tool-widget.js +55 -4
  67. package/dist/renderer/tty.js +43 -4
  68. package/dist/renderer/verify-chain.js +77 -0
  69. package/dist/repl/at-picker.js +60 -7
  70. package/dist/repl/bracketed-paste.js +28 -19
  71. package/dist/repl/builtin-commands.js +42 -0
  72. package/dist/repl/current-transport.js +10 -0
  73. package/dist/repl/early-line-buffer.js +68 -0
  74. package/dist/repl/history.js +86 -0
  75. package/dist/repl/ink-stdin-guard.js +64 -0
  76. package/dist/repl/inquirer-guard.js +70 -5
  77. package/dist/repl/mode-ceiling.js +16 -0
  78. package/dist/repl/mode-cycle.js +104 -0
  79. package/dist/repl/numbered-menu.js +131 -0
  80. package/dist/repl/post-turn-status.js +26 -6
  81. package/dist/repl/rule8-detector.js +17 -2
  82. package/dist/repl/safety-confirm.js +111 -2
  83. package/dist/repl/safety-mode-state.js +19 -3
  84. package/dist/repl/slash-completer.js +5 -0
  85. package/dist/repl/slash-picker.js +10 -15
  86. package/dist/repl.js +1232 -95
  87. package/dist/rewind/candidates.js +194 -0
  88. package/dist/rewind/cli.js +137 -0
  89. package/dist/rewind/format.js +27 -0
  90. package/dist/rewind/restore.js +245 -0
  91. package/dist/router/classifier.js +150 -6
  92. package/dist/sap/capability-matrix.js +20 -0
  93. package/dist/sap/capability-matrix.json +11236 -0
  94. package/dist/sap/capability.js +146 -0
  95. package/dist/sap/connection-manager.js +19 -1
  96. package/dist/sap/onboarding.js +42 -4
  97. package/dist/session/audit-export.js +459 -0
  98. package/dist/session/context-report.js +163 -0
  99. package/dist/session/pending.js +27 -0
  100. package/dist/session/recap.js +160 -0
  101. package/dist/skill-catalog.js +51 -40
  102. package/dist/skills/bundled-skills.js +272 -1
  103. package/dist/skills/promotion-dispatch.js +23 -0
  104. package/dist/tools/_command-shared.js +36 -12
  105. package/dist/tools/_filesystem-shared.js +139 -4
  106. package/dist/tools/_flag.js +25 -0
  107. package/dist/tools/approval.js +177 -26
  108. package/dist/tools/ask-question.js +400 -7
  109. package/dist/tools/capability/tool.js +74 -0
  110. package/dist/tools/dispatch-skill.js +22 -1
  111. package/dist/tools/extend-model/anchored-insert.js +1414 -0
  112. package/dist/tools/extend-model/tool.js +340 -0
  113. package/dist/tools/filesystem/extract-document.js +57 -0
  114. package/dist/tools/filesystem/file-edit.js +12 -2
  115. package/dist/tools/filesystem/file-read.js +2 -2
  116. package/dist/tools/filesystem/file-write.js +11 -2
  117. package/dist/tools/filesystem/glob.js +11 -0
  118. package/dist/tools/filesystem/grep.js +10 -0
  119. package/dist/tools/filesystem/read-document.js +107 -0
  120. package/dist/tools/fiori/apply.js +50 -0
  121. package/dist/tools/fiori/bin.js +3 -0
  122. package/dist/tools/fiori/catalog/index.js +27 -0
  123. package/dist/tools/fiori/catalog/value-help.js +230 -0
  124. package/dist/tools/fiori/catalog/viz-chart.js +177 -0
  125. package/dist/tools/fiori/cli.js +71 -0
  126. package/dist/tools/fiori/deploy-config.js +73 -0
  127. package/dist/tools/fiori/fe-extend.js +76 -0
  128. package/dist/tools/fiori/fe-scaffold.js +71 -0
  129. package/dist/tools/fiori/floorplan-map.js +19 -0
  130. package/dist/tools/fiori/i18n.js +39 -0
  131. package/dist/tools/fiori/manifest.js +70 -0
  132. package/dist/tools/fiori/render.js +77 -0
  133. package/dist/tools/fiori/samples/data/index.json +13602 -0
  134. package/dist/tools/fiori/samples/data/sources.generated.js +808 -0
  135. package/dist/tools/fiori/samples/loader.js +248 -0
  136. package/dist/tools/fiori/samples/search.js +63 -0
  137. package/dist/tools/fiori/samples/types.js +2 -0
  138. package/dist/tools/fiori/scaffold.js +39 -0
  139. package/dist/tools/fiori/smoke/assertions.js +74 -0
  140. package/dist/tools/fiori/smoke/browser.js +52 -0
  141. package/dist/tools/fiori/smoke/driver.js +89 -0
  142. package/dist/tools/fiori/smoke/freestyle-spec.js +317 -0
  143. package/dist/tools/fiori/smoke/run-smoke.js +149 -0
  144. package/dist/tools/fiori/tools.js +681 -0
  145. package/dist/tools/fiori/types.js +1 -0
  146. package/dist/tools/local-build.js +86 -0
  147. package/dist/tools/local-files.js +31 -0
  148. package/dist/tools/project/_merge-shared.js +68 -0
  149. package/dist/tools/project/cca_merge.js +164 -0
  150. package/dist/tools/project/playbook_get.js +1 -1
  151. package/dist/tools/project/upgrade_merge_progress.js +206 -0
  152. package/dist/tools/sap-read.js +132 -20
  153. package/dist/tools/sap-write.js +550 -21
  154. package/dist/tools/shell/shell_exec.js +41 -6
  155. package/dist/tools/snapshot.js +63 -14
  156. package/dist/tools/subagent/agent_run.js +27 -3
  157. package/dist/tools/subagent/background_run.js +17 -1
  158. package/dist/tools/todo.js +144 -0
  159. package/dist/tools/transport-resolution.js +86 -0
  160. package/dist/tools/transport.js +224 -5
  161. package/dist/tools/write-mode.js +4 -0
  162. package/dist/ui/app.js +378 -21
  163. package/dist/ui/approval-modal.js +49 -16
  164. package/dist/ui/ask-question-emitter.js +14 -0
  165. package/dist/ui/body.js +13 -0
  166. package/dist/ui/context-grid.js +108 -0
  167. package/dist/ui/footer.js +120 -27
  168. package/dist/ui/header.js +7 -0
  169. package/dist/ui/line-resolution.js +35 -8
  170. package/dist/ui/rewind-emitter.js +10 -0
  171. package/dist/ui/rewind-panel.js +81 -0
  172. package/dist/ui/sap-state-store.js +1 -0
  173. package/dist/ui/session-timeline.js +1 -0
  174. package/dist/ui/status-line.js +43 -0
  175. package/dist/ui/text-input.js +214 -0
  176. package/dist/ui/todo-emitter.js +25 -0
  177. package/dist/ui/todo-panel.js +64 -0
  178. package/dist/ui/turn-status-emitter.js +50 -4
  179. package/dist/ui/turn-status.js +18 -3
  180. package/dist/ui/widgets/ask-form.js +242 -0
  181. package/dist/ui/widgets/ask-question-modal.js +21 -8
  182. package/package.json +22 -3
  183. package/bench/README.md +0 -78
  184. package/bench/prompts/abap-document-cds.md +0 -44
  185. package/bench/prompts/abap-explain-bdef-handler.md +0 -57
  186. package/bench/prompts/abap-test-method.md +0 -42
  187. package/bench/results/abap-document-cds/claude-haiku-4-5.md +0 -189
  188. package/bench/results/abap-document-cds/claude-opus-4-7.md +0 -120
  189. package/bench/results/abap-document-cds/claude-sonnet-4-6.md +0 -151
  190. package/bench/results/abap-explain-bdef-handler/claude-haiku-4-5.md +0 -112
  191. package/bench/results/abap-explain-bdef-handler/claude-opus-4-7.md +0 -101
  192. package/bench/results/abap-explain-bdef-handler/claude-sonnet-4-6.md +0 -101
  193. package/bench/results/abap-test-method/claude-haiku-4-5.md +0 -186
  194. package/bench/results/abap-test-method/claude-opus-4-7.md +0 -193
  195. package/bench/results/abap-test-method/claude-sonnet-4-6.md +0 -234
@@ -0,0 +1,50 @@
1
+ // cspeach-cli/src/tools/fiori/apply.ts
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { getEntry } from './catalog/index.js';
5
+ import { renderEntry } from './render.js';
6
+ import { mergeManifest } from './manifest.js';
7
+ import { appendI18n } from './i18n.js';
8
+ import { resolveSafePath } from '../_filesystem-shared.js';
9
+ /**
10
+ * Render a catalog entry and apply it into an existing scaffolded app on disk.
11
+ *
12
+ * SECURITY — containment is enforced HERE, on every write.
13
+ * --------------------------------------------------------
14
+ * Each output path is derived from a catalog EJS template rendered with
15
+ * CUSTOMER params (e.g. `webapp/ext/fragment/<%- ns %>Chart.fragment.xml`), so a
16
+ * hostile param could otherwise carry `..` and escape `appDir`. Every path this
17
+ * function writes — rendered files, manifest.json, and the i18n file (whose
18
+ * relative location is itself read from on-disk manifest data) — is resolved
19
+ * through `resolveSafePath(opts.appDir, rel)`, which throws PathOutsideRootError
20
+ * if the resolved absolute path is not contained under appDir. This is the
21
+ * authoritative defense; the catalog-side `ns` pattern (render.ts) is the
22
+ * second, earlier layer.
23
+ */
24
+ export function applyEntry(opts) {
25
+ const rendered = renderEntry(getEntry(opts.entryName), opts.params);
26
+ // 1. write files — re-validate EVERY rendered (param-derived) path against appDir.
27
+ for (const f of rendered.files) {
28
+ const abs = resolveSafePath(opts.appDir, f.path);
29
+ mkdirSync(dirname(abs), { recursive: true });
30
+ writeFileSync(abs, f.content, 'utf8');
31
+ }
32
+ // 2. merge manifest — ALWAYS re-read from disk (I2). Fixed relative path, but
33
+ // routed through resolveSafePath for uniform containment.
34
+ const manifestPath = resolveSafePath(opts.appDir, 'webapp/manifest.json');
35
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
36
+ let merged = mergeManifest(manifest, rendered.manifestPatch);
37
+ for (const lib of rendered.libraries) {
38
+ merged = mergeManifest(merged, { 'sap.ui5': { dependencies: { libs: { [lib]: {} } } } });
39
+ }
40
+ writeFileSync(manifestPath, JSON.stringify(merged, null, 2), 'utf8');
41
+ // 3. append i18n — resolve path from sap.app.i18n (M1). i18nRel is on-disk
42
+ // data, not a render param, but it still drives a write target, so contain it.
43
+ const i18nRel = merged['sap.app']?.i18n ?? 'i18n/i18n.properties';
44
+ const i18nPath = resolveSafePath(opts.appDir, `webapp/${i18nRel}`);
45
+ const existing = existsSync(i18nPath) ? readFileSync(i18nPath, 'utf8') : '';
46
+ if (Object.keys(rendered.i18nKeys).length) {
47
+ mkdirSync(dirname(i18nPath), { recursive: true });
48
+ writeFileSync(i18nPath, appendI18n(existing, rendered.i18nKeys), 'utf8');
49
+ }
50
+ }
@@ -0,0 +1,3 @@
1
+ // cspeach-cli/src/tools/fiori/bin.ts
2
+ import { runFioriCli } from './cli.js';
3
+ runFioriCli(process.argv.slice(2)).then((code) => process.exit(code));
@@ -0,0 +1,27 @@
1
+ // cspeach-cli/src/tools/fiori/catalog/index.ts
2
+ //
3
+ // Catalog index for the Fiori engine.
4
+ // v1 scope: only the two "fiddly" SAPUI5 patterns — viz-chart and value-help.
5
+ // confirm-dialog (4.1), list-table-page (4.2), object-detail-page (4.3),
6
+ // message-toast (4.4) are SKILL.md recipes, not catalog entries.
7
+ import { vizChart } from './viz-chart.js';
8
+ import { valueHelp } from './value-help.js';
9
+ export const CATALOG = [vizChart, valueHelp];
10
+ export function getEntry(name) {
11
+ const e = CATALOG.find((x) => x.name === name);
12
+ if (!e)
13
+ throw new Error(`Unknown catalog entry "${name}". Available: ${CATALOG.map((c) => c.name).join(', ')}`);
14
+ return e;
15
+ }
16
+ export function listCatalog() {
17
+ // params included so the agent can construct a correct `apply --params` call
18
+ // straight from `fiori list` — no trial-and-error against required-param errors.
19
+ return CATALOG.map(({ name, title, description, categories, libraries, params }) => ({
20
+ name,
21
+ title,
22
+ description,
23
+ categories,
24
+ libraries,
25
+ params,
26
+ }));
27
+ }
@@ -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
+ }