@cspeach/cli 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -0
- package/README.md +108 -0
- package/dist/agent/anthropic-provider.js +59 -0
- package/dist/agent/llm-provider.js +1 -0
- package/dist/agent/loop.js +709 -0
- package/dist/agent/maybe-build-project-context.js +126 -0
- package/dist/agent/providers/ai-hub-provider.js +58 -0
- package/dist/agent/providers/byok-provider.js +53 -0
- package/dist/agent/providers/factory.js +13 -0
- package/dist/agent/providers/local-provider.js +125 -0
- package/dist/agent/repair-partial.js +31 -0
- package/dist/agent/retry-key.js +58 -0
- package/dist/agent/retry.js +17 -0
- package/dist/agent/sap-connection-adapter.js +82 -0
- package/dist/agent/skill-checkpoint.js +119 -0
- package/dist/agent/tool-dispatch.js +47 -0
- package/dist/agent/turn-assistant-text.js +49 -0
- package/dist/agent/turn-error-ux.js +126 -0
- package/dist/agent/turn-stream.js +79 -0
- package/dist/agent/turn-watchdog.js +71 -0
- package/dist/approvals/advisory-prompt.js +40 -0
- package/dist/approvals/advisory-render.js +38 -0
- package/dist/approvals/approval-prompt.js +100 -0
- package/dist/approvals/jwt.js +33 -0
- package/dist/approvals/render.js +211 -0
- package/dist/approvals/risk-floor.js +26 -0
- package/dist/auth/api-key.js +40 -0
- package/dist/auth/auth-file.js +59 -0
- package/dist/auth/device.js +8 -0
- package/dist/auth/me.js +19 -0
- package/dist/classifier/client.js +58 -0
- package/dist/cli-args.js +38 -0
- package/dist/cli.js +148 -0
- package/dist/commands/config-set.js +245 -0
- package/dist/commands/config-show.js +159 -0
- package/dist/commands/help.js +93 -0
- package/dist/commands/login.js +122 -0
- package/dist/commands/logout.js +17 -0
- package/dist/commands/project-context-impact.js +215 -0
- package/dist/commands/reroute.js +60 -0
- package/dist/commands/spec-gap-status.js +52 -0
- package/dist/commands/whoami.js +35 -0
- package/dist/config/loader.js +67 -0
- package/dist/config/paths.js +20 -0
- package/dist/doctor/checks/_http-probe.js +56 -0
- package/dist/doctor/checks/auth.js +15 -0
- package/dist/doctor/checks/cert.js +24 -0
- package/dist/doctor/checks/forge-rules.js +102 -0
- package/dist/doctor/checks/keychain-fallback.js +14 -0
- package/dist/doctor/checks/keychain.js +23 -0
- package/dist/doctor/checks/llm-mode.js +27 -0
- package/dist/doctor/checks/proxy.js +13 -0
- package/dist/doctor/checks/sap.js +33 -0
- package/dist/doctor/checks/skill.js +24 -0
- package/dist/doctor/checks/write-mode.js +34 -0
- package/dist/doctor/checks/zcspeach.js +76 -0
- package/dist/doctor/run.js +46 -0
- package/dist/errors/codes.js +12 -0
- package/dist/index.js +6 -0
- package/dist/lock-contention.js +22 -0
- package/dist/one-shot.js +104 -0
- package/dist/project-context/conventions.js +309 -0
- package/dist/project-context/detect.js +250 -0
- package/dist/project-context/domain/abap-cloud.js +26 -0
- package/dist/project-context/domain/abapgit.js +177 -0
- package/dist/project-context/domain/cap.js +164 -0
- package/dist/project-context/domain/fiori.js +326 -0
- package/dist/project-context/git.js +115 -0
- package/dist/project-context/index-files.js +235 -0
- package/dist/project-context/index.js +117 -0
- package/dist/project-context/render.js +308 -0
- package/dist/project-context/types.js +14 -0
- package/dist/projects/build.js +20 -0
- package/dist/projects/canonicalize.js +39 -0
- package/dist/projects/email-template.js +54 -0
- package/dist/projects/extract-cca.js +139 -0
- package/dist/projects/extract-design.js +107 -0
- package/dist/projects/extract-estimate.js +93 -0
- package/dist/projects/extract-modernize.js +130 -0
- package/dist/projects/extract-spec-gap.js +101 -0
- package/dist/projects/extract-test-coverage.js +137 -0
- package/dist/projects/extract-upgrade.js +230 -0
- package/dist/projects/filename.js +18 -0
- package/dist/projects/index.js +8 -0
- package/dist/projects/migration.js +111 -0
- package/dist/projects/promote-command.js +96 -0
- package/dist/projects/promote.js +107 -0
- package/dist/projects/save-command.js +124 -0
- package/dist/projects/save.js +21 -0
- package/dist/projects/status.js +170 -0
- package/dist/projects/types.js +1 -0
- package/dist/projects/validate.js +146 -0
- package/dist/projects/workspace.js +478 -0
- package/dist/renderer/abap-inline.js +121 -0
- package/dist/renderer/banners.js +39 -0
- package/dist/renderer/highlighters/abap.js +126 -0
- package/dist/renderer/highlighters/bdef.js +81 -0
- package/dist/renderer/highlighters/cds.js +91 -0
- package/dist/renderer/markdown.js +291 -0
- package/dist/renderer/pipeline.js +201 -0
- package/dist/renderer/progress-chatter.js +237 -0
- package/dist/renderer/question-normalizer.js +306 -0
- package/dist/renderer/severity.js +61 -0
- package/dist/renderer/status-footer.js +50 -0
- package/dist/renderer/syntax.js +58 -0
- package/dist/renderer/tables.js +55 -0
- package/dist/renderer/thinking-heartbeat.js +70 -0
- package/dist/renderer/tool-widget.js +199 -0
- package/dist/renderer/tty.js +66 -0
- package/dist/renderer/widget-extractor.js +87 -0
- package/dist/renderer/widget-fallback.js +78 -0
- package/dist/renderer/widget-schemas.js +43 -0
- package/dist/repl/at-completer.js +64 -0
- package/dist/repl/at-picker.js +122 -0
- package/dist/repl/bracketed-paste.js +284 -0
- package/dist/repl/current-transport.js +46 -0
- package/dist/repl/diff-display.js +41 -0
- package/dist/repl/file-picker.js +219 -0
- package/dist/repl/inquirer-guard.js +130 -0
- package/dist/repl/inquirer-theme.js +41 -0
- package/dist/repl/rule8-detector.js +99 -0
- package/dist/repl/safety-confirm.js +106 -0
- package/dist/repl/safety-mode-state.js +36 -0
- package/dist/repl/slash-completer.js +59 -0
- package/dist/repl/slash-picker.js +124 -0
- package/dist/repl/update-method-preview-hook.js +45 -0
- package/dist/repl.js +1383 -0
- package/dist/router/classifier.js +38 -0
- package/dist/router/intent-extractor.js +140 -0
- package/dist/router/routing-decision.js +19 -0
- package/dist/sap/connection-manager.js +52 -0
- package/dist/sap/onboarding.js +178 -0
- package/dist/sap/system-info.js +515 -0
- package/dist/session/awaiting-answer.js +73 -0
- package/dist/session/gc.js +28 -0
- package/dist/session/pending.js +37 -0
- package/dist/session/resume.js +77 -0
- package/dist/session/schema.js +20 -0
- package/dist/session/store.js +147 -0
- package/dist/session/time-ago.js +41 -0
- package/dist/skill-catalog.js +222 -0
- package/dist/skills/bundled-skills.js +1 -0
- package/dist/skills/canonical.js +12 -0
- package/dist/skills/manifest-client.js +93 -0
- package/dist/skills/promotion-dispatch.js +24 -0
- package/dist/skills/signing-public-key.js +4 -0
- package/dist/skills/source-bundled.js +20 -0
- package/dist/skills/source-managed.js +26 -0
- package/dist/skills/source-manifest.js +26 -0
- package/dist/tools/_command-shared.js +110 -0
- package/dist/tools/_filesystem-shared.js +81 -0
- package/dist/tools/_flag.js +39 -0
- package/dist/tools/approval.js +228 -0
- package/dist/tools/ask-question.js +205 -0
- package/dist/tools/dispatch-skill.js +81 -0
- package/dist/tools/filesystem/file-edit.js +140 -0
- package/dist/tools/filesystem/file-read.js +89 -0
- package/dist/tools/filesystem/file-write.js +128 -0
- package/dist/tools/filesystem/glob.js +177 -0
- package/dist/tools/filesystem/grep.js +163 -0
- package/dist/tools/index.js +32 -0
- package/dist/tools/project/convention_get.js +91 -0
- package/dist/tools/project/playbook_get.js +132 -0
- package/dist/tools/project/project_context_get.js +101 -0
- package/dist/tools/sap-read.js +454 -0
- package/dist/tools/sap-write.js +746 -0
- package/dist/tools/shell/shell_exec.js +209 -0
- package/dist/tools/snapshot.js +107 -0
- package/dist/tools/subagent/_background-shared.js +133 -0
- package/dist/tools/subagent/agent_run.js +186 -0
- package/dist/tools/subagent/background_run.js +143 -0
- package/dist/tools/subagent/monitor_emit.js +65 -0
- package/dist/tools/subagent/schedule_create.js +131 -0
- package/dist/tools/transport.js +233 -0
- package/dist/tools/update-method-intercept.js +119 -0
- package/dist/tools/verify.js +39 -0
- package/dist/tools/web/_web-shared.js +251 -0
- package/dist/tools/web/web_fetch.js +257 -0
- package/dist/tools/web/web_search.js +195 -0
- package/dist/tools/write-mode.js +22 -0
- package/dist/ui/app.js +95 -0
- package/dist/ui/approval-emitter.js +10 -0
- package/dist/ui/approval-modal.js +53 -0
- package/dist/ui/ascii-chars.js +6 -0
- package/dist/ui/body.js +102 -0
- package/dist/ui/coaching-picker-classic.js +36 -0
- package/dist/ui/coaching-picker-emitter.js +27 -0
- package/dist/ui/command-palette.js +34 -0
- package/dist/ui/error-emitter.js +21 -0
- package/dist/ui/footer.js +103 -0
- package/dist/ui/header.js +17 -0
- package/dist/ui/ink-classifier-route.js +19 -0
- package/dist/ui/login-banner.js +72 -0
- package/dist/ui/rich-error-box.js +9 -0
- package/dist/ui/sap-state-store.js +65 -0
- package/dist/ui/session-timeline.js +31 -0
- package/dist/ui/sidebar.js +10 -0
- package/dist/ui/skill-picker.js +50 -0
- package/dist/ui/status-row.js +12 -0
- package/dist/ui/widget-control.js +4 -0
- package/dist/ui/widgets/bar-chart.js +15 -0
- package/dist/ui/widgets/coaching-picker.js +41 -0
- package/dist/ui/widgets/component-registry.js +12 -0
- package/dist/ui/widgets/dep-graph.js +9 -0
- package/dist/ui/widgets/diff-viewer.js +11 -0
- package/dist/ui/widgets/question-card.js +11 -0
- package/dist/ui/widgets/stack-frames.js +5 -0
- package/dist/upgrade-check.js +28 -0
- package/dist/upgrade.js +13 -0
- package/package.json +83 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SignJWT, jwtVerify } from 'jose';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
// Session-scoped HMAC key — generated per CLI start, never persisted.
|
|
4
|
+
const sessionKey = crypto.randomBytes(32);
|
|
5
|
+
const usedNonces = new Set();
|
|
6
|
+
export async function mintApprovalId(payload) {
|
|
7
|
+
const nonce = crypto.randomBytes(16).toString('hex');
|
|
8
|
+
return new SignJWT({ ...payload, nonce })
|
|
9
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
10
|
+
.setIssuedAt()
|
|
11
|
+
.setExpirationTime('5m')
|
|
12
|
+
.sign(sessionKey);
|
|
13
|
+
}
|
|
14
|
+
export async function verifyAndSpendApprovalId(jwt, expectedObject, expectedOp) {
|
|
15
|
+
try {
|
|
16
|
+
const { payload } = await jwtVerify(jwt, sessionKey);
|
|
17
|
+
const p = payload;
|
|
18
|
+
if (usedNonces.has(p.nonce))
|
|
19
|
+
return { ok: false, reason: 'nonce_spent' };
|
|
20
|
+
if (p.object !== expectedObject)
|
|
21
|
+
return { ok: false, reason: 'object_mismatch' };
|
|
22
|
+
if (p.op !== expectedOp)
|
|
23
|
+
return { ok: false, reason: 'op_mismatch' };
|
|
24
|
+
usedNonces.add(p.nonce);
|
|
25
|
+
return { ok: true, payload: p };
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (String(err?.code).includes('JWTExpired') || String(err?.message).toLowerCase().includes('expired')) {
|
|
29
|
+
return { ok: false, reason: 'expired' };
|
|
30
|
+
}
|
|
31
|
+
return { ok: false, reason: 'invalid_signature' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { select, input } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { shouldUseInk } from '../renderer/tty.js';
|
|
4
|
+
import { approvalEmitter } from '../ui/approval-emitter.js';
|
|
5
|
+
import { withInquirer } from '../repl/inquirer-guard.js';
|
|
6
|
+
import { inquirerTheme } from '../repl/inquirer-theme.js';
|
|
7
|
+
import { PEACH } from '../renderer/banners.js';
|
|
8
|
+
const peach = (s) => chalk.hex(PEACH)(s);
|
|
9
|
+
/**
|
|
10
|
+
* Op-row glyph. 2026-05-01: dropped the per-op color spread (yellow M,
|
|
11
|
+
* green A, blue >, magenta ^). Using 5 different hues for routine plan
|
|
12
|
+
* rows trains the user to ignore color as semantic signal — by the time
|
|
13
|
+
* an actual delete shows up, red has already appeared on three other
|
|
14
|
+
* lines and lost its punch. Now: only `D` (delete) gets red, because
|
|
15
|
+
* delete is the only op that's destructive enough to deserve a hot
|
|
16
|
+
* accent. Everything else renders dim — the row layout already tells
|
|
17
|
+
* the user it's a change row.
|
|
18
|
+
*/
|
|
19
|
+
function opIcon(op) {
|
|
20
|
+
switch (op) {
|
|
21
|
+
case 'create': return chalk.dim('A');
|
|
22
|
+
case 'modify': return chalk.dim('M');
|
|
23
|
+
case 'delete': return chalk.red('D');
|
|
24
|
+
case 'activate': return chalk.dim('>');
|
|
25
|
+
case 'release': return chalk.dim('^');
|
|
26
|
+
default: return chalk.dim('?');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Risk tag. 2026-05-01: yellow used to fire on `medium` too, which
|
|
31
|
+
* meant most plans had a yellow tag — defeating yellow's purpose as
|
|
32
|
+
* an "attention required" signal. Now: yellow ONLY on `high`,
|
|
33
|
+
* everything else dim. (Risk is also raised to red.bold for high so
|
|
34
|
+
* it pops without yellow doing extra work.)
|
|
35
|
+
*/
|
|
36
|
+
function riskTag(r) {
|
|
37
|
+
if (r === 'high')
|
|
38
|
+
return chalk.red.bold('high risk');
|
|
39
|
+
return chalk.dim(r === 'medium' ? 'medium risk' : 'low risk');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Colorize a unified-diff string for terminal display:
|
|
43
|
+
* - `+ …` (additions) → green
|
|
44
|
+
* - `- …` (removals) → red
|
|
45
|
+
* - `@@ …` (hunk headers) → peach (brand)
|
|
46
|
+
* - `--- …` / `+++ …` headers → dim (informational, not part of the change)
|
|
47
|
+
* - context lines → dim
|
|
48
|
+
*
|
|
49
|
+
* Operates line-by-line so a long diff stays readable. The leading +/- char
|
|
50
|
+
* is part of the colored span, matching standard `git diff` rendering.
|
|
51
|
+
*/
|
|
52
|
+
function colorizeDiff(diff) {
|
|
53
|
+
return diff
|
|
54
|
+
.split('\n')
|
|
55
|
+
.map((line) => {
|
|
56
|
+
if (line.startsWith('+++') || line.startsWith('---'))
|
|
57
|
+
return chalk.dim(line);
|
|
58
|
+
if (line.startsWith('@@'))
|
|
59
|
+
return peach(line);
|
|
60
|
+
if (line.startsWith('+'))
|
|
61
|
+
return chalk.green(line);
|
|
62
|
+
if (line.startsWith('-'))
|
|
63
|
+
return chalk.red(line);
|
|
64
|
+
return chalk.dim(line);
|
|
65
|
+
})
|
|
66
|
+
.join('\n');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Plan-level gate. Renders a compact summary of the change set and asks the
|
|
70
|
+
* user to apply-all, review-each, or cancel. Apply-all is the default —
|
|
71
|
+
* matches how users actually operate ("yes, do it") and skips the redundant
|
|
72
|
+
* per-change prompts that were the main "so much confusion" complaint.
|
|
73
|
+
*
|
|
74
|
+
* Color discipline:
|
|
75
|
+
* - Peach (PEACH) for the brand-bearing "Plan" header.
|
|
76
|
+
* - Default text for the summary, change list. No cyan, no bold-on-bold.
|
|
77
|
+
* - Yellow tag for medium risk, red for high. Low is dim — no shouting.
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* Collapse the changes list for *display purposes only* — `activate` is the
|
|
81
|
+
* implicit follow-on of a `modify`/`create` on the same object, so we hide
|
|
82
|
+
* the activate row when its sibling write is already in the list. The
|
|
83
|
+
* underlying `changes[]` (which drives approval-id minting and per-change
|
|
84
|
+
* approval prompts) is unchanged — only the user-facing plan summary is
|
|
85
|
+
* compressed. This is what fixes the "what 2 changes?" complaint when the
|
|
86
|
+
* model declares modify+activate of the same object.
|
|
87
|
+
*/
|
|
88
|
+
function collapseImplicitActivates(changes) {
|
|
89
|
+
const writeKeys = new Set();
|
|
90
|
+
for (const c of changes) {
|
|
91
|
+
if (c.op === 'modify' || c.op === 'create') {
|
|
92
|
+
writeKeys.add(`${c.type}:${c.object}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return changes.filter((c) => {
|
|
96
|
+
if (c.op !== 'activate')
|
|
97
|
+
return true;
|
|
98
|
+
return !writeKeys.has(`${c.type}:${c.object}`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export async function renderPlanGate(summary, changes, transport, effectiveRisk) {
|
|
102
|
+
const visibleChanges = collapseImplicitActivates(changes);
|
|
103
|
+
const visibleCount = visibleChanges.length;
|
|
104
|
+
const noun = visibleCount === 1 ? 'change' : 'changes';
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(`${peach('Plan')} ${summary}`);
|
|
107
|
+
const meta = [`${visibleCount} ${noun}`, riskTag(effectiveRisk.level)];
|
|
108
|
+
if (transport)
|
|
109
|
+
meta.push(`transport ${transport}`);
|
|
110
|
+
console.log(` ${chalk.dim(meta.join(' · '))}`);
|
|
111
|
+
if (effectiveRisk.raisedBy.length > 0) {
|
|
112
|
+
console.log(` ${chalk.dim(`(raised from low: ${effectiveRisk.raisedBy.join(', ')})`)}`);
|
|
113
|
+
}
|
|
114
|
+
for (const c of visibleChanges) {
|
|
115
|
+
console.log(` ${opIcon(c.op)} ${c.object.padEnd(30)} ${chalk.dim(`(${c.type})`)}`);
|
|
116
|
+
}
|
|
117
|
+
console.log('');
|
|
118
|
+
const choice = await withInquirer(() => select({
|
|
119
|
+
message: 'Apply?',
|
|
120
|
+
default: 'apply_all',
|
|
121
|
+
theme: inquirerTheme,
|
|
122
|
+
choices: [
|
|
123
|
+
{ value: 'apply_all', name: `Apply all ${visibleCount} ${noun}` },
|
|
124
|
+
{ value: 'review', name: 'Review each change individually' },
|
|
125
|
+
{ value: 'cancel', name: 'Cancel' },
|
|
126
|
+
],
|
|
127
|
+
}));
|
|
128
|
+
return choice;
|
|
129
|
+
}
|
|
130
|
+
export async function renderPerChangeApproval(change, effectiveRisk, transport) {
|
|
131
|
+
// No "Approval requested" preamble — the inquirer message below carries the
|
|
132
|
+
// change identifier. The risk + transport line was redundant with what
|
|
133
|
+
// renderPlanGate already showed, and was the main source of "why so much
|
|
134
|
+
// text for one prompt" feedback. Show it ONLY when this per-change call
|
|
135
|
+
// fires standalone (single-change request_approval) AND risk is non-low.
|
|
136
|
+
void effectiveRisk;
|
|
137
|
+
void transport;
|
|
138
|
+
const objLabel = `${opIcon(change.op)} ${change.object} (${change.type})`;
|
|
139
|
+
const choice = await withInquirer(() => select({
|
|
140
|
+
message: `${objLabel} — apply?`,
|
|
141
|
+
default: 'apply',
|
|
142
|
+
theme: inquirerTheme,
|
|
143
|
+
choices: [
|
|
144
|
+
{ value: 'apply', name: 'Apply' },
|
|
145
|
+
{ value: 'diff', name: 'Show diff' },
|
|
146
|
+
{ value: 'skip', name: 'Skip (tell model why)' },
|
|
147
|
+
{ value: 'cancel', name: 'Cancel turn' },
|
|
148
|
+
],
|
|
149
|
+
}));
|
|
150
|
+
if (choice === 'apply')
|
|
151
|
+
return { approved: true };
|
|
152
|
+
if (choice === 'cancel')
|
|
153
|
+
return { approved: false, reason: '__cancel_turn__' };
|
|
154
|
+
if (choice === 'skip') {
|
|
155
|
+
const reason = await withInquirer(() => input({ message: 'Why skip?', theme: inquirerTheme }));
|
|
156
|
+
return { approved: false, reason };
|
|
157
|
+
}
|
|
158
|
+
// 'diff' — print colorized + lines (green) / - lines (red) / hunk headers
|
|
159
|
+
// (peach) so the user can scan the change at a glance instead of squinting
|
|
160
|
+
// at flat-gray text.
|
|
161
|
+
//
|
|
162
|
+
// 2026-05-01: detect "model sent a description, not a diff" — when the
|
|
163
|
+
// payload has no +/-/@ lines and no `--- /+++ ` headers, colorizeDiff
|
|
164
|
+
// would render every line in dim grey, making the summary look like a
|
|
165
|
+
// useless diff and forcing the user to hit Skip in frustration. Show an
|
|
166
|
+
// explicit warning instead so the user knows the model didn't supply
|
|
167
|
+
// proper diff content.
|
|
168
|
+
if (change.diff) {
|
|
169
|
+
if (looksLikeUnifiedDiff(change.diff)) {
|
|
170
|
+
console.log('\n' + colorizeDiff(change.diff) + '\n');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.log('\n' + chalk.yellow('⚠ Model did not provide a unified diff — only a description:'));
|
|
174
|
+
console.log(chalk.dim(change.diff) + '\n');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.log(chalk.dim('(No diff provided by the model)'));
|
|
179
|
+
}
|
|
180
|
+
return renderPerChangeApproval(change, effectiveRisk, transport);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Heuristic: a real unified diff has at least one line starting with `+`,
|
|
184
|
+
* `-`, or `@@`. A summary or change description won't. Used to gate
|
|
185
|
+
* colorizeDiff vs the "model didn't supply a diff" warning so the Show diff
|
|
186
|
+
* option doesn't return useless dim grey text when the model passed a
|
|
187
|
+
* one-paragraph description in the `diff` field.
|
|
188
|
+
*/
|
|
189
|
+
function looksLikeUnifiedDiff(s) {
|
|
190
|
+
return s.split('\n').some((line) => {
|
|
191
|
+
if (line.startsWith('+++') || line.startsWith('---'))
|
|
192
|
+
return false;
|
|
193
|
+
return line.startsWith('+') || line.startsWith('-') || line.startsWith('@@');
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* v0.3 dispatcher: routes to Ink modal (via approvalEmitter) when running
|
|
198
|
+
* under Ink, else delegates to the existing inquirer-based flow.
|
|
199
|
+
*/
|
|
200
|
+
export async function renderPerChangeApprovalV3(change, riskResult, transport) {
|
|
201
|
+
if (!shouldUseInk()) {
|
|
202
|
+
// Non-TTY: delegate to existing inquirer flow.
|
|
203
|
+
return renderPerChangeApproval(change, riskResult, transport);
|
|
204
|
+
}
|
|
205
|
+
return renderApprovalModalAndAwait(change, riskResult, transport);
|
|
206
|
+
}
|
|
207
|
+
async function renderApprovalModalAndAwait(change, riskResult, transport) {
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
approvalEmitter.emit('request', { change, effectiveRisk: riskResult, transport, resolve });
|
|
210
|
+
});
|
|
211
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function effectiveRisk(declared, changes, ctx) {
|
|
2
|
+
let floor = declared;
|
|
3
|
+
const raisedBy = [];
|
|
4
|
+
const rank = (r) => ({ low: 0, medium: 1, high: 2 }[r]);
|
|
5
|
+
const raise = (r, label) => {
|
|
6
|
+
if (rank(r) > rank(floor)) {
|
|
7
|
+
floor = r;
|
|
8
|
+
raisedBy.push(label);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
if (ctx.autoApprovePolicy === 'never')
|
|
12
|
+
raise('high', 'non_prod_override');
|
|
13
|
+
if (changes.some((c) => c.op === 'delete'))
|
|
14
|
+
raise('medium', 'delete_op');
|
|
15
|
+
if (changes.some((c) => c.op === 'release' || c.op === 'activate'))
|
|
16
|
+
raise('medium', 'shared_transport');
|
|
17
|
+
if (ctx.recentAtcFailure && changes.some((c) => ctx.recentAtcFailure.includes(c.object)))
|
|
18
|
+
raise('medium', 'atc_failed_24h');
|
|
19
|
+
if ((ctx.transportRecentDevelopers ?? 0) >= 3)
|
|
20
|
+
raise('medium', 'shared_transport');
|
|
21
|
+
if (changes.length > 5)
|
|
22
|
+
raise('medium', 'escalated_in_session');
|
|
23
|
+
if (ctx.prodNamespaces && changes.some((c) => ctx.prodNamespaces.some((p) => c.object.startsWith(p))))
|
|
24
|
+
raise('medium', 'prod_namespace');
|
|
25
|
+
return { level: floor, raisedBy };
|
|
26
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import keytar from 'keytar';
|
|
2
|
+
const SERVICE = 'cspeach';
|
|
3
|
+
const ACCOUNT = 'api-key';
|
|
4
|
+
class KeytarStore {
|
|
5
|
+
async get() { return keytar.getPassword(SERVICE, ACCOUNT); }
|
|
6
|
+
async set(key) { await keytar.setPassword(SERVICE, ACCOUNT, key); }
|
|
7
|
+
async delete() { await keytar.deletePassword(SERVICE, ACCOUNT); }
|
|
8
|
+
async isKeychainAvailable() {
|
|
9
|
+
try {
|
|
10
|
+
const probe = `probe-${Date.now()}`;
|
|
11
|
+
await keytar.setPassword(SERVICE, 'probe', probe);
|
|
12
|
+
const ok = (await keytar.getPassword(SERVICE, 'probe')) === probe;
|
|
13
|
+
await keytar.deletePassword(SERVICE, 'probe');
|
|
14
|
+
return ok;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
class EphemeralStore {
|
|
22
|
+
cached = null;
|
|
23
|
+
async get() { return this.cached; }
|
|
24
|
+
async set(key) { this.cached = key; }
|
|
25
|
+
async delete() { this.cached = null; }
|
|
26
|
+
async isKeychainAvailable() { return false; }
|
|
27
|
+
}
|
|
28
|
+
let store;
|
|
29
|
+
export async function getStore() {
|
|
30
|
+
if (store)
|
|
31
|
+
return store;
|
|
32
|
+
const ktr = new KeytarStore();
|
|
33
|
+
if (await ktr.isKeychainAvailable()) {
|
|
34
|
+
store = ktr;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
store = new EphemeralStore();
|
|
38
|
+
}
|
|
39
|
+
return store;
|
|
40
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read/write ~/.cspeach/auth.json — the customer's CSPeach API key
|
|
3
|
+
* + optional Anthropic key for BYOK trials.
|
|
4
|
+
*
|
|
5
|
+
* Permissions: 0600 on POSIX. Windows uses NTFS ACLs which Node's
|
|
6
|
+
* fs mode flag doesn't translate; we rely on the user-dir default
|
|
7
|
+
* permissions on Windows and recommend OS keychain (v2 roadmap)
|
|
8
|
+
* for that platform.
|
|
9
|
+
*
|
|
10
|
+
* Coexists with the existing keytar-based auth/api-key.ts. The
|
|
11
|
+
* device-code flow (cspeach login) writes here; older config-set
|
|
12
|
+
* paths still write to keytar. Task 9 wires the REPL to read here
|
|
13
|
+
* first, fall back to keytar.
|
|
14
|
+
*/
|
|
15
|
+
import { promises as fs } from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { cspeachRoot } from '../config/paths.js';
|
|
18
|
+
let pathOverride = null;
|
|
19
|
+
/** Test-only override; pass null to reset. */
|
|
20
|
+
export function __setAuthFilePathForTests(p) {
|
|
21
|
+
pathOverride = p;
|
|
22
|
+
}
|
|
23
|
+
function authFilePath() {
|
|
24
|
+
return pathOverride ?? path.join(cspeachRoot(), 'auth.json');
|
|
25
|
+
}
|
|
26
|
+
export async function readAuthFile() {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await fs.readFile(authFilePath(), 'utf-8');
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (typeof parsed.apiKey !== 'string' || typeof parsed.customerId !== 'string')
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
apiKey: parsed.apiKey,
|
|
34
|
+
customerId: parsed.customerId,
|
|
35
|
+
byokAnthropicKey: typeof parsed.byokAnthropicKey === 'string' ? parsed.byokAnthropicKey : null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function writeAuthFile(auth) {
|
|
43
|
+
const p = authFilePath();
|
|
44
|
+
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
45
|
+
await fs.writeFile(p, JSON.stringify(auth, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
46
|
+
// Defensive: re-chmod in case the create-time mode was ignored (e.g., umask 022).
|
|
47
|
+
if (process.platform !== 'win32') {
|
|
48
|
+
try {
|
|
49
|
+
await fs.chmod(p, 0o600);
|
|
50
|
+
}
|
|
51
|
+
catch { /* ignore */ }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function deleteAuthFile() {
|
|
55
|
+
try {
|
|
56
|
+
await fs.unlink(authFilePath());
|
|
57
|
+
}
|
|
58
|
+
catch { /* ignore */ }
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// node-machine-id is CJS — import the default, then destructure.
|
|
2
|
+
import nodeMachineId from 'node-machine-id';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
const { machineId } = nodeMachineId;
|
|
5
|
+
export async function getDeviceFingerprint() {
|
|
6
|
+
const mid = await machineId(true).catch(() => 'unknown');
|
|
7
|
+
return { machineId: mid, hostname: os.hostname() };
|
|
8
|
+
}
|
package/dist/auth/me.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch /v1/me — customer profile + entitlements + usage.
|
|
3
|
+
* Result envelope so the REPL doesn't need try/catch.
|
|
4
|
+
*/
|
|
5
|
+
export async function fetchMe(opts) {
|
|
6
|
+
const url = `${opts.proxyUrl.replace(/\/$/, '')}/v1/me`;
|
|
7
|
+
try {
|
|
8
|
+
const r = await fetch(url, { headers: { Authorization: `Bearer ${opts.apiKey}` } });
|
|
9
|
+
if (!r.ok) {
|
|
10
|
+
const body = await r.json().catch(() => ({}));
|
|
11
|
+
return { ok: false, status: r.status, error: body.error ?? 'unknown' };
|
|
12
|
+
}
|
|
13
|
+
const profile = await r.json();
|
|
14
|
+
return { ok: true, profile };
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return { ok: false, status: 0, error: err instanceof Error ? err.message : String(err) };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const RULES = [
|
|
2
|
+
{ skill: 'abap-atc-fix', patterns: [/atc\s+(fix|auto\s+fix)|abap\s+test\s+cockpit.*fix/i] },
|
|
3
|
+
{ skill: 'abap-cca', patterns: [/custom\s+code\s+analysis|\bcca\b|clean\s+core\s+(assessment|analysis)/i] },
|
|
4
|
+
{ skill: 'abap-cloud-check', patterns: [/cloud\s+ready|clean\s+core(?!\s+(assessment|analysis))|s\/4hana\s+cloud\s+check/i] },
|
|
5
|
+
{ skill: 'abap-data-model', patterns: [/data\s+model|ddic\s+(design|structure)|domain|data\s+element|create\s+(a\s+)?z[_\w]*\s+(table|structure)|new\s+(z[_\w]*\s+)?(table|structure|domain)/i] },
|
|
6
|
+
{ skill: 'abap-design', patterns: [/solution\s+architecture|object\s+list|design\s+the\s+solution/i] },
|
|
7
|
+
{ skill: 'abap-handover', patterns: [/technical\s+documentation|object\s+catalog|doc(ument)?\s+package|handover/i] },
|
|
8
|
+
{ skill: 'abap-document', patterns: [/developer\s+documentation|document\s+this\s+(class|program|object)/i] },
|
|
9
|
+
// abap-incident MUST precede dump rules — pipeline framing beats keyword-only matches
|
|
10
|
+
{ skill: 'abap-incident', patterns: [/incident|resolve.*dump.*to.*fix|pipeline.*(short\s+dump|st22|dump)|(dump|st22).*to.*transport|formal\s+(incident|dump)\s+report|end.to.end.*dump/i] },
|
|
11
|
+
{ skill: 'abap-dump-triage', patterns: [/dump\s+triage|structured\s+investigation.*dump|dump\s+forensic/i] },
|
|
12
|
+
{ skill: 'abap-dump-analyze', patterns: [/short\s+dump|runtime\s+error|\bst22\b|dump\s+analys/i] },
|
|
13
|
+
{ skill: 'abap-eml', patterns: [/\beml\b|entity\s+manipulation|behavior\s+handler/i] },
|
|
14
|
+
{ skill: 'abap-enhance', patterns: [/\bbadi\b|enhancement\s+spot|implicit\s+enhancement|customer\s+exit|user\s+exit|(hook\s+into|extend)\s+(sap|standard|me\d+|migo|miro|va\d+|vf\d+|vl\d+|fb\d+)/i] },
|
|
15
|
+
{ skill: 'abap-estimate', patterns: [/effort\s+estimat|ticket\s+estimat|size\s+this\s+(work|task|ticket|story)|how\s+(many|long).*(hours|days).*(build|develop|implement)/i] },
|
|
16
|
+
{ skill: 'abap-explain', patterns: [/\bexplain\b|what\s+does\s+this\s+(do|code)|walk.*through/i] },
|
|
17
|
+
// abap-test MUST precede abap-generate so "generate tests" routes to the test skill
|
|
18
|
+
// (test-gen merged into abap-test on 2026-05-02 — phrases preserved here).
|
|
19
|
+
{ skill: 'abap-test', patterns: [/unit\s+test|abap\s+unit|test\s+(class|doubles)|generate\s+tests|test\s+generation|improve\s+test\s+coverage/i] },
|
|
20
|
+
// abap-modernize MUST precede abap-rap so "rebuild as RAP" / "convert to RAP" stays
|
|
21
|
+
// on modernize (rebuild intent) instead of falling to abap-rap's bare \brap\b match.
|
|
22
|
+
{ skill: 'abap-modernize', patterns: [/modernise|modernize|classic.*to.*modern|convert.*to\s+(rap|fiori|odata)|rebuild.*as\s+(rap|fiori)/i] },
|
|
23
|
+
// abap-rap MUST precede abap-generate so RAP-stack prompts route to rap, not the
|
|
24
|
+
// generic generator (cluster I, 2026-05-03). Also captures "scaffold a CDS view +
|
|
25
|
+
// BDEF + service binding" type prompts which collide with abap-generate.
|
|
26
|
+
{ skill: 'abap-rap', patterns: [/\brap\b|business\s+object(?!s?\s+for)|bdef|service\s+binding|srvb|scaffold.*(cds|odata\s+v4|bdef|behavior)|odata\s+v4\s+service/i] },
|
|
27
|
+
{ skill: 'abap-generate', patterns: [/\bgenerate\b|create\s+(a\s+)?(new\s+)?(class|program|report|include)|build.*from\s+scratch/i] },
|
|
28
|
+
{ skill: 'abap-impact', patterns: [/impact\s+analysis|where.*used|what.*affected\s+by/i] },
|
|
29
|
+
// abap-upgrade-* must precede abap-migrate
|
|
30
|
+
{ skill: 'abap-upgrade-verify', patterns: [/upgrade\s+verify|remediation\s+report|verify\s+upgrade/i] },
|
|
31
|
+
{ skill: 'abap-upgrade-fix', patterns: [/upgrade\s+fix|atc\s+remediation|fix\s+upgrade\s+finding/i] },
|
|
32
|
+
{ skill: 'abap-upgrade-scan', patterns: [/upgrade\s+scan|scan.*s\/4|readiness\s+variant/i] },
|
|
33
|
+
{ skill: 'abap-migrate', patterns: [/ecc.*migration|migrate.*s\/4|api\s+successor/i] },
|
|
34
|
+
{ skill: 'abap-performance', patterns: [/performance|\bn\+1\b|select\s+inside\s+loop|slow\s+(query|select)/i] },
|
|
35
|
+
{ skill: 'abap-preflight', patterns: [/preflight|pre.release|go\/no.go|release\s+readiness/i] },
|
|
36
|
+
{ skill: 'abap-radar', patterns: [/radar|quick\s+look|overview\s+of\s+(this\s+)?(class|program|object)/i] },
|
|
37
|
+
{ skill: 'abap-refactor', patterns: [/refactor|clean\s+up\s+(code|abap)|procedural.*to\s+oo/i] },
|
|
38
|
+
{ skill: 'abap-review', patterns: [/^review|code\s+review|code\s+quality|clean\s+abap(?!\s+conformity)/i] },
|
|
39
|
+
{ skill: 'abap-segw', patterns: [/segw|dpc_ext|mpc_ext|gateway\s+(project|service)|build.*odata.*(ecc|gateway)/i] },
|
|
40
|
+
{ skill: 'abap-spec-gap', patterns: [/spec\s+gap|missing\s+(spec|requirements)|business\s+questions|clarif(y|ying)\s+(question|requirement)|gaps?\s+in\s+the\s+(spec|story|requirement)/i] },
|
|
41
|
+
// abap-transport-analysis MUST precede abap-transport
|
|
42
|
+
{ skill: 'abap-transport-analysis', patterns: [/transport\s+analysis|pre.release\s+transport|transport\s+readiness/i] },
|
|
43
|
+
{ skill: 'abap-transport', patterns: [/transport\s+(create|release|list)|\bcts\b|release.*request/i] },
|
|
44
|
+
];
|
|
45
|
+
if (RULES.length !== 32) {
|
|
46
|
+
// Warn-not-throw — managed-mode users never exercise classify() (the proxy
|
|
47
|
+
// does server-side classification), so a module-load crash would break
|
|
48
|
+
// them for a local-only consistency check. Unit tests assert the exact
|
|
49
|
+
// count.
|
|
50
|
+
console.warn(`[classifier] RULES drift: expected 32, got ${RULES.length} — reconcile with .claude/skills/`);
|
|
51
|
+
}
|
|
52
|
+
export function classify(prompt) {
|
|
53
|
+
for (const rule of RULES) {
|
|
54
|
+
if (rule.patterns.some(p => p.test(prompt)))
|
|
55
|
+
return rule.skill;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
package/dist/cli-args.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure argument-parsing helpers for the CSPeach CLI entry point.
|
|
3
|
+
* Extracted for unit-testability — no side effects, no I/O.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Scan `args` for `--sap-alias <name>`.
|
|
7
|
+
*
|
|
8
|
+
* - If the flag is absent: returns `{ rest: args }` (no-op).
|
|
9
|
+
* - If the flag is present and the alias is in `availableAliases`: strips the
|
|
10
|
+
* two tokens and returns `{ sapAlias, rest }`.
|
|
11
|
+
* - If the alias is NOT in `availableAliases`: returns `{ error, rest: args }`.
|
|
12
|
+
* - If `--sap-alias` is the last token (no name follows): returns `{ error, rest: args }`.
|
|
13
|
+
*/
|
|
14
|
+
export function extractSapAliasFlag(args, availableAliases) {
|
|
15
|
+
const idx = args.indexOf('--sap-alias');
|
|
16
|
+
if (idx === -1) {
|
|
17
|
+
return { rest: args };
|
|
18
|
+
}
|
|
19
|
+
// Flag present but no value after it
|
|
20
|
+
if (idx === args.length - 1) {
|
|
21
|
+
return {
|
|
22
|
+
rest: args,
|
|
23
|
+
error: 'error: --sap-alias requires a name argument. Usage: cspeach --sap-alias <name>',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const alias = args[idx + 1];
|
|
27
|
+
// Unknown alias
|
|
28
|
+
if (availableAliases.length > 0 && !availableAliases.includes(alias)) {
|
|
29
|
+
const list = availableAliases.join(', ');
|
|
30
|
+
return {
|
|
31
|
+
rest: args,
|
|
32
|
+
error: `error: alias '${alias}' not found in config. Available: ${list}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// Strip the two tokens from args
|
|
36
|
+
const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
|
|
37
|
+
return { sapAlias: alias, rest };
|
|
38
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { runAddSapWizard } from './sap/onboarding.js';
|
|
3
|
+
import { runGcOnStartup } from './session/gc.js';
|
|
4
|
+
import { checkForUpgrade } from './upgrade-check.js';
|
|
5
|
+
import { extractSapAliasFlag } from './cli-args.js';
|
|
6
|
+
export async function main(argv) {
|
|
7
|
+
runGcOnStartup().catch(() => { });
|
|
8
|
+
checkForUpgrade().catch(() => { });
|
|
9
|
+
let args = argv.slice(2);
|
|
10
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
11
|
+
printHelp();
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
if (args[0] === '--version') {
|
|
15
|
+
console.log(process.env.npm_package_version ?? '0.5.0');
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
if (args[0] === '--upgrade') {
|
|
19
|
+
const { runUpgrade } = await import('./upgrade.js');
|
|
20
|
+
return runUpgrade();
|
|
21
|
+
}
|
|
22
|
+
if (args[0] === '--resume') {
|
|
23
|
+
const { runResume } = await import('./session/resume.js');
|
|
24
|
+
return runResume(args[1]);
|
|
25
|
+
}
|
|
26
|
+
// C.1 — extract --sap-alias <name> early so it is available for REPL and
|
|
27
|
+
// one-shot paths. Validation requires loading config to know available aliases.
|
|
28
|
+
let sapAlias;
|
|
29
|
+
if (args.includes('--sap-alias')) {
|
|
30
|
+
const { loadConfig } = await import('./config/loader.js');
|
|
31
|
+
const cfg = await loadConfig();
|
|
32
|
+
const availableAliases = Object.keys(cfg.sap);
|
|
33
|
+
const parsed = extractSapAliasFlag(args, availableAliases);
|
|
34
|
+
if (parsed.error) {
|
|
35
|
+
console.error(chalk.red(parsed.error));
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
sapAlias = parsed.sapAlias;
|
|
39
|
+
args = parsed.rest;
|
|
40
|
+
}
|
|
41
|
+
if (args[0] === 'configure') {
|
|
42
|
+
console.log(chalk.dim('(alias for `cspeach config add-sap`)'));
|
|
43
|
+
await runAddSapWizard();
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
if (args[0] === 'config' && args[1] === 'add-sap') {
|
|
47
|
+
await runAddSapWizard();
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
if (args[0] === 'config' && args[1] === 'set') {
|
|
51
|
+
const { runConfigSet, ConfigSetError } = await import('./commands/config-set.js');
|
|
52
|
+
try {
|
|
53
|
+
await runConfigSet(args.slice(2));
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (e instanceof ConfigSetError) {
|
|
57
|
+
console.error(chalk.red(`✗ ${e.message}`));
|
|
58
|
+
process.exit(e.exitCode);
|
|
59
|
+
}
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
if (args[0] === 'config' && args[1] === 'show') {
|
|
65
|
+
const { runConfigShow } = await import('./commands/config-show.js');
|
|
66
|
+
await runConfigShow(args.slice(2));
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
if (args[0] === 'doctor') {
|
|
70
|
+
const { runDoctor } = await import('./doctor/run.js');
|
|
71
|
+
return runDoctor();
|
|
72
|
+
}
|
|
73
|
+
if (args[0] === 'login') {
|
|
74
|
+
const { runLoginCommand } = await import('./commands/login.js');
|
|
75
|
+
const { input } = await import('@inquirer/prompts');
|
|
76
|
+
const { loadConfig } = await import('./config/loader.js');
|
|
77
|
+
const cfg = await loadConfig();
|
|
78
|
+
const open = async (url) => {
|
|
79
|
+
const { spawn } = await import('node:child_process');
|
|
80
|
+
// On Windows, `start` is a cmd builtin and needs a shell; spawn the URL via cmd /c.
|
|
81
|
+
if (process.platform === 'win32') {
|
|
82
|
+
spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
86
|
+
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
return runLoginCommand({
|
|
90
|
+
proxyUrl: cfg.proxy_url,
|
|
91
|
+
open,
|
|
92
|
+
print: (s) => console.log(s),
|
|
93
|
+
prompt: (q) => input({ message: q }),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (args[0] === 'logout') {
|
|
97
|
+
const { runLogoutCommand } = await import('./commands/logout.js');
|
|
98
|
+
return runLogoutCommand({ print: (s) => console.log(s) });
|
|
99
|
+
}
|
|
100
|
+
if (args[0] === 'whoami') {
|
|
101
|
+
const { runWhoamiCommand } = await import('./commands/whoami.js');
|
|
102
|
+
const { loadConfig } = await import('./config/loader.js');
|
|
103
|
+
const cfg = await loadConfig();
|
|
104
|
+
return runWhoamiCommand({ proxyUrl: cfg.proxy_url, print: (s) => console.log(s) });
|
|
105
|
+
}
|
|
106
|
+
if (args[0] === 'analyze' && args[1] === 'project-context') {
|
|
107
|
+
const { runProjectContextImpactCommand } = await import('./commands/project-context-impact.js');
|
|
108
|
+
return runProjectContextImpactCommand(args.slice(2));
|
|
109
|
+
}
|
|
110
|
+
if (args[0] === '--one-shot') {
|
|
111
|
+
const { runOneShot } = await import('./one-shot.js');
|
|
112
|
+
return runOneShot(args.slice(1).join(' '), { sapAlias });
|
|
113
|
+
}
|
|
114
|
+
// Default: if first arg exists and is not a flag, treat as one-shot prompt.
|
|
115
|
+
if (args.length > 0 && !args[0].startsWith('-')) {
|
|
116
|
+
const { runOneShot } = await import('./one-shot.js');
|
|
117
|
+
return runOneShot(args.join(' '), { sapAlias });
|
|
118
|
+
}
|
|
119
|
+
// No args → REPL.
|
|
120
|
+
const { runRepl } = await import('./repl.js');
|
|
121
|
+
return runRepl({ sapAlias });
|
|
122
|
+
}
|
|
123
|
+
function printHelp() {
|
|
124
|
+
console.log(`
|
|
125
|
+
CSPeach — AI-assisted ABAP development CLI
|
|
126
|
+
|
|
127
|
+
Usage:
|
|
128
|
+
cspeach Start interactive REPL
|
|
129
|
+
cspeach "<prompt>" One-shot — runs one turn and exits
|
|
130
|
+
cspeach --one-shot "<prompt>" Same as above, explicit flag
|
|
131
|
+
cspeach --sap-alias <name> Use a specific SAP alias (overrides default)
|
|
132
|
+
cspeach --resume [id] Resume a previous session (list if no id)
|
|
133
|
+
cspeach config add-sap Add a SAP system interactively
|
|
134
|
+
cspeach configure Alias for \`cspeach config add-sap\`
|
|
135
|
+
cspeach config set <key> <value> Update a config key
|
|
136
|
+
cspeach config show [key] Show config (full or single key)
|
|
137
|
+
cspeach doctor Diagnose install + connectivity
|
|
138
|
+
cspeach login Sign in via device-code flow
|
|
139
|
+
cspeach logout Sign out (keep keytar key, if any)
|
|
140
|
+
cspeach whoami Print current account info
|
|
141
|
+
cspeach analyze project-context Compare flag-on vs flag-off session metrics
|
|
142
|
+
cspeach --upgrade Upgrade to latest via npm
|
|
143
|
+
cspeach --version Print version
|
|
144
|
+
cspeach --help Show this help
|
|
145
|
+
|
|
146
|
+
Docs: https://cspeach.dev
|
|
147
|
+
`);
|
|
148
|
+
}
|