simplemdg-dev-cli 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -0
- package/dist/commands/cds.command.d.ts +2 -0
- package/dist/commands/cds.command.js +377 -0
- package/dist/commands/cds.command.js.map +1 -0
- package/dist/commands/cf.command.d.ts +2 -0
- package/dist/commands/cf.command.js +667 -0
- package/dist/commands/cf.command.js.map +1 -0
- package/dist/commands/npmrc.command.d.ts +2 -0
- package/dist/commands/npmrc.command.js +447 -0
- package/dist/commands/npmrc.command.js.map +1 -0
- package/dist/core/cache.d.ts +23 -0
- package/dist/core/cache.js +269 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/cds.d.ts +16 -0
- package/dist/core/cds.js +208 -0
- package/dist/core/cds.js.map +1 -0
- package/dist/core/cf-env-parser.d.ts +2 -0
- package/dist/core/cf-env-parser.js +109 -0
- package/dist/core/cf-env-parser.js.map +1 -0
- package/dist/core/cf.d.ts +23 -0
- package/dist/core/cf.js +188 -0
- package/dist/core/cf.js.map +1 -0
- package/dist/core/doctor.d.ts +5 -0
- package/dist/core/doctor.js +54 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/install.d.ts +2 -0
- package/dist/core/install.js +77 -0
- package/dist/core/install.js.map +1 -0
- package/dist/core/npmrc.d.ts +26 -0
- package/dist/core/npmrc.js +119 -0
- package/dist/core/npmrc.js.map +1 -0
- package/dist/core/process.d.ts +29 -0
- package/dist/core/process.js +183 -0
- package/dist/core/process.js.map +1 -0
- package/dist/core/prompts.d.ts +29 -0
- package/dist/core/prompts.js +159 -0
- package/dist/core/prompts.js.map +1 -0
- package/dist/core/repository.d.ts +3 -0
- package/dist/core/repository.js +27 -0
- package/dist/core/repository.js.map +1 -0
- package/dist/core/scanner.d.ts +5 -0
- package/dist/core/scanner.js +31 -0
- package/dist/core/scanner.js.map +1 -0
- package/dist/core/types.d.ts +131 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/version-conflict.d.ts +8 -0
- package/dist/core/version-conflict.js +33 -0
- package/dist/core/version-conflict.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +320 -0
- package/dist/index.js.map +1 -0
- package/dist/types-local.d.ts +10 -0
- package/dist/types-local.js +2 -0
- package/dist/types-local.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import nodeFs from "node:fs";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import prompts from "prompts";
|
|
7
|
+
import { authenticateCloudFoundry, buildCloudFoundryTargetKey, listCloudFoundryApps, inferCloudFoundryRegionFromApiEndpoint, listCloudFoundryOrganizations, listCloudFoundrySpaces, readCloudFoundryTarget, scanCloudFoundryOrganizationsAcrossRegions, setCloudFoundryApiEndpoint, targetCloudFoundryOrg, targetCloudFoundrySpace, } from "../core/cf.js";
|
|
8
|
+
import { parseCloudFoundryEnvironment } from "../core/cf-env-parser.js";
|
|
9
|
+
import { readCache, rememberCloudFoundryApps, rememberCloudFoundryLoginProfile, rememberCloudFoundryOrgEntries, rememberEnvironmentFileName, rememberSelectedApp, } from "../core/cache.js";
|
|
10
|
+
import { ensureCommandAvailable, runCommand, runCommandInherit } from "../core/process.js";
|
|
11
|
+
import { resolveRepositoryPath } from "../core/repository.js";
|
|
12
|
+
import { searchableSelectChoice, selectFromHistoryOrInput } from "../core/prompts.js";
|
|
13
|
+
function validateRequired(value) {
|
|
14
|
+
return value.trim() ? true : "Value is required";
|
|
15
|
+
}
|
|
16
|
+
function buildCloudFoundryLogsArgs(options) {
|
|
17
|
+
const args = ["logs", options.appName];
|
|
18
|
+
if (options.recent) {
|
|
19
|
+
args.push("--recent");
|
|
20
|
+
}
|
|
21
|
+
return args;
|
|
22
|
+
}
|
|
23
|
+
function shouldIncludeLogLine(line, options) {
|
|
24
|
+
const trimmedInstance = options.instance?.trim();
|
|
25
|
+
const trimmedProcess = options.process?.trim();
|
|
26
|
+
if (!trimmedInstance && !trimmedProcess) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const normalizedLine = line.toLowerCase();
|
|
30
|
+
if (trimmedProcess) {
|
|
31
|
+
const normalizedProcess = trimmedProcess.toLowerCase();
|
|
32
|
+
if (!normalizedLine.includes(`[app/proc/${normalizedProcess}/`) && !normalizedLine.includes(`[app/${normalizedProcess}/`)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (trimmedInstance) {
|
|
37
|
+
const instancePattern = new RegExp(`\\/(?:${trimmedInstance})\\]`, "i");
|
|
38
|
+
if (!instancePattern.test(line)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
function filterCloudFoundryLogsOutput(output, options) {
|
|
45
|
+
return output
|
|
46
|
+
.split(/\r?\n/)
|
|
47
|
+
.filter((line) => shouldIncludeLogLine(line, options))
|
|
48
|
+
.join("\n");
|
|
49
|
+
}
|
|
50
|
+
function writeFilteredLogChunk(chunk, options) {
|
|
51
|
+
const text = chunk.toString("utf8");
|
|
52
|
+
const filteredText = filterCloudFoundryLogsOutput(text, options);
|
|
53
|
+
if (!filteredText.trim()) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const outputText = filteredText.endsWith("\n") ? filteredText : `${filteredText}\n`;
|
|
57
|
+
if (options.isError) {
|
|
58
|
+
process.stderr.write(outputText);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
process.stdout.write(outputText);
|
|
62
|
+
}
|
|
63
|
+
options.outputStream?.write(outputText);
|
|
64
|
+
}
|
|
65
|
+
async function refreshAppsCacheForCurrentTarget() {
|
|
66
|
+
const target = await readCloudFoundryTarget();
|
|
67
|
+
const targetKey = buildCloudFoundryTargetKey(target);
|
|
68
|
+
const apps = await listCloudFoundryApps();
|
|
69
|
+
await rememberCloudFoundryApps(targetKey, apps);
|
|
70
|
+
return apps;
|
|
71
|
+
}
|
|
72
|
+
function refreshAppsCacheInDetachedProcess() {
|
|
73
|
+
const entryFilePath = process.argv[1];
|
|
74
|
+
if (!entryFilePath) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const childProcess = spawn(process.execPath, [entryFilePath, "cf", "apps-cache-refresh"], {
|
|
78
|
+
detached: true,
|
|
79
|
+
stdio: "ignore",
|
|
80
|
+
windowsHide: true,
|
|
81
|
+
});
|
|
82
|
+
childProcess.unref();
|
|
83
|
+
}
|
|
84
|
+
async function getAppsWithCache(options) {
|
|
85
|
+
if (options.refresh) {
|
|
86
|
+
return refreshAppsCacheForCurrentTarget();
|
|
87
|
+
}
|
|
88
|
+
const target = await readCloudFoundryTarget();
|
|
89
|
+
const targetKey = buildCloudFoundryTargetKey(target);
|
|
90
|
+
const cache = await readCache();
|
|
91
|
+
const cachedEntry = cache.cloudFoundry.appListsByTarget[targetKey];
|
|
92
|
+
if (cachedEntry?.apps.length) {
|
|
93
|
+
if (options.startBackgroundRefresh) {
|
|
94
|
+
refreshAppsCacheInDetachedProcess();
|
|
95
|
+
}
|
|
96
|
+
return cachedEntry.apps;
|
|
97
|
+
}
|
|
98
|
+
return refreshAppsCacheForCurrentTarget();
|
|
99
|
+
}
|
|
100
|
+
async function resolveAppSelection(options) {
|
|
101
|
+
if (options.app?.trim()) {
|
|
102
|
+
await rememberSelectedApp(options.app.trim());
|
|
103
|
+
return options.app.trim();
|
|
104
|
+
}
|
|
105
|
+
const apps = await getAppsWithCache({ refresh: options.refresh, startBackgroundRefresh: !options.refresh });
|
|
106
|
+
const cache = await readCache();
|
|
107
|
+
const cachedSelectedAppNames = cache.cloudFoundry.selectedApps;
|
|
108
|
+
const cachedSelectedApps = cachedSelectedAppNames
|
|
109
|
+
.filter((appName) => !apps.some((app) => app.name === appName))
|
|
110
|
+
.map((appName) => ({ title: `${appName} ${chalk.gray("cached selected")}`, value: appName }));
|
|
111
|
+
const appName = await searchableSelectChoice({
|
|
112
|
+
message: options.message,
|
|
113
|
+
choices: [
|
|
114
|
+
...apps.map((app) => ({
|
|
115
|
+
title: [app.name, app.requestedState, app.routes].filter(Boolean).join(" | "),
|
|
116
|
+
value: app.name,
|
|
117
|
+
})),
|
|
118
|
+
...cachedSelectedApps,
|
|
119
|
+
],
|
|
120
|
+
validateCustomValue: validateRequired,
|
|
121
|
+
customValueTitle: (value) => `Use typed app name: ${value}`,
|
|
122
|
+
});
|
|
123
|
+
await rememberSelectedApp(appName);
|
|
124
|
+
return appName;
|
|
125
|
+
}
|
|
126
|
+
function printTarget(target) {
|
|
127
|
+
console.log(`API Endpoint: ${target.apiEndpoint ?? "N/A"}`);
|
|
128
|
+
console.log(`User: ${target.user ?? "N/A"}`);
|
|
129
|
+
console.log(`Org: ${target.org ?? "N/A"}`);
|
|
130
|
+
console.log(`Space: ${target.space ?? "N/A"}`);
|
|
131
|
+
}
|
|
132
|
+
const DEFAULT_CLOUD_FOUNDRY_API_ENDPOINTS = [
|
|
133
|
+
"https://api.cf.br10.hana.ondemand.com",
|
|
134
|
+
"https://api.cf.eu10.hana.ondemand.com",
|
|
135
|
+
"https://api.cf.us10.hana.ondemand.com",
|
|
136
|
+
"https://api.cf.ap10.hana.ondemand.com",
|
|
137
|
+
"https://api.cf.ap11.hana.ondemand.com",
|
|
138
|
+
"https://api.cf.ap21.hana.ondemand.com",
|
|
139
|
+
"https://api.cf.jp10.hana.ondemand.com",
|
|
140
|
+
"https://api.cf.ca10.hana.ondemand.com",
|
|
141
|
+
"https://api.cf.ch20.hana.ondemand.com",
|
|
142
|
+
"https://api.cf.eu20.hana.ondemand.com",
|
|
143
|
+
"https://api.cf.us20.hana.ondemand.com",
|
|
144
|
+
];
|
|
145
|
+
function uniqueValues(values) {
|
|
146
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
147
|
+
}
|
|
148
|
+
async function selectCloudFoundryApiEndpoint(options) {
|
|
149
|
+
if (options.api?.trim()) {
|
|
150
|
+
return options.api.trim();
|
|
151
|
+
}
|
|
152
|
+
const cachedApiEndpoints = uniqueValues(options.cachedApiEndpoints);
|
|
153
|
+
if (cachedApiEndpoints.length === 1) {
|
|
154
|
+
return cachedApiEndpoints[0];
|
|
155
|
+
}
|
|
156
|
+
const choices = [
|
|
157
|
+
...cachedApiEndpoints.map((apiEndpoint) => ({
|
|
158
|
+
title: `${apiEndpoint} ${chalk.gray("cached")}`,
|
|
159
|
+
value: apiEndpoint,
|
|
160
|
+
})),
|
|
161
|
+
...DEFAULT_CLOUD_FOUNDRY_API_ENDPOINTS
|
|
162
|
+
.filter((apiEndpoint) => !cachedApiEndpoints.includes(apiEndpoint))
|
|
163
|
+
.map((apiEndpoint) => ({ title: apiEndpoint, value: apiEndpoint })),
|
|
164
|
+
{ title: "Enter CF API endpoint manually", value: "__ENTER_MANUAL__" },
|
|
165
|
+
];
|
|
166
|
+
return searchableSelectChoice({
|
|
167
|
+
message: "Select CF API endpoint",
|
|
168
|
+
choices: choices.filter((choice) => choice.value !== "__ENTER_MANUAL__"),
|
|
169
|
+
validateCustomValue: validateRequired,
|
|
170
|
+
customValueTitle: (value) => `Use typed CF API endpoint: ${value}`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async function selectCloudFoundryOrganization(options) {
|
|
174
|
+
if (options.org?.trim()) {
|
|
175
|
+
return options.org.trim();
|
|
176
|
+
}
|
|
177
|
+
const organizations = await listCloudFoundryOrganizations();
|
|
178
|
+
if (organizations.length === 0) {
|
|
179
|
+
return selectFromHistoryOrInput({
|
|
180
|
+
message: "Select CF org",
|
|
181
|
+
values: options.cachedOrganizations,
|
|
182
|
+
initialValue: options.cachedOrganizations[0],
|
|
183
|
+
validate: validateRequired,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return searchableSelectChoice({
|
|
187
|
+
message: "Select CF org",
|
|
188
|
+
choices: organizations.map((organization) => ({ title: organization, value: organization })),
|
|
189
|
+
validateCustomValue: validateRequired,
|
|
190
|
+
customValueTitle: (value) => `Use typed CF org: ${value}`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function selectCloudFoundrySpace(options) {
|
|
194
|
+
if (options.space?.trim()) {
|
|
195
|
+
return options.space.trim();
|
|
196
|
+
}
|
|
197
|
+
const spaces = await listCloudFoundrySpaces();
|
|
198
|
+
if (spaces.length === 0) {
|
|
199
|
+
return selectFromHistoryOrInput({
|
|
200
|
+
message: "Select CF space",
|
|
201
|
+
values: options.cachedSpaces,
|
|
202
|
+
initialValue: options.cachedSpaces[0] ?? "app",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const initialSpace = spaces.includes("app") ? "app" : spaces[0];
|
|
206
|
+
return searchableSelectChoice({
|
|
207
|
+
message: "Select CF space",
|
|
208
|
+
choices: [
|
|
209
|
+
...spaces
|
|
210
|
+
.filter((space) => space === initialSpace)
|
|
211
|
+
.map((space) => ({ title: space, value: space })),
|
|
212
|
+
...spaces
|
|
213
|
+
.filter((space) => space !== initialSpace)
|
|
214
|
+
.map((space) => ({ title: space, value: space })),
|
|
215
|
+
],
|
|
216
|
+
validateCustomValue: validateRequired,
|
|
217
|
+
customValueTitle: (value) => `Use typed CF space: ${value}`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
async function runLoginCommand(options) {
|
|
221
|
+
const cache = await readCache();
|
|
222
|
+
const lastProfile = cache.cloudFoundry.loginProfiles[0];
|
|
223
|
+
const apiEndpoint = await selectCloudFoundryApiEndpoint({
|
|
224
|
+
api: options.api,
|
|
225
|
+
cachedApiEndpoints: cache.cloudFoundry.loginProfiles.map((item) => item.apiEndpoint),
|
|
226
|
+
});
|
|
227
|
+
const username = options.username ?? await selectFromHistoryOrInput({
|
|
228
|
+
message: "Select CF username",
|
|
229
|
+
values: cache.cloudFoundry.loginProfiles.map((item) => item.username),
|
|
230
|
+
initialValue: lastProfile?.username,
|
|
231
|
+
validate: validateRequired,
|
|
232
|
+
});
|
|
233
|
+
let password = options.password ?? lastProfile?.password;
|
|
234
|
+
if (!password) {
|
|
235
|
+
const response = await prompts({
|
|
236
|
+
type: "password",
|
|
237
|
+
name: "password",
|
|
238
|
+
message: "Enter CF password",
|
|
239
|
+
validate: validateRequired,
|
|
240
|
+
});
|
|
241
|
+
if (!response.password) {
|
|
242
|
+
throw new Error("Cancelled");
|
|
243
|
+
}
|
|
244
|
+
password = response.password;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const response = await prompts({
|
|
248
|
+
type: "select",
|
|
249
|
+
name: "useCachedPassword",
|
|
250
|
+
message: "Use cached password?",
|
|
251
|
+
choices: [
|
|
252
|
+
{ title: "Yes", value: true },
|
|
253
|
+
{ title: "No, enter password again", value: false },
|
|
254
|
+
],
|
|
255
|
+
initial: 0,
|
|
256
|
+
});
|
|
257
|
+
if (!response.useCachedPassword) {
|
|
258
|
+
const passwordResponse = await prompts({
|
|
259
|
+
type: "password",
|
|
260
|
+
name: "password",
|
|
261
|
+
message: "Enter CF password",
|
|
262
|
+
validate: validateRequired,
|
|
263
|
+
});
|
|
264
|
+
if (!passwordResponse.password) {
|
|
265
|
+
throw new Error("Cancelled");
|
|
266
|
+
}
|
|
267
|
+
password = passwordResponse.password;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const apiExitCode = await setCloudFoundryApiEndpoint(apiEndpoint);
|
|
271
|
+
if (apiExitCode !== 0) {
|
|
272
|
+
process.exitCode = apiExitCode;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const authExitCode = await authenticateCloudFoundry({ username, password });
|
|
276
|
+
if (authExitCode !== 0) {
|
|
277
|
+
process.exitCode = authExitCode;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const org = await selectCloudFoundryOrganization({
|
|
281
|
+
org: options.org,
|
|
282
|
+
cachedOrganizations: cache.cloudFoundry.loginProfiles
|
|
283
|
+
.filter((item) => item.apiEndpoint === apiEndpoint && item.username === username)
|
|
284
|
+
.map((item) => item.org),
|
|
285
|
+
});
|
|
286
|
+
const orgExitCode = await targetCloudFoundryOrg(org);
|
|
287
|
+
if (orgExitCode !== 0) {
|
|
288
|
+
process.exitCode = orgExitCode;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const space = await selectCloudFoundrySpace({
|
|
292
|
+
space: options.space,
|
|
293
|
+
cachedSpaces: cache.cloudFoundry.loginProfiles
|
|
294
|
+
.filter((item) => item.apiEndpoint === apiEndpoint && item.username === username && item.org === org)
|
|
295
|
+
.map((item) => item.space ?? "")
|
|
296
|
+
.filter(Boolean),
|
|
297
|
+
});
|
|
298
|
+
if (space) {
|
|
299
|
+
const spaceExitCode = await targetCloudFoundrySpace(space);
|
|
300
|
+
if (spaceExitCode !== 0) {
|
|
301
|
+
process.exitCode = spaceExitCode;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
await rememberCloudFoundryLoginProfile({
|
|
306
|
+
apiEndpoint,
|
|
307
|
+
username,
|
|
308
|
+
org,
|
|
309
|
+
space,
|
|
310
|
+
password: options.savePassword ? password : undefined,
|
|
311
|
+
updatedAt: new Date().toISOString(),
|
|
312
|
+
});
|
|
313
|
+
if (options.savePassword) {
|
|
314
|
+
console.log(chalk.yellow("Password was cached in ~/.simplemdg/cache.json. Do not use --save-password on shared machines."));
|
|
315
|
+
}
|
|
316
|
+
console.log(chalk.green("CF login completed."));
|
|
317
|
+
}
|
|
318
|
+
function formatCloudFoundryOrgEntry(entry, target) {
|
|
319
|
+
const isCurrent = entry.apiEndpoint === target.apiEndpoint && entry.org === target.org;
|
|
320
|
+
const spaceText = typeof entry.spaceCount === "number"
|
|
321
|
+
? `${entry.spaceCount} ${entry.spaceCount === 1 ? "space" : "spaces"}`
|
|
322
|
+
: "spaces unknown";
|
|
323
|
+
return `${entry.org} ${chalk.gray(`${entry.region} · ${spaceText}${isCurrent ? " · current" : ""}`)}`;
|
|
324
|
+
}
|
|
325
|
+
function getCloudFoundryApiEndpointsForOrgSearch(options, target, cache) {
|
|
326
|
+
return uniqueValues([
|
|
327
|
+
options.api ?? "",
|
|
328
|
+
target.apiEndpoint ?? "",
|
|
329
|
+
...cache.cloudFoundry.loginProfiles.map((item) => item.apiEndpoint),
|
|
330
|
+
...DEFAULT_CLOUD_FOUNDRY_API_ENDPOINTS,
|
|
331
|
+
]);
|
|
332
|
+
}
|
|
333
|
+
async function getCloudFoundryOrganizationsAcrossRegions(options) {
|
|
334
|
+
const target = await readCloudFoundryTarget();
|
|
335
|
+
const cache = await readCache();
|
|
336
|
+
const cachedEntries = cache.cloudFoundry.orgsAcrossRegions ?? [];
|
|
337
|
+
if (!options.refresh && cachedEntries.length) {
|
|
338
|
+
return cachedEntries;
|
|
339
|
+
}
|
|
340
|
+
const apiEndpoints = getCloudFoundryApiEndpointsForOrgSearch(options, target, cache);
|
|
341
|
+
console.log(chalk.gray(`Searching CF orgs across ${apiEndpoints.length} region endpoint(s)...`));
|
|
342
|
+
const entries = await scanCloudFoundryOrganizationsAcrossRegions(apiEndpoints);
|
|
343
|
+
if (entries.length) {
|
|
344
|
+
await rememberCloudFoundryOrgEntries(entries);
|
|
345
|
+
return entries;
|
|
346
|
+
}
|
|
347
|
+
const currentOrganizations = await listCloudFoundryOrganizations().catch(() => []);
|
|
348
|
+
const currentRegion = target.apiEndpoint ? inferCloudFoundryRegionFromApiEndpoint(target.apiEndpoint) : "current";
|
|
349
|
+
const fallbackEntries = currentOrganizations.map((organization) => ({
|
|
350
|
+
apiEndpoint: target.apiEndpoint ?? "",
|
|
351
|
+
region: currentRegion,
|
|
352
|
+
org: organization,
|
|
353
|
+
updatedAt: new Date().toISOString(),
|
|
354
|
+
}));
|
|
355
|
+
if (fallbackEntries.length) {
|
|
356
|
+
await rememberCloudFoundryOrgEntries(fallbackEntries);
|
|
357
|
+
}
|
|
358
|
+
return fallbackEntries;
|
|
359
|
+
}
|
|
360
|
+
async function runOrgCommand(options) {
|
|
361
|
+
const target = await readCloudFoundryTarget();
|
|
362
|
+
if (!target.apiEndpoint || !target.user) {
|
|
363
|
+
console.log(chalk.yellow("You are not logged in to Cloud Foundry yet. Run smdg cf login first."));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const action = options.list
|
|
367
|
+
? "list"
|
|
368
|
+
: options.switch
|
|
369
|
+
? "switch"
|
|
370
|
+
: await searchableSelectChoice({
|
|
371
|
+
message: "What do you want to do with CF org?",
|
|
372
|
+
choices: [
|
|
373
|
+
{ title: "List orgs across regions", value: "list" },
|
|
374
|
+
{ title: "Switch org across regions", value: "switch" },
|
|
375
|
+
],
|
|
376
|
+
validateCustomValue: (value) => {
|
|
377
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
378
|
+
return normalizedValue === "list" || normalizedValue === "switch"
|
|
379
|
+
? true
|
|
380
|
+
: "Type list or switch, or select one option.";
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
const organizationEntries = await getCloudFoundryOrganizationsAcrossRegions({
|
|
384
|
+
api: options.api,
|
|
385
|
+
refresh: options.refresh,
|
|
386
|
+
});
|
|
387
|
+
const latestTarget = await readCloudFoundryTarget();
|
|
388
|
+
if (action === "list") {
|
|
389
|
+
printTarget(latestTarget);
|
|
390
|
+
console.log("");
|
|
391
|
+
if (!organizationEntries.length) {
|
|
392
|
+
console.log(chalk.yellow("No orgs found for current CF user. Try smdg cf org --list --refresh after login."));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
for (const entry of organizationEntries) {
|
|
396
|
+
const marker = entry.apiEndpoint === latestTarget.apiEndpoint && entry.org === latestTarget.org ? chalk.green("*") : " ";
|
|
397
|
+
console.log(`${marker} ${formatCloudFoundryOrgEntry(entry, latestTarget)}`);
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
let selectedEntry;
|
|
402
|
+
if (options.org?.trim()) {
|
|
403
|
+
selectedEntry = organizationEntries.find((entry) => {
|
|
404
|
+
return entry.org === options.org?.trim() && (!options.api?.trim() || entry.apiEndpoint === options.api.trim());
|
|
405
|
+
}) ?? {
|
|
406
|
+
apiEndpoint: options.api?.trim() || latestTarget.apiEndpoint || "",
|
|
407
|
+
region: options.api?.trim()
|
|
408
|
+
? inferCloudFoundryRegionFromApiEndpoint(options.api.trim())
|
|
409
|
+
: inferCloudFoundryRegionFromApiEndpoint(latestTarget.apiEndpoint ?? "current"),
|
|
410
|
+
org: options.org.trim(),
|
|
411
|
+
updatedAt: new Date().toISOString(),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
const selectedIndex = await searchableSelectChoice({
|
|
416
|
+
message: "Search CF org across regions",
|
|
417
|
+
choices: organizationEntries.map((entry, index) => ({
|
|
418
|
+
title: formatCloudFoundryOrgEntry(entry, latestTarget),
|
|
419
|
+
value: String(index),
|
|
420
|
+
})),
|
|
421
|
+
validateCustomValue: validateRequired,
|
|
422
|
+
customValueTitle: (value) => `Use typed CF org in current region: ${value}`,
|
|
423
|
+
});
|
|
424
|
+
selectedEntry = organizationEntries[Number(selectedIndex)] ?? {
|
|
425
|
+
apiEndpoint: latestTarget.apiEndpoint ?? "",
|
|
426
|
+
region: inferCloudFoundryRegionFromApiEndpoint(latestTarget.apiEndpoint ?? "current"),
|
|
427
|
+
org: selectedIndex,
|
|
428
|
+
updatedAt: new Date().toISOString(),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (!selectedEntry.apiEndpoint) {
|
|
432
|
+
throw new Error("Cannot determine CF API endpoint for selected org.");
|
|
433
|
+
}
|
|
434
|
+
const apiExitCode = await setCloudFoundryApiEndpoint(selectedEntry.apiEndpoint);
|
|
435
|
+
if (apiExitCode !== 0) {
|
|
436
|
+
process.exitCode = apiExitCode;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const orgExitCode = await targetCloudFoundryOrg(selectedEntry.org);
|
|
440
|
+
if (orgExitCode !== 0) {
|
|
441
|
+
console.log(chalk.yellow("Cannot switch to this org with current CF session. Run smdg cf login for this region, then try again."));
|
|
442
|
+
process.exitCode = orgExitCode;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const spaces = selectedEntry.spaces?.length ? selectedEntry.spaces : await listCloudFoundrySpaces();
|
|
446
|
+
const currentTargetAfterOrgSwitch = await readCloudFoundryTarget();
|
|
447
|
+
const preferredSpace = options.space?.trim() || currentTargetAfterOrgSwitch.space || (spaces.includes("app") ? "app" : spaces[0]);
|
|
448
|
+
const space = options.space?.trim() || await searchableSelectChoice({
|
|
449
|
+
message: "Select CF space",
|
|
450
|
+
choices: [
|
|
451
|
+
...spaces
|
|
452
|
+
.filter((spaceName) => spaceName === preferredSpace)
|
|
453
|
+
.map((spaceName) => ({ title: `${spaceName} ${spaceName === currentTargetAfterOrgSwitch.space ? chalk.gray("current") : chalk.gray("suggested")}`, value: spaceName })),
|
|
454
|
+
...spaces
|
|
455
|
+
.filter((spaceName) => spaceName !== preferredSpace)
|
|
456
|
+
.map((spaceName) => ({ title: spaceName, value: spaceName })),
|
|
457
|
+
],
|
|
458
|
+
validateCustomValue: validateRequired,
|
|
459
|
+
customValueTitle: (value) => `Use typed CF space: ${value}`,
|
|
460
|
+
});
|
|
461
|
+
if (space) {
|
|
462
|
+
const spaceExitCode = await targetCloudFoundrySpace(space);
|
|
463
|
+
if (spaceExitCode !== 0) {
|
|
464
|
+
process.exitCode = spaceExitCode;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const switchedTarget = await readCloudFoundryTarget();
|
|
469
|
+
console.log(chalk.green("CF org/space switched."));
|
|
470
|
+
printTarget(switchedTarget);
|
|
471
|
+
}
|
|
472
|
+
async function runAppsCommand(options) {
|
|
473
|
+
const target = await readCloudFoundryTarget();
|
|
474
|
+
const targetKey = buildCloudFoundryTargetKey(target);
|
|
475
|
+
const cache = await readCache();
|
|
476
|
+
const cachedEntry = cache.cloudFoundry.appListsByTarget[targetKey];
|
|
477
|
+
printTarget(target);
|
|
478
|
+
console.log("");
|
|
479
|
+
const shouldUseCache = !options.refresh && Boolean(cachedEntry?.apps.length);
|
|
480
|
+
const apps = await getAppsWithCache({
|
|
481
|
+
refresh: options.refresh,
|
|
482
|
+
startBackgroundRefresh: shouldUseCache,
|
|
483
|
+
});
|
|
484
|
+
if (shouldUseCache && cachedEntry) {
|
|
485
|
+
console.log(chalk.gray(`Using cached cf apps: ${cachedEntry.apps.length} apps, updated at ${cachedEntry.updatedAt}`));
|
|
486
|
+
console.log(chalk.gray("Refreshing cf apps cache in background. Use --refresh when you want to wait for fresh data."));
|
|
487
|
+
console.log("");
|
|
488
|
+
}
|
|
489
|
+
else if (options.refresh) {
|
|
490
|
+
console.log(chalk.green(`Refreshed cf apps cache for ${targetKey}.`));
|
|
491
|
+
console.log("");
|
|
492
|
+
}
|
|
493
|
+
if (options.select) {
|
|
494
|
+
const appName = await resolveAppSelection({ message: "Select BTP app", refresh: options.refresh });
|
|
495
|
+
console.log(appName);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
for (const app of apps) {
|
|
499
|
+
console.log([app.name, app.requestedState, app.processes, app.routes].filter(Boolean).join(" | "));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function runAppsCacheRefreshCommand() {
|
|
503
|
+
await refreshAppsCacheForCurrentTarget();
|
|
504
|
+
}
|
|
505
|
+
async function runBindCommand(options) {
|
|
506
|
+
const repositoryPath = await resolveRepositoryPath(options.cwd ?? process.cwd());
|
|
507
|
+
const appName = await resolveAppSelection({ app: options.app, refresh: options.refresh, message: "Select app to cds bind" });
|
|
508
|
+
const exitCode = await runCommandInherit("cds", ["bind", "--to-app-services", appName], { cwd: repositoryPath });
|
|
509
|
+
process.exitCode = exitCode;
|
|
510
|
+
}
|
|
511
|
+
async function runEnvCommand(options) {
|
|
512
|
+
const repositoryPath = await resolveRepositoryPath(options.cwd ?? process.cwd());
|
|
513
|
+
const appName = await resolveAppSelection({ app: options.app, refresh: options.refresh, message: "Select app to export cf env" });
|
|
514
|
+
const cache = await readCache();
|
|
515
|
+
const outputFileName = options.out ?? await selectFromHistoryOrInput({
|
|
516
|
+
message: "Select output env file name",
|
|
517
|
+
values: cache.cloudFoundry.envFileNames,
|
|
518
|
+
initialValue: cache.cloudFoundry.envFileNames[0] ?? "default-env.json",
|
|
519
|
+
validate: validateRequired,
|
|
520
|
+
});
|
|
521
|
+
const result = await runCommand("cf", ["env", appName]);
|
|
522
|
+
if (result.exitCode !== 0) {
|
|
523
|
+
throw new Error(result.stderr || result.stdout || "cf env failed");
|
|
524
|
+
}
|
|
525
|
+
const outputPath = path.resolve(repositoryPath, outputFileName);
|
|
526
|
+
if (options.raw) {
|
|
527
|
+
await fs.writeFile(outputPath, result.stdout, "utf8");
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
const parsedEnvironment = parseCloudFoundryEnvironment(result.stdout);
|
|
531
|
+
await fs.writeJson(outputPath, parsedEnvironment, { spaces: 2 });
|
|
532
|
+
}
|
|
533
|
+
await rememberSelectedApp(appName);
|
|
534
|
+
await rememberEnvironmentFileName(outputFileName);
|
|
535
|
+
console.log(chalk.green(`Exported ${options.raw ? "raw env" : "clean JSON env"} to ${outputPath}`));
|
|
536
|
+
}
|
|
537
|
+
async function runLogsCommand(options) {
|
|
538
|
+
const appName = await resolveAppSelection({ app: options.app, refresh: options.refresh, message: "Select app to view logs" });
|
|
539
|
+
const shouldFollow = options.follow || !options.recent;
|
|
540
|
+
const shouldReadRecent = options.recent || !shouldFollow;
|
|
541
|
+
const outputPath = options.out ? path.resolve(process.cwd(), options.out) : undefined;
|
|
542
|
+
if (outputPath) {
|
|
543
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
544
|
+
}
|
|
545
|
+
if (shouldReadRecent && !shouldFollow) {
|
|
546
|
+
const result = await runCommand("cf", buildCloudFoundryLogsArgs({ appName, recent: true }));
|
|
547
|
+
const combinedOutput = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
548
|
+
const filteredOutput = filterCloudFoundryLogsOutput(combinedOutput, {
|
|
549
|
+
instance: options.instance,
|
|
550
|
+
process: options.process,
|
|
551
|
+
});
|
|
552
|
+
if (outputPath) {
|
|
553
|
+
await fs.writeFile(outputPath, filteredOutput.endsWith("\n") ? filteredOutput : `${filteredOutput}\n`, "utf8");
|
|
554
|
+
console.log(chalk.green(`Exported recent logs to ${outputPath}`));
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
console.log(filteredOutput);
|
|
558
|
+
}
|
|
559
|
+
process.exitCode = result.exitCode;
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const outputStream = outputPath ? nodeFs.createWriteStream(outputPath, { flags: "a" }) : undefined;
|
|
563
|
+
if (outputPath) {
|
|
564
|
+
console.log(chalk.gray(`Streaming logs and appending to ${outputPath}`));
|
|
565
|
+
}
|
|
566
|
+
console.log(chalk.gray("Press Ctrl+C to stop realtime logs."));
|
|
567
|
+
const resolvedCommand = await ensureCommandAvailable("cf");
|
|
568
|
+
const childProcess = spawn(resolvedCommand.command, buildCloudFoundryLogsArgs({ appName, recent: false }), {
|
|
569
|
+
env: resolvedCommand.env,
|
|
570
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
571
|
+
shell: false,
|
|
572
|
+
windowsHide: true,
|
|
573
|
+
});
|
|
574
|
+
childProcess.stdout?.on("data", (chunk) => {
|
|
575
|
+
writeFilteredLogChunk(chunk, {
|
|
576
|
+
instance: options.instance,
|
|
577
|
+
process: options.process,
|
|
578
|
+
outputStream,
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
childProcess.stderr?.on("data", (chunk) => {
|
|
582
|
+
writeFilteredLogChunk(chunk, {
|
|
583
|
+
instance: options.instance,
|
|
584
|
+
process: options.process,
|
|
585
|
+
outputStream,
|
|
586
|
+
isError: true,
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
childProcess.on("close", (exitCode) => {
|
|
590
|
+
outputStream?.end();
|
|
591
|
+
process.exitCode = exitCode ?? 0;
|
|
592
|
+
});
|
|
593
|
+
await new Promise((resolve) => {
|
|
594
|
+
childProcess.on("close", () => resolve());
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async function runTargetCommand() {
|
|
598
|
+
const target = await readCloudFoundryTarget();
|
|
599
|
+
printTarget(target);
|
|
600
|
+
}
|
|
601
|
+
async function runCacheCommand() {
|
|
602
|
+
const cache = await readCache();
|
|
603
|
+
console.log(JSON.stringify(cache.cloudFoundry, null, 2));
|
|
604
|
+
}
|
|
605
|
+
export function registerCloudFoundryCommands(program) {
|
|
606
|
+
const cfCommand = program.command("cf").description("Cloud Foundry helper commands for SimpleMDG");
|
|
607
|
+
cfCommand
|
|
608
|
+
.command("login")
|
|
609
|
+
.description("Login to Cloud Foundry and cache login profile")
|
|
610
|
+
.option("--api <apiEndpoint>", "CF API endpoint")
|
|
611
|
+
.option("--username <username>", "CF username")
|
|
612
|
+
.option("--password <password>", "CF password")
|
|
613
|
+
.option("--org <org>", "CF org")
|
|
614
|
+
.option("--space <space>", "CF space")
|
|
615
|
+
.option("--save-password", "Cache password in ~/.simplemdg/cache.json. Avoid on shared machines.")
|
|
616
|
+
.action(runLoginCommand);
|
|
617
|
+
cfCommand.command("target").description("Show current cf target").action(runTargetCommand);
|
|
618
|
+
cfCommand
|
|
619
|
+
.command("org")
|
|
620
|
+
.description("List orgs or switch to another org/space without logging in again")
|
|
621
|
+
.option("--list", "List orgs across known CF regions")
|
|
622
|
+
.option("--switch", "Switch to another org and space across known CF regions")
|
|
623
|
+
.option("--refresh", "Search orgs from CF region endpoints and update cache")
|
|
624
|
+
.option("--api <apiEndpoint>", "Limit org search/switch to one CF API endpoint")
|
|
625
|
+
.option("--org <org>", "CF org name")
|
|
626
|
+
.option("--space <space>", "CF space name")
|
|
627
|
+
.action(runOrgCommand);
|
|
628
|
+
cfCommand
|
|
629
|
+
.command("apps")
|
|
630
|
+
.description("List BTP apps in current org and space with per-target cache")
|
|
631
|
+
.option("--refresh", "Wait for fresh app list from cf apps and update cache")
|
|
632
|
+
.option("--select", "Select one app and print its name")
|
|
633
|
+
.action(runAppsCommand);
|
|
634
|
+
cfCommand
|
|
635
|
+
.command("bind")
|
|
636
|
+
.description("Run cds bind --to-app-services <app>")
|
|
637
|
+
.option("--app <appName>", "BTP app name")
|
|
638
|
+
.option("--cwd <path>", "Repository path", process.cwd())
|
|
639
|
+
.option("--refresh", "Refresh app list before selecting")
|
|
640
|
+
.action(runBindCommand);
|
|
641
|
+
cfCommand
|
|
642
|
+
.command("env")
|
|
643
|
+
.description("Export cf env <app> to clean JSON file")
|
|
644
|
+
.option("--app <appName>", "BTP app name")
|
|
645
|
+
.option("--out <fileName>", "Output file name", undefined)
|
|
646
|
+
.option("--cwd <path>", "Repository path", process.cwd())
|
|
647
|
+
.option("--refresh", "Refresh app list before selecting")
|
|
648
|
+
.option("--raw", "Export raw cf env output instead of clean JSON")
|
|
649
|
+
.action(runEnvCommand);
|
|
650
|
+
cfCommand
|
|
651
|
+
.command("logs")
|
|
652
|
+
.description("View realtime or recent logs for a BTP app")
|
|
653
|
+
.option("--app <appName>", "BTP app name")
|
|
654
|
+
.option("--refresh", "Refresh app list before selecting")
|
|
655
|
+
.option("--recent", "Show recent logs and exit")
|
|
656
|
+
.option("--follow", "Follow realtime logs. This is the default when --recent is not used")
|
|
657
|
+
.option("--out <fileName>", "Export logs to file. With realtime logs, append until Ctrl+C")
|
|
658
|
+
.option("--instance <index>", "Filter logs by app instance index, for example 0 or 1")
|
|
659
|
+
.option("--process <processName>", "Filter logs by process name, for example WEB")
|
|
660
|
+
.action(runLogsCommand);
|
|
661
|
+
cfCommand
|
|
662
|
+
.command("apps-cache-refresh")
|
|
663
|
+
.description("Refresh cached cf apps for current target. Internal command used by smdg cf apps.")
|
|
664
|
+
.action(runAppsCacheRefreshCommand);
|
|
665
|
+
cfCommand.command("cache").description("Print cached Cloud Foundry values").action(runCacheCommand);
|
|
666
|
+
}
|
|
667
|
+
//# sourceMappingURL=cf.command.js.map
|