@cspeach/cli 1.1.1 → 1.1.3
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/dist/agent/loop.js +33 -4
- package/dist/agent/session-context.js +24 -0
- package/dist/agent/tool-dispatch.js +17 -1
- package/dist/approvals/jwt.js +12 -1
- package/dist/cli-args.js +24 -0
- package/dist/cli.js +33 -4
- package/dist/commands/config-set.js +139 -4
- package/dist/config/loader.js +34 -0
- package/dist/cost/pricing.js +4 -0
- package/dist/cost/session-cost.js +28 -0
- package/dist/doctor/checks/_standalone-skip.js +37 -0
- package/dist/doctor/checks/cert.js +5 -0
- package/dist/doctor/checks/forge-rules.js +6 -0
- package/dist/doctor/checks/sap.js +5 -0
- package/dist/doctor/checks/standards.js +27 -0
- package/dist/doctor/checks/system-roles.js +5 -0
- package/dist/doctor/checks/zcspeach.js +5 -0
- package/dist/doctor/run.js +2 -0
- package/dist/one-shot.js +36 -11
- package/dist/renderer/abap-inline.js +2 -1
- package/dist/renderer/syntax.js +7 -1
- package/dist/repl/current-transport.js +23 -1
- package/dist/repl/post-turn-status.js +9 -6
- package/dist/repl.js +40 -7
- package/dist/sap/offline-adt-client.js +58 -0
- package/dist/sap/standalone-onboarding.js +118 -0
- package/dist/sap/standalone-profile.js +237 -0
- package/dist/skills/bundled-skills.js +41 -41
- package/dist/standards/standards-file.js +107 -0
- package/dist/standards/standards-init.js +194 -0
- package/dist/tools/capability/tool.js +2 -0
- package/dist/tools/fiori/fe-scaffold.js +25 -0
- package/dist/tools/fiori/smoke/assertions.js +8 -2
- package/dist/tools/fiori/smoke/driver.js +6 -0
- package/dist/tools/fiori/smoke/run-smoke.js +69 -2
- package/dist/tools/fiori/tools.js +4 -2
- package/dist/tools/index.js +25 -0
- package/dist/tools/sap-read.js +18 -0
- package/dist/tools/sap-write.js +32 -0
- package/dist/tools/snapshot.js +3 -0
- package/dist/tools/todo.js +11 -3
- package/dist/tools/transport.js +4 -0
- package/dist/ui/app.js +1 -1
- package/dist/ui/sap-state-store.js +1 -1
- package/dist/ui/status-line.js +2 -2
- package/dist/ui/todo-panel.js +27 -6
- package/fiori/zlas-inspection-lr/package.json +26 -0
- package/fiori/zlas-inspection-lr/ui5-local.yaml +45 -0
- package/fiori/zlas-inspection-lr/ui5.yaml +41 -0
- package/fiori/zlas-inspection-lr/webapp/Component.js +12 -0
- package/fiori/zlas-inspection-lr/webapp/i18n/i18n.properties +9 -0
- package/fiori/zlas-inspection-lr/webapp/index.html +35 -0
- package/fiori/zlas-inspection-lr/webapp/manifest.json +145 -0
- package/package.json +109 -109
|
@@ -18,11 +18,17 @@
|
|
|
18
18
|
import { loadConfig } from '../../config/loader.js';
|
|
19
19
|
import { getConnection } from '../../sap/connection-manager.js';
|
|
20
20
|
import { AdtClient, snapshots } from '@cspeach/sap-client';
|
|
21
|
+
import { sapChecksSkipped, standaloneSkip } from './_standalone-skip.js';
|
|
21
22
|
const PROBE_NAME = 'Z_CSPEACH_DOCTOR_PROBE';
|
|
22
23
|
const PROBE_PACKAGE = '$TMP';
|
|
23
24
|
const PROBE_SOURCE = `REPORT ${PROBE_NAME}.\nWRITE / 'cspeach doctor probe — safe to delete'.`;
|
|
24
25
|
export async function checkForgeRules() {
|
|
25
26
|
const cfg = await loadConfig();
|
|
27
|
+
// Standalone mode ONLY — the Rule 7/10 probe needs a live system, and an
|
|
28
|
+
// opted-in paste-mode user has none. Everything below (including the
|
|
29
|
+
// no-alias skip) is unchanged.
|
|
30
|
+
if (sapChecksSkipped(cfg))
|
|
31
|
+
return standaloneSkip('forge-rules');
|
|
26
32
|
const aliases = Object.keys(cfg.sap);
|
|
27
33
|
if (aliases.length === 0) {
|
|
28
34
|
return {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { loadConfig, resolveSapTls } from '../../config/loader.js';
|
|
3
3
|
import { probeGet } from './_http-probe.js';
|
|
4
|
+
import { sapChecksSkipped, standaloneSkip } from './_standalone-skip.js';
|
|
4
5
|
export async function checkSap() {
|
|
5
6
|
const cfg = await loadConfig();
|
|
7
|
+
// Standalone mode ONLY — an opted-in paste-mode user has nothing to probe.
|
|
8
|
+
// Everything below (including the no-alias failure) is unchanged.
|
|
9
|
+
if (sapChecksSkipped(cfg))
|
|
10
|
+
return standaloneSkip('sap');
|
|
6
11
|
const systems = Object.entries(cfg.sap);
|
|
7
12
|
if (systems.length === 0)
|
|
8
13
|
return { name: 'sap', ok: false, message: 'no SAP systems configured', hint: 'Run: cspeach config add-sap' };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor check: standards (2026-09-16).
|
|
3
|
+
*
|
|
4
|
+
* Purely informational — surfaces WHICH development-standards document the
|
|
5
|
+
* session context is carrying, so a team that thinks their rules are being
|
|
6
|
+
* followed can confirm it at a glance. Having none is a normal state (CSPeach's
|
|
7
|
+
* built-in conventions apply), so it renders as a neutral `-` line, never a
|
|
8
|
+
* failure.
|
|
9
|
+
*/
|
|
10
|
+
import { resolveStandardsFile, displayStandardsPath } from '../../standards/standards-file.js';
|
|
11
|
+
export async function checkStandards() {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const file = await resolveStandardsFile(cwd);
|
|
14
|
+
if (!file) {
|
|
15
|
+
return {
|
|
16
|
+
name: 'standards',
|
|
17
|
+
ok: false,
|
|
18
|
+
skipped: true,
|
|
19
|
+
message: 'none (run: cspeach standards init)',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
name: 'standards',
|
|
24
|
+
ok: true,
|
|
25
|
+
message: displayStandardsPath(file.path, cwd),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sapChecksSkipped, standaloneSkip } from './_standalone-skip.js';
|
|
1
2
|
/**
|
|
2
3
|
* Doctor check: system-roles (UX Wave 1, Task 1).
|
|
3
4
|
*
|
|
@@ -11,6 +12,10 @@
|
|
|
11
12
|
* never counts toward the doctor's failed total. Pure — no I/O beyond cfg.
|
|
12
13
|
*/
|
|
13
14
|
export async function checkSystemRoles(cfg) {
|
|
15
|
+
// Standalone mode ONLY — nothing to put a write-mode ceiling on. Everything
|
|
16
|
+
// below (including the no-alias skip) is unchanged.
|
|
17
|
+
if (sapChecksSkipped(cfg))
|
|
18
|
+
return standaloneSkip('system-roles');
|
|
14
19
|
const entries = Object.entries(cfg.sap ?? {});
|
|
15
20
|
if (entries.length === 0) {
|
|
16
21
|
return {
|
|
@@ -2,9 +2,14 @@ import keytar from 'keytar';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { loadConfig, resolveSapTls } from '../../config/loader.js';
|
|
4
4
|
import { probeGet } from './_http-probe.js';
|
|
5
|
+
import { sapChecksSkipped, standaloneSkip } from './_standalone-skip.js';
|
|
5
6
|
const SAP_SERVICE = 'cspeach-sap';
|
|
6
7
|
export async function checkZcspeach() {
|
|
7
8
|
const cfg = await loadConfig();
|
|
9
|
+
// Standalone mode ONLY — an opted-in paste-mode user has nothing to probe.
|
|
10
|
+
// Everything below (including the no-alias failure) is unchanged.
|
|
11
|
+
if (sapChecksSkipped(cfg))
|
|
12
|
+
return standaloneSkip('zcspeach');
|
|
8
13
|
const systems = Object.entries(cfg.sap);
|
|
9
14
|
if (systems.length === 0) {
|
|
10
15
|
return {
|
package/dist/doctor/run.js
CHANGED
|
@@ -12,6 +12,7 @@ import { checkWriteMode } from './checks/write-mode.js';
|
|
|
12
12
|
import { checkLlmMode } from './checks/llm-mode.js';
|
|
13
13
|
import { checkForgeRules } from './checks/forge-rules.js';
|
|
14
14
|
import { checkSystemRoles } from './checks/system-roles.js';
|
|
15
|
+
import { checkStandards } from './checks/standards.js';
|
|
15
16
|
export async function runDoctor() {
|
|
16
17
|
console.log(chalk.cyan.bold('\nCSPeach — doctor\n'));
|
|
17
18
|
const cfg = await loadConfig();
|
|
@@ -28,6 +29,7 @@ export async function runDoctor() {
|
|
|
28
29
|
checks.push(await checkSkill());
|
|
29
30
|
checks.push(await checkKeychainFallback());
|
|
30
31
|
checks.push(await checkForgeRules());
|
|
32
|
+
checks.push(await checkStandards());
|
|
31
33
|
let failed = 0;
|
|
32
34
|
for (const c of checks) {
|
|
33
35
|
const icon = c.skipped
|
package/dist/one-shot.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { loadConfig, resolveLocalBuild } from './config/loader.js';
|
|
2
|
+
import { loadConfig, resolveLocalBuild, resolveStandalone } from './config/loader.js';
|
|
3
|
+
import { createOfflineAdtClient } from './sap/offline-adt-client.js';
|
|
4
|
+
import { STANDALONE_ALIAS } from './tools/index.js';
|
|
3
5
|
import { getStore } from './auth/api-key.js';
|
|
4
6
|
import { getConnection } from './sap/connection-manager.js';
|
|
5
7
|
import { AdtClient } from '@cspeach/sap-client';
|
|
@@ -50,6 +52,9 @@ import './tools/subagent/schedule_draft_create.js';
|
|
|
50
52
|
import './tools/subagent/agent_run.js';
|
|
51
53
|
export async function runOneShot(prompt, opts) {
|
|
52
54
|
const cfg = await loadConfig();
|
|
55
|
+
// Standalone (2026-09-16) — run with no SAP system at all: no connection, no
|
|
56
|
+
// AdtClient, SAP tools hidden. `--no-sap` for this run, or `no_sap = true`.
|
|
57
|
+
const standalone = resolveStandalone(cfg, opts?.noSap === true);
|
|
53
58
|
setRenderingOverride(cfg.ui.rendering);
|
|
54
59
|
// model-governance step 2d — fire-and-forget fetch of the served model-config
|
|
55
60
|
// (managed mode only, never blocks). A turn that starts before it lands uses
|
|
@@ -70,7 +75,9 @@ export async function runOneShot(prompt, opts) {
|
|
|
70
75
|
// says gated/auto — the banner must say so. No role ⇒ effective === config
|
|
71
76
|
// (byte-identical). Alias absent (no SAP configured) ⇒ error out below,
|
|
72
77
|
// same as before.
|
|
73
|
-
|
|
78
|
+
// Standalone has no alias, so no role clamp applies — the configured
|
|
79
|
+
// write_mode stands on its own.
|
|
80
|
+
const oneShotAlias = standalone ? undefined : (opts?.sapAlias ?? Object.keys(cfg.sap)[0]);
|
|
74
81
|
const oneShotRole = oneShotAlias !== undefined ? cfg.sap[oneShotAlias]?.role : undefined;
|
|
75
82
|
if (getEffectiveWriteMode(cfg.write_mode, oneShotRole) === 'advisory-only') {
|
|
76
83
|
console.log(chalk.yellow('⚠ Advisory mode: writes will be proposed, not executed.'));
|
|
@@ -91,17 +98,35 @@ export async function runOneShot(prompt, opts) {
|
|
|
91
98
|
return 1;
|
|
92
99
|
}
|
|
93
100
|
const aliases = Object.keys(cfg.sap);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
let alias;
|
|
102
|
+
let adt;
|
|
103
|
+
if (standalone) {
|
|
104
|
+
// No wizard, no picker, no getConnection. The offline stand-in rejects
|
|
105
|
+
// every call with an actionable message if anything slips past the tool
|
|
106
|
+
// filter + dispatch guard.
|
|
107
|
+
alias = STANDALONE_ALIAS;
|
|
108
|
+
adt = createOfflineAdtClient();
|
|
109
|
+
console.log(chalk.dim('Standalone mode: no SAP system connected — skills work from pasted code. ' +
|
|
110
|
+
'Connect later with: cspeach config add-sap'));
|
|
111
|
+
// One-shot is headless by design (no prompts), so an unanswered standalone
|
|
112
|
+
// profile can't be filled in here — say how, and carry on with "unknown".
|
|
113
|
+
if (!cfg.standalone) {
|
|
114
|
+
console.log(chalk.dim('Standalone profile not set — run: cspeach config set standalone.platform ...'));
|
|
115
|
+
}
|
|
97
116
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
else {
|
|
118
|
+
if (aliases.length === 0) {
|
|
119
|
+
console.error('No SAP system configured. Run `cspeach config add-sap` first.');
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
// C.1 — --sap-alias flag override
|
|
123
|
+
alias = opts?.sapAlias ?? aliases[0];
|
|
124
|
+
if (opts?.sapAlias) {
|
|
125
|
+
console.log(chalk.dim(`Using --sap-alias ${opts.sapAlias}`));
|
|
126
|
+
}
|
|
127
|
+
const conn = await getConnection(alias, cfg.sap[alias]);
|
|
128
|
+
adt = new AdtClient(conn);
|
|
102
129
|
}
|
|
103
|
-
const conn = await getConnection(alias, cfg.sap[alias]);
|
|
104
|
-
const adt = new AdtClient(conn);
|
|
105
130
|
const session = newSession(makeSessionId(), alias, null, resolveModelRole('session_default', cfg));
|
|
106
131
|
await saveSession(session);
|
|
107
132
|
const parsed = parseShortcut(prompt);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* ```abap → existing ABAP regex logic (unchanged)
|
|
8
8
|
* ```cds → cli-highlight CDS grammar (via syntax.ts)
|
|
9
9
|
* ```bdef → cli-highlight BDEF grammar (via syntax.ts)
|
|
10
|
+
* ```ddl → cli-highlight CDS grammar (ddl is a CDS alias; via syntax.ts)
|
|
10
11
|
* ```<any other or empty> → pass through (no highlight)
|
|
11
12
|
*
|
|
12
13
|
* The function is renamed `applyCodeHighlight`. A deprecation re-export
|
|
@@ -64,7 +65,7 @@ export function applyCodeHighlight(text) {
|
|
|
64
65
|
// dependency for the hottest fenced-block type.
|
|
65
66
|
out.push(highlightAbapBlock(body));
|
|
66
67
|
}
|
|
67
|
-
else if (lang === 'cds' || lang === 'bdef') {
|
|
68
|
+
else if (lang === 'cds' || lang === 'bdef' || lang === 'ddl') {
|
|
68
69
|
// cli-highlight grammars via the Task 5 dispatcher.
|
|
69
70
|
out.push(highlightCodeBlock(body, lang));
|
|
70
71
|
}
|
package/dist/renderer/syntax.js
CHANGED
|
@@ -47,7 +47,13 @@ const hljs = createRequire(require.resolve('cli-highlight'))('highlight.js');
|
|
|
47
47
|
hljs.registerLanguage('abap', () => abapGrammar);
|
|
48
48
|
hljs.registerLanguage('cds', () => cdsGrammar);
|
|
49
49
|
hljs.registerLanguage('bdef', () => bdefGrammar);
|
|
50
|
-
|
|
50
|
+
// `ddl` is an alias of the CDS grammar: ```ddl fences carry CDS DDL source
|
|
51
|
+
// (the abap-rap templates emit them). Registering it on the same hljs
|
|
52
|
+
// instance also silences marked-terminal's "Could not find the language
|
|
53
|
+
// 'ddl'" warning, since marked-terminal calls cli-highlight directly with
|
|
54
|
+
// the raw fence lang and never goes through highlightCodeBlock().
|
|
55
|
+
hljs.registerLanguage('ddl', () => cdsGrammar);
|
|
56
|
+
const SUPPORTED = new Set(['abap', 'cds', 'bdef', 'ddl']);
|
|
51
57
|
/**
|
|
52
58
|
* Highlight a code block body (without fence markers).
|
|
53
59
|
*
|
|
@@ -15,12 +15,23 @@
|
|
|
15
15
|
* Module-scope singleton per CLI process. Tests must call clearCurrentTransport()
|
|
16
16
|
* in beforeEach to avoid leakage across Vitest test files.
|
|
17
17
|
*
|
|
18
|
-
* Task 9 (ux-wave1) — this setter is ALSO
|
|
18
|
+
* Task 9 (ux-wave1) — this setter is ALSO a statusline wire for the
|
|
19
19
|
* `tr <TRKORR>` segment: every path that changes the session transport
|
|
20
20
|
* (sap_transport create success, owning-transport adoption in the create
|
|
21
21
|
* guard, /transport slash command) funnels through set/clear here, so the
|
|
22
22
|
* globalStore push lives HERE rather than scattered across tool handlers.
|
|
23
23
|
* Store writes are harmless in classic mode (nothing subscribes there).
|
|
24
|
+
*
|
|
25
|
+
* F-L2-1 (2026-08-11) — that wire alone is not enough, so this module now
|
|
26
|
+
* carries a SECOND, write-side wire: noteWriteTransport(). The session flow
|
|
27
|
+
* above is the only writer of `store.transport`, but a per-write `corrNr`
|
|
28
|
+
* coming from a prompt or a plan never travels through it — live evidence: a
|
|
29
|
+
* whole session's writes stamped S4HK903635 while the statusline read
|
|
30
|
+
* `tr none`. noteWriteTransport records what a write ACTUALLY stamped into a
|
|
31
|
+
* SEPARATE store field (`lastWriteTransport`) that the statusline falls back
|
|
32
|
+
* to. It deliberately does NOT touch the session pointer below: that pointer
|
|
33
|
+
* feeds the 3-tier write-transport fallback, and moving it would change write
|
|
34
|
+
* behaviour, not just the display.
|
|
24
35
|
*/
|
|
25
36
|
import { globalStore } from '../ui/sap-state-store.js';
|
|
26
37
|
let currentTransport = null;
|
|
@@ -35,6 +46,17 @@ export function clearCurrentTransport() {
|
|
|
35
46
|
currentTransport = null;
|
|
36
47
|
globalStore.update({ transport: null });
|
|
37
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Statusline-only wire (F-L2-1): record the transport actually stamped on the
|
|
51
|
+
* last successful write. Never touches the session transport pointer — see the
|
|
52
|
+
* module doc. No-op on a falsy trkorr (local / $TMP objects write without one,
|
|
53
|
+
* and blanking the display there would lose the last real datapoint).
|
|
54
|
+
*/
|
|
55
|
+
export function noteWriteTransport(trkorr) {
|
|
56
|
+
if (!trkorr)
|
|
57
|
+
return;
|
|
58
|
+
globalStore.update({ lastWriteTransport: trkorr });
|
|
59
|
+
}
|
|
38
60
|
/**
|
|
39
61
|
* Handles the `/transport [<trkorr>|clear]` slash command. Returns user-facing
|
|
40
62
|
* status text. Mirrors the shape of handleSafetyModeCommand in safety-mode-state.ts.
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* primary per-turn status surface.
|
|
19
19
|
*/
|
|
20
20
|
import { computeCost } from '../cost/pricing.js';
|
|
21
|
+
import { computeSessionCost } from '../cost/session-cost.js';
|
|
21
22
|
import { globalStore } from '../ui/sap-state-store.js';
|
|
22
23
|
import { printStatusFooter, formatStatusFooter } from '../renderer/status-footer.js';
|
|
23
24
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
@@ -43,12 +44,9 @@ export function pushPostTurnStatus(params) {
|
|
|
43
44
|
cacheCreate: (session.usage.cache_creation_input_tokens ?? 0) - snapshot.cacheCreate,
|
|
44
45
|
};
|
|
45
46
|
const turnCost = computeCost(turnModel ?? session.model, turnCostTokens);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
cacheRead: session.usage.cache_read_input_tokens,
|
|
50
|
-
cacheCreate: session.usage.cache_creation_input_tokens ?? 0,
|
|
51
|
-
});
|
|
47
|
+
// F-L2-2: extracted to cost/session-cost.ts so the agent loop can push the
|
|
48
|
+
// SAME total mid-stream. This call site is unchanged in behaviour.
|
|
49
|
+
const sessionCost = computeSessionCost(session);
|
|
52
50
|
// Task 9 (ux-wave1) — statusline spend wire. Honest semantics note: the
|
|
53
51
|
// store field is named `dollarSpendToday` (legacy sidebar naming, kept to
|
|
54
52
|
// avoid rename churn) but the value written is SESSION-scoped — the same
|
|
@@ -56,6 +54,11 @@ export function pushPostTurnStatus(params) {
|
|
|
56
54
|
// lifetime session.usage at session.model rates (mixed-model turns are an
|
|
57
55
|
// approximation, same caveat as the footer's session line). There is no
|
|
58
56
|
// day-spanning ledger; the statusline shows session spend.
|
|
57
|
+
//
|
|
58
|
+
// F-L2-2 (2026-08-11): the agent loop now pushes the same value mid-stream
|
|
59
|
+
// so the segment ticks DURING a turn, not only between turns. This push
|
|
60
|
+
// stays as the authoritative end-of-turn settle (the loop's pushes are
|
|
61
|
+
// gated to REPL turns; this one runs on every path that reaches here).
|
|
59
62
|
globalStore.update({ dollarSpendToday: sessionCost });
|
|
60
63
|
const statusPayload = {
|
|
61
64
|
sessionId: session.id,
|
package/dist/repl.js
CHANGED
|
@@ -7,7 +7,9 @@ import chalk from 'chalk';
|
|
|
7
7
|
// 'node:readline' directly for that specific operation.
|
|
8
8
|
import * as readline from 'node:readline/promises';
|
|
9
9
|
import { select, password as promptPassword } from '@inquirer/prompts';
|
|
10
|
-
import { loadConfig, resolveLocalBuild } from './config/loader.js';
|
|
10
|
+
import { loadConfig, resolveLocalBuild, resolveStandalone } from './config/loader.js';
|
|
11
|
+
import { createOfflineAdtClient } from './sap/offline-adt-client.js';
|
|
12
|
+
import { STANDALONE_ALIAS } from './tools/index.js';
|
|
11
13
|
import { getStore } from './auth/api-key.js';
|
|
12
14
|
import { readAuthFile } from './auth/auth-file.js';
|
|
13
15
|
import { fetchMe } from './auth/me.js';
|
|
@@ -373,7 +375,26 @@ export async function runRepl(opts) {
|
|
|
373
375
|
let cfg = await loadConfig();
|
|
374
376
|
// Apply user-configured rendering override BEFORE any shouldUseInk() call.
|
|
375
377
|
setRenderingOverride(cfg.ui.rendering);
|
|
376
|
-
|
|
378
|
+
// Standalone (2026-09-16) — run with no SAP system at all. Decided BEFORE
|
|
379
|
+
// the onboarding wizard: someone who asked for --no-sap (or persisted
|
|
380
|
+
// no_sap) must never be pushed through the SAP setup they don't have.
|
|
381
|
+
// Having aliases configured does NOT override the request.
|
|
382
|
+
// A session that WAS standalone stays standalone on `--resume`: its
|
|
383
|
+
// sap_system is the reserved 'none' alias, which has no entry in cfg.sap.
|
|
384
|
+
const standalone = resolveStandalone(cfg, opts?.noSap === true) ||
|
|
385
|
+
opts?.resumedSession?.sap_system === STANDALONE_ALIAS;
|
|
386
|
+
if (standalone) {
|
|
387
|
+
console.log(chalk.dim('\nStandalone mode: no SAP system connected — skills work from pasted code. ' +
|
|
388
|
+
'Connect later with: cspeach config add-sap'));
|
|
389
|
+
// Ask the profile questions ONCE — they become the <sap_system> element
|
|
390
|
+
// every skill reads, so without them a design would target "unknown".
|
|
391
|
+
if (!cfg.standalone) {
|
|
392
|
+
const { runStandaloneProfileWizard } = await import('./sap/standalone-onboarding.js');
|
|
393
|
+
await runStandaloneProfileWizard({ cwd: process.cwd() });
|
|
394
|
+
cfg = await loadConfig();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else if (Object.keys(cfg.sap).length === 0) {
|
|
377
398
|
console.log(chalk.yellow('\nNo SAP systems configured. Starting onboarding wizard.'));
|
|
378
399
|
await runAddSapWizard();
|
|
379
400
|
cfg = await loadConfig();
|
|
@@ -418,7 +439,12 @@ export async function runRepl(opts) {
|
|
|
418
439
|
const aliases = Object.keys(cfg.sap);
|
|
419
440
|
// C.1 — --sap-alias flag override skips the interactive picker
|
|
420
441
|
let selectedAlias;
|
|
421
|
-
if (
|
|
442
|
+
if (standalone) {
|
|
443
|
+
// No picker: there is nothing to pick. The reserved 'none' alias is what
|
|
444
|
+
// isStandalone(ctx) reads downstream.
|
|
445
|
+
selectedAlias = STANDALONE_ALIAS;
|
|
446
|
+
}
|
|
447
|
+
else if (opts?.sapAlias) {
|
|
422
448
|
console.log(chalk.dim(`Using --sap-alias ${opts.sapAlias}`));
|
|
423
449
|
selectedAlias = opts.sapAlias;
|
|
424
450
|
}
|
|
@@ -461,8 +487,11 @@ export async function runRepl(opts) {
|
|
|
461
487
|
console.log(chalk.gray(`Write mode: ${startupWriteMode}${capped}\n`));
|
|
462
488
|
}
|
|
463
489
|
}
|
|
464
|
-
|
|
465
|
-
|
|
490
|
+
// Standalone: the offline stand-in rejects every call with an actionable
|
|
491
|
+
// message. This is the single construction site the AdtClient cast lives at.
|
|
492
|
+
const adt = standalone
|
|
493
|
+
? createOfflineAdtClient()
|
|
494
|
+
: new AdtClient(await getConnection(selectedAlias, cfg.sap[selectedAlias]));
|
|
466
495
|
// Phase A #3 (2026-05-16) — sidebar SAP-state wire, trigger 1 of 4.
|
|
467
496
|
// Push system + user the moment we know the alias. Before this commit
|
|
468
497
|
// the sidebar showed "no system / user: —" forever because the store
|
|
@@ -475,8 +504,8 @@ export async function runRepl(opts) {
|
|
|
475
504
|
// superseded (unmounted) header.tsx — so nothing else loses the host;
|
|
476
505
|
// the connection banner above already printed it once to scrollback.
|
|
477
506
|
sapStateStore.update({
|
|
478
|
-
system: selectedAlias,
|
|
479
|
-
user: cfg.sap[selectedAlias].username,
|
|
507
|
+
system: standalone ? 'standalone' : selectedAlias,
|
|
508
|
+
user: standalone ? '' : cfg.sap[selectedAlias].username,
|
|
480
509
|
writeMode: startupWriteMode,
|
|
481
510
|
});
|
|
482
511
|
// v0.6 — when resuming, adopt the on-disk SessionState verbatim. This
|
|
@@ -822,6 +851,7 @@ export async function runRepl(opts) {
|
|
|
822
851
|
skill: rr.skill,
|
|
823
852
|
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
|
|
824
853
|
chunkEmitter,
|
|
854
|
+
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
825
855
|
});
|
|
826
856
|
}
|
|
827
857
|
finally {
|
|
@@ -1254,6 +1284,7 @@ export async function runRepl(opts) {
|
|
|
1254
1284
|
skill: skill,
|
|
1255
1285
|
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
|
|
1256
1286
|
chunkEmitter,
|
|
1287
|
+
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
1257
1288
|
signal: currentTurnAbort.signal,
|
|
1258
1289
|
suppressSaveHook: planResume !== null || planContinue !== null,
|
|
1259
1290
|
// A4 — undefined on every non-plan turn (and when tiering is off).
|
|
@@ -2270,6 +2301,7 @@ export async function runRepl(opts) {
|
|
|
2270
2301
|
userMessage: rr.prompt,
|
|
2271
2302
|
skill: rr.skill,
|
|
2272
2303
|
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter: classicOutputEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
|
|
2304
|
+
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
2273
2305
|
});
|
|
2274
2306
|
continue;
|
|
2275
2307
|
}
|
|
@@ -2797,6 +2829,7 @@ export async function runRepl(opts) {
|
|
|
2797
2829
|
userMessage: prompt,
|
|
2798
2830
|
skill: skill,
|
|
2799
2831
|
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter: classicOutputEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
|
|
2832
|
+
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
2800
2833
|
signal: turnAbort.signal,
|
|
2801
2834
|
suppressSaveHook: planResume !== null || planContinue !== null,
|
|
2802
2835
|
// A4 — undefined on every non-plan turn (and when tiering is off).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline AdtClient stand-in for standalone mode (`--no-sap` / `no_sap = true`).
|
|
3
|
+
*
|
|
4
|
+
* `ToolContext.adt` is typed as a live `AdtClient` and read unconditionally at
|
|
5
|
+
* many call sites. Rather than widen that type across the codebase (and force
|
|
6
|
+
* every handler to grow a null-check), standalone injects a Proxy whose every
|
|
7
|
+
* method call returns a rejected Promise carrying `CspeachNotConnectedError`.
|
|
8
|
+
*
|
|
9
|
+
* The model should never get here — SAP-dependent tools are filtered out of the
|
|
10
|
+
* tool list (`requiresSap`, tools/index.ts) and blocked again at dispatch
|
|
11
|
+
* (agent/tool-dispatch.ts). This is the third, last-resort layer: if anything
|
|
12
|
+
* slips through it fails with an actionable message instead of a TypeError.
|
|
13
|
+
*/
|
|
14
|
+
export const NOT_CONNECTED_MESSAGE = 'No SAP system connected (standalone mode). Paste the ABAP source or describe the object, or run: cspeach config add-sap';
|
|
15
|
+
export class CspeachNotConnectedError extends Error {
|
|
16
|
+
constructor(message = NOT_CONNECTED_MESSAGE) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'CspeachNotConnectedError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Property names that must resolve to `undefined` rather than to a callable.
|
|
23
|
+
*
|
|
24
|
+
* `then` is the important one: returning a function for it would make the proxy
|
|
25
|
+
* a thenable, so `await client` (or any `Promise.resolve(client)`) would invoke
|
|
26
|
+
* it as a resolver and never settle. The rest are inspection / serialisation
|
|
27
|
+
* hooks that Node, vitest and JSON.stringify probe on arbitrary objects.
|
|
28
|
+
*/
|
|
29
|
+
const NON_CALLABLE = new Set([
|
|
30
|
+
'then',
|
|
31
|
+
'catch',
|
|
32
|
+
'finally',
|
|
33
|
+
'toJSON',
|
|
34
|
+
'constructor',
|
|
35
|
+
'inspect',
|
|
36
|
+
'nodeType',
|
|
37
|
+
'$$typeof',
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Build the standalone stand-in. Cast to `AdtClient` at the single construction
|
|
41
|
+
* site in the caller — the shape is intentionally structural, not a subclass,
|
|
42
|
+
* so a new sap-client method needs no change here.
|
|
43
|
+
*/
|
|
44
|
+
export function createOfflineAdtClient() {
|
|
45
|
+
const target = {};
|
|
46
|
+
return new Proxy(target, {
|
|
47
|
+
get(_t, prop) {
|
|
48
|
+
if (typeof prop === 'symbol')
|
|
49
|
+
return undefined;
|
|
50
|
+
if (NON_CALLABLE.has(prop))
|
|
51
|
+
return undefined;
|
|
52
|
+
return (..._args) => Promise.reject(new CspeachNotConnectedError());
|
|
53
|
+
},
|
|
54
|
+
has() {
|
|
55
|
+
return true;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone profile wizard — asked ONCE the first time CSPeach starts without
|
|
3
|
+
* a SAP system (`--no-sap` / `no_sap = true`), and re-runnable via
|
|
4
|
+
* `cspeach config standalone`.
|
|
5
|
+
*
|
|
6
|
+
* With nothing to probe, these answers ARE the system context: they become the
|
|
7
|
+
* `<sap_system …/>` element every skill reads (see standalone-profile.ts), so a
|
|
8
|
+
* generated design targets the customer's real release instead of guessing.
|
|
9
|
+
*
|
|
10
|
+
* Prompts are injected so the flow is unit-testable without a TTY; the default
|
|
11
|
+
* implementation uses the same `@inquirer/prompts` primitives as the SAP
|
|
12
|
+
* onboarding wizard.
|
|
13
|
+
*/
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { loadConfig, saveConfig } from '../config/loader.js';
|
|
16
|
+
import { runStandardsInit } from '../standards/standards-init.js';
|
|
17
|
+
import { STANDALONE_PLATFORMS, STANDALONE_DATABASES, STANDALONE_DEPLOYMENTS, STANDALONE_IDES, deriveDatabase, deriveDeployment, defaultAbapCloudOnly, defaultRelease, releaseExamples, } from './standalone-profile.js';
|
|
18
|
+
async function defaultPrompts() {
|
|
19
|
+
const { input, select, confirm } = await import('@inquirer/prompts');
|
|
20
|
+
return {
|
|
21
|
+
select: (o) => select({ message: o.message, choices: o.choices, default: o.default }),
|
|
22
|
+
input: (o) => input({ message: o.message, default: o.default }),
|
|
23
|
+
confirm: (o) => confirm({ message: o.message, default: o.default }),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Ask the standalone profile questions, persist `[standalone]`, and return the
|
|
28
|
+
* profile. Questions the platform already answers (database on any S/4HANA or
|
|
29
|
+
* BTP system, deployment on the cloud editions) are derived, not asked.
|
|
30
|
+
*/
|
|
31
|
+
export async function runStandaloneProfileWizard(opts = {}) {
|
|
32
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
33
|
+
const p = opts.prompts ?? (await defaultPrompts());
|
|
34
|
+
console.log(chalk.cyan.bold('\nCSPeach — standalone setup\n'));
|
|
35
|
+
console.log(chalk.dim('No SAP system is connected, so tell CSPeach what system your code will land on.\n'));
|
|
36
|
+
// 1 — platform
|
|
37
|
+
const platform = (await p.select({
|
|
38
|
+
key: 'platform',
|
|
39
|
+
message: 'Which SAP platform do you develop for?',
|
|
40
|
+
choices: STANDALONE_PLATFORMS.map((v) => ({ value: v, name: v })),
|
|
41
|
+
default: 'S/4HANA',
|
|
42
|
+
}));
|
|
43
|
+
// 2 — release (free text, platform-appropriate examples + default)
|
|
44
|
+
const release = (await p.input({
|
|
45
|
+
key: 'release',
|
|
46
|
+
message: `Release (e.g. ${releaseExamples(platform)}):`,
|
|
47
|
+
default: defaultRelease(platform),
|
|
48
|
+
})).trim() || defaultRelease(platform);
|
|
49
|
+
// 3 — database (only ECC can be something other than HANA)
|
|
50
|
+
const derivedDb = deriveDatabase(platform);
|
|
51
|
+
const database = (derivedDb ??
|
|
52
|
+
(await p.select({
|
|
53
|
+
key: 'database',
|
|
54
|
+
message: 'Database:',
|
|
55
|
+
choices: STANDALONE_DATABASES.map((v) => ({ value: v, name: v })),
|
|
56
|
+
default: 'HANA',
|
|
57
|
+
})));
|
|
58
|
+
// 4 — deployment (derived for the cloud editions)
|
|
59
|
+
const derivedDeployment = deriveDeployment(platform);
|
|
60
|
+
const deployment = (derivedDeployment ??
|
|
61
|
+
(await p.select({
|
|
62
|
+
key: 'deployment',
|
|
63
|
+
message: 'Where does it run?',
|
|
64
|
+
choices: [
|
|
65
|
+
{ value: 'on-prem', name: 'on-prem' },
|
|
66
|
+
{ value: 'private-cloud', name: 'private-cloud (hosted / RISE private edition)' },
|
|
67
|
+
].filter((c) => STANDALONE_DEPLOYMENTS.includes(c.value)),
|
|
68
|
+
default: 'on-prem',
|
|
69
|
+
})));
|
|
70
|
+
// 5 — IDE the team uses (drives where a Manual Implementation Guide points)
|
|
71
|
+
const ide = (await p.select({
|
|
72
|
+
key: 'ide',
|
|
73
|
+
message: 'Which IDE does the team use?',
|
|
74
|
+
choices: STANDALONE_IDES.map((v) => ({
|
|
75
|
+
value: v,
|
|
76
|
+
name: v === 'both' ? 'both ADT and SE80' : v,
|
|
77
|
+
})),
|
|
78
|
+
default: 'ADT',
|
|
79
|
+
}));
|
|
80
|
+
// 6 — clean-core requirement
|
|
81
|
+
const abapCloudOnly = await p.confirm({
|
|
82
|
+
key: 'abap_cloud_only',
|
|
83
|
+
message: 'Must new code be ABAP Cloud / clean-core compliant?',
|
|
84
|
+
default: defaultAbapCloudOnly(platform),
|
|
85
|
+
});
|
|
86
|
+
// 7 — optional one-liner
|
|
87
|
+
const notes = (await p.input({
|
|
88
|
+
key: 'notes',
|
|
89
|
+
message: 'Anything else in one line (e.g. "no BTP", "transports via ChaRM")? Enter to skip:',
|
|
90
|
+
default: '',
|
|
91
|
+
})).trim();
|
|
92
|
+
const profile = {
|
|
93
|
+
platform,
|
|
94
|
+
release,
|
|
95
|
+
database,
|
|
96
|
+
deployment,
|
|
97
|
+
ide,
|
|
98
|
+
abap_cloud_only: abapCloudOnly,
|
|
99
|
+
};
|
|
100
|
+
if (notes.length > 0)
|
|
101
|
+
profile.notes = notes;
|
|
102
|
+
const cfg = await loadConfig();
|
|
103
|
+
cfg.standalone = profile;
|
|
104
|
+
// Mark the table explicit so sparse serialisation keeps it.
|
|
105
|
+
if (!Array.isArray(cfg.file_keys))
|
|
106
|
+
cfg.file_keys = [];
|
|
107
|
+
if (!cfg.file_keys.includes('standalone'))
|
|
108
|
+
cfg.file_keys.push('standalone');
|
|
109
|
+
await saveConfig(cfg);
|
|
110
|
+
console.log(chalk.green(`\n✓ Standalone profile saved: ${platform} ${release} (${deployment}, ${ide}` +
|
|
111
|
+
`${abapCloudOnly ? ', ABAP Cloud only' : ''})`));
|
|
112
|
+
console.log(chalk.dim(' Change it any time: cspeach config set standalone.<key> <value>\n'));
|
|
113
|
+
// 8 — development standards (the last question; also `cspeach standards init`)
|
|
114
|
+
if (!opts.skipStandards) {
|
|
115
|
+
await runStandardsInit(cwd, p);
|
|
116
|
+
}
|
|
117
|
+
return profile;
|
|
118
|
+
}
|