@cspeach/cli 1.1.2 → 1.1.4
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 +31 -4
- package/dist/agent/session-context.js +24 -0
- package/dist/agent/tool-dispatch.js +17 -1
- package/dist/cli-args.js +24 -0
- package/dist/cli.js +33 -4
- package/dist/commands/config-set.js +155 -4
- package/dist/commands/cost.js +56 -4
- package/dist/commands/export-audit.js +4 -1
- package/dist/commands/plan-chain.js +8 -1
- package/dist/config/loader.js +44 -0
- package/dist/cost/cost-log.js +6 -1
- package/dist/cost/credits-wire.js +101 -0
- package/dist/cost/credits.js +199 -0
- package/dist/cost/display-mode.js +29 -0
- package/dist/cost/pricing.js +4 -0
- package/dist/doctor/checks/_standalone-skip.js +37 -0
- package/dist/doctor/checks/cert.js +5 -0
- package/dist/doctor/checks/credits.js +48 -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 +4 -0
- package/dist/one-shot.js +43 -11
- package/dist/renderer/status-footer.js +13 -3
- package/dist/repl/post-turn-status.js +5 -0
- package/dist/repl/session-spend-line.js +20 -0
- package/dist/repl.js +48 -9
- 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/session/audit-export.js +23 -4
- 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/index.js +25 -0
- package/dist/tools/sap-read.js +18 -0
- package/dist/tools/sap-write.js +14 -0
- package/dist/tools/snapshot.js +3 -0
- package/dist/tools/transport.js +4 -0
- package/dist/ui/header.js +3 -1
- package/dist/ui/sap-state-store.js +4 -0
- package/dist/ui/status-line.js +20 -1
- package/package.json +109 -109
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';
|
|
@@ -17,6 +19,7 @@ import { createClassicOutputEmitter } from './renderer/notices.js';
|
|
|
17
19
|
import { todoEmitter } from './ui/todo-emitter.js';
|
|
18
20
|
import { applyLocalBuildConfig } from './tools/local-build.js';
|
|
19
21
|
import { getEffectiveWriteMode } from './repl/mode-cycle.js';
|
|
22
|
+
import { initCredits } from './cost/credits-wire.js';
|
|
20
23
|
// Side-effect imports for tool registration.
|
|
21
24
|
import './tools/sap-read.js';
|
|
22
25
|
import './tools/sap-write.js';
|
|
@@ -50,6 +53,9 @@ import './tools/subagent/schedule_draft_create.js';
|
|
|
50
53
|
import './tools/subagent/agent_run.js';
|
|
51
54
|
export async function runOneShot(prompt, opts) {
|
|
52
55
|
const cfg = await loadConfig();
|
|
56
|
+
// Standalone (2026-09-16) — run with no SAP system at all: no connection, no
|
|
57
|
+
// AdtClient, SAP tools hidden. `--no-sap` for this run, or `no_sap = true`.
|
|
58
|
+
const standalone = resolveStandalone(cfg, opts?.noSap === true);
|
|
53
59
|
setRenderingOverride(cfg.ui.rendering);
|
|
54
60
|
// model-governance step 2d — fire-and-forget fetch of the served model-config
|
|
55
61
|
// (managed mode only, never blocks). A turn that starts before it lands uses
|
|
@@ -70,7 +76,9 @@ export async function runOneShot(prompt, opts) {
|
|
|
70
76
|
// says gated/auto — the banner must say so. No role ⇒ effective === config
|
|
71
77
|
// (byte-identical). Alias absent (no SAP configured) ⇒ error out below,
|
|
72
78
|
// same as before.
|
|
73
|
-
|
|
79
|
+
// Standalone has no alias, so no role clamp applies — the configured
|
|
80
|
+
// write_mode stands on its own.
|
|
81
|
+
const oneShotAlias = standalone ? undefined : (opts?.sapAlias ?? Object.keys(cfg.sap)[0]);
|
|
74
82
|
const oneShotRole = oneShotAlias !== undefined ? cfg.sap[oneShotAlias]?.role : undefined;
|
|
75
83
|
if (getEffectiveWriteMode(cfg.write_mode, oneShotRole) === 'advisory-only') {
|
|
76
84
|
console.log(chalk.yellow('⚠ Advisory mode: writes will be proposed, not executed.'));
|
|
@@ -90,18 +98,42 @@ export async function runOneShot(prompt, opts) {
|
|
|
90
98
|
console.error('No API key configured. Run `cspeach` (interactive) first to set one.');
|
|
91
99
|
return 1;
|
|
92
100
|
}
|
|
101
|
+
// Credits (2026-09-16) — resolve the display mode before the turn runs, so
|
|
102
|
+
// the per-turn cost JSONL records the ledger charge and nothing downstream
|
|
103
|
+
// can print a dollar to a managed customer. One-shot prints no cost figure
|
|
104
|
+
// of its own today, but the agent loop's cost-log path is shared with the
|
|
105
|
+
// REPL and must not diverge. Capped + fail-safe; BYOK/local skip the call.
|
|
106
|
+
await initCredits(cfg, async () => (await store.get()) ?? '');
|
|
93
107
|
const aliases = Object.keys(cfg.sap);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
let alias;
|
|
109
|
+
let adt;
|
|
110
|
+
if (standalone) {
|
|
111
|
+
// No wizard, no picker, no getConnection. The offline stand-in rejects
|
|
112
|
+
// every call with an actionable message if anything slips past the tool
|
|
113
|
+
// filter + dispatch guard.
|
|
114
|
+
alias = STANDALONE_ALIAS;
|
|
115
|
+
adt = createOfflineAdtClient();
|
|
116
|
+
console.log(chalk.dim('Standalone mode: no SAP system connected — skills work from pasted code. ' +
|
|
117
|
+
'Connect later with: cspeach config add-sap'));
|
|
118
|
+
// One-shot is headless by design (no prompts), so an unanswered standalone
|
|
119
|
+
// profile can't be filled in here — say how, and carry on with "unknown".
|
|
120
|
+
if (!cfg.standalone) {
|
|
121
|
+
console.log(chalk.dim('Standalone profile not set — run: cspeach config set standalone.platform ...'));
|
|
122
|
+
}
|
|
97
123
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
else {
|
|
125
|
+
if (aliases.length === 0) {
|
|
126
|
+
console.error('No SAP system configured. Run `cspeach config add-sap` first.');
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
// C.1 — --sap-alias flag override
|
|
130
|
+
alias = opts?.sapAlias ?? aliases[0];
|
|
131
|
+
if (opts?.sapAlias) {
|
|
132
|
+
console.log(chalk.dim(`Using --sap-alias ${opts.sapAlias}`));
|
|
133
|
+
}
|
|
134
|
+
const conn = await getConnection(alias, cfg.sap[alias]);
|
|
135
|
+
adt = new AdtClient(conn);
|
|
102
136
|
}
|
|
103
|
-
const conn = await getConnection(alias, cfg.sap[alias]);
|
|
104
|
-
const adt = new AdtClient(conn);
|
|
105
137
|
const session = newSession(makeSessionId(), alias, null, resolveModelRole('session_default', cfg));
|
|
106
138
|
await saveSession(session);
|
|
107
139
|
const parsed = parseShortcut(prompt);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { formatCost } from '../cost/pricing.js';
|
|
3
|
+
import { creditsActive, formatCreditsLine, CREDITS_PENDING } from '../cost/credits.js';
|
|
3
4
|
const RULE_WIDTH = 72;
|
|
4
5
|
const RULE = chalk.dim('─'.repeat(RULE_WIDTH));
|
|
5
6
|
/**
|
|
@@ -40,15 +41,24 @@ export const LARGE_SESSION_THRESHOLD = 100_000;
|
|
|
40
41
|
* direct stdout (which would corrupt the live Ink frame).
|
|
41
42
|
*/
|
|
42
43
|
export function formatStatusFooter(params) {
|
|
43
|
-
const { sessionId, sapAlias, skill, turnCount, tokens, cachedTokens, elapsedMs, costThisTurn, costSession, sessionInputTokens, variant } = params;
|
|
44
|
+
const { sessionId, sapAlias, skill, turnCount, tokens, cachedTokens, elapsedMs, costThisTurn, costSession, sessionInputTokens, variant, credits } = params;
|
|
44
45
|
const shortSession = sessionId.slice(0, 8);
|
|
45
46
|
const skillDisplay = skill.startsWith('/') ? skill : `/${skill}`;
|
|
46
47
|
const turns = `${turnCount} turn${turnCount === 1 ? '' : 's'}`;
|
|
47
48
|
const tokStr = tokens.toLocaleString() + ' tok this turn';
|
|
48
49
|
const cachedStr = cachedTokens > 0 ? `${cachedTokens.toLocaleString()} cached` : '';
|
|
49
50
|
const elapsed = elapsedMs > 0 ? `${(elapsedMs / 1000).toFixed(1)}s` : '';
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
// Credits mode: the whole spend story collapses into ONE customer-facing
|
|
52
|
+
// segment (turn · session · balance) in place of the two dollar segments.
|
|
53
|
+
// Both variants read `costTurnStr`, so putting the combined line there
|
|
54
|
+
// covers the slim Ink receipt and the full classic receipt alike.
|
|
55
|
+
const inCredits = creditsActive();
|
|
56
|
+
const costTurnStr = inCredits
|
|
57
|
+
? (credits ? formatCreditsLine(credits) : CREDITS_PENDING)
|
|
58
|
+
: (costThisTurn != null ? formatCost(costThisTurn) : '');
|
|
59
|
+
const costSessStr = inCredits
|
|
60
|
+
? ''
|
|
61
|
+
: (costSession != null ? `session: ${formatCost(costSession)}` : '');
|
|
52
62
|
// Ink turn-receipt (see the variant doc on StatusFooterParams):
|
|
53
63
|
// turn-only facts — everything session-scoped lives in <StatusLine />.
|
|
54
64
|
const parts = variant === 'turn'
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { computeCost } from '../cost/pricing.js';
|
|
21
21
|
import { computeSessionCost } from '../cost/session-cost.js';
|
|
22
22
|
import { globalStore } from '../ui/sap-state-store.js';
|
|
23
|
+
import { globalCredits } from '../cost/credits.js';
|
|
23
24
|
import { printStatusFooter, formatStatusFooter } from '../renderer/status-footer.js';
|
|
24
25
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
25
26
|
export function snapshotTurnStart(session) {
|
|
@@ -71,6 +72,10 @@ export function pushPostTurnStatus(params) {
|
|
|
71
72
|
costThisTurn: turnCost,
|
|
72
73
|
costSession: sessionCost,
|
|
73
74
|
sessionInputTokens: session.usage.input_tokens,
|
|
75
|
+
// Credits (2026-09-16): the agent loop already sampled the post-turn
|
|
76
|
+
// balance, so this snapshot is fresh. formatStatusFooter ignores it
|
|
77
|
+
// entirely in USD mode — the dollar receipt stays byte-identical.
|
|
78
|
+
credits: globalCredits.snapshot(),
|
|
74
79
|
};
|
|
75
80
|
if (shouldUseInk() && chunkEmitter) {
|
|
76
81
|
// Push through chunkEmitter so the line lands in Body's <Static>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `spend:` row of the /exit session summary (2026-09-16).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from repl.tsx's printGoodbye so the credits/USD switch has one
|
|
5
|
+
* testable home. USD output is byte-identical to the line printed before
|
|
6
|
+
* credits existed: ` spend: $1.37`.
|
|
7
|
+
*/
|
|
8
|
+
import { formatCost } from '../cost/pricing.js';
|
|
9
|
+
import { creditsActive, formatCredits, CREDITS_PENDING } from '../cost/credits.js';
|
|
10
|
+
export function formatSessionSpendLine(sessionCostUsd, credits) {
|
|
11
|
+
if (!creditsActive())
|
|
12
|
+
return ` spend: ${formatCost(sessionCostUsd)}`;
|
|
13
|
+
if (!credits)
|
|
14
|
+
return ` spend: ${CREDITS_PENDING}`;
|
|
15
|
+
const value = formatCredits(credits.sessionTotal, credits.unitLabel);
|
|
16
|
+
const balance = credits.balance !== null
|
|
17
|
+
? ` · balance ${Math.round(credits.balance).toLocaleString('en-US')}`
|
|
18
|
+
: '';
|
|
19
|
+
return ` spend: ${value}${balance}`;
|
|
20
|
+
}
|
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';
|
|
@@ -20,7 +22,10 @@ import { makeSessionId, saveSession } from './session/store.js';
|
|
|
20
22
|
import { newSession } from './session/schema.js';
|
|
21
23
|
import { resolveModelRole } from './models/resolve.js';
|
|
22
24
|
import { loadServerModelConfig } from './models/server-config.js';
|
|
23
|
-
import { computeCost
|
|
25
|
+
import { computeCost } from './cost/pricing.js';
|
|
26
|
+
import { formatSessionSpendLine } from './repl/session-spend-line.js';
|
|
27
|
+
import { globalCredits } from './cost/credits.js';
|
|
28
|
+
import { initCredits } from './cost/credits-wire.js';
|
|
24
29
|
import { createProviderForMode } from './agent/providers/factory.js';
|
|
25
30
|
import { assertModeLicensed, CspeachLicenseError } from './agent/providers/license-gate.js';
|
|
26
31
|
import { runTurn } from './agent/loop.js';
|
|
@@ -240,7 +245,7 @@ function printGoodbye(session) {
|
|
|
240
245
|
console.log(chalk.dim(` turns: ${userTurns}`));
|
|
241
246
|
console.log(chalk.dim(` last skill: ${session.skill ?? '(none)'}`));
|
|
242
247
|
console.log(chalk.dim(` tokens: ${totalTokens.toLocaleString()} (input+output+cache)`));
|
|
243
|
-
console.log(chalk.dim(
|
|
248
|
+
console.log(chalk.dim(formatSessionSpendLine(sessionCost, globalCredits.snapshot())));
|
|
244
249
|
console.log('');
|
|
245
250
|
console.log(chalk.cyan(`Resume with: cspeach --resume ${session.id}`));
|
|
246
251
|
console.log(chalk.dim(`Full log: ~/.cspeach/streams/${session.id}.md`));
|
|
@@ -373,7 +378,26 @@ export async function runRepl(opts) {
|
|
|
373
378
|
let cfg = await loadConfig();
|
|
374
379
|
// Apply user-configured rendering override BEFORE any shouldUseInk() call.
|
|
375
380
|
setRenderingOverride(cfg.ui.rendering);
|
|
376
|
-
|
|
381
|
+
// Standalone (2026-09-16) — run with no SAP system at all. Decided BEFORE
|
|
382
|
+
// the onboarding wizard: someone who asked for --no-sap (or persisted
|
|
383
|
+
// no_sap) must never be pushed through the SAP setup they don't have.
|
|
384
|
+
// Having aliases configured does NOT override the request.
|
|
385
|
+
// A session that WAS standalone stays standalone on `--resume`: its
|
|
386
|
+
// sap_system is the reserved 'none' alias, which has no entry in cfg.sap.
|
|
387
|
+
const standalone = resolveStandalone(cfg, opts?.noSap === true) ||
|
|
388
|
+
opts?.resumedSession?.sap_system === STANDALONE_ALIAS;
|
|
389
|
+
if (standalone) {
|
|
390
|
+
console.log(chalk.dim('\nStandalone mode: no SAP system connected — skills work from pasted code. ' +
|
|
391
|
+
'Connect later with: cspeach config add-sap'));
|
|
392
|
+
// Ask the profile questions ONCE — they become the <sap_system> element
|
|
393
|
+
// every skill reads, so without them a design would target "unknown".
|
|
394
|
+
if (!cfg.standalone) {
|
|
395
|
+
const { runStandaloneProfileWizard } = await import('./sap/standalone-onboarding.js');
|
|
396
|
+
await runStandaloneProfileWizard({ cwd: process.cwd() });
|
|
397
|
+
cfg = await loadConfig();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else if (Object.keys(cfg.sap).length === 0) {
|
|
377
401
|
console.log(chalk.yellow('\nNo SAP systems configured. Starting onboarding wizard.'));
|
|
378
402
|
await runAddSapWizard();
|
|
379
403
|
cfg = await loadConfig();
|
|
@@ -383,6 +407,13 @@ export async function runRepl(opts) {
|
|
|
383
407
|
// fetch lands simply uses the built-ins (fail-safe). Precedence when it does
|
|
384
408
|
// land: env > local config > server > built-in.
|
|
385
409
|
void loadServerModelConfig(cfg);
|
|
410
|
+
// Credits (2026-09-16) — resolve ONCE, before any cost figure can render.
|
|
411
|
+
// A managed customer is billed in credits at a multiplier we do not
|
|
412
|
+
// disclose, so the dollar figures (our Anthropic COGS) must never reach
|
|
413
|
+
// their screen. Awaited, but hard-capped at 2.5s inside fetchBalance and
|
|
414
|
+
// fail-safe: any failure leaves the process in USD mode, i.e. exactly the
|
|
415
|
+
// pre-credits behaviour. BYOK/ai-hub/local skip the round-trip entirely.
|
|
416
|
+
await initCredits(cfg, async () => apiKey ?? '');
|
|
386
417
|
// Task C.10 — surface the current write_mode prominently. advisory-only gets
|
|
387
418
|
// a bold yellow banner (no-writes is a surprising-enough state that the user
|
|
388
419
|
// should not miss it). Other modes get a quiet one-liner so the state is
|
|
@@ -418,7 +449,12 @@ export async function runRepl(opts) {
|
|
|
418
449
|
const aliases = Object.keys(cfg.sap);
|
|
419
450
|
// C.1 — --sap-alias flag override skips the interactive picker
|
|
420
451
|
let selectedAlias;
|
|
421
|
-
if (
|
|
452
|
+
if (standalone) {
|
|
453
|
+
// No picker: there is nothing to pick. The reserved 'none' alias is what
|
|
454
|
+
// isStandalone(ctx) reads downstream.
|
|
455
|
+
selectedAlias = STANDALONE_ALIAS;
|
|
456
|
+
}
|
|
457
|
+
else if (opts?.sapAlias) {
|
|
422
458
|
console.log(chalk.dim(`Using --sap-alias ${opts.sapAlias}`));
|
|
423
459
|
selectedAlias = opts.sapAlias;
|
|
424
460
|
}
|
|
@@ -461,8 +497,11 @@ export async function runRepl(opts) {
|
|
|
461
497
|
console.log(chalk.gray(`Write mode: ${startupWriteMode}${capped}\n`));
|
|
462
498
|
}
|
|
463
499
|
}
|
|
464
|
-
|
|
465
|
-
|
|
500
|
+
// Standalone: the offline stand-in rejects every call with an actionable
|
|
501
|
+
// message. This is the single construction site the AdtClient cast lives at.
|
|
502
|
+
const adt = standalone
|
|
503
|
+
? createOfflineAdtClient()
|
|
504
|
+
: new AdtClient(await getConnection(selectedAlias, cfg.sap[selectedAlias]));
|
|
466
505
|
// Phase A #3 (2026-05-16) — sidebar SAP-state wire, trigger 1 of 4.
|
|
467
506
|
// Push system + user the moment we know the alias. Before this commit
|
|
468
507
|
// the sidebar showed "no system / user: —" forever because the store
|
|
@@ -475,8 +514,8 @@ export async function runRepl(opts) {
|
|
|
475
514
|
// superseded (unmounted) header.tsx — so nothing else loses the host;
|
|
476
515
|
// the connection banner above already printed it once to scrollback.
|
|
477
516
|
sapStateStore.update({
|
|
478
|
-
system: selectedAlias,
|
|
479
|
-
user: cfg.sap[selectedAlias].username,
|
|
517
|
+
system: standalone ? 'standalone' : selectedAlias,
|
|
518
|
+
user: standalone ? '' : cfg.sap[selectedAlias].username,
|
|
480
519
|
writeMode: startupWriteMode,
|
|
481
520
|
});
|
|
482
521
|
// v0.6 — when resuming, adopt the on-disk SessionState verbatim. This
|
|
@@ -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
|
+
}
|