@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.
- package/LICENSE +8 -0
- package/README.md +108 -0
- package/dist/agent/anthropic-provider.js +59 -0
- package/dist/agent/llm-provider.js +1 -0
- package/dist/agent/loop.js +709 -0
- package/dist/agent/maybe-build-project-context.js +126 -0
- package/dist/agent/providers/ai-hub-provider.js +58 -0
- package/dist/agent/providers/byok-provider.js +53 -0
- package/dist/agent/providers/factory.js +13 -0
- package/dist/agent/providers/local-provider.js +125 -0
- package/dist/agent/repair-partial.js +31 -0
- package/dist/agent/retry-key.js +58 -0
- package/dist/agent/retry.js +17 -0
- package/dist/agent/sap-connection-adapter.js +82 -0
- package/dist/agent/skill-checkpoint.js +119 -0
- package/dist/agent/tool-dispatch.js +47 -0
- package/dist/agent/turn-assistant-text.js +49 -0
- package/dist/agent/turn-error-ux.js +126 -0
- package/dist/agent/turn-stream.js +79 -0
- package/dist/agent/turn-watchdog.js +71 -0
- package/dist/approvals/advisory-prompt.js +40 -0
- package/dist/approvals/advisory-render.js +38 -0
- package/dist/approvals/approval-prompt.js +100 -0
- package/dist/approvals/jwt.js +33 -0
- package/dist/approvals/render.js +211 -0
- package/dist/approvals/risk-floor.js +26 -0
- package/dist/auth/api-key.js +40 -0
- package/dist/auth/auth-file.js +59 -0
- package/dist/auth/device.js +8 -0
- package/dist/auth/me.js +19 -0
- package/dist/classifier/client.js +58 -0
- package/dist/cli-args.js +38 -0
- package/dist/cli.js +148 -0
- package/dist/commands/config-set.js +245 -0
- package/dist/commands/config-show.js +159 -0
- package/dist/commands/help.js +93 -0
- package/dist/commands/login.js +122 -0
- package/dist/commands/logout.js +17 -0
- package/dist/commands/project-context-impact.js +215 -0
- package/dist/commands/reroute.js +60 -0
- package/dist/commands/spec-gap-status.js +52 -0
- package/dist/commands/whoami.js +35 -0
- package/dist/config/loader.js +67 -0
- package/dist/config/paths.js +20 -0
- package/dist/doctor/checks/_http-probe.js +56 -0
- package/dist/doctor/checks/auth.js +15 -0
- package/dist/doctor/checks/cert.js +24 -0
- package/dist/doctor/checks/forge-rules.js +102 -0
- package/dist/doctor/checks/keychain-fallback.js +14 -0
- package/dist/doctor/checks/keychain.js +23 -0
- package/dist/doctor/checks/llm-mode.js +27 -0
- package/dist/doctor/checks/proxy.js +13 -0
- package/dist/doctor/checks/sap.js +33 -0
- package/dist/doctor/checks/skill.js +24 -0
- package/dist/doctor/checks/write-mode.js +34 -0
- package/dist/doctor/checks/zcspeach.js +76 -0
- package/dist/doctor/run.js +46 -0
- package/dist/errors/codes.js +12 -0
- package/dist/index.js +6 -0
- package/dist/lock-contention.js +22 -0
- package/dist/one-shot.js +104 -0
- package/dist/project-context/conventions.js +309 -0
- package/dist/project-context/detect.js +250 -0
- package/dist/project-context/domain/abap-cloud.js +26 -0
- package/dist/project-context/domain/abapgit.js +177 -0
- package/dist/project-context/domain/cap.js +164 -0
- package/dist/project-context/domain/fiori.js +326 -0
- package/dist/project-context/git.js +115 -0
- package/dist/project-context/index-files.js +235 -0
- package/dist/project-context/index.js +117 -0
- package/dist/project-context/render.js +308 -0
- package/dist/project-context/types.js +14 -0
- package/dist/projects/build.js +20 -0
- package/dist/projects/canonicalize.js +39 -0
- package/dist/projects/email-template.js +54 -0
- package/dist/projects/extract-cca.js +139 -0
- package/dist/projects/extract-design.js +107 -0
- package/dist/projects/extract-estimate.js +93 -0
- package/dist/projects/extract-modernize.js +130 -0
- package/dist/projects/extract-spec-gap.js +101 -0
- package/dist/projects/extract-test-coverage.js +137 -0
- package/dist/projects/extract-upgrade.js +230 -0
- package/dist/projects/filename.js +18 -0
- package/dist/projects/index.js +8 -0
- package/dist/projects/migration.js +111 -0
- package/dist/projects/promote-command.js +96 -0
- package/dist/projects/promote.js +107 -0
- package/dist/projects/save-command.js +124 -0
- package/dist/projects/save.js +21 -0
- package/dist/projects/status.js +170 -0
- package/dist/projects/types.js +1 -0
- package/dist/projects/validate.js +146 -0
- package/dist/projects/workspace.js +478 -0
- package/dist/renderer/abap-inline.js +121 -0
- package/dist/renderer/banners.js +39 -0
- package/dist/renderer/highlighters/abap.js +126 -0
- package/dist/renderer/highlighters/bdef.js +81 -0
- package/dist/renderer/highlighters/cds.js +91 -0
- package/dist/renderer/markdown.js +291 -0
- package/dist/renderer/pipeline.js +201 -0
- package/dist/renderer/progress-chatter.js +237 -0
- package/dist/renderer/question-normalizer.js +306 -0
- package/dist/renderer/severity.js +61 -0
- package/dist/renderer/status-footer.js +50 -0
- package/dist/renderer/syntax.js +58 -0
- package/dist/renderer/tables.js +55 -0
- package/dist/renderer/thinking-heartbeat.js +70 -0
- package/dist/renderer/tool-widget.js +199 -0
- package/dist/renderer/tty.js +66 -0
- package/dist/renderer/widget-extractor.js +87 -0
- package/dist/renderer/widget-fallback.js +78 -0
- package/dist/renderer/widget-schemas.js +43 -0
- package/dist/repl/at-completer.js +64 -0
- package/dist/repl/at-picker.js +122 -0
- package/dist/repl/bracketed-paste.js +284 -0
- package/dist/repl/current-transport.js +46 -0
- package/dist/repl/diff-display.js +41 -0
- package/dist/repl/file-picker.js +219 -0
- package/dist/repl/inquirer-guard.js +130 -0
- package/dist/repl/inquirer-theme.js +41 -0
- package/dist/repl/rule8-detector.js +99 -0
- package/dist/repl/safety-confirm.js +106 -0
- package/dist/repl/safety-mode-state.js +36 -0
- package/dist/repl/slash-completer.js +59 -0
- package/dist/repl/slash-picker.js +124 -0
- package/dist/repl/update-method-preview-hook.js +45 -0
- package/dist/repl.js +1383 -0
- package/dist/router/classifier.js +38 -0
- package/dist/router/intent-extractor.js +140 -0
- package/dist/router/routing-decision.js +19 -0
- package/dist/sap/connection-manager.js +52 -0
- package/dist/sap/onboarding.js +178 -0
- package/dist/sap/system-info.js +515 -0
- package/dist/session/awaiting-answer.js +73 -0
- package/dist/session/gc.js +28 -0
- package/dist/session/pending.js +37 -0
- package/dist/session/resume.js +77 -0
- package/dist/session/schema.js +20 -0
- package/dist/session/store.js +147 -0
- package/dist/session/time-ago.js +41 -0
- package/dist/skill-catalog.js +222 -0
- package/dist/skills/bundled-skills.js +1 -0
- package/dist/skills/canonical.js +12 -0
- package/dist/skills/manifest-client.js +93 -0
- package/dist/skills/promotion-dispatch.js +24 -0
- package/dist/skills/signing-public-key.js +4 -0
- package/dist/skills/source-bundled.js +20 -0
- package/dist/skills/source-managed.js +26 -0
- package/dist/skills/source-manifest.js +26 -0
- package/dist/tools/_command-shared.js +110 -0
- package/dist/tools/_filesystem-shared.js +81 -0
- package/dist/tools/_flag.js +39 -0
- package/dist/tools/approval.js +228 -0
- package/dist/tools/ask-question.js +205 -0
- package/dist/tools/dispatch-skill.js +81 -0
- package/dist/tools/filesystem/file-edit.js +140 -0
- package/dist/tools/filesystem/file-read.js +89 -0
- package/dist/tools/filesystem/file-write.js +128 -0
- package/dist/tools/filesystem/glob.js +177 -0
- package/dist/tools/filesystem/grep.js +163 -0
- package/dist/tools/index.js +32 -0
- package/dist/tools/project/convention_get.js +91 -0
- package/dist/tools/project/playbook_get.js +132 -0
- package/dist/tools/project/project_context_get.js +101 -0
- package/dist/tools/sap-read.js +454 -0
- package/dist/tools/sap-write.js +746 -0
- package/dist/tools/shell/shell_exec.js +209 -0
- package/dist/tools/snapshot.js +107 -0
- package/dist/tools/subagent/_background-shared.js +133 -0
- package/dist/tools/subagent/agent_run.js +186 -0
- package/dist/tools/subagent/background_run.js +143 -0
- package/dist/tools/subagent/monitor_emit.js +65 -0
- package/dist/tools/subagent/schedule_create.js +131 -0
- package/dist/tools/transport.js +233 -0
- package/dist/tools/update-method-intercept.js +119 -0
- package/dist/tools/verify.js +39 -0
- package/dist/tools/web/_web-shared.js +251 -0
- package/dist/tools/web/web_fetch.js +257 -0
- package/dist/tools/web/web_search.js +195 -0
- package/dist/tools/write-mode.js +22 -0
- package/dist/ui/app.js +95 -0
- package/dist/ui/approval-emitter.js +10 -0
- package/dist/ui/approval-modal.js +53 -0
- package/dist/ui/ascii-chars.js +6 -0
- package/dist/ui/body.js +102 -0
- package/dist/ui/coaching-picker-classic.js +36 -0
- package/dist/ui/coaching-picker-emitter.js +27 -0
- package/dist/ui/command-palette.js +34 -0
- package/dist/ui/error-emitter.js +21 -0
- package/dist/ui/footer.js +103 -0
- package/dist/ui/header.js +17 -0
- package/dist/ui/ink-classifier-route.js +19 -0
- package/dist/ui/login-banner.js +72 -0
- package/dist/ui/rich-error-box.js +9 -0
- package/dist/ui/sap-state-store.js +65 -0
- package/dist/ui/session-timeline.js +31 -0
- package/dist/ui/sidebar.js +10 -0
- package/dist/ui/skill-picker.js +50 -0
- package/dist/ui/status-row.js +12 -0
- package/dist/ui/widget-control.js +4 -0
- package/dist/ui/widgets/bar-chart.js +15 -0
- package/dist/ui/widgets/coaching-picker.js +41 -0
- package/dist/ui/widgets/component-registry.js +12 -0
- package/dist/ui/widgets/dep-graph.js +9 -0
- package/dist/ui/widgets/diff-viewer.js +11 -0
- package/dist/ui/widgets/question-card.js +11 -0
- package/dist/ui/widgets/stack-frames.js +5 -0
- package/dist/upgrade-check.js +28 -0
- package/dist/upgrade.js +13 -0
- 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
|
+
}
|