@cspeach/cli 1.1.17 → 1.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +80 -22
- 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/skills/signing-public-key.js +1 -1
- 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
package/dist/approvals/render.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { select, input } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { riskReasonText, shownRiskReasons } from './risk-floor.js';
|
|
3
4
|
import { verbLine, viewAffordance, toSourceLines } from './op-labels.js';
|
|
4
5
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
5
6
|
import { approvalEmitter } from '../ui/approval-emitter.js';
|
|
6
|
-
import { askQuestionEmitter } from '../ui/ask-question-emitter.js';
|
|
7
7
|
import { withInquirer } from '../repl/inquirer-guard.js';
|
|
8
8
|
import { inquirerTheme } from '../repl/inquirer-theme.js';
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import { askConfirm } from '../ui/confirm-request.js';
|
|
10
|
+
import { emitTrace } from '../renderer/trace.js';
|
|
11
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
12
|
+
import { paint } from '../renderer/theme.js';
|
|
13
|
+
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
14
|
+
import { withSpinnerSuspended } from '../renderer/tool-widget.js';
|
|
15
|
+
// Piece 2 §1.1 — the brand accent comes from the theme role, not a legacy hex.
|
|
16
|
+
const peach = (s) => paint('brand', s);
|
|
11
17
|
/**
|
|
12
18
|
* Risk tag. 2026-05-01: yellow used to fire on `medium` too, which
|
|
13
19
|
* meant most plans had a yellow tag — defeating yellow's purpose as
|
|
@@ -54,7 +60,7 @@ function colorizeDiff(diff) {
|
|
|
54
60
|
* per-change prompts that were the main "so much confusion" complaint.
|
|
55
61
|
*
|
|
56
62
|
* Color discipline:
|
|
57
|
-
* -
|
|
63
|
+
* - The brand role for the brand-bearing "Plan" header.
|
|
58
64
|
* - Default text for the summary, change list. No cyan, no bold-on-bold.
|
|
59
65
|
* - Yellow tag for medium risk, red for high. Low is dim — no shouting.
|
|
60
66
|
*/
|
|
@@ -80,57 +86,79 @@ function collapseImplicitActivates(changes) {
|
|
|
80
86
|
return !writeKeys.has(`${c.type}:${c.object}`);
|
|
81
87
|
});
|
|
82
88
|
}
|
|
83
|
-
export async function renderPlanGate(summary, changes, transport, effectiveRisk) {
|
|
89
|
+
export async function renderPlanGate(summary, changes, transport, effectiveRisk, system) {
|
|
84
90
|
const visibleChanges = collapseImplicitActivates(changes);
|
|
85
91
|
const visibleCount = visibleChanges.length;
|
|
86
92
|
const noun = visibleCount === 1 ? 'change' : 'changes';
|
|
87
|
-
// Ink path (
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
93
|
+
// Ink path (piece 2 §5): the plan gate is an APPROVAL confirm card in the
|
|
94
|
+
// card slot. Same three values, same order, cursor on Apply all; anything
|
|
95
|
+
// else (Esc, no listener) is 'cancel'. Digits pick on an APPROVAL card
|
|
96
|
+
// (fix round 3), but typed-ahead keys are dropped (ConfirmCard), so a `1`
|
|
97
|
+
// or Enter typed before the card opened can't apply a plan. Long plans
|
|
98
|
+
// print in full to scrollback; the card keeps 4 (controller ruling, C1 fix
|
|
99
|
+
// round 1: 8 steps broke the spec §2 height cap — rows − 2 — on a 24-row
|
|
100
|
+
// terminal).
|
|
94
101
|
if (shouldUseInk()) {
|
|
95
|
-
const
|
|
102
|
+
const MAX_STEPS = 4;
|
|
103
|
+
const packages = [...new Set(visibleChanges.map((c) => c.package).filter((p) => !!p))];
|
|
104
|
+
const facts = [
|
|
96
105
|
summary,
|
|
97
|
-
[
|
|
98
|
-
|
|
106
|
+
[
|
|
107
|
+
`${visibleCount} ${noun}`,
|
|
108
|
+
transport ? `transport ${transport}` : 'no transport',
|
|
109
|
+
...(packages.length > 0 ? [`package ${packages.join(', ')}`] : []),
|
|
110
|
+
`${effectiveRisk.level} risk`,
|
|
111
|
+
].join(' · '),
|
|
99
112
|
];
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
const shownReasons = shownRiskReasons(effectiveRisk.raisedBy);
|
|
114
|
+
if (shownReasons.length > 0) {
|
|
115
|
+
facts.push(`Risk raised to ${effectiveRisk.level}: ${shownReasons.map(riskReasonText).join('; ')}`);
|
|
102
116
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
const steps = visibleChanges.map((c, i) => `${i + 1}. ${verbLine(c)}`);
|
|
118
|
+
if (steps.length > MAX_STEPS) {
|
|
119
|
+
// Object names are model-supplied: neutralise control characters
|
|
120
|
+
// before they reach scrollback (the card sanitises its own rows).
|
|
121
|
+
emitTrace(sanitizeControlChars(['Plan', ...steps.map((s) => ` ${s}`)].join('\n')));
|
|
122
|
+
facts.push(...steps.slice(0, MAX_STEPS), `${getGlyphs().more} ${steps.length - MAX_STEPS} more steps, listed above`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
facts.push(...steps);
|
|
126
|
+
}
|
|
127
|
+
const res = await askConfirm({
|
|
108
128
|
id: 'plan-gate',
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
context: ctx.join('\n').replace(/\x1b\[[0-9;]*m/g, ''),
|
|
113
|
-
kind: 'choice',
|
|
129
|
+
tag: 'APPROVAL',
|
|
130
|
+
title: system ? `Apply this plan on ${system}?` : 'Apply this plan?',
|
|
131
|
+
facts,
|
|
114
132
|
choices: [
|
|
115
133
|
{ value: 'apply_all', label: `Apply all ${visibleCount} ${noun}` },
|
|
116
|
-
{ value: 'review', label: 'Review each change
|
|
134
|
+
{ value: 'review', label: 'Review each change one by one' },
|
|
117
135
|
{ value: 'cancel', label: 'Cancel' },
|
|
118
136
|
],
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
137
|
+
initial: 0,
|
|
138
|
+
footnote: 'Forge Rule 8',
|
|
139
|
+
}, { traceLabel: 'Plan' });
|
|
140
|
+
if (res.value === 'apply_all' || res.value === 'review' || res.value === 'cancel') {
|
|
141
|
+
return res.value;
|
|
122
142
|
}
|
|
123
|
-
return 'cancel'; // Esc / no
|
|
143
|
+
return 'cancel'; // Esc / no listener / unknown → cancel the plan (safe default)
|
|
124
144
|
}
|
|
125
145
|
// Non-Ink (one-shot / non-TTY / classic): the original inquirer flow.
|
|
146
|
+
// Review M1: request_approval runs under a live tool spinner — keep it
|
|
147
|
+
// suspended for the whole preamble + prompt so no stale frame is left.
|
|
148
|
+
return withSpinnerSuspended(() => classicPlanGate(summary, visibleChanges, transport, effectiveRisk));
|
|
149
|
+
}
|
|
150
|
+
async function classicPlanGate(summary, visibleChanges, transport, effectiveRisk) {
|
|
151
|
+
const visibleCount = visibleChanges.length;
|
|
152
|
+
const noun = visibleCount === 1 ? 'change' : 'changes';
|
|
126
153
|
console.log('');
|
|
127
154
|
console.log(`${peach('Plan')} ${summary}`);
|
|
128
155
|
const meta = [`${visibleCount} ${noun}`, riskTag(effectiveRisk.level)];
|
|
129
156
|
if (transport)
|
|
130
157
|
meta.push(`transport ${transport}`);
|
|
131
158
|
console.log(` ${chalk.dim(meta.join(' · '))}`);
|
|
132
|
-
|
|
133
|
-
|
|
159
|
+
const shownReasons = shownRiskReasons(effectiveRisk.raisedBy);
|
|
160
|
+
if (shownReasons.length > 0) {
|
|
161
|
+
console.log(` ${chalk.dim(`(risk raised: ${shownReasons.map(riskReasonText).join('; ')})`)}`);
|
|
134
162
|
}
|
|
135
163
|
visibleChanges.forEach((c, i) => {
|
|
136
164
|
console.log(` ${chalk.dim(`${i + 1}.`)} ${verbLine(c)}`);
|
|
@@ -183,7 +211,7 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
183
211
|
//
|
|
184
212
|
// I3 (2026-07-05 review): a 'v' affordance (create/delete View source) is
|
|
185
213
|
// NOT a diff — the payload is plain/all-adds source. Routing it through the
|
|
186
|
-
// diff printer fired the "
|
|
214
|
+
// diff printer fired the "Model did not provide a unified diff" warning
|
|
187
215
|
// under a View source label. Print numbered plain source instead — never
|
|
188
216
|
// the diff path or its warning. (The affordance is only offered when
|
|
189
217
|
// change.diff is present, so the fallback line is belt-and-braces.)
|
|
@@ -214,7 +242,7 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
214
242
|
console.log('\n' + colorizeDiff(change.diff) + '\n');
|
|
215
243
|
}
|
|
216
244
|
else {
|
|
217
|
-
console.log('\n' + chalk.yellow(
|
|
245
|
+
console.log('\n' + chalk.yellow(`${getGlyphs().attention} Model did not provide a unified diff — only a description:`));
|
|
218
246
|
console.log(chalk.dim(change.diff) + '\n');
|
|
219
247
|
}
|
|
220
248
|
}
|
|
@@ -10,10 +10,33 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
10
10
|
};
|
|
11
11
|
if (ctx.autoApprovePolicy === 'never')
|
|
12
12
|
raise('high', 'non_prod_override');
|
|
13
|
+
// Round 7 item 8e — never auto-approve anything on an object whose last
|
|
14
|
+
// syntax check found errors (high is never auto-approved).
|
|
15
|
+
if (ctx.syntaxErrorObjects && ctx.syntaxErrorObjects.length > 0) {
|
|
16
|
+
// Keys are `TYPE:NAME` (tools/syntax-state.ts syntaxKey). Re-review 2
|
|
17
|
+
// item 1: the floor matches on the NAME alone — a type word can be
|
|
18
|
+
// ambiguous (VIEW, FUNC, TABLE vs STRUCTURE…) or unknown, so any recorded
|
|
19
|
+
// entry with that name raises. Over-raise, never under-raise: the cost is
|
|
20
|
+
// a card the user would otherwise not see.
|
|
21
|
+
const badNames = new Set(ctx.syntaxErrorObjects.map((k) => k.slice(k.lastIndexOf(':') + 1).toUpperCase()));
|
|
22
|
+
const hit = (c) => badNames.has(c.object.trim().split(/\s+/)[0].toUpperCase());
|
|
23
|
+
if (changes.some(hit)) {
|
|
24
|
+
raise('high', 'syntax_errors');
|
|
25
|
+
// Final review A M-4 — the reason is always shown, even when the floor
|
|
26
|
+
// was already high (policy never, or declared high).
|
|
27
|
+
if (!raisedBy.includes('syntax_errors'))
|
|
28
|
+
raisedBy.push('syntax_errors');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
13
31
|
if (changes.some((c) => c.op === 'delete'))
|
|
14
32
|
raise('medium', 'delete_op');
|
|
15
|
-
|
|
16
|
-
|
|
33
|
+
// Round 4 item 8a — these were both labelled 'shared_transport', so a plan
|
|
34
|
+
// on a transport created seconds earlier read "raised … by shared_transport".
|
|
35
|
+
// Each reason now says what actually raised it.
|
|
36
|
+
if (changes.some((c) => c.op === 'release'))
|
|
37
|
+
raise('medium', 'transport_release');
|
|
38
|
+
if (changes.some((c) => c.op === 'activate'))
|
|
39
|
+
raise('medium', 'activation');
|
|
17
40
|
if (ctx.recentAtcFailure && changes.some((c) => ctx.recentAtcFailure.includes(c.object)))
|
|
18
41
|
raise('medium', 'atc_failed_24h');
|
|
19
42
|
if ((ctx.transportRecentDevelopers ?? 0) >= 3)
|
|
@@ -24,3 +47,31 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
24
47
|
raise('medium', 'prod_namespace');
|
|
25
48
|
return { level: floor, raisedBy };
|
|
26
49
|
}
|
|
50
|
+
/** Plain words for each risk reason code — the codes stay in audit data only. */
|
|
51
|
+
const RISK_REASON_TEXT = {
|
|
52
|
+
non_prod_override: 'auto-approve is set to never on this system (a setting, not a risk)',
|
|
53
|
+
delete_op: 'the plan deletes an object',
|
|
54
|
+
transport_release: 'the plan releases a transport',
|
|
55
|
+
activation: 'activation makes the change live on the system',
|
|
56
|
+
atc_failed_24h: 'ATC failed on this object in the last 24 hours',
|
|
57
|
+
shared_transport: 'the transport is shared by 3 or more developers',
|
|
58
|
+
escalated_in_session: 'more than 5 changes in one plan',
|
|
59
|
+
prod_namespace: 'the object is in a production namespace',
|
|
60
|
+
syntax_errors: 'the last syntax check of this object found errors',
|
|
61
|
+
};
|
|
62
|
+
/** Round 8 item 4 — every code the floor can raise (each has plain words above). */
|
|
63
|
+
export const RISK_REASON_CODES = Object.keys(RISK_REASON_TEXT);
|
|
64
|
+
/** Round 4 item 8a — never show an internal reason code raw. */
|
|
65
|
+
export function riskReasonText(code) {
|
|
66
|
+
return RISK_REASON_TEXT[code] ?? code.replace(/_/g, ' ');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Round 7 item 8b — the reasons a user is SHOWN. `non_prod_override` (the
|
|
70
|
+
* alias's auto_approve is 'never') raises the floor so nothing auto-approves,
|
|
71
|
+
* but it is a setting, not a risk: the card read "Risk raised to high:
|
|
72
|
+
* auto-approve is off for this system". It stays in audit data only.
|
|
73
|
+
*/
|
|
74
|
+
const NOT_A_RISK_REASON = new Set(['non_prod_override']);
|
|
75
|
+
export function shownRiskReasons(raisedBy) {
|
|
76
|
+
return raisedBy.filter((c) => !NOT_A_RISK_REASON.has(c));
|
|
77
|
+
}
|
package/dist/auth/api-key.js
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import keychain from './keychain.js';
|
|
1
|
+
import keychain, { newProbeAccount } from './keychain.js';
|
|
2
2
|
const SERVICE = 'cspeach';
|
|
3
3
|
const ACCOUNT = 'api-key';
|
|
4
4
|
class KeychainStore {
|
|
5
|
+
/** One probe per store: repeat checks (repl.tsx) reuse the first answer. */
|
|
6
|
+
available;
|
|
5
7
|
async get() { return keychain.getPassword(SERVICE, ACCOUNT); }
|
|
6
8
|
async set(key) { await keychain.setPassword(SERVICE, ACCOUNT, key); }
|
|
7
9
|
async delete() { await keychain.deletePassword(SERVICE, ACCOUNT); }
|
|
8
|
-
|
|
10
|
+
isKeychainAvailable() {
|
|
11
|
+
this.available ??= this.probe();
|
|
12
|
+
return this.available;
|
|
13
|
+
}
|
|
14
|
+
async probe() {
|
|
9
15
|
try {
|
|
16
|
+
// Round 4 item 7 — a per-process account: a fixed 'probe' entry was
|
|
17
|
+
// deleted by a second cspeach starting at the same time.
|
|
18
|
+
const account = newProbeAccount();
|
|
10
19
|
const probe = `probe-${Date.now()}`;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
try {
|
|
21
|
+
await keychain.setPassword(SERVICE, account, probe);
|
|
22
|
+
return (await keychain.getPassword(SERVICE, account)) === probe;
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await keychain.deletePassword(SERVICE, account).catch(() => false);
|
|
26
|
+
}
|
|
15
27
|
}
|
|
16
28
|
catch {
|
|
17
29
|
return false;
|
|
@@ -25,16 +37,18 @@ class EphemeralStore {
|
|
|
25
37
|
async delete() { this.cached = null; }
|
|
26
38
|
async isKeychainAvailable() { return false; }
|
|
27
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Round 4 item 4 — memoise the PROMISE, not the result. The startup upgrade
|
|
42
|
+
* check and runOneShot call getStore() concurrently; with a memoised result
|
|
43
|
+
* both ran a probe, and a probe that lost the race REPLACED the shared store
|
|
44
|
+
* with the in-memory EphemeralStore — the next get() found no key
|
|
45
|
+
* ("No API key configured" / uncaught no_api_key on `cspeach … < /dev/null`).
|
|
46
|
+
*/
|
|
28
47
|
let store;
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
store = ktr;
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
store = new EphemeralStore();
|
|
38
|
-
}
|
|
48
|
+
export function getStore() {
|
|
49
|
+
store ??= (async () => {
|
|
50
|
+
const ktr = new KeychainStore();
|
|
51
|
+
return (await ktr.isKeychainAvailable()) ? ktr : new EphemeralStore();
|
|
52
|
+
})();
|
|
39
53
|
return store;
|
|
40
54
|
}
|
package/dist/auth/keychain.js
CHANGED
|
Binary file
|
package/dist/auth/me.js
CHANGED
|
@@ -2,18 +2,36 @@
|
|
|
2
2
|
* Fetch /v1/me — customer profile + entitlements + usage.
|
|
3
3
|
* Result envelope so the REPL doesn't need try/catch.
|
|
4
4
|
*/
|
|
5
|
+
/** Default /v1/me budget. The panels brand line waits for this call (D5), so a
|
|
6
|
+
* hung proxy must not mean a blank screen. */
|
|
7
|
+
export const FETCH_ME_TIMEOUT_MS = 5000;
|
|
5
8
|
export async function fetchMe(opts) {
|
|
6
9
|
const url = `${opts.proxyUrl.replace(/\/$/, '')}/v1/me`;
|
|
10
|
+
const timeoutMs = opts.timeoutMs ?? FETCH_ME_TIMEOUT_MS;
|
|
11
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
12
|
+
// Race the signal too, so even a fetch that ignores it cannot hang startup.
|
|
13
|
+
// A timeout is a status-0 failure (the failed-refresh path), never a 401.
|
|
14
|
+
const timedOut = new Promise((_, reject) => {
|
|
15
|
+
signal.addEventListener('abort', () => reject(new Error(`timed out after ${timeoutMs} ms`)), { once: true });
|
|
16
|
+
});
|
|
17
|
+
timedOut.catch(() => { });
|
|
7
18
|
try {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
return await Promise.race([
|
|
20
|
+
(async () => {
|
|
21
|
+
const r = await fetch(url, { headers: { Authorization: `Bearer ${opts.apiKey}` }, signal });
|
|
22
|
+
if (!r.ok) {
|
|
23
|
+
const body = await r.json().catch(() => ({}));
|
|
24
|
+
return { ok: false, status: r.status, error: body.error ?? 'unknown' };
|
|
25
|
+
}
|
|
26
|
+
const profile = await r.json();
|
|
27
|
+
return { ok: true, profile };
|
|
28
|
+
})(),
|
|
29
|
+
timedOut,
|
|
30
|
+
]);
|
|
15
31
|
}
|
|
16
32
|
catch (err) {
|
|
33
|
+
if (signal.aborted)
|
|
34
|
+
return { ok: false, status: 0, error: `timed out after ${timeoutMs} ms` };
|
|
17
35
|
return { ok: false, status: 0, error: err instanceof Error ? err.message : String(err) };
|
|
18
36
|
}
|
|
19
37
|
}
|
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,16 @@ import { runAddSapWizard } from './sap/onboarding.js';
|
|
|
4
4
|
import { runGcOnStartup } from './session/gc.js';
|
|
5
5
|
import { checkForUpgrade } from './upgrade-check.js';
|
|
6
6
|
import { extractSapAliasFlag, extractNoSapFlag } from './cli-args.js';
|
|
7
|
+
import { applyColorMode } from './renderer/color-mode.js';
|
|
7
8
|
/**
|
|
8
9
|
* The verbs that make `cspeach project …` a mistyped team command rather than
|
|
9
10
|
* a prompt somebody meant to send. See the check in `main`.
|
|
10
11
|
*/
|
|
11
12
|
const TEAM_SUBCOMMANDS = new Set(['init', 'join', 'lane', 'refresh', 'status', 'outbox', 'rule', 'leave']);
|
|
12
13
|
export async function main(argv) {
|
|
14
|
+
// NO_COLOR (piece 2 §1.2) — before ANY output, for every command (REPL,
|
|
15
|
+
// one-shot, doctor, login, help, config …).
|
|
16
|
+
applyColorMode();
|
|
13
17
|
runGcOnStartup().catch(() => { });
|
|
14
18
|
checkForUpgrade().catch(() => { });
|
|
15
19
|
let args = argv.slice(2);
|
|
@@ -36,12 +40,9 @@ export async function main(argv) {
|
|
|
36
40
|
const { runUpgrade } = await import('./upgrade.js');
|
|
37
41
|
return runUpgrade();
|
|
38
42
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
// C.1 — extract --sap-alias <name> early so it is available for REPL and
|
|
44
|
-
// one-shot paths. Validation requires loading config to know available aliases.
|
|
43
|
+
// C.1 — extract --sap-alias <name> early so it is available for REPL,
|
|
44
|
+
// one-shot and resume paths (round 6 review C1: it used to be parsed after
|
|
45
|
+
// the --resume branch, so a resume never saw it). Validation requires loading config to know available aliases.
|
|
45
46
|
let sapAlias;
|
|
46
47
|
if (args.includes('--sap-alias')) {
|
|
47
48
|
const { loadConfig } = await import('./config/loader.js');
|
|
@@ -55,6 +56,15 @@ export async function main(argv) {
|
|
|
55
56
|
sapAlias = parsed.sapAlias;
|
|
56
57
|
args = parsed.rest;
|
|
57
58
|
}
|
|
59
|
+
if (args[0] === '--resume') {
|
|
60
|
+
// Final batch 5 — `--resume --one-shot "…"`: a flag is never a session id.
|
|
61
|
+
if (args[1]?.startsWith('-')) {
|
|
62
|
+
console.error(chalk.red(`error: --resume takes a session id, not '${args[1]}'. Usage: cspeach --resume [<id>] (no id → pick from a list)`));
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
const { runResume } = await import('./session/resume.js');
|
|
66
|
+
return runResume(args[1], { sapAlias, noSap });
|
|
67
|
+
}
|
|
58
68
|
// Standalone profile wizard — re-runnable any time. Checked BEFORE the plain
|
|
59
69
|
// `configure` branch so `configure --standalone` isn't swallowed by it.
|
|
60
70
|
// Asks what system the code will land on, then the standards question.
|
|
@@ -203,6 +213,15 @@ export async function main(argv) {
|
|
|
203
213
|
const { runOneShot } = await import('./one-shot.js');
|
|
204
214
|
return runOneShot(args.join(' '), { sapAlias, noSap });
|
|
205
215
|
}
|
|
216
|
+
// Round 8 item 3 — `echo "prompt" | cspeach`: a piped stdin carrying one
|
|
217
|
+
// prompt runs as --one-shot (stdout = the answer only). Scripted pipes
|
|
218
|
+
// (slash lines), empty or live stdin stay with the REPL, nothing consumed.
|
|
219
|
+
const { readPipedPrompt } = await import('./lib/piped-prompt.js');
|
|
220
|
+
const piped = await readPipedPrompt(process.stdin);
|
|
221
|
+
if (piped.kind === 'prompt') {
|
|
222
|
+
const { runOneShot } = await import('./one-shot.js');
|
|
223
|
+
return runOneShot(piped.text, { sapAlias, noSap });
|
|
224
|
+
}
|
|
206
225
|
// No args → REPL.
|
|
207
226
|
const { runRepl } = await import('./repl.js');
|
|
208
227
|
return runRepl({ sapAlias, noSap });
|
package/dist/commands/compact.js
CHANGED
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import fs from 'node:fs/promises';
|
|
28
28
|
import path from 'node:path';
|
|
29
|
-
import chalk from 'chalk';
|
|
30
29
|
import { sessionsDir } from '../config/paths.js';
|
|
30
|
+
import { panelHeading, panelRow } from '../renderer/panel-rows.js';
|
|
31
|
+
import { formatInfo } from '../renderer/notice-shape.js';
|
|
32
|
+
import { paint } from '../renderer/theme.js';
|
|
33
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
31
34
|
import { saveSession } from '../session/store.js';
|
|
32
35
|
import { isUserPrompt } from '../session/user-prompt.js';
|
|
33
36
|
/** Keep this many recent user-prompt anchored turns verbatim. */
|
|
@@ -277,18 +280,18 @@ export function formatCompactResult(result,
|
|
|
277
280
|
/** Optional — falls back to the legacy constant for back-compat. */
|
|
278
281
|
keepRecentTurns = KEEP_RECENT_TURNS) {
|
|
279
282
|
if (!result.performed) {
|
|
280
|
-
return
|
|
283
|
+
return formatInfo(`/compact: ${result.reason ?? 'nothing to do'}`).trimEnd();
|
|
281
284
|
}
|
|
282
285
|
const lines = [
|
|
283
286
|
'',
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
panelHeading('Compacted', 'older turns summarised'),
|
|
288
|
+
panelRow('Dropped', `${result.droppedTurns} messages`),
|
|
289
|
+
panelRow('Kept', `${result.keptTurns} messages (most recent ${keepRecentTurns} user turns intact)`),
|
|
290
|
+
panelRow('Summary', `~${result.summaryTokens?.toLocaleString()} tokens`),
|
|
291
|
+
panelRow('Backup', `${result.backupPath}`),
|
|
289
292
|
'',
|
|
290
|
-
|
|
291
|
-
|
|
293
|
+
`${paint('ok', getGlyphs().done)} The next turn's context is roughly summary-size instead of the full history.`,
|
|
294
|
+
formatInfo('The very next turn rebuilds the prompt cache once; turns after that are cheap again.').trimEnd(),
|
|
292
295
|
'',
|
|
293
296
|
];
|
|
294
297
|
return lines.join('\n');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { formatAttention } from '../renderer/notice-shape.js';
|
|
2
3
|
import { loadConfig, saveConfig, isValidWriteMode } from '../config/loader.js';
|
|
3
4
|
import { STANDALONE_PLATFORMS, STANDALONE_DATABASES, STANDALONE_DEPLOYMENTS, STANDALONE_IDES, isStandalonePlatform, deriveDatabase, deriveDeployment, defaultAbapCloudOnly, defaultRelease, } from '../sap/standalone-profile.js';
|
|
4
5
|
/**
|
|
@@ -350,8 +351,11 @@ export async function runConfigSet(args) {
|
|
|
350
351
|
}
|
|
351
352
|
cfg.sap[alias].insecure_skip_tls_verify = b;
|
|
352
353
|
if (b) {
|
|
353
|
-
console.log(
|
|
354
|
-
|
|
354
|
+
console.log(formatAttention({
|
|
355
|
+
text: `TLS verification is off for ${alias}`,
|
|
356
|
+
fact: 'dev only, never in production',
|
|
357
|
+
next: `cspeach config set sap.${alias}.ca_cert_path <path-to-corporate-CA>`,
|
|
358
|
+
}).trimEnd());
|
|
355
359
|
}
|
|
356
360
|
await saveConfig(cfg);
|
|
357
361
|
console.log(chalk.green(`Set ${key} = ${b}`));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadConfig } from '../config/loader.js';
|
|
3
|
+
import { paint } from '../renderer/theme.js';
|
|
4
|
+
import { formatAttention } from '../renderer/notice-shape.js';
|
|
3
5
|
/**
|
|
4
6
|
* Dotted paths of keys that must never appear in plaintext config output.
|
|
5
7
|
* SAP passwords live in the keychain and should never land in config.toml,
|
|
@@ -57,7 +59,7 @@ function printLlmMissingHints(llm) {
|
|
|
57
59
|
for (const key of required) {
|
|
58
60
|
const value = llm[key];
|
|
59
61
|
if (value === undefined || value === null || value === '') {
|
|
60
|
-
console.log(`${key} = ${
|
|
62
|
+
console.log(`${key} = ${paint('bad', '<not set>')}`);
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
}
|
|
@@ -83,14 +85,14 @@ function printConfigAsToml(cfg, prefix = '') {
|
|
|
83
85
|
// the implication (writes proposed, never executed) without having to
|
|
84
86
|
// read the docs.
|
|
85
87
|
if (fullKey === 'write_mode' && v === 'advisory-only') {
|
|
86
|
-
console.log(
|
|
88
|
+
console.log(' ' + formatAttention({ text: 'Advisory mode', fact: 'writes are proposed, never executed' }).trimEnd());
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
for (const [k, v] of tables) {
|
|
91
93
|
const fullKey = prefix ? `${prefix}.${k}` : k;
|
|
92
94
|
console.log('');
|
|
93
|
-
console.log(chalk.
|
|
95
|
+
console.log(chalk.bold(`[${fullKey}]`));
|
|
94
96
|
printConfigAsToml(v, fullKey);
|
|
95
97
|
// Mode-aware extras for the `[llm]` section — print required keys that
|
|
96
98
|
// are empty as `<not set>` so a user who ran `cspeach config set llm.mode
|
|
@@ -158,7 +160,7 @@ export async function runConfigShow(args) {
|
|
|
158
160
|
if (typeof resolved === 'object' && resolved !== null) {
|
|
159
161
|
// Sub-object (e.g. sap.S4H-DEV) — print as TOML section
|
|
160
162
|
console.log('');
|
|
161
|
-
console.log(chalk.
|
|
163
|
+
console.log(chalk.bold(`[${keyPath}]`));
|
|
162
164
|
for (const [k, v] of Object.entries(resolved)) {
|
|
163
165
|
const subKey = `${keyPath}.${k}`;
|
|
164
166
|
if (REDACTED_KEYS.has(subKey)) {
|
|
@@ -173,7 +175,7 @@ export async function runConfigShow(args) {
|
|
|
173
175
|
else {
|
|
174
176
|
console.log(String(resolved));
|
|
175
177
|
if (keyPath === 'write_mode' && resolved === 'advisory-only') {
|
|
176
|
-
console.log(
|
|
178
|
+
console.log(' ' + formatAttention({ text: 'Advisory mode', fact: 'writes are proposed, never executed' }).trimEnd());
|
|
177
179
|
}
|
|
178
180
|
}
|
|
179
181
|
}
|