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.
- package/README.md +65 -243
- package/USER_GUIDE.md +55 -249
- 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 +1625 -198
- 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,131 @@
|
|
|
1
|
+
import type { TParsedCloudFoundryEnvironment } from "./types";
|
|
2
|
+
|
|
3
|
+
const SECTION_MARKERS = [
|
|
4
|
+
"System-Provided:",
|
|
5
|
+
"User-Provided:",
|
|
6
|
+
"Running Environment Variable Groups:",
|
|
7
|
+
"Staging Environment Variable Groups:",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function stripAnsi(value: string): string {
|
|
11
|
+
return value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function findMatchingJsonEnd(value: string, startIndex: number): number {
|
|
15
|
+
const openingCharacter = value[startIndex];
|
|
16
|
+
const closingCharacter = openingCharacter === "{" ? "}" : "]";
|
|
17
|
+
let depth = 0;
|
|
18
|
+
let isInsideString = false;
|
|
19
|
+
let isEscaped = false;
|
|
20
|
+
|
|
21
|
+
for (let index = startIndex; index < value.length; index += 1) {
|
|
22
|
+
const character = value[index];
|
|
23
|
+
|
|
24
|
+
if (isInsideString) {
|
|
25
|
+
if (isEscaped) {
|
|
26
|
+
isEscaped = false;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (character === "\\") {
|
|
31
|
+
isEscaped = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (character === '"') {
|
|
36
|
+
isInsideString = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (character === '"') {
|
|
43
|
+
isInsideString = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (character === openingCharacter) depth += 1;
|
|
48
|
+
if (character === closingCharacter) {
|
|
49
|
+
depth -= 1;
|
|
50
|
+
if (depth === 0) return index;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return -1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseSimpleValue(rawValue: string): unknown {
|
|
58
|
+
const value = rawValue.trim();
|
|
59
|
+
|
|
60
|
+
if (value === "true") return true;
|
|
61
|
+
if (value === "false") return false;
|
|
62
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
|
|
63
|
+
|
|
64
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
65
|
+
return value.slice(1, -1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseKeyValueBlock(block: string): TParsedCloudFoundryEnvironment {
|
|
72
|
+
const result: TParsedCloudFoundryEnvironment = {};
|
|
73
|
+
let cursor = 0;
|
|
74
|
+
|
|
75
|
+
while (cursor < block.length) {
|
|
76
|
+
const nextMatch = /(^|\n)([A-Za-z_][A-Za-z0-9_]*):\s*/g;
|
|
77
|
+
nextMatch.lastIndex = cursor;
|
|
78
|
+
const match = nextMatch.exec(block);
|
|
79
|
+
|
|
80
|
+
if (!match) break;
|
|
81
|
+
|
|
82
|
+
const key = match[2];
|
|
83
|
+
const valueStart = nextMatch.lastIndex;
|
|
84
|
+
const firstCharacter = block[valueStart];
|
|
85
|
+
|
|
86
|
+
if (firstCharacter === "{" || firstCharacter === "[") {
|
|
87
|
+
const valueEnd = findMatchingJsonEnd(block, valueStart);
|
|
88
|
+
if (valueEnd === -1) {
|
|
89
|
+
throw new Error(`Cannot parse JSON value for ${key}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rawJson = block.slice(valueStart, valueEnd + 1);
|
|
93
|
+
result[key] = JSON.parse(rawJson);
|
|
94
|
+
cursor = valueEnd + 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const nextLineIndex = block.indexOf("\n", valueStart);
|
|
99
|
+
const valueEnd = nextLineIndex === -1 ? block.length : nextLineIndex;
|
|
100
|
+
result[key] = parseSimpleValue(block.slice(valueStart, valueEnd));
|
|
101
|
+
cursor = valueEnd + 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getSection(raw: string, sectionName: string): string {
|
|
108
|
+
const startIndex = raw.indexOf(sectionName);
|
|
109
|
+
if (startIndex === -1) return "";
|
|
110
|
+
|
|
111
|
+
const contentStartIndex = startIndex + sectionName.length;
|
|
112
|
+
const nextSectionIndexes = SECTION_MARKERS
|
|
113
|
+
.filter((marker) => marker !== sectionName)
|
|
114
|
+
.map((marker) => raw.indexOf(marker, contentStartIndex))
|
|
115
|
+
.filter((index) => index !== -1);
|
|
116
|
+
|
|
117
|
+
const contentEndIndex = nextSectionIndexes.length > 0 ? Math.min(...nextSectionIndexes) : raw.length;
|
|
118
|
+
return raw.slice(contentStartIndex, contentEndIndex).trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function parseCloudFoundryEnvironment(rawOutput: string): TParsedCloudFoundryEnvironment {
|
|
122
|
+
const cleanedOutput = stripAnsi(rawOutput);
|
|
123
|
+
const systemProvidedBlock = getSection(cleanedOutput, "System-Provided:");
|
|
124
|
+
const userProvidedBlock = getSection(cleanedOutput, "User-Provided:");
|
|
125
|
+
const result: TParsedCloudFoundryEnvironment = {};
|
|
126
|
+
|
|
127
|
+
Object.assign(result, parseKeyValueBlock(systemProvidedBlock));
|
|
128
|
+
Object.assign(result, parseKeyValueBlock(userProvidedBlock));
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
package/src/core/cf.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { TCloudFoundryApp, TCloudFoundryOrgEntry, TCloudFoundryTarget } from "./types";
|
|
2
|
+
import { runCommand } from "./process";
|
|
3
|
+
|
|
4
|
+
export function buildCloudFoundryTargetKey(target: TCloudFoundryTarget): string {
|
|
5
|
+
return [
|
|
6
|
+
target.apiEndpoint ?? "unknown-api",
|
|
7
|
+
target.org ?? "unknown-org",
|
|
8
|
+
target.space ?? "unknown-space",
|
|
9
|
+
].join("|");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function readCloudFoundryTarget(): Promise<TCloudFoundryTarget> {
|
|
13
|
+
const result = await runCommand("cf", ["target"]);
|
|
14
|
+
|
|
15
|
+
if (result.exitCode !== 0) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const target: TCloudFoundryTarget = {};
|
|
20
|
+
|
|
21
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
22
|
+
const [rawKey, ...rawValueParts] = line.split(":");
|
|
23
|
+
const key = rawKey.trim().toLowerCase();
|
|
24
|
+
const value = rawValueParts.join(":").trim();
|
|
25
|
+
|
|
26
|
+
if (key === "api endpoint") target.apiEndpoint = value;
|
|
27
|
+
if (key === "user") target.user = value;
|
|
28
|
+
if (key === "org") target.org = value;
|
|
29
|
+
if (key === "space") target.space = value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return target;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
export async function setCloudFoundryApiEndpoint(apiEndpoint: string): Promise<number> {
|
|
37
|
+
const result = await runCommand("cf", ["api", apiEndpoint]);
|
|
38
|
+
|
|
39
|
+
if (result.stdout) console.log(result.stdout);
|
|
40
|
+
if (result.stderr) console.error(result.stderr);
|
|
41
|
+
|
|
42
|
+
return result.exitCode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function authenticateCloudFoundry(options: {
|
|
46
|
+
username: string;
|
|
47
|
+
password: string;
|
|
48
|
+
}): Promise<number> {
|
|
49
|
+
const result = await runCommand("cf", ["auth", options.username, options.password]);
|
|
50
|
+
|
|
51
|
+
if (result.stdout) console.log(result.stdout);
|
|
52
|
+
if (result.stderr) console.error(result.stderr);
|
|
53
|
+
|
|
54
|
+
return result.exitCode;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function targetCloudFoundryOrg(org: string): Promise<number> {
|
|
58
|
+
const result = await runCommand("cf", ["target", "-o", org]);
|
|
59
|
+
|
|
60
|
+
if (result.stdout) console.log(result.stdout);
|
|
61
|
+
if (result.stderr) console.error(result.stderr);
|
|
62
|
+
|
|
63
|
+
return result.exitCode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function targetCloudFoundrySpace(space: string): Promise<number> {
|
|
67
|
+
const result = await runCommand("cf", ["target", "-s", space]);
|
|
68
|
+
|
|
69
|
+
if (result.stdout) console.log(result.stdout);
|
|
70
|
+
if (result.stderr) console.error(result.stderr);
|
|
71
|
+
|
|
72
|
+
return result.exitCode;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function listCloudFoundryOrganizations(): Promise<string[]> {
|
|
76
|
+
const result = await runCommand("cf", ["orgs"]);
|
|
77
|
+
|
|
78
|
+
if (result.exitCode !== 0) {
|
|
79
|
+
throw new Error(result.stderr || result.stdout || "cf orgs failed");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return parseCloudFoundryNameList(result.stdout, "name");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function listCloudFoundrySpaces(): Promise<string[]> {
|
|
86
|
+
const result = await runCommand("cf", ["spaces"]);
|
|
87
|
+
|
|
88
|
+
if (result.exitCode !== 0) {
|
|
89
|
+
throw new Error(result.stderr || result.stdout || "cf spaces failed");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return parseCloudFoundryNameList(result.stdout, "name");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseCloudFoundryNameList(output: string, headerName: string): string[] {
|
|
96
|
+
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
97
|
+
const headerIndex = lines.findIndex((line) => line.toLowerCase() === headerName.toLowerCase());
|
|
98
|
+
|
|
99
|
+
if (headerIndex === -1) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return lines
|
|
104
|
+
.slice(headerIndex + 1)
|
|
105
|
+
.filter((line) => !/^Getting\s+/i.test(line))
|
|
106
|
+
.filter((line) => !/^OK$/i.test(line))
|
|
107
|
+
.filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
export function inferCloudFoundryRegionFromApiEndpoint(apiEndpoint: string): string {
|
|
110
|
+
const match = apiEndpoint.match(/api\.cf\.([^.]+)\./i);
|
|
111
|
+
return match?.[1] ?? apiEndpoint.replace(/^https?:\/\//, "");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function scanCloudFoundryOrganizationsAcrossRegions(apiEndpoints: string[], credentials: Array<{ apiEndpoint: string; username: string; password?: string }> = []): Promise<TCloudFoundryOrgEntry[]> {
|
|
115
|
+
const originalTarget = await readCloudFoundryTarget();
|
|
116
|
+
const orgEntries: TCloudFoundryOrgEntry[] = [];
|
|
117
|
+
const uniqueApiEndpoints = [...new Set(apiEndpoints.map((apiEndpoint) => apiEndpoint.trim()).filter(Boolean))];
|
|
118
|
+
|
|
119
|
+
for (const apiEndpoint of uniqueApiEndpoints) {
|
|
120
|
+
const apiResult = await runCommand("cf", ["api", apiEndpoint]);
|
|
121
|
+
|
|
122
|
+
if (apiResult.exitCode !== 0) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let orgResult = await runCommand("cf", ["orgs"]);
|
|
127
|
+
|
|
128
|
+
if (orgResult.exitCode !== 0) {
|
|
129
|
+
const endpointCredentials = [
|
|
130
|
+
...credentials.filter((item) => item.apiEndpoint === apiEndpoint && item.password?.trim()),
|
|
131
|
+
...credentials.filter((item) => item.apiEndpoint !== apiEndpoint && item.password?.trim()),
|
|
132
|
+
];
|
|
133
|
+
const triedUsers = new Set<string>();
|
|
134
|
+
|
|
135
|
+
for (const credential of endpointCredentials) {
|
|
136
|
+
const credentialKey = `${credential.username}|${credential.password ?? ""}`;
|
|
137
|
+
|
|
138
|
+
if (triedUsers.has(credentialKey)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
triedUsers.add(credentialKey);
|
|
143
|
+
const authResult = await runCommand("cf", ["auth", credential.username, credential.password as string]);
|
|
144
|
+
|
|
145
|
+
if (authResult.exitCode !== 0) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
orgResult = await runCommand("cf", ["orgs"]);
|
|
150
|
+
|
|
151
|
+
if (orgResult.exitCode === 0) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (orgResult.exitCode !== 0) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const organizations = parseCloudFoundryNameList(orgResult.stdout, "name");
|
|
162
|
+
const region = inferCloudFoundryRegionFromApiEndpoint(apiEndpoint);
|
|
163
|
+
|
|
164
|
+
for (const org of organizations) {
|
|
165
|
+
let spaces: string[] = [];
|
|
166
|
+
const orgTargetResult = await runCommand("cf", ["target", "-o", org]);
|
|
167
|
+
|
|
168
|
+
if (orgTargetResult.exitCode === 0) {
|
|
169
|
+
const spacesResult = await runCommand("cf", ["spaces"]);
|
|
170
|
+
spaces = spacesResult.exitCode === 0 ? parseCloudFoundryNameList(spacesResult.stdout, "name") : [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
orgEntries.push({
|
|
174
|
+
apiEndpoint,
|
|
175
|
+
region,
|
|
176
|
+
org,
|
|
177
|
+
spaceCount: spaces.length,
|
|
178
|
+
spaces,
|
|
179
|
+
updatedAt: new Date().toISOString(),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (originalTarget.apiEndpoint) {
|
|
185
|
+
await runCommand("cf", ["api", originalTarget.apiEndpoint]);
|
|
186
|
+
|
|
187
|
+
if (originalTarget.org) {
|
|
188
|
+
await runCommand("cf", ["target", "-o", originalTarget.org]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (originalTarget.space) {
|
|
192
|
+
await runCommand("cf", ["target", "-s", originalTarget.space]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return orgEntries.sort((left, right) => {
|
|
197
|
+
const byOrg = left.org.localeCompare(right.org);
|
|
198
|
+
return byOrg !== 0 ? byOrg : left.region.localeCompare(right.region);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function loginCloudFoundry(options: {
|
|
203
|
+
apiEndpoint: string;
|
|
204
|
+
username: string;
|
|
205
|
+
password: string;
|
|
206
|
+
org: string;
|
|
207
|
+
space?: string;
|
|
208
|
+
}): Promise<number> {
|
|
209
|
+
const apiExitCode = await setCloudFoundryApiEndpoint(options.apiEndpoint);
|
|
210
|
+
|
|
211
|
+
if (apiExitCode !== 0) {
|
|
212
|
+
return apiExitCode;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const authExitCode = await authenticateCloudFoundry({
|
|
216
|
+
username: options.username,
|
|
217
|
+
password: options.password,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (authExitCode !== 0) {
|
|
221
|
+
return authExitCode;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const orgExitCode = await targetCloudFoundryOrg(options.org);
|
|
225
|
+
|
|
226
|
+
if (orgExitCode !== 0) {
|
|
227
|
+
return orgExitCode;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!options.space?.trim()) {
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return targetCloudFoundrySpace(options.space.trim());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function listCloudFoundryApps(): Promise<TCloudFoundryApp[]> {
|
|
238
|
+
const result = await runCommand("cf", ["apps"]);
|
|
239
|
+
|
|
240
|
+
if (result.exitCode !== 0) {
|
|
241
|
+
throw new Error(result.stderr || result.stdout || "cf apps failed");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return parseCloudFoundryApps(result.stdout);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function parseCloudFoundryApps(output: string): TCloudFoundryApp[] {
|
|
248
|
+
const lines = output.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean);
|
|
249
|
+
const nameHeaderIndex = lines.findIndex((line) => /^name\s+/i.test(line.trim()));
|
|
250
|
+
|
|
251
|
+
if (nameHeaderIndex === -1) {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const result: TCloudFoundryApp[] = [];
|
|
256
|
+
|
|
257
|
+
for (const line of lines.slice(nameHeaderIndex + 1)) {
|
|
258
|
+
if (/^Getting apps/i.test(line) || /^OK$/i.test(line)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const columns = line.trim().split(/\s{2,}|\t+/).filter(Boolean);
|
|
263
|
+
const [name, requestedState, processes, routes] = columns;
|
|
264
|
+
|
|
265
|
+
if (name && name !== "name") {
|
|
266
|
+
result.push({ name, requestedState, processes, routes });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { readCache, rememberCloudFoundryApps } from "../cache";
|
|
2
|
+
import {
|
|
3
|
+
buildCloudFoundryTargetKey,
|
|
4
|
+
authenticateCloudFoundry,
|
|
5
|
+
inferCloudFoundryRegionFromApiEndpoint,
|
|
6
|
+
listCloudFoundryApps,
|
|
7
|
+
readCloudFoundryTarget,
|
|
8
|
+
setCloudFoundryApiEndpoint,
|
|
9
|
+
targetCloudFoundryOrg,
|
|
10
|
+
targetCloudFoundrySpace,
|
|
11
|
+
} from "../cf";
|
|
12
|
+
import { runCommand } from "../process";
|
|
13
|
+
import { parseCloudFoundryEnvironment } from "../cf-env-parser";
|
|
14
|
+
import { detectDatabaseServiceCandidates } from "./db-vcap-parser";
|
|
15
|
+
import { upsertConnectionFromDraft } from "./db-cache";
|
|
16
|
+
import type { TConnectionDraft } from "./db-cache";
|
|
17
|
+
import type { TDatabaseConnectionProfile, TDatabaseServiceCandidate, TDatabaseType } from "./db-types";
|
|
18
|
+
import type { TCloudFoundryApp, TCloudFoundryTarget } from "../types";
|
|
19
|
+
|
|
20
|
+
export type TCloudFoundrySessionState = {
|
|
21
|
+
loggedIn: boolean;
|
|
22
|
+
target: TCloudFoundryTarget;
|
|
23
|
+
message?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
async function isCloudFoundryCliAvailable(): Promise<boolean> {
|
|
27
|
+
const result = await runCommand("cf", ["--version"]).catch(() => undefined);
|
|
28
|
+
return Boolean(result && result.exitCode === 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Confirm there is a usable CF session. When the CF CLI reports no session, try
|
|
33
|
+
* to silently re-login using a cached profile that stored its password.
|
|
34
|
+
*/
|
|
35
|
+
export async function ensureCloudFoundrySession(): Promise<TCloudFoundrySessionState> {
|
|
36
|
+
if (!(await isCloudFoundryCliAvailable())) {
|
|
37
|
+
return {
|
|
38
|
+
loggedIn: false,
|
|
39
|
+
target: {},
|
|
40
|
+
message: "Cloud Foundry CLI 'cf' is not installed or not on PATH. Install it, then run: smdg cf login",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const orgsCheck = await runCommand("cf", ["orgs"]);
|
|
45
|
+
|
|
46
|
+
if (orgsCheck.exitCode === 0) {
|
|
47
|
+
return { loggedIn: true, target: await readCloudFoundryTarget() };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const cache = await readCache();
|
|
51
|
+
const target = await readCloudFoundryTarget();
|
|
52
|
+
const profilesWithPassword = cache.cloudFoundry.loginProfiles.filter((profile) => profile.password?.trim());
|
|
53
|
+
|
|
54
|
+
if (profilesWithPassword.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
loggedIn: false,
|
|
57
|
+
target,
|
|
58
|
+
message: "Not logged in to Cloud Foundry and no cached password was found. Run: smdg cf login",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const preferredProfiles = target.apiEndpoint
|
|
63
|
+
? [
|
|
64
|
+
...profilesWithPassword.filter((profile) => profile.apiEndpoint === target.apiEndpoint),
|
|
65
|
+
...profilesWithPassword.filter((profile) => profile.apiEndpoint !== target.apiEndpoint),
|
|
66
|
+
]
|
|
67
|
+
: profilesWithPassword;
|
|
68
|
+
|
|
69
|
+
for (const profile of preferredProfiles) {
|
|
70
|
+
const apiExitCode = await setCloudFoundryApiEndpoint(profile.apiEndpoint);
|
|
71
|
+
if (apiExitCode !== 0) continue;
|
|
72
|
+
|
|
73
|
+
const authExitCode = await authenticateCloudFoundry({ username: profile.username, password: profile.password as string });
|
|
74
|
+
if (authExitCode !== 0) continue;
|
|
75
|
+
|
|
76
|
+
await targetCloudFoundryOrg(profile.org).catch(() => undefined);
|
|
77
|
+
if (profile.space) {
|
|
78
|
+
await targetCloudFoundrySpace(profile.space).catch(() => undefined);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const recheck = await runCommand("cf", ["orgs"]);
|
|
82
|
+
if (recheck.exitCode === 0) {
|
|
83
|
+
return { loggedIn: true, target: await readCloudFoundryTarget() };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
loggedIn: false,
|
|
89
|
+
target,
|
|
90
|
+
message: "Automatic Cloud Foundry re-login failed. Run: smdg cf login",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function getCloudFoundryTargetSummary(): Promise<TCloudFoundryTarget & { region?: string }> {
|
|
95
|
+
const target = await readCloudFoundryTarget();
|
|
96
|
+
return {
|
|
97
|
+
...target,
|
|
98
|
+
region: target.apiEndpoint ? inferCloudFoundryRegionFromApiEndpoint(target.apiEndpoint) : undefined,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function listCloudFoundryAppsWithCache(options?: { refresh?: boolean }): Promise<TCloudFoundryApp[]> {
|
|
103
|
+
const target = await readCloudFoundryTarget();
|
|
104
|
+
const targetKey = buildCloudFoundryTargetKey(target);
|
|
105
|
+
|
|
106
|
+
if (!options?.refresh) {
|
|
107
|
+
const cache = await readCache();
|
|
108
|
+
const cachedEntry = cache.cloudFoundry.appListsByTarget[targetKey];
|
|
109
|
+
if (cachedEntry?.apps.length) {
|
|
110
|
+
return cachedEntry.apps;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const apps = await listCloudFoundryApps();
|
|
115
|
+
await rememberCloudFoundryApps(targetKey, apps).catch(() => undefined);
|
|
116
|
+
return apps;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read and parse VCAP_SERVICES from `cf env <app>`. Never logs the raw output
|
|
121
|
+
* because it contains credentials.
|
|
122
|
+
*/
|
|
123
|
+
export async function readAppVcapServices(appName: string): Promise<unknown> {
|
|
124
|
+
const result = await runCommand("cf", ["env", appName]);
|
|
125
|
+
|
|
126
|
+
if (result.exitCode !== 0) {
|
|
127
|
+
throw new Error(result.stderr || result.stdout || `cf env ${appName} failed`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parsed = parseCloudFoundryEnvironment(result.stdout);
|
|
131
|
+
|
|
132
|
+
if (parsed.VCAP_SERVICES === undefined) {
|
|
133
|
+
throw new Error(`VCAP_SERVICES was not found in cf env ${appName}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return parsed.VCAP_SERVICES;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function detectAppDatabaseServices(appName: string): Promise<TDatabaseServiceCandidate[]> {
|
|
140
|
+
const vcapServices = await readAppVcapServices(appName);
|
|
141
|
+
const candidates = detectDatabaseServiceCandidates(vcapServices);
|
|
142
|
+
|
|
143
|
+
if (candidates.length === 0) {
|
|
144
|
+
throw new Error(`No HANA or PostgreSQL service was detected in cf env ${appName}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return candidates;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function buildDraftFromCandidate(
|
|
151
|
+
candidate: TDatabaseServiceCandidate,
|
|
152
|
+
context: { region?: string; org?: string; space?: string; app?: string },
|
|
153
|
+
): TConnectionDraft {
|
|
154
|
+
const databaseTypeLabel: Record<TDatabaseType, string> = { hana: "HANA", postgresql: "PostgreSQL" };
|
|
155
|
+
const namePieces = [context.app ?? "btp-app", candidate.serviceName, databaseTypeLabel[candidate.type]].filter(Boolean);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
name: namePieces.join(" / "),
|
|
159
|
+
type: candidate.type,
|
|
160
|
+
region: context.region,
|
|
161
|
+
org: context.org,
|
|
162
|
+
space: context.space,
|
|
163
|
+
app: context.app,
|
|
164
|
+
serviceName: candidate.serviceName,
|
|
165
|
+
servicePlan: candidate.servicePlan,
|
|
166
|
+
host: candidate.host,
|
|
167
|
+
port: candidate.port,
|
|
168
|
+
database: candidate.database,
|
|
169
|
+
schema: candidate.schema,
|
|
170
|
+
username: candidate.username,
|
|
171
|
+
password: candidate.password,
|
|
172
|
+
ssl: candidate.ssl,
|
|
173
|
+
sslValidateCertificate: candidate.sslValidateCertificate,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Import a single detected database service from a BTP app into the encrypted
|
|
179
|
+
* connection cache.
|
|
180
|
+
*/
|
|
181
|
+
export async function importConnectionFromApp(options: {
|
|
182
|
+
app: string;
|
|
183
|
+
serviceName?: string;
|
|
184
|
+
type?: TDatabaseType;
|
|
185
|
+
context?: { region?: string; org?: string; space?: string };
|
|
186
|
+
}): Promise<{ profile: TDatabaseConnectionProfile; candidates: TDatabaseServiceCandidate[] }> {
|
|
187
|
+
const candidates = await detectAppDatabaseServices(options.app);
|
|
188
|
+
const target = await readCloudFoundryTarget();
|
|
189
|
+
|
|
190
|
+
const chosen = options.serviceName
|
|
191
|
+
? candidates.find((candidate) => candidate.serviceName === options.serviceName && (!options.type || candidate.type === options.type))
|
|
192
|
+
: candidates[0];
|
|
193
|
+
|
|
194
|
+
if (!chosen) {
|
|
195
|
+
throw new Error(`Service '${options.serviceName ?? ""}' was not found among detected database services for ${options.app}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const draft = buildDraftFromCandidate(chosen, {
|
|
199
|
+
region: options.context?.region ?? (target.apiEndpoint ? inferCloudFoundryRegionFromApiEndpoint(target.apiEndpoint) : undefined),
|
|
200
|
+
org: options.context?.org ?? target.org,
|
|
201
|
+
space: options.context?.space ?? target.space,
|
|
202
|
+
app: options.app,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const profile = await upsertConnectionFromDraft(draft);
|
|
206
|
+
return { profile, candidates };
|
|
207
|
+
}
|