simplemdg-dev-cli 1.2.0 → 1.3.1
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 +130 -71
- package/USER_GUIDE.md +229 -0
- package/dist/commands/cds.command.js +0 -3
- package/dist/commands/cds.command.js.map +1 -1
- package/dist/commands/cf.command.js +561 -13
- package/dist/commands/cf.command.js.map +1 -1
- package/dist/core/cds.js +1 -0
- package/dist/core/cds.js.map +1 -1
- package/dist/core/cf.d.ts +5 -1
- package/dist/core/cf.js +15 -5
- package/dist/core/cf.js.map +1 -1
- package/dist/core/guide.d.ts +5 -0
- package/dist/core/guide.js +228 -0
- package/dist/core/guide.js.map +1 -0
- package/dist/core/process.d.ts +2 -15
- package/dist/core/process.js +2 -129
- package/dist/core/process.js.map +1 -1
- package/dist/index.js +46 -27
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
|
@@ -7,12 +7,73 @@ import prompts from "prompts";
|
|
|
7
7
|
import { authenticateCloudFoundry, buildCloudFoundryTargetKey, listCloudFoundryApps, inferCloudFoundryRegionFromApiEndpoint, listCloudFoundryOrganizations, listCloudFoundrySpaces, readCloudFoundryTarget, scanCloudFoundryOrganizationsAcrossRegions, setCloudFoundryApiEndpoint, targetCloudFoundryOrg, targetCloudFoundrySpace, } from "../core/cf.js";
|
|
8
8
|
import { parseCloudFoundryEnvironment } from "../core/cf-env-parser.js";
|
|
9
9
|
import { readCache, rememberCloudFoundryApps, rememberCloudFoundryLoginProfile, rememberCloudFoundryOrgEntries, rememberEnvironmentFileName, rememberSelectedApp, } from "../core/cache.js";
|
|
10
|
-
import {
|
|
10
|
+
import { runCommand, runCommandInherit } from "../core/process.js";
|
|
11
11
|
import { resolveRepositoryPath } from "../core/repository.js";
|
|
12
12
|
import { searchableSelectChoice, selectFromHistoryOrInput } from "../core/prompts.js";
|
|
13
13
|
function validateRequired(value) {
|
|
14
14
|
return value.trim() ? true : "Value is required";
|
|
15
15
|
}
|
|
16
|
+
async function ensureCloudFoundrySessionFromCache() {
|
|
17
|
+
const target = await readCloudFoundryTarget();
|
|
18
|
+
if (target.apiEndpoint && target.user) {
|
|
19
|
+
return target;
|
|
20
|
+
}
|
|
21
|
+
const cache = await readCache();
|
|
22
|
+
const profilesWithPassword = cache.cloudFoundry.loginProfiles.filter((profile) => profile.password?.trim());
|
|
23
|
+
if (!profilesWithPassword.length) {
|
|
24
|
+
console.log(chalk.yellow("You are not logged in to Cloud Foundry yet and no cached password was found."));
|
|
25
|
+
console.log(chalk.gray("Run smdg cf login once and choose to save the password for automatic re-login."));
|
|
26
|
+
throw new Error("Cloud Foundry login is required");
|
|
27
|
+
}
|
|
28
|
+
const preferredProfiles = target.apiEndpoint
|
|
29
|
+
? [
|
|
30
|
+
...profilesWithPassword.filter((profile) => profile.apiEndpoint === target.apiEndpoint),
|
|
31
|
+
...profilesWithPassword.filter((profile) => profile.apiEndpoint !== target.apiEndpoint),
|
|
32
|
+
]
|
|
33
|
+
: profilesWithPassword;
|
|
34
|
+
const selectedProfileIndex = preferredProfiles.length === 1
|
|
35
|
+
? "0"
|
|
36
|
+
: await searchableSelectChoice({
|
|
37
|
+
message: "Select cached CF login profile for automatic re-login",
|
|
38
|
+
choices: preferredProfiles.map((profile, index) => ({
|
|
39
|
+
title: `${profile.username} · ${profile.org}${profile.space ? `/${profile.space}` : ""} · ${inferCloudFoundryRegionFromApiEndpoint(profile.apiEndpoint)}`,
|
|
40
|
+
value: String(index),
|
|
41
|
+
})),
|
|
42
|
+
allowCustomValue: false,
|
|
43
|
+
});
|
|
44
|
+
const profile = preferredProfiles[Number(selectedProfileIndex)] ?? preferredProfiles[0];
|
|
45
|
+
console.log(chalk.gray(`Auto login CF: ${profile.username} · ${inferCloudFoundryRegionFromApiEndpoint(profile.apiEndpoint)} · ${profile.org}${profile.space ? `/${profile.space}` : ""}`));
|
|
46
|
+
const apiExitCode = await setCloudFoundryApiEndpoint(profile.apiEndpoint);
|
|
47
|
+
if (apiExitCode !== 0) {
|
|
48
|
+
process.exitCode = apiExitCode;
|
|
49
|
+
throw new Error("CF api target failed");
|
|
50
|
+
}
|
|
51
|
+
const authExitCode = await authenticateCloudFoundry({
|
|
52
|
+
username: profile.username,
|
|
53
|
+
password: profile.password,
|
|
54
|
+
});
|
|
55
|
+
if (authExitCode !== 0) {
|
|
56
|
+
process.exitCode = authExitCode;
|
|
57
|
+
throw new Error("CF automatic login failed. Run smdg cf login and update the cached password.");
|
|
58
|
+
}
|
|
59
|
+
const orgExitCode = await targetCloudFoundryOrg(profile.org);
|
|
60
|
+
if (orgExitCode !== 0) {
|
|
61
|
+
process.exitCode = orgExitCode;
|
|
62
|
+
throw new Error("CF org target failed");
|
|
63
|
+
}
|
|
64
|
+
if (profile.space) {
|
|
65
|
+
const spaceExitCode = await targetCloudFoundrySpace(profile.space);
|
|
66
|
+
if (spaceExitCode !== 0) {
|
|
67
|
+
process.exitCode = spaceExitCode;
|
|
68
|
+
throw new Error("CF space target failed");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await rememberCloudFoundryLoginProfile({
|
|
72
|
+
...profile,
|
|
73
|
+
updatedAt: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
return readCloudFoundryTarget();
|
|
76
|
+
}
|
|
16
77
|
function buildCloudFoundryLogsArgs(options) {
|
|
17
78
|
const args = ["logs", options.appName];
|
|
18
79
|
if (options.recent) {
|
|
@@ -20,6 +81,293 @@ function buildCloudFoundryLogsArgs(options) {
|
|
|
20
81
|
}
|
|
21
82
|
return args;
|
|
22
83
|
}
|
|
84
|
+
function parsePositivePort(value, defaultValue) {
|
|
85
|
+
if (!value?.trim()) {
|
|
86
|
+
return defaultValue;
|
|
87
|
+
}
|
|
88
|
+
const port = Number(value.trim());
|
|
89
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
90
|
+
throw new Error(`Invalid port: ${value}`);
|
|
91
|
+
}
|
|
92
|
+
return port;
|
|
93
|
+
}
|
|
94
|
+
function buildNodeInspectorRemoteCommand(remotePort) {
|
|
95
|
+
if (remotePort !== 9229) {
|
|
96
|
+
return [
|
|
97
|
+
`echo "Remote port ${remotePort} was requested."`,
|
|
98
|
+
`echo "SIGUSR1 starts the Node.js inspector on its default port 9229 for a running Node process."`,
|
|
99
|
+
`echo "Use remote port 9229, or start the app process with NODE_OPTIONS=--inspect=127.0.0.1:${remotePort}."`,
|
|
100
|
+
`exit 2`,
|
|
101
|
+
].join("; ");
|
|
102
|
+
}
|
|
103
|
+
return [
|
|
104
|
+
`PID=$(pgrep -xo node || pgrep -x node | head -n 1)`,
|
|
105
|
+
`if [ -z "$PID" ]; then echo "No Node.js PID found in app container" >&2; exit 1; fi`,
|
|
106
|
+
`if command -v ss >/dev/null 2>&1 && ss -H -ntl "sport = :9229" | grep -q .; then echo "Node inspector already listening on 127.0.0.1:9229"`,
|
|
107
|
+
`else kill -s SIGUSR1 "$PID" && echo "Started Node inspector for PID $PID on 127.0.0.1:9229"; fi`,
|
|
108
|
+
`tail -f /dev/null`,
|
|
109
|
+
].join("; ");
|
|
110
|
+
}
|
|
111
|
+
function buildCloudFoundryDebugSshArgs(options) {
|
|
112
|
+
const args = [
|
|
113
|
+
"ssh",
|
|
114
|
+
options.appName,
|
|
115
|
+
"-i",
|
|
116
|
+
options.instanceIndex,
|
|
117
|
+
];
|
|
118
|
+
if (options.processName?.trim()) {
|
|
119
|
+
args.push("--process", options.processName.trim());
|
|
120
|
+
}
|
|
121
|
+
args.push("-T", "-L", `${options.localPort}:127.0.0.1:${options.remotePort}`, "-c", buildNodeInspectorRemoteCommand(options.remotePort));
|
|
122
|
+
return args;
|
|
123
|
+
}
|
|
124
|
+
async function getNodeInspectorDebugUrl(localPort) {
|
|
125
|
+
const response = await fetch(`http://127.0.0.1:${localPort}/json/list`);
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const targets = await response.json();
|
|
130
|
+
const webSocketDebuggerUrl = targets.find((target) => target.webSocketDebuggerUrl)?.webSocketDebuggerUrl;
|
|
131
|
+
if (!webSocketDebuggerUrl) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const webSocketAddress = webSocketDebuggerUrl.replace(/^ws:\/\//, "");
|
|
135
|
+
return `devtools://devtools/bundled/inspector.html?ws=${webSocketAddress}`;
|
|
136
|
+
}
|
|
137
|
+
async function waitForNodeInspectorDebugUrl(localPort, timeoutMs = 10000) {
|
|
138
|
+
const startedAt = Date.now();
|
|
139
|
+
let lastError;
|
|
140
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
141
|
+
try {
|
|
142
|
+
const debugUrl = await getNodeInspectorDebugUrl(localPort);
|
|
143
|
+
if (debugUrl) {
|
|
144
|
+
return debugUrl;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
lastError = error;
|
|
149
|
+
}
|
|
150
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
151
|
+
}
|
|
152
|
+
if (lastError instanceof Error) {
|
|
153
|
+
console.log(chalk.gray(`Could not read inspector JSON yet: ${lastError.message}`));
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
function printNodeInspectorAttachInfo(options) {
|
|
158
|
+
console.log("");
|
|
159
|
+
console.log(chalk.green(`Debug tunnel is ready for ${options.appName} instance ${options.instanceIndex}.`));
|
|
160
|
+
console.log(`Chrome inspect: ${chalk.cyan("chrome://inspect")}`);
|
|
161
|
+
console.log(`Local inspector JSON: ${chalk.cyan(`http://127.0.0.1:${options.localPort}/json/list`)}`);
|
|
162
|
+
if (options.debugUrl) {
|
|
163
|
+
console.log(`Direct DevTools link: ${chalk.cyan(options.debugUrl)}`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
console.log(chalk.yellow("Direct DevTools link was not detected yet. Open chrome://inspect and configure localhost target."));
|
|
167
|
+
}
|
|
168
|
+
console.log("");
|
|
169
|
+
console.log(chalk.gray("VS Code attach config:"));
|
|
170
|
+
console.log(JSON.stringify({
|
|
171
|
+
type: "node",
|
|
172
|
+
request: "attach",
|
|
173
|
+
name: `Attach ${options.appName} on BTP`,
|
|
174
|
+
address: "127.0.0.1",
|
|
175
|
+
port: options.localPort,
|
|
176
|
+
localRoot: "${workspaceFolder}",
|
|
177
|
+
remoteRoot: "/home/vcap/app",
|
|
178
|
+
skipFiles: ["<node_internals>/**"],
|
|
179
|
+
}, null, 2));
|
|
180
|
+
console.log("");
|
|
181
|
+
console.log(chalk.gray("Keep this terminal open. Press Ctrl+C to close the debug tunnel."));
|
|
182
|
+
}
|
|
183
|
+
function buildVscodeNodeAttachConfiguration(options) {
|
|
184
|
+
return {
|
|
185
|
+
type: "node",
|
|
186
|
+
request: "attach",
|
|
187
|
+
name: `Attach BTP ${options.appName}`,
|
|
188
|
+
address: "127.0.0.1",
|
|
189
|
+
port: options.localPort,
|
|
190
|
+
localRoot: "${workspaceFolder}",
|
|
191
|
+
remoteRoot: options.remoteRoot,
|
|
192
|
+
protocol: "inspector",
|
|
193
|
+
sourceMaps: true,
|
|
194
|
+
restart: true,
|
|
195
|
+
skipFiles: ["<node_internals>/**"],
|
|
196
|
+
outFiles: [
|
|
197
|
+
"${workspaceFolder}/**/*.js",
|
|
198
|
+
"!**/node_modules/**",
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async function writeVscodeLaunchConfiguration(options) {
|
|
203
|
+
const vscodeDirectoryPath = path.resolve(options.cwd, ".vscode");
|
|
204
|
+
const launchJsonPath = path.join(vscodeDirectoryPath, "launch.json");
|
|
205
|
+
const configuration = buildVscodeNodeAttachConfiguration({
|
|
206
|
+
appName: options.appName,
|
|
207
|
+
localPort: options.localPort,
|
|
208
|
+
remoteRoot: options.remoteRoot ?? "/home/vcap/app",
|
|
209
|
+
});
|
|
210
|
+
await fs.ensureDir(vscodeDirectoryPath);
|
|
211
|
+
let launchJson = {
|
|
212
|
+
version: "0.2.0",
|
|
213
|
+
configurations: [],
|
|
214
|
+
};
|
|
215
|
+
if (await fs.pathExists(launchJsonPath)) {
|
|
216
|
+
try {
|
|
217
|
+
const currentContent = await fs.readFile(launchJsonPath, "utf8");
|
|
218
|
+
const parsed = JSON.parse(currentContent);
|
|
219
|
+
launchJson = {
|
|
220
|
+
version: typeof parsed.version === "string" ? parsed.version : "0.2.0",
|
|
221
|
+
configurations: Array.isArray(parsed.configurations) ? parsed.configurations : [],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
const backupPath = `${launchJsonPath}.backup-${Date.now()}`;
|
|
226
|
+
await fs.copyFile(launchJsonPath, backupPath);
|
|
227
|
+
console.log(chalk.yellow(`Existing launch.json is not valid JSON. Backup created: ${backupPath}`));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const configurationName = String(configuration.name);
|
|
231
|
+
const existingIndex = launchJson.configurations.findIndex((item) => item.name === configurationName);
|
|
232
|
+
if (existingIndex >= 0) {
|
|
233
|
+
launchJson.configurations[existingIndex] = configuration;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
launchJson.configurations.unshift(configuration);
|
|
237
|
+
}
|
|
238
|
+
await fs.writeFile(launchJsonPath, `${JSON.stringify(launchJson, null, 2)}\n`, "utf8");
|
|
239
|
+
return launchJsonPath;
|
|
240
|
+
}
|
|
241
|
+
function printVscodeAttachInstructions(options) {
|
|
242
|
+
console.log("");
|
|
243
|
+
console.log(chalk.green(`VS Code debug config is ready for ${options.appName} instance ${options.instanceIndex}.`));
|
|
244
|
+
console.log(`Launch file: ${chalk.cyan(options.launchJsonPath)}`);
|
|
245
|
+
console.log(`Attach config: ${chalk.cyan(`Attach BTP ${options.appName}`)}`);
|
|
246
|
+
console.log(`Inspector: ${chalk.cyan(`127.0.0.1:${options.localPort}`)}`);
|
|
247
|
+
console.log("");
|
|
248
|
+
console.log(chalk.gray("Keep this terminal open, then open VS Code Run and Debug and choose the attach config above."));
|
|
249
|
+
console.log(chalk.gray("Press Ctrl+C in this terminal to close the tunnel."));
|
|
250
|
+
}
|
|
251
|
+
async function openVisualStudioCode(options) {
|
|
252
|
+
const result = await runCommand("code", [options.cwd]);
|
|
253
|
+
if (result.exitCode !== 0) {
|
|
254
|
+
console.log(chalk.yellow("Could not open VS Code automatically. Open this folder manually in VS Code."));
|
|
255
|
+
if (result.stderr)
|
|
256
|
+
console.log(chalk.gray(result.stderr));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function selectDebugMode(options) {
|
|
260
|
+
if (options.check)
|
|
261
|
+
return "check-ssh";
|
|
262
|
+
if (options.enableSsh)
|
|
263
|
+
return "enable-ssh";
|
|
264
|
+
if (options.configOnly)
|
|
265
|
+
return "config-only";
|
|
266
|
+
if (options.linkOnly)
|
|
267
|
+
return "link-only";
|
|
268
|
+
if (options.chrome)
|
|
269
|
+
return "chrome";
|
|
270
|
+
if (options.vscode)
|
|
271
|
+
return "vscode";
|
|
272
|
+
return searchableSelectChoice({
|
|
273
|
+
message: "Select debug mode",
|
|
274
|
+
choices: [
|
|
275
|
+
{
|
|
276
|
+
title: "VS Code attach debugger (recommended)",
|
|
277
|
+
value: "vscode",
|
|
278
|
+
description: "Create/update .vscode/launch.json and open a CF SSH tunnel",
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
title: "Chrome DevTools / chrome://inspect",
|
|
282
|
+
value: "chrome",
|
|
283
|
+
description: "Open a CF SSH tunnel and print Chrome inspector links",
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
title: "Create/update VS Code launch.json only",
|
|
287
|
+
value: "config-only",
|
|
288
|
+
description: "Use when the tunnel is already open or you only need config",
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
title: "Print attach links/config only",
|
|
292
|
+
value: "link-only",
|
|
293
|
+
description: "Use when localhost inspector tunnel is already open",
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
title: "Check SSH enabled for app",
|
|
297
|
+
value: "check-ssh",
|
|
298
|
+
description: "Run cf ssh-enabled <app>",
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
title: "Enable SSH and restart app",
|
|
302
|
+
value: "enable-ssh",
|
|
303
|
+
description: "Run cf enable-ssh <app> and cf restart <app>",
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
allowCustomValue: false,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async function maybeSwitchCloudFoundryTargetForDebug(options) {
|
|
310
|
+
if (options.skipOrgSelect || options.app?.trim()) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const target = await ensureCloudFoundrySessionFromCache();
|
|
314
|
+
const currentTargetLabel = [
|
|
315
|
+
target.org ? `org: ${target.org}` : "org: N/A",
|
|
316
|
+
target.space ? `space: ${target.space}` : "space: N/A",
|
|
317
|
+
target.apiEndpoint ? inferCloudFoundryRegionFromApiEndpoint(target.apiEndpoint) : "current region",
|
|
318
|
+
].join(" · ");
|
|
319
|
+
const action = await searchableSelectChoice({
|
|
320
|
+
message: "Select BTP target for debug",
|
|
321
|
+
choices: [
|
|
322
|
+
{ title: `Use current target (${currentTargetLabel})`, value: "current" },
|
|
323
|
+
{ title: "Search org across regions and switch", value: "switch" },
|
|
324
|
+
],
|
|
325
|
+
allowCustomValue: false,
|
|
326
|
+
});
|
|
327
|
+
if (action === "switch") {
|
|
328
|
+
await runOrgCommand({ switch: true });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async function selectDebugInstance(options) {
|
|
332
|
+
if (options.instance?.trim()) {
|
|
333
|
+
return options.instance.trim();
|
|
334
|
+
}
|
|
335
|
+
return searchableSelectChoice({
|
|
336
|
+
message: "Select app instance index",
|
|
337
|
+
choices: [
|
|
338
|
+
{ title: "0", value: "0" },
|
|
339
|
+
{ title: "1", value: "1" },
|
|
340
|
+
{ title: "2", value: "2" },
|
|
341
|
+
{ title: "3", value: "3" },
|
|
342
|
+
],
|
|
343
|
+
validateCustomValue: (value) => /^\d+$/.test(value.trim()) ? true : "Instance index must be a number",
|
|
344
|
+
customValueTitle: (value) => `Use typed instance index: ${value}`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
async function selectDebugPort(options) {
|
|
348
|
+
if (options.value?.trim()) {
|
|
349
|
+
return parsePositivePort(options.value, options.defaultPort);
|
|
350
|
+
}
|
|
351
|
+
const portValue = await searchableSelectChoice({
|
|
352
|
+
message: options.message,
|
|
353
|
+
choices: [
|
|
354
|
+
{ title: `${options.defaultPort} recommended`, value: String(options.defaultPort) },
|
|
355
|
+
{ title: "9230", value: "9230" },
|
|
356
|
+
{ title: "9231", value: "9231" },
|
|
357
|
+
],
|
|
358
|
+
validateCustomValue: (value) => {
|
|
359
|
+
try {
|
|
360
|
+
parsePositivePort(value, options.defaultPort);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
return error instanceof Error ? error.message : "Invalid port";
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
customValueTitle: (value) => `Use typed port: ${value}`,
|
|
368
|
+
});
|
|
369
|
+
return parsePositivePort(portValue, options.defaultPort);
|
|
370
|
+
}
|
|
23
371
|
function shouldIncludeLogLine(line, options) {
|
|
24
372
|
const trimmedInstance = options.instance?.trim();
|
|
25
373
|
const trimmedProcess = options.process?.trim();
|
|
@@ -231,6 +579,7 @@ async function runLoginCommand(options) {
|
|
|
231
579
|
validate: validateRequired,
|
|
232
580
|
});
|
|
233
581
|
let password = options.password ?? lastProfile?.password;
|
|
582
|
+
let shouldSavePassword = options.savePassword ?? false;
|
|
234
583
|
if (!password) {
|
|
235
584
|
const response = await prompts({
|
|
236
585
|
type: "password",
|
|
@@ -267,6 +616,19 @@ async function runLoginCommand(options) {
|
|
|
267
616
|
password = passwordResponse.password;
|
|
268
617
|
}
|
|
269
618
|
}
|
|
619
|
+
if (!shouldSavePassword) {
|
|
620
|
+
const savePasswordResponse = await prompts({
|
|
621
|
+
type: "select",
|
|
622
|
+
name: "savePassword",
|
|
623
|
+
message: "Save password for automatic re-login and region scan?",
|
|
624
|
+
choices: [
|
|
625
|
+
{ title: "Yes, save password on this machine", value: true },
|
|
626
|
+
{ title: "No", value: false },
|
|
627
|
+
],
|
|
628
|
+
initial: 0,
|
|
629
|
+
});
|
|
630
|
+
shouldSavePassword = Boolean(savePasswordResponse.savePassword);
|
|
631
|
+
}
|
|
270
632
|
const apiExitCode = await setCloudFoundryApiEndpoint(apiEndpoint);
|
|
271
633
|
if (apiExitCode !== 0) {
|
|
272
634
|
process.exitCode = apiExitCode;
|
|
@@ -307,11 +669,11 @@ async function runLoginCommand(options) {
|
|
|
307
669
|
username,
|
|
308
670
|
org,
|
|
309
671
|
space,
|
|
310
|
-
password:
|
|
672
|
+
password: shouldSavePassword ? password : undefined,
|
|
311
673
|
updatedAt: new Date().toISOString(),
|
|
312
674
|
});
|
|
313
|
-
if (
|
|
314
|
-
console.log(chalk.yellow("Password was cached in ~/.simplemdg/cache.json. Do not use
|
|
675
|
+
if (shouldSavePassword) {
|
|
676
|
+
console.log(chalk.yellow("Password was cached in ~/.simplemdg/cache.json for automatic re-login. Do not use this on shared machines."));
|
|
315
677
|
}
|
|
316
678
|
console.log(chalk.green("CF login completed."));
|
|
317
679
|
}
|
|
@@ -339,7 +701,12 @@ async function getCloudFoundryOrganizationsAcrossRegions(options) {
|
|
|
339
701
|
}
|
|
340
702
|
const apiEndpoints = getCloudFoundryApiEndpointsForOrgSearch(options, target, cache);
|
|
341
703
|
console.log(chalk.gray(`Searching CF orgs across ${apiEndpoints.length} region endpoint(s)...`));
|
|
342
|
-
const
|
|
704
|
+
const credentials = cache.cloudFoundry.loginProfiles.map((profile) => ({
|
|
705
|
+
apiEndpoint: profile.apiEndpoint,
|
|
706
|
+
username: profile.username,
|
|
707
|
+
password: profile.password,
|
|
708
|
+
}));
|
|
709
|
+
const entries = await scanCloudFoundryOrganizationsAcrossRegions(apiEndpoints, credentials);
|
|
343
710
|
if (entries.length) {
|
|
344
711
|
await rememberCloudFoundryOrgEntries(entries);
|
|
345
712
|
return entries;
|
|
@@ -358,11 +725,7 @@ async function getCloudFoundryOrganizationsAcrossRegions(options) {
|
|
|
358
725
|
return fallbackEntries;
|
|
359
726
|
}
|
|
360
727
|
async function runOrgCommand(options) {
|
|
361
|
-
const target = await
|
|
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
|
-
}
|
|
728
|
+
const target = await ensureCloudFoundrySessionFromCache();
|
|
366
729
|
const action = options.list
|
|
367
730
|
? "list"
|
|
368
731
|
: options.switch
|
|
@@ -412,6 +775,11 @@ async function runOrgCommand(options) {
|
|
|
412
775
|
};
|
|
413
776
|
}
|
|
414
777
|
else {
|
|
778
|
+
if (!organizationEntries.length) {
|
|
779
|
+
console.log(chalk.yellow("No orgs were found across regions."));
|
|
780
|
+
console.log(chalk.gray("Run smdg cf login and save the password, then run smdg cf org --list --refresh."));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
415
783
|
const selectedIndex = await searchableSelectChoice({
|
|
416
784
|
message: "Search CF org across regions",
|
|
417
785
|
choices: organizationEntries.map((entry, index) => ({
|
|
@@ -564,9 +932,7 @@ async function runLogsCommand(options) {
|
|
|
564
932
|
console.log(chalk.gray(`Streaming logs and appending to ${outputPath}`));
|
|
565
933
|
}
|
|
566
934
|
console.log(chalk.gray("Press Ctrl+C to stop realtime logs."));
|
|
567
|
-
const
|
|
568
|
-
const childProcess = spawn(resolvedCommand.command, buildCloudFoundryLogsArgs({ appName, recent: false }), {
|
|
569
|
-
env: resolvedCommand.env,
|
|
935
|
+
const childProcess = spawn("cf", buildCloudFoundryLogsArgs({ appName, recent: false }), {
|
|
570
936
|
stdio: ["ignore", "pipe", "pipe"],
|
|
571
937
|
shell: false,
|
|
572
938
|
windowsHide: true,
|
|
@@ -594,6 +960,169 @@ async function runLogsCommand(options) {
|
|
|
594
960
|
childProcess.on("close", () => resolve());
|
|
595
961
|
});
|
|
596
962
|
}
|
|
963
|
+
async function runDebugCommand(options) {
|
|
964
|
+
await maybeSwitchCloudFoundryTargetForDebug(options);
|
|
965
|
+
const appName = await resolveAppSelection({
|
|
966
|
+
app: options.app,
|
|
967
|
+
refresh: options.refresh,
|
|
968
|
+
message: "Search/select BTP app to debug",
|
|
969
|
+
});
|
|
970
|
+
const debugMode = await selectDebugMode(options);
|
|
971
|
+
const instanceIndex = await selectDebugInstance(options);
|
|
972
|
+
const localPort = await selectDebugPort({
|
|
973
|
+
value: options.localPort,
|
|
974
|
+
message: "Select local debug port",
|
|
975
|
+
defaultPort: 9229,
|
|
976
|
+
});
|
|
977
|
+
const remotePort = parsePositivePort(options.remotePort, 9229);
|
|
978
|
+
const repositoryPath = await resolveRepositoryPath(process.cwd()).catch(() => process.cwd());
|
|
979
|
+
if (debugMode === "check-ssh") {
|
|
980
|
+
const result = await runCommand("cf", ["ssh-enabled", appName]);
|
|
981
|
+
if (result.stdout)
|
|
982
|
+
console.log(result.stdout);
|
|
983
|
+
if (result.stderr)
|
|
984
|
+
console.error(result.stderr);
|
|
985
|
+
process.exitCode = result.exitCode;
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (debugMode === "enable-ssh") {
|
|
989
|
+
const result = await runCommand("cf", ["enable-ssh", appName]);
|
|
990
|
+
if (result.stdout)
|
|
991
|
+
console.log(result.stdout);
|
|
992
|
+
if (result.stderr)
|
|
993
|
+
console.error(result.stderr);
|
|
994
|
+
if (result.exitCode !== 0) {
|
|
995
|
+
process.exitCode = result.exitCode;
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const restartResponse = await prompts({
|
|
999
|
+
type: "select",
|
|
1000
|
+
name: "restart",
|
|
1001
|
+
message: "Restart app now so SSH setting takes effect?",
|
|
1002
|
+
choices: [
|
|
1003
|
+
{ title: "Yes, restart app", value: true },
|
|
1004
|
+
{ title: "No, I will restart later", value: false },
|
|
1005
|
+
],
|
|
1006
|
+
initial: 0,
|
|
1007
|
+
});
|
|
1008
|
+
if (restartResponse.restart) {
|
|
1009
|
+
const restartExitCode = await runCommandInherit("cf", ["restart", appName]);
|
|
1010
|
+
process.exitCode = restartExitCode;
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
console.log(chalk.yellow(`SSH was enabled. Restart the app before debugging: cf restart ${appName}`));
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
let launchJsonPath;
|
|
1017
|
+
if (debugMode === "vscode" || debugMode === "config-only") {
|
|
1018
|
+
launchJsonPath = await writeVscodeLaunchConfiguration({
|
|
1019
|
+
cwd: repositoryPath,
|
|
1020
|
+
appName,
|
|
1021
|
+
localPort,
|
|
1022
|
+
remoteRoot: "/home/vcap/app",
|
|
1023
|
+
});
|
|
1024
|
+
console.log(chalk.green(`Updated VS Code launch config: ${launchJsonPath}`));
|
|
1025
|
+
const openResponse = await prompts({
|
|
1026
|
+
type: "select",
|
|
1027
|
+
name: "open",
|
|
1028
|
+
message: "Open current folder in VS Code?",
|
|
1029
|
+
choices: [
|
|
1030
|
+
{ title: "No", value: false },
|
|
1031
|
+
{ title: "Yes", value: true },
|
|
1032
|
+
],
|
|
1033
|
+
initial: options.open ? 1 : 0,
|
|
1034
|
+
});
|
|
1035
|
+
if (openResponse.open) {
|
|
1036
|
+
await openVisualStudioCode({ cwd: repositoryPath });
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (debugMode === "config-only") {
|
|
1040
|
+
printVscodeAttachInstructions({
|
|
1041
|
+
appName,
|
|
1042
|
+
instanceIndex,
|
|
1043
|
+
localPort,
|
|
1044
|
+
launchJsonPath: launchJsonPath ?? path.resolve(repositoryPath, ".vscode", "launch.json"),
|
|
1045
|
+
});
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (debugMode === "link-only") {
|
|
1049
|
+
const debugUrl = await waitForNodeInspectorDebugUrl(localPort, 2000);
|
|
1050
|
+
printNodeInspectorAttachInfo({ appName, instanceIndex, localPort, debugUrl });
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (options.enableSsh) {
|
|
1054
|
+
const result = await runCommand("cf", ["enable-ssh", appName]);
|
|
1055
|
+
if (result.stdout)
|
|
1056
|
+
console.log(result.stdout);
|
|
1057
|
+
if (result.stderr)
|
|
1058
|
+
console.error(result.stderr);
|
|
1059
|
+
if (result.exitCode !== 0) {
|
|
1060
|
+
process.exitCode = result.exitCode;
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (options.restart) {
|
|
1064
|
+
const restartExitCode = await runCommandInherit("cf", ["restart", appName]);
|
|
1065
|
+
if (restartExitCode !== 0) {
|
|
1066
|
+
process.exitCode = restartExitCode;
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
console.log(chalk.yellow("SSH was enabled. If cf ssh still fails, restart the app or run: cf restart " + appName));
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
console.log(chalk.gray(`Starting Node.js inspector tunnel for ${appName} instance ${instanceIndex}...`));
|
|
1075
|
+
console.log(chalk.gray(`Forwarding localhost:${localPort} -> app container 127.0.0.1:${remotePort}`));
|
|
1076
|
+
const childProcess = spawn("cf", buildCloudFoundryDebugSshArgs({
|
|
1077
|
+
appName,
|
|
1078
|
+
instanceIndex,
|
|
1079
|
+
processName: options.process,
|
|
1080
|
+
localPort,
|
|
1081
|
+
remotePort,
|
|
1082
|
+
}), {
|
|
1083
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1084
|
+
shell: false,
|
|
1085
|
+
windowsHide: true,
|
|
1086
|
+
});
|
|
1087
|
+
let hasPrintedAttachInfo = false;
|
|
1088
|
+
const printAttachInfoOnce = async () => {
|
|
1089
|
+
if (hasPrintedAttachInfo) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
hasPrintedAttachInfo = true;
|
|
1093
|
+
if (debugMode === "vscode") {
|
|
1094
|
+
printVscodeAttachInstructions({
|
|
1095
|
+
appName,
|
|
1096
|
+
instanceIndex,
|
|
1097
|
+
localPort,
|
|
1098
|
+
launchJsonPath: launchJsonPath ?? path.resolve(repositoryPath, ".vscode", "launch.json"),
|
|
1099
|
+
});
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
const debugUrl = await waitForNodeInspectorDebugUrl(localPort);
|
|
1103
|
+
printNodeInspectorAttachInfo({ appName, instanceIndex, localPort, debugUrl });
|
|
1104
|
+
};
|
|
1105
|
+
childProcess.stdout?.on("data", (chunk) => {
|
|
1106
|
+
const text = chunk.toString("utf8");
|
|
1107
|
+
process.stdout.write(text);
|
|
1108
|
+
if (/inspector|debug|listening|started/i.test(text)) {
|
|
1109
|
+
void printAttachInfoOnce();
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
childProcess.stderr?.on("data", (chunk) => {
|
|
1113
|
+
process.stderr.write(chunk.toString("utf8"));
|
|
1114
|
+
});
|
|
1115
|
+
const fallbackTimer = setTimeout(() => {
|
|
1116
|
+
void printAttachInfoOnce();
|
|
1117
|
+
}, 3000);
|
|
1118
|
+
childProcess.on("close", (exitCode) => {
|
|
1119
|
+
clearTimeout(fallbackTimer);
|
|
1120
|
+
process.exitCode = exitCode ?? 0;
|
|
1121
|
+
});
|
|
1122
|
+
await new Promise((resolve) => {
|
|
1123
|
+
childProcess.on("close", () => resolve());
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
597
1126
|
async function runTargetCommand() {
|
|
598
1127
|
const target = await readCloudFoundryTarget();
|
|
599
1128
|
printTarget(target);
|
|
@@ -658,6 +1187,25 @@ export function registerCloudFoundryCommands(program) {
|
|
|
658
1187
|
.option("--instance <index>", "Filter logs by app instance index, for example 0 or 1")
|
|
659
1188
|
.option("--process <processName>", "Filter logs by process name, for example WEB")
|
|
660
1189
|
.action(runLogsCommand);
|
|
1190
|
+
cfCommand
|
|
1191
|
+
.command("debug")
|
|
1192
|
+
.description("Debug a deployed BTP Cloud Foundry Node.js app with selectable VS Code or Chrome mode")
|
|
1193
|
+
.option("--app <appName>", "BTP app name")
|
|
1194
|
+
.option("--refresh", "Refresh app list before selecting")
|
|
1195
|
+
.option("--instance <index>", "App instance index", "0")
|
|
1196
|
+
.option("--process <processName>", "CF process name for multi-process apps")
|
|
1197
|
+
.option("--local-port <port>", "Local inspector port", "9229")
|
|
1198
|
+
.option("--remote-port <port>", "Remote inspector port in app container", "9229")
|
|
1199
|
+
.option("--enable-ssh", "Run cf enable-ssh <app> before opening the debug tunnel")
|
|
1200
|
+
.option("--restart", "Restart app after --enable-ssh")
|
|
1201
|
+
.option("--check", "Run cf ssh-enabled <app> and exit")
|
|
1202
|
+
.option("--link-only", "Only print attach links/config for an already-open tunnel")
|
|
1203
|
+
.option("--vscode", "Use VS Code attach debug mode")
|
|
1204
|
+
.option("--chrome", "Use Chrome DevTools debug mode")
|
|
1205
|
+
.option("--config-only", "Only create/update .vscode/launch.json")
|
|
1206
|
+
.option("--open", "Open current folder in VS Code after creating launch.json")
|
|
1207
|
+
.option("--skip-org-select", "Use current CF org/space without asking")
|
|
1208
|
+
.action(runDebugCommand);
|
|
661
1209
|
cfCommand
|
|
662
1210
|
.command("apps-cache-refresh")
|
|
663
1211
|
.description("Refresh cached cf apps for current target. Internal command used by smdg cf apps.")
|