simplemdg-dev-cli 2.0.4 → 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.
- package/README.md +63 -354
- package/USER_GUIDE.md +55 -378
- package/dist/commands/cds.command.js +69 -60
- package/dist/commands/cds.command.js.map +1 -1
- package/dist/commands/cf-db.command.d.ts +2 -0
- package/dist/commands/cf-db.command.js +606 -0
- package/dist/commands/cf-db.command.js.map +1 -0
- package/dist/commands/cf.command.js +291 -280
- package/dist/commands/cf.command.js.map +1 -1
- package/dist/commands/gitlab.command.d.ts +2 -0
- package/dist/commands/gitlab.command.js +351 -0
- package/dist/commands/gitlab.command.js.map +1 -0
- package/dist/commands/npmrc.command.js +50 -44
- package/dist/commands/npmrc.command.js.map +1 -1
- package/dist/core/cache.d.ts +1 -1
- package/dist/core/cache.js +58 -31
- package/dist/core/cache.js.map +1 -1
- package/dist/core/cds.js +32 -22
- package/dist/core/cds.js.map +1 -1
- package/dist/core/cf-env-parser.d.ts +1 -1
- package/dist/core/cf-env-parser.js +4 -1
- package/dist/core/cf-env-parser.js.map +1 -1
- package/dist/core/cf.d.ts +1 -1
- package/dist/core/cf.js +46 -31
- package/dist/core/cf.js.map +1 -1
- package/dist/core/db/db-btp.d.ts +48 -0
- package/dist/core/db/db-btp.js +162 -0
- package/dist/core/db/db-btp.js.map +1 -0
- package/dist/core/db/db-cache.d.ts +35 -0
- package/dist/core/db/db-cache.js +164 -0
- package/dist/core/db/db-cache.js.map +1 -0
- package/dist/core/db/db-connection.d.ts +22 -0
- package/dist/core/db/db-connection.js +73 -0
- package/dist/core/db/db-connection.js.map +1 -0
- package/dist/core/db/db-crypto.d.ts +3 -0
- package/dist/core/db/db-crypto.js +54 -0
- package/dist/core/db/db-crypto.js.map +1 -0
- package/dist/core/db/db-hana-adapter.d.ts +32 -0
- package/dist/core/db/db-hana-adapter.js +243 -0
- package/dist/core/db/db-hana-adapter.js.map +1 -0
- package/dist/core/db/db-metadata.d.ts +25 -0
- package/dist/core/db/db-metadata.js +150 -0
- package/dist/core/db/db-metadata.js.map +1 -0
- package/dist/core/db/db-postgres-adapter.d.ts +30 -0
- package/dist/core/db/db-postgres-adapter.js +245 -0
- package/dist/core/db/db-postgres-adapter.js.map +1 -0
- package/dist/core/db/db-query-files.d.ts +20 -0
- package/dist/core/db/db-query-files.js +106 -0
- package/dist/core/db/db-query-files.js.map +1 -0
- package/dist/core/db/db-query-history.d.ts +5 -0
- package/dist/core/db/db-query-history.js +49 -0
- package/dist/core/db/db-query-history.js.map +1 -0
- package/dist/core/db/db-row.d.ts +22 -0
- package/dist/core/db/db-row.js +70 -0
- package/dist/core/db/db-row.js.map +1 -0
- package/dist/core/db/db-studio-html.d.ts +4 -0
- package/dist/core/db/db-studio-html.js +437 -0
- package/dist/core/db/db-studio-html.js.map +1 -0
- package/dist/core/db/db-studio-server.d.ts +11 -0
- package/dist/core/db/db-studio-server.js +465 -0
- package/dist/core/db/db-studio-server.js.map +1 -0
- package/dist/core/db/db-types.d.ts +174 -0
- package/dist/core/db/db-types.js +3 -0
- package/dist/core/db/db-types.js.map +1 -0
- package/dist/core/db/db-vcap-parser.d.ts +7 -0
- package/dist/core/db/db-vcap-parser.js +137 -0
- package/dist/core/db/db-vcap-parser.js.map +1 -0
- package/dist/core/doctor.d.ts +1 -1
- package/dist/core/doctor.js +14 -8
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/guide.js +31 -26
- package/dist/core/guide.js.map +1 -1
- package/dist/core/install.d.ts +1 -1
- package/dist/core/install.js +17 -11
- package/dist/core/install.js.map +1 -1
- package/dist/core/navigator.d.ts +17 -0
- package/dist/core/navigator.js +140 -0
- package/dist/core/navigator.js.map +1 -0
- package/dist/core/npmrc.js +29 -16
- package/dist/core/npmrc.js.map +1 -1
- package/dist/core/process.js +11 -6
- package/dist/core/process.js.map +1 -1
- package/dist/core/prompts.js +16 -8
- package/dist/core/prompts.js.map +1 -1
- package/dist/core/repository.d.ts +1 -1
- package/dist/core/repository.js +16 -9
- package/dist/core/repository.js.map +1 -1
- package/dist/core/scanner.d.ts +1 -1
- package/dist/core/scanner.js +13 -7
- package/dist/core/scanner.js.map +1 -1
- package/dist/core/tooling.d.ts +28 -0
- package/dist/core/tooling.js +168 -0
- package/dist/core/tooling.js.map +1 -0
- package/dist/core/types.js +2 -1
- package/dist/core/version-conflict.d.ts +2 -2
- package/dist/core/version-conflict.js +11 -6
- package/dist/core/version-conflict.js.map +1 -1
- package/dist/index.js +65 -48
- package/dist/index.js.map +1 -1
- package/dist/types-local.js +2 -1
- package/package.json +12 -6
- package/src/commands/cds.command.ts +529 -0
- package/src/commands/cf-db.command.ts +636 -0
- package/src/commands/cf.command.ts +3345 -0
- package/src/commands/gitlab.command.ts +373 -0
- package/src/commands/npmrc.command.ts +581 -0
- package/src/core/cache.ts +332 -0
- package/src/core/cds.ts +278 -0
- package/src/core/cf-env-parser.ts +131 -0
- package/src/core/cf.ts +271 -0
- package/src/core/db/db-btp.ts +207 -0
- package/src/core/db/db-cache.ts +215 -0
- package/src/core/db/db-connection.ts +79 -0
- package/src/core/db/db-crypto.ts +53 -0
- package/src/core/db/db-hana-adapter.ts +294 -0
- package/src/core/db/db-metadata.ts +174 -0
- package/src/core/db/db-postgres-adapter.ts +275 -0
- package/src/core/db/db-query-files.ts +130 -0
- package/src/core/db/db-query-history.ts +53 -0
- package/src/core/db/db-row.ts +93 -0
- package/src/core/db/db-studio-html.ts +439 -0
- package/src/core/db/db-studio-server.ts +559 -0
- package/src/core/db/db-types.ts +195 -0
- package/src/core/db/db-vcap-parser.ts +182 -0
- package/src/core/doctor.ts +70 -0
- package/src/core/guide.ts +261 -0
- package/src/core/install.ts +91 -0
- package/src/core/navigator.ts +164 -0
- package/src/core/npmrc.ts +171 -0
- package/src/core/process.ts +75 -0
- package/src/core/prompts.ts +225 -0
- package/src/core/repository.ts +36 -0
- package/src/core/scanner.ts +41 -0
- package/src/core/tooling.ts +207 -0
- package/src/core/types.ts +152 -0
- package/src/core/version-conflict.ts +46 -0
- package/src/index.ts +460 -0
- package/src/types/external.d.ts +3 -0
- package/src/types-local.ts +11 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
|
|
3
|
+
const CUSTOM_VALUE_PREFIX = "__SMDG_CUSTOM_VALUE__:";
|
|
4
|
+
|
|
5
|
+
function normalizeValue(value: string): string {
|
|
6
|
+
return value.trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function uniqueValues(values: Array<string | undefined>): string[] {
|
|
10
|
+
return [...new Set(values.map((value) => normalizeValue(value ?? "")).filter(Boolean))];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function scoreMatch(input: string, value: string): number {
|
|
14
|
+
const normalizedInput = input.toLowerCase().trim();
|
|
15
|
+
const normalizedValue = value.toLowerCase();
|
|
16
|
+
|
|
17
|
+
if (!normalizedInput) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (normalizedValue === normalizedInput) {
|
|
22
|
+
return 100;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (normalizedValue.startsWith(normalizedInput)) {
|
|
26
|
+
return 80;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (normalizedValue.includes(normalizedInput)) {
|
|
30
|
+
return 60;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const inputParts = normalizedInput.split(/\s+/).filter(Boolean);
|
|
34
|
+
|
|
35
|
+
if (inputParts.every((part) => normalizedValue.includes(part))) {
|
|
36
|
+
return 40;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return -1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildSearchableChoices(options: {
|
|
43
|
+
input: string;
|
|
44
|
+
choices: prompts.Choice[];
|
|
45
|
+
allowCustomValue: boolean;
|
|
46
|
+
customValueTitle?: (value: string) => string;
|
|
47
|
+
}): prompts.Choice[] {
|
|
48
|
+
const input = normalizeValue(options.input);
|
|
49
|
+
const scoredChoices = options.choices
|
|
50
|
+
.map((choice, index) => {
|
|
51
|
+
const title = String(choice.title ?? "");
|
|
52
|
+
const value = String(choice.value ?? choice.title ?? "");
|
|
53
|
+
const score = input ? Math.max(scoreMatch(input, title), scoreMatch(input, value)) : 0;
|
|
54
|
+
|
|
55
|
+
return { choice, index, score };
|
|
56
|
+
})
|
|
57
|
+
.filter((item) => !input || item.score >= 0)
|
|
58
|
+
.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
59
|
+
.map((item) => item.choice);
|
|
60
|
+
|
|
61
|
+
const hasExactMatch = options.choices.some((choice) => {
|
|
62
|
+
const title = String(choice.title ?? "").trim().toLowerCase();
|
|
63
|
+
const value = String(choice.value ?? choice.title ?? "").trim().toLowerCase();
|
|
64
|
+
const normalizedInput = input.toLowerCase();
|
|
65
|
+
|
|
66
|
+
return title === normalizedInput || value === normalizedInput;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (options.allowCustomValue && input && !hasExactMatch) {
|
|
70
|
+
return [
|
|
71
|
+
...scoredChoices,
|
|
72
|
+
{
|
|
73
|
+
title: options.customValueTitle?.(input) ?? `Use typed value: ${input}`,
|
|
74
|
+
value: `${CUSTOM_VALUE_PREFIX}${input}`,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return scoredChoices;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function searchableSelectChoice<TValue extends string>(options: {
|
|
83
|
+
message: string;
|
|
84
|
+
choices: Array<{ title: string; value: TValue; description?: string }>;
|
|
85
|
+
validateCustomValue?: (value: string) => true | string;
|
|
86
|
+
allowCustomValue?: boolean;
|
|
87
|
+
customValueTitle?: (value: string) => string;
|
|
88
|
+
limit?: number;
|
|
89
|
+
}): Promise<string> {
|
|
90
|
+
const allowCustomValue = options.allowCustomValue ?? true;
|
|
91
|
+
const validate = options.validateCustomValue ?? ((value: string) => value.trim() ? true : "Value is required");
|
|
92
|
+
const choices = options.choices.map((choice) => ({
|
|
93
|
+
title: choice.title,
|
|
94
|
+
value: choice.value,
|
|
95
|
+
description: choice.description,
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
if (choices.length === 0) {
|
|
99
|
+
const response = await prompts({
|
|
100
|
+
type: "text",
|
|
101
|
+
name: "value",
|
|
102
|
+
message: options.message,
|
|
103
|
+
validate,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.value) {
|
|
107
|
+
throw new Error("Cancelled");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return String(response.value).trim();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const response = await prompts({
|
|
114
|
+
type: "autocomplete",
|
|
115
|
+
name: "value",
|
|
116
|
+
message: options.message,
|
|
117
|
+
choices,
|
|
118
|
+
initial: 0,
|
|
119
|
+
limit: options.limit ?? 12,
|
|
120
|
+
suggest: async (input: string, currentChoices: prompts.Choice[]) => buildSearchableChoices({
|
|
121
|
+
input,
|
|
122
|
+
choices: currentChoices,
|
|
123
|
+
allowCustomValue,
|
|
124
|
+
customValueTitle: options.customValueTitle,
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!response.value) {
|
|
129
|
+
throw new Error("Cancelled");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const selectedValue = String(response.value);
|
|
133
|
+
|
|
134
|
+
if (selectedValue.startsWith(CUSTOM_VALUE_PREFIX)) {
|
|
135
|
+
const customValue = selectedValue.slice(CUSTOM_VALUE_PREFIX.length).trim();
|
|
136
|
+
const validationResult = validate(customValue);
|
|
137
|
+
|
|
138
|
+
if (validationResult !== true) {
|
|
139
|
+
throw new Error(validationResult);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return customValue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return selectedValue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function searchableSelectOrInput(options: {
|
|
149
|
+
message: string;
|
|
150
|
+
values: string[];
|
|
151
|
+
initialValue?: string;
|
|
152
|
+
inputMessage?: string;
|
|
153
|
+
validate?: (value: string) => true | string;
|
|
154
|
+
allowCustomValue?: boolean;
|
|
155
|
+
customValueTitle?: (value: string) => string;
|
|
156
|
+
limit?: number;
|
|
157
|
+
}): Promise<string> {
|
|
158
|
+
const values = uniqueValues([options.initialValue, ...options.values]);
|
|
159
|
+
const choices = values.map((value) => ({ title: value, value }));
|
|
160
|
+
const allowCustomValue = options.allowCustomValue ?? true;
|
|
161
|
+
const validate = options.validate ?? ((value: string) => value.trim() ? true : "Value is required");
|
|
162
|
+
|
|
163
|
+
if (choices.length > 0) {
|
|
164
|
+
const response = await prompts({
|
|
165
|
+
type: "autocomplete",
|
|
166
|
+
name: "value",
|
|
167
|
+
message: options.message,
|
|
168
|
+
choices,
|
|
169
|
+
initial: 0,
|
|
170
|
+
limit: options.limit ?? 12,
|
|
171
|
+
suggest: async (input: string, currentChoices: prompts.Choice[]) => buildSearchableChoices({
|
|
172
|
+
input,
|
|
173
|
+
choices: currentChoices,
|
|
174
|
+
allowCustomValue,
|
|
175
|
+
customValueTitle: options.customValueTitle,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!response.value) {
|
|
180
|
+
throw new Error("Cancelled");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const selectedValue = String(response.value);
|
|
184
|
+
|
|
185
|
+
if (selectedValue.startsWith(CUSTOM_VALUE_PREFIX)) {
|
|
186
|
+
const customValue = selectedValue.slice(CUSTOM_VALUE_PREFIX.length).trim();
|
|
187
|
+
const validationResult = validate(customValue);
|
|
188
|
+
|
|
189
|
+
if (validationResult !== true) {
|
|
190
|
+
throw new Error(validationResult);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return customValue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return selectedValue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const input = await prompts({
|
|
200
|
+
type: "text",
|
|
201
|
+
name: "value",
|
|
202
|
+
message: options.inputMessage ?? options.message,
|
|
203
|
+
initial: options.initialValue ?? values[0] ?? "",
|
|
204
|
+
validate,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!input.value) {
|
|
208
|
+
throw new Error("Cancelled");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return String(input.value).trim();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function selectFromHistoryOrInput(options: {
|
|
215
|
+
message: string;
|
|
216
|
+
values: string[];
|
|
217
|
+
initialValue?: string;
|
|
218
|
+
inputMessage?: string;
|
|
219
|
+
validate?: (value: string) => true | string;
|
|
220
|
+
}): Promise<string> {
|
|
221
|
+
return searchableSelectOrInput({
|
|
222
|
+
...options,
|
|
223
|
+
allowCustomValue: true,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import type { TRepositoryInfo } from "./types";
|
|
4
|
+
|
|
5
|
+
export async function findNearestRepository(startPath: string): Promise<TRepositoryInfo | undefined> {
|
|
6
|
+
let currentPath = path.resolve(startPath);
|
|
7
|
+
|
|
8
|
+
while (true) {
|
|
9
|
+
if (await fs.pathExists(path.join(currentPath, "package.json"))) {
|
|
10
|
+
return { repositoryPath: currentPath };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const parentPath = path.dirname(currentPath);
|
|
14
|
+
|
|
15
|
+
if (parentPath === currentPath) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
currentPath = parentPath;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function resolveRepositoryPath(cwd: string): Promise<string> {
|
|
24
|
+
const absoluteCwd = path.resolve(cwd);
|
|
25
|
+
const repository = await findNearestRepository(absoluteCwd);
|
|
26
|
+
|
|
27
|
+
if (repository?.repositoryPath) {
|
|
28
|
+
return repository.repositoryPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (await fs.pathExists(path.join(absoluteCwd, "package.json"))) {
|
|
32
|
+
return absoluteCwd;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new Error(`Cannot find repository from ${absoluteCwd}`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import fastGlob from "fast-glob";
|
|
4
|
+
import type { TScannedVariable } from "./types";
|
|
5
|
+
|
|
6
|
+
const VARIABLE_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7
|
+
|
|
8
|
+
export async function scanRepositoryVariables(options: {
|
|
9
|
+
repositoryPath: string;
|
|
10
|
+
filePatterns: string[];
|
|
11
|
+
}): Promise<TScannedVariable[]> {
|
|
12
|
+
const filePaths = await fastGlob(options.filePatterns, {
|
|
13
|
+
cwd: options.repositoryPath,
|
|
14
|
+
absolute: true,
|
|
15
|
+
onlyFiles: true,
|
|
16
|
+
ignore: ["node_modules/**", "dist/**", ".git/**"],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const result: TScannedVariable[] = [];
|
|
20
|
+
|
|
21
|
+
for (const filePath of filePaths) {
|
|
22
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
23
|
+
const occurrencesByVariable = new Map<string, number>();
|
|
24
|
+
let match: RegExpExecArray | null;
|
|
25
|
+
|
|
26
|
+
while ((match = VARIABLE_PATTERN.exec(content)) !== null) {
|
|
27
|
+
const variableName = match[1];
|
|
28
|
+
occurrencesByVariable.set(variableName, (occurrencesByVariable.get(variableName) ?? 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const [variableName, occurrences] of occurrencesByVariable.entries()) {
|
|
32
|
+
result.push({
|
|
33
|
+
variableName,
|
|
34
|
+
occurrences,
|
|
35
|
+
filePath: path.relative(options.repositoryPath, filePath),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result.sort((left, right) => left.variableName.localeCompare(right.variableName));
|
|
41
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { searchableSelectChoice } from "./prompts";
|
|
4
|
+
|
|
5
|
+
export type TPackageManager = "winget" | "choco" | "scoop" | "brew" | "apt-get";
|
|
6
|
+
|
|
7
|
+
type TInstallStrategy =
|
|
8
|
+
| { kind: "package-manager"; manager: TPackageManager; args: string[] }
|
|
9
|
+
| { kind: "npm-global"; packageName: string };
|
|
10
|
+
|
|
11
|
+
type TExternalTool = {
|
|
12
|
+
command: string;
|
|
13
|
+
versionArgs: string[];
|
|
14
|
+
displayName: string;
|
|
15
|
+
docsUrl: string;
|
|
16
|
+
/** Best-effort install recipes per package manager (Windows/macOS/Linux). */
|
|
17
|
+
packageManagerArgs: Partial<Record<TPackageManager, string[]>>;
|
|
18
|
+
/** Set when the tool ships as a global npm package. */
|
|
19
|
+
npmGlobalPackage?: string;
|
|
20
|
+
manualHint: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const EXTERNAL_TOOLS: Record<string, TExternalTool> = {
|
|
24
|
+
cf: {
|
|
25
|
+
command: "cf",
|
|
26
|
+
versionArgs: ["version"],
|
|
27
|
+
displayName: "Cloud Foundry CLI",
|
|
28
|
+
docsUrl: "https://docs.cloudfoundry.org/cf-cli/install-go-cli.html",
|
|
29
|
+
packageManagerArgs: {
|
|
30
|
+
choco: ["install", "cloudfoundry-cli", "-y"],
|
|
31
|
+
brew: ["install", "cloudfoundry/tap/cf-cli@8"],
|
|
32
|
+
},
|
|
33
|
+
manualHint: "Windows installer: https://github.com/cloudfoundry/cli/releases",
|
|
34
|
+
},
|
|
35
|
+
cds: {
|
|
36
|
+
command: "cds",
|
|
37
|
+
versionArgs: ["--version"],
|
|
38
|
+
displayName: "SAP CAP CLI (@sap/cds-dk)",
|
|
39
|
+
docsUrl: "https://cap.cloud.sap/docs/tools/cds-cli",
|
|
40
|
+
packageManagerArgs: {},
|
|
41
|
+
npmGlobalPackage: "@sap/cds-dk",
|
|
42
|
+
manualHint: "Install globally with: npm install -g @sap/cds-dk",
|
|
43
|
+
},
|
|
44
|
+
git: {
|
|
45
|
+
command: "git",
|
|
46
|
+
versionArgs: ["--version"],
|
|
47
|
+
displayName: "Git",
|
|
48
|
+
docsUrl: "https://git-scm.com/downloads",
|
|
49
|
+
packageManagerArgs: {
|
|
50
|
+
winget: ["install", "-e", "--id", "Git.Git"],
|
|
51
|
+
choco: ["install", "git", "-y"],
|
|
52
|
+
scoop: ["install", "git"],
|
|
53
|
+
brew: ["install", "git"],
|
|
54
|
+
"apt-get": ["install", "-y", "git"],
|
|
55
|
+
},
|
|
56
|
+
manualHint: "Download Git from https://git-scm.com/downloads",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check whether a command is resolvable on PATH. Uses the platform resolver
|
|
62
|
+
* (`where` on Windows, `which` elsewhere) so it also finds `.cmd`/`.ps1` shims
|
|
63
|
+
* (e.g. scoop, npm) without actually executing the tool.
|
|
64
|
+
*/
|
|
65
|
+
export async function isCommandAvailable(command: string): Promise<boolean> {
|
|
66
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const result = await execa(finder, [command], { reject: false, shell: false, timeout: 15000 });
|
|
70
|
+
return !result.failed && (result.exitCode ?? 1) === 0 && result.stdout.trim().length > 0;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function detectAvailablePackageManagers(): Promise<TPackageManager[]> {
|
|
77
|
+
const candidates: TPackageManager[] = process.platform === "win32"
|
|
78
|
+
? ["winget", "choco", "scoop"]
|
|
79
|
+
: process.platform === "darwin"
|
|
80
|
+
? ["brew"]
|
|
81
|
+
: ["apt-get"];
|
|
82
|
+
|
|
83
|
+
const available: TPackageManager[] = [];
|
|
84
|
+
|
|
85
|
+
for (const manager of candidates) {
|
|
86
|
+
if (await isCommandAvailable(manager)) {
|
|
87
|
+
available.push(manager);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return available;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function resolveInstallStrategies(tool: TExternalTool): Promise<TInstallStrategy[]> {
|
|
95
|
+
const strategies: TInstallStrategy[] = [];
|
|
96
|
+
const managers = await detectAvailablePackageManagers();
|
|
97
|
+
|
|
98
|
+
for (const manager of managers) {
|
|
99
|
+
const args = tool.packageManagerArgs[manager];
|
|
100
|
+
if (args) {
|
|
101
|
+
strategies.push({ kind: "package-manager", manager, args });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (tool.npmGlobalPackage && (await isCommandAvailable("npm"))) {
|
|
106
|
+
strategies.push({ kind: "npm-global", packageName: tool.npmGlobalPackage });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return strategies;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function describeStrategy(strategy: TInstallStrategy): string {
|
|
113
|
+
if (strategy.kind === "npm-global") {
|
|
114
|
+
return `npm install -g ${strategy.packageName}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return `${strategy.manager} ${strategy.args.join(" ")}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function runInstallStrategy(strategy: TInstallStrategy): Promise<number> {
|
|
121
|
+
const [command, args] = strategy.kind === "npm-global"
|
|
122
|
+
? ["npm", ["install", "-g", strategy.packageName]] as const
|
|
123
|
+
: [strategy.manager, strategy.args] as const;
|
|
124
|
+
|
|
125
|
+
console.log(chalk.gray(`Running: ${command} ${args.join(" ")}`));
|
|
126
|
+
const result = await execa(command, args as string[], { stdio: "inherit", reject: false, shell: false });
|
|
127
|
+
return result.exitCode ?? 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function printManualInstructions(tool: TExternalTool, strategies: TInstallStrategy[]): void {
|
|
131
|
+
console.log(chalk.yellow(`${tool.displayName} ('${tool.command}') is required but was not found on PATH.`));
|
|
132
|
+
|
|
133
|
+
if (strategies.length > 0) {
|
|
134
|
+
console.log("Install it with one of:");
|
|
135
|
+
for (const strategy of strategies) {
|
|
136
|
+
console.log(` ${chalk.cyan(describeStrategy(strategy))}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(chalk.gray(tool.manualHint));
|
|
141
|
+
console.log(chalk.gray(`Docs: ${tool.docsUrl}`));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Ensure an external CLI tool is installed before a command relies on it.
|
|
146
|
+
*
|
|
147
|
+
* When the tool is missing, offer to install it via a detected package manager
|
|
148
|
+
* (or npm for npm-backed tools). This fails fast and avoids the situation where
|
|
149
|
+
* a long interactive flow only discovers a missing prerequisite at the end.
|
|
150
|
+
*/
|
|
151
|
+
export async function ensureExternalTool(toolId: keyof typeof EXTERNAL_TOOLS | string): Promise<void> {
|
|
152
|
+
const tool = EXTERNAL_TOOLS[toolId];
|
|
153
|
+
|
|
154
|
+
if (!tool) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (await isCommandAvailable(tool.command)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const strategies = await resolveInstallStrategies(tool);
|
|
163
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
164
|
+
|
|
165
|
+
if (strategies.length === 0 || !interactive) {
|
|
166
|
+
printManualInstructions(tool, strategies);
|
|
167
|
+
throw new Error(`${tool.displayName} is required. Install it, then re-run this command.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(chalk.yellow(`${tool.displayName} ('${tool.command}') is not installed.`));
|
|
171
|
+
|
|
172
|
+
const choice = await searchableSelectChoice({
|
|
173
|
+
message: `Install ${tool.displayName} now?`,
|
|
174
|
+
choices: [
|
|
175
|
+
...strategies.map((strategy, index) => ({
|
|
176
|
+
title: `Install with ${describeStrategy(strategy)}`,
|
|
177
|
+
value: String(index),
|
|
178
|
+
})),
|
|
179
|
+
{ title: "Skip and show manual instructions", value: "__SKIP__" },
|
|
180
|
+
],
|
|
181
|
+
allowCustomValue: false,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (choice === "__SKIP__") {
|
|
185
|
+
printManualInstructions(tool, strategies);
|
|
186
|
+
throw new Error(`${tool.displayName} is required. Install it, then re-run this command.`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const strategy = strategies[Number(choice)];
|
|
190
|
+
const exitCode = await runInstallStrategy(strategy);
|
|
191
|
+
|
|
192
|
+
if (exitCode !== 0) {
|
|
193
|
+
console.log(chalk.red(`Install command failed (exit code ${exitCode}).`));
|
|
194
|
+
printManualInstructions(tool, strategies);
|
|
195
|
+
throw new Error(`Could not install ${tool.displayName} automatically.`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (await isCommandAvailable(tool.command)) {
|
|
199
|
+
console.log(chalk.green(`${tool.displayName} is now installed.`));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Many installers update PATH only for new shells.
|
|
204
|
+
console.log(chalk.yellow(`${tool.displayName} was installed, but it is not on PATH in this session yet.`));
|
|
205
|
+
console.log(chalk.gray("Open a new terminal so PATH refreshes, then re-run this command."));
|
|
206
|
+
throw new Error(`${tool.displayName} install completed; restart your terminal to use it.`);
|
|
207
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export type TKeyValueMap = Record<string, string>;
|
|
2
|
+
|
|
3
|
+
export type TRepositoryInfo = {
|
|
4
|
+
repositoryPath: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type TScannedVariable = {
|
|
8
|
+
variableName: string;
|
|
9
|
+
filePath: string;
|
|
10
|
+
occurrences: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type TInstallRepositoryOptions = {
|
|
14
|
+
repositoryPath: string;
|
|
15
|
+
installCommand: string;
|
|
16
|
+
variableValues: Record<string, string>;
|
|
17
|
+
temporaryOverrides: Record<string, string>;
|
|
18
|
+
filePatterns: string[];
|
|
19
|
+
onLog?: (value: string) => void;
|
|
20
|
+
onErrorLog?: (value: string) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type TInstallRepositoryResult = {
|
|
24
|
+
stdout: string;
|
|
25
|
+
stderr: string;
|
|
26
|
+
exitCode: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type TDoctorPackageResult = {
|
|
30
|
+
packageName: string;
|
|
31
|
+
versions: string[];
|
|
32
|
+
occurrences: TPackageOccurrence[];
|
|
33
|
+
hasMultipleVersions: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type TPackageOccurrence = {
|
|
37
|
+
version?: string;
|
|
38
|
+
path?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type TLoadedLocationConflict = {
|
|
42
|
+
packageName: string;
|
|
43
|
+
rawMessage: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type TPackageConflictInspection = {
|
|
47
|
+
packageName: string;
|
|
48
|
+
suggestedVersions: string[];
|
|
49
|
+
doctorResult: TDoctorPackageResult;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type TCloudFoundryTarget = {
|
|
53
|
+
apiEndpoint?: string;
|
|
54
|
+
user?: string;
|
|
55
|
+
org?: string;
|
|
56
|
+
space?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type TCloudFoundryApp = {
|
|
60
|
+
name: string;
|
|
61
|
+
requestedState?: string;
|
|
62
|
+
processes?: string;
|
|
63
|
+
routes?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type TCloudFoundryOrgEntry = {
|
|
67
|
+
apiEndpoint: string;
|
|
68
|
+
region: string;
|
|
69
|
+
org: string;
|
|
70
|
+
spaceCount?: number;
|
|
71
|
+
spaces?: string[];
|
|
72
|
+
updatedAt: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type TCloudFoundryLoginProfile = {
|
|
76
|
+
apiEndpoint: string;
|
|
77
|
+
org: string;
|
|
78
|
+
space?: string;
|
|
79
|
+
username: string;
|
|
80
|
+
password?: string;
|
|
81
|
+
updatedAt: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type TCloudFoundryAppsCacheEntry = {
|
|
85
|
+
targetKey: string;
|
|
86
|
+
apps: TCloudFoundryApp[];
|
|
87
|
+
updatedAt: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type TCloudFoundryCache = {
|
|
91
|
+
loginProfiles: TCloudFoundryLoginProfile[];
|
|
92
|
+
appListsByTarget: Record<string, TCloudFoundryAppsCacheEntry>;
|
|
93
|
+
orgsAcrossRegions: TCloudFoundryOrgEntry[];
|
|
94
|
+
orgsAcrossRegionsUpdatedAt?: string;
|
|
95
|
+
envFileNames: string[];
|
|
96
|
+
selectedApps: string[];
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type TCdsCache = {
|
|
100
|
+
profiles: string[];
|
|
101
|
+
ports: string[];
|
|
102
|
+
services: string[];
|
|
103
|
+
edmxOutputFileNames: string[];
|
|
104
|
+
models: string[];
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type TNpmrcPackageEntry = {
|
|
108
|
+
packageId: string;
|
|
109
|
+
packageName: string;
|
|
110
|
+
scope: string;
|
|
111
|
+
host: string;
|
|
112
|
+
updatedAt: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type TNpmrcTokenEntry = {
|
|
116
|
+
scope: string;
|
|
117
|
+
host: string;
|
|
118
|
+
token: string;
|
|
119
|
+
label: string;
|
|
120
|
+
updatedAt: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type TNpmrcProjectCache = {
|
|
124
|
+
projectName: string;
|
|
125
|
+
packageIds: string[];
|
|
126
|
+
packages: TNpmrcPackageEntry[];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type TNpmrcCache = {
|
|
130
|
+
hosts: string[];
|
|
131
|
+
scopes: string[];
|
|
132
|
+
packageIds: string[];
|
|
133
|
+
packages: TNpmrcPackageEntry[];
|
|
134
|
+
packageIdsByProject: Record<string, TNpmrcProjectCache>;
|
|
135
|
+
tokens: string[];
|
|
136
|
+
tokenEntries: TNpmrcTokenEntry[];
|
|
137
|
+
outputFileNames: string[];
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export type TSimpleMdgCache = {
|
|
141
|
+
variables: Record<string, string[]>;
|
|
142
|
+
overrides: Record<string, string[]>;
|
|
143
|
+
cloudFoundry: TCloudFoundryCache;
|
|
144
|
+
cds: TCdsCache;
|
|
145
|
+
npmrc: TNpmrcCache;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export type TParsedCloudFoundryEnvironment = {
|
|
149
|
+
VCAP_SERVICES?: unknown;
|
|
150
|
+
VCAP_APPLICATION?: unknown;
|
|
151
|
+
[key: string]: unknown;
|
|
152
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { doctorPackage } from "./doctor";
|
|
2
|
+
import { rememberResolvedOverrideVersions } from "./cache";
|
|
3
|
+
import type { TLoadedLocationConflict, TPackageConflictInspection } from "./types";
|
|
4
|
+
|
|
5
|
+
export { rememberResolvedOverrideVersions };
|
|
6
|
+
|
|
7
|
+
export function parseLoadedLocationConflicts(output: string): TLoadedLocationConflict[] {
|
|
8
|
+
const result: TLoadedLocationConflict[] = [];
|
|
9
|
+
const lines = output.split(/\r?\n/);
|
|
10
|
+
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
if (!/loaded from different locations/i.test(line)) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const packageMatch = line.match(/(@[\w.-]+\/[\w.-]+|[\w.-]+)(?=(?:\s|['"`]|,|:))/);
|
|
17
|
+
|
|
18
|
+
result.push({
|
|
19
|
+
packageName: packageMatch?.[1] ?? "@sap/cds",
|
|
20
|
+
rawMessage: line.trim(),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function inspectPackageConflicts(options: {
|
|
28
|
+
repositoryPath: string;
|
|
29
|
+
packageNames: string[];
|
|
30
|
+
}): Promise<TPackageConflictInspection[]> {
|
|
31
|
+
const result: TPackageConflictInspection[] = [];
|
|
32
|
+
|
|
33
|
+
for (const packageName of options.packageNames) {
|
|
34
|
+
const doctorResult = await doctorPackage({ repositoryPath: options.repositoryPath, packageName });
|
|
35
|
+
|
|
36
|
+
if (doctorResult.hasMultipleVersions || doctorResult.occurrences.length > 1) {
|
|
37
|
+
result.push({
|
|
38
|
+
packageName,
|
|
39
|
+
doctorResult,
|
|
40
|
+
suggestedVersions: doctorResult.versions,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
}
|