simplemdg-dev-cli 1.5.1 → 2.4.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.
Files changed (140) hide show
  1. package/README.md +65 -243
  2. package/USER_GUIDE.md +55 -249
  3. package/dist/commands/cds.command.js +69 -60
  4. package/dist/commands/cds.command.js.map +1 -1
  5. package/dist/commands/cf-db.command.d.ts +2 -0
  6. package/dist/commands/cf-db.command.js +606 -0
  7. package/dist/commands/cf-db.command.js.map +1 -0
  8. package/dist/commands/cf.command.js +1625 -198
  9. package/dist/commands/cf.command.js.map +1 -1
  10. package/dist/commands/gitlab.command.d.ts +2 -0
  11. package/dist/commands/gitlab.command.js +351 -0
  12. package/dist/commands/gitlab.command.js.map +1 -0
  13. package/dist/commands/npmrc.command.js +50 -44
  14. package/dist/commands/npmrc.command.js.map +1 -1
  15. package/dist/core/cache.d.ts +1 -1
  16. package/dist/core/cache.js +58 -31
  17. package/dist/core/cache.js.map +1 -1
  18. package/dist/core/cds.js +32 -22
  19. package/dist/core/cds.js.map +1 -1
  20. package/dist/core/cf-env-parser.d.ts +1 -1
  21. package/dist/core/cf-env-parser.js +4 -1
  22. package/dist/core/cf-env-parser.js.map +1 -1
  23. package/dist/core/cf.d.ts +1 -1
  24. package/dist/core/cf.js +46 -31
  25. package/dist/core/cf.js.map +1 -1
  26. package/dist/core/db/db-btp.d.ts +48 -0
  27. package/dist/core/db/db-btp.js +162 -0
  28. package/dist/core/db/db-btp.js.map +1 -0
  29. package/dist/core/db/db-cache.d.ts +35 -0
  30. package/dist/core/db/db-cache.js +164 -0
  31. package/dist/core/db/db-cache.js.map +1 -0
  32. package/dist/core/db/db-connection.d.ts +22 -0
  33. package/dist/core/db/db-connection.js +73 -0
  34. package/dist/core/db/db-connection.js.map +1 -0
  35. package/dist/core/db/db-crypto.d.ts +3 -0
  36. package/dist/core/db/db-crypto.js +54 -0
  37. package/dist/core/db/db-crypto.js.map +1 -0
  38. package/dist/core/db/db-hana-adapter.d.ts +32 -0
  39. package/dist/core/db/db-hana-adapter.js +243 -0
  40. package/dist/core/db/db-hana-adapter.js.map +1 -0
  41. package/dist/core/db/db-metadata.d.ts +25 -0
  42. package/dist/core/db/db-metadata.js +150 -0
  43. package/dist/core/db/db-metadata.js.map +1 -0
  44. package/dist/core/db/db-postgres-adapter.d.ts +30 -0
  45. package/dist/core/db/db-postgres-adapter.js +245 -0
  46. package/dist/core/db/db-postgres-adapter.js.map +1 -0
  47. package/dist/core/db/db-query-files.d.ts +20 -0
  48. package/dist/core/db/db-query-files.js +106 -0
  49. package/dist/core/db/db-query-files.js.map +1 -0
  50. package/dist/core/db/db-query-history.d.ts +5 -0
  51. package/dist/core/db/db-query-history.js +49 -0
  52. package/dist/core/db/db-query-history.js.map +1 -0
  53. package/dist/core/db/db-row.d.ts +22 -0
  54. package/dist/core/db/db-row.js +70 -0
  55. package/dist/core/db/db-row.js.map +1 -0
  56. package/dist/core/db/db-studio-html.d.ts +4 -0
  57. package/dist/core/db/db-studio-html.js +437 -0
  58. package/dist/core/db/db-studio-html.js.map +1 -0
  59. package/dist/core/db/db-studio-server.d.ts +11 -0
  60. package/dist/core/db/db-studio-server.js +465 -0
  61. package/dist/core/db/db-studio-server.js.map +1 -0
  62. package/dist/core/db/db-types.d.ts +174 -0
  63. package/dist/core/db/db-types.js +3 -0
  64. package/dist/core/db/db-types.js.map +1 -0
  65. package/dist/core/db/db-vcap-parser.d.ts +7 -0
  66. package/dist/core/db/db-vcap-parser.js +137 -0
  67. package/dist/core/db/db-vcap-parser.js.map +1 -0
  68. package/dist/core/doctor.d.ts +1 -1
  69. package/dist/core/doctor.js +14 -8
  70. package/dist/core/doctor.js.map +1 -1
  71. package/dist/core/guide.js +31 -26
  72. package/dist/core/guide.js.map +1 -1
  73. package/dist/core/install.d.ts +1 -1
  74. package/dist/core/install.js +17 -11
  75. package/dist/core/install.js.map +1 -1
  76. package/dist/core/navigator.d.ts +17 -0
  77. package/dist/core/navigator.js +140 -0
  78. package/dist/core/navigator.js.map +1 -0
  79. package/dist/core/npmrc.js +29 -16
  80. package/dist/core/npmrc.js.map +1 -1
  81. package/dist/core/process.js +11 -6
  82. package/dist/core/process.js.map +1 -1
  83. package/dist/core/prompts.js +16 -8
  84. package/dist/core/prompts.js.map +1 -1
  85. package/dist/core/repository.d.ts +1 -1
  86. package/dist/core/repository.js +16 -9
  87. package/dist/core/repository.js.map +1 -1
  88. package/dist/core/scanner.d.ts +1 -1
  89. package/dist/core/scanner.js +13 -7
  90. package/dist/core/scanner.js.map +1 -1
  91. package/dist/core/tooling.d.ts +28 -0
  92. package/dist/core/tooling.js +168 -0
  93. package/dist/core/tooling.js.map +1 -0
  94. package/dist/core/types.js +2 -1
  95. package/dist/core/version-conflict.d.ts +2 -2
  96. package/dist/core/version-conflict.js +11 -6
  97. package/dist/core/version-conflict.js.map +1 -1
  98. package/dist/index.js +65 -48
  99. package/dist/index.js.map +1 -1
  100. package/dist/types-local.js +2 -1
  101. package/package.json +12 -6
  102. package/src/commands/cds.command.ts +529 -0
  103. package/src/commands/cf-db.command.ts +636 -0
  104. package/src/commands/cf.command.ts +3345 -0
  105. package/src/commands/gitlab.command.ts +373 -0
  106. package/src/commands/npmrc.command.ts +581 -0
  107. package/src/core/cache.ts +332 -0
  108. package/src/core/cds.ts +278 -0
  109. package/src/core/cf-env-parser.ts +131 -0
  110. package/src/core/cf.ts +271 -0
  111. package/src/core/db/db-btp.ts +207 -0
  112. package/src/core/db/db-cache.ts +215 -0
  113. package/src/core/db/db-connection.ts +79 -0
  114. package/src/core/db/db-crypto.ts +53 -0
  115. package/src/core/db/db-hana-adapter.ts +294 -0
  116. package/src/core/db/db-metadata.ts +174 -0
  117. package/src/core/db/db-postgres-adapter.ts +275 -0
  118. package/src/core/db/db-query-files.ts +130 -0
  119. package/src/core/db/db-query-history.ts +53 -0
  120. package/src/core/db/db-row.ts +93 -0
  121. package/src/core/db/db-studio-html.ts +439 -0
  122. package/src/core/db/db-studio-server.ts +559 -0
  123. package/src/core/db/db-types.ts +195 -0
  124. package/src/core/db/db-vcap-parser.ts +182 -0
  125. package/src/core/doctor.ts +70 -0
  126. package/src/core/guide.ts +261 -0
  127. package/src/core/install.ts +91 -0
  128. package/src/core/navigator.ts +164 -0
  129. package/src/core/npmrc.ts +171 -0
  130. package/src/core/process.ts +75 -0
  131. package/src/core/prompts.ts +225 -0
  132. package/src/core/repository.ts +36 -0
  133. package/src/core/scanner.ts +41 -0
  134. package/src/core/tooling.ts +207 -0
  135. package/src/core/types.ts +152 -0
  136. package/src/core/version-conflict.ts +46 -0
  137. package/src/index.ts +460 -0
  138. package/src/types/external.d.ts +3 -0
  139. package/src/types-local.ts +11 -0
  140. package/tsconfig.json +17 -0
@@ -0,0 +1,332 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import fs from "fs-extra";
4
+ import type {
5
+ TCloudFoundryApp,
6
+ TCloudFoundryAppsCacheEntry,
7
+ TCloudFoundryLoginProfile,
8
+ TCloudFoundryOrgEntry,
9
+ TSimpleMdgCache,
10
+ TNpmrcPackageEntry,
11
+ TNpmrcTokenEntry,
12
+ } from "./types";
13
+
14
+ const CACHE_DIRECTORY = path.join(os.homedir(), ".simplemdg");
15
+ const CACHE_FILE_PATH = path.join(CACHE_DIRECTORY, "cache.json");
16
+ const MAX_HISTORY_ITEMS = 20;
17
+ const MAX_NPMRC_PACKAGE_ITEMS = 500;
18
+
19
+ const EMPTY_CACHE: TSimpleMdgCache = {
20
+ variables: {},
21
+ overrides: {},
22
+ cloudFoundry: {
23
+ loginProfiles: [],
24
+ appListsByTarget: {},
25
+ orgsAcrossRegions: [],
26
+ orgsAcrossRegionsUpdatedAt: undefined,
27
+ envFileNames: ["default-env.json", "default-envBTP.json", "default-envBTPConfigAdmin.json"],
28
+ selectedApps: [],
29
+ },
30
+ cds: {
31
+ profiles: ["hybrid", "development", "production"],
32
+ ports: ["4004", "4005", "4010"],
33
+ services: [],
34
+ edmxOutputFileNames: [],
35
+ models: ["srv", "."],
36
+ },
37
+ npmrc: {
38
+ hosts: ["gitlab.simplemdg.com"],
39
+ scopes: ["@simplemdg"],
40
+ packageIds: [],
41
+ packages: [],
42
+ packageIdsByProject: {},
43
+ tokens: [],
44
+ tokenEntries: [],
45
+ outputFileNames: [".npmrc"],
46
+ },
47
+ };
48
+
49
+ function cloneEmptyCache(): TSimpleMdgCache {
50
+ return JSON.parse(JSON.stringify(EMPTY_CACHE)) as TSimpleMdgCache;
51
+ }
52
+
53
+ function uniqueLatest(values: string[]): string[] {
54
+ const normalizedValues = values.map((value) => value.trim()).filter(Boolean);
55
+ return [...new Set(normalizedValues)].slice(0, MAX_HISTORY_ITEMS);
56
+ }
57
+
58
+ function uniquePackageEntries(values: TNpmrcPackageEntry[]): TNpmrcPackageEntry[] {
59
+ const result: TNpmrcPackageEntry[] = [];
60
+ const keys = new Set<string>();
61
+
62
+ for (const value of values) {
63
+ const packageId = value.packageId.trim();
64
+ const scope = value.scope.trim();
65
+ const host = value.host.trim();
66
+
67
+ if (!packageId || !scope || !host) {
68
+ continue;
69
+ }
70
+
71
+ const key = `${host}|${scope}|${packageId}`;
72
+
73
+ if (keys.has(key)) {
74
+ continue;
75
+ }
76
+
77
+ keys.add(key);
78
+ result.push({
79
+ ...value,
80
+ packageId,
81
+ packageName: value.packageName.trim() || packageId,
82
+ scope,
83
+ host,
84
+ });
85
+ }
86
+
87
+ return result.slice(0, MAX_NPMRC_PACKAGE_ITEMS);
88
+ }
89
+
90
+ function uniqueTokenEntries(values: TNpmrcTokenEntry[]): TNpmrcTokenEntry[] {
91
+ const result: TNpmrcTokenEntry[] = [];
92
+ const keys = new Set<string>();
93
+
94
+ for (const value of values) {
95
+ const scope = value.scope.trim();
96
+ const host = value.host.trim();
97
+
98
+ if (!scope || !host || !value.token.trim()) {
99
+ continue;
100
+ }
101
+
102
+ const key = `${host}|${scope}`;
103
+
104
+ if (keys.has(key)) {
105
+ continue;
106
+ }
107
+
108
+ keys.add(key);
109
+ result.push({
110
+ ...value,
111
+ scope,
112
+ host,
113
+ token: value.token.trim(),
114
+ label: value.label.trim() || `${scope} @ ${host}`,
115
+ });
116
+ }
117
+
118
+ return result.slice(0, MAX_HISTORY_ITEMS);
119
+ }
120
+
121
+ function migrateNpmrcProjectCache(
122
+ value: TSimpleMdgCache["npmrc"]["packageIdsByProject"],
123
+ ): TSimpleMdgCache["npmrc"]["packageIdsByProject"] {
124
+ const migratedEntries: TSimpleMdgCache["npmrc"]["packageIdsByProject"] = {};
125
+
126
+ for (const [projectName, projectCache] of Object.entries(value ?? {})) {
127
+ migratedEntries[projectName] = {
128
+ projectName: projectCache.projectName ?? projectName,
129
+ packageIds: projectCache.packageIds ?? [],
130
+ packages: projectCache.packages ?? [],
131
+ };
132
+ }
133
+
134
+ return migratedEntries;
135
+ }
136
+
137
+ export async function readCache(): Promise<TSimpleMdgCache> {
138
+ if (!(await fs.pathExists(CACHE_FILE_PATH))) {
139
+ return cloneEmptyCache();
140
+ }
141
+
142
+ const cache = await fs.readJson(CACHE_FILE_PATH).catch(() => cloneEmptyCache()) as Partial<TSimpleMdgCache>;
143
+
144
+ return {
145
+ variables: cache.variables ?? {},
146
+ overrides: cache.overrides ?? {},
147
+ cloudFoundry: {
148
+ loginProfiles: cache.cloudFoundry?.loginProfiles ?? [],
149
+ appListsByTarget: cache.cloudFoundry?.appListsByTarget ?? {},
150
+ orgsAcrossRegions: cache.cloudFoundry?.orgsAcrossRegions ?? [],
151
+ orgsAcrossRegionsUpdatedAt: cache.cloudFoundry?.orgsAcrossRegionsUpdatedAt,
152
+ envFileNames: cache.cloudFoundry?.envFileNames ?? EMPTY_CACHE.cloudFoundry.envFileNames,
153
+ selectedApps: cache.cloudFoundry?.selectedApps ?? [],
154
+ },
155
+ cds: {
156
+ profiles: cache.cds?.profiles ?? EMPTY_CACHE.cds.profiles,
157
+ ports: cache.cds?.ports ?? EMPTY_CACHE.cds.ports,
158
+ services: cache.cds?.services ?? EMPTY_CACHE.cds.services,
159
+ edmxOutputFileNames: cache.cds?.edmxOutputFileNames ?? EMPTY_CACHE.cds.edmxOutputFileNames,
160
+ models: cache.cds?.models ?? EMPTY_CACHE.cds.models,
161
+ },
162
+ npmrc: {
163
+ hosts: cache.npmrc?.hosts ?? EMPTY_CACHE.npmrc.hosts,
164
+ scopes: cache.npmrc?.scopes ?? EMPTY_CACHE.npmrc.scopes,
165
+ packageIds: cache.npmrc?.packageIds ?? EMPTY_CACHE.npmrc.packageIds,
166
+ packages: cache.npmrc?.packages ?? EMPTY_CACHE.npmrc.packages,
167
+ packageIdsByProject: migrateNpmrcProjectCache(cache.npmrc?.packageIdsByProject ?? EMPTY_CACHE.npmrc.packageIdsByProject),
168
+ tokens: cache.npmrc?.tokens ?? EMPTY_CACHE.npmrc.tokens,
169
+ tokenEntries: cache.npmrc?.tokenEntries ?? EMPTY_CACHE.npmrc.tokenEntries,
170
+ outputFileNames: cache.npmrc?.outputFileNames ?? EMPTY_CACHE.npmrc.outputFileNames,
171
+ },
172
+ };
173
+ }
174
+
175
+ export async function writeCache(cache: TSimpleMdgCache): Promise<void> {
176
+ await fs.ensureDir(CACHE_DIRECTORY);
177
+ await fs.writeJson(CACHE_FILE_PATH, cache, { spaces: 2 });
178
+ }
179
+
180
+ export async function rememberVariableValue(variableName: string, value: string): Promise<void> {
181
+ const cache = await readCache();
182
+ cache.variables[variableName] = uniqueLatest([value, ...(cache.variables[variableName] ?? [])]);
183
+ await writeCache(cache);
184
+ }
185
+
186
+ export async function rememberOverrideValue(packageName: string, version: string): Promise<void> {
187
+ const cache = await readCache();
188
+ cache.overrides[packageName] = uniqueLatest([version, ...(cache.overrides[packageName] ?? [])]);
189
+ await writeCache(cache);
190
+ }
191
+
192
+ export async function rememberResolvedOverrideVersions(overrides: Record<string, string>): Promise<void> {
193
+ for (const [packageName, version] of Object.entries(overrides)) {
194
+ await rememberOverrideValue(packageName, version);
195
+ }
196
+ }
197
+
198
+ export async function rememberCloudFoundryLoginProfile(profile: TCloudFoundryLoginProfile): Promise<void> {
199
+ const cache = await readCache();
200
+ const nextProfiles = cache.cloudFoundry.loginProfiles.filter((item) => {
201
+ return !(item.apiEndpoint === profile.apiEndpoint && item.org === profile.org && item.space === profile.space && item.username === profile.username);
202
+ });
203
+
204
+ cache.cloudFoundry.loginProfiles = [profile, ...nextProfiles].slice(0, MAX_HISTORY_ITEMS);
205
+ await writeCache(cache);
206
+ }
207
+
208
+ export async function rememberCloudFoundryApps(targetKey: string, apps: TCloudFoundryApp[]): Promise<void> {
209
+ const cache = await readCache();
210
+ const entry: TCloudFoundryAppsCacheEntry = {
211
+ targetKey,
212
+ apps,
213
+ updatedAt: new Date().toISOString(),
214
+ };
215
+
216
+ cache.cloudFoundry.appListsByTarget[targetKey] = entry;
217
+ await writeCache(cache);
218
+ }
219
+
220
+
221
+ export async function rememberCloudFoundryOrgEntries(orgEntries: TCloudFoundryOrgEntry[]): Promise<void> {
222
+ const cache = await readCache();
223
+ cache.cloudFoundry.orgsAcrossRegions = orgEntries;
224
+ cache.cloudFoundry.orgsAcrossRegionsUpdatedAt = new Date().toISOString();
225
+ await writeCache(cache);
226
+ }
227
+
228
+ export async function rememberSelectedApp(appName: string): Promise<void> {
229
+ const cache = await readCache();
230
+ cache.cloudFoundry.selectedApps = uniqueLatest([appName, ...cache.cloudFoundry.selectedApps]);
231
+ await writeCache(cache);
232
+ }
233
+
234
+ export async function rememberEnvironmentFileName(fileName: string): Promise<void> {
235
+ const cache = await readCache();
236
+ cache.cloudFoundry.envFileNames = uniqueLatest([fileName, ...cache.cloudFoundry.envFileNames]);
237
+ await writeCache(cache);
238
+ }
239
+
240
+ export async function rememberCdsProfile(profile: string): Promise<void> {
241
+ const cache = await readCache();
242
+ cache.cds.profiles = uniqueLatest([profile, ...cache.cds.profiles]);
243
+ await writeCache(cache);
244
+ }
245
+
246
+ export async function rememberCdsPort(port: string): Promise<void> {
247
+ const cache = await readCache();
248
+ cache.cds.ports = uniqueLatest([port, ...cache.cds.ports]);
249
+ await writeCache(cache);
250
+ }
251
+
252
+ export async function rememberCdsService(serviceName: string): Promise<void> {
253
+ const cache = await readCache();
254
+ cache.cds.services = uniqueLatest([serviceName, ...cache.cds.services]);
255
+ await writeCache(cache);
256
+ }
257
+
258
+ export async function rememberCdsEdmxOutputFileName(fileName: string): Promise<void> {
259
+ const cache = await readCache();
260
+ cache.cds.edmxOutputFileNames = uniqueLatest([fileName, ...cache.cds.edmxOutputFileNames]);
261
+ await writeCache(cache);
262
+ }
263
+
264
+ export async function rememberCdsModel(model: string): Promise<void> {
265
+ const cache = await readCache();
266
+ cache.cds.models = uniqueLatest([model, ...cache.cds.models]);
267
+ await writeCache(cache);
268
+ }
269
+
270
+
271
+ export async function rememberNpmrcHost(host: string): Promise<void> {
272
+ const cache = await readCache();
273
+ cache.npmrc.hosts = uniqueLatest([host, ...cache.npmrc.hosts]);
274
+ await writeCache(cache);
275
+ }
276
+
277
+ export async function rememberNpmrcScope(scope: string): Promise<void> {
278
+ const cache = await readCache();
279
+ cache.npmrc.scopes = uniqueLatest([scope, ...cache.npmrc.scopes]);
280
+ await writeCache(cache);
281
+ }
282
+
283
+ export async function rememberNpmrcPackageIds(projectName: string, packageIds: string[]): Promise<void> {
284
+ const fallbackEntries = packageIds.map<TNpmrcPackageEntry>((packageId) => ({
285
+ packageId,
286
+ packageName: packageId,
287
+ scope: "@simplemdg",
288
+ host: "gitlab.simplemdg.com",
289
+ updatedAt: new Date().toISOString(),
290
+ }));
291
+
292
+ await rememberNpmrcPackages(projectName, fallbackEntries);
293
+ }
294
+
295
+ export async function rememberNpmrcPackages(projectName: string, packages: TNpmrcPackageEntry[]): Promise<void> {
296
+ const cache = await readCache();
297
+ const nextPackageIds = packages.map((item) => item.packageId);
298
+ cache.npmrc.packageIds = uniqueLatest([...nextPackageIds, ...cache.npmrc.packageIds]);
299
+ cache.npmrc.packages = uniquePackageEntries([...packages, ...cache.npmrc.packages]);
300
+
301
+ const projectKey = projectName.trim();
302
+
303
+ if (projectKey) {
304
+ const currentProjectCache = cache.npmrc.packageIdsByProject[projectKey];
305
+ cache.npmrc.packageIdsByProject[projectKey] = {
306
+ projectName: projectKey,
307
+ packageIds: uniqueLatest([...nextPackageIds, ...(currentProjectCache?.packageIds ?? [])]),
308
+ packages: uniquePackageEntries([...packages, ...(currentProjectCache?.packages ?? [])]),
309
+ };
310
+ }
311
+
312
+ await writeCache(cache);
313
+ }
314
+
315
+ export async function rememberNpmrcToken(token: string): Promise<void> {
316
+ const cache = await readCache();
317
+ cache.npmrc.tokens = uniqueLatest([token, ...cache.npmrc.tokens]);
318
+ await writeCache(cache);
319
+ }
320
+
321
+ export async function rememberNpmrcTokenEntry(entry: TNpmrcTokenEntry): Promise<void> {
322
+ const cache = await readCache();
323
+ cache.npmrc.tokens = uniqueLatest([entry.token, ...cache.npmrc.tokens]);
324
+ cache.npmrc.tokenEntries = uniqueTokenEntries([entry, ...cache.npmrc.tokenEntries]);
325
+ await writeCache(cache);
326
+ }
327
+
328
+ export async function rememberNpmrcOutputFileName(fileName: string): Promise<void> {
329
+ const cache = await readCache();
330
+ cache.npmrc.outputFileNames = uniqueLatest([fileName, ...cache.npmrc.outputFileNames]);
331
+ await writeCache(cache);
332
+ }
@@ -0,0 +1,278 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import fg from "fast-glob";
4
+
5
+ const DEFAULT_CAP_PROFILES = ["hybrid", "development", "production", "mock"];
6
+ const CAP_CONFIG_FILE_NAMES = [
7
+ "package.json",
8
+ ".cdsrc.json",
9
+ ".cdsrc-private.json",
10
+ "default-env.json",
11
+ "default-envBTP.json",
12
+ ];
13
+
14
+ function normalizeProfileName(value: string): string {
15
+ return value.trim().replace(/^\[/, "").replace(/\]$/, "");
16
+ }
17
+
18
+ function addProfile(result: Set<string>, value: string | undefined): void {
19
+ const profileName = normalizeProfileName(value ?? "");
20
+
21
+ if (!profileName || profileName.startsWith("-") || profileName.includes("/")) {
22
+ return;
23
+ }
24
+
25
+ result.add(profileName);
26
+ }
27
+
28
+ function collectProfilesFromText(text: string, result: Set<string>): void {
29
+ const commandProfileRegex = /--profile(?:=|\s+)([A-Za-z0-9_.:-]+)/g;
30
+ let commandMatch: RegExpExecArray | null;
31
+
32
+ while ((commandMatch = commandProfileRegex.exec(text)) !== null) {
33
+ addProfile(result, commandMatch[1]);
34
+ }
35
+
36
+ const bracketProfileRegex = /"\[([A-Za-z0-9_.:-]+)\]"\s*:/g;
37
+ let bracketMatch: RegExpExecArray | null;
38
+
39
+ while ((bracketMatch = bracketProfileRegex.exec(text)) !== null) {
40
+ addProfile(result, bracketMatch[1]);
41
+ }
42
+
43
+ const plainProfileRegex = /"profile"\s*:\s*"([A-Za-z0-9_.:-]+)"/g;
44
+ let plainMatch: RegExpExecArray | null;
45
+
46
+ while ((plainMatch = plainProfileRegex.exec(text)) !== null) {
47
+ addProfile(result, plainMatch[1]);
48
+ }
49
+ }
50
+
51
+ async function collectProfilesFromPackageJson(repositoryPath: string, result: Set<string>): Promise<void> {
52
+ const packageJsonPath = path.join(repositoryPath, "package.json");
53
+
54
+ if (!(await fs.pathExists(packageJsonPath))) {
55
+ return;
56
+ }
57
+
58
+ const packageJson = await fs.readJson(packageJsonPath).catch(() => undefined) as Record<string, unknown> | undefined;
59
+
60
+ if (!packageJson) {
61
+ return;
62
+ }
63
+
64
+ const scripts = packageJson.scripts;
65
+
66
+ if (scripts && typeof scripts === "object") {
67
+ for (const [scriptName, scriptCommand] of Object.entries(scripts as Record<string, unknown>)) {
68
+ collectProfilesFromText(scriptName, result);
69
+
70
+ if (typeof scriptCommand === "string") {
71
+ collectProfilesFromText(scriptCommand, result);
72
+ }
73
+ }
74
+ }
75
+
76
+ const cdsConfig = packageJson.cds;
77
+
78
+ if (cdsConfig) {
79
+ collectProfilesFromText(JSON.stringify(cdsConfig), result);
80
+ }
81
+ }
82
+
83
+ export async function scanCapProfiles(repositoryPath: string): Promise<string[]> {
84
+ const result = new Set<string>();
85
+
86
+ await collectProfilesFromPackageJson(repositoryPath, result);
87
+
88
+ const configFilePaths = await fg(CAP_CONFIG_FILE_NAMES, {
89
+ cwd: repositoryPath,
90
+ onlyFiles: true,
91
+ dot: true,
92
+ absolute: true,
93
+ unique: true,
94
+ });
95
+
96
+ for (const filePath of configFilePaths) {
97
+ const text = await fs.readFile(filePath, "utf8").catch(() => "");
98
+ collectProfilesFromText(text, result);
99
+ }
100
+
101
+ for (const defaultProfile of DEFAULT_CAP_PROFILES) {
102
+ if (result.has(defaultProfile)) {
103
+ continue;
104
+ }
105
+
106
+ result.add(defaultProfile);
107
+ }
108
+
109
+ return [...result].sort((left, right) => {
110
+ const defaultLeftIndex = DEFAULT_CAP_PROFILES.indexOf(left);
111
+ const defaultRightIndex = DEFAULT_CAP_PROFILES.indexOf(right);
112
+
113
+ if (defaultLeftIndex !== -1 || defaultRightIndex !== -1) {
114
+ return (defaultLeftIndex === -1 ? 999 : defaultLeftIndex) - (defaultRightIndex === -1 ? 999 : defaultRightIndex);
115
+ }
116
+
117
+ return left.localeCompare(right);
118
+ });
119
+ }
120
+
121
+ export type TCdsServiceDefinition = {
122
+ serviceName: string;
123
+ filePath: string;
124
+ relativeFilePath: string;
125
+ namespace?: string;
126
+ fullServiceName: string;
127
+ };
128
+
129
+ const DEFAULT_CDS_SCAN_PATTERNS = [
130
+ "srv/**/*.cds",
131
+ "app/**/*.cds",
132
+ "db/**/*.cds",
133
+ "*.cds",
134
+ ];
135
+
136
+ function stripCdsComments(text: string): string {
137
+ return text
138
+ .replace(/\/\*[\s\S]*?\*\//g, "")
139
+ .replace(/(^|[^:])\/\/.*$/gm, "$1");
140
+ }
141
+
142
+ function collectNamespaceFromText(text: string): string | undefined {
143
+ const namespaceMatch = /\bnamespace\s+([A-Za-z_$][A-Za-z0-9_.$]*)\s*;/m.exec(stripCdsComments(text));
144
+ return namespaceMatch?.[1]?.trim();
145
+ }
146
+
147
+ function collectServicesFromText(text: string): string[] {
148
+ const services = new Set<string>();
149
+ const cleanedText = stripCdsComments(text);
150
+ const serviceRegex = /\bservice\s+([A-Za-z_$][A-Za-z0-9_.$]*)\b/g;
151
+ let match: RegExpExecArray | null;
152
+
153
+ while ((match = serviceRegex.exec(cleanedText)) !== null) {
154
+ const serviceName = match[1]?.trim();
155
+
156
+ if (serviceName) {
157
+ services.add(serviceName);
158
+ }
159
+ }
160
+
161
+ return [...services];
162
+ }
163
+
164
+ function toFullServiceName(serviceName: string, namespace: string | undefined): string {
165
+ if (!namespace || serviceName.includes(".")) {
166
+ return serviceName;
167
+ }
168
+
169
+ return `${namespace}.${serviceName}`;
170
+ }
171
+
172
+ export async function scanCapServices(repositoryPath: string): Promise<TCdsServiceDefinition[]> {
173
+ const cdsFilePaths = await fg(DEFAULT_CDS_SCAN_PATTERNS, {
174
+ cwd: repositoryPath,
175
+ onlyFiles: true,
176
+ dot: true,
177
+ absolute: true,
178
+ unique: true,
179
+ ignore: [
180
+ "**/node_modules/**",
181
+ "**/dist/**",
182
+ "**/gen/**",
183
+ "**/.git/**",
184
+ "**/.cds/**",
185
+ ],
186
+ });
187
+
188
+ const result: TCdsServiceDefinition[] = [];
189
+ const keys = new Set<string>();
190
+
191
+ for (const filePath of cdsFilePaths) {
192
+ const text = await fs.readFile(filePath, "utf8").catch(() => "");
193
+ const serviceNames = collectServicesFromText(text);
194
+ const namespace = collectNamespaceFromText(text);
195
+ const relativeFilePath = path.relative(repositoryPath, filePath).replace(/\\/g, "/");
196
+
197
+ for (const serviceName of serviceNames) {
198
+ const fullServiceName = toFullServiceName(serviceName, namespace);
199
+ const key = `${fullServiceName}|${relativeFilePath}`;
200
+
201
+ if (keys.has(key)) {
202
+ continue;
203
+ }
204
+
205
+ keys.add(key);
206
+ result.push({
207
+ serviceName,
208
+ filePath,
209
+ relativeFilePath,
210
+ namespace,
211
+ fullServiceName,
212
+ });
213
+ }
214
+ }
215
+
216
+ return result.sort((left, right) => left.fullServiceName.localeCompare(right.fullServiceName));
217
+ }
218
+
219
+ export async function resolveDefaultCdsModel(repositoryPath: string): Promise<string> {
220
+ const srvPath = path.join(repositoryPath, "srv");
221
+
222
+ if (await fs.pathExists(srvPath)) {
223
+ return "srv";
224
+ }
225
+
226
+ return ".";
227
+ }
228
+
229
+ function normalizeToSnakeCase(value: string): string {
230
+ return value
231
+ .replace(/^@[^/]+\//, "")
232
+ .replace(/^simplemdg[-_]/i, "")
233
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
234
+ .replace(/[^A-Za-z0-9]+/g, "_")
235
+ .replace(/^_+|_+$/g, "")
236
+ .replace(/_+/g, "_")
237
+ .toLowerCase();
238
+ }
239
+
240
+ async function readPackageName(repositoryPath: string): Promise<string | undefined> {
241
+ const packageJsonPath = path.join(repositoryPath, "package.json");
242
+
243
+ if (!(await fs.pathExists(packageJsonPath))) {
244
+ return undefined;
245
+ }
246
+
247
+ const packageJson = await fs.readJson(packageJsonPath).catch(() => undefined) as { name?: unknown } | undefined;
248
+ return typeof packageJson?.name === "string" ? packageJson.name : undefined;
249
+ }
250
+
251
+ export async function buildDefaultCompileOutputFileNames(options: {
252
+ repositoryPath: string;
253
+ serviceName?: string;
254
+ to: string;
255
+ }): Promise<string[]> {
256
+ const extension = options.to === "edmx" ? "xml" : options.to;
257
+ const names = new Set<string>();
258
+ const packageName = await readPackageName(options.repositoryPath);
259
+ const folderName = path.basename(options.repositoryPath);
260
+
261
+ if (packageName) {
262
+ names.add(`${normalizeToSnakeCase(packageName)}.${extension}`);
263
+ }
264
+
265
+ if (folderName) {
266
+ names.add(`${normalizeToSnakeCase(folderName)}.${extension}`);
267
+ }
268
+
269
+ if (options.serviceName) {
270
+ names.add(`${normalizeToSnakeCase(options.serviceName)}.${extension}`);
271
+ }
272
+
273
+ return [...names].filter((value) => value !== `.${extension}`);
274
+ }
275
+
276
+ export function buildDefaultEdmxOutputFileName(serviceName: string): string {
277
+ return `${normalizeToSnakeCase(serviceName)}.xml`;
278
+ }