@saptools/service-flow 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,804 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ discoverRepositories,
4
+ linkWorkspace,
5
+ normalizePath,
6
+ parseCdsFile,
7
+ parseDecorators,
8
+ parseHandlerRegistrations,
9
+ parseOutboundCalls,
10
+ parsePackageJson,
11
+ parseServiceBindings,
12
+ trace
13
+ } from "./chunk-RFBHT6BQ.js";
14
+
15
+ // src/cli.ts
16
+ import { Command } from "commander";
17
+ import path6 from "path";
18
+ import fs6 from "fs/promises";
19
+ import pc from "picocolors";
20
+
21
+ // src/config/defaults.ts
22
+ var DEFAULT_IGNORES = [
23
+ "node_modules",
24
+ "gen",
25
+ "dist",
26
+ "coverage",
27
+ ".git",
28
+ ".turbo",
29
+ ".next",
30
+ ".cache",
31
+ ".service-flow"
32
+ ];
33
+ var CONFIG_DIR = ".service-flow";
34
+ var CONFIG_FILE = "config.json";
35
+ var DEFAULT_DB_FILE = "service-flow.db";
36
+
37
+ // src/config/workspace-config.ts
38
+ import fs from "fs/promises";
39
+ import path from "path";
40
+ import { z } from "zod";
41
+ var schema = z.object({
42
+ rootPath: z.string(),
43
+ dbPath: z.string(),
44
+ ignore: z.array(z.string()),
45
+ createdAt: z.string(),
46
+ updatedAt: z.string()
47
+ });
48
+ function configPath(rootPath) {
49
+ return path.join(rootPath, CONFIG_DIR, CONFIG_FILE);
50
+ }
51
+ function defaultDbPath(rootPath) {
52
+ return path.join(rootPath, CONFIG_DIR, DEFAULT_DB_FILE);
53
+ }
54
+ async function saveWorkspaceConfig(config) {
55
+ await fs.mkdir(path.dirname(configPath(config.rootPath)), {
56
+ recursive: true
57
+ });
58
+ await fs.writeFile(
59
+ configPath(config.rootPath),
60
+ `${JSON.stringify(config, null, 2)}
61
+ `
62
+ );
63
+ }
64
+ async function loadWorkspaceConfig(workspace) {
65
+ const root = path.resolve(workspace ?? process.cwd());
66
+ const data = await fs.readFile(configPath(root), "utf8");
67
+ return schema.parse(JSON.parse(data));
68
+ }
69
+ function createWorkspaceConfig(rootPath, dbPath, ignore = [...DEFAULT_IGNORES]) {
70
+ const now = (/* @__PURE__ */ new Date()).toISOString();
71
+ const root = path.resolve(rootPath);
72
+ return {
73
+ rootPath: root,
74
+ dbPath: path.resolve(dbPath ?? defaultDbPath(root)),
75
+ ignore,
76
+ createdAt: now,
77
+ updatedAt: now
78
+ };
79
+ }
80
+
81
+ // src/db/connection.ts
82
+ import { execFileSync } from "child_process";
83
+ import fs2 from "fs";
84
+ import path2 from "path";
85
+
86
+ // src/db/schema.ts
87
+ var schemaSql = `
88
+ CREATE TABLE IF NOT EXISTS workspaces (id INTEGER PRIMARY KEY, root_path TEXT UNIQUE NOT NULL, db_path TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
89
+ CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, UNIQUE(workspace_id, absolute_path));
90
+ CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, relative_path TEXT NOT NULL, extension TEXT NOT NULL, sha256 TEXT NOT NULL, size_bytes INTEGER NOT NULL, last_indexed_at TEXT NOT NULL, UNIQUE(repo_id, relative_path));
91
+ CREATE TABLE IF NOT EXISTS cds_requires (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, alias TEXT NOT NULL, kind TEXT, model TEXT, destination TEXT, service_path TEXT, request_timeout INTEGER, raw_json TEXT NOT NULL);
92
+ CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL);
93
+ CREATE TABLE IF NOT EXISTS cds_operations (id INTEGER PRIMARY KEY, service_id INTEGER NOT NULL, operation_type TEXT NOT NULL, operation_name TEXT NOT NULL, operation_path TEXT NOT NULL, params_json TEXT NOT NULL, return_type TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL);
94
+ CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL);
95
+ CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, class_name TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL);
96
+ CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL);
97
+ CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL);
98
+ CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL);
99
+ CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT);
100
+ CREATE TABLE IF NOT EXISTS graph_edges (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, edge_type TEXT NOT NULL, from_kind TEXT NOT NULL, from_id TEXT NOT NULL, to_kind TEXT NOT NULL, to_id TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, is_dynamic INTEGER NOT NULL, unresolved_reason TEXT);
101
+ CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL);
102
+ CREATE TABLE IF NOT EXISTS diagnostics (id INTEGER PRIMARY KEY, repo_id INTEGER, file_id INTEGER, severity TEXT NOT NULL, code TEXT NOT NULL, message TEXT NOT NULL, source_file TEXT, source_line INTEGER);
103
+ CREATE INDEX IF NOT EXISTS idx_repo_name ON repositories(name);
104
+ CREATE INDEX IF NOT EXISTS idx_service_path ON cds_services(service_path);
105
+ CREATE INDEX IF NOT EXISTS idx_operation_name ON cds_operations(operation_name, operation_path);
106
+ CREATE INDEX IF NOT EXISTS idx_calls_repo ON outbound_calls(repo_id, call_type);
107
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, repo);
108
+ `;
109
+
110
+ // src/db/migrations.ts
111
+ function migrate(db) {
112
+ db.exec(schemaSql);
113
+ }
114
+
115
+ // src/db/connection.ts
116
+ function quote(value) {
117
+ if (value === null || value === void 0) return "NULL";
118
+ if (typeof value === "number")
119
+ return Number.isFinite(value) ? String(value) : "NULL";
120
+ if (typeof value === "boolean") return value ? "1" : "0";
121
+ return `'${String(value).replaceAll("'", "''")}'`;
122
+ }
123
+ function bind(sql, params) {
124
+ let index = 0;
125
+ return sql.replaceAll("?", () => quote(params[index++]));
126
+ }
127
+ function call(dbPath, args) {
128
+ return execFileSync("sqlite3", [dbPath, ...args], { encoding: "utf8" });
129
+ }
130
+ function jsonRows(dbPath, sql) {
131
+ const out = call(dbPath, ["-json", sql]);
132
+ if (!out.trim()) return [];
133
+ return JSON.parse(out);
134
+ }
135
+ function openDatabase(dbPath) {
136
+ fs2.mkdirSync(path2.dirname(dbPath), { recursive: true });
137
+ const db = {
138
+ path: dbPath,
139
+ exec(sql) {
140
+ call(dbPath, [sql]);
141
+ },
142
+ prepare(sql) {
143
+ return {
144
+ run: (...params) => {
145
+ call(dbPath, [bind(sql, params)]);
146
+ return { changes: 0 };
147
+ },
148
+ get: (...params) => jsonRows(dbPath, bind(sql, params))[0],
149
+ all: (...params) => jsonRows(dbPath, bind(sql, params))
150
+ };
151
+ },
152
+ pragma(sql) {
153
+ call(dbPath, [`PRAGMA ${sql}`]);
154
+ },
155
+ close() {
156
+ }
157
+ };
158
+ db.pragma("journal_mode = WAL");
159
+ db.pragma("foreign_keys = ON");
160
+ migrate(db);
161
+ return db;
162
+ }
163
+
164
+ // src/db/repositories.ts
165
+ function upsertWorkspace(db, rootPath, dbPath) {
166
+ const now = (/* @__PURE__ */ new Date()).toISOString();
167
+ db.prepare(
168
+ "INSERT INTO workspaces(root_path,db_path,created_at,updated_at) VALUES(?,?,?,?) ON CONFLICT(root_path) DO UPDATE SET db_path=excluded.db_path,updated_at=excluded.updated_at"
169
+ ).run(rootPath, dbPath, now, now);
170
+ return Number(
171
+ db.prepare("SELECT id FROM workspaces WHERE root_path=?").get(rootPath)?.id
172
+ );
173
+ }
174
+ function getWorkspace(db, rootPath) {
175
+ return db.prepare("SELECT * FROM workspaces WHERE root_path=?").get(rootPath);
176
+ }
177
+ function upsertRepository(db, workspaceId, r) {
178
+ db.prepare(
179
+ `INSERT INTO repositories(workspace_id,name,absolute_path,relative_path,package_name,package_version,dependencies_json,kind,is_git_repo) VALUES(?,?,?,?,?,?,?,?,?) ON CONFLICT(workspace_id,absolute_path) DO UPDATE SET name=excluded.name,relative_path=excluded.relative_path,package_name=excluded.package_name,package_version=excluded.package_version,dependencies_json=excluded.dependencies_json,kind=excluded.kind`
180
+ ).run(
181
+ workspaceId,
182
+ r.name,
183
+ r.absolutePath,
184
+ r.relativePath,
185
+ r.packageName,
186
+ r.packageVersion,
187
+ JSON.stringify(r.dependencies ?? {}),
188
+ r.kind ?? "unknown",
189
+ r.isGitRepo ? 1 : 0
190
+ );
191
+ return Number(
192
+ db.prepare(
193
+ "SELECT id FROM repositories WHERE workspace_id=? AND absolute_path=?"
194
+ ).get(workspaceId, r.absolutePath)?.id
195
+ );
196
+ }
197
+ function listRepositories(db) {
198
+ return db.prepare("SELECT * FROM repositories ORDER BY name").all();
199
+ }
200
+ function repoByName(db, name) {
201
+ return db.prepare("SELECT * FROM repositories WHERE name=? OR package_name=?").get(name, name);
202
+ }
203
+ function clearRepoFacts(db, repoId) {
204
+ for (const t of [
205
+ "cds_requires",
206
+ "cds_services",
207
+ "symbols",
208
+ "handler_classes",
209
+ "handler_registrations",
210
+ "service_bindings",
211
+ "outbound_calls",
212
+ "diagnostics",
213
+ "files"
214
+ ])
215
+ db.prepare(`DELETE FROM ${t} WHERE repo_id=?`).run(repoId);
216
+ }
217
+ function insertRequires(db, repoId, rows) {
218
+ const stmt = db.prepare(
219
+ "INSERT INTO cds_requires(repo_id,alias,kind,model,destination,service_path,request_timeout,raw_json) VALUES(?,?,?,?,?,?,?,?)"
220
+ );
221
+ for (const r of rows)
222
+ stmt.run(
223
+ repoId,
224
+ r.alias,
225
+ r.kind,
226
+ r.model,
227
+ r.destination,
228
+ r.servicePath,
229
+ r.requestTimeout,
230
+ r.rawJson
231
+ );
232
+ }
233
+ function insertService(db, repoId, s) {
234
+ const id = Number(
235
+ db.prepare(
236
+ "INSERT INTO cds_services(repo_id,namespace,service_name,qualified_name,service_path,is_extend,source_file,source_line) VALUES(?,?,?,?,?,?,?,?) RETURNING id"
237
+ ).get(
238
+ repoId,
239
+ s.namespace,
240
+ s.serviceName,
241
+ s.qualifiedName,
242
+ s.servicePath,
243
+ s.isExtend ? 1 : 0,
244
+ s.sourceFile,
245
+ s.sourceLine
246
+ )?.id
247
+ );
248
+ const stmt = db.prepare(
249
+ "INSERT INTO cds_operations(service_id,operation_type,operation_name,operation_path,params_json,return_type,source_file,source_line) VALUES(?,?,?,?,?,?,?,?)"
250
+ );
251
+ for (const o of s.operations)
252
+ stmt.run(
253
+ id,
254
+ o.operationType,
255
+ o.operationName,
256
+ o.operationPath,
257
+ o.paramsJson,
258
+ o.returnType,
259
+ o.sourceFile,
260
+ o.sourceLine
261
+ );
262
+ return id;
263
+ }
264
+ function insertHandler(db, repoId, h) {
265
+ const sid = Number(
266
+ db.prepare(
267
+ "INSERT INTO symbols(repo_id,kind,name,qualified_name,exported,start_line,end_line) VALUES(?,?,?,?,?,?,?) RETURNING id"
268
+ ).get(
269
+ repoId,
270
+ "class",
271
+ h.className,
272
+ h.className,
273
+ 1,
274
+ h.sourceLine,
275
+ h.sourceLine
276
+ )?.id
277
+ );
278
+ const hid = Number(
279
+ db.prepare(
280
+ "INSERT INTO handler_classes(repo_id,symbol_id,class_name,source_file,source_line) VALUES(?,?,?,?,?) RETURNING id"
281
+ ).get(repoId, sid, h.className, h.sourceFile, h.sourceLine)?.id
282
+ );
283
+ const stmt = db.prepare(
284
+ "INSERT INTO handler_methods(handler_class_id,method_name,decorator_kind,decorator_value,decorator_raw_expression,source_file,source_line) VALUES(?,?,?,?,?,?,?)"
285
+ );
286
+ for (const m of h.methods)
287
+ stmt.run(
288
+ hid,
289
+ m.methodName,
290
+ m.decoratorKind,
291
+ m.decoratorValue,
292
+ m.decoratorRawExpression,
293
+ m.sourceFile,
294
+ m.sourceLine
295
+ );
296
+ return hid;
297
+ }
298
+ function insertRegistrations(db, repoId, rows) {
299
+ const stmt = db.prepare(
300
+ "INSERT INTO handler_registrations(repo_id,handler_class_id,registration_file,registration_line,registration_kind,confidence) VALUES(?,?,?,?,?,?)"
301
+ );
302
+ for (const r of rows)
303
+ stmt.run(
304
+ repoId,
305
+ null,
306
+ r.registrationFile,
307
+ r.registrationLine,
308
+ r.registrationKind,
309
+ r.confidence
310
+ );
311
+ }
312
+ function insertBindings(db, repoId, rows) {
313
+ const stmt = db.prepare(
314
+ "INSERT INTO service_bindings(repo_id,variable_name,alias,destination_expr,service_path_expr,is_dynamic,placeholders_json,source_file,source_line) VALUES(?,?,?,?,?,?,?,?,?)"
315
+ );
316
+ for (const r of rows)
317
+ stmt.run(
318
+ repoId,
319
+ r.variableName,
320
+ r.alias,
321
+ r.destinationExpr,
322
+ r.servicePathExpr,
323
+ r.isDynamic ? 1 : 0,
324
+ JSON.stringify(r.placeholders),
325
+ r.sourceFile,
326
+ r.sourceLine
327
+ );
328
+ }
329
+ function insertCalls(db, repoId, rows) {
330
+ const stmt = db.prepare(
331
+ "INSERT INTO outbound_calls(repo_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,service_binding_id) VALUES(?,?,?,?,?,?,?,?,?,?,?,(SELECT id FROM service_bindings WHERE repo_id=? AND variable_name=? ORDER BY id DESC LIMIT 1))"
332
+ );
333
+ for (const r of rows)
334
+ stmt.run(
335
+ repoId,
336
+ r.callType,
337
+ r.method,
338
+ r.operationPathExpr,
339
+ r.queryEntity,
340
+ r.eventNameExpr,
341
+ r.payloadSummary,
342
+ r.sourceFile,
343
+ r.sourceLine,
344
+ r.confidence,
345
+ r.unresolvedReason,
346
+ repoId,
347
+ r.serviceVariableName
348
+ );
349
+ }
350
+
351
+ // src/discovery/classify-repository.ts
352
+ import fs3 from "fs/promises";
353
+ import path3 from "path";
354
+ async function classifyRepository(repoPath, facts) {
355
+ const hasCdsDep = Boolean(
356
+ facts.dependencies["@sap/cds"] ?? facts.dependencies.cds ?? facts.dependencies["cds-routing-handlers"]
357
+ );
358
+ const cdsFiles = await findFiles(repoPath, ".cds");
359
+ const serverFiles = await Promise.all(
360
+ ["srv/server.ts", "srv/server.js", "src/server.ts", "src/server.js"].map(
361
+ async (f) => fs3.access(path3.join(repoPath, f)).then(() => true).catch(() => false)
362
+ )
363
+ );
364
+ const helper = Object.keys(facts.dependencies).includes("cds-routing-handlers") || facts.packageName?.includes("helper") === true;
365
+ if (helper && cdsFiles.length > 0) return "mixed";
366
+ if (helper) return "helper-package";
367
+ if (hasCdsDep && (cdsFiles.length > 0 || serverFiles.some(Boolean)))
368
+ return "cap-service";
369
+ if (cdsFiles.length > 0)
370
+ return serverFiles.some(Boolean) ? "cap-service" : "cap-db-model";
371
+ return "unknown";
372
+ }
373
+ async function findFiles(root, suffix) {
374
+ const out = [];
375
+ async function walk(dir) {
376
+ const entries = await fs3.readdir(dir, { withFileTypes: true }).catch(() => []);
377
+ for (const e of entries) {
378
+ if (e.isDirectory()) {
379
+ if (!["node_modules", "dist", "gen", ".git"].includes(e.name))
380
+ await walk(path3.join(dir, e.name));
381
+ } else if (e.name.endsWith(suffix)) out.push(path3.join(dir, e.name));
382
+ }
383
+ }
384
+ await walk(root);
385
+ return out;
386
+ }
387
+
388
+ // src/indexer/repository-indexer.ts
389
+ import fs5 from "fs/promises";
390
+ import path5 from "path";
391
+
392
+ // src/indexer/incremental-index.ts
393
+ import fs4 from "fs/promises";
394
+ import path4 from "path";
395
+
396
+ // src/utils/hashing.ts
397
+ import { createHash } from "crypto";
398
+ import { readFile } from "fs/promises";
399
+ async function sha256File(filePath) {
400
+ return createHash("sha256").update(await readFile(filePath)).digest("hex");
401
+ }
402
+
403
+ // src/indexer/incremental-index.ts
404
+ async function recordFile(db, repoId, repoPath, relativeFile) {
405
+ const abs = path4.join(repoPath, relativeFile);
406
+ const stat = await fs4.stat(abs);
407
+ const hash = await sha256File(abs);
408
+ db.prepare(
409
+ "INSERT INTO files(repo_id,relative_path,extension,sha256,size_bytes,last_indexed_at) VALUES(?,?,?,?,?,?) ON CONFLICT(repo_id,relative_path) DO UPDATE SET sha256=excluded.sha256,size_bytes=excluded.size_bytes,last_indexed_at=excluded.last_indexed_at"
410
+ ).run(
411
+ repoId,
412
+ normalizePath(relativeFile),
413
+ path4.extname(relativeFile),
414
+ hash,
415
+ stat.size,
416
+ (/* @__PURE__ */ new Date()).toISOString()
417
+ );
418
+ }
419
+
420
+ // src/utils/diagnostics.ts
421
+ function errorMessage(error) {
422
+ return error instanceof Error ? error.message : String(error);
423
+ }
424
+
425
+ // src/indexer/repository-indexer.ts
426
+ async function indexRepository(db, repo, force) {
427
+ void force;
428
+ let diagnostics = 0;
429
+ let files = 0;
430
+ const facts = await parsePackageJson(repo.absolute_path);
431
+ const kind = await classifyRepository(repo.absolute_path, facts);
432
+ db.prepare(
433
+ "UPDATE repositories SET package_name=?, package_version=?, dependencies_json=?, kind=?, index_status=? WHERE id=?"
434
+ ).run(
435
+ facts.packageName,
436
+ facts.packageVersion,
437
+ JSON.stringify(facts.dependencies),
438
+ kind,
439
+ "indexing",
440
+ repo.id
441
+ );
442
+ clearRepoFacts(db, repo.id);
443
+ insertRequires(db, repo.id, facts.cdsRequires);
444
+ const sourceFiles = await findSourceFiles(repo.absolute_path);
445
+ for (const file of sourceFiles) {
446
+ try {
447
+ await recordFile(db, repo.id, repo.absolute_path, file);
448
+ files += 1;
449
+ if (file.endsWith(".cds"))
450
+ for (const s of await parseCdsFile(repo.absolute_path, file))
451
+ insertService(db, repo.id, s);
452
+ if (/\.[jt]s$/.test(file)) {
453
+ for (const h of await parseDecorators(repo.absolute_path, file))
454
+ insertHandler(db, repo.id, h);
455
+ insertRegistrations(
456
+ db,
457
+ repo.id,
458
+ await parseHandlerRegistrations(repo.absolute_path, file)
459
+ );
460
+ insertBindings(
461
+ db,
462
+ repo.id,
463
+ await parseServiceBindings(repo.absolute_path, file)
464
+ );
465
+ insertCalls(
466
+ db,
467
+ repo.id,
468
+ await parseOutboundCalls(repo.absolute_path, file)
469
+ );
470
+ }
471
+ } catch (error) {
472
+ diagnostics += 1;
473
+ db.prepare(
474
+ "INSERT INTO diagnostics(repo_id,severity,code,message,source_file) VALUES(?,?,?,?,?)"
475
+ ).run(repo.id, "warning", "parse_failed", errorMessage(error), file);
476
+ }
477
+ }
478
+ db.prepare(
479
+ "UPDATE repositories SET last_indexed_at=?, index_status=?, error_count=? WHERE id=?"
480
+ ).run(
481
+ (/* @__PURE__ */ new Date()).toISOString(),
482
+ diagnostics ? "partial" : "indexed",
483
+ diagnostics,
484
+ repo.id
485
+ );
486
+ return { fileCount: files, diagnosticCount: diagnostics };
487
+ }
488
+ async function findSourceFiles(root) {
489
+ const out = [];
490
+ async function walk(dir, prefix = "") {
491
+ const entries = await fs5.readdir(dir, { withFileTypes: true }).catch(() => []);
492
+ for (const e of entries) {
493
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
494
+ if (e.isDirectory()) {
495
+ if (!["node_modules", "dist", "gen", "coverage", ".git"].includes(e.name))
496
+ await walk(path5.join(dir, e.name), rel);
497
+ } else if (/\.(cds|ts|js)$/.test(e.name)) out.push(rel);
498
+ }
499
+ }
500
+ await walk(root);
501
+ return out.sort();
502
+ }
503
+
504
+ // src/indexer/workspace-indexer.ts
505
+ async function indexWorkspace(db, workspaceId, options) {
506
+ const started = (/* @__PURE__ */ new Date()).toISOString();
507
+ const repos = options.repo ? [repoByName(db, options.repo)].filter((r) => r !== void 0) : listRepositories(db);
508
+ const runId = Number(
509
+ db.prepare(
510
+ "INSERT INTO index_runs(workspace_id,started_at,status,repo_count,file_count,diagnostic_count) VALUES(?,?,?,?,?,?) RETURNING id"
511
+ ).get(workspaceId, started, "running", repos.length, 0, 0)?.id
512
+ );
513
+ let fileCount = 0;
514
+ let diagnosticCount = 0;
515
+ for (const repo of repos) {
516
+ const result = await indexRepository(db, repo, options.force);
517
+ fileCount += result.fileCount;
518
+ diagnosticCount += result.diagnosticCount;
519
+ }
520
+ db.prepare(
521
+ "UPDATE index_runs SET finished_at=?, status=?, file_count=?, diagnostic_count=? WHERE id=?"
522
+ ).run(
523
+ (/* @__PURE__ */ new Date()).toISOString(),
524
+ diagnosticCount ? "partial" : "success",
525
+ fileCount,
526
+ diagnosticCount,
527
+ runId
528
+ );
529
+ return { repoCount: repos.length, fileCount, diagnosticCount };
530
+ }
531
+
532
+ // src/trace/selectors.ts
533
+ function parseVars(values) {
534
+ const out = {};
535
+ for (const value of values ?? []) {
536
+ const [key, ...rest] = value.split("=");
537
+ if (key && rest.length > 0) out[key] = rest.join("=");
538
+ }
539
+ return out;
540
+ }
541
+ function startLabel(start) {
542
+ return [
543
+ start.repo,
544
+ start.servicePath,
545
+ start.operation ?? start.operationPath ?? start.handler
546
+ ].filter(Boolean).join(" ");
547
+ }
548
+
549
+ // src/output/table-output.ts
550
+ function renderTraceTable(trace2) {
551
+ const lines = [
552
+ `Start: ${startLabel(trace2.start)}`,
553
+ "",
554
+ "Step Type From To Evidence"
555
+ ];
556
+ for (const e of trace2.edges)
557
+ lines.push(
558
+ `${String(e.step).padEnd(5)} ${e.type.padEnd(20)} ${e.from.slice(0, 34).padEnd(35)} ${e.to.slice(0, 35).padEnd(36)} ${String(e.evidence.file ?? "")}:${String(e.evidence.line ?? "")}`
559
+ );
560
+ return `${lines.join("\n")}
561
+ `;
562
+ }
563
+
564
+ // src/output/json-output.ts
565
+ function renderJson(value) {
566
+ return `${JSON.stringify(value, null, 2)}
567
+ `;
568
+ }
569
+ function renderTraceJson(trace2) {
570
+ return renderJson(trace2);
571
+ }
572
+
573
+ // src/output/mermaid-output.ts
574
+ function safe(value) {
575
+ return value.replace(/[^\w-]/g, "_").slice(0, 60);
576
+ }
577
+ function renderMermaid(trace2) {
578
+ const lines = ["flowchart TD"];
579
+ for (const e of trace2.edges)
580
+ lines.push(
581
+ ` ${safe(e.from)}["${e.from}"] -->|${e.type}| ${safe(e.to)}["${e.to}"]`
582
+ );
583
+ return `${lines.join("\n")}
584
+ `;
585
+ }
586
+
587
+ // src/cli.ts
588
+ async function init(workspace, options) {
589
+ const config = createWorkspaceConfig(
590
+ workspace,
591
+ options.db,
592
+ options.ignore?.length ? options.ignore : [...DEFAULT_IGNORES]
593
+ );
594
+ const repos = await discoverRepositories(config.rootPath, config.ignore);
595
+ await saveWorkspaceConfig(config);
596
+ const db = openDatabase(config.dbPath);
597
+ const workspaceId = upsertWorkspace(db, config.rootPath, config.dbPath);
598
+ for (const repo of repos) {
599
+ const pkg = await parsePackageJson(repo.absolutePath);
600
+ const kind = await classifyRepository(repo.absolutePath, pkg);
601
+ upsertRepository(db, workspaceId, {
602
+ ...repo,
603
+ packageName: pkg.packageName,
604
+ packageVersion: pkg.packageVersion,
605
+ dependencies: pkg.dependencies,
606
+ kind
607
+ });
608
+ }
609
+ db.close();
610
+ process.stdout.write(
611
+ `Workspace: ${config.rootPath}
612
+ Database: ${config.dbPath}
613
+ Repositories: ${repos.length}
614
+ Ignored: ${config.ignore.join(", ")}
615
+ Next: service-flow index --workspace ${config.rootPath}
616
+ `
617
+ );
618
+ }
619
+ async function withWorkspace(workspace, fn) {
620
+ const config = await loadWorkspaceConfig(workspace);
621
+ const db = openDatabase(config.dbPath);
622
+ try {
623
+ const row = getWorkspace(db, config.rootPath);
624
+ const workspaceId = row?.id ?? upsertWorkspace(db, config.rootPath, config.dbPath);
625
+ return await fn(db, workspaceId, config.rootPath);
626
+ } finally {
627
+ db.close();
628
+ }
629
+ }
630
+ function createProgram() {
631
+ const program = new Command();
632
+ program.name("service-flow").description(
633
+ "Trace SAP CAP service-to-service flows across multi-repository workspaces"
634
+ ).version("0.1.0");
635
+ program.command("init").argument("<workspace>").option("--db <path>").option("--ignore <pattern...>").action(
636
+ (workspace, opts) => void init(workspace, opts).catch(fail)
637
+ );
638
+ program.command("index").option("--workspace <path>").option("--repo <name>").option("--force").option("--concurrency <n>", "reserved for future parallel indexing", "1").action(
639
+ (opts) => void withWorkspace(opts.workspace, async (db, workspaceId) => {
640
+ const r = await indexWorkspace(db, workspaceId, {
641
+ repo: opts.repo,
642
+ force: Boolean(opts.force)
643
+ });
644
+ process.stdout.write(
645
+ `Indexed ${r.repoCount} repositories, ${r.fileCount} files, ${r.diagnosticCount} diagnostics
646
+ `
647
+ );
648
+ }).catch(fail)
649
+ );
650
+ program.command("link").option("--workspace <path>").option("--force").action(
651
+ (opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
652
+ const r = linkWorkspace(db, workspaceId);
653
+ process.stdout.write(
654
+ `Linked ${r.edgeCount} edges, ${r.unresolvedCount} unresolved
655
+ `
656
+ );
657
+ }).catch(fail)
658
+ );
659
+ program.command("trace").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--handler <name>").option("--depth <n>", "trace depth", "25").option("--format <format>", "table|json|mermaid", "table").option("--include-external").option("--include-db").option("--include-async").option("--var <key=value>", "dynamic variable", collect, []).action(
660
+ (opts) => void withWorkspace(opts.workspace, (db) => {
661
+ const result = trace(
662
+ db,
663
+ {
664
+ repo: opts.repo,
665
+ servicePath: opts.service,
666
+ operation: opts.operation,
667
+ operationPath: opts.path,
668
+ handler: opts.handler
669
+ },
670
+ {
671
+ depth: Number(opts.depth),
672
+ vars: parseVars(opts.var),
673
+ includeExternal: Boolean(opts.includeExternal),
674
+ includeDb: Boolean(opts.includeDb),
675
+ includeAsync: Boolean(opts.includeAsync)
676
+ }
677
+ );
678
+ process.stdout.write(
679
+ opts.format === "json" ? renderTraceJson(result) : opts.format === "mermaid" ? renderMermaid(result) : renderTraceTable(result)
680
+ );
681
+ }).catch(fail)
682
+ );
683
+ const list = program.command("list");
684
+ list.command("repos").option("--workspace <path>").action(
685
+ (opts) => void withWorkspace(
686
+ opts.workspace,
687
+ (db) => process.stdout.write(
688
+ renderJson(
689
+ listRepositories(db).map((r) => ({
690
+ name: r.name,
691
+ kind: r.kind,
692
+ packageName: r.package_name
693
+ }))
694
+ )
695
+ )
696
+ ).catch(fail)
697
+ );
698
+ list.command("services").option("--workspace <path>").option("--repo <name>").action(
699
+ (opts) => void withWorkspace(opts.workspace, (db) => {
700
+ const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
701
+ const rows = db.prepare(
702
+ "SELECT r.name repo,s.service_path servicePath,s.qualified_name qualifiedName FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path"
703
+ ).all(repo?.id, repo?.id);
704
+ process.stdout.write(renderJson(rows));
705
+ }).catch(fail)
706
+ );
707
+ list.command("operations").option("--workspace <path>").option("--repo <name>").option("--service <path>").action(
708
+ (opts) => void withWorkspace(opts.workspace, (db) => {
709
+ const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
710
+ const rows = db.prepare(
711
+ "SELECT r.name repo,s.service_path servicePath,o.operation_name operation,o.operation_path path FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)"
712
+ ).all(repo?.id, repo?.id, opts.service, opts.service);
713
+ process.stdout.write(renderJson(rows));
714
+ }).catch(fail)
715
+ );
716
+ list.command("calls").option("--workspace <path>").option("--repo <name>").option("--operation <name>").action(
717
+ (opts) => void withWorkspace(opts.workspace, (db) => {
718
+ const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
719
+ const rows = db.prepare(
720
+ "SELECT r.name repo,c.call_type type,c.operation_path_expr path,c.source_file file,c.source_line line FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?)"
721
+ ).all(repo?.id, repo?.id);
722
+ process.stdout.write(renderJson(rows));
723
+ }).catch(fail)
724
+ );
725
+ program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--format <format>", "mermaid|json", "mermaid").action(
726
+ (opts) => void withWorkspace(opts.workspace, (db) => {
727
+ const result = trace(
728
+ db,
729
+ {
730
+ repo: opts.repo,
731
+ operation: opts.operation,
732
+ servicePath: opts.service,
733
+ operationPath: opts.path
734
+ },
735
+ {
736
+ depth: 100,
737
+ includeAsync: true,
738
+ includeDb: true,
739
+ includeExternal: true
740
+ }
741
+ );
742
+ process.stdout.write(
743
+ opts.format === "json" ? renderTraceJson(result) : renderMermaid(result)
744
+ );
745
+ }).catch(fail)
746
+ );
747
+ const inspect = program.command("inspect");
748
+ inspect.command("repo").argument("<name>").option("--workspace <path>").action(
749
+ (name, opts) => void withWorkspace(
750
+ opts.workspace,
751
+ (db) => process.stdout.write(
752
+ renderJson(repoByName(db, name) ?? { error: "repo not found" })
753
+ )
754
+ ).catch(fail)
755
+ );
756
+ inspect.command("operation").argument("<selector>").option("--workspace <path>").action(
757
+ (selector, opts) => void withWorkspace(opts.workspace, (db) => {
758
+ const rows = db.prepare(
759
+ "SELECT * FROM cds_operations WHERE operation_name=? OR operation_path=?"
760
+ ).all(selector, selector);
761
+ process.stdout.write(renderJson(rows));
762
+ }).catch(fail)
763
+ );
764
+ program.command("doctor").option("--workspace <path>").action(
765
+ (opts) => void withWorkspace(opts.workspace, (db) => {
766
+ const diagnostics = db.prepare(
767
+ "SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id"
768
+ ).all();
769
+ process.stdout.write(
770
+ diagnostics.length ? renderJson(diagnostics) : `${pc.green("No diagnostics recorded")}
771
+ `
772
+ );
773
+ }).catch(fail)
774
+ );
775
+ program.command("clean").option("--workspace <path>").option("--db-only").action(
776
+ (opts) => void (async () => {
777
+ const config = await loadWorkspaceConfig(opts.workspace);
778
+ await fs6.rm(config.dbPath, { force: true });
779
+ if (!opts.dbOnly)
780
+ await fs6.rm(path6.dirname(config.dbPath), {
781
+ recursive: true,
782
+ force: true
783
+ });
784
+ process.stdout.write("Cleaned service-flow state\n");
785
+ })().catch(fail)
786
+ );
787
+ return program;
788
+ }
789
+ function collect(value, previous) {
790
+ previous.push(value);
791
+ return previous;
792
+ }
793
+ function fail(error) {
794
+ process.stderr.write(
795
+ `${error instanceof Error ? error.message : String(error)}
796
+ `
797
+ );
798
+ process.exitCode = 1;
799
+ }
800
+ createProgram().parse(process.argv);
801
+ export {
802
+ createProgram
803
+ };
804
+ //# sourceMappingURL=cli.js.map