@cspeach/cli 0.6.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 (210) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +108 -0
  3. package/dist/agent/anthropic-provider.js +59 -0
  4. package/dist/agent/llm-provider.js +1 -0
  5. package/dist/agent/loop.js +709 -0
  6. package/dist/agent/maybe-build-project-context.js +126 -0
  7. package/dist/agent/providers/ai-hub-provider.js +58 -0
  8. package/dist/agent/providers/byok-provider.js +53 -0
  9. package/dist/agent/providers/factory.js +13 -0
  10. package/dist/agent/providers/local-provider.js +125 -0
  11. package/dist/agent/repair-partial.js +31 -0
  12. package/dist/agent/retry-key.js +58 -0
  13. package/dist/agent/retry.js +17 -0
  14. package/dist/agent/sap-connection-adapter.js +82 -0
  15. package/dist/agent/skill-checkpoint.js +119 -0
  16. package/dist/agent/tool-dispatch.js +47 -0
  17. package/dist/agent/turn-assistant-text.js +49 -0
  18. package/dist/agent/turn-error-ux.js +126 -0
  19. package/dist/agent/turn-stream.js +79 -0
  20. package/dist/agent/turn-watchdog.js +71 -0
  21. package/dist/approvals/advisory-prompt.js +40 -0
  22. package/dist/approvals/advisory-render.js +38 -0
  23. package/dist/approvals/approval-prompt.js +100 -0
  24. package/dist/approvals/jwt.js +33 -0
  25. package/dist/approvals/render.js +211 -0
  26. package/dist/approvals/risk-floor.js +26 -0
  27. package/dist/auth/api-key.js +40 -0
  28. package/dist/auth/auth-file.js +59 -0
  29. package/dist/auth/device.js +8 -0
  30. package/dist/auth/me.js +19 -0
  31. package/dist/classifier/client.js +58 -0
  32. package/dist/cli-args.js +38 -0
  33. package/dist/cli.js +148 -0
  34. package/dist/commands/config-set.js +245 -0
  35. package/dist/commands/config-show.js +159 -0
  36. package/dist/commands/help.js +93 -0
  37. package/dist/commands/login.js +122 -0
  38. package/dist/commands/logout.js +17 -0
  39. package/dist/commands/project-context-impact.js +215 -0
  40. package/dist/commands/reroute.js +60 -0
  41. package/dist/commands/spec-gap-status.js +52 -0
  42. package/dist/commands/whoami.js +35 -0
  43. package/dist/config/loader.js +67 -0
  44. package/dist/config/paths.js +20 -0
  45. package/dist/doctor/checks/_http-probe.js +56 -0
  46. package/dist/doctor/checks/auth.js +15 -0
  47. package/dist/doctor/checks/cert.js +24 -0
  48. package/dist/doctor/checks/forge-rules.js +102 -0
  49. package/dist/doctor/checks/keychain-fallback.js +14 -0
  50. package/dist/doctor/checks/keychain.js +23 -0
  51. package/dist/doctor/checks/llm-mode.js +27 -0
  52. package/dist/doctor/checks/proxy.js +13 -0
  53. package/dist/doctor/checks/sap.js +33 -0
  54. package/dist/doctor/checks/skill.js +24 -0
  55. package/dist/doctor/checks/write-mode.js +34 -0
  56. package/dist/doctor/checks/zcspeach.js +76 -0
  57. package/dist/doctor/run.js +46 -0
  58. package/dist/errors/codes.js +12 -0
  59. package/dist/index.js +6 -0
  60. package/dist/lock-contention.js +22 -0
  61. package/dist/one-shot.js +104 -0
  62. package/dist/project-context/conventions.js +309 -0
  63. package/dist/project-context/detect.js +250 -0
  64. package/dist/project-context/domain/abap-cloud.js +26 -0
  65. package/dist/project-context/domain/abapgit.js +177 -0
  66. package/dist/project-context/domain/cap.js +164 -0
  67. package/dist/project-context/domain/fiori.js +326 -0
  68. package/dist/project-context/git.js +115 -0
  69. package/dist/project-context/index-files.js +235 -0
  70. package/dist/project-context/index.js +117 -0
  71. package/dist/project-context/render.js +308 -0
  72. package/dist/project-context/types.js +14 -0
  73. package/dist/projects/build.js +20 -0
  74. package/dist/projects/canonicalize.js +39 -0
  75. package/dist/projects/email-template.js +54 -0
  76. package/dist/projects/extract-cca.js +139 -0
  77. package/dist/projects/extract-design.js +107 -0
  78. package/dist/projects/extract-estimate.js +93 -0
  79. package/dist/projects/extract-modernize.js +130 -0
  80. package/dist/projects/extract-spec-gap.js +101 -0
  81. package/dist/projects/extract-test-coverage.js +137 -0
  82. package/dist/projects/extract-upgrade.js +230 -0
  83. package/dist/projects/filename.js +18 -0
  84. package/dist/projects/index.js +8 -0
  85. package/dist/projects/migration.js +111 -0
  86. package/dist/projects/promote-command.js +96 -0
  87. package/dist/projects/promote.js +107 -0
  88. package/dist/projects/save-command.js +124 -0
  89. package/dist/projects/save.js +21 -0
  90. package/dist/projects/status.js +170 -0
  91. package/dist/projects/types.js +1 -0
  92. package/dist/projects/validate.js +146 -0
  93. package/dist/projects/workspace.js +478 -0
  94. package/dist/renderer/abap-inline.js +121 -0
  95. package/dist/renderer/banners.js +39 -0
  96. package/dist/renderer/highlighters/abap.js +126 -0
  97. package/dist/renderer/highlighters/bdef.js +81 -0
  98. package/dist/renderer/highlighters/cds.js +91 -0
  99. package/dist/renderer/markdown.js +291 -0
  100. package/dist/renderer/pipeline.js +201 -0
  101. package/dist/renderer/progress-chatter.js +237 -0
  102. package/dist/renderer/question-normalizer.js +306 -0
  103. package/dist/renderer/severity.js +61 -0
  104. package/dist/renderer/status-footer.js +50 -0
  105. package/dist/renderer/syntax.js +58 -0
  106. package/dist/renderer/tables.js +55 -0
  107. package/dist/renderer/thinking-heartbeat.js +70 -0
  108. package/dist/renderer/tool-widget.js +199 -0
  109. package/dist/renderer/tty.js +66 -0
  110. package/dist/renderer/widget-extractor.js +87 -0
  111. package/dist/renderer/widget-fallback.js +78 -0
  112. package/dist/renderer/widget-schemas.js +43 -0
  113. package/dist/repl/at-completer.js +64 -0
  114. package/dist/repl/at-picker.js +122 -0
  115. package/dist/repl/bracketed-paste.js +284 -0
  116. package/dist/repl/current-transport.js +46 -0
  117. package/dist/repl/diff-display.js +41 -0
  118. package/dist/repl/file-picker.js +219 -0
  119. package/dist/repl/inquirer-guard.js +130 -0
  120. package/dist/repl/inquirer-theme.js +41 -0
  121. package/dist/repl/rule8-detector.js +99 -0
  122. package/dist/repl/safety-confirm.js +106 -0
  123. package/dist/repl/safety-mode-state.js +36 -0
  124. package/dist/repl/slash-completer.js +59 -0
  125. package/dist/repl/slash-picker.js +124 -0
  126. package/dist/repl/update-method-preview-hook.js +45 -0
  127. package/dist/repl.js +1383 -0
  128. package/dist/router/classifier.js +38 -0
  129. package/dist/router/intent-extractor.js +140 -0
  130. package/dist/router/routing-decision.js +19 -0
  131. package/dist/sap/connection-manager.js +52 -0
  132. package/dist/sap/onboarding.js +178 -0
  133. package/dist/sap/system-info.js +515 -0
  134. package/dist/session/awaiting-answer.js +73 -0
  135. package/dist/session/gc.js +28 -0
  136. package/dist/session/pending.js +37 -0
  137. package/dist/session/resume.js +77 -0
  138. package/dist/session/schema.js +20 -0
  139. package/dist/session/store.js +147 -0
  140. package/dist/session/time-ago.js +41 -0
  141. package/dist/skill-catalog.js +222 -0
  142. package/dist/skills/bundled-skills.js +1 -0
  143. package/dist/skills/canonical.js +12 -0
  144. package/dist/skills/manifest-client.js +93 -0
  145. package/dist/skills/promotion-dispatch.js +24 -0
  146. package/dist/skills/signing-public-key.js +4 -0
  147. package/dist/skills/source-bundled.js +20 -0
  148. package/dist/skills/source-managed.js +26 -0
  149. package/dist/skills/source-manifest.js +26 -0
  150. package/dist/tools/_command-shared.js +110 -0
  151. package/dist/tools/_filesystem-shared.js +81 -0
  152. package/dist/tools/_flag.js +39 -0
  153. package/dist/tools/approval.js +228 -0
  154. package/dist/tools/ask-question.js +205 -0
  155. package/dist/tools/dispatch-skill.js +81 -0
  156. package/dist/tools/filesystem/file-edit.js +140 -0
  157. package/dist/tools/filesystem/file-read.js +89 -0
  158. package/dist/tools/filesystem/file-write.js +128 -0
  159. package/dist/tools/filesystem/glob.js +177 -0
  160. package/dist/tools/filesystem/grep.js +163 -0
  161. package/dist/tools/index.js +32 -0
  162. package/dist/tools/project/convention_get.js +91 -0
  163. package/dist/tools/project/playbook_get.js +132 -0
  164. package/dist/tools/project/project_context_get.js +101 -0
  165. package/dist/tools/sap-read.js +454 -0
  166. package/dist/tools/sap-write.js +746 -0
  167. package/dist/tools/shell/shell_exec.js +209 -0
  168. package/dist/tools/snapshot.js +107 -0
  169. package/dist/tools/subagent/_background-shared.js +133 -0
  170. package/dist/tools/subagent/agent_run.js +186 -0
  171. package/dist/tools/subagent/background_run.js +143 -0
  172. package/dist/tools/subagent/monitor_emit.js +65 -0
  173. package/dist/tools/subagent/schedule_create.js +131 -0
  174. package/dist/tools/transport.js +233 -0
  175. package/dist/tools/update-method-intercept.js +119 -0
  176. package/dist/tools/verify.js +39 -0
  177. package/dist/tools/web/_web-shared.js +251 -0
  178. package/dist/tools/web/web_fetch.js +257 -0
  179. package/dist/tools/web/web_search.js +195 -0
  180. package/dist/tools/write-mode.js +22 -0
  181. package/dist/ui/app.js +95 -0
  182. package/dist/ui/approval-emitter.js +10 -0
  183. package/dist/ui/approval-modal.js +53 -0
  184. package/dist/ui/ascii-chars.js +6 -0
  185. package/dist/ui/body.js +102 -0
  186. package/dist/ui/coaching-picker-classic.js +36 -0
  187. package/dist/ui/coaching-picker-emitter.js +27 -0
  188. package/dist/ui/command-palette.js +34 -0
  189. package/dist/ui/error-emitter.js +21 -0
  190. package/dist/ui/footer.js +103 -0
  191. package/dist/ui/header.js +17 -0
  192. package/dist/ui/ink-classifier-route.js +19 -0
  193. package/dist/ui/login-banner.js +72 -0
  194. package/dist/ui/rich-error-box.js +9 -0
  195. package/dist/ui/sap-state-store.js +65 -0
  196. package/dist/ui/session-timeline.js +31 -0
  197. package/dist/ui/sidebar.js +10 -0
  198. package/dist/ui/skill-picker.js +50 -0
  199. package/dist/ui/status-row.js +12 -0
  200. package/dist/ui/widget-control.js +4 -0
  201. package/dist/ui/widgets/bar-chart.js +15 -0
  202. package/dist/ui/widgets/coaching-picker.js +41 -0
  203. package/dist/ui/widgets/component-registry.js +12 -0
  204. package/dist/ui/widgets/dep-graph.js +9 -0
  205. package/dist/ui/widgets/diff-viewer.js +11 -0
  206. package/dist/ui/widgets/question-card.js +11 -0
  207. package/dist/ui/widgets/stack-frames.js +5 -0
  208. package/dist/upgrade-check.js +28 -0
  209. package/dist/upgrade.js +13 -0
  210. package/package.json +83 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Bracketed-paste decoder for the CSPeach REPL.
3
+ *
4
+ * Why this exists: Node's `readline` treats `\n` as a hard line terminator
5
+ * and submits a line the instant it sees one. When a user pastes prose
6
+ * that contains newlines, the first line fires `rl.question()` and the
7
+ * remaining lines go to the next prompt (or get eaten by a sub-prompt).
8
+ * We observed this in the wild: a ~260-char pasted prompt reached
9
+ * `runTurn` as 88 chars (the first wrapped line only), silently losing
10
+ * the package and transport the user had stated.
11
+ *
12
+ * Modern terminals support "bracketed paste mode": when enabled, pastes
13
+ * arrive wrapped in the escape sequences `\x1b[200~` (start) and
14
+ * `\x1b[201~` (end). Newlines *inside* those markers are part of the
15
+ * paste, not line terminators.
16
+ *
17
+ * This module provides:
18
+ * - `enableBracketedPaste()` / `disableBracketedPaste()` — emits the
19
+ * DEC control sequences that ask the terminal to wrap future pastes.
20
+ * - `BracketedPasteDecoder` — a `Transform` stream you pipe stdin
21
+ * through. When it sees the paste-start marker, it buffers until the
22
+ * paste-end marker, then emits the buffer as a single chunk with
23
+ * internal `\r\n` / `\n` replaced by a single space. Outside paste
24
+ * mode, data passes through untouched.
25
+ *
26
+ * Wiring: repl.tsx pipes `process.stdin -> decoder` and passes `decoder`
27
+ * as the `input` option to `readline.createInterface`. Readline's
28
+ * line-splitting logic operates on the post-decode stream, which no
29
+ * longer contains mid-paste newlines.
30
+ *
31
+ * Caveats:
32
+ * - Not all terminals support bracketed paste (e.g. basic Windows
33
+ * cmd.exe without Windows Terminal, some ssh clients). If the terminal
34
+ * ignores our `\x1b[?2004h` request, pastes still arrive as raw text;
35
+ * the decoder never sees a start marker so the stream is pass-through.
36
+ * Users on old terminals are no worse off than before — they still
37
+ * have the line-split behavior, not a new failure.
38
+ * - Ink has its own stdin handling. This decoder is only for the
39
+ * classic REPL path (non-Ink). Ink mode uses its own input component.
40
+ */
41
+ import { Transform } from 'node:stream';
42
+ /** DEC Private Mode 2004 — enable bracketed paste. */
43
+ export const BRACKETED_PASTE_ENABLE = '\x1b[?2004h';
44
+ /** DEC Private Mode 2004 — disable bracketed paste. */
45
+ export const BRACKETED_PASTE_DISABLE = '\x1b[?2004l';
46
+ const START_MARKER = Buffer.from('\x1b[200~', 'ascii');
47
+ const END_MARKER = Buffer.from('\x1b[201~', 'ascii');
48
+ /**
49
+ * Ask the terminal to wrap future pastes with the 2004 control sequences.
50
+ * Safe to call when the terminal doesn't support bracketed paste — the
51
+ * sequence is simply ignored.
52
+ */
53
+ export function enableBracketedPaste(stream = process.stdout) {
54
+ if (stream.isTTY)
55
+ stream.write(BRACKETED_PASTE_ENABLE);
56
+ }
57
+ /** Reverse of enableBracketedPaste. Call on REPL shutdown. */
58
+ export function disableBracketedPaste(stream = process.stdout) {
59
+ if (stream.isTTY)
60
+ stream.write(BRACKETED_PASTE_DISABLE);
61
+ }
62
+ /**
63
+ * Transform stream that collapses the content between `\x1b[200~` and
64
+ * `\x1b[201~` into a single line (newlines replaced with spaces). The
65
+ * markers themselves are stripped. All other bytes pass through
66
+ * unchanged, including keystrokes, arrow-key escape sequences, and
67
+ * terminal responses — these are not affected because they never contain
68
+ * the paste markers.
69
+ *
70
+ * The implementation uses Buffer operations (not string concat) to avoid
71
+ * splitting multi-byte UTF-8 sequences at chunk boundaries when a paste
72
+ * spans multiple `data` events.
73
+ */
74
+ export class BracketedPasteDecoder extends Transform {
75
+ /**
76
+ * Readline with `terminal: true` inspects `input.isTTY` and calls
77
+ * `input.setRawMode(true)` so arrow keys, history navigation, and
78
+ * Ctrl-C work. A plain Transform has neither flag nor method, so
79
+ * readline would silently fall back to non-TTY line mode and the user
80
+ * would lose history and keyboard shortcuts.
81
+ *
82
+ * We present as a TTY and forward the raw-mode toggle and the size
83
+ * getters to the real `process.stdin` / `process.stdout`.
84
+ */
85
+ // eslint-disable-next-line @typescript-eslint/naming-convention
86
+ isTTY = true;
87
+ setRawMode(mode) {
88
+ if (typeof process.stdin.setRawMode === 'function')
89
+ process.stdin.setRawMode(mode);
90
+ return this;
91
+ }
92
+ get columns() {
93
+ return process.stdout.columns;
94
+ }
95
+ get rows() {
96
+ return process.stdout.rows;
97
+ }
98
+ inPaste = false;
99
+ pasteBuf = Buffer.alloc(0);
100
+ /**
101
+ * Carry-over bytes from the tail of the previous chunk. Used when a
102
+ * chunk ends mid-marker (e.g. the chunk finishes with `\x1b[20` and the
103
+ * next starts with `0~`). We stash up to 5 trailing bytes (marker is 6
104
+ * bytes, so max 5 can ambiguously be a partial match) and prepend them
105
+ * on the next chunk.
106
+ */
107
+ carry = Buffer.alloc(0);
108
+ /**
109
+ * Timer that flushes the carry buffer if no follow-up bytes arrive.
110
+ *
111
+ * 2026-05-02: without this, a bare Esc keystroke (single byte 0x1B)
112
+ * was held forever — the decoder couldn't tell whether more bytes
113
+ * (forming `\x1b[200~`) were coming. Esc never reached readline,
114
+ * so our keypress handler (and any user expecting Esc to do
115
+ * anything) was permanently blocked.
116
+ *
117
+ * Real escape sequences arrive as a burst — ~ms apart at most. A
118
+ * bare keystroke has a much larger gap before the next event. 50ms
119
+ * is well above any sensible burst latency and well below any
120
+ * latency the user would notice on a deliberate keypress.
121
+ */
122
+ carryTimer = null;
123
+ static CARRY_FLUSH_MS = 50;
124
+ _transform(chunk, _enc, cb) {
125
+ if (this.carryTimer) {
126
+ clearTimeout(this.carryTimer);
127
+ this.carryTimer = null;
128
+ }
129
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
130
+ const combined = Buffer.concat([this.carry, buf]);
131
+ this.carry = Buffer.alloc(0);
132
+ try {
133
+ this.process(combined);
134
+ this.scheduleCarryFlush();
135
+ cb();
136
+ }
137
+ catch (err) {
138
+ cb(err);
139
+ }
140
+ }
141
+ /**
142
+ * Arm the carry-flush timer. If no new data arrives within
143
+ * CARRY_FLUSH_MS, emit whatever's in `this.carry` so a bare Esc
144
+ * (or any other prefix-of-marker keystroke without a follow-up)
145
+ * isn't permanently held back. Only relevant when not currently
146
+ * inside a paste — paste content is buffered separately.
147
+ */
148
+ scheduleCarryFlush() {
149
+ if (this.inPaste)
150
+ return;
151
+ if (this.carry.length === 0)
152
+ return;
153
+ this.carryTimer = setTimeout(() => {
154
+ this.carryTimer = null;
155
+ if (this.carry.length > 0 && !this.inPaste) {
156
+ const flushed = this.carry;
157
+ this.carry = Buffer.alloc(0);
158
+ this.push(flushed);
159
+ }
160
+ }, BracketedPasteDecoder.CARRY_FLUSH_MS);
161
+ this.carryTimer.unref?.();
162
+ }
163
+ _flush(cb) {
164
+ if (this.carryTimer) {
165
+ clearTimeout(this.carryTimer);
166
+ this.carryTimer = null;
167
+ }
168
+ // End of stream. If we were still inside a paste (unterminated),
169
+ // flush the buffered content as a single line so it's not lost.
170
+ if (this.inPaste && this.pasteBuf.length > 0) {
171
+ this.push(this.collapseNewlines(this.pasteBuf));
172
+ this.pasteBuf = Buffer.alloc(0);
173
+ this.inPaste = false;
174
+ }
175
+ if (this.carry.length > 0) {
176
+ this.push(this.carry);
177
+ this.carry = Buffer.alloc(0);
178
+ }
179
+ cb();
180
+ }
181
+ process(data) {
182
+ let i = 0;
183
+ while (i < data.length) {
184
+ if (this.inPaste) {
185
+ const endIdx = data.indexOf(END_MARKER, i);
186
+ if (endIdx === -1) {
187
+ // Whole remainder is paste content; check if the tail might be
188
+ // a partial end-marker and carry it.
189
+ const safe = this.safeSliceLength(data, i);
190
+ this.pasteBuf = Buffer.concat([this.pasteBuf, data.subarray(i, safe)]);
191
+ this.carry = data.subarray(safe);
192
+ return;
193
+ }
194
+ this.pasteBuf = Buffer.concat([this.pasteBuf, data.subarray(i, endIdx)]);
195
+ this.push(this.collapseNewlines(this.pasteBuf));
196
+ // Append a newline so readline commits the pasted line as a
197
+ // single input — otherwise the user would still have to press
198
+ // Enter after the paste.
199
+ this.push(Buffer.from('\n'));
200
+ this.pasteBuf = Buffer.alloc(0);
201
+ this.inPaste = false;
202
+ i = endIdx + END_MARKER.length;
203
+ }
204
+ else {
205
+ const startIdx = data.indexOf(START_MARKER, i);
206
+ if (startIdx === -1) {
207
+ const safe = this.safeSliceLength(data, i);
208
+ this.push(data.subarray(i, safe));
209
+ this.carry = data.subarray(safe);
210
+ return;
211
+ }
212
+ if (startIdx > i)
213
+ this.push(data.subarray(i, startIdx));
214
+ this.inPaste = true;
215
+ i = startIdx + START_MARKER.length;
216
+ }
217
+ }
218
+ }
219
+ /**
220
+ * Return the index up to which it's safe to emit bytes without risk of
221
+ * splitting a paste marker across chunks. If the tail of `data` could
222
+ * be a prefix of either marker, hold those bytes back.
223
+ */
224
+ safeSliceLength(data, from) {
225
+ const tail = data.length - from;
226
+ if (tail === 0)
227
+ return data.length;
228
+ const markerLen = START_MARKER.length; // both markers are 6 bytes
229
+ const windowStart = Math.max(from, data.length - (markerLen - 1));
230
+ for (let k = windowStart; k < data.length; k++) {
231
+ if (isPrefixOfMarker(data, k, data.length))
232
+ return k;
233
+ }
234
+ return data.length;
235
+ }
236
+ collapseNewlines(buf) {
237
+ // Replace \r\n, \n, and \r with a single space. Fast path: if there
238
+ // are no newlines, return the buffer as-is.
239
+ const hasNewline = buf.includes(0x0a) || buf.includes(0x0d);
240
+ if (!hasNewline)
241
+ return buf;
242
+ const out = Buffer.alloc(buf.length);
243
+ let o = 0;
244
+ for (let i = 0; i < buf.length; i++) {
245
+ const b = buf[i];
246
+ if (b === 0x0d) {
247
+ // Swallow \r and, if followed by \n, the \n too. Emit a single space.
248
+ if (i + 1 < buf.length && buf[i + 1] === 0x0a)
249
+ i++;
250
+ out[o++] = 0x20;
251
+ }
252
+ else if (b === 0x0a) {
253
+ out[o++] = 0x20;
254
+ }
255
+ else {
256
+ out[o++] = b;
257
+ }
258
+ }
259
+ return out.subarray(0, o);
260
+ }
261
+ }
262
+ /**
263
+ * True when `data[start..end)` is a strict prefix of either the start or
264
+ * end paste marker (i.e. too short to decide yet, but could become one).
265
+ */
266
+ function isPrefixOfMarker(data, start, end) {
267
+ const len = end - start;
268
+ if (len === 0)
269
+ return false;
270
+ if (len >= START_MARKER.length)
271
+ return false; // full-length matches handled elsewhere
272
+ for (const marker of [START_MARKER, END_MARKER]) {
273
+ let match = true;
274
+ for (let k = 0; k < len; k++) {
275
+ if (data[start + k] !== marker[k]) {
276
+ match = false;
277
+ break;
278
+ }
279
+ }
280
+ if (match)
281
+ return true;
282
+ }
283
+ return false;
284
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Session-scoped current-transport state. Set by:
3
+ * - successful sap_transport_create
4
+ * - explicit slash command /transport <trkorr>
5
+ * Cleared by:
6
+ * - explicit slash command /transport clear
7
+ * Read by:
8
+ * - tool handlers via ctx.currentTransport.get() (3-tier fallback when LLM omits transport arg)
9
+ *
10
+ * Rule 9 (transport isolation) — keeps all writes in one session attached to the
11
+ * same dedicated transport. The LLM can still override per-call by passing
12
+ * `transport` explicitly; in that case the handler logs a warning if the
13
+ * passed transport differs from session current-transport.
14
+ *
15
+ * Module-scope singleton per CLI process. Tests must call clearCurrentTransport()
16
+ * in beforeEach to avoid leakage across Vitest test files.
17
+ */
18
+ let currentTransport = null;
19
+ export function setCurrentTransport(trkorr) {
20
+ currentTransport = trkorr;
21
+ }
22
+ export function getCurrentTransport() {
23
+ return currentTransport;
24
+ }
25
+ export function clearCurrentTransport() {
26
+ currentTransport = null;
27
+ }
28
+ /**
29
+ * Handles the `/transport [<trkorr>|clear]` slash command. Returns user-facing
30
+ * status text. Mirrors the shape of handleSafetyModeCommand in safety-mode-state.ts.
31
+ */
32
+ export function handleTransportCommand(arg) {
33
+ const trimmed = arg.trim();
34
+ if (trimmed === '') {
35
+ const cur = getCurrentTransport();
36
+ return cur === null
37
+ ? 'No current transport set. Use `/transport <TRKORR>` to attach one.'
38
+ : `Current transport: ${cur}`;
39
+ }
40
+ if (trimmed.toLowerCase() === 'clear') {
41
+ clearCurrentTransport();
42
+ return 'Current transport cleared.';
43
+ }
44
+ setCurrentTransport(trimmed);
45
+ return `Current transport set to ${trimmed}.`;
46
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Renders a unified diff between two ABAP source strings for terminal display.
3
+ * Used as the preview gate before any sap_update_method call so the user sees
4
+ * exactly what's about to change in SAP, with green +adds / red -removes.
5
+ *
6
+ * Pure function: no I/O, no side effects. The REPL composes this with an
7
+ * @inquirer/prompts.confirm gate (see update-method-intercept.ts).
8
+ */
9
+ import { createPatch } from 'diff';
10
+ import chalk from 'chalk';
11
+ const NO_CHANGES_PREFIX = 'No changes';
12
+ export function renderDiff(currentSource, proposedSource, opts) {
13
+ if (currentSource === proposedSource) {
14
+ return `${NO_CHANGES_PREFIX} for ${opts.label}.`;
15
+ }
16
+ const patch = createPatch(opts.label, currentSource, proposedSource, '(current)', '(proposed)');
17
+ if (!opts.syntaxHighlight)
18
+ return patch;
19
+ return colorizeUnified(patch);
20
+ }
21
+ /**
22
+ * Colorises a unified-diff string. Lines starting with `+` (but not `+++`) get
23
+ * green; lines starting with `-` (but not `---`) get red; `@@` hunk headers get
24
+ * cyan; everything else passes through unchanged.
25
+ */
26
+ function colorizeUnified(patch) {
27
+ return patch
28
+ .split('\n')
29
+ .map((line) => {
30
+ if (line.startsWith('+++') || line.startsWith('---'))
31
+ return line;
32
+ if (line.startsWith('+'))
33
+ return chalk.green(line);
34
+ if (line.startsWith('-'))
35
+ return chalk.red(line);
36
+ if (line.startsWith('@@'))
37
+ return chalk.cyan(line);
38
+ return line;
39
+ })
40
+ .join('\n');
41
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Two-step interactive picker for `.cspeach.json` files in the workspace.
3
+ *
4
+ * UX:
5
+ * 1. Search-as-you-type list of files (filename + artefactType + version)
6
+ * 2. Action menu tailored to the picked artefactType — pick one of:
7
+ * spec-gap → --status, --from /abap-design
8
+ * design → --status, --from /abap-estimate
9
+ * estimate → --status only
10
+ * unknown → all --status variants
11
+ *
12
+ * Returns the fully-formed slash command (e.g. "/abap-design --from
13
+ * @<filename>"), or null if the user cancelled at either step.
14
+ *
15
+ * Uses the same withInquirer guard / cancel-sentinel pattern as
16
+ * slash-picker.ts so cancellation in either step doesn't drop the user
17
+ * back to the parent shell.
18
+ */
19
+ import { search, select } from '@inquirer/prompts';
20
+ import { withInquirer } from './inquirer-guard.js';
21
+ import { listProjectFiles, detectDesignShape } from '../projects/workspace.js';
22
+ const CANCEL_VALUE = '__cspeach_cancel__';
23
+ /**
24
+ * Compute the menu of valid actions for an envelope of a given
25
+ * artefactType. Order: most-common first.
26
+ *
27
+ * For `design` envelopes, the BUILD-skill recommendation depends on
28
+ * what the design actually contains. RAP-shaped designs (BDEF /
29
+ * Behavior Pool / Service Binding present) recommend `/abap-rap`;
30
+ * classic designs (report / class / function module) recommend
31
+ * `/abap-generate`. Estimate is always demoted to a side option —
32
+ * it's optional, not the canonical next step. `designShape` defaults
33
+ * to 'unknown' so non-design call sites pass nothing.
34
+ */
35
+ function envelopeActionsFor(type, designShape = 'unknown') {
36
+ if (type === 'spec-gap') {
37
+ return [
38
+ { label: '/abap-design --from (promote → design)', command: '/abap-design --from' },
39
+ { label: '/abap-spec-gap --status (inspect)', command: '/abap-spec-gap --status' },
40
+ ];
41
+ }
42
+ if (type === 'design') {
43
+ const ALL = {
44
+ rap: { label: '/abap-rap --from (build the RAP stack — recommended)', command: '/abap-rap --from' },
45
+ gen: { label: '/abap-generate --from (build the objects)', command: '/abap-generate --from' },
46
+ status: { label: '/abap-design --status (inspect)', command: '/abap-design --status' },
47
+ est: { label: '/abap-estimate --from (effort estimate — optional)', command: '/abap-estimate --from' },
48
+ };
49
+ if (designShape === 'rap') {
50
+ return [
51
+ ALL.rap,
52
+ { ...ALL.gen, label: '/abap-generate --from (build as classic — alternative)' },
53
+ ALL.status,
54
+ ALL.est,
55
+ ];
56
+ }
57
+ if (designShape === 'classic') {
58
+ return [
59
+ { ...ALL.gen, label: '/abap-generate --from (build the objects — recommended)' },
60
+ { ...ALL.rap, label: '/abap-rap --from (build as RAP — alternative)' },
61
+ ALL.status,
62
+ ALL.est,
63
+ ];
64
+ }
65
+ // unknown shape — neutral order, no recommendation badge
66
+ return [ALL.gen, ALL.rap, ALL.status, ALL.est];
67
+ }
68
+ if (type === 'estimate') {
69
+ return [
70
+ { label: '/abap-estimate --status (inspect)', command: '/abap-estimate --status' },
71
+ ];
72
+ }
73
+ if (type === 'upgrade-scan') {
74
+ return [
75
+ { label: '/abap-upgrade-fix --from (start remediation — recommended)', command: '/abap-upgrade-fix --from' },
76
+ { label: '/abap-upgrade-scan --status (inspect findings)', command: '/abap-upgrade-scan --status' },
77
+ ];
78
+ }
79
+ if (type === 'upgrade-progress') {
80
+ return [
81
+ { label: '/abap-upgrade-verify --from (run formal verification — recommended)', command: '/abap-upgrade-verify --from' },
82
+ { label: '/abap-upgrade-fix --from (continue remediation)', command: '/abap-upgrade-fix --from' },
83
+ { label: '/abap-upgrade-fix --status (inspect progress)', command: '/abap-upgrade-fix --status' },
84
+ ];
85
+ }
86
+ if (type === 'upgrade-report') {
87
+ // Terminal artefact — verification is sign-off.
88
+ return [
89
+ { label: '/abap-upgrade-verify --status (inspect final report)', command: '/abap-upgrade-verify --status' },
90
+ ];
91
+ }
92
+ if (type === 'cca-assessment') {
93
+ // v0.7 — cca-assessment is NO LONGER terminal in the --from chain.
94
+ // It fans out into three specialist skills (test, upgrade-fix,
95
+ // modernize), each consuming the same assessment for its own
96
+ // workstream. Order surfaces the recommended sequence: lock
97
+ // behaviour with tests first, fix syntax second, modernize last.
98
+ return [
99
+ { label: '/abap-test --from (lock current behaviour first — recommended)', command: '/abap-test --from' },
100
+ { label: '/abap-upgrade-fix --from (mechanical ATC fixes, fast)', command: '/abap-upgrade-fix --from' },
101
+ { label: '/abap-modernize --from (structural Clean Core + REDESIGN work)', command: '/abap-modernize --from' },
102
+ { label: '/abap-cca --status (inspect classifications)', command: '/abap-cca --status' },
103
+ { label: '/abap-cca --resume (continue where left off)', command: '/abap-cca --resume' },
104
+ ];
105
+ }
106
+ if (type === 'modernize-result') {
107
+ return [
108
+ { label: '/abap-modernize --status (inspect what was modernized)', command: '/abap-modernize --status' },
109
+ ];
110
+ }
111
+ if (type === 'test-coverage') {
112
+ return [
113
+ { label: '/abap-test --status (inspect generated test coverage)', command: '/abap-test --status' },
114
+ ];
115
+ }
116
+ // Unknown / cca / incident — fall back to inspection only across all
117
+ // three known status renderers; the renderer auto-detects the type.
118
+ return [
119
+ { label: '/abap-spec-gap --status (inspect)', command: '/abap-spec-gap --status' },
120
+ { label: '/abap-design --status (inspect)', command: '/abap-design --status' },
121
+ { label: '/abap-estimate --status (inspect)', command: '/abap-estimate --status' },
122
+ ];
123
+ }
124
+ /**
125
+ * Action menu for a TEXT input file (.txt / .md). The user's intent
126
+ * is to feed the file contents to a skill as the requirement / spec.
127
+ * The CLI reads the file at runtime and substitutes its content into
128
+ * the prompt (see expandTextFileAttachments in workspace.ts).
129
+ *
130
+ * Order: most-common skills for "I have a requirement, do something
131
+ * with it" — start with spec-gap (the canonical entry point) then the
132
+ * downstream skills in case the user wants to skip a step.
133
+ */
134
+ function textActionsFor() {
135
+ return [
136
+ { label: '/abap-spec-gap (find missing questions in this spec)', command: '/abap-spec-gap' },
137
+ { label: '/abap-design (design directly from this spec)', command: '/abap-design' },
138
+ { label: '/abap-estimate (effort estimate from this spec)', command: '/abap-estimate' },
139
+ { label: '/abap-explain (plain-language explanation)', command: '/abap-explain' },
140
+ ];
141
+ }
142
+ /**
143
+ * Open the picker. Returns one of three tagged outcomes — see
144
+ * `FilePickerResult`. Caller is responsible for dispatching the
145
+ * picked command and for choosing whether to print a file listing
146
+ * (only on 'empty', never on 'cancelled').
147
+ *
148
+ * Both inquirer prompts (file search + action select) run inside ONE
149
+ * `withInquirer` block. Two consecutive `withInquirer` calls would
150
+ * race: the first call's `process.nextTick(() => setSuppress(false))`
151
+ * clears the close-suppress flag BEFORE the second call's pause arms
152
+ * it again, and the spurious 'close' inquirer emits between calls
153
+ * slips through and exits the REPL silently. One guard around the
154
+ * whole flow avoids that gap.
155
+ */
156
+ export async function openFilePicker() {
157
+ const files = await listProjectFiles();
158
+ if (files.length === 0) {
159
+ return { kind: 'empty' };
160
+ }
161
+ try {
162
+ return await withInquirer(async () => {
163
+ // Step 1 — pick a file.
164
+ const fileResult = await search({
165
+ message: 'Pick a project file',
166
+ source: async (input) => {
167
+ const filter = (input ?? '').toLowerCase();
168
+ const filtered = filter.length === 0
169
+ ? files
170
+ : files.filter((f) => f.filename.toLowerCase().includes(filter));
171
+ const cancelChoice = {
172
+ name: '← Cancel',
173
+ value: CANCEL_VALUE,
174
+ short: 'cancel',
175
+ };
176
+ const choices = filtered.map((f) => ({
177
+ name: `${f.filename} [${f.artefactType} v${f.version ?? '?'}]`,
178
+ value: f.filename,
179
+ short: f.filename,
180
+ }));
181
+ return [cancelChoice, ...choices];
182
+ },
183
+ });
184
+ if (fileResult === CANCEL_VALUE)
185
+ return { kind: 'cancelled' };
186
+ const pickedFile = files.find((f) => f.filename === fileResult);
187
+ if (!pickedFile)
188
+ return { kind: 'cancelled' };
189
+ // Step 2 — pick an action for the file. Action menu depends on
190
+ // file kind: envelopes get promote/inspect, text files get the
191
+ // skill-input commands that ingest the file contents. For design
192
+ // envelopes, additionally inspect the object list to pick the
193
+ // right BUILD skill recommendation (RAP vs classic).
194
+ const designShape = pickedFile.artefactType === 'design'
195
+ ? detectDesignShape(pickedFile.path)
196
+ : 'unknown';
197
+ const actionsList = pickedFile.kind === 'text'
198
+ ? textActionsFor()
199
+ : envelopeActionsFor(pickedFile.artefactType, designShape);
200
+ const action = await select({
201
+ message: `What with ${pickedFile.filename}?`,
202
+ choices: [
203
+ ...actionsList.map((a) => ({
204
+ name: a.label,
205
+ value: a.command,
206
+ })),
207
+ { name: '← Cancel', value: CANCEL_VALUE },
208
+ ],
209
+ });
210
+ if (action === CANCEL_VALUE)
211
+ return { kind: 'cancelled' };
212
+ return { kind: 'command', command: `${action} @${pickedFile.filename}` };
213
+ });
214
+ }
215
+ catch {
216
+ // Ctrl+C inside either prompt — treat as cancellation.
217
+ return { kind: 'cancelled' };
218
+ }
219
+ }