@cspeach/cli 1.1.18 → 1.1.20
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/anthropic-provider.js +30 -10
- package/dist/agent/cache-keepalive.js +162 -0
- package/dist/agent/cold-prune.js +116 -0
- package/dist/agent/loop.js +804 -159
- package/dist/agent/provider-shape.js +263 -0
- package/dist/agent/providers/ai-hub-provider.js +17 -2
- package/dist/agent/providers/byok-provider.js +33 -3
- package/dist/agent/providers/local-provider.js +12 -2
- package/dist/agent/repair-partial.js +125 -7
- package/dist/agent/summarise-via-provider.js +6 -1
- package/dist/agent/system-prompt.js +38 -0
- package/dist/agent/tool-dispatch.js +66 -21
- package/dist/agent/tool-loading-pin.js +100 -0
- 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 +36 -6
- package/dist/commands/auto-compact.js +33 -16
- package/dist/commands/compact.js +49 -11
- package/dist/commands/config-set.js +16 -3
- package/dist/commands/config-show.js +18 -5
- package/dist/commands/cost.js +54 -35
- 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-audit.js +1 -0
- 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 +70 -5
- package/dist/cost/cost-log.js +62 -2
- package/dist/cost/pricing.js +11 -6
- 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/lib/spill-labels.js +13 -0
- package/dist/lock-contention.js +2 -2
- package/dist/models/resolve.js +93 -2
- package/dist/models/server-config.js +158 -3
- package/dist/one-shot.js +150 -45
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/image-attachments.js +15 -2
- 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 +100 -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 +154 -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 +269 -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 +55 -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/current-transport.js +13 -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 +43 -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 +863 -339
- 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/repin-model.js +18 -0
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +84 -28
- 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/preamble.js +75 -0
- package/dist/skills/source-manifest.js +11 -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-read.js +11 -1
- 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/result-spill.js +238 -0
- package/dist/tools/sap-read.js +74 -15
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +39 -12
- package/dist/tools/subagent/adt-serial.js +33 -0
- package/dist/tools/subagent/agent_run.js +2 -0
- package/dist/tools/subagent/read_agent.js +178 -0
- package/dist/tools/subagent/reader-prompt.js +48 -0
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +26 -19
- package/dist/tools/tool-loading.js +255 -0
- package/dist/tools/tool-output-read.js +117 -0
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +39 -8
- 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 +2 -1
- 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 +65 -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 +12 -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
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import stringWidth from 'string-width';
|
|
2
3
|
import { loadConfig, saveConfig } from '../config/loader.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { shouldUseInk } from '../renderer/tty.js';
|
|
5
|
+
import { uiWidth } from '../renderer/ui-width.js';
|
|
6
|
+
import { paint } from '../renderer/theme.js';
|
|
7
|
+
import { emitTrace } from '../renderer/trace.js';
|
|
8
|
+
import { askConfirm } from '../ui/confirm-request.js';
|
|
9
|
+
// Classic box: width from the §1.4 rule (was a fixed 62); padding counts
|
|
10
|
+
// VISIBLE cells (string-width ignores ANSI) so coloured rows line up with
|
|
11
|
+
// plain ones. (strip-ansi is a devDependency only — not usable here.)
|
|
12
|
+
const boxWidth = () => uiWidth() - 2;
|
|
13
|
+
const top = () => '┌' + '─'.repeat(boxWidth()) + '┐';
|
|
14
|
+
const bottom = () => '└' + '─'.repeat(boxWidth()) + '┘';
|
|
15
|
+
const pad = (s) => '│ ' + s + ' '.repeat(Math.max(0, boxWidth() - 2 - stringWidth(s))) + '│';
|
|
16
|
+
const blank = () => '│' + ' '.repeat(boxWidth()) + '│';
|
|
8
17
|
/**
|
|
9
18
|
* Capture a single keypress for the y/n/d answer. Raw-mode readline so the
|
|
10
19
|
* hand-drawn box stays the only visible affordance.
|
|
@@ -43,6 +52,31 @@ async function promptNagChoice() {
|
|
|
43
52
|
process.stdin.on('data', onData);
|
|
44
53
|
});
|
|
45
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Piece 2 §6.1 — under Ink the nag is a confirm card in the card slot.
|
|
57
|
+
* Raw stdin reads while Ink is mounted steal or double keystrokes.
|
|
58
|
+
* y / n / d stay as keys; Esc (or no listener) is 'n', like Ctrl+C today.
|
|
59
|
+
* Cursor starts on "No": the one choice that writes nothing.
|
|
60
|
+
*/
|
|
61
|
+
async function promptNagChoiceInk(sapAlias) {
|
|
62
|
+
const res = await askConfirm({
|
|
63
|
+
id: 'auto-approve-nag',
|
|
64
|
+
tag: 'QUESTION',
|
|
65
|
+
title: `Auto-approve low-risk changes on ${sapAlias}?`,
|
|
66
|
+
facts: [
|
|
67
|
+
'This change was classified as low risk.',
|
|
68
|
+
`Today every change on ${sapAlias} asks you first.`,
|
|
69
|
+
],
|
|
70
|
+
choices: [
|
|
71
|
+
{ value: 'y', label: 'Yes, auto-approve low-risk changes', hint: 'DEV systems only' },
|
|
72
|
+
{ value: 'n', label: 'No, keep asking every time' },
|
|
73
|
+
{ value: 'd', label: `Don't ask me this again on ${sapAlias}` },
|
|
74
|
+
],
|
|
75
|
+
initial: 1,
|
|
76
|
+
aliases: { y: 'y', n: 'n', d: 'd' },
|
|
77
|
+
});
|
|
78
|
+
return res.value === 'y' || res.value === 'd' ? res.value : 'n';
|
|
79
|
+
}
|
|
46
80
|
/**
|
|
47
81
|
* Called just before `renderPerChangeApproval` when the current change
|
|
48
82
|
* is low-risk on a `never` system and the nag has not been dismissed.
|
|
@@ -61,29 +95,42 @@ export async function maybeShowAutoApproveNag(ctx) {
|
|
|
61
95
|
return;
|
|
62
96
|
if (sapCfg.nag_dismissed ?? false)
|
|
63
97
|
return;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
const ink = shouldUseInk();
|
|
99
|
+
let choice;
|
|
100
|
+
if (ink) {
|
|
101
|
+
choice = await promptNagChoiceInk(ctx.sapAlias);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Classic: the box is the UI — keypress answers it directly.
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(top());
|
|
107
|
+
console.log(blank());
|
|
108
|
+
console.log(pad(chalk.yellow('This change was classified as LOW risk.')));
|
|
109
|
+
console.log(blank());
|
|
110
|
+
console.log(pad(`Your current policy on ${ctx.sapAlias} prompts on everything.`));
|
|
111
|
+
console.log(pad('Want to auto-approve low-risk changes on this system?'));
|
|
112
|
+
console.log(blank());
|
|
113
|
+
console.log(pad(chalk.green('[y] yes') + ' (safer for DEV systems only)'));
|
|
114
|
+
console.log(pad(chalk.dim('[n] no ') + ' (keep prompting every time)'));
|
|
115
|
+
console.log(pad(chalk.dim('[d] don\'t ask me this again (on ' + ctx.sapAlias + ')')));
|
|
116
|
+
console.log(blank());
|
|
117
|
+
console.log(bottom());
|
|
118
|
+
process.stdout.write(chalk.dim('Press y / n / d: '));
|
|
119
|
+
choice = await promptNagChoice();
|
|
120
|
+
}
|
|
80
121
|
if (choice === 'y') {
|
|
81
122
|
const fresh = await loadConfig(); // re-read to avoid stale write
|
|
82
123
|
if (fresh.sap[ctx.sapAlias]) {
|
|
83
124
|
fresh.sap[ctx.sapAlias].auto_approve = 'low';
|
|
84
125
|
await saveConfig(fresh);
|
|
85
|
-
|
|
86
|
-
|
|
126
|
+
if (ink) {
|
|
127
|
+
emitTrace(paint('ok', `Auto-approve set to "low" for ${ctx.sapAlias}.`));
|
|
128
|
+
emitTrace(paint('dim', `Change with: cspeach config set sap.${ctx.sapAlias}.auto_approve never`));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(chalk.green(`Auto-approve set to "low" for ${ctx.sapAlias}.`));
|
|
132
|
+
console.log(chalk.gray('Change with: cspeach config set sap.' + ctx.sapAlias + '.auto_approve never\n'));
|
|
133
|
+
}
|
|
87
134
|
}
|
|
88
135
|
}
|
|
89
136
|
else if (choice === 'd') {
|
|
@@ -93,7 +140,10 @@ export async function maybeShowAutoApproveNag(ctx) {
|
|
|
93
140
|
if (fresh.sap[ctx.sapAlias]) {
|
|
94
141
|
fresh.sap[ctx.sapAlias].nag_dismissed = true;
|
|
95
142
|
await saveConfig(fresh);
|
|
96
|
-
|
|
143
|
+
if (ink)
|
|
144
|
+
emitTrace(paint('dim', `Noted. This prompt will not appear again on ${ctx.sapAlias}.`));
|
|
145
|
+
else
|
|
146
|
+
console.log(chalk.gray(`Noted. This prompt will not appear again on ${ctx.sapAlias}.\n`));
|
|
97
147
|
}
|
|
98
148
|
}
|
|
99
149
|
// 'n' → no config change, fall through to per-change approval normally.
|
package/dist/approvals/jwt.js
CHANGED
|
@@ -55,12 +55,13 @@ export async function mintApprovalId(payload) {
|
|
|
55
55
|
* through this function instead of calling mintApprovalId with a free-text
|
|
56
56
|
* object string.
|
|
57
57
|
*/
|
|
58
|
-
export async function mintChangeApproval(change, sessionId) {
|
|
58
|
+
export async function mintChangeApproval(change, sessionId, opts = {}) {
|
|
59
59
|
return mintApprovalId({
|
|
60
60
|
object: canonicalApprovalObject(change.object),
|
|
61
61
|
type: change.type,
|
|
62
62
|
op: change.op,
|
|
63
63
|
session_id: sessionId,
|
|
64
|
+
...(opts.auto ? { auto: true } : {}),
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
export async function verifyAndSpendApprovalId(jwt, expectedObject, expectedOp) {
|
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.
|
|
@@ -109,6 +119,17 @@ export async function main(argv) {
|
|
|
109
119
|
}
|
|
110
120
|
if (args[0] === 'config' && args[1] === 'show') {
|
|
111
121
|
const { runConfigShow } = await import('./commands/config-show.js');
|
|
122
|
+
if (args.length === 2) {
|
|
123
|
+
// Task 12 — the full dump ends with "# model set by your account admin"
|
|
124
|
+
// when /v1/me/model-config says the admin's model is enforced. Capped at
|
|
125
|
+
// 1.5 s and fail-open: offline / signed out ⇒ the output is unchanged.
|
|
126
|
+
const [{ loadConfig }, { getStore }, { loadCustomerModelConfig }] = await Promise.all([
|
|
127
|
+
import('./config/loader.js'),
|
|
128
|
+
import('./auth/api-key.js'),
|
|
129
|
+
import('./models/server-config.js'),
|
|
130
|
+
]);
|
|
131
|
+
await loadCustomerModelConfig(await loadConfig(), async () => (await (await getStore()).get()) ?? '', { timeoutMs: 1500 }).catch(() => { });
|
|
132
|
+
}
|
|
112
133
|
await runConfigShow(args.slice(2));
|
|
113
134
|
return 0;
|
|
114
135
|
}
|
|
@@ -203,6 +224,15 @@ export async function main(argv) {
|
|
|
203
224
|
const { runOneShot } = await import('./one-shot.js');
|
|
204
225
|
return runOneShot(args.join(' '), { sapAlias, noSap });
|
|
205
226
|
}
|
|
227
|
+
// Round 8 item 3 — `echo "prompt" | cspeach`: a piped stdin carrying one
|
|
228
|
+
// prompt runs as --one-shot (stdout = the answer only). Scripted pipes
|
|
229
|
+
// (slash lines), empty or live stdin stay with the REPL, nothing consumed.
|
|
230
|
+
const { readPipedPrompt } = await import('./lib/piped-prompt.js');
|
|
231
|
+
const piped = await readPipedPrompt(process.stdin);
|
|
232
|
+
if (piped.kind === 'prompt') {
|
|
233
|
+
const { runOneShot } = await import('./one-shot.js');
|
|
234
|
+
return runOneShot(piped.text, { sapAlias, noSap });
|
|
235
|
+
}
|
|
206
236
|
// No args → REPL.
|
|
207
237
|
const { runRepl } = await import('./repl.js');
|
|
208
238
|
return runRepl({ sapAlias, noSap });
|