@cspeach/cli 1.1.18 → 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.
- package/README.md +3 -4
- package/dist/agent/loop.js +143 -25
- package/dist/agent/providers/local-provider.js +4 -1
- package/dist/agent/repair-partial.js +76 -19
- package/dist/agent/tool-dispatch.js +58 -21
- package/dist/agent/turn-error-ux.js +77 -26
- package/dist/agent/turn-stream.js +25 -3
- package/dist/approvals/adt-type.js +27 -0
- package/dist/approvals/advisory-prompt.js +48 -0
- package/dist/approvals/advisory-render.js +25 -11
- package/dist/approvals/approval-prompt.js +74 -24
- package/dist/approvals/jwt.js +2 -1
- package/dist/approvals/render.js +64 -36
- package/dist/approvals/risk-floor.js +53 -2
- package/dist/auth/api-key.js +30 -16
- package/dist/auth/keychain.js +0 -0
- package/dist/auth/me.js +25 -7
- package/dist/cli.js +25 -6
- package/dist/commands/compact.js +12 -9
- package/dist/commands/config-set.js +6 -2
- package/dist/commands/config-show.js +7 -5
- package/dist/commands/cost.js +41 -34
- package/dist/commands/help.js +88 -50
- package/dist/commands/login.js +28 -19
- package/dist/commands/logout.js +7 -3
- package/dist/commands/plan-chain.js +71 -77
- package/dist/commands/plan-continue.js +1 -0
- package/dist/commands/plan-resume.js +20 -12
- package/dist/commands/whoami.js +23 -9
- package/dist/config/loader.js +15 -3
- package/dist/cost/pricing.js +6 -5
- package/dist/doctor/checks/forge-rules.js +13 -3
- package/dist/doctor/checks/keychain.js +6 -4
- package/dist/doctor/run.js +101 -20
- package/dist/index.js +5 -2
- package/dist/lib/graceful-exit.js +56 -0
- package/dist/lib/piped-prompt.js +65 -0
- package/dist/lock-contention.js +2 -2
- package/dist/one-shot.js +135 -40
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/plan-run.js +97 -54
- package/dist/projects/promote-command.js +11 -2
- package/dist/projects/save-command.js +3 -2
- package/dist/projects/status.js +2 -1
- package/dist/projects/yes-no.js +12 -0
- package/dist/renderer/abap-inline.js +18 -14
- package/dist/renderer/answer-trim.js +69 -0
- package/dist/renderer/banners.js +8 -8
- package/dist/renderer/brand-settled.js +19 -0
- package/dist/renderer/change-card.js +156 -0
- package/dist/renderer/checklist-format.js +130 -0
- package/dist/renderer/color-mode.js +45 -0
- package/dist/renderer/error-detail.js +104 -0
- package/dist/renderer/fence-state.js +46 -0
- package/dist/renderer/footer-line.js +96 -0
- package/dist/renderer/glyphs.js +25 -0
- package/dist/renderer/goodbye.js +38 -0
- package/dist/renderer/legacy-palette.js +41 -0
- package/dist/renderer/look.js +30 -0
- package/dist/renderer/markdown.js +299 -92
- package/dist/renderer/notice-log.js +72 -0
- package/dist/renderer/notice-shape.js +48 -0
- package/dist/renderer/notices.js +40 -8
- package/dist/renderer/panel-rows.js +16 -0
- package/dist/renderer/pipeline.js +66 -0
- package/dist/renderer/progress-chatter.js +23 -17
- package/dist/renderer/rendering-mode.js +34 -0
- package/dist/renderer/routing-line.js +14 -0
- package/dist/renderer/sanitize.js +12 -0
- package/dist/renderer/severity.js +2 -7
- package/dist/renderer/startup-lines.js +152 -0
- package/dist/renderer/status-footer.js +52 -34
- package/dist/renderer/steering-echo.js +41 -0
- package/dist/renderer/syntax.js +31 -12
- package/dist/renderer/tables.js +5 -1
- package/dist/renderer/theme.js +44 -0
- package/dist/renderer/thinking-heartbeat.js +33 -2
- package/dist/renderer/todo-block.js +10 -49
- package/dist/renderer/tool-labels.js +238 -0
- package/dist/renderer/tool-widget.js +245 -51
- package/dist/renderer/trace.js +42 -0
- package/dist/renderer/transcript-flow.js +132 -0
- package/dist/renderer/tty.js +34 -1
- package/dist/renderer/ui-width.js +42 -0
- package/dist/renderer/verify-chain.js +4 -2
- package/dist/renderer/widget-fallback.js +58 -65
- package/dist/repl/bracketed-paste.js +7 -1
- package/dist/repl/builtin-commands.js +19 -8
- package/dist/repl/credential-handover-gate.js +28 -0
- package/dist/repl/early-line-buffer.js +5 -2
- package/dist/repl/file-picker.js +54 -12
- package/dist/repl/ink-stdin-guard.js +66 -0
- package/dist/repl/inquirer-guard.js +59 -16
- package/dist/repl/inquirer-theme.js +27 -33
- package/dist/repl/paste-marker.js +19 -0
- package/dist/repl/plan-turn-end.js +20 -0
- package/dist/repl/post-turn-status.js +36 -11
- package/dist/repl/reroute-turn.js +21 -0
- package/dist/repl/reset-tty-stdin.js +44 -0
- package/dist/repl/restore-guard.js +22 -0
- package/dist/repl/resume-standalone.js +63 -0
- package/dist/repl/rule8-detector.js +11 -3
- package/dist/repl/safety-confirm.js +161 -93
- package/dist/repl/session-spend-line.js +8 -4
- package/dist/repl/themed-prompts.js +19 -0
- package/dist/repl/ui-look-command.js +75 -0
- package/dist/repl/update-method-preview-hook.js +48 -5
- package/dist/repl.js +795 -332
- package/dist/rewind/cli.js +3 -2
- package/dist/rewind/format.js +14 -9
- package/dist/rewind/restore.js +11 -0
- package/dist/router/classifier.js +47 -4
- package/dist/router/intent-extractor.js +24 -8
- package/dist/router/piped-routing.js +23 -0
- package/dist/router/resume-routing.js +16 -0
- package/dist/router/routing-failure.js +57 -0
- package/dist/sap/first-run-choice.js +1 -1
- package/dist/sap/onboarding.js +3 -2
- package/dist/sap/standalone-onboarding.js +1 -1
- package/dist/sap/system-info.js +4 -2
- package/dist/sap/unreachable.js +28 -0
- package/dist/sap-errors/clean-error-text.js +182 -0
- package/dist/session/interrupt-reason.js +39 -0
- package/dist/session/recap.js +36 -24
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +69 -27
- package/dist/session/user-prompt.js +21 -0
- package/dist/skill-catalog.js +7 -0
- package/dist/skills/bundled-skills.js +76 -69
- package/dist/standards/standards-init.js +1 -1
- package/dist/test-helpers/answer-any.js +46 -0
- package/dist/tools/approval.js +27 -11
- package/dist/tools/ask-question.js +47 -5
- package/dist/tools/filesystem/file-write.js +19 -2
- package/dist/tools/fiori/fe-extend.js +16 -1
- package/dist/tools/fiori/fe-scaffold.js +105 -3
- package/dist/tools/fiori/html-escapes.js +28 -0
- package/dist/tools/fiori/metadata/audit.js +605 -0
- package/dist/tools/fiori/metadata/references.js +104 -0
- package/dist/tools/fiori/metadata/types.js +8 -0
- package/dist/tools/fiori/preview/app-guard.js +215 -0
- package/dist/tools/fiori/preview/env.js +193 -0
- package/dist/tools/fiori/preview/readiness.js +127 -0
- package/dist/tools/fiori/preview/registry.js +412 -0
- package/dist/tools/fiori/preview/start.js +469 -0
- package/dist/tools/fiori/samples/loader.js +20 -5
- package/dist/tools/fiori/scaffold.js +18 -6
- package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
- package/dist/tools/fiori/smoke/app-driver.js +100 -0
- package/dist/tools/fiori/smoke/assertions.js +161 -24
- package/dist/tools/fiori/smoke/audit-columns.js +21 -0
- package/dist/tools/fiori/smoke/batch.js +273 -0
- package/dist/tools/fiori/smoke/draft-safety.js +241 -0
- package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
- package/dist/tools/fiori/smoke/driver.js +171 -4
- package/dist/tools/fiori/smoke/evidence.js +35 -0
- package/dist/tools/fiori/smoke/labels-codes.js +207 -0
- package/dist/tools/fiori/smoke/nav-error.js +26 -0
- package/dist/tools/fiori/smoke/run-smoke.js +155 -12
- package/dist/tools/fiori/tools.js +651 -7
- package/dist/tools/fiori/ui5-version.js +16 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/local-build.js +6 -0
- package/dist/tools/sap-read.js +16 -1
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +30 -12
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +23 -18
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +33 -7
- package/dist/tools/verify.js +184 -8
- package/dist/ui/app.js +284 -146
- package/dist/ui/approval-modal.js +41 -15
- package/dist/ui/ask-answer-text.js +17 -0
- package/dist/ui/body.js +25 -6
- package/dist/ui/card-slot.js +98 -0
- package/dist/ui/checklist.js +9 -0
- package/dist/ui/coaching-picker-classic.js +8 -5
- package/dist/ui/command-palette.js +6 -4
- package/dist/ui/confirm-request.js +18 -0
- package/dist/ui/context-grid.js +2 -1
- package/dist/ui/file-palette.js +6 -4
- package/dist/ui/files-card-emitter.js +19 -0
- package/dist/ui/footer-line.js +16 -0
- package/dist/ui/footer.js +105 -104
- package/dist/ui/input-wrap.js +122 -0
- package/dist/ui/interrupt.js +103 -0
- package/dist/ui/key-burst.js +117 -0
- package/dist/ui/line-resolution.js +4 -3
- package/dist/ui/live-area.js +71 -0
- package/dist/ui/login-banner.js +54 -43
- package/dist/ui/rewind-panel.js +76 -24
- package/dist/ui/role-props.js +47 -0
- package/dist/ui/sap-state-store.js +1 -0
- package/dist/ui/session-timeline.js +49 -3
- package/dist/ui/skill-picker.js +5 -47
- package/dist/ui/text-input.js +128 -24
- package/dist/ui/todo-emitter.js +19 -0
- package/dist/ui/turn-status-emitter.js +28 -2
- package/dist/ui/turn-status.js +16 -19
- package/dist/ui/use-card-request.js +44 -0
- package/dist/ui/widgets/ask-card.js +521 -0
- package/dist/ui/widgets/bar-chart.js +11 -5
- package/dist/ui/widgets/coaching-picker.js +50 -55
- package/dist/ui/widgets/confirm-card.js +193 -0
- package/dist/ui/widgets/dep-graph.js +3 -2
- package/dist/ui/widgets/diff-viewer.js +6 -5
- package/dist/ui/widgets/files-card.js +97 -0
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
- package/dist/ui/widgets/shortcuts-sheet.js +25 -0
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/upgrade-check.js +39 -8
- package/package.json +11 -2
- package/test-harness/fe-draft/app.ts +188 -0
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
- package/test-harness/fe-draft/routes.ts +224 -0
- package/test-harness/fe-draft/routes.unit.test.ts +45 -0
- package/test-harness/fe-draft/server.ts +225 -0
- package/test-harness/fe-draft/variants.ts +196 -0
- package/vitest.smoke-e2e.config.ts +20 -0
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
// picker, the tracker renderer, and the revision builder unit-testable
|
|
7
7
|
// without touching disk.
|
|
8
8
|
import chalk from 'chalk';
|
|
9
|
+
import stringWidth from 'string-width';
|
|
10
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
11
|
+
import { paint } from '../renderer/theme.js';
|
|
12
|
+
import { countLabel } from '../renderer/checklist-format.js';
|
|
13
|
+
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
14
|
+
import { truncateToWidth, uiWidth } from '../renderer/ui-width.js';
|
|
9
15
|
/** Statuses a dead session may have left behind — resumable, picked before fresh `todo`s. */
|
|
10
16
|
const IN_PROGRESS = ['designing', 'building', 'verifying'];
|
|
11
17
|
/**
|
|
@@ -29,8 +35,8 @@ export function isAuditResolved(audit) {
|
|
|
29
35
|
// against the work — and the phase already passed its own exit gate
|
|
30
36
|
// (activation + ATC) — so walling verified work on an infra outage is wrong.
|
|
31
37
|
// It resolves like a warn: dependants unlock, the chain continues, and the
|
|
32
|
-
// "not verified" fact is surfaced (loud chain note + the
|
|
33
|
-
//
|
|
38
|
+
// "not verified" fact is surfaced (loud chain note + the `! audit not run`
|
|
39
|
+
// tag) and recorded (state stays infra_failed; NOT laundered to waived).
|
|
34
40
|
return audit.state === 'passed' || audit.state === 'waived'
|
|
35
41
|
|| audit.state === 'warn' || audit.state === 'infra_failed';
|
|
36
42
|
}
|
|
@@ -85,13 +91,14 @@ export function computeNextPhase(phases, statuses) {
|
|
|
85
91
|
}
|
|
86
92
|
/**
|
|
87
93
|
* Plan-progress board printed before and after every resume turn (and by
|
|
88
|
-
* `--status` for plan envelopes).
|
|
89
|
-
*
|
|
94
|
+
* `--status` for plan envelopes). Flows through chunkEmitter in Ink mode and
|
|
95
|
+
* console.log in classic mode alike.
|
|
90
96
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
97
|
+
* HR Dayforce Extract (v3) · 2 of 7 done
|
|
98
|
+
*
|
|
99
|
+
* ✓ Types — ZDOM_DAYF_STATUS ✓ audited
|
|
100
|
+
* ◼ Tables — Reason checktable + text table active
|
|
101
|
+
* ☐ Service — Service binding published
|
|
95
102
|
*/
|
|
96
103
|
// Human phase labels — a phase is named for the KIND of work it does, so a
|
|
97
104
|
// non-ABAP reader can follow the plan. The exit-gate / produced objects carry
|
|
@@ -140,89 +147,120 @@ function phaseDetail(p, s) {
|
|
|
140
147
|
return shortGate(p.exitGate);
|
|
141
148
|
}
|
|
142
149
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
150
|
+
* Audit tag after the phase status (spec §4 words): `✓ audited`, `! N notes`,
|
|
151
|
+
* `✗ audit failed`, `· hand-edited`, `· waived`, `! audit not run`. Empty
|
|
152
|
+
* string when the phase carries no audit — legacy envelopes render no tag.
|
|
146
153
|
*/
|
|
147
|
-
function
|
|
154
|
+
function auditTag(audit) {
|
|
148
155
|
if (!audit)
|
|
149
156
|
return '';
|
|
157
|
+
const g = getGlyphs();
|
|
150
158
|
switch (audit.state) {
|
|
151
|
-
case 'passed': return ` ${
|
|
159
|
+
case 'passed': return ` ${paint('ok', `${g.done} audited`)}`;
|
|
152
160
|
case 'warn': {
|
|
153
161
|
// Non-blocking tier: validated-WITH-NOTES. Distinct from a clean pass and
|
|
154
162
|
// from the "not run" states — it ran, passed, and carries N notes.
|
|
155
163
|
const n = audit.findings?.length ?? 0;
|
|
156
|
-
return ` ${
|
|
164
|
+
return ` ${paint('warn', `${g.attention} ${n} note${n === 1 ? '' : 's'}`)}`;
|
|
165
|
+
}
|
|
166
|
+
case 'waived': {
|
|
167
|
+
const cat = audit.waiveCategory ? ` (${sanitizeControlChars(audit.waiveCategory)})` : '';
|
|
168
|
+
return ` ${paint('dim', `${g.info} waived${cat}`)}`;
|
|
157
169
|
}
|
|
158
|
-
case '
|
|
159
|
-
case '
|
|
160
|
-
case 'not_audited_hand_edited': return ` ${chalk.yellow('✋ hand-edited')}`;
|
|
170
|
+
case 'failed': return ` ${paint('bad', `${g.failed} audit failed`)}`;
|
|
171
|
+
case 'not_audited_hand_edited': return ` ${paint('dim', `${g.info} hand-edited`)}`;
|
|
161
172
|
case 'pending':
|
|
162
173
|
case 'infra_failed':
|
|
163
174
|
default:
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
return ` ${chalk.yellow('⚠ audit not run')}`;
|
|
175
|
+
// Deliberate forward-compat: an unknown future state reads "audit not run".
|
|
176
|
+
return ` ${paint('warn', `${g.attention} audit not run`)}`;
|
|
167
177
|
}
|
|
168
178
|
}
|
|
169
179
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
* Pre-cut a row's title and detail (plain text, before painting) so both fit
|
|
181
|
+
* in `room` cells. The title has priority; the detail, with its separator,
|
|
182
|
+
* gets what is left and is dropped when too little room remains.
|
|
183
|
+
*/
|
|
184
|
+
function fitRow(title, detail, sep, room) {
|
|
185
|
+
const t = truncateToWidth(title, room);
|
|
186
|
+
const rest = room - stringWidth(t);
|
|
187
|
+
const full = detail ? `${sep}${detail}` : '';
|
|
188
|
+
const tail = full && rest > stringWidth(sep) + 1 ? truncateToWidth(full, rest) : '';
|
|
189
|
+
return { title: t, tail };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Plan-progress board (spec §4) — printed once per phase event to scrollback,
|
|
193
|
+
* never a live element. Same glyphs and "N of M" count as the live checklist:
|
|
194
|
+
* ✓ done (dim) · ◼ current (SAP blue, bold) · ☐ to do · ✗ blocked (bad).
|
|
195
|
+
* Titles and details come from plan files / the model: control characters are
|
|
196
|
+
* neutralised and every row is pre-cut to the UI width before painting.
|
|
197
|
+
* Flows through chunkEmitter (Ink) and console.log (classic) alike.
|
|
175
198
|
*/
|
|
176
199
|
export function renderPlanTracker(a) {
|
|
177
|
-
const
|
|
200
|
+
const g = getGlyphs();
|
|
201
|
+
const width = uiWidth();
|
|
178
202
|
const lines = [];
|
|
179
203
|
const total = a.content.phases.length;
|
|
180
204
|
// Waived phases count as done in the header tally — the DAG treats them as
|
|
181
|
-
// satisfied
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
205
|
+
// satisfied. Audit-aware: a validated-but-audit-failed phase must NOT count,
|
|
206
|
+
// or the header would say "N of N done" while the completion check refuses
|
|
207
|
+
// to complete — the tally uses the same predicate as the DAG (pinned by
|
|
208
|
+
// plan-tracker-pin).
|
|
185
209
|
const validated = a.content.phases.filter((p) => isPhaseSatisfied(a.statuses[p.id], p.audit)).length;
|
|
186
210
|
if (a.includeHeader !== false) {
|
|
187
|
-
|
|
211
|
+
const tail = ` (v${a.version}) · ${countLabel(validated, total)} done`;
|
|
212
|
+
const head = truncateToWidth(sanitizeControlChars(a.title), Math.max(1, width - stringWidth(tail)));
|
|
213
|
+
lines.push(chalk.bold(head) + paint('dim', tail));
|
|
188
214
|
lines.push('');
|
|
189
215
|
}
|
|
190
216
|
for (const p of a.content.phases) {
|
|
191
217
|
const s = a.statuses[p.id] ?? 'todo';
|
|
192
218
|
const isCurrent = a.currentId != null && p.id === a.currentId;
|
|
193
|
-
const title = phaseTitle(p);
|
|
194
|
-
const detail = phaseDetail(p, s);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const audit = auditGlyph(p.audit);
|
|
219
|
+
const title = sanitizeControlChars(phaseTitle(p));
|
|
220
|
+
const detail = sanitizeControlChars(phaseDetail(p, s));
|
|
221
|
+
// Auditor verdict after the status — '' on legacy phases (no audit field).
|
|
222
|
+
const audit = auditTag(p.audit);
|
|
223
|
+
const auditW = stringWidth(audit);
|
|
199
224
|
if (s === 'validated') {
|
|
200
|
-
|
|
225
|
+
const lead = ` ${g.done} `;
|
|
226
|
+
const r = fitRow(title, detail, ' — ', width - stringWidth(lead) - auditW);
|
|
227
|
+
lines.push(paint('dim', `${lead}${r.title}${r.tail}`) + audit);
|
|
201
228
|
}
|
|
202
229
|
else if (s === 'validated-with-waiver') {
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
// Done-but-waived: the exit gate was never met; the user accepted it.
|
|
231
|
+
const lead = ` ${g.done} `;
|
|
232
|
+
const mark = `${g.done}* (waived)`;
|
|
233
|
+
const r = fitRow(title, detail, ' — ', width - stringWidth(lead) - 1 - stringWidth(mark) - auditW);
|
|
234
|
+
lines.push(paint('dim', `${lead}${r.title}${r.tail}`) + ` ${paint('warn', mark)}` + audit);
|
|
207
235
|
}
|
|
208
236
|
else if (s === 'blocked') {
|
|
209
|
-
|
|
237
|
+
const lead = ` ${g.failed} `;
|
|
238
|
+
const mark = ' — blocked';
|
|
239
|
+
const r = fitRow(title, detail, ' · ', width - stringWidth(lead) - stringWidth(mark) - auditW);
|
|
240
|
+
lines.push(` ${paint('bad', `${g.failed} ${r.title}${mark}`)}${r.tail ? paint('dim', r.tail) : ''}${audit}`);
|
|
210
241
|
}
|
|
211
242
|
else if (isCurrent) {
|
|
212
|
-
|
|
243
|
+
const lead = ` ${g.current} `;
|
|
244
|
+
const r = fitRow(title, detail, ' — ', width - stringWidth(lead) - auditW);
|
|
245
|
+
lines.push(` ${chalk.bold(paint('sap', `${g.current} ${r.title}`))}${paint('dim', r.tail)}${audit}`);
|
|
213
246
|
}
|
|
214
247
|
else if (IN_PROGRESS.includes(s)) {
|
|
215
|
-
|
|
248
|
+
const lead = ` ${g.pending} `;
|
|
249
|
+
const note = ` (${s})`;
|
|
250
|
+
const r = fitRow(title, detail, ' — ', width - stringWidth(lead) - stringWidth(note) - auditW);
|
|
251
|
+
lines.push(` ${g.pending} ${chalk.bold(r.title)}${paint('dim', r.tail)}${audit}${paint('dim', note)}`);
|
|
216
252
|
}
|
|
217
253
|
else {
|
|
218
|
-
|
|
254
|
+
const lead = ` ${g.pending} `;
|
|
255
|
+
const r = fitRow(title, detail, ' — ', width - stringWidth(lead) - auditW);
|
|
256
|
+
lines.push(` ${paint('dim', g.pending)} ${r.title}${paint('dim', r.tail)}${audit}`);
|
|
219
257
|
}
|
|
220
258
|
}
|
|
221
259
|
// Off-switch footer — honesty: state plainly that this run's phases are not
|
|
222
260
|
// being audited. Rendered only when the caller threads auditsOff (in-memory).
|
|
223
261
|
if (a.auditsOff) {
|
|
224
262
|
lines.push('');
|
|
225
|
-
lines.push(
|
|
263
|
+
lines.push(paint('dim', ' audits: off'));
|
|
226
264
|
}
|
|
227
265
|
return lines.join('\n');
|
|
228
266
|
}
|
|
@@ -272,7 +310,10 @@ export function findServiceBinding(phases) {
|
|
|
272
310
|
* /abap-fiori-build — with the EXACT binding name when resolvable. A plan
|
|
273
311
|
* that already built its UI, or never built a service, gets no chain line.
|
|
274
312
|
*/
|
|
275
|
-
export function planCompletionLines(phases
|
|
313
|
+
export function planCompletionLines(phases,
|
|
314
|
+
// Terminal callers follow the terminal's glyph set; file writers (the
|
|
315
|
+
// handover markdown) pass UNICODE_GLYPHS so file content never varies.
|
|
316
|
+
g = getGlyphs()) {
|
|
276
317
|
const lines = ['Plan complete — every phase validated.'];
|
|
277
318
|
// Confidence-tiered redesign (2026-07-08): a completed plan that carried
|
|
278
319
|
// non-blocking warn verdicts surfaces them in the FINAL summary — a warned
|
|
@@ -283,9 +324,10 @@ export function planCompletionLines(phases) {
|
|
|
283
324
|
if (p.audit?.state !== 'warn')
|
|
284
325
|
continue;
|
|
285
326
|
const notes = p.audit.findings ?? [];
|
|
327
|
+
// Findings are model-written text: sanitise before they reach a terminal.
|
|
286
328
|
lines.push(notes.length > 0
|
|
287
|
-
?
|
|
288
|
-
:
|
|
329
|
+
? `${g.attention} ${p.id} passed with ${notes.length} note${notes.length === 1 ? '' : 's'}: ${sanitizeControlChars(notes.join('; '))}.`
|
|
330
|
+
: `${g.attention} ${p.id} passed with notes.`);
|
|
289
331
|
}
|
|
290
332
|
// §10 Q2 (audit-confidence redesign): a waived phase surfaces its light
|
|
291
333
|
// category + reason in the FINAL summary — an "all validated" plan never
|
|
@@ -295,9 +337,10 @@ export function planCompletionLines(phases) {
|
|
|
295
337
|
for (const p of phases) {
|
|
296
338
|
if (p.audit?.state !== 'waived')
|
|
297
339
|
continue;
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
340
|
+
// Same "· waived" tag as the board (auditTag); the reason is user-typed text.
|
|
341
|
+
const cat = p.audit.waiveCategory ? ` (${sanitizeControlChars(p.audit.waiveCategory)})` : '';
|
|
342
|
+
const reason = p.audit.waivedReason ? `: ${sanitizeControlChars(p.audit.waivedReason)}` : '';
|
|
343
|
+
lines.push(`${g.info} ${p.id} waived${cat}${reason}.`);
|
|
301
344
|
}
|
|
302
345
|
// audit-infra-continue (2026-07-09): a phase whose audit could NOT RUN
|
|
303
346
|
// surfaces in the FINAL summary as a DISTINCT "unverified" line — an "all
|
|
@@ -308,7 +351,7 @@ export function planCompletionLines(phases) {
|
|
|
308
351
|
for (const p of phases) {
|
|
309
352
|
if (p.audit?.state !== 'infra_failed')
|
|
310
353
|
continue;
|
|
311
|
-
lines.push(
|
|
354
|
+
lines.push(`${g.attention} ${p.id} was NOT audit-verified (audit infrastructure failed — its own exit gate, activation + ATC, passed).`);
|
|
312
355
|
}
|
|
313
356
|
const hasUi = phases.some((p) => p.layer === 'ui' || p.delegateTo === 'abap-fiori-build');
|
|
314
357
|
const hasService = phases.some((p) => p.layer === 'service');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readProjectFile } from './status.js';
|
|
2
2
|
import { buildPromotedFromSnapshot, buildReviewDecisionsBlock, buildDetailPathHint, validatePromotionSource } from './promote.js';
|
|
3
3
|
import { resolveAtTokenAsync, formatProjectFileList } from './workspace.js';
|
|
4
|
+
import { askYesNo } from './yes-no.js';
|
|
4
5
|
/**
|
|
5
6
|
* Per-skill predecessor map. v0.7 admits multi-source via array values —
|
|
6
7
|
* e.g. /abap-upgrade-fix accepts BOTH upgrade-scan (the existing chain
|
|
@@ -79,14 +80,22 @@ export async function runPromoteCommand(args) {
|
|
|
79
80
|
}
|
|
80
81
|
for (const w of validation.warnings) {
|
|
81
82
|
if (w.kind === 'ZERO_ANSWERED') {
|
|
82
|
-
const reply = await
|
|
83
|
+
const reply = await askYesNo({
|
|
84
|
+
question: `Source has no answered items (${w.count} still open). Continue anyway?`,
|
|
85
|
+
confirm: args.confirm,
|
|
86
|
+
prompt: args.prompt,
|
|
87
|
+
});
|
|
83
88
|
if (!affirmative(reply))
|
|
84
89
|
return null;
|
|
85
90
|
}
|
|
86
91
|
else if (w.kind === 'PARTIAL_OPEN') {
|
|
87
92
|
const sample = w.openIds.slice(0, 5).join(', ');
|
|
88
93
|
const more = w.openIds.length > 5 ? `, ... ${w.openIds.length - 5} more` : '';
|
|
89
|
-
const reply = await
|
|
94
|
+
const reply = await askYesNo({
|
|
95
|
+
question: `${w.openIds.length} item(s) still open (${sample}${more}). Promote anyway?`,
|
|
96
|
+
confirm: args.confirm,
|
|
97
|
+
prompt: args.prompt,
|
|
98
|
+
});
|
|
90
99
|
if (!affirmative(reply))
|
|
91
100
|
return null;
|
|
92
101
|
}
|
|
@@ -17,6 +17,7 @@ import { saveProject } from './save.js';
|
|
|
17
17
|
import { renderEmailTemplate } from './email-template.js';
|
|
18
18
|
import { writePlanHandover } from './handover-md.js';
|
|
19
19
|
import { ensureWorkspace, resolveWorkspacePath } from './workspace.js';
|
|
20
|
+
import { askYesNo, isYes } from './yes-no.js';
|
|
20
21
|
const SKILL_REGISTRY = {
|
|
21
22
|
'abap-spec-gap': {
|
|
22
23
|
artefactType: 'spec-gap',
|
|
@@ -317,8 +318,8 @@ export async function runSaveCommand(args) {
|
|
|
317
318
|
// question added here would silently receive 'y' too. If you need another
|
|
318
319
|
// prompt, give maybeOfferSave's headless prompt a question-aware answer
|
|
319
320
|
// first.
|
|
320
|
-
const answer =
|
|
321
|
-
if (answer
|
|
321
|
+
const answer = await askYesNo({ question: 'Save as project file?', confirm: args.confirm, prompt: args.prompt });
|
|
322
|
+
if (!isYes(answer))
|
|
322
323
|
return null;
|
|
323
324
|
let env;
|
|
324
325
|
if (planExtract && prior) {
|
package/dist/projects/status.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { validateEnvelope } from './validate.js';
|
|
3
3
|
import { canonicalSha256 } from './canonicalize.js';
|
|
4
4
|
import { statusesFromItems, computeNextPhase, renderPlanTracker } from './plan-run.js';
|
|
5
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
5
6
|
export function readProjectFile(path) {
|
|
6
7
|
const raw = readFileSync(path, 'utf8');
|
|
7
8
|
let parsed;
|
|
@@ -167,7 +168,7 @@ export async function renderMultiFileStatus(envs) {
|
|
|
167
168
|
const distinct = new Set(shas);
|
|
168
169
|
if (distinct.size > 1) {
|
|
169
170
|
const lines = [];
|
|
170
|
-
lines.push(
|
|
171
|
+
lines.push(`${getGlyphs().attention} Two v${firstVer} files received from same source, with conflicting changes.`);
|
|
171
172
|
for (let i = 0; i < envs.length; i++) {
|
|
172
173
|
const e = envs[i];
|
|
173
174
|
const items = e.interaction.items;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export async function askYesNo(a) {
|
|
2
|
+
if (a.confirm)
|
|
3
|
+
return a.confirm(a.question, { default: 'n' });
|
|
4
|
+
if (!a.prompt)
|
|
5
|
+
return '';
|
|
6
|
+
return (await a.prompt(`${a.question} [y/N]: `)).trim().toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
/** Today's decision rule at all six sites. */
|
|
9
|
+
export function isYes(reply) {
|
|
10
|
+
const v = reply.trim().toLowerCase();
|
|
11
|
+
return v === 'y' || v === 'yes';
|
|
12
|
+
}
|
|
@@ -36,6 +36,22 @@ const ABAP_KEYWORDS = new Set([
|
|
|
36
36
|
// Now accepts any alphanumeric/_/- lang slug and makes it available as m[1].
|
|
37
37
|
const OPEN_FENCE_RE = /^```([a-z0-9_-]*)\s*$/i;
|
|
38
38
|
const CLOSE_FENCE_RE = /^```\s*$/;
|
|
39
|
+
/** Highlight one fenced block body for the languages CSPeach owns
|
|
40
|
+
* (abap, cds, bdef, ddl). undefined = not ours (caller keeps its default). */
|
|
41
|
+
export function highlightFencedBody(body, lang) {
|
|
42
|
+
// Only the first word of the info string names the language
|
|
43
|
+
// (```abap linenums → abap).
|
|
44
|
+
const l = (lang.trim().split(/\s+/)[0] ?? '').toLowerCase();
|
|
45
|
+
// Preserve v0.1.1 regex-based highlighter for ABAP — it is stable,
|
|
46
|
+
// has been tested in production, and avoids a second runtime
|
|
47
|
+
// dependency for the hottest fenced-block type.
|
|
48
|
+
if (l === 'abap')
|
|
49
|
+
return highlightAbapBlock(body);
|
|
50
|
+
// cli-highlight grammars via the Task 5 dispatcher.
|
|
51
|
+
if (l === 'cds' || l === 'bdef' || l === 'ddl')
|
|
52
|
+
return highlightCodeBlock(body, l);
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
39
55
|
/** Whole-text pass. Splits by fenced blocks; routes each block by lang. */
|
|
40
56
|
export function applyCodeHighlight(text) {
|
|
41
57
|
const lines = text.split('\n');
|
|
@@ -59,20 +75,8 @@ export function applyCodeHighlight(text) {
|
|
|
59
75
|
// inFence === true
|
|
60
76
|
if (CLOSE_FENCE_RE.test(line)) {
|
|
61
77
|
const body = blockLines.join('\n');
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// has been tested in production, and avoids a second runtime
|
|
65
|
-
// dependency for the hottest fenced-block type.
|
|
66
|
-
out.push(highlightAbapBlock(body));
|
|
67
|
-
}
|
|
68
|
-
else if (lang === 'cds' || lang === 'bdef' || lang === 'ddl') {
|
|
69
|
-
// cli-highlight grammars via the Task 5 dispatcher.
|
|
70
|
-
out.push(highlightCodeBlock(body, lang));
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
// Unknown / empty lang → pass through unchanged.
|
|
74
|
-
out.push(body);
|
|
75
|
-
}
|
|
78
|
+
// Unknown / empty lang → pass through unchanged.
|
|
79
|
+
out.push(highlightFencedBody(body, lang) ?? body);
|
|
76
80
|
out.push(line);
|
|
77
81
|
inFence = false;
|
|
78
82
|
lang = '';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/renderer/answer-trim.ts
|
|
2
|
+
//
|
|
3
|
+
// Round 6 item 4f — one-shot stdout is the answer only: no blank lines before
|
|
4
|
+
// it, exactly one newline after it. Newlines (and colour codes next to them)
|
|
5
|
+
// are held until visible text proves they are inside the answer; leading ones
|
|
6
|
+
// are dropped, trailing ones are dropped at end().
|
|
7
|
+
// eslint-disable-next-line no-control-regex
|
|
8
|
+
const ESC_SEQ = /^\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07]*\x07)/;
|
|
9
|
+
export class AnswerTrim {
|
|
10
|
+
started = false;
|
|
11
|
+
/** Held newlines + escapes, in order. */
|
|
12
|
+
held = '';
|
|
13
|
+
/**
|
|
14
|
+
* Round 6 review M3 — a newline the caller already wrote on its own
|
|
15
|
+
* (writeAboveSpinner ends a line before repainting the spinner). The next
|
|
16
|
+
* held newline, or the final one, is covered by it.
|
|
17
|
+
*/
|
|
18
|
+
forced = false;
|
|
19
|
+
/** The caller wrote a newline itself after the last output. */
|
|
20
|
+
noteForcedNewline() {
|
|
21
|
+
if (this.started)
|
|
22
|
+
this.forced = true;
|
|
23
|
+
}
|
|
24
|
+
flushHeld() {
|
|
25
|
+
let h = this.held;
|
|
26
|
+
if (this.forced) {
|
|
27
|
+
h = h.replace(/\r?\n/, '');
|
|
28
|
+
this.forced = false;
|
|
29
|
+
}
|
|
30
|
+
return h;
|
|
31
|
+
}
|
|
32
|
+
feed(s) {
|
|
33
|
+
let out = '';
|
|
34
|
+
let i = 0;
|
|
35
|
+
while (i < s.length) {
|
|
36
|
+
const ch = s[i];
|
|
37
|
+
if (ch === '\x1b') {
|
|
38
|
+
const m = ESC_SEQ.exec(s.slice(i));
|
|
39
|
+
if (m) {
|
|
40
|
+
this.held += m[0];
|
|
41
|
+
i += m[0].length;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (ch === '\n' || ch === '\r') {
|
|
46
|
+
this.held += ch;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Before the answer starts, held newlines are dropped (escapes kept).
|
|
50
|
+
out += this.started ? this.flushHeld() : this.held.replace(/[\r\n]/g, '');
|
|
51
|
+
this.held = '';
|
|
52
|
+
this.started = true;
|
|
53
|
+
out += ch;
|
|
54
|
+
}
|
|
55
|
+
i++;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** What to write when the answer is done: held escapes + one newline (nothing if no answer). */
|
|
60
|
+
end() {
|
|
61
|
+
const escapes = this.held.replace(/[\r\n]/g, '');
|
|
62
|
+
this.held = '';
|
|
63
|
+
if (this.forced) {
|
|
64
|
+
this.forced = false;
|
|
65
|
+
return escapes;
|
|
66
|
+
}
|
|
67
|
+
return this.started ? `${escapes}\n` : escapes;
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/renderer/banners.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { CLI_VERSION } from '../lib/version.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* The startup banner uses the 🍑 emoji directly (no tint) for identity. */
|
|
7
|
-
export const PEACH = '#FFCBA4';
|
|
3
|
+
// Legacy brand colour — frozen in legacy-palette.ts (piece 2 §1.1).
|
|
4
|
+
import { LEGACY_PEACH as PEACH } from './legacy-palette.js';
|
|
5
|
+
import { formatAttention } from './notice-shape.js';
|
|
8
6
|
/**
|
|
9
7
|
* Minimal startup line — terse, single bullet, three lines max. Modeled on
|
|
10
8
|
* Claude Code's `✻ Welcome to Claude Code v1.0.x` pattern but with the
|
|
@@ -33,8 +31,10 @@ export function printStartupBanner() {
|
|
|
33
31
|
console.log(` ${chalk.gray('ABAP + AI + safety gates')} ${chalk.dim('·')} ${chalk.gray('by Cremencing Solutions')}`);
|
|
34
32
|
console.log('');
|
|
35
33
|
}
|
|
34
|
+
/** Piece 2 §10: the keychain fallback is ONE "!" line (was three). */
|
|
35
|
+
export function keychainFallbackNotice() {
|
|
36
|
+
return formatAttention({ text: 'OS keychain unavailable', fact: 'the API key and SAP passwords are asked each session' });
|
|
37
|
+
}
|
|
36
38
|
export function printKeytarFallbackBanner() {
|
|
37
|
-
|
|
38
|
-
console.log(chalk.yellow(' API key + SAP passwords will be prompted per session.'));
|
|
39
|
-
console.log(chalk.yellow(' Run `cspeach doctor` for remediation hints.\n'));
|
|
39
|
+
process.stdout.write(keychainFallbackNotice());
|
|
40
40
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/renderer/brand-settled.ts
|
|
2
|
+
//
|
|
3
|
+
// The early panels brand line (startup-lines.ts startEarlyBrand) is written
|
|
4
|
+
// without its newline while the sign-in check runs. Anything that prints on
|
|
5
|
+
// its own schedule during startup (the fire-and-forget upgrade check) awaits
|
|
6
|
+
// brandSettled() first, so it can never land in the middle of that line.
|
|
7
|
+
// Resolved by default: with no early brand line open there is nothing to wait for.
|
|
8
|
+
// Kept dependency-free: cli.ts reaches it statically through upgrade-check.ts.
|
|
9
|
+
let settled = Promise.resolve();
|
|
10
|
+
let release;
|
|
11
|
+
export function brandSettled() {
|
|
12
|
+
return settled;
|
|
13
|
+
}
|
|
14
|
+
/** Marks the brand line open; returns the function that settles it (idempotent). */
|
|
15
|
+
export function holdBrand() {
|
|
16
|
+
settled = new Promise((r) => { release = r; });
|
|
17
|
+
const mine = release;
|
|
18
|
+
return () => { mine(); };
|
|
19
|
+
}
|