@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
|
@@ -3,6 +3,7 @@ import { getTool, isStandalone } from '../tools/index.js';
|
|
|
3
3
|
import { NOT_CONNECTED_MESSAGE } from '../sap/offline-adt-client.js';
|
|
4
4
|
import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
|
|
5
5
|
import { shouldGateRule8, recordWriteOp, renderPlanSummary, getWriteOpsThisTurn, } from '../repl/rule8-detector.js';
|
|
6
|
+
import { isCredentialHandoverApproved, markCredentialHandoverApproved, } from '../repl/credential-handover-gate.js';
|
|
6
7
|
export async function dispatchTool(name, args, ctx) {
|
|
7
8
|
const tool = getTool(name);
|
|
8
9
|
if (!tool) {
|
|
@@ -23,6 +24,37 @@ export async function dispatchTool(name, args, ctx) {
|
|
|
23
24
|
is_error: true,
|
|
24
25
|
};
|
|
25
26
|
}
|
|
27
|
+
// L1-6 R1-6 (owner decision 2026-09-21) — a call that hands the stored SAP
|
|
28
|
+
// login to a local child process ALWAYS asks a human, once per session per
|
|
29
|
+
// alias, even as the first mutating call of a turn (where Rule 8 below stays
|
|
30
|
+
// silent). Headless runs are declined, never auto-approved.
|
|
31
|
+
// The hook may be async (it reads the app's files). A hook that throws is
|
|
32
|
+
// doubt — never let the call through unprompted: fail the call instead.
|
|
33
|
+
let handover = null;
|
|
34
|
+
try {
|
|
35
|
+
handover = (await tool.credentialHandover?.(args, ctx)) ?? null;
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
return {
|
|
39
|
+
content: JSON.stringify({ error: 'credential_handover_check_failed', reason: String(err) }),
|
|
40
|
+
is_error: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const sessionId = ctx.session?.id;
|
|
44
|
+
if (handover && !isCredentialHandoverApproved(sessionId, handover.alias)) {
|
|
45
|
+
const r = await presentSafetyConfirmation({
|
|
46
|
+
rule: 'CREDENTIAL_HANDOVER',
|
|
47
|
+
op: tool.name,
|
|
48
|
+
object: { type: 'SAP login', name: handover.alias },
|
|
49
|
+
what_will_happen: handover.summary,
|
|
50
|
+
rollback_available: false, // a handed-over secret cannot be taken back
|
|
51
|
+
transport: '',
|
|
52
|
+
args,
|
|
53
|
+
});
|
|
54
|
+
if (!r.confirmed)
|
|
55
|
+
return declined(r);
|
|
56
|
+
markCredentialHandoverApproved(sessionId, handover.alias);
|
|
57
|
+
}
|
|
26
58
|
// Rule 8 — no batch without plan. 2nd+ mutating call this turn requires
|
|
27
59
|
// explicit confirmation. Fires regardless of /safety-mode toggle (batch
|
|
28
60
|
// detection is unconditional per Plan 3 §6.6).
|
|
@@ -31,7 +63,12 @@ export async function dispatchTool(name, args, ctx) {
|
|
|
31
63
|
// GUIDE.md) — local scratch, not SAP objects. Rule 8 protects SAP batches;
|
|
32
64
|
// applying it here made the 2nd+ file (GUIDE.md) hit the batch card and,
|
|
33
65
|
// headless, get refused. Exempt from the gate AND from the batch count.
|
|
34
|
-
|
|
66
|
+
// Local project work (owner decision 2026-09-25, CAP spike F2): the same
|
|
67
|
+
// reasoning for ALL local work — file edits (the filesystem tools refuse any
|
|
68
|
+
// path outside the project) and the safelisted local build commands. One-shot
|
|
69
|
+
// refused the 2nd edit of a CAP schema and left it half-changed. SAP writes
|
|
70
|
+
// are gated exactly as before.
|
|
71
|
+
const scratchWrite = isLocalProjectWork(tool, args) || isScratchOutputWrite(tool, args, ctx.cwd);
|
|
35
72
|
if (tool.isMutating && !scratchWrite && shouldGateRule8()) {
|
|
36
73
|
const r = await presentSafetyConfirmation({
|
|
37
74
|
rule: 'RULE_8_BATCH_PLAN',
|
|
@@ -44,28 +81,10 @@ export async function dispatchTool(name, args, ctx) {
|
|
|
44
81
|
rollback_available: true, // snapshots taken per-write
|
|
45
82
|
transport: typeof args?.transport === 'string' ? args.transport : '',
|
|
46
83
|
batch_count: getWriteOpsThisTurn().length + 1,
|
|
84
|
+
args,
|
|
47
85
|
});
|
|
48
|
-
if (!r.confirmed)
|
|
49
|
-
|
|
50
|
-
// saw the card) must not masquerade as a user decision: the model
|
|
51
|
-
// (and any transcript reader) reacts differently to "the user said
|
|
52
|
-
// no" vs "no user was available to say yes". Uniform `headless: true`
|
|
53
|
-
// marker matches the other headless result payloads.
|
|
54
|
-
if (r.headless) {
|
|
55
|
-
return {
|
|
56
|
-
content: JSON.stringify({
|
|
57
|
-
error: 'headless_safety_decline',
|
|
58
|
-
headless: true,
|
|
59
|
-
reason: r.reason,
|
|
60
|
-
}),
|
|
61
|
-
is_error: true,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
content: JSON.stringify({ error: 'cancelled_by_user', reason: r.reason }),
|
|
66
|
-
is_error: true,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
86
|
+
if (!r.confirmed)
|
|
87
|
+
return declined(r);
|
|
69
88
|
}
|
|
70
89
|
try {
|
|
71
90
|
const result = await tool.handler(args, ctx);
|
|
@@ -83,6 +102,45 @@ export async function dispatchTool(name, args, ctx) {
|
|
|
83
102
|
return { content: JSON.stringify({ error: String(err) }), is_error: true };
|
|
84
103
|
}
|
|
85
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* B5 — discriminate WHO declined a safety card. A headless fail-fast (nobody
|
|
107
|
+
* ever saw the card) must not masquerade as a user decision: the model (and
|
|
108
|
+
* any transcript reader) reacts differently to "the user said no" vs "no user
|
|
109
|
+
* was available to say yes". Uniform `headless: true` marker matches the
|
|
110
|
+
* other headless result payloads.
|
|
111
|
+
*/
|
|
112
|
+
function declined(r) {
|
|
113
|
+
if (r.headless) {
|
|
114
|
+
return {
|
|
115
|
+
content: JSON.stringify({
|
|
116
|
+
error: 'headless_safety_decline',
|
|
117
|
+
headless: true,
|
|
118
|
+
reason: r.reason,
|
|
119
|
+
}),
|
|
120
|
+
is_error: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
content: JSON.stringify({ error: 'cancelled_by_user', reason: r.reason }),
|
|
125
|
+
is_error: true,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* True for local project work: the filesystem tools (confined to the project
|
|
130
|
+
* folder by resolveSafePath) and shell_exec (a hardcoded safelist of local build
|
|
131
|
+
* and VCS commands run in the project). Neither touches an SAP object, which is
|
|
132
|
+
* what Rule 8 protects — EXCEPT a deploy or publish: `npm run deploy` /
|
|
133
|
+
* `ui5 deploy` / `fiori deploy` upload to the ABAP repository, so any shell
|
|
134
|
+
* command with a deploy/publish argument still counts (deliberately broad).
|
|
135
|
+
*/
|
|
136
|
+
export function isLocalProjectWork(tool, args) {
|
|
137
|
+
if (tool.category === 'filesystem')
|
|
138
|
+
return true;
|
|
139
|
+
if (tool.category !== 'shell')
|
|
140
|
+
return false;
|
|
141
|
+
const argv = [args?.command, ...(Array.isArray(args?.args) ? args.args : [])].map((a) => String(a ?? ''));
|
|
142
|
+
return !argv.some((a) => /deploy|publish/i.test(a));
|
|
143
|
+
}
|
|
86
144
|
/**
|
|
87
145
|
* True for a mutating filesystem tool whose target sits under the project's
|
|
88
146
|
* own ./cspeach-out/ (cwd). Accepts `cspeach-out/…`, `./cspeach-out/…`, the
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* everything. The new behaviour:
|
|
6
6
|
*
|
|
7
7
|
* - Writes the full error (message + stack) to ~/.cspeach/logs/<sessionId>-<ts>.log
|
|
8
|
-
* - Prints
|
|
9
|
-
* stream path, log path
|
|
8
|
+
* - Prints the piece 2 failure shape to stderr: cause, session id,
|
|
9
|
+
* stream path, log path and resume facts as dim detail, then one Next: line.
|
|
10
10
|
* - Never throws — best-effort. The CLI returns to the prompt cleanly so
|
|
11
11
|
* the user can either resume or type a new prompt.
|
|
12
12
|
*
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* pushed any partial assistant content onto session.messages and saved
|
|
15
15
|
* the session before the throw bubbled here. Resume picks up that state.
|
|
16
16
|
*/
|
|
17
|
-
import
|
|
17
|
+
import { formatFailure, formatInfo } from '../renderer/notice-shape.js';
|
|
18
|
+
import { paint } from '../renderer/theme.js';
|
|
19
|
+
import { getLook } from '../renderer/look.js';
|
|
18
20
|
import { promises as fs } from 'node:fs';
|
|
19
21
|
import * as os from 'node:os';
|
|
20
22
|
import * as path from 'node:path';
|
|
@@ -48,7 +50,37 @@ export function describeError(err) {
|
|
|
48
50
|
return 'network timeout';
|
|
49
51
|
if (msg.toLowerCase().includes('aborterror'))
|
|
50
52
|
return 'request aborted';
|
|
51
|
-
|
|
53
|
+
const api = describeApiError(err, msg);
|
|
54
|
+
if (api)
|
|
55
|
+
return api;
|
|
56
|
+
return msg.replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 2026-09-27 — an upstream API error arrives as `Error: 400 {"type":"error",
|
|
60
|
+
* "error":{…,"message":"…"},"request_id":"…"}`. Show ONE readable line
|
|
61
|
+
* (`service error 400: <message>`), never the raw JSON body.
|
|
62
|
+
*/
|
|
63
|
+
function describeApiError(err, msg) {
|
|
64
|
+
const brace = msg.indexOf('{');
|
|
65
|
+
if (brace < 0)
|
|
66
|
+
return null;
|
|
67
|
+
const end = msg.lastIndexOf('}');
|
|
68
|
+
let body;
|
|
69
|
+
try {
|
|
70
|
+
body = JSON.parse(msg.slice(brace, end + 1));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (!body || typeof body !== 'object')
|
|
76
|
+
return null;
|
|
77
|
+
const head = msg.slice(0, brace).match(/(\d{3})\s*$/);
|
|
78
|
+
const statusRaw = head?.[1] ?? err?.status;
|
|
79
|
+
const status = statusRaw != null && /^\d{3}$/.test(String(statusRaw)) ? String(statusRaw) : null;
|
|
80
|
+
const inner = body?.error?.message ?? body?.message;
|
|
81
|
+
const text = typeof inner === 'string' ? inner.replace(/\s+/g, ' ').trim() : '';
|
|
82
|
+
const label = status ? `service error ${status}` : 'service error';
|
|
83
|
+
return (text ? `${label}: ${text}` : label).slice(0, 200);
|
|
52
84
|
}
|
|
53
85
|
/**
|
|
54
86
|
* Write a full diagnostic log to ~/.cspeach/logs/<sessionId>-<ts>.log.
|
|
@@ -96,38 +128,57 @@ async function writeErrorLog(sessionId, err) {
|
|
|
96
128
|
return null;
|
|
97
129
|
}
|
|
98
130
|
}
|
|
131
|
+
/** Print a multi-line block through the line-based `print` sink. */
|
|
132
|
+
function printBlock(print, block) {
|
|
133
|
+
for (const l of block.replace(/\n$/, '').split('\n'))
|
|
134
|
+
print(l);
|
|
135
|
+
}
|
|
99
136
|
/**
|
|
100
|
-
* Render the user-facing turn-error block
|
|
101
|
-
*
|
|
137
|
+
* Render the user-facing turn-error block (piece 2 §9 failure shape). Writes a
|
|
138
|
+
* log file (best-effort) and prints the cause plus the Session, Stream, Log and
|
|
139
|
+
* Resume facts as dim detail, ending with ONE Next: line — "type continue"
|
|
140
|
+
* inside a live session, `cspeach --resume <id>` otherwise.
|
|
102
141
|
*/
|
|
103
142
|
export async function renderTurnError(input) {
|
|
104
143
|
const print = input.print ?? ((line) => process.stderr.write(line + '\n'));
|
|
144
|
+
const id = input.session.id;
|
|
145
|
+
const resumeRows = [
|
|
146
|
+
`Resume: cspeach --resume ${id}`,
|
|
147
|
+
` cspeach --resume (no arg → pick from a list)`,
|
|
148
|
+
];
|
|
105
149
|
print('');
|
|
106
150
|
if (input.userInterrupted) {
|
|
107
|
-
// User pressed Ctrl+C / Esc —
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
151
|
+
// User pressed Ctrl+C / Esc — a deliberate cancellation, not a crash.
|
|
152
|
+
// Skip the log file (no diagnostic value). Round 6 item 4d — the panels
|
|
153
|
+
// shape: ONE notice line. Review M4 — the legacy look keeps its rows.
|
|
154
|
+
if (getLook() === 'panels') {
|
|
155
|
+
printBlock(print, formatInfo(input.inSession
|
|
156
|
+
? 'Turn cancelled · your work so far is kept · type continue to pick up'
|
|
157
|
+
: `Turn cancelled · resume: cspeach --resume ${id}`));
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
printBlock(print, formatInfo('Turn cancelled'));
|
|
161
|
+
for (const r of [`Session: ${id}`, ...resumeRows])
|
|
162
|
+
print(` ${paint('dim', r)}`);
|
|
163
|
+
}
|
|
113
164
|
print('');
|
|
114
165
|
return;
|
|
115
166
|
}
|
|
116
167
|
const reason = describeError(input.err);
|
|
117
|
-
const logPath = await writeErrorLog(
|
|
118
|
-
|
|
119
|
-
if (input.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
168
|
+
const logPath = await writeErrorLog(id, input.err);
|
|
169
|
+
const detail = [reason, `Session: ${id}`];
|
|
170
|
+
if (input.streamFilePath)
|
|
171
|
+
detail.push(`Stream: ${input.streamFilePath}`);
|
|
172
|
+
if (logPath)
|
|
173
|
+
detail.push(`Log: ${logPath}`);
|
|
174
|
+
detail.push(...resumeRows);
|
|
175
|
+
printBlock(print, formatFailure({
|
|
176
|
+
what: 'Turn interrupted',
|
|
177
|
+
detail,
|
|
178
|
+
next: input.inSession
|
|
179
|
+
? 'type continue to pick up where it stopped (your work so far is saved)'
|
|
180
|
+
: `cspeach --resume ${id}`,
|
|
181
|
+
}));
|
|
131
182
|
print('');
|
|
132
183
|
}
|
|
133
184
|
/** Re-export for callers that want to compute a stream path independently. */
|
|
@@ -29,6 +29,17 @@ export function streamsRoot() {
|
|
|
29
29
|
export class TurnStreamWriter {
|
|
30
30
|
filePath;
|
|
31
31
|
headerWritten = false;
|
|
32
|
+
/**
|
|
33
|
+
* Fix round 3 (item 11a) — every write runs after the previous one. The
|
|
34
|
+
* loop calls append() per text delta WITHOUT awaiting, so unserialised
|
|
35
|
+
* appendFile calls interleaved on disk (a scrambled log) and two racing
|
|
36
|
+
* first appends could each write the header.
|
|
37
|
+
*/
|
|
38
|
+
chain = Promise.resolve();
|
|
39
|
+
enqueue(work) {
|
|
40
|
+
this.chain = this.chain.then(work, work);
|
|
41
|
+
return this.chain;
|
|
42
|
+
}
|
|
32
43
|
constructor(sessionId, startedAt = new Date()) {
|
|
33
44
|
// ISO timestamp with filename-safe punctuation.
|
|
34
45
|
const stamp = startedAt.toISOString().replace(/[:.]/g, '-');
|
|
@@ -43,9 +54,12 @@ export class TurnStreamWriter {
|
|
|
43
54
|
* file-IO errors so a write hiccup never kills a working turn. Lazily writes
|
|
44
55
|
* a small header on the first append so an unused turn produces no file.
|
|
45
56
|
*/
|
|
46
|
-
|
|
57
|
+
append(text) {
|
|
47
58
|
if (!text || text.length === 0)
|
|
48
|
-
return;
|
|
59
|
+
return Promise.resolve();
|
|
60
|
+
return this.enqueue(() => this.write(text));
|
|
61
|
+
}
|
|
62
|
+
async write(text) {
|
|
49
63
|
try {
|
|
50
64
|
if (!this.headerWritten) {
|
|
51
65
|
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
@@ -63,7 +77,15 @@ export class TurnStreamWriter {
|
|
|
63
77
|
* Close the stream with an optional footer (token totals, duration, end
|
|
64
78
|
* status). Idempotent — safe to call multiple times.
|
|
65
79
|
*/
|
|
66
|
-
|
|
80
|
+
close(footer) {
|
|
81
|
+
return this.enqueue(() => this.writeFooter(footer));
|
|
82
|
+
}
|
|
83
|
+
async writeFooter(footer) {
|
|
84
|
+
// Round 6 item 4e — a turn cut before it streamed any text (a cancel
|
|
85
|
+
// while thinking, /exit mid-turn) still gets a log with its footer, so
|
|
86
|
+
// the goodbye can point at it. A clean turn with no text writes nothing.
|
|
87
|
+
if (!this.headerWritten && footer)
|
|
88
|
+
await this.write('');
|
|
67
89
|
if (!this.headerWritten)
|
|
68
90
|
return;
|
|
69
91
|
try {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-review item 3 — one normaliser for object types, shared by the syntax
|
|
3
|
+
* state (tools/syntax-state.ts) and the 8e risk-floor backstop. Approvals
|
|
4
|
+
* carry whatever type word the model chose ("CLASS", "Program", "PROG/P");
|
|
5
|
+
* the syntax state records the tool's ADT code. Unknown words return
|
|
6
|
+
* undefined so the caller can fall back to a name-only match.
|
|
7
|
+
*/
|
|
8
|
+
const ALIASES = {
|
|
9
|
+
CLASS: 'CLAS', PROGRAM: 'PROG', REPORT: 'PROG', INTERFACE: 'INTF',
|
|
10
|
+
'FUNCTION GROUP': 'FUGR', FUNCTIONGROUP: 'FUGR', 'FUNCTION MODULE': 'FUNC', FUNCTION: 'FUNC',
|
|
11
|
+
TABLE: 'TABL', STRUCTURE: 'TABL', 'DATA ELEMENT': 'DTEL', DOMAIN: 'DOMA', INCLUDE: 'PROG',
|
|
12
|
+
'CDS VIEW': 'DDLS', CDS: 'DDLS', 'CDS VIEW ENTITY': 'DDLS', 'BEHAVIOR DEFINITION': 'BDEF', 'BEHAVIOUR DEFINITION': 'BDEF',
|
|
13
|
+
'SERVICE DEFINITION': 'SRVD', 'SERVICE BINDING': 'SRVB', 'METADATA EXTENSION': 'DDLX', 'ACCESS CONTROL': 'DCLS',
|
|
14
|
+
'MESSAGE CLASS': 'MSAG', PACKAGE: 'DEVC',
|
|
15
|
+
};
|
|
16
|
+
const CODES = new Set([
|
|
17
|
+
'CLAS', 'PROG', 'INTF', 'FUGR', 'FUNC', 'TABL', 'DTEL', 'DOMA', 'DDLS', 'BDEF', 'SRVD', 'SRVB',
|
|
18
|
+
'DDLX', 'DCLS', 'MSAG', 'DEVC', 'ENHO', 'XSLT', 'TTYP', 'VIEW', 'SHLP', 'NROB',
|
|
19
|
+
]);
|
|
20
|
+
/** ADT code (CLAS, PROG, …) for a type word, or undefined when unknown. */
|
|
21
|
+
export function normaliseAdtType(type) {
|
|
22
|
+
const t = (type ?? '').trim().toUpperCase().replace(/\s+/g, ' ');
|
|
23
|
+
const head = t.split('/')[0].trim();
|
|
24
|
+
if (CODES.has(head))
|
|
25
|
+
return head;
|
|
26
|
+
return ALIASES[t] ?? ALIASES[head];
|
|
27
|
+
}
|
|
@@ -1,7 +1,55 @@
|
|
|
1
1
|
import clipboardy from 'clipboardy';
|
|
2
2
|
import readline from 'node:readline/promises';
|
|
3
3
|
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
+
import { shouldUseInk } from '../renderer/tty.js';
|
|
5
|
+
import { emitTrace, formatDecisionTrace } from '../renderer/trace.js';
|
|
6
|
+
import { paint } from '../renderer/theme.js';
|
|
7
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
8
|
+
import { askConfirm } from '../ui/confirm-request.js';
|
|
9
|
+
const ADVISORY_LABEL = { applied: 'I applied it', skipped: 'Skip', aborted: 'Stop' };
|
|
10
|
+
/**
|
|
11
|
+
* Piece 2 §6.2 — Ink: the proposal goes to scrollback once; a confirm card
|
|
12
|
+
* holds the choices. `c` copies, shows "· Copied" and asks again (not a
|
|
13
|
+
* decision, as today). y/c/s/n and the words yes/skip/abort still work.
|
|
14
|
+
* Esc / no listener → 'aborted': stop, nothing is written. The cursor starts
|
|
15
|
+
* on Copy, so a bare Enter decides nothing (as a bare Enter does today).
|
|
16
|
+
*/
|
|
17
|
+
async function promptAdvisoryInk(args) {
|
|
18
|
+
emitTrace(args.displayText);
|
|
19
|
+
for (;;) {
|
|
20
|
+
const res = await askConfirm({
|
|
21
|
+
id: 'advisory-proposal',
|
|
22
|
+
tag: 'ADVISORY',
|
|
23
|
+
title: 'Did you apply this change in ADT or SE80?',
|
|
24
|
+
facts: ['The proposal is shown above.', 'Nothing is written to SAP from here.'],
|
|
25
|
+
choices: [
|
|
26
|
+
{ value: 'applied', label: ADVISORY_LABEL.applied },
|
|
27
|
+
{ value: 'copy', label: 'Copy to clipboard' },
|
|
28
|
+
{ value: 'skipped', label: ADVISORY_LABEL.skipped },
|
|
29
|
+
{ value: 'aborted', label: ADVISORY_LABEL.aborted },
|
|
30
|
+
],
|
|
31
|
+
initial: 1,
|
|
32
|
+
aliases: {
|
|
33
|
+
y: 'applied', yes: 'applied', applied: 'applied',
|
|
34
|
+
c: 'copy', copy: 'copy',
|
|
35
|
+
s: 'skipped', skip: 'skipped', skipped: 'skipped',
|
|
36
|
+
n: 'aborted', no: 'aborted', abort: 'aborted', aborted: 'aborted',
|
|
37
|
+
},
|
|
38
|
+
footnote: 'Advisory mode',
|
|
39
|
+
}, { trace: false });
|
|
40
|
+
if (res.value === 'copy') {
|
|
41
|
+
await clipboardy.write(args.clipboardText);
|
|
42
|
+
emitTrace(paint('dim', `${getGlyphs().info} Copied`));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const outcome = res.value === 'applied' ? 'applied' : res.value === 'skipped' ? 'skipped' : 'aborted';
|
|
46
|
+
emitTrace(formatDecisionTrace('Advisory proposal', ADVISORY_LABEL[outcome]));
|
|
47
|
+
return outcome;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
4
50
|
export async function promptAdvisory(args) {
|
|
51
|
+
if (shouldUseInk())
|
|
52
|
+
return promptAdvisoryInk(args);
|
|
5
53
|
output.write(args.displayText + '\n');
|
|
6
54
|
const rl = readline.createInterface({ input, output });
|
|
7
55
|
try {
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
|
|
2
|
+
import { paint } from '../renderer/theme.js';
|
|
3
|
+
import { uiWidth } from '../renderer/ui-width.js';
|
|
4
|
+
import { sanitizeControlChars as clean } from '../renderer/sanitize.js';
|
|
5
|
+
const ADVISORY_TITLE = ' ADVISORY MODE — proposed change (no SAP write will occur) ';
|
|
6
|
+
export const ADVISORY_KEY_HINT = 'Press c to copy to clipboard, y when applied, s to skip, n to abort.';
|
|
7
|
+
export function renderAdvisoryProposal(args, opts = {}) {
|
|
8
|
+
// Model text is shown with control characters made harmless (it must not
|
|
9
|
+
// drive the terminal); the clipboard copy keeps it verbatim.
|
|
3
10
|
const displayParts = [];
|
|
4
11
|
const clipboardParts = [];
|
|
5
|
-
|
|
6
|
-
|
|
12
|
+
// Borders line up: top and bottom share one width (§1.4 rule, widened
|
|
13
|
+
// only if the title itself would not fit).
|
|
14
|
+
const w = Math.max(uiWidth(), ADVISORY_TITLE.length + 6);
|
|
15
|
+
const top = '╔══' + ADVISORY_TITLE + '═'.repeat(w - 4 - ADVISORY_TITLE.length) + '╗';
|
|
16
|
+
const bottom = '╚' + '═'.repeat(w - 2) + '╝';
|
|
17
|
+
const rule = paint('dim', '─'.repeat(w));
|
|
18
|
+
displayParts.push(chalk.bold(paint('warn', top)));
|
|
19
|
+
displayParts.push(chalk.bold(`Summary: ${clean(args.summary)}`));
|
|
7
20
|
displayParts.push(`Risk: ${args.risk}`);
|
|
8
21
|
if (args.transport)
|
|
9
|
-
displayParts.push(`Target transport: ${args.transport}`);
|
|
22
|
+
displayParts.push(`Target transport: ${clean(args.transport)}`);
|
|
10
23
|
displayParts.push('');
|
|
11
24
|
clipboardParts.push(`# ${args.summary}`);
|
|
12
25
|
if (args.transport)
|
|
@@ -14,21 +27,22 @@ export function renderAdvisoryProposal(args) {
|
|
|
14
27
|
clipboardParts.push('');
|
|
15
28
|
args.changes.forEach((change, idx) => {
|
|
16
29
|
const header = `Change ${idx + 1}: ${change.op.toUpperCase()} ${change.type} ${change.object}`;
|
|
17
|
-
displayParts.push(
|
|
30
|
+
displayParts.push(paint('sap', clean(header)));
|
|
18
31
|
clipboardParts.push(`## ${header}`);
|
|
19
32
|
if (change.diff) {
|
|
20
|
-
displayParts.push(
|
|
21
|
-
displayParts.push(change.diff);
|
|
22
|
-
displayParts.push(
|
|
33
|
+
displayParts.push(rule);
|
|
34
|
+
displayParts.push(clean(change.diff));
|
|
35
|
+
displayParts.push(rule);
|
|
23
36
|
clipboardParts.push('');
|
|
24
37
|
clipboardParts.push(change.diff);
|
|
25
38
|
clipboardParts.push('');
|
|
26
39
|
}
|
|
27
40
|
});
|
|
28
41
|
displayParts.push('');
|
|
29
|
-
displayParts.push(
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
displayParts.push(paint('warn', 'Next step: open this object in ADT or SE80, apply the change, save and activate.'));
|
|
43
|
+
if (opts.interactiveHint !== false)
|
|
44
|
+
displayParts.push(paint('dim', ADVISORY_KEY_HINT));
|
|
45
|
+
displayParts.push(chalk.bold(paint('warn', bottom)));
|
|
32
46
|
clipboardParts.push('');
|
|
33
47
|
clipboardParts.push('# This is an advisory proposal. Apply manually in ADT/SE80.');
|
|
34
48
|
return {
|
|
@@ -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) {
|