@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,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project conventions reader.
|
|
3
|
+
*
|
|
4
|
+
* Source priority (highest first):
|
|
5
|
+
*
|
|
6
|
+
* 1. CLAUDE.md — preferred. Matches Claude Code's convention so
|
|
7
|
+
* developers who use both tools don't maintain two files.
|
|
8
|
+
* Read as free-text, included verbatim in `freeText`.
|
|
9
|
+
*
|
|
10
|
+
* 2. CSPEACH.md — optional CSPeach-specific override. Appended to
|
|
11
|
+
* CLAUDE.md content when both exist. Use when CSPeach should
|
|
12
|
+
* behave differently from Claude Code in the same repo (rare).
|
|
13
|
+
*
|
|
14
|
+
* 3. .cspeach/conventions.toml — structured overrides for naming
|
|
15
|
+
* patterns + style. Sits alongside the optional .cspeach/ workspace
|
|
16
|
+
* directory that already exists for project-rooted CSPeach files.
|
|
17
|
+
*
|
|
18
|
+
* 4. Inferred from existing files — when none of the above exist, we
|
|
19
|
+
* sample one or two files to detect indent + EOL. Naming patterns
|
|
20
|
+
* stay empty (best-effort inference is a follow-up; manual
|
|
21
|
+
* override via .cspeach/conventions.toml is the recommended path
|
|
22
|
+
* for projects that need it).
|
|
23
|
+
*
|
|
24
|
+
* Returns sensible defaults on every error. Never throws.
|
|
25
|
+
*/
|
|
26
|
+
import { promises as fs } from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
/**
|
|
29
|
+
* Default conventions used when no source has anything to say. Picked
|
|
30
|
+
* to match the most common conventions in modern SAP / web projects:
|
|
31
|
+
* 2-space indent, LF line endings, no inferred naming patterns.
|
|
32
|
+
*/
|
|
33
|
+
const DEFAULT_CONVENTIONS = {
|
|
34
|
+
freeText: null,
|
|
35
|
+
namingPatterns: {
|
|
36
|
+
classes: [],
|
|
37
|
+
interfaces: [],
|
|
38
|
+
cdsViews: [],
|
|
39
|
+
},
|
|
40
|
+
style: {
|
|
41
|
+
indentSize: 2,
|
|
42
|
+
indentChar: 'space',
|
|
43
|
+
eol: 'lf',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Read project conventions. Best-effort: returns DEFAULT_CONVENTIONS on
|
|
48
|
+
* any IO failure. Combines the four sources per priority order; later
|
|
49
|
+
* sources override earlier ones for structured fields, and append to
|
|
50
|
+
* `freeText` rather than replace it.
|
|
51
|
+
*/
|
|
52
|
+
export async function readConventions(cwd) {
|
|
53
|
+
const claudeBody = await readFileSafe(path.join(cwd, 'CLAUDE.md'));
|
|
54
|
+
const cspeachBody = await readFileSafe(path.join(cwd, 'CSPEACH.md'));
|
|
55
|
+
const freeText = combineFreeText(claudeBody, cspeachBody);
|
|
56
|
+
const tomlPath = path.join(cwd, '.cspeach', 'conventions.toml');
|
|
57
|
+
const tomlOverrides = await readConventionsToml(tomlPath);
|
|
58
|
+
const inferred = await inferStyleFromExistingFiles(cwd);
|
|
59
|
+
return {
|
|
60
|
+
freeText,
|
|
61
|
+
namingPatterns: {
|
|
62
|
+
classes: tomlOverrides?.namingPatterns?.classes ?? DEFAULT_CONVENTIONS.namingPatterns.classes,
|
|
63
|
+
interfaces: tomlOverrides?.namingPatterns?.interfaces ?? DEFAULT_CONVENTIONS.namingPatterns.interfaces,
|
|
64
|
+
cdsViews: tomlOverrides?.namingPatterns?.cdsViews ?? DEFAULT_CONVENTIONS.namingPatterns.cdsViews,
|
|
65
|
+
},
|
|
66
|
+
style: {
|
|
67
|
+
indentSize: tomlOverrides?.style?.indentSize ?? inferred?.indentSize ?? DEFAULT_CONVENTIONS.style.indentSize,
|
|
68
|
+
indentChar: tomlOverrides?.style?.indentChar ?? inferred?.indentChar ?? DEFAULT_CONVENTIONS.style.indentChar,
|
|
69
|
+
eol: tomlOverrides?.style?.eol ?? inferred?.eol ?? DEFAULT_CONVENTIONS.style.eol,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/* ── Free-text combination ─────────────────────────────────────────── */
|
|
74
|
+
function combineFreeText(claude, cspeach) {
|
|
75
|
+
if (claude === null && cspeach === null)
|
|
76
|
+
return null;
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (claude !== null) {
|
|
79
|
+
parts.push('## CLAUDE.md\n\n' + claude.trim());
|
|
80
|
+
}
|
|
81
|
+
if (cspeach !== null) {
|
|
82
|
+
parts.push('## CSPEACH.md\n\n' + cspeach.trim());
|
|
83
|
+
}
|
|
84
|
+
return parts.join('\n\n');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse `.cspeach/conventions.toml` if it exists. Returns null when the
|
|
88
|
+
* file is missing or malformed. We use a minimal TOML parser (just
|
|
89
|
+
* enough for the subset we accept) to avoid adding a dependency.
|
|
90
|
+
*/
|
|
91
|
+
async function readConventionsToml(p) {
|
|
92
|
+
const raw = await readFileSafe(p);
|
|
93
|
+
if (raw === null)
|
|
94
|
+
return null;
|
|
95
|
+
try {
|
|
96
|
+
return parseMinimalToml(raw);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Tiny TOML parser supporting only the subset we need:
|
|
104
|
+
* - Section headers: [namingPatterns], [style]
|
|
105
|
+
* - Key = "string", Key = 123, Key = ["a", "b"]
|
|
106
|
+
* - # comments to end of line
|
|
107
|
+
*
|
|
108
|
+
* We avoid pulling in a TOML dependency (e.g. `smol-toml`) because the
|
|
109
|
+
* surface we parse is tiny and a typo-tolerant minimal parser keeps the
|
|
110
|
+
* convention file from failing the whole context read.
|
|
111
|
+
*/
|
|
112
|
+
function parseMinimalToml(raw) {
|
|
113
|
+
const out = {};
|
|
114
|
+
let section = null;
|
|
115
|
+
for (const rawLine of raw.split('\n')) {
|
|
116
|
+
const line = rawLine.replace(/#.*$/, '').trim();
|
|
117
|
+
if (line.length === 0)
|
|
118
|
+
continue;
|
|
119
|
+
// Section header
|
|
120
|
+
const sectionMatch = /^\[([a-zA-Z_]+)\]$/.exec(line);
|
|
121
|
+
if (sectionMatch) {
|
|
122
|
+
const name = sectionMatch[1];
|
|
123
|
+
if (name === 'namingPatterns' || name === 'style') {
|
|
124
|
+
section = name;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
section = null;
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Key = value
|
|
132
|
+
const kvMatch = /^([a-zA-Z_]+)\s*=\s*(.+)$/.exec(line);
|
|
133
|
+
if (!kvMatch)
|
|
134
|
+
continue;
|
|
135
|
+
const key = kvMatch[1];
|
|
136
|
+
const valueRaw = kvMatch[2].trim();
|
|
137
|
+
if (section === 'namingPatterns') {
|
|
138
|
+
const arr = parseStringArray(valueRaw);
|
|
139
|
+
if (arr !== null) {
|
|
140
|
+
if (!out.namingPatterns)
|
|
141
|
+
out.namingPatterns = {};
|
|
142
|
+
if (key === 'classes')
|
|
143
|
+
out.namingPatterns.classes = arr;
|
|
144
|
+
else if (key === 'interfaces')
|
|
145
|
+
out.namingPatterns.interfaces = arr;
|
|
146
|
+
else if (key === 'cdsViews')
|
|
147
|
+
out.namingPatterns.cdsViews = arr;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (section === 'style') {
|
|
151
|
+
if (!out.style)
|
|
152
|
+
out.style = {};
|
|
153
|
+
if (key === 'indentSize') {
|
|
154
|
+
const n = Number.parseInt(valueRaw, 10);
|
|
155
|
+
if (Number.isFinite(n) && n > 0)
|
|
156
|
+
out.style.indentSize = n;
|
|
157
|
+
}
|
|
158
|
+
else if (key === 'indentChar') {
|
|
159
|
+
const s = parseString(valueRaw);
|
|
160
|
+
if (s === 'tab' || s === 'space')
|
|
161
|
+
out.style.indentChar = s;
|
|
162
|
+
}
|
|
163
|
+
else if (key === 'eol') {
|
|
164
|
+
const s = parseString(valueRaw);
|
|
165
|
+
if (s === 'lf' || s === 'crlf')
|
|
166
|
+
out.style.eol = s;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
function parseString(v) {
|
|
173
|
+
const m = /^"([^"]*)"$/.exec(v) ?? /^'([^']*)'$/.exec(v);
|
|
174
|
+
return m ? m[1] : null;
|
|
175
|
+
}
|
|
176
|
+
function parseStringArray(v) {
|
|
177
|
+
if (!v.startsWith('[') || !v.endsWith(']'))
|
|
178
|
+
return null;
|
|
179
|
+
const inner = v.slice(1, -1).trim();
|
|
180
|
+
if (inner.length === 0)
|
|
181
|
+
return [];
|
|
182
|
+
const items = [];
|
|
183
|
+
// Simple split — doesn't handle escaped commas, which is fine for our subset.
|
|
184
|
+
for (const part of inner.split(',')) {
|
|
185
|
+
const s = parseString(part.trim());
|
|
186
|
+
if (s === null)
|
|
187
|
+
return null;
|
|
188
|
+
items.push(s);
|
|
189
|
+
}
|
|
190
|
+
return items;
|
|
191
|
+
}
|
|
192
|
+
/* ── Style inference ───────────────────────────────────────────────── */
|
|
193
|
+
/**
|
|
194
|
+
* Sample one or two text files in the project tree and infer indent +
|
|
195
|
+
* EOL. Used only when conventions.toml doesn't override. Cheap heuristic
|
|
196
|
+
* — peek at the first few hundred bytes of the first 2 files we find.
|
|
197
|
+
*
|
|
198
|
+
* Returns null on any failure; caller falls back to DEFAULT_CONVENTIONS.style.
|
|
199
|
+
*/
|
|
200
|
+
async function inferStyleFromExistingFiles(cwd) {
|
|
201
|
+
try {
|
|
202
|
+
const samplePaths = await findSampleFiles(cwd, 2);
|
|
203
|
+
if (samplePaths.length === 0)
|
|
204
|
+
return null;
|
|
205
|
+
let indentChar;
|
|
206
|
+
let indentSize;
|
|
207
|
+
let eol;
|
|
208
|
+
for (const p of samplePaths) {
|
|
209
|
+
try {
|
|
210
|
+
const raw = await fs.readFile(p, 'utf-8');
|
|
211
|
+
const head = raw.slice(0, 4096);
|
|
212
|
+
// EOL: presence of \r\n means CRLF.
|
|
213
|
+
if (eol === undefined) {
|
|
214
|
+
eol = head.includes('\r\n') ? 'crlf' : 'lf';
|
|
215
|
+
}
|
|
216
|
+
// Indent: scan for the first CODE-shaped indented line. Tab vs
|
|
217
|
+
// space is determined by what character appears first.
|
|
218
|
+
//
|
|
219
|
+
// Skip JSDoc / block-comment continuation lines (they start with
|
|
220
|
+
// ` *` — one space + asterisk — which would otherwise be misread
|
|
221
|
+
// as "1-space indent." This was a real dogfooding finding: most
|
|
222
|
+
// CSPeach .ts files start with a `/** ... */` docblock; the very
|
|
223
|
+
// first indented line was the ` * comment` continuation, and the
|
|
224
|
+
// detector reported indentSize=1.
|
|
225
|
+
if (indentChar === undefined) {
|
|
226
|
+
for (const line of head.split('\n')) {
|
|
227
|
+
if (line.length === 0)
|
|
228
|
+
continue;
|
|
229
|
+
// Skip block-comment continuation: `\s*\*\s` pattern (any
|
|
230
|
+
// amount of leading whitespace, then `*`, then a space).
|
|
231
|
+
if (/^\s*\*(\s|$|\/)/.test(line))
|
|
232
|
+
continue;
|
|
233
|
+
// Also skip pure-whitespace lines (some editors leave
|
|
234
|
+
// trailing whitespace inside docblocks).
|
|
235
|
+
if (/^\s*$/.test(line))
|
|
236
|
+
continue;
|
|
237
|
+
const ch = line[0];
|
|
238
|
+
if (ch === '\t') {
|
|
239
|
+
indentChar = 'tab';
|
|
240
|
+
indentSize = 1;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
if (ch === ' ') {
|
|
244
|
+
indentChar = 'space';
|
|
245
|
+
let count = 0;
|
|
246
|
+
while (count < line.length && line[count] === ' ')
|
|
247
|
+
count++;
|
|
248
|
+
indentSize = count > 0 ? count : 2;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Skip unreadable file
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (indentChar === undefined && eol === undefined)
|
|
259
|
+
return null;
|
|
260
|
+
return { indentChar, indentSize, eol };
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Find up to `limit` text-ish source files for sampling. Skips
|
|
268
|
+
* node_modules, .git, dist, build, .cspeach output dirs. Prefers files
|
|
269
|
+
* likely to reflect human-authored style (ts, js, abap, cds, xml).
|
|
270
|
+
*/
|
|
271
|
+
async function findSampleFiles(cwd, limit) {
|
|
272
|
+
const out = [];
|
|
273
|
+
const TEXT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.abap', '.cds', '.xml', '.yaml', '.yml'];
|
|
274
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.cspeach', '.abapforge']);
|
|
275
|
+
async function walk(d) {
|
|
276
|
+
if (out.length >= limit)
|
|
277
|
+
return;
|
|
278
|
+
let entries;
|
|
279
|
+
try {
|
|
280
|
+
entries = await fs.readdir(d, { withFileTypes: true });
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const ent of entries) {
|
|
286
|
+
if (out.length >= limit)
|
|
287
|
+
return;
|
|
288
|
+
if (ent.isFile()) {
|
|
289
|
+
if (TEXT_EXTENSIONS.some((ext) => ent.name.endsWith(ext))) {
|
|
290
|
+
out.push(path.join(d, ent.name));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else if (ent.isDirectory() && !SKIP_DIRS.has(ent.name)) {
|
|
294
|
+
await walk(path.join(d, ent.name));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
await walk(cwd);
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
/* ── Small filesystem helper ───────────────────────────────────────── */
|
|
302
|
+
async function readFileSafe(p) {
|
|
303
|
+
try {
|
|
304
|
+
return await fs.readFile(p, 'utf-8');
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-type detection — identifies what kind of SAP/development
|
|
3
|
+
* project lives at a given directory.
|
|
4
|
+
*
|
|
5
|
+
* Detection is purely structural — it looks at files + directories,
|
|
6
|
+
* not contents (except parsing one package.json for the Fiori/CAP
|
|
7
|
+
* discrimination). The function never runs ABAP, never connects to
|
|
8
|
+
* SAP, never modifies anything. Pure read-only filesystem inspection.
|
|
9
|
+
*
|
|
10
|
+
* Priority order matters: a CAP project that happens to contain a
|
|
11
|
+
* `webapp/` directory should still classify as CAP, not Fiori. The
|
|
12
|
+
* priority below is calibrated to that case.
|
|
13
|
+
*/
|
|
14
|
+
import { promises as fs } from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
/**
|
|
17
|
+
* Detect the project type at the given working directory.
|
|
18
|
+
*
|
|
19
|
+
* Priority (first match wins):
|
|
20
|
+
* 1. `mta.yaml` at root → mixed (multi-target application)
|
|
21
|
+
* 2. CAP Node: `package.json` with `@sap/cds` + `srv/` + `db/` dirs
|
|
22
|
+
* 3. Fiori UI5: `webapp/manifest.json` + (`ui5.yaml` OR
|
|
23
|
+
* `package.json` with `@sap/ux-ui5-tooling`)
|
|
24
|
+
* 4. abapGit: `.abapgit.xml` at root
|
|
25
|
+
* OR `src/` containing `*.prog.abap` / `*.clas.abap` / `*.intf.abap`
|
|
26
|
+
* 5. ABAP Cloud: same disk shape as abapGit, but with `.bdef` /
|
|
27
|
+
* `.behaviordefinition` / `.dcl` / `.ddls` files indicating cloud
|
|
28
|
+
* ABAP patterns
|
|
29
|
+
* 6. Anything else → `unknown`
|
|
30
|
+
*
|
|
31
|
+
* Returns null on IO errors (missing dir, permission denied, etc.) so
|
|
32
|
+
* the caller can decide whether to treat null as "no project" or "error
|
|
33
|
+
* reading project." Today's only consumer in Phase 1A treats both as
|
|
34
|
+
* "no project context" — that semantic is intentional.
|
|
35
|
+
*/
|
|
36
|
+
export async function detectProjectType(cwd) {
|
|
37
|
+
try {
|
|
38
|
+
const stat = await fs.stat(cwd);
|
|
39
|
+
if (!stat.isDirectory())
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// cwd doesn't exist or isn't accessible → not a project.
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
// 1. Mixed (mta.yaml at root)
|
|
47
|
+
if (await fileExists(path.join(cwd, 'mta.yaml'))) {
|
|
48
|
+
return 'mixed';
|
|
49
|
+
}
|
|
50
|
+
// 2. CAP Node (checked BEFORE Fiori — a CAP project may have a webapp/
|
|
51
|
+
// subdirectory under its `app/` folder, which would otherwise match
|
|
52
|
+
// Fiori detection).
|
|
53
|
+
if (await isCapNode(cwd)) {
|
|
54
|
+
return 'cap-node';
|
|
55
|
+
}
|
|
56
|
+
// 3. Fiori UI5 (webapp/manifest.json + ui5.yaml or package.json with
|
|
57
|
+
// @sap/ux-ui5-tooling)
|
|
58
|
+
if (await isFioriUi5(cwd)) {
|
|
59
|
+
return 'fiori-ui5';
|
|
60
|
+
}
|
|
61
|
+
// 4 & 5. abapGit and ABAP Cloud share the same on-disk shape
|
|
62
|
+
// (src/*.<type>.abap). We discriminate by the presence of
|
|
63
|
+
// cloud-specific extensions.
|
|
64
|
+
if (await isAbapDiskProject(cwd)) {
|
|
65
|
+
if (await hasAbapCloudMarkers(cwd)) {
|
|
66
|
+
return 'abap-cloud';
|
|
67
|
+
}
|
|
68
|
+
return 'abapgit';
|
|
69
|
+
}
|
|
70
|
+
// 6. Nothing matched.
|
|
71
|
+
return 'unknown';
|
|
72
|
+
}
|
|
73
|
+
/* ── Per-type detection helpers ─────────────────────────────────────── */
|
|
74
|
+
async function isCapNode(cwd) {
|
|
75
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
76
|
+
const pkg = await readJsonSafe(pkgPath);
|
|
77
|
+
if (!pkg)
|
|
78
|
+
return false;
|
|
79
|
+
const deps = collectDeps(pkg);
|
|
80
|
+
if (!('@sap/cds' in deps))
|
|
81
|
+
return false;
|
|
82
|
+
// CAP convention: srv/ for service definitions, db/ for entity models
|
|
83
|
+
return ((await directoryExists(path.join(cwd, 'srv'))) &&
|
|
84
|
+
(await directoryExists(path.join(cwd, 'db'))));
|
|
85
|
+
}
|
|
86
|
+
async function isFioriUi5(cwd) {
|
|
87
|
+
// Must have webapp/manifest.json
|
|
88
|
+
if (!(await fileExists(path.join(cwd, 'webapp', 'manifest.json')))) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
// PLUS either ui5.yaml OR package.json with @sap/ux-ui5-tooling
|
|
92
|
+
if (await fileExists(path.join(cwd, 'ui5.yaml'))) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const pkg = await readJsonSafe(path.join(cwd, 'package.json'));
|
|
96
|
+
if (pkg) {
|
|
97
|
+
const deps = collectDeps(pkg);
|
|
98
|
+
if ('@sap/ux-ui5-tooling' in deps)
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* True if a `src/` directory exists and contains at least one ABAP
|
|
105
|
+
* source file. Accepts BOTH classic abapGit naming (.prog.abap,
|
|
106
|
+
* .clas.abap, etc.) AND ABAP-Cloud-specific naming (.bdef.asbdef,
|
|
107
|
+
* .srvd.srvd, .dcls.asdcls, etc.) — a cloud-only project still
|
|
108
|
+
* qualifies as a disk-project; discrimination of cloud-vs-classic
|
|
109
|
+
* happens in the calling code via `hasAbapCloudMarkers`.
|
|
110
|
+
*/
|
|
111
|
+
async function isAbapDiskProject(cwd) {
|
|
112
|
+
// .abapgit.xml at root is a strong signal regardless of src/ contents.
|
|
113
|
+
if (await fileExists(path.join(cwd, '.abapgit.xml'))) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
const srcDir = path.join(cwd, 'src');
|
|
117
|
+
if (!(await directoryExists(srcDir)))
|
|
118
|
+
return false;
|
|
119
|
+
// Check both extension catalogs — a project with ONLY cloud-specific
|
|
120
|
+
// markers (.bdef, .srvd, .dcls, etc.) should still qualify as a
|
|
121
|
+
// disk-project, otherwise hasAbapCloudMarkers never gets to run.
|
|
122
|
+
const allAbapExtensions = [
|
|
123
|
+
...ABAP_DISK_EXTENSIONS,
|
|
124
|
+
...ABAP_CLOUD_EXTENSIONS,
|
|
125
|
+
];
|
|
126
|
+
return await directoryContainsExtension(srcDir, allAbapExtensions, /* recursive */ true);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Cloud-specific markers that distinguish ABAP Cloud / RAP-on-cloud
|
|
130
|
+
* (on-disk) from a generic abapGit working copy. Behavior definitions
|
|
131
|
+
* (`.bdef.abap` / `.behaviordefinition.bdef`) and ABAP Cloud package
|
|
132
|
+
* markers are the canonical signal.
|
|
133
|
+
*/
|
|
134
|
+
async function hasAbapCloudMarkers(cwd) {
|
|
135
|
+
const srcDir = path.join(cwd, 'src');
|
|
136
|
+
return await directoryContainsExtension(srcDir, ABAP_CLOUD_EXTENSIONS, /* recursive */ true);
|
|
137
|
+
}
|
|
138
|
+
/* ── File-system primitives ─────────────────────────────────────────── */
|
|
139
|
+
async function fileExists(p) {
|
|
140
|
+
try {
|
|
141
|
+
const s = await fs.stat(p);
|
|
142
|
+
return s.isFile();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function directoryExists(p) {
|
|
149
|
+
try {
|
|
150
|
+
const s = await fs.stat(p);
|
|
151
|
+
return s.isDirectory();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function readJsonSafe(p) {
|
|
158
|
+
try {
|
|
159
|
+
const raw = await fs.readFile(p, 'utf-8');
|
|
160
|
+
return JSON.parse(raw);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Merge dependencies + devDependencies + peerDependencies into one
|
|
168
|
+
* lookup. Returns empty record if pkg is malformed.
|
|
169
|
+
*/
|
|
170
|
+
function collectDeps(pkg) {
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
173
|
+
const block = pkg[field];
|
|
174
|
+
if (block && typeof block === 'object') {
|
|
175
|
+
for (const [k, v] of Object.entries(block)) {
|
|
176
|
+
if (typeof v === 'string')
|
|
177
|
+
out[k] = v;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* True if any file under `dir` ends with one of the given extensions.
|
|
185
|
+
* Recursive walk; bails after first hit. Cap at 5000 dir entries
|
|
186
|
+
* checked total to prevent runaway walks on very large trees.
|
|
187
|
+
*/
|
|
188
|
+
async function directoryContainsExtension(dir, extensions, recursive) {
|
|
189
|
+
let budget = 5000;
|
|
190
|
+
async function walk(d) {
|
|
191
|
+
if (budget <= 0)
|
|
192
|
+
return false;
|
|
193
|
+
let entries;
|
|
194
|
+
try {
|
|
195
|
+
entries = await fs.readdir(d, { withFileTypes: true });
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
for (const ent of entries) {
|
|
201
|
+
if (budget <= 0)
|
|
202
|
+
return false;
|
|
203
|
+
budget--;
|
|
204
|
+
if (ent.isFile()) {
|
|
205
|
+
for (const ext of extensions) {
|
|
206
|
+
if (ent.name.endsWith(ext))
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else if (recursive && ent.isDirectory()) {
|
|
211
|
+
// Skip noisy dirs that shouldn't be walked.
|
|
212
|
+
if (ent.name === 'node_modules' || ent.name === '.git')
|
|
213
|
+
continue;
|
|
214
|
+
if (await walk(path.join(d, ent.name)))
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
return walk(dir);
|
|
221
|
+
}
|
|
222
|
+
/* ── Extension catalogs ─────────────────────────────────────────────── */
|
|
223
|
+
/**
|
|
224
|
+
* abapGit naming convention: <object_name>.<type>.abap and friends.
|
|
225
|
+
* Object types we accept as evidence of an ABAP working copy.
|
|
226
|
+
*/
|
|
227
|
+
const ABAP_DISK_EXTENSIONS = [
|
|
228
|
+
'.prog.abap', // programs (executable + module-pool)
|
|
229
|
+
'.clas.abap', // classes
|
|
230
|
+
'.intf.abap', // interfaces
|
|
231
|
+
'.fugr.abap', // function groups
|
|
232
|
+
'.tabl.xml', // tables (DDIC)
|
|
233
|
+
'.dtel.xml', // data elements
|
|
234
|
+
'.doma.xml', // domains
|
|
235
|
+
'.ddls.asddls', // CDS views (interface DDL source)
|
|
236
|
+
'.ddls.asccds', // CDS views (consumption DDL source, abapGit naming)
|
|
237
|
+
];
|
|
238
|
+
/**
|
|
239
|
+
* ABAP Cloud / RAP-on-disk specific markers. Presence of any of these
|
|
240
|
+
* tells us we're looking at ABAP Cloud rather than classic abapGit
|
|
241
|
+
* (even though the two share the disk layout under src/).
|
|
242
|
+
*/
|
|
243
|
+
const ABAP_CLOUD_EXTENSIONS = [
|
|
244
|
+
'.bdef.asbdef', // RAP behavior definition (cloud)
|
|
245
|
+
'.behaviordefinition', // alternate naming some projects use
|
|
246
|
+
'.dcls.asdcls', // CDS access control / DCL
|
|
247
|
+
'.ddlx.asddlx', // metadata extension
|
|
248
|
+
'.srvb.srvb', // service binding
|
|
249
|
+
'.srvd.srvd', // service definition
|
|
250
|
+
];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ABAP Cloud / RAP-on-disk domain extractor.
|
|
3
|
+
*
|
|
4
|
+
* Builds on the abapGit walker — same on-disk shape, same filename
|
|
5
|
+
* convention, same package inference. The only delta is that ABAP Cloud
|
|
6
|
+
* projects emit RAP-specific objects (behavior definitions, service
|
|
7
|
+
* definitions, service bindings, DCL access controls, metadata
|
|
8
|
+
* extensions) that we surface as a separate concept.
|
|
9
|
+
*
|
|
10
|
+
* For Phase 1E we promote behavior definitions specifically — they're
|
|
11
|
+
* the entry point a RAP-aware skill needs to know about most often.
|
|
12
|
+
* Other RAP objects (SRVD, SRVB, DCLS, DDLX) are still surfaced through
|
|
13
|
+
* the generic objects[] list, just not called out separately.
|
|
14
|
+
*/
|
|
15
|
+
import { collectAbapObjects } from './abapgit.js';
|
|
16
|
+
/**
|
|
17
|
+
* Walk `src/` under cwd and return the abap-cloud DomainObjectIndex
|
|
18
|
+
* variant: full object list plus the names of all behavior definitions.
|
|
19
|
+
*/
|
|
20
|
+
export async function extractAbapCloud(cwd) {
|
|
21
|
+
const objects = await collectAbapObjects(cwd);
|
|
22
|
+
const behaviorDefinitions = objects
|
|
23
|
+
.filter((o) => o.type === 'BDEF')
|
|
24
|
+
.map((o) => o.name);
|
|
25
|
+
return { type: 'abap-cloud', objects, behaviorDefinitions };
|
|
26
|
+
}
|