@cspeach/cli 1.1.17 → 1.1.19

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 (225) hide show
  1. package/README.md +3 -4
  2. package/dist/agent/loop.js +143 -25
  3. package/dist/agent/providers/local-provider.js +4 -1
  4. package/dist/agent/repair-partial.js +76 -19
  5. package/dist/agent/tool-dispatch.js +80 -22
  6. package/dist/agent/turn-error-ux.js +77 -26
  7. package/dist/agent/turn-stream.js +25 -3
  8. package/dist/approvals/adt-type.js +27 -0
  9. package/dist/approvals/advisory-prompt.js +48 -0
  10. package/dist/approvals/advisory-render.js +25 -11
  11. package/dist/approvals/approval-prompt.js +74 -24
  12. package/dist/approvals/jwt.js +2 -1
  13. package/dist/approvals/render.js +64 -36
  14. package/dist/approvals/risk-floor.js +53 -2
  15. package/dist/auth/api-key.js +30 -16
  16. package/dist/auth/keychain.js +0 -0
  17. package/dist/auth/me.js +25 -7
  18. package/dist/cli.js +25 -6
  19. package/dist/commands/compact.js +12 -9
  20. package/dist/commands/config-set.js +6 -2
  21. package/dist/commands/config-show.js +7 -5
  22. package/dist/commands/cost.js +41 -34
  23. package/dist/commands/help.js +88 -50
  24. package/dist/commands/login.js +28 -19
  25. package/dist/commands/logout.js +7 -3
  26. package/dist/commands/plan-chain.js +71 -77
  27. package/dist/commands/plan-continue.js +1 -0
  28. package/dist/commands/plan-resume.js +20 -12
  29. package/dist/commands/whoami.js +23 -9
  30. package/dist/config/loader.js +15 -3
  31. package/dist/cost/pricing.js +6 -5
  32. package/dist/doctor/checks/forge-rules.js +13 -3
  33. package/dist/doctor/checks/keychain.js +6 -4
  34. package/dist/doctor/run.js +101 -20
  35. package/dist/index.js +5 -2
  36. package/dist/lib/graceful-exit.js +56 -0
  37. package/dist/lib/piped-prompt.js +65 -0
  38. package/dist/lock-contention.js +2 -2
  39. package/dist/one-shot.js +135 -40
  40. package/dist/projects/answer-blockers.js +6 -1
  41. package/dist/projects/handover-md.js +15 -14
  42. package/dist/projects/plan-run.js +97 -54
  43. package/dist/projects/promote-command.js +11 -2
  44. package/dist/projects/save-command.js +3 -2
  45. package/dist/projects/status.js +2 -1
  46. package/dist/projects/yes-no.js +12 -0
  47. package/dist/renderer/abap-inline.js +18 -14
  48. package/dist/renderer/answer-trim.js +69 -0
  49. package/dist/renderer/banners.js +8 -8
  50. package/dist/renderer/brand-settled.js +19 -0
  51. package/dist/renderer/change-card.js +156 -0
  52. package/dist/renderer/checklist-format.js +130 -0
  53. package/dist/renderer/color-mode.js +45 -0
  54. package/dist/renderer/error-detail.js +104 -0
  55. package/dist/renderer/fence-state.js +46 -0
  56. package/dist/renderer/footer-line.js +96 -0
  57. package/dist/renderer/glyphs.js +25 -0
  58. package/dist/renderer/goodbye.js +38 -0
  59. package/dist/renderer/legacy-palette.js +41 -0
  60. package/dist/renderer/look.js +30 -0
  61. package/dist/renderer/markdown.js +299 -92
  62. package/dist/renderer/notice-log.js +72 -0
  63. package/dist/renderer/notice-shape.js +48 -0
  64. package/dist/renderer/notices.js +40 -8
  65. package/dist/renderer/panel-rows.js +16 -0
  66. package/dist/renderer/pipeline.js +66 -0
  67. package/dist/renderer/progress-chatter.js +23 -17
  68. package/dist/renderer/rendering-mode.js +34 -0
  69. package/dist/renderer/routing-line.js +14 -0
  70. package/dist/renderer/sanitize.js +12 -0
  71. package/dist/renderer/severity.js +2 -7
  72. package/dist/renderer/startup-lines.js +152 -0
  73. package/dist/renderer/status-footer.js +52 -34
  74. package/dist/renderer/steering-echo.js +41 -0
  75. package/dist/renderer/syntax.js +31 -12
  76. package/dist/renderer/tables.js +5 -1
  77. package/dist/renderer/theme.js +44 -0
  78. package/dist/renderer/thinking-heartbeat.js +33 -2
  79. package/dist/renderer/todo-block.js +10 -49
  80. package/dist/renderer/tool-labels.js +238 -0
  81. package/dist/renderer/tool-widget.js +245 -51
  82. package/dist/renderer/trace.js +42 -0
  83. package/dist/renderer/transcript-flow.js +132 -0
  84. package/dist/renderer/tty.js +34 -1
  85. package/dist/renderer/ui-width.js +42 -0
  86. package/dist/renderer/verify-chain.js +4 -2
  87. package/dist/renderer/widget-fallback.js +58 -65
  88. package/dist/repl/bracketed-paste.js +7 -1
  89. package/dist/repl/builtin-commands.js +19 -8
  90. package/dist/repl/credential-handover-gate.js +28 -0
  91. package/dist/repl/early-line-buffer.js +5 -2
  92. package/dist/repl/file-picker.js +54 -12
  93. package/dist/repl/ink-stdin-guard.js +66 -0
  94. package/dist/repl/inquirer-guard.js +59 -16
  95. package/dist/repl/inquirer-theme.js +27 -33
  96. package/dist/repl/paste-marker.js +19 -0
  97. package/dist/repl/plan-turn-end.js +20 -0
  98. package/dist/repl/post-turn-status.js +36 -11
  99. package/dist/repl/reroute-turn.js +21 -0
  100. package/dist/repl/reset-tty-stdin.js +44 -0
  101. package/dist/repl/restore-guard.js +22 -0
  102. package/dist/repl/resume-standalone.js +63 -0
  103. package/dist/repl/rule8-detector.js +11 -3
  104. package/dist/repl/safety-confirm.js +161 -93
  105. package/dist/repl/session-spend-line.js +8 -4
  106. package/dist/repl/themed-prompts.js +19 -0
  107. package/dist/repl/ui-look-command.js +75 -0
  108. package/dist/repl/update-method-preview-hook.js +48 -5
  109. package/dist/repl.js +795 -332
  110. package/dist/rewind/cli.js +3 -2
  111. package/dist/rewind/format.js +14 -9
  112. package/dist/rewind/restore.js +11 -0
  113. package/dist/router/classifier.js +47 -4
  114. package/dist/router/intent-extractor.js +24 -8
  115. package/dist/router/piped-routing.js +23 -0
  116. package/dist/router/resume-routing.js +16 -0
  117. package/dist/router/routing-failure.js +57 -0
  118. package/dist/sap/first-run-choice.js +1 -1
  119. package/dist/sap/onboarding.js +3 -2
  120. package/dist/sap/standalone-onboarding.js +1 -1
  121. package/dist/sap/system-info.js +4 -2
  122. package/dist/sap/unreachable.js +28 -0
  123. package/dist/sap-errors/clean-error-text.js +182 -0
  124. package/dist/session/interrupt-reason.js +39 -0
  125. package/dist/session/recap.js +36 -24
  126. package/dist/session/resume.js +104 -36
  127. package/dist/session/store.js +69 -27
  128. package/dist/session/user-prompt.js +21 -0
  129. package/dist/skill-catalog.js +7 -0
  130. package/dist/skills/bundled-skills.js +76 -69
  131. package/dist/skills/signing-public-key.js +1 -1
  132. package/dist/standards/standards-init.js +1 -1
  133. package/dist/test-helpers/answer-any.js +46 -0
  134. package/dist/tools/approval.js +27 -11
  135. package/dist/tools/ask-question.js +47 -5
  136. package/dist/tools/filesystem/file-write.js +19 -2
  137. package/dist/tools/fiori/fe-extend.js +16 -1
  138. package/dist/tools/fiori/fe-scaffold.js +105 -3
  139. package/dist/tools/fiori/html-escapes.js +28 -0
  140. package/dist/tools/fiori/metadata/audit.js +605 -0
  141. package/dist/tools/fiori/metadata/references.js +104 -0
  142. package/dist/tools/fiori/metadata/types.js +8 -0
  143. package/dist/tools/fiori/preview/app-guard.js +215 -0
  144. package/dist/tools/fiori/preview/env.js +193 -0
  145. package/dist/tools/fiori/preview/readiness.js +127 -0
  146. package/dist/tools/fiori/preview/registry.js +412 -0
  147. package/dist/tools/fiori/preview/start.js +469 -0
  148. package/dist/tools/fiori/samples/loader.js +20 -5
  149. package/dist/tools/fiori/scaffold.js +18 -6
  150. package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
  151. package/dist/tools/fiori/smoke/app-driver.js +100 -0
  152. package/dist/tools/fiori/smoke/assertions.js +161 -24
  153. package/dist/tools/fiori/smoke/audit-columns.js +21 -0
  154. package/dist/tools/fiori/smoke/batch.js +273 -0
  155. package/dist/tools/fiori/smoke/draft-safety.js +241 -0
  156. package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
  157. package/dist/tools/fiori/smoke/driver.js +171 -4
  158. package/dist/tools/fiori/smoke/evidence.js +35 -0
  159. package/dist/tools/fiori/smoke/labels-codes.js +207 -0
  160. package/dist/tools/fiori/smoke/nav-error.js +26 -0
  161. package/dist/tools/fiori/smoke/run-smoke.js +155 -12
  162. package/dist/tools/fiori/tools.js +651 -7
  163. package/dist/tools/fiori/ui5-version.js +16 -0
  164. package/dist/tools/index.js +8 -0
  165. package/dist/tools/local-build.js +6 -0
  166. package/dist/tools/sap-read.js +16 -1
  167. package/dist/tools/sap-write.js +122 -10
  168. package/dist/tools/shell/shell_exec.js +30 -12
  169. package/dist/tools/syntax-state.js +67 -0
  170. package/dist/tools/todo.js +23 -18
  171. package/dist/tools/transport-fit.js +462 -0
  172. package/dist/tools/transport-resolution.js +3 -1
  173. package/dist/tools/transport.js +33 -7
  174. package/dist/tools/verify.js +184 -8
  175. package/dist/ui/app.js +284 -146
  176. package/dist/ui/approval-modal.js +41 -15
  177. package/dist/ui/ask-answer-text.js +17 -0
  178. package/dist/ui/body.js +25 -6
  179. package/dist/ui/card-slot.js +98 -0
  180. package/dist/ui/checklist.js +9 -0
  181. package/dist/ui/coaching-picker-classic.js +8 -5
  182. package/dist/ui/command-palette.js +6 -4
  183. package/dist/ui/confirm-request.js +18 -0
  184. package/dist/ui/context-grid.js +2 -1
  185. package/dist/ui/file-palette.js +6 -4
  186. package/dist/ui/files-card-emitter.js +19 -0
  187. package/dist/ui/footer-line.js +16 -0
  188. package/dist/ui/footer.js +105 -104
  189. package/dist/ui/input-wrap.js +122 -0
  190. package/dist/ui/interrupt.js +103 -0
  191. package/dist/ui/key-burst.js +117 -0
  192. package/dist/ui/line-resolution.js +4 -3
  193. package/dist/ui/live-area.js +71 -0
  194. package/dist/ui/login-banner.js +54 -43
  195. package/dist/ui/rewind-panel.js +76 -24
  196. package/dist/ui/role-props.js +47 -0
  197. package/dist/ui/sap-state-store.js +1 -0
  198. package/dist/ui/session-timeline.js +49 -3
  199. package/dist/ui/skill-picker.js +5 -47
  200. package/dist/ui/text-input.js +128 -24
  201. package/dist/ui/todo-emitter.js +19 -0
  202. package/dist/ui/turn-status-emitter.js +28 -2
  203. package/dist/ui/turn-status.js +16 -19
  204. package/dist/ui/use-card-request.js +44 -0
  205. package/dist/ui/widgets/ask-card.js +521 -0
  206. package/dist/ui/widgets/bar-chart.js +11 -5
  207. package/dist/ui/widgets/coaching-picker.js +50 -55
  208. package/dist/ui/widgets/confirm-card.js +193 -0
  209. package/dist/ui/widgets/dep-graph.js +3 -2
  210. package/dist/ui/widgets/diff-viewer.js +6 -5
  211. package/dist/ui/widgets/files-card.js +97 -0
  212. package/dist/ui/widgets/question-card.js +2 -1
  213. package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
  214. package/dist/ui/widgets/shortcuts-sheet.js +25 -0
  215. package/dist/ui/widgets/stack-frames.js +2 -1
  216. package/dist/upgrade-check.js +39 -8
  217. package/package.json +11 -2
  218. package/test-harness/fe-draft/app.ts +188 -0
  219. package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
  220. package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
  221. package/test-harness/fe-draft/routes.ts +224 -0
  222. package/test-harness/fe-draft/routes.unit.test.ts +45 -0
  223. package/test-harness/fe-draft/server.ts +225 -0
  224. package/test-harness/fe-draft/variants.ts +196 -0
  225. package/vitest.smoke-e2e.config.ts +20 -0
@@ -0,0 +1,241 @@
1
+ export const MARKER_PREFIX = 'CSP-SMOKE-';
2
+ const NAME = '[A-Za-z0-9_]+';
3
+ /** `/sap/opu/odata4/<group>/<binding>/<srvd|srvd_a2x|srvd_f4>/<ns>/<definition>/<nnnn>/` (+ `$metadata`). */
4
+ const V4_PATH = new RegExp(`^/sap/opu/odata4/(${NAME})/(${NAME})/(srvd|srvd_a2x|srvd_f4)/(${NAME})/(${NAME})/(\\d{4})/(\\$metadata)?$`, 'i');
5
+ const CUSTOMER = /^[ZY]/i;
6
+ /**
7
+ * Is this the path of one of the customer's OWN OData V4 services? Both the
8
+ * service binding and the service definition must start with Z or Y. Anything
9
+ * that is not exactly the V4 path grammar — a traversal, an encoded character,
10
+ * a query, a full URL, an OData V2 path — is refused, not interpreted.
11
+ */
12
+ export function customerService(servicePath) {
13
+ const p = typeof servicePath === 'string' ? servicePath : '';
14
+ if (/[%?#\\]|\/\.\.?\/|\/\/|\s/.test(p)) {
15
+ return { ok: false, reason: `service path "${p}" contains an encoded character, a query, "..", "//" or a backslash` };
16
+ }
17
+ const m = V4_PATH.exec(p);
18
+ if (!m) {
19
+ return {
20
+ ok: false,
21
+ reason: `service path "${p}" is not an OData V4 service path of the form ` +
22
+ '/sap/opu/odata4/<group>/<binding>/srvd/<ns>/<definition>/<version>/',
23
+ };
24
+ }
25
+ const binding = m[2].toUpperCase();
26
+ const definition = m[5].toUpperCase();
27
+ const foreign = [binding, definition].filter((n) => !CUSTOMER.test(n));
28
+ if (foreign.length > 0) {
29
+ return {
30
+ ok: false,
31
+ reason: `${foreign.join(' and ')} ${foreign.length > 1 ? 'are' : 'is'} not in the customer namespace (Z*/Y*) — ` +
32
+ 'fiori_draft_smoke writes test rows only through the customer\'s own services',
33
+ };
34
+ }
35
+ const serviceRoot = p.replace(/\$metadata$/i, '');
36
+ return { ok: true, binding, definition, serviceRoot: serviceRoot.endsWith('/') ? serviceRoot : `${serviceRoot}/` };
37
+ }
38
+ // ── App URL ─────────────────────────────────────────────────────────────────
39
+ /** A local preview: http(s) on localhost, 127.0.0.0/8 or [::1] — never a deployed app. */
40
+ export function isLoopbackAppUrl(url) {
41
+ let u;
42
+ try {
43
+ u = new URL(url);
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ if (u.protocol !== 'http:' && u.protocol !== 'https:')
49
+ return false;
50
+ if (u.username || u.password)
51
+ return false;
52
+ const h = u.hostname.toLowerCase();
53
+ if (h === 'localhost' || h === '[::1]')
54
+ return true;
55
+ return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
56
+ }
57
+ // ── Marker ──────────────────────────────────────────────────────────────────
58
+ /** `yyyymmddHHMMSS-xxxx` (local time + four hex). */
59
+ export function newRunId(now = new Date(), rand = () => Math.floor(Math.random() * 0x10000)) {
60
+ const p = (n, w = 2) => String(n).padStart(w, '0');
61
+ const stamp = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}` +
62
+ `${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
63
+ return `${stamp}-${(rand() & 0xffff).toString(16).padStart(4, '0')}`;
64
+ }
65
+ export function isMarked(value) {
66
+ return typeof value === 'string' && value.startsWith(MARKER_PREFIX);
67
+ }
68
+ /** The rows a sweep may delete: ONLY those whose marker field, as read, carries the marker. */
69
+ export function selectSweepTargets(rows, markerField) {
70
+ return rows.filter((r) => isMarked(r?.[markerField]));
71
+ }
72
+ // ── Keys ────────────────────────────────────────────────────────────────────
73
+ const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
74
+ const NUMBER = /^-?\d+(\.\d+)?$/;
75
+ function keyLiteral(p, v) {
76
+ if (v === undefined || v === null)
77
+ throw new Error(`key ${p.name} has no value`);
78
+ switch (p.type) {
79
+ case 'Edm.Guid':
80
+ if (!GUID.test(String(v)))
81
+ throw new Error(`key ${p.name} is not a Guid: ${String(v).slice(0, 40)}`);
82
+ return String(v);
83
+ case 'Edm.Boolean':
84
+ if (typeof v !== 'boolean')
85
+ throw new Error(`key ${p.name} is not a boolean`);
86
+ return String(v);
87
+ case 'Edm.Byte':
88
+ case 'Edm.SByte':
89
+ case 'Edm.Int16':
90
+ case 'Edm.Int32':
91
+ case 'Edm.Int64':
92
+ case 'Edm.Decimal':
93
+ if (!NUMBER.test(String(v)))
94
+ throw new Error(`key ${p.name} is not a number`);
95
+ return String(v);
96
+ case 'Edm.Date':
97
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(v)))
98
+ throw new Error(`key ${p.name} is not a date`);
99
+ return String(v);
100
+ case 'Edm.String':
101
+ return `'${encodeURIComponent(String(v).replace(/'/g, "''")).replace(/%27/g, "'")}'`;
102
+ default:
103
+ throw new Error(`key ${p.name} has a type the smoke does not address rows by (${p.type})`);
104
+ }
105
+ }
106
+ /** `(K1=v1,K2=v2)` for a row, typed by the audit's key properties. */
107
+ export function keyPredicate(keys, row) {
108
+ if (keys.length === 0)
109
+ throw new Error('the entity has no key properties');
110
+ return `(${keys.map((k) => `${k.name}=${keyLiteral(k, row[k.name])}`).join(',')})`;
111
+ }
112
+ // ── Preconditions from the audit ────────────────────────────────────────────
113
+ export function markerFieldProblem(audit, markerField, marker) {
114
+ const p = audit.properties.find((x) => x.name === markerField);
115
+ if (!p)
116
+ return `the entity ${audit.entitySet} has no property "${markerField}" for the marker`;
117
+ if (p.isKey)
118
+ return `marker field ${markerField} is a key`;
119
+ if (p.computed)
120
+ return `marker field ${markerField} is computed by the backend`;
121
+ if (p.type !== 'Edm.String')
122
+ return `marker field ${markerField} is ${p.type}, not Edm.String`;
123
+ if (p.maxLength !== undefined && p.maxLength < marker.length) {
124
+ return `marker field ${markerField} holds ${p.maxLength} characters; the marker needs ${marker.length}`;
125
+ }
126
+ return undefined;
127
+ }
128
+ export function draftActionsProblem(audit) {
129
+ if (!audit.draft.root)
130
+ return `${audit.entitySet} is not a draft root (no Common.DraftRoot) — nothing to round-trip`;
131
+ if (!audit.draft.actions.activate) {
132
+ return `${audit.entitySet}'s Common.DraftRoot names no ActivationAction — the smoke never guesses one`;
133
+ }
134
+ return undefined;
135
+ }
136
+ // ── Setup payload ───────────────────────────────────────────────────────────
137
+ function isoDate(d) {
138
+ return d.toISOString().slice(0, 10);
139
+ }
140
+ /**
141
+ * The body of the new draft: the marker, plus every mandatory, non-key,
142
+ * non-computed property — its first value-help value when it has one
143
+ * (`firstValues`), else a plain value of its type. A mandatory field it cannot
144
+ * fill honestly is an error.
145
+ */
146
+ export function buildSetupPayload(audit, markerField, marker, firstValues, now) {
147
+ const payload = { [markerField]: marker };
148
+ for (const p of audit.properties) {
149
+ if (!p.mandatory || p.isKey || p.computed || p.name === markerField)
150
+ continue;
151
+ if (firstValues[p.name] !== undefined) {
152
+ payload[p.name] = firstValues[p.name];
153
+ continue;
154
+ }
155
+ switch (p.type) {
156
+ case 'Edm.String':
157
+ payload[p.name] = 'CSP smoke'.slice(0, p.maxLength ?? 9);
158
+ break;
159
+ case 'Edm.Date':
160
+ payload[p.name] = isoDate(now);
161
+ break;
162
+ case 'Edm.DateTimeOffset':
163
+ payload[p.name] = now.toISOString();
164
+ break;
165
+ case 'Edm.TimeOfDay':
166
+ payload[p.name] = '00:00:00';
167
+ break;
168
+ case 'Edm.Boolean':
169
+ payload[p.name] = false;
170
+ break;
171
+ case 'Edm.Byte':
172
+ case 'Edm.SByte':
173
+ case 'Edm.Int16':
174
+ case 'Edm.Int32':
175
+ case 'Edm.Int64':
176
+ case 'Edm.Decimal':
177
+ case 'Edm.Double':
178
+ case 'Edm.Single':
179
+ payload[p.name] = 1;
180
+ break;
181
+ default:
182
+ return {
183
+ error: `cannot fill the mandatory field ${p.name} (${p.type}) of a new row — give it a value help or a default`,
184
+ };
185
+ }
186
+ }
187
+ return { payload };
188
+ }
189
+ // ── Which field the round trip changes ──────────────────────────────────────
190
+ export function pickEditField(audit, explicit, markerField) {
191
+ const usable = (p) => !!p.valueHelp && !p.isKey && !p.computed;
192
+ if (explicit !== undefined) {
193
+ if (explicit === markerField)
194
+ return { error: `editField ${explicit} is the marker field` };
195
+ const p = audit.properties.find((x) => x.name === explicit);
196
+ if (!p)
197
+ return { error: `the entity ${audit.entitySet} has no property "${explicit}"` };
198
+ if (!usable(p))
199
+ return { error: `editField ${explicit} has no value help (or is a key / computed)` };
200
+ return { property: p };
201
+ }
202
+ const order = audit.lineItem.map((c) => c.property).filter((x) => !!x);
203
+ const ranked = [...audit.properties]
204
+ .filter((p) => usable(p) && p.rendered && p.name !== markerField)
205
+ .sort((a, b) => {
206
+ const ia = order.indexOf(a.name);
207
+ const ib = order.indexOf(b.name);
208
+ if (a.coded !== b.coded)
209
+ return a.coded ? -1 : 1;
210
+ return (ia < 0 ? 1e6 : ia) - (ib < 0 ? 1e6 : ib);
211
+ });
212
+ if (ranked.length === 0) {
213
+ return { error: `${audit.entitySet} has no field with a value help to change — the value-help check needs one` };
214
+ }
215
+ return { property: ranked[0] };
216
+ }
217
+ /** A CAP OData service path: /seg/…/<service>/ (optionally …/$metadata) — never /sap/. */
218
+ export function capService(servicePath) {
219
+ const p = typeof servicePath === 'string' ? servicePath.replace(/\$metadata$/i, '') : '';
220
+ if (/[%?#\\]|\/\.\.?\/|\/\/|\s/.test(p)) {
221
+ return { ok: false, reason: `service path "${p}" contains an encoded character, a query, "..", "//" or a backslash` };
222
+ }
223
+ const root = p.endsWith('/') ? p : `${p}/`;
224
+ if (!/^\/(?:[A-Za-z0-9_-]+\/)+$/.test(root) || root.startsWith('/sap/')) {
225
+ return { ok: false, reason: `service path "${p}" is not a CAP service path like /odata/v4/<service>/` };
226
+ }
227
+ const segs = root.split('/').filter(Boolean);
228
+ return { ok: true, service: segs[segs.length - 1], serviceRoot: root };
229
+ }
230
+ /**
231
+ * True when `html` is CAP's development index ("Welcome to @sap/cds") AND it
232
+ * lists `serviceRoot`. CAP serves this page only in development — a production
233
+ * CAP app serves none, and a UI5 preview proxy serves its own page — so this is
234
+ * the proof that the loopback origin is the CAP dev server itself.
235
+ */
236
+ export function isCapDevIndex(html, serviceRoot) {
237
+ if (typeof html !== 'string' || !/Welcome to/.test(html) || !/@sap\/cds/.test(html))
238
+ return false;
239
+ const rel = serviceRoot.replace(/^\//, '').replace(/\/$/, '');
240
+ return html.includes(`href="${rel}"`) || html.includes(`href="/${rel}"`);
241
+ }