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
|
@@ -1,28 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerCloudFoundryCommands = registerCloudFoundryCommands;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
10
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
13
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
14
|
+
const cf_db_command_1 = require("./cf-db.command");
|
|
15
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
16
|
+
const cf_1 = require("../core/cf");
|
|
17
|
+
const cf_env_parser_1 = require("../core/cf-env-parser");
|
|
18
|
+
const cache_1 = require("../core/cache");
|
|
19
|
+
const process_1 = require("../core/process");
|
|
20
|
+
const repository_1 = require("../core/repository");
|
|
21
|
+
const prompts_2 = require("../core/prompts");
|
|
22
|
+
const tooling_1 = require("../core/tooling");
|
|
13
23
|
function validateRequired(value) {
|
|
14
24
|
return value.trim() ? true : "Value is required";
|
|
15
25
|
}
|
|
16
26
|
async function ensureCloudFoundrySessionFromCache() {
|
|
17
|
-
|
|
27
|
+
await (0, tooling_1.ensureExternalTool)("cf");
|
|
28
|
+
const target = await (0, cf_1.readCloudFoundryTarget)();
|
|
18
29
|
if (target.apiEndpoint && target.user) {
|
|
19
30
|
return target;
|
|
20
31
|
}
|
|
21
|
-
const cache = await readCache();
|
|
32
|
+
const cache = await (0, cache_1.readCache)();
|
|
22
33
|
const profilesWithPassword = cache.cloudFoundry.loginProfiles.filter((profile) => profile.password?.trim());
|
|
23
34
|
if (!profilesWithPassword.length) {
|
|
24
|
-
console.log(
|
|
25
|
-
console.log(
|
|
35
|
+
console.log(chalk_1.default.yellow("You are not logged in to Cloud Foundry yet and no cached password was found."));
|
|
36
|
+
console.log(chalk_1.default.gray("Run smdg cf login once and choose to save the password for automatic re-login."));
|
|
26
37
|
throw new Error("Cloud Foundry login is required");
|
|
27
38
|
}
|
|
28
39
|
const preferredProfiles = target.apiEndpoint
|
|
@@ -33,22 +44,22 @@ async function ensureCloudFoundrySessionFromCache() {
|
|
|
33
44
|
: profilesWithPassword;
|
|
34
45
|
const selectedProfileIndex = preferredProfiles.length === 1
|
|
35
46
|
? "0"
|
|
36
|
-
: await searchableSelectChoice({
|
|
47
|
+
: await (0, prompts_2.searchableSelectChoice)({
|
|
37
48
|
message: "Select cached CF login profile for automatic re-login",
|
|
38
49
|
choices: preferredProfiles.map((profile, index) => ({
|
|
39
|
-
title: `${profile.username} · ${profile.org}${profile.space ? `/${profile.space}` : ""} · ${inferCloudFoundryRegionFromApiEndpoint(profile.apiEndpoint)}`,
|
|
50
|
+
title: `${profile.username} · ${profile.org}${profile.space ? `/${profile.space}` : ""} · ${(0, cf_1.inferCloudFoundryRegionFromApiEndpoint)(profile.apiEndpoint)}`,
|
|
40
51
|
value: String(index),
|
|
41
52
|
})),
|
|
42
53
|
allowCustomValue: false,
|
|
43
54
|
});
|
|
44
55
|
const profile = preferredProfiles[Number(selectedProfileIndex)] ?? preferredProfiles[0];
|
|
45
|
-
console.log(
|
|
46
|
-
const apiExitCode = await setCloudFoundryApiEndpoint(profile.apiEndpoint);
|
|
56
|
+
console.log(chalk_1.default.gray(`Auto login CF: ${profile.username} · ${(0, cf_1.inferCloudFoundryRegionFromApiEndpoint)(profile.apiEndpoint)} · ${profile.org}${profile.space ? `/${profile.space}` : ""}`));
|
|
57
|
+
const apiExitCode = await (0, cf_1.setCloudFoundryApiEndpoint)(profile.apiEndpoint);
|
|
47
58
|
if (apiExitCode !== 0) {
|
|
48
59
|
process.exitCode = apiExitCode;
|
|
49
60
|
throw new Error("CF api target failed");
|
|
50
61
|
}
|
|
51
|
-
const authExitCode = await authenticateCloudFoundry({
|
|
62
|
+
const authExitCode = await (0, cf_1.authenticateCloudFoundry)({
|
|
52
63
|
username: profile.username,
|
|
53
64
|
password: profile.password,
|
|
54
65
|
});
|
|
@@ -56,23 +67,88 @@ async function ensureCloudFoundrySessionFromCache() {
|
|
|
56
67
|
process.exitCode = authExitCode;
|
|
57
68
|
throw new Error("CF automatic login failed. Run smdg cf login and update the cached password.");
|
|
58
69
|
}
|
|
59
|
-
const orgExitCode = await targetCloudFoundryOrg(profile.org);
|
|
70
|
+
const orgExitCode = await (0, cf_1.targetCloudFoundryOrg)(profile.org);
|
|
60
71
|
if (orgExitCode !== 0) {
|
|
61
72
|
process.exitCode = orgExitCode;
|
|
62
73
|
throw new Error("CF org target failed");
|
|
63
74
|
}
|
|
64
75
|
if (profile.space) {
|
|
65
|
-
const spaceExitCode = await targetCloudFoundrySpace(profile.space);
|
|
76
|
+
const spaceExitCode = await (0, cf_1.targetCloudFoundrySpace)(profile.space);
|
|
66
77
|
if (spaceExitCode !== 0) {
|
|
67
78
|
process.exitCode = spaceExitCode;
|
|
68
79
|
throw new Error("CF space target failed");
|
|
69
80
|
}
|
|
70
81
|
}
|
|
71
|
-
await rememberCloudFoundryLoginProfile({
|
|
82
|
+
await (0, cache_1.rememberCloudFoundryLoginProfile)({
|
|
72
83
|
...profile,
|
|
73
84
|
updatedAt: new Date().toISOString(),
|
|
74
85
|
});
|
|
75
|
-
return readCloudFoundryTarget();
|
|
86
|
+
return (0, cf_1.readCloudFoundryTarget)();
|
|
87
|
+
}
|
|
88
|
+
function sortCloudFoundryProfilesForEndpoint(options) {
|
|
89
|
+
const profilesWithPassword = options.profiles.filter((profile) => profile.password?.trim());
|
|
90
|
+
return [
|
|
91
|
+
...profilesWithPassword.filter((profile) => profile.apiEndpoint === options.apiEndpoint && profile.org === options.preferredOrg),
|
|
92
|
+
...profilesWithPassword.filter((profile) => profile.apiEndpoint === options.apiEndpoint && profile.org !== options.preferredOrg),
|
|
93
|
+
...profilesWithPassword.filter((profile) => profile.apiEndpoint !== options.apiEndpoint && profile.org === options.preferredOrg),
|
|
94
|
+
...profilesWithPassword.filter((profile) => profile.apiEndpoint !== options.apiEndpoint && profile.org !== options.preferredOrg),
|
|
95
|
+
].filter((profile, index, array) => {
|
|
96
|
+
return array.findIndex((item) => {
|
|
97
|
+
return item.apiEndpoint === profile.apiEndpoint
|
|
98
|
+
&& item.username === profile.username
|
|
99
|
+
&& item.password === profile.password
|
|
100
|
+
&& item.org === profile.org
|
|
101
|
+
&& item.space === profile.space;
|
|
102
|
+
}) === index;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async function ensureCloudFoundryAuthenticatedForApiEndpoint(options) {
|
|
106
|
+
const apiExitCode = await (0, cf_1.setCloudFoundryApiEndpoint)(options.apiEndpoint);
|
|
107
|
+
if (apiExitCode !== 0) {
|
|
108
|
+
process.exitCode = apiExitCode;
|
|
109
|
+
throw new Error(`Cannot set CF API endpoint: ${options.apiEndpoint}`);
|
|
110
|
+
}
|
|
111
|
+
const orgsCheck = await (0, process_1.runCommand)("cf", ["orgs"]);
|
|
112
|
+
if (orgsCheck.exitCode === 0) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const cache = await (0, cache_1.readCache)();
|
|
116
|
+
const profiles = sortCloudFoundryProfilesForEndpoint({
|
|
117
|
+
profiles: cache.cloudFoundry.loginProfiles,
|
|
118
|
+
apiEndpoint: options.apiEndpoint,
|
|
119
|
+
preferredOrg: options.preferredOrg,
|
|
120
|
+
});
|
|
121
|
+
if (!profiles.length) {
|
|
122
|
+
console.log(chalk_1.default.yellow(`Not logged in to ${(0, cf_1.inferCloudFoundryRegionFromApiEndpoint)(options.apiEndpoint)} and no cached password was found for automatic login.`));
|
|
123
|
+
console.log(chalk_1.default.gray("Run smdg cf login once, choose to save password, then retry this command."));
|
|
124
|
+
throw new Error("Cloud Foundry automatic login is required");
|
|
125
|
+
}
|
|
126
|
+
let lastError = orgsCheck.stderr || orgsCheck.stdout || "cf orgs failed";
|
|
127
|
+
for (const profile of profiles) {
|
|
128
|
+
console.log(chalk_1.default.gray(`Auto auth CF ${(0, cf_1.inferCloudFoundryRegionFromApiEndpoint)(options.apiEndpoint)} as ${profile.username}...`));
|
|
129
|
+
const authExitCode = await (0, cf_1.authenticateCloudFoundry)({
|
|
130
|
+
username: profile.username,
|
|
131
|
+
password: profile.password,
|
|
132
|
+
});
|
|
133
|
+
if (authExitCode !== 0) {
|
|
134
|
+
lastError = `cf auth failed for ${profile.username}`;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const nextOrgsCheck = await (0, process_1.runCommand)("cf", ["orgs"]);
|
|
138
|
+
if (nextOrgsCheck.exitCode === 0) {
|
|
139
|
+
const updatedProfile = {
|
|
140
|
+
...profile,
|
|
141
|
+
apiEndpoint: options.apiEndpoint,
|
|
142
|
+
org: options.preferredOrg || profile.org,
|
|
143
|
+
space: options.preferredSpace || profile.space,
|
|
144
|
+
updatedAt: new Date().toISOString(),
|
|
145
|
+
};
|
|
146
|
+
await (0, cache_1.rememberCloudFoundryLoginProfile)(updatedProfile);
|
|
147
|
+
return updatedProfile;
|
|
148
|
+
}
|
|
149
|
+
lastError = nextOrgsCheck.stderr || nextOrgsCheck.stdout || lastError;
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`CF automatic login failed for ${options.apiEndpoint}. ${lastError}`);
|
|
76
152
|
}
|
|
77
153
|
function buildCloudFoundryLogsArgs(options) {
|
|
78
154
|
const args = ["logs", options.appName];
|
|
@@ -100,13 +176,20 @@ function buildNodeInspectorRemoteCommand(remotePort) {
|
|
|
100
176
|
`exit 2`,
|
|
101
177
|
].join("; ");
|
|
102
178
|
}
|
|
103
|
-
|
|
104
|
-
`PID
|
|
105
|
-
`if
|
|
106
|
-
`if
|
|
107
|
-
`
|
|
179
|
+
const detectNodePidScript = [
|
|
180
|
+
`PID=""`,
|
|
181
|
+
`if command -v pgrep >/dev/null 2>&1; then PID=$(pgrep -f "(^|/| )node( |$)" 2>/dev/null | head -n 1 || true); fi`,
|
|
182
|
+
`if [ -z "$PID" ]; then PID=$(ps -eo pid=,args= 2>/dev/null | awk '/[n]ode/ && $0 !~ /awk/ && $0 !~ /pgrep/ {print $1; exit}'); fi`,
|
|
183
|
+
`if [ -z "$PID" ]; then echo "No Node.js PID found in app container. Use prepare mode: Set NODE_OPTIONS and restart app." >&2; ps -eo pid,args 2>/dev/null | head -n 40 >&2; exit 1; fi`,
|
|
184
|
+
`echo "Detected Node.js PID: $PID"`,
|
|
185
|
+
`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"; tail -f /dev/null; fi`,
|
|
186
|
+
`if command -v netstat >/dev/null 2>&1 && netstat -ntl 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)9229$'; then echo "Node inspector already listening on 127.0.0.1:9229"; tail -f /dev/null; fi`,
|
|
187
|
+
`kill -USR1 "$PID" || { echo "Cannot send SIGUSR1 to Node.js PID $PID. Use prepare mode: Set NODE_OPTIONS and restart app." >&2; exit 1; }`,
|
|
188
|
+
`echo "Requested Node inspector for PID $PID on 127.0.0.1:9229"`,
|
|
189
|
+
`COUNT=0; while [ "$COUNT" -lt 20 ]; do if command -v ss >/dev/null 2>&1 && ss -H -ntl "sport = :9229" | grep -q .; then echo "Node inspector is listening on 127.0.0.1:9229"; break; fi; if command -v netstat >/dev/null 2>&1 && netstat -ntl 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)9229$'; then echo "Node inspector is listening on 127.0.0.1:9229"; break; fi; COUNT=$((COUNT + 1)); sleep 1; done`,
|
|
108
190
|
`tail -f /dev/null`,
|
|
109
|
-
]
|
|
191
|
+
];
|
|
192
|
+
return detectNodePidScript.join("; ");
|
|
110
193
|
}
|
|
111
194
|
function buildKeepAliveRemoteCommand() {
|
|
112
195
|
return [
|
|
@@ -131,7 +214,7 @@ function buildCloudFoundryDebugSshArgs(options) {
|
|
|
131
214
|
return args;
|
|
132
215
|
}
|
|
133
216
|
async function selectNodeInspectorPrepareMode(options) {
|
|
134
|
-
return searchableSelectChoice({
|
|
217
|
+
return (0, prompts_2.searchableSelectChoice)({
|
|
135
218
|
message: "Prepare Node.js inspector",
|
|
136
219
|
choices: [
|
|
137
220
|
{
|
|
@@ -154,14 +237,14 @@ async function selectNodeInspectorPrepareMode(options) {
|
|
|
154
237
|
});
|
|
155
238
|
}
|
|
156
239
|
async function ensureSshEnabledForDebug(appName) {
|
|
157
|
-
const sshEnabledResult = await runCommand("cf", ["ssh-enabled", appName]);
|
|
240
|
+
const sshEnabledResult = await (0, process_1.runCommand)("cf", ["ssh-enabled", appName]);
|
|
158
241
|
const combinedOutput = `${sshEnabledResult.stdout}
|
|
159
242
|
${sshEnabledResult.stderr}`;
|
|
160
243
|
if (sshEnabledResult.exitCode === 0 && /enabled/i.test(combinedOutput) && !/not enabled/i.test(combinedOutput)) {
|
|
161
244
|
return;
|
|
162
245
|
}
|
|
163
|
-
console.log(
|
|
164
|
-
const enableResult = await runCommand("cf", ["enable-ssh", appName]);
|
|
246
|
+
console.log(chalk_1.default.yellow("SSH is not enabled for this app. Enabling SSH..."));
|
|
247
|
+
const enableResult = await (0, process_1.runCommand)("cf", ["enable-ssh", appName]);
|
|
165
248
|
if (enableResult.stdout)
|
|
166
249
|
console.log(enableResult.stdout);
|
|
167
250
|
if (enableResult.stderr)
|
|
@@ -172,8 +255,8 @@ ${sshEnabledResult.stderr}`;
|
|
|
172
255
|
}
|
|
173
256
|
async function setNodeInspectorEnvironmentAndRestart(options) {
|
|
174
257
|
const nodeOptions = `--inspect=0.0.0.0:${options.remotePort} --enable-source-maps`;
|
|
175
|
-
console.log(
|
|
176
|
-
const setEnvResult = await runCommand("cf", ["set-env", options.appName, "NODE_OPTIONS", nodeOptions]);
|
|
258
|
+
console.log(chalk_1.default.gray(`Setting NODE_OPTIONS for ${options.appName}: ${nodeOptions}`));
|
|
259
|
+
const setEnvResult = await (0, process_1.runCommand)("cf", ["set-env", options.appName, "NODE_OPTIONS", nodeOptions]);
|
|
177
260
|
if (setEnvResult.stdout)
|
|
178
261
|
console.log(setEnvResult.stdout);
|
|
179
262
|
if (setEnvResult.stderr)
|
|
@@ -181,8 +264,8 @@ async function setNodeInspectorEnvironmentAndRestart(options) {
|
|
|
181
264
|
if (setEnvResult.exitCode !== 0) {
|
|
182
265
|
throw new Error("cf set-env NODE_OPTIONS failed");
|
|
183
266
|
}
|
|
184
|
-
console.log(
|
|
185
|
-
const restartExitCode = await runCommandInherit("cf", ["restart", options.appName]);
|
|
267
|
+
console.log(chalk_1.default.yellow("Restarting app so NODE_OPTIONS takes effect..."));
|
|
268
|
+
const restartExitCode = await (0, process_1.runCommandInherit)("cf", ["restart", options.appName]);
|
|
186
269
|
if (restartExitCode !== 0) {
|
|
187
270
|
throw new Error(`cf restart ${options.appName} failed`);
|
|
188
271
|
}
|
|
@@ -216,23 +299,23 @@ async function waitForNodeInspectorDebugUrl(localPort, timeoutMs = 10000) {
|
|
|
216
299
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
217
300
|
}
|
|
218
301
|
if (lastError instanceof Error) {
|
|
219
|
-
console.log(
|
|
302
|
+
console.log(chalk_1.default.gray(`Could not read inspector JSON yet: ${lastError.message}`));
|
|
220
303
|
}
|
|
221
304
|
return undefined;
|
|
222
305
|
}
|
|
223
306
|
function printNodeInspectorAttachInfo(options) {
|
|
224
307
|
console.log("");
|
|
225
|
-
console.log(
|
|
226
|
-
console.log(`Chrome inspect: ${
|
|
227
|
-
console.log(`Local inspector JSON: ${
|
|
308
|
+
console.log(chalk_1.default.green(`Debug tunnel is ready for ${options.appName} instance ${options.instanceIndex}.`));
|
|
309
|
+
console.log(`Chrome inspect: ${chalk_1.default.cyan("chrome://inspect")}`);
|
|
310
|
+
console.log(`Local inspector JSON: ${chalk_1.default.cyan(`http://127.0.0.1:${options.localPort}/json/list`)}`);
|
|
228
311
|
if (options.debugUrl) {
|
|
229
|
-
console.log(`Direct DevTools link: ${
|
|
312
|
+
console.log(`Direct DevTools link: ${chalk_1.default.cyan(options.debugUrl)}`);
|
|
230
313
|
}
|
|
231
314
|
else {
|
|
232
|
-
console.log(
|
|
315
|
+
console.log(chalk_1.default.yellow("Direct DevTools link was not detected yet. Open chrome://inspect and configure localhost target."));
|
|
233
316
|
}
|
|
234
317
|
console.log("");
|
|
235
|
-
console.log(
|
|
318
|
+
console.log(chalk_1.default.gray("VS Code attach config:"));
|
|
236
319
|
console.log(JSON.stringify({
|
|
237
320
|
type: "node",
|
|
238
321
|
request: "attach",
|
|
@@ -244,7 +327,7 @@ function printNodeInspectorAttachInfo(options) {
|
|
|
244
327
|
skipFiles: ["<node_internals>/**"],
|
|
245
328
|
}, null, 2));
|
|
246
329
|
console.log("");
|
|
247
|
-
console.log(
|
|
330
|
+
console.log(chalk_1.default.gray("Keep this terminal open. Press Ctrl+C to close the debug tunnel."));
|
|
248
331
|
}
|
|
249
332
|
function buildVscodeNodeAttachConfiguration(options) {
|
|
250
333
|
return {
|
|
@@ -266,21 +349,21 @@ function buildVscodeNodeAttachConfiguration(options) {
|
|
|
266
349
|
};
|
|
267
350
|
}
|
|
268
351
|
async function writeVscodeLaunchConfiguration(options) {
|
|
269
|
-
const vscodeDirectoryPath =
|
|
270
|
-
const launchJsonPath =
|
|
352
|
+
const vscodeDirectoryPath = node_path_1.default.resolve(options.cwd, ".vscode");
|
|
353
|
+
const launchJsonPath = node_path_1.default.join(vscodeDirectoryPath, "launch.json");
|
|
271
354
|
const configuration = buildVscodeNodeAttachConfiguration({
|
|
272
355
|
appName: options.appName,
|
|
273
356
|
localPort: options.localPort,
|
|
274
357
|
remoteRoot: options.remoteRoot ?? "/home/vcap/app",
|
|
275
358
|
});
|
|
276
|
-
await
|
|
359
|
+
await fs_extra_1.default.ensureDir(vscodeDirectoryPath);
|
|
277
360
|
let launchJson = {
|
|
278
361
|
version: "0.2.0",
|
|
279
362
|
configurations: [],
|
|
280
363
|
};
|
|
281
|
-
if (await
|
|
364
|
+
if (await fs_extra_1.default.pathExists(launchJsonPath)) {
|
|
282
365
|
try {
|
|
283
|
-
const currentContent = await
|
|
366
|
+
const currentContent = await fs_extra_1.default.readFile(launchJsonPath, "utf8");
|
|
284
367
|
const parsed = JSON.parse(currentContent);
|
|
285
368
|
launchJson = {
|
|
286
369
|
version: typeof parsed.version === "string" ? parsed.version : "0.2.0",
|
|
@@ -289,8 +372,8 @@ async function writeVscodeLaunchConfiguration(options) {
|
|
|
289
372
|
}
|
|
290
373
|
catch {
|
|
291
374
|
const backupPath = `${launchJsonPath}.backup-${Date.now()}`;
|
|
292
|
-
await
|
|
293
|
-
console.log(
|
|
375
|
+
await fs_extra_1.default.copyFile(launchJsonPath, backupPath);
|
|
376
|
+
console.log(chalk_1.default.yellow(`Existing launch.json is not valid JSON. Backup created: ${backupPath}`));
|
|
294
377
|
}
|
|
295
378
|
}
|
|
296
379
|
const configurationName = String(configuration.name);
|
|
@@ -301,41 +384,41 @@ async function writeVscodeLaunchConfiguration(options) {
|
|
|
301
384
|
else {
|
|
302
385
|
launchJson.configurations.unshift(configuration);
|
|
303
386
|
}
|
|
304
|
-
await
|
|
387
|
+
await fs_extra_1.default.writeFile(launchJsonPath, `${JSON.stringify(launchJson, null, 2)}\n`, "utf8");
|
|
305
388
|
return launchJsonPath;
|
|
306
389
|
}
|
|
307
390
|
function printVscodeAttachInstructions(options) {
|
|
308
391
|
console.log("");
|
|
309
392
|
if (options.inspectorReady === false) {
|
|
310
|
-
console.log(
|
|
311
|
-
console.log(
|
|
393
|
+
console.log(chalk_1.default.yellow(`VS Code config was created, but the Node inspector is not reachable yet for ${options.appName} instance ${options.instanceIndex}.`));
|
|
394
|
+
console.log(chalk_1.default.yellow("The VS Code debug toolbar appears only after the inspector is reachable and you start the attach config."));
|
|
312
395
|
}
|
|
313
396
|
else {
|
|
314
|
-
console.log(
|
|
397
|
+
console.log(chalk_1.default.green(`VS Code debug config is ready for ${options.appName} instance ${options.instanceIndex}.`));
|
|
315
398
|
}
|
|
316
|
-
console.log(`Launch file: ${
|
|
317
|
-
console.log(`Attach config: ${
|
|
318
|
-
console.log(`Inspector: ${
|
|
399
|
+
console.log(`Launch file: ${chalk_1.default.cyan(options.launchJsonPath)}`);
|
|
400
|
+
console.log(`Attach config: ${chalk_1.default.cyan(`Attach BTP ${options.appName}`)}`);
|
|
401
|
+
console.log(`Inspector: ${chalk_1.default.cyan(`127.0.0.1:${options.localPort}`)}`);
|
|
319
402
|
console.log("");
|
|
320
|
-
console.log(
|
|
403
|
+
console.log(chalk_1.default.cyan("How to start debugging in VS Code:"));
|
|
321
404
|
console.log("1. Keep this terminal open. It owns the CF SSH tunnel.");
|
|
322
405
|
console.log("2. Open VS Code Run and Debug panel with Ctrl+Shift+D.");
|
|
323
|
-
console.log(`3. Select ${
|
|
406
|
+
console.log(`3. Select ${chalk_1.default.cyan(`Attach BTP ${options.appName}`)}.`);
|
|
324
407
|
console.log("4. Press F5 or the green Start Debugging button.");
|
|
325
408
|
console.log("5. Debug buttons such as pause, step over, step into, restart, and stop appear only after attach succeeds.");
|
|
326
409
|
console.log("");
|
|
327
|
-
console.log(
|
|
410
|
+
console.log(chalk_1.default.gray("Press Ctrl+C in this terminal to close the tunnel."));
|
|
328
411
|
}
|
|
329
412
|
async function openVisualStudioCode(options) {
|
|
330
413
|
const args = options.debugPanel
|
|
331
414
|
? ["--reuse-window", options.cwd, "--command", "workbench.view.debug"]
|
|
332
415
|
: [options.cwd];
|
|
333
|
-
const result = await runCommand("code", args);
|
|
416
|
+
const result = await (0, process_1.runCommand)("code", args);
|
|
334
417
|
if (result.exitCode !== 0) {
|
|
335
|
-
console.log(
|
|
336
|
-
console.log(
|
|
418
|
+
console.log(chalk_1.default.yellow("Could not open VS Code automatically. Open this folder manually in VS Code."));
|
|
419
|
+
console.log(chalk_1.default.gray("Then open Run and Debug with Ctrl+Shift+D."));
|
|
337
420
|
if (result.stderr)
|
|
338
|
-
console.log(
|
|
421
|
+
console.log(chalk_1.default.gray(result.stderr));
|
|
339
422
|
}
|
|
340
423
|
}
|
|
341
424
|
async function selectDebugMode(options) {
|
|
@@ -351,7 +434,7 @@ async function selectDebugMode(options) {
|
|
|
351
434
|
return "chrome";
|
|
352
435
|
if (options.vscode)
|
|
353
436
|
return "vscode";
|
|
354
|
-
return searchableSelectChoice({
|
|
437
|
+
return (0, prompts_2.searchableSelectChoice)({
|
|
355
438
|
message: "Select debug mode",
|
|
356
439
|
choices: [
|
|
357
440
|
{
|
|
@@ -396,9 +479,9 @@ async function maybeSwitchCloudFoundryTargetForDebug(options) {
|
|
|
396
479
|
const currentTargetLabel = [
|
|
397
480
|
target.org ? `org: ${target.org}` : "org: N/A",
|
|
398
481
|
target.space ? `space: ${target.space}` : "space: N/A",
|
|
399
|
-
target.apiEndpoint ? inferCloudFoundryRegionFromApiEndpoint(target.apiEndpoint) : "current region",
|
|
482
|
+
target.apiEndpoint ? (0, cf_1.inferCloudFoundryRegionFromApiEndpoint)(target.apiEndpoint) : "current region",
|
|
400
483
|
].join(" · ");
|
|
401
|
-
const action = await searchableSelectChoice({
|
|
484
|
+
const action = await (0, prompts_2.searchableSelectChoice)({
|
|
402
485
|
message: "Select BTP target for debug",
|
|
403
486
|
choices: [
|
|
404
487
|
{ title: `Use current target (${currentTargetLabel})`, value: "current" },
|
|
@@ -414,7 +497,7 @@ async function selectDebugInstance(options) {
|
|
|
414
497
|
if (options.instance?.trim()) {
|
|
415
498
|
return options.instance.trim();
|
|
416
499
|
}
|
|
417
|
-
return searchableSelectChoice({
|
|
500
|
+
return (0, prompts_2.searchableSelectChoice)({
|
|
418
501
|
message: "Select app instance index",
|
|
419
502
|
choices: [
|
|
420
503
|
{ title: "0", value: "0" },
|
|
@@ -430,7 +513,7 @@ async function selectDebugPort(options) {
|
|
|
430
513
|
if (options.value?.trim()) {
|
|
431
514
|
return parsePositivePort(options.value, options.defaultPort);
|
|
432
515
|
}
|
|
433
|
-
const portValue = await searchableSelectChoice({
|
|
516
|
+
const portValue = await (0, prompts_2.searchableSelectChoice)({
|
|
434
517
|
message: options.message,
|
|
435
518
|
choices: [
|
|
436
519
|
{ title: `${options.defaultPort} recommended`, value: String(options.defaultPort) },
|
|
@@ -493,10 +576,10 @@ function writeFilteredLogChunk(chunk, options) {
|
|
|
493
576
|
options.outputStream?.write(outputText);
|
|
494
577
|
}
|
|
495
578
|
async function refreshAppsCacheForCurrentTarget() {
|
|
496
|
-
const target = await readCloudFoundryTarget();
|
|
497
|
-
const targetKey = buildCloudFoundryTargetKey(target);
|
|
498
|
-
const apps = await listCloudFoundryApps();
|
|
499
|
-
await rememberCloudFoundryApps(targetKey, apps);
|
|
579
|
+
const target = await (0, cf_1.readCloudFoundryTarget)();
|
|
580
|
+
const targetKey = (0, cf_1.buildCloudFoundryTargetKey)(target);
|
|
581
|
+
const apps = await (0, cf_1.listCloudFoundryApps)();
|
|
582
|
+
await (0, cache_1.rememberCloudFoundryApps)(targetKey, apps);
|
|
500
583
|
return apps;
|
|
501
584
|
}
|
|
502
585
|
function refreshAppsCacheInDetachedProcess() {
|
|
@@ -504,7 +587,7 @@ function refreshAppsCacheInDetachedProcess() {
|
|
|
504
587
|
if (!entryFilePath) {
|
|
505
588
|
return;
|
|
506
589
|
}
|
|
507
|
-
const childProcess = spawn(process.execPath, [entryFilePath, "cf", "apps-cache-refresh"], {
|
|
590
|
+
const childProcess = (0, node_child_process_1.spawn)(process.execPath, [entryFilePath, "cf", "apps-cache-refresh"], {
|
|
508
591
|
detached: true,
|
|
509
592
|
stdio: "ignore",
|
|
510
593
|
windowsHide: true,
|
|
@@ -512,12 +595,13 @@ function refreshAppsCacheInDetachedProcess() {
|
|
|
512
595
|
childProcess.unref();
|
|
513
596
|
}
|
|
514
597
|
async function getAppsWithCache(options) {
|
|
598
|
+
await ensureCloudFoundrySessionFromCache();
|
|
515
599
|
if (options.refresh) {
|
|
516
600
|
return refreshAppsCacheForCurrentTarget();
|
|
517
601
|
}
|
|
518
|
-
const target = await readCloudFoundryTarget();
|
|
519
|
-
const targetKey = buildCloudFoundryTargetKey(target);
|
|
520
|
-
const cache = await readCache();
|
|
602
|
+
const target = await (0, cf_1.readCloudFoundryTarget)();
|
|
603
|
+
const targetKey = (0, cf_1.buildCloudFoundryTargetKey)(target);
|
|
604
|
+
const cache = await (0, cache_1.readCache)();
|
|
521
605
|
const cachedEntry = cache.cloudFoundry.appListsByTarget[targetKey];
|
|
522
606
|
if (cachedEntry?.apps.length) {
|
|
523
607
|
if (options.startBackgroundRefresh) {
|
|
@@ -529,16 +613,16 @@ async function getAppsWithCache(options) {
|
|
|
529
613
|
}
|
|
530
614
|
async function resolveAppSelection(options) {
|
|
531
615
|
if (options.app?.trim()) {
|
|
532
|
-
await rememberSelectedApp(options.app.trim());
|
|
616
|
+
await (0, cache_1.rememberSelectedApp)(options.app.trim());
|
|
533
617
|
return options.app.trim();
|
|
534
618
|
}
|
|
535
619
|
const apps = await getAppsWithCache({ refresh: options.refresh, startBackgroundRefresh: !options.refresh });
|
|
536
|
-
const cache = await readCache();
|
|
620
|
+
const cache = await (0, cache_1.readCache)();
|
|
537
621
|
const cachedSelectedAppNames = cache.cloudFoundry.selectedApps;
|
|
538
622
|
const cachedSelectedApps = cachedSelectedAppNames
|
|
539
623
|
.filter((appName) => !apps.some((app) => app.name === appName))
|
|
540
|
-
.map((appName) => ({ title: `${appName} ${
|
|
541
|
-
const appName = await searchableSelectChoice({
|
|
624
|
+
.map((appName) => ({ title: `${appName} ${chalk_1.default.gray("cached selected")}`, value: appName }));
|
|
625
|
+
const appName = await (0, prompts_2.searchableSelectChoice)({
|
|
542
626
|
message: options.message,
|
|
543
627
|
choices: [
|
|
544
628
|
...apps.map((app) => ({
|
|
@@ -550,7 +634,7 @@ async function resolveAppSelection(options) {
|
|
|
550
634
|
validateCustomValue: validateRequired,
|
|
551
635
|
customValueTitle: (value) => `Use typed app name: ${value}`,
|
|
552
636
|
});
|
|
553
|
-
await rememberSelectedApp(appName);
|
|
637
|
+
await (0, cache_1.rememberSelectedApp)(appName);
|
|
554
638
|
return appName;
|
|
555
639
|
}
|
|
556
640
|
function printTarget(target) {
|
|
@@ -562,15 +646,24 @@ function printTarget(target) {
|
|
|
562
646
|
const DEFAULT_CLOUD_FOUNDRY_API_ENDPOINTS = [
|
|
563
647
|
"https://api.cf.br10.hana.ondemand.com",
|
|
564
648
|
"https://api.cf.eu10.hana.ondemand.com",
|
|
649
|
+
"https://api.cf.eu10-004.hana.ondemand.com",
|
|
650
|
+
"https://api.cf.eu10-005.hana.ondemand.com",
|
|
651
|
+
"https://api.cf.eu20.hana.ondemand.com",
|
|
652
|
+
"https://api.cf.eu20-001.hana.ondemand.com",
|
|
653
|
+
"https://api.cf.eu20-002.hana.ondemand.com",
|
|
565
654
|
"https://api.cf.us10.hana.ondemand.com",
|
|
655
|
+
"https://api.cf.us10-001.hana.ondemand.com",
|
|
656
|
+
"https://api.cf.us11.hana.ondemand.com",
|
|
657
|
+
"https://api.cf.us20.hana.ondemand.com",
|
|
658
|
+
"https://api.cf.us21.hana.ondemand.com",
|
|
566
659
|
"https://api.cf.ap10.hana.ondemand.com",
|
|
567
660
|
"https://api.cf.ap11.hana.ondemand.com",
|
|
661
|
+
"https://api.cf.ap20.hana.ondemand.com",
|
|
568
662
|
"https://api.cf.ap21.hana.ondemand.com",
|
|
569
663
|
"https://api.cf.jp10.hana.ondemand.com",
|
|
570
664
|
"https://api.cf.ca10.hana.ondemand.com",
|
|
571
665
|
"https://api.cf.ch20.hana.ondemand.com",
|
|
572
|
-
"https://api.cf.
|
|
573
|
-
"https://api.cf.us20.hana.ondemand.com",
|
|
666
|
+
"https://api.cf.sa10.hana.ondemand.com",
|
|
574
667
|
];
|
|
575
668
|
function uniqueValues(values) {
|
|
576
669
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
@@ -585,7 +678,7 @@ async function selectCloudFoundryApiEndpoint(options) {
|
|
|
585
678
|
}
|
|
586
679
|
const choices = [
|
|
587
680
|
...cachedApiEndpoints.map((apiEndpoint) => ({
|
|
588
|
-
title: `${apiEndpoint} ${
|
|
681
|
+
title: `${apiEndpoint} ${chalk_1.default.gray("cached")}`,
|
|
589
682
|
value: apiEndpoint,
|
|
590
683
|
})),
|
|
591
684
|
...DEFAULT_CLOUD_FOUNDRY_API_ENDPOINTS
|
|
@@ -593,7 +686,7 @@ async function selectCloudFoundryApiEndpoint(options) {
|
|
|
593
686
|
.map((apiEndpoint) => ({ title: apiEndpoint, value: apiEndpoint })),
|
|
594
687
|
{ title: "Enter CF API endpoint manually", value: "__ENTER_MANUAL__" },
|
|
595
688
|
];
|
|
596
|
-
return searchableSelectChoice({
|
|
689
|
+
return (0, prompts_2.searchableSelectChoice)({
|
|
597
690
|
message: "Select CF API endpoint",
|
|
598
691
|
choices: choices.filter((choice) => choice.value !== "__ENTER_MANUAL__"),
|
|
599
692
|
validateCustomValue: validateRequired,
|
|
@@ -604,16 +697,16 @@ async function selectCloudFoundryOrganization(options) {
|
|
|
604
697
|
if (options.org?.trim()) {
|
|
605
698
|
return options.org.trim();
|
|
606
699
|
}
|
|
607
|
-
const organizations = await listCloudFoundryOrganizations();
|
|
700
|
+
const organizations = await (0, cf_1.listCloudFoundryOrganizations)();
|
|
608
701
|
if (organizations.length === 0) {
|
|
609
|
-
return selectFromHistoryOrInput({
|
|
702
|
+
return (0, prompts_2.selectFromHistoryOrInput)({
|
|
610
703
|
message: "Select CF org",
|
|
611
704
|
values: options.cachedOrganizations,
|
|
612
705
|
initialValue: options.cachedOrganizations[0],
|
|
613
706
|
validate: validateRequired,
|
|
614
707
|
});
|
|
615
708
|
}
|
|
616
|
-
return searchableSelectChoice({
|
|
709
|
+
return (0, prompts_2.searchableSelectChoice)({
|
|
617
710
|
message: "Select CF org",
|
|
618
711
|
choices: organizations.map((organization) => ({ title: organization, value: organization })),
|
|
619
712
|
validateCustomValue: validateRequired,
|
|
@@ -624,16 +717,16 @@ async function selectCloudFoundrySpace(options) {
|
|
|
624
717
|
if (options.space?.trim()) {
|
|
625
718
|
return options.space.trim();
|
|
626
719
|
}
|
|
627
|
-
const spaces = await listCloudFoundrySpaces();
|
|
720
|
+
const spaces = await (0, cf_1.listCloudFoundrySpaces)();
|
|
628
721
|
if (spaces.length === 0) {
|
|
629
|
-
return selectFromHistoryOrInput({
|
|
722
|
+
return (0, prompts_2.selectFromHistoryOrInput)({
|
|
630
723
|
message: "Select CF space",
|
|
631
724
|
values: options.cachedSpaces,
|
|
632
725
|
initialValue: options.cachedSpaces[0] ?? "app",
|
|
633
726
|
});
|
|
634
727
|
}
|
|
635
728
|
const initialSpace = spaces.includes("app") ? "app" : spaces[0];
|
|
636
|
-
return searchableSelectChoice({
|
|
729
|
+
return (0, prompts_2.searchableSelectChoice)({
|
|
637
730
|
message: "Select CF space",
|
|
638
731
|
choices: [
|
|
639
732
|
...spaces
|
|
@@ -648,13 +741,14 @@ async function selectCloudFoundrySpace(options) {
|
|
|
648
741
|
});
|
|
649
742
|
}
|
|
650
743
|
async function runLoginCommand(options) {
|
|
651
|
-
|
|
744
|
+
await (0, tooling_1.ensureExternalTool)("cf");
|
|
745
|
+
const cache = await (0, cache_1.readCache)();
|
|
652
746
|
const lastProfile = cache.cloudFoundry.loginProfiles[0];
|
|
653
747
|
const apiEndpoint = await selectCloudFoundryApiEndpoint({
|
|
654
748
|
api: options.api,
|
|
655
749
|
cachedApiEndpoints: cache.cloudFoundry.loginProfiles.map((item) => item.apiEndpoint),
|
|
656
750
|
});
|
|
657
|
-
const username = options.username ?? await selectFromHistoryOrInput({
|
|
751
|
+
const username = options.username ?? await (0, prompts_2.selectFromHistoryOrInput)({
|
|
658
752
|
message: "Select CF username",
|
|
659
753
|
values: cache.cloudFoundry.loginProfiles.map((item) => item.username),
|
|
660
754
|
initialValue: lastProfile?.username,
|
|
@@ -663,7 +757,7 @@ async function runLoginCommand(options) {
|
|
|
663
757
|
let password = options.password ?? lastProfile?.password;
|
|
664
758
|
let shouldSavePassword = options.savePassword ?? false;
|
|
665
759
|
if (!password) {
|
|
666
|
-
const response = await
|
|
760
|
+
const response = await (0, prompts_1.default)({
|
|
667
761
|
type: "password",
|
|
668
762
|
name: "password",
|
|
669
763
|
message: "Enter CF password",
|
|
@@ -675,7 +769,7 @@ async function runLoginCommand(options) {
|
|
|
675
769
|
password = response.password;
|
|
676
770
|
}
|
|
677
771
|
else {
|
|
678
|
-
const response = await
|
|
772
|
+
const response = await (0, prompts_1.default)({
|
|
679
773
|
type: "select",
|
|
680
774
|
name: "useCachedPassword",
|
|
681
775
|
message: "Use cached password?",
|
|
@@ -686,7 +780,7 @@ async function runLoginCommand(options) {
|
|
|
686
780
|
initial: 0,
|
|
687
781
|
});
|
|
688
782
|
if (!response.useCachedPassword) {
|
|
689
|
-
const passwordResponse = await
|
|
783
|
+
const passwordResponse = await (0, prompts_1.default)({
|
|
690
784
|
type: "password",
|
|
691
785
|
name: "password",
|
|
692
786
|
message: "Enter CF password",
|
|
@@ -699,7 +793,7 @@ async function runLoginCommand(options) {
|
|
|
699
793
|
}
|
|
700
794
|
}
|
|
701
795
|
if (!shouldSavePassword) {
|
|
702
|
-
const savePasswordResponse = await
|
|
796
|
+
const savePasswordResponse = await (0, prompts_1.default)({
|
|
703
797
|
type: "select",
|
|
704
798
|
name: "savePassword",
|
|
705
799
|
message: "Save password for automatic re-login and region scan?",
|
|
@@ -711,12 +805,12 @@ async function runLoginCommand(options) {
|
|
|
711
805
|
});
|
|
712
806
|
shouldSavePassword = Boolean(savePasswordResponse.savePassword);
|
|
713
807
|
}
|
|
714
|
-
const apiExitCode = await setCloudFoundryApiEndpoint(apiEndpoint);
|
|
808
|
+
const apiExitCode = await (0, cf_1.setCloudFoundryApiEndpoint)(apiEndpoint);
|
|
715
809
|
if (apiExitCode !== 0) {
|
|
716
810
|
process.exitCode = apiExitCode;
|
|
717
811
|
return;
|
|
718
812
|
}
|
|
719
|
-
const authExitCode = await authenticateCloudFoundry({ username, password });
|
|
813
|
+
const authExitCode = await (0, cf_1.authenticateCloudFoundry)({ username, password });
|
|
720
814
|
if (authExitCode !== 0) {
|
|
721
815
|
process.exitCode = authExitCode;
|
|
722
816
|
return;
|
|
@@ -727,7 +821,7 @@ async function runLoginCommand(options) {
|
|
|
727
821
|
.filter((item) => item.apiEndpoint === apiEndpoint && item.username === username)
|
|
728
822
|
.map((item) => item.org),
|
|
729
823
|
});
|
|
730
|
-
const orgExitCode = await targetCloudFoundryOrg(org);
|
|
824
|
+
const orgExitCode = await (0, cf_1.targetCloudFoundryOrg)(org);
|
|
731
825
|
if (orgExitCode !== 0) {
|
|
732
826
|
process.exitCode = orgExitCode;
|
|
733
827
|
return;
|
|
@@ -740,13 +834,13 @@ async function runLoginCommand(options) {
|
|
|
740
834
|
.filter(Boolean),
|
|
741
835
|
});
|
|
742
836
|
if (space) {
|
|
743
|
-
const spaceExitCode = await targetCloudFoundrySpace(space);
|
|
837
|
+
const spaceExitCode = await (0, cf_1.targetCloudFoundrySpace)(space);
|
|
744
838
|
if (spaceExitCode !== 0) {
|
|
745
839
|
process.exitCode = spaceExitCode;
|
|
746
840
|
return;
|
|
747
841
|
}
|
|
748
842
|
}
|
|
749
|
-
await rememberCloudFoundryLoginProfile({
|
|
843
|
+
await (0, cache_1.rememberCloudFoundryLoginProfile)({
|
|
750
844
|
apiEndpoint,
|
|
751
845
|
username,
|
|
752
846
|
org,
|
|
@@ -755,28 +849,29 @@ async function runLoginCommand(options) {
|
|
|
755
849
|
updatedAt: new Date().toISOString(),
|
|
756
850
|
});
|
|
757
851
|
if (shouldSavePassword) {
|
|
758
|
-
console.log(
|
|
852
|
+
console.log(chalk_1.default.yellow("Password was cached in ~/.simplemdg/cache.json for automatic re-login. Do not use this on shared machines."));
|
|
759
853
|
}
|
|
760
|
-
console.log(
|
|
854
|
+
console.log(chalk_1.default.green("CF login completed."));
|
|
761
855
|
}
|
|
762
856
|
function formatCloudFoundryOrgEntry(entry, target) {
|
|
763
857
|
const isCurrent = entry.apiEndpoint === target.apiEndpoint && entry.org === target.org;
|
|
764
858
|
const spaceText = typeof entry.spaceCount === "number"
|
|
765
859
|
? `${entry.spaceCount} ${entry.spaceCount === 1 ? "space" : "spaces"}`
|
|
766
860
|
: "spaces unknown";
|
|
767
|
-
return `${entry.org} ${
|
|
861
|
+
return `${entry.org} ${chalk_1.default.gray(`${entry.region} · ${spaceText}${isCurrent ? " · current" : ""}`)}`;
|
|
768
862
|
}
|
|
769
863
|
function getCloudFoundryApiEndpointsForOrgSearch(options, target, cache) {
|
|
770
864
|
return uniqueValues([
|
|
771
865
|
options.api ?? "",
|
|
772
866
|
target.apiEndpoint ?? "",
|
|
773
867
|
...cache.cloudFoundry.loginProfiles.map((item) => item.apiEndpoint),
|
|
868
|
+
...cache.cloudFoundry.orgsAcrossRegions.map((item) => item.apiEndpoint),
|
|
774
869
|
...DEFAULT_CLOUD_FOUNDRY_API_ENDPOINTS,
|
|
775
870
|
]);
|
|
776
871
|
}
|
|
777
872
|
async function getCloudFoundryOrganizationsAcrossRegions(options) {
|
|
778
|
-
const target = await readCloudFoundryTarget();
|
|
779
|
-
const cache = await readCache();
|
|
873
|
+
const target = await (0, cf_1.readCloudFoundryTarget)();
|
|
874
|
+
const cache = await (0, cache_1.readCache)();
|
|
780
875
|
const cachedEntries = cache.cloudFoundry.orgsAcrossRegions ?? [];
|
|
781
876
|
const cachedRegionCount = new Set(cachedEntries.map((entry) => entry.region)).size;
|
|
782
877
|
if (!options.refresh && cachedEntries.length && cachedRegionCount > 1) {
|
|
@@ -786,21 +881,21 @@ async function getCloudFoundryOrganizationsAcrossRegions(options) {
|
|
|
786
881
|
});
|
|
787
882
|
}
|
|
788
883
|
const apiEndpoints = getCloudFoundryApiEndpointsForOrgSearch(options, target, cache);
|
|
789
|
-
console.log(
|
|
884
|
+
console.log(chalk_1.default.gray(`Searching CF orgs across ${apiEndpoints.length} region endpoint(s)...`));
|
|
790
885
|
const credentials = cache.cloudFoundry.loginProfiles.map((profile) => ({
|
|
791
886
|
apiEndpoint: profile.apiEndpoint,
|
|
792
887
|
username: profile.username,
|
|
793
888
|
password: profile.password,
|
|
794
889
|
}));
|
|
795
|
-
const entries = await scanCloudFoundryOrganizationsAcrossRegions(apiEndpoints, credentials);
|
|
890
|
+
const entries = await (0, cf_1.scanCloudFoundryOrganizationsAcrossRegions)(apiEndpoints, credentials);
|
|
796
891
|
if (entries.length) {
|
|
797
892
|
const regionCount = new Set(entries.map((entry) => entry.region)).size;
|
|
798
|
-
console.log(
|
|
799
|
-
await rememberCloudFoundryOrgEntries(entries);
|
|
893
|
+
console.log(chalk_1.default.green(`Found ${entries.length} org(s) across ${regionCount} region(s).`));
|
|
894
|
+
await (0, cache_1.rememberCloudFoundryOrgEntries)(entries);
|
|
800
895
|
return entries;
|
|
801
896
|
}
|
|
802
|
-
const currentOrganizations = await listCloudFoundryOrganizations().catch(() => []);
|
|
803
|
-
const currentRegion = target.apiEndpoint ? inferCloudFoundryRegionFromApiEndpoint(target.apiEndpoint) : "current";
|
|
897
|
+
const currentOrganizations = await (0, cf_1.listCloudFoundryOrganizations)().catch(() => []);
|
|
898
|
+
const currentRegion = target.apiEndpoint ? (0, cf_1.inferCloudFoundryRegionFromApiEndpoint)(target.apiEndpoint) : "current";
|
|
804
899
|
const fallbackEntries = currentOrganizations.map((organization) => ({
|
|
805
900
|
apiEndpoint: target.apiEndpoint ?? "",
|
|
806
901
|
region: currentRegion,
|
|
@@ -808,7 +903,7 @@ async function getCloudFoundryOrganizationsAcrossRegions(options) {
|
|
|
808
903
|
updatedAt: new Date().toISOString(),
|
|
809
904
|
}));
|
|
810
905
|
if (fallbackEntries.length) {
|
|
811
|
-
await rememberCloudFoundryOrgEntries(fallbackEntries);
|
|
906
|
+
await (0, cache_1.rememberCloudFoundryOrgEntries)(fallbackEntries);
|
|
812
907
|
}
|
|
813
908
|
return fallbackEntries;
|
|
814
909
|
}
|
|
@@ -818,7 +913,7 @@ async function runOrgCommand(options) {
|
|
|
818
913
|
? "list"
|
|
819
914
|
: options.switch
|
|
820
915
|
? "switch"
|
|
821
|
-
: await searchableSelectChoice({
|
|
916
|
+
: await (0, prompts_2.searchableSelectChoice)({
|
|
822
917
|
message: "What do you want to do with CF org?",
|
|
823
918
|
choices: [
|
|
824
919
|
{ title: "List orgs across regions", value: "list" },
|
|
@@ -836,18 +931,18 @@ async function runOrgCommand(options) {
|
|
|
836
931
|
refresh: options.refresh,
|
|
837
932
|
});
|
|
838
933
|
const organizationRegionCount = new Set(organizationEntries.map((entry) => entry.region)).size;
|
|
839
|
-
const latestTarget = await readCloudFoundryTarget();
|
|
934
|
+
const latestTarget = await (0, cf_1.readCloudFoundryTarget)();
|
|
840
935
|
if (action === "list") {
|
|
841
936
|
printTarget(latestTarget);
|
|
842
937
|
console.log("");
|
|
843
938
|
if (!organizationEntries.length) {
|
|
844
|
-
console.log(
|
|
939
|
+
console.log(chalk_1.default.yellow("No orgs found for current CF user. Run smdg cf login, save the password, then run smdg cf org again."));
|
|
845
940
|
return;
|
|
846
941
|
}
|
|
847
|
-
console.log(
|
|
942
|
+
console.log(chalk_1.default.gray(`Showing ${organizationEntries.length} org(s) across ${organizationRegionCount} region(s).`));
|
|
848
943
|
console.log("");
|
|
849
944
|
for (const entry of organizationEntries) {
|
|
850
|
-
const marker = entry.apiEndpoint === latestTarget.apiEndpoint && entry.org === latestTarget.org ?
|
|
945
|
+
const marker = entry.apiEndpoint === latestTarget.apiEndpoint && entry.org === latestTarget.org ? chalk_1.default.green("*") : " ";
|
|
851
946
|
console.log(`${marker} ${formatCloudFoundryOrgEntry(entry, latestTarget)}`);
|
|
852
947
|
}
|
|
853
948
|
return;
|
|
@@ -859,19 +954,19 @@ async function runOrgCommand(options) {
|
|
|
859
954
|
}) ?? {
|
|
860
955
|
apiEndpoint: options.api?.trim() || latestTarget.apiEndpoint || "",
|
|
861
956
|
region: options.api?.trim()
|
|
862
|
-
? inferCloudFoundryRegionFromApiEndpoint(options.api.trim())
|
|
863
|
-
: inferCloudFoundryRegionFromApiEndpoint(latestTarget.apiEndpoint ?? "current"),
|
|
957
|
+
? (0, cf_1.inferCloudFoundryRegionFromApiEndpoint)(options.api.trim())
|
|
958
|
+
: (0, cf_1.inferCloudFoundryRegionFromApiEndpoint)(latestTarget.apiEndpoint ?? "current"),
|
|
864
959
|
org: options.org.trim(),
|
|
865
960
|
updatedAt: new Date().toISOString(),
|
|
866
961
|
};
|
|
867
962
|
}
|
|
868
963
|
else {
|
|
869
964
|
if (!organizationEntries.length) {
|
|
870
|
-
console.log(
|
|
871
|
-
console.log(
|
|
965
|
+
console.log(chalk_1.default.yellow("No orgs were found across regions."));
|
|
966
|
+
console.log(chalk_1.default.gray("Run smdg cf login and save the password, then run smdg cf org --list --refresh."));
|
|
872
967
|
return;
|
|
873
968
|
}
|
|
874
|
-
const selectedIndex = await searchableSelectChoice({
|
|
969
|
+
const selectedIndex = await (0, prompts_2.searchableSelectChoice)({
|
|
875
970
|
message: `Search CF org across ${organizationRegionCount} region(s)`,
|
|
876
971
|
choices: organizationEntries.map((entry, index) => ({
|
|
877
972
|
title: formatCloudFoundryOrgEntry(entry, latestTarget),
|
|
@@ -882,7 +977,7 @@ async function runOrgCommand(options) {
|
|
|
882
977
|
});
|
|
883
978
|
selectedEntry = organizationEntries[Number(selectedIndex)] ?? {
|
|
884
979
|
apiEndpoint: latestTarget.apiEndpoint ?? "",
|
|
885
|
-
region: inferCloudFoundryRegionFromApiEndpoint(latestTarget.apiEndpoint ?? "current"),
|
|
980
|
+
region: (0, cf_1.inferCloudFoundryRegionFromApiEndpoint)(latestTarget.apiEndpoint ?? "current"),
|
|
886
981
|
org: selectedIndex,
|
|
887
982
|
updatedAt: new Date().toISOString(),
|
|
888
983
|
};
|
|
@@ -890,26 +985,28 @@ async function runOrgCommand(options) {
|
|
|
890
985
|
if (!selectedEntry.apiEndpoint) {
|
|
891
986
|
throw new Error("Cannot determine CF API endpoint for selected org.");
|
|
892
987
|
}
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
988
|
+
const authenticatedProfile = await ensureCloudFoundryAuthenticatedForApiEndpoint({
|
|
989
|
+
apiEndpoint: selectedEntry.apiEndpoint,
|
|
990
|
+
preferredOrg: selectedEntry.org,
|
|
991
|
+
preferredSpace: options.space,
|
|
992
|
+
reason: "switch-org",
|
|
993
|
+
});
|
|
994
|
+
const orgExitCode = await (0, cf_1.targetCloudFoundryOrg)(selectedEntry.org);
|
|
899
995
|
if (orgExitCode !== 0) {
|
|
900
|
-
console.log(
|
|
996
|
+
console.log(chalk_1.default.yellow("Cannot switch to this org after automatic authentication."));
|
|
997
|
+
console.log(chalk_1.default.gray("Run smdg cf login, save the password, then try again."));
|
|
901
998
|
process.exitCode = orgExitCode;
|
|
902
999
|
return;
|
|
903
1000
|
}
|
|
904
|
-
const spaces = selectedEntry.spaces?.length ? selectedEntry.spaces : await listCloudFoundrySpaces();
|
|
905
|
-
const currentTargetAfterOrgSwitch = await readCloudFoundryTarget();
|
|
1001
|
+
const spaces = selectedEntry.spaces?.length ? selectedEntry.spaces : await (0, cf_1.listCloudFoundrySpaces)();
|
|
1002
|
+
const currentTargetAfterOrgSwitch = await (0, cf_1.readCloudFoundryTarget)();
|
|
906
1003
|
const preferredSpace = options.space?.trim() || currentTargetAfterOrgSwitch.space || (spaces.includes("app") ? "app" : spaces[0]);
|
|
907
|
-
const space = options.space?.trim() || await searchableSelectChoice({
|
|
1004
|
+
const space = options.space?.trim() || await (0, prompts_2.searchableSelectChoice)({
|
|
908
1005
|
message: "Select CF space",
|
|
909
1006
|
choices: [
|
|
910
1007
|
...spaces
|
|
911
1008
|
.filter((spaceName) => spaceName === preferredSpace)
|
|
912
|
-
.map((spaceName) => ({ title: `${spaceName} ${spaceName === currentTargetAfterOrgSwitch.space ?
|
|
1009
|
+
.map((spaceName) => ({ title: `${spaceName} ${spaceName === currentTargetAfterOrgSwitch.space ? chalk_1.default.gray("current") : chalk_1.default.gray("suggested")}`, value: spaceName })),
|
|
913
1010
|
...spaces
|
|
914
1011
|
.filter((spaceName) => spaceName !== preferredSpace)
|
|
915
1012
|
.map((spaceName) => ({ title: spaceName, value: spaceName })),
|
|
@@ -918,20 +1015,29 @@ async function runOrgCommand(options) {
|
|
|
918
1015
|
customValueTitle: (value) => `Use typed CF space: ${value}`,
|
|
919
1016
|
});
|
|
920
1017
|
if (space) {
|
|
921
|
-
const spaceExitCode = await targetCloudFoundrySpace(space);
|
|
1018
|
+
const spaceExitCode = await (0, cf_1.targetCloudFoundrySpace)(space);
|
|
922
1019
|
if (spaceExitCode !== 0) {
|
|
923
1020
|
process.exitCode = spaceExitCode;
|
|
924
1021
|
return;
|
|
925
1022
|
}
|
|
926
1023
|
}
|
|
927
|
-
|
|
928
|
-
|
|
1024
|
+
if (authenticatedProfile?.password) {
|
|
1025
|
+
await (0, cache_1.rememberCloudFoundryLoginProfile)({
|
|
1026
|
+
...authenticatedProfile,
|
|
1027
|
+
apiEndpoint: selectedEntry.apiEndpoint,
|
|
1028
|
+
org: selectedEntry.org,
|
|
1029
|
+
space,
|
|
1030
|
+
updatedAt: new Date().toISOString(),
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
const switchedTarget = await (0, cf_1.readCloudFoundryTarget)();
|
|
1034
|
+
console.log(chalk_1.default.green("CF org/space switched."));
|
|
929
1035
|
printTarget(switchedTarget);
|
|
930
1036
|
}
|
|
931
1037
|
async function runAppsCommand(options) {
|
|
932
|
-
const target = await readCloudFoundryTarget();
|
|
933
|
-
const targetKey = buildCloudFoundryTargetKey(target);
|
|
934
|
-
const cache = await readCache();
|
|
1038
|
+
const target = await (0, cf_1.readCloudFoundryTarget)();
|
|
1039
|
+
const targetKey = (0, cf_1.buildCloudFoundryTargetKey)(target);
|
|
1040
|
+
const cache = await (0, cache_1.readCache)();
|
|
935
1041
|
const cachedEntry = cache.cloudFoundry.appListsByTarget[targetKey];
|
|
936
1042
|
printTarget(target);
|
|
937
1043
|
console.log("");
|
|
@@ -941,12 +1047,12 @@ async function runAppsCommand(options) {
|
|
|
941
1047
|
startBackgroundRefresh: shouldUseCache,
|
|
942
1048
|
});
|
|
943
1049
|
if (shouldUseCache && cachedEntry) {
|
|
944
|
-
console.log(
|
|
945
|
-
console.log(
|
|
1050
|
+
console.log(chalk_1.default.gray(`Using cached cf apps: ${cachedEntry.apps.length} apps, updated at ${cachedEntry.updatedAt}`));
|
|
1051
|
+
console.log(chalk_1.default.gray("Refreshing cf apps cache in background. Use --refresh when you want to wait for fresh data."));
|
|
946
1052
|
console.log("");
|
|
947
1053
|
}
|
|
948
1054
|
else if (options.refresh) {
|
|
949
|
-
console.log(
|
|
1055
|
+
console.log(chalk_1.default.green(`Refreshed cf apps cache for ${targetKey}.`));
|
|
950
1056
|
console.log("");
|
|
951
1057
|
}
|
|
952
1058
|
if (options.select) {
|
|
@@ -962,55 +1068,55 @@ async function runAppsCacheRefreshCommand() {
|
|
|
962
1068
|
await refreshAppsCacheForCurrentTarget();
|
|
963
1069
|
}
|
|
964
1070
|
async function runBindCommand(options) {
|
|
965
|
-
const repositoryPath = await resolveRepositoryPath(options.cwd ?? process.cwd());
|
|
1071
|
+
const repositoryPath = await (0, repository_1.resolveRepositoryPath)(options.cwd ?? process.cwd());
|
|
966
1072
|
const appName = await resolveAppSelection({ app: options.app, refresh: options.refresh, message: "Select app to cds bind" });
|
|
967
|
-
const exitCode = await runCommandInherit("cds", ["bind", "--to-app-services", appName], { cwd: repositoryPath });
|
|
1073
|
+
const exitCode = await (0, process_1.runCommandInherit)("cds", ["bind", "--to-app-services", appName], { cwd: repositoryPath });
|
|
968
1074
|
process.exitCode = exitCode;
|
|
969
1075
|
}
|
|
970
1076
|
async function runEnvCommand(options) {
|
|
971
|
-
const repositoryPath = await resolveRepositoryPath(options.cwd ?? process.cwd());
|
|
1077
|
+
const repositoryPath = await (0, repository_1.resolveRepositoryPath)(options.cwd ?? process.cwd());
|
|
972
1078
|
const appName = await resolveAppSelection({ app: options.app, refresh: options.refresh, message: "Select app to export cf env" });
|
|
973
|
-
const cache = await readCache();
|
|
974
|
-
const outputFileName = options.out ?? await selectFromHistoryOrInput({
|
|
1079
|
+
const cache = await (0, cache_1.readCache)();
|
|
1080
|
+
const outputFileName = options.out ?? await (0, prompts_2.selectFromHistoryOrInput)({
|
|
975
1081
|
message: "Select output env file name",
|
|
976
1082
|
values: cache.cloudFoundry.envFileNames,
|
|
977
1083
|
initialValue: cache.cloudFoundry.envFileNames[0] ?? "default-env.json",
|
|
978
1084
|
validate: validateRequired,
|
|
979
1085
|
});
|
|
980
|
-
const result = await runCommand("cf", ["env", appName]);
|
|
1086
|
+
const result = await (0, process_1.runCommand)("cf", ["env", appName]);
|
|
981
1087
|
if (result.exitCode !== 0) {
|
|
982
1088
|
throw new Error(result.stderr || result.stdout || "cf env failed");
|
|
983
1089
|
}
|
|
984
|
-
const outputPath =
|
|
1090
|
+
const outputPath = node_path_1.default.resolve(repositoryPath, outputFileName);
|
|
985
1091
|
if (options.raw) {
|
|
986
|
-
await
|
|
1092
|
+
await fs_extra_1.default.writeFile(outputPath, result.stdout, "utf8");
|
|
987
1093
|
}
|
|
988
1094
|
else {
|
|
989
|
-
const parsedEnvironment = parseCloudFoundryEnvironment(result.stdout);
|
|
990
|
-
await
|
|
1095
|
+
const parsedEnvironment = (0, cf_env_parser_1.parseCloudFoundryEnvironment)(result.stdout);
|
|
1096
|
+
await fs_extra_1.default.writeJson(outputPath, parsedEnvironment, { spaces: 2 });
|
|
991
1097
|
}
|
|
992
|
-
await rememberSelectedApp(appName);
|
|
993
|
-
await rememberEnvironmentFileName(outputFileName);
|
|
994
|
-
console.log(
|
|
1098
|
+
await (0, cache_1.rememberSelectedApp)(appName);
|
|
1099
|
+
await (0, cache_1.rememberEnvironmentFileName)(outputFileName);
|
|
1100
|
+
console.log(chalk_1.default.green(`Exported ${options.raw ? "raw env" : "clean JSON env"} to ${outputPath}`));
|
|
995
1101
|
}
|
|
996
1102
|
async function runLogsCommand(options) {
|
|
997
1103
|
const appName = await resolveAppSelection({ app: options.app, refresh: options.refresh, message: "Select app to view logs" });
|
|
998
1104
|
const shouldFollow = options.follow || !options.recent;
|
|
999
1105
|
const shouldReadRecent = options.recent || !shouldFollow;
|
|
1000
|
-
const outputPath = options.out ?
|
|
1106
|
+
const outputPath = options.out ? node_path_1.default.resolve(process.cwd(), options.out) : undefined;
|
|
1001
1107
|
if (outputPath) {
|
|
1002
|
-
await
|
|
1108
|
+
await fs_extra_1.default.ensureDir(node_path_1.default.dirname(outputPath));
|
|
1003
1109
|
}
|
|
1004
1110
|
if (shouldReadRecent && !shouldFollow) {
|
|
1005
|
-
const result = await runCommand("cf", buildCloudFoundryLogsArgs({ appName, recent: true }));
|
|
1111
|
+
const result = await (0, process_1.runCommand)("cf", buildCloudFoundryLogsArgs({ appName, recent: true }));
|
|
1006
1112
|
const combinedOutput = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
1007
1113
|
const filteredOutput = filterCloudFoundryLogsOutput(combinedOutput, {
|
|
1008
1114
|
instance: options.instance,
|
|
1009
1115
|
process: options.process,
|
|
1010
1116
|
});
|
|
1011
1117
|
if (outputPath) {
|
|
1012
|
-
await
|
|
1013
|
-
console.log(
|
|
1118
|
+
await fs_extra_1.default.writeFile(outputPath, filteredOutput.endsWith("\n") ? filteredOutput : `${filteredOutput}\n`, "utf8");
|
|
1119
|
+
console.log(chalk_1.default.green(`Exported recent logs to ${outputPath}`));
|
|
1014
1120
|
}
|
|
1015
1121
|
else {
|
|
1016
1122
|
console.log(filteredOutput);
|
|
@@ -1018,12 +1124,12 @@ async function runLogsCommand(options) {
|
|
|
1018
1124
|
process.exitCode = result.exitCode;
|
|
1019
1125
|
return;
|
|
1020
1126
|
}
|
|
1021
|
-
const outputStream = outputPath ?
|
|
1127
|
+
const outputStream = outputPath ? node_fs_1.default.createWriteStream(outputPath, { flags: "a" }) : undefined;
|
|
1022
1128
|
if (outputPath) {
|
|
1023
|
-
console.log(
|
|
1129
|
+
console.log(chalk_1.default.gray(`Streaming logs and appending to ${outputPath}`));
|
|
1024
1130
|
}
|
|
1025
|
-
console.log(
|
|
1026
|
-
const childProcess = spawn("cf", buildCloudFoundryLogsArgs({ appName, recent: false }), {
|
|
1131
|
+
console.log(chalk_1.default.gray("Press Ctrl+C to stop realtime logs."));
|
|
1132
|
+
const childProcess = (0, node_child_process_1.spawn)("cf", buildCloudFoundryLogsArgs({ appName, recent: false }), {
|
|
1027
1133
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1028
1134
|
shell: false,
|
|
1029
1135
|
windowsHide: true,
|
|
@@ -1051,8 +1157,1291 @@ async function runLogsCommand(options) {
|
|
|
1051
1157
|
childProcess.on("close", () => resolve());
|
|
1052
1158
|
});
|
|
1053
1159
|
}
|
|
1160
|
+
function parseWebSocketUrl(value) {
|
|
1161
|
+
const url = new URL(value);
|
|
1162
|
+
return {
|
|
1163
|
+
host: url.hostname,
|
|
1164
|
+
port: Number(url.port || 80),
|
|
1165
|
+
path: `${url.pathname}${url.search}`,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
async function getNodeInspectorWebSocketUrl(localPort) {
|
|
1169
|
+
const response = await fetch(`http://127.0.0.1:${localPort}/json/list`);
|
|
1170
|
+
if (!response.ok) {
|
|
1171
|
+
return undefined;
|
|
1172
|
+
}
|
|
1173
|
+
const targets = await response.json();
|
|
1174
|
+
return targets.find((target) => target.webSocketDebuggerUrl)?.webSocketDebuggerUrl;
|
|
1175
|
+
}
|
|
1176
|
+
async function waitForNodeInspectorWebSocketUrl(localPort, timeoutMs = 15000) {
|
|
1177
|
+
const startedAt = Date.now();
|
|
1178
|
+
let lastError;
|
|
1179
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
1180
|
+
try {
|
|
1181
|
+
const webSocketUrl = await getNodeInspectorWebSocketUrl(localPort);
|
|
1182
|
+
if (webSocketUrl) {
|
|
1183
|
+
return webSocketUrl;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
catch (error) {
|
|
1187
|
+
lastError = error;
|
|
1188
|
+
}
|
|
1189
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1190
|
+
}
|
|
1191
|
+
if (lastError instanceof Error) {
|
|
1192
|
+
console.log(chalk_1.default.gray(`Could not read inspector WebSocket yet: ${lastError.message}`));
|
|
1193
|
+
}
|
|
1194
|
+
return undefined;
|
|
1195
|
+
}
|
|
1196
|
+
function encodeWebSocketFrame(payload) {
|
|
1197
|
+
const payloadBuffer = Buffer.from(payload, "utf8");
|
|
1198
|
+
const maskKey = node_crypto_1.default.randomBytes(4);
|
|
1199
|
+
let header;
|
|
1200
|
+
if (payloadBuffer.length < 126) {
|
|
1201
|
+
header = Buffer.alloc(2);
|
|
1202
|
+
header[0] = 0x81;
|
|
1203
|
+
header[1] = 0x80 | payloadBuffer.length;
|
|
1204
|
+
}
|
|
1205
|
+
else if (payloadBuffer.length <= 0xffff) {
|
|
1206
|
+
header = Buffer.alloc(4);
|
|
1207
|
+
header[0] = 0x81;
|
|
1208
|
+
header[1] = 0x80 | 126;
|
|
1209
|
+
header.writeUInt16BE(payloadBuffer.length, 2);
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
header = Buffer.alloc(10);
|
|
1213
|
+
header[0] = 0x81;
|
|
1214
|
+
header[1] = 0x80 | 127;
|
|
1215
|
+
header.writeBigUInt64BE(BigInt(payloadBuffer.length), 2);
|
|
1216
|
+
}
|
|
1217
|
+
const maskedPayload = Buffer.alloc(payloadBuffer.length);
|
|
1218
|
+
for (let index = 0; index < payloadBuffer.length; index += 1) {
|
|
1219
|
+
maskedPayload[index] = payloadBuffer[index] ^ maskKey[index % 4];
|
|
1220
|
+
}
|
|
1221
|
+
return Buffer.concat([header, maskKey, maskedPayload]);
|
|
1222
|
+
}
|
|
1223
|
+
function decodeWebSocketFrames(buffer) {
|
|
1224
|
+
const messages = [];
|
|
1225
|
+
let offset = 0;
|
|
1226
|
+
while (offset + 2 <= buffer.length) {
|
|
1227
|
+
const firstByte = buffer[offset];
|
|
1228
|
+
const secondByte = buffer[offset + 1];
|
|
1229
|
+
const opcode = firstByte & 0x0f;
|
|
1230
|
+
const isMasked = Boolean(secondByte & 0x80);
|
|
1231
|
+
let payloadLength = secondByte & 0x7f;
|
|
1232
|
+
let headerLength = 2;
|
|
1233
|
+
if (payloadLength === 126) {
|
|
1234
|
+
if (offset + 4 > buffer.length)
|
|
1235
|
+
break;
|
|
1236
|
+
payloadLength = buffer.readUInt16BE(offset + 2);
|
|
1237
|
+
headerLength = 4;
|
|
1238
|
+
}
|
|
1239
|
+
else if (payloadLength === 127) {
|
|
1240
|
+
if (offset + 10 > buffer.length)
|
|
1241
|
+
break;
|
|
1242
|
+
const longLength = buffer.readBigUInt64BE(offset + 2);
|
|
1243
|
+
if (longLength > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
1244
|
+
throw new Error("WebSocket frame is too large");
|
|
1245
|
+
}
|
|
1246
|
+
payloadLength = Number(longLength);
|
|
1247
|
+
headerLength = 10;
|
|
1248
|
+
}
|
|
1249
|
+
const maskLength = isMasked ? 4 : 0;
|
|
1250
|
+
const frameLength = headerLength + maskLength + payloadLength;
|
|
1251
|
+
if (offset + frameLength > buffer.length) {
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
let payload = buffer.subarray(offset + headerLength + maskLength, offset + frameLength);
|
|
1255
|
+
if (isMasked) {
|
|
1256
|
+
const maskKey = buffer.subarray(offset + headerLength, offset + headerLength + 4);
|
|
1257
|
+
const unmaskedPayload = Buffer.alloc(payload.length);
|
|
1258
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
1259
|
+
unmaskedPayload[index] = payload[index] ^ maskKey[index % 4];
|
|
1260
|
+
}
|
|
1261
|
+
payload = unmaskedPayload;
|
|
1262
|
+
}
|
|
1263
|
+
if (opcode === 0x1) {
|
|
1264
|
+
messages.push(payload.toString("utf8"));
|
|
1265
|
+
}
|
|
1266
|
+
offset += frameLength;
|
|
1267
|
+
}
|
|
1268
|
+
return { messages, remaining: buffer.subarray(offset) };
|
|
1269
|
+
}
|
|
1270
|
+
async function sendInspectorEvaluateCommand(options) {
|
|
1271
|
+
const connection = parseWebSocketUrl(options.webSocketUrl);
|
|
1272
|
+
const timeoutMs = options.timeoutMs ?? 10000;
|
|
1273
|
+
const key = node_crypto_1.default.randomBytes(16).toString("base64");
|
|
1274
|
+
const request = [
|
|
1275
|
+
`GET ${connection.path} HTTP/1.1`,
|
|
1276
|
+
`Host: ${connection.host}:${connection.port}`,
|
|
1277
|
+
"Upgrade: websocket",
|
|
1278
|
+
"Connection: Upgrade",
|
|
1279
|
+
`Sec-WebSocket-Key: ${key}`,
|
|
1280
|
+
"Sec-WebSocket-Version: 13",
|
|
1281
|
+
"",
|
|
1282
|
+
"",
|
|
1283
|
+
].join("\r\n");
|
|
1284
|
+
await new Promise((resolve, reject) => {
|
|
1285
|
+
const socket = node_net_1.default.createConnection({ host: connection.host, port: connection.port });
|
|
1286
|
+
const commandId = 1;
|
|
1287
|
+
let isHandshakeComplete = false;
|
|
1288
|
+
let handshakeBuffer = Buffer.alloc(0);
|
|
1289
|
+
let frameBuffer = Buffer.alloc(0);
|
|
1290
|
+
const timer = setTimeout(() => {
|
|
1291
|
+
socket.destroy();
|
|
1292
|
+
reject(new Error("Inspector Runtime.evaluate timed out"));
|
|
1293
|
+
}, timeoutMs);
|
|
1294
|
+
const cleanup = () => {
|
|
1295
|
+
clearTimeout(timer);
|
|
1296
|
+
socket.removeAllListeners();
|
|
1297
|
+
socket.end();
|
|
1298
|
+
socket.destroy();
|
|
1299
|
+
};
|
|
1300
|
+
socket.on("connect", () => {
|
|
1301
|
+
socket.write(request);
|
|
1302
|
+
});
|
|
1303
|
+
socket.on("error", (error) => {
|
|
1304
|
+
cleanup();
|
|
1305
|
+
reject(error);
|
|
1306
|
+
});
|
|
1307
|
+
socket.on("data", (chunk) => {
|
|
1308
|
+
try {
|
|
1309
|
+
if (!isHandshakeComplete) {
|
|
1310
|
+
handshakeBuffer = Buffer.concat([handshakeBuffer, chunk]);
|
|
1311
|
+
const headerEndIndex = handshakeBuffer.indexOf("\r\n\r\n");
|
|
1312
|
+
if (headerEndIndex < 0) {
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const headerText = handshakeBuffer.subarray(0, headerEndIndex).toString("utf8");
|
|
1316
|
+
if (!/^HTTP\/1\.1 101/i.test(headerText)) {
|
|
1317
|
+
throw new Error(`Inspector WebSocket upgrade failed: ${headerText.split("\r\n")[0]}`);
|
|
1318
|
+
}
|
|
1319
|
+
isHandshakeComplete = true;
|
|
1320
|
+
const rest = handshakeBuffer.subarray(headerEndIndex + 4);
|
|
1321
|
+
frameBuffer = rest.length ? Buffer.concat([frameBuffer, rest]) : frameBuffer;
|
|
1322
|
+
const payload = JSON.stringify({
|
|
1323
|
+
id: commandId,
|
|
1324
|
+
method: "Runtime.evaluate",
|
|
1325
|
+
params: {
|
|
1326
|
+
expression: options.expression,
|
|
1327
|
+
awaitPromise: false,
|
|
1328
|
+
returnByValue: true,
|
|
1329
|
+
},
|
|
1330
|
+
});
|
|
1331
|
+
socket.write(encodeWebSocketFrame(payload));
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
frameBuffer = Buffer.concat([frameBuffer, chunk]);
|
|
1335
|
+
}
|
|
1336
|
+
const decoded = decodeWebSocketFrames(frameBuffer);
|
|
1337
|
+
frameBuffer = Buffer.from(decoded.remaining);
|
|
1338
|
+
for (const message of decoded.messages) {
|
|
1339
|
+
const parsed = JSON.parse(message);
|
|
1340
|
+
if (parsed.id === commandId) {
|
|
1341
|
+
cleanup();
|
|
1342
|
+
if (parsed.error) {
|
|
1343
|
+
reject(new Error(`Inspector Runtime.evaluate failed: ${JSON.stringify(parsed.error)}`));
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
resolve();
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
catch (error) {
|
|
1352
|
+
cleanup();
|
|
1353
|
+
reject(error);
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
function extractJsonFromCloudFoundryLogLine(line) {
|
|
1359
|
+
const jsonStart = line.indexOf("{");
|
|
1360
|
+
if (jsonStart < 0)
|
|
1361
|
+
return undefined;
|
|
1362
|
+
try {
|
|
1363
|
+
return JSON.parse(line.slice(jsonStart));
|
|
1364
|
+
}
|
|
1365
|
+
catch {
|
|
1366
|
+
return undefined;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
function parseHttpWatchAppLine(line) {
|
|
1370
|
+
if (!line.includes("[APP/") || !line.includes("OUT"))
|
|
1371
|
+
return undefined;
|
|
1372
|
+
const payload = extractJsonFromCloudFoundryLogLine(line);
|
|
1373
|
+
if (!payload)
|
|
1374
|
+
return undefined;
|
|
1375
|
+
const msg = String(payload.msg ?? "");
|
|
1376
|
+
const methodMatch = msg.match(/\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+([^\s{]+)/);
|
|
1377
|
+
if (!methodMatch)
|
|
1378
|
+
return undefined;
|
|
1379
|
+
return {
|
|
1380
|
+
source: "APP",
|
|
1381
|
+
method: methodMatch[1],
|
|
1382
|
+
url: methodMatch[2],
|
|
1383
|
+
requestId: String(payload.request_id ?? payload.x_vcap_request_id ?? payload.x_request_id ?? ""),
|
|
1384
|
+
correlationId: String(payload.correlation_id ?? payload.x_correlationid ?? payload.x_correlation_id ?? ""),
|
|
1385
|
+
instance: String(payload.x_cf_instanceindex ?? payload.component_instance ?? ""),
|
|
1386
|
+
user: String(payload.remote_user ?? ""),
|
|
1387
|
+
tenant: String(payload.tenant_subdomain ?? payload.tenantid ?? payload.tenant_id ?? ""),
|
|
1388
|
+
userAgent: String(payload.user_agent ?? ""),
|
|
1389
|
+
contentLength: String(payload.content_length ?? payload.request_size_b ?? ""),
|
|
1390
|
+
authorization: String(payload.authorization ?? ""),
|
|
1391
|
+
message: msg,
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function parseKeyValueFromRouterLine(line, key) {
|
|
1395
|
+
const regex = new RegExp(`${key}:"([^"]*)"`);
|
|
1396
|
+
return line.match(regex)?.[1];
|
|
1397
|
+
}
|
|
1398
|
+
function parseHttpWatchRouterLine(line) {
|
|
1399
|
+
if (!line.includes("[RTR/") || !line.includes("HTTP/"))
|
|
1400
|
+
return undefined;
|
|
1401
|
+
const requestMatch = line.match(/"(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+([^\s]+)\s+HTTP\/[^"]+"\s+(\d{3})\s+(\d+)\s+(\d+)/);
|
|
1402
|
+
if (!requestMatch)
|
|
1403
|
+
return undefined;
|
|
1404
|
+
const responseTimeSeconds = Number(line.match(/response_time:([0-9.]+)/)?.[1] ?? "");
|
|
1405
|
+
return {
|
|
1406
|
+
source: "RTR",
|
|
1407
|
+
method: requestMatch[1],
|
|
1408
|
+
url: requestMatch[2],
|
|
1409
|
+
status: requestMatch[3],
|
|
1410
|
+
requestBytes: requestMatch[4],
|
|
1411
|
+
responseBytes: requestMatch[5],
|
|
1412
|
+
durationMs: Number.isFinite(responseTimeSeconds) ? Math.round(responseTimeSeconds * 1000) : undefined,
|
|
1413
|
+
requestId: parseKeyValueFromRouterLine(line, "vcap_request_id"),
|
|
1414
|
+
correlationId: parseKeyValueFromRouterLine(line, "x_correlationid"),
|
|
1415
|
+
instance: line.match(/app_index:"([^"]*)"/)?.[1],
|
|
1416
|
+
tenant: parseKeyValueFromRouterLine(line, "tenantid"),
|
|
1417
|
+
userAgent: line.match(/"\s+"([^"]*)"\s+"[^\"]+:\d+"/)?.[1],
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
function parseHttpWatchLine(line) {
|
|
1421
|
+
return parseHttpWatchAppLine(line) ?? parseHttpWatchRouterLine(line);
|
|
1422
|
+
}
|
|
1423
|
+
function formatHttpWatchEvent(appName, event) {
|
|
1424
|
+
const status = event.status ? chalk_1.default.green(String(event.status)) : chalk_1.default.gray("APP");
|
|
1425
|
+
const duration = event.durationMs !== undefined ? chalk_1.default.gray(`${event.durationMs}ms`) : "";
|
|
1426
|
+
const source = event.source === "RTR" ? chalk_1.default.magenta("RTR") : chalk_1.default.blue("APP");
|
|
1427
|
+
const requestId = event.requestId ? chalk_1.default.gray(` req=${event.requestId}`) : "";
|
|
1428
|
+
const instance = event.instance ? chalk_1.default.gray(` i=${event.instance}`) : "";
|
|
1429
|
+
const user = event.user ? chalk_1.default.gray(` user=${event.user}`) : "";
|
|
1430
|
+
const tenant = event.tenant ? chalk_1.default.gray(` tenant=${event.tenant}`) : "";
|
|
1431
|
+
const size = event.contentLength || event.requestBytes ? chalk_1.default.gray(` bytes=${event.contentLength || event.requestBytes}`) : "";
|
|
1432
|
+
const auth = event.authorization ? chalk_1.default.gray(` auth=${event.authorization}`) : "";
|
|
1433
|
+
return `${source} ${chalk_1.default.cyan(`[${appName}]`)} ${status} ${chalk_1.default.bold(event.method ?? "")} ${event.url ?? ""} ${duration}${instance}${user}${tenant}${size}${auth}${requestId}`.trim();
|
|
1434
|
+
}
|
|
1435
|
+
function printHttpWatchLine(appName, line, outputFile) {
|
|
1436
|
+
const event = parseHttpWatchLine(line);
|
|
1437
|
+
if (!event)
|
|
1438
|
+
return;
|
|
1439
|
+
const formatted = formatHttpWatchEvent(appName, event);
|
|
1440
|
+
console.log(formatted);
|
|
1441
|
+
if (outputFile) {
|
|
1442
|
+
const plain = formatted.replace(/\u001b\[[0-9;]*m/g, "");
|
|
1443
|
+
fs_extra_1.default.appendFileSync(outputFile, `${plain}\n`, "utf8");
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
async function resolveHttpWatchApps(options) {
|
|
1447
|
+
if (options.app?.trim()) {
|
|
1448
|
+
return uniqueValues(options.app.split(","));
|
|
1449
|
+
}
|
|
1450
|
+
return resolveRequestTraceApps({ app: options.app, refresh: options.refresh });
|
|
1451
|
+
}
|
|
1452
|
+
async function runHttpWatchForApps(options) {
|
|
1453
|
+
if (!options.appNames.length)
|
|
1454
|
+
throw new Error("No app selected for HTTP watch");
|
|
1455
|
+
if (options.out) {
|
|
1456
|
+
await fs_extra_1.default.ensureDir(node_path_1.default.dirname(node_path_1.default.resolve(options.out)));
|
|
1457
|
+
await fs_extra_1.default.writeFile(options.out, "", "utf8");
|
|
1458
|
+
}
|
|
1459
|
+
if (options.recent) {
|
|
1460
|
+
for (const appName of options.appNames) {
|
|
1461
|
+
const result = await (0, process_1.runCommand)("cf", ["logs", appName, "--recent"]);
|
|
1462
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
1463
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1464
|
+
printHttpWatchLine(appName, line, options.out);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
const children = [];
|
|
1470
|
+
const stopAll = () => {
|
|
1471
|
+
for (const child of children) {
|
|
1472
|
+
if (!child.killed)
|
|
1473
|
+
child.kill();
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
process.once("SIGINT", () => {
|
|
1477
|
+
console.log(chalk_1.default.gray("\nStopping HTTP watch..."));
|
|
1478
|
+
stopAll();
|
|
1479
|
+
process.exit(0);
|
|
1480
|
+
});
|
|
1481
|
+
for (const appName of options.appNames) {
|
|
1482
|
+
const child = (0, node_child_process_1.spawn)("cf", ["logs", appName], {
|
|
1483
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1484
|
+
shell: false,
|
|
1485
|
+
windowsHide: true,
|
|
1486
|
+
});
|
|
1487
|
+
children.push(child);
|
|
1488
|
+
child.stdout.on("data", (chunk) => {
|
|
1489
|
+
for (const line of chunk.toString("utf8").split(/\r?\n/)) {
|
|
1490
|
+
printHttpWatchLine(appName, line, options.out);
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
child.stderr.on("data", (chunk) => {
|
|
1494
|
+
for (const line of chunk.toString("utf8").split(/\r?\n/)) {
|
|
1495
|
+
printHttpWatchLine(appName, line, options.out);
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
console.log(chalk_1.default.green(`HTTP watch is watching ${options.appNames.length} app(s).`));
|
|
1500
|
+
console.log(chalk_1.default.gray("This uses existing CF/CDS/RTR logs. It shows method/path/status/user/tenant/size, but not full request body or full token."));
|
|
1501
|
+
console.log(chalk_1.default.gray("Press Ctrl+C to stop."));
|
|
1502
|
+
await new Promise((resolve) => {
|
|
1503
|
+
let closedCount = 0;
|
|
1504
|
+
for (const child of children) {
|
|
1505
|
+
child.on("close", () => {
|
|
1506
|
+
closedCount += 1;
|
|
1507
|
+
if (closedCount >= children.length)
|
|
1508
|
+
resolve();
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
async function runHttpWatchCommand(options) {
|
|
1514
|
+
if (!options.skipOrgSelect) {
|
|
1515
|
+
await maybeSwitchCloudFoundryTargetForDebug({ app: options.app, refresh: options.refresh, skipOrgSelect: false });
|
|
1516
|
+
}
|
|
1517
|
+
await ensureCloudFoundrySessionFromCache();
|
|
1518
|
+
const appNames = await resolveHttpWatchApps(options);
|
|
1519
|
+
await runHttpWatchForApps({ appNames, recent: options.recent, out: options.out });
|
|
1520
|
+
}
|
|
1521
|
+
async function runRequestTraceDoctorCommand(options) {
|
|
1522
|
+
await maybeSwitchCloudFoundryTargetForDebug({
|
|
1523
|
+
app: options.app,
|
|
1524
|
+
refresh: options.refresh,
|
|
1525
|
+
instance: options.instance,
|
|
1526
|
+
process: options.process,
|
|
1527
|
+
localPort: options.localPort,
|
|
1528
|
+
remotePort: options.remotePort,
|
|
1529
|
+
skipOrgSelect: options.skipOrgSelect,
|
|
1530
|
+
});
|
|
1531
|
+
await ensureCloudFoundrySessionFromCache();
|
|
1532
|
+
const appNames = await resolveRequestTraceApps({ app: options.app, refresh: options.refresh });
|
|
1533
|
+
const instanceIndex = await selectDebugInstance({ instance: options.instance });
|
|
1534
|
+
for (const appName of appNames) {
|
|
1535
|
+
console.log(chalk_1.default.cyan(`\nRequest trace doctor for ${appName} instance ${instanceIndex}`));
|
|
1536
|
+
console.log(chalk_1.default.gray("Recent router/app HTTP traffic:"));
|
|
1537
|
+
const result = await (0, process_1.runCommand)("cf", ["logs", appName, "--recent"]);
|
|
1538
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
1539
|
+
let count = 0;
|
|
1540
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1541
|
+
const event = parseHttpWatchLine(line);
|
|
1542
|
+
if (event) {
|
|
1543
|
+
count += 1;
|
|
1544
|
+
console.log(formatHttpWatchEvent(appName, event));
|
|
1545
|
+
if (count >= 10)
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (!count) {
|
|
1550
|
+
console.log(chalk_1.default.yellow("No recent HTTP traffic found in CF logs for this app."));
|
|
1551
|
+
}
|
|
1552
|
+
console.log(chalk_1.default.gray("\nRemote process list:"));
|
|
1553
|
+
const processList = await (0, process_1.runCommand)("cf", ["ssh", appName, "-i", instanceIndex, "-T", "-c", "ps -eo pid,args 2>/dev/null | head -n 40"]);
|
|
1554
|
+
if (processList.stdout)
|
|
1555
|
+
console.log(processList.stdout);
|
|
1556
|
+
if (processList.stderr)
|
|
1557
|
+
console.error(processList.stderr);
|
|
1558
|
+
}
|
|
1559
|
+
console.log(chalk_1.default.yellow("\nDoctor summary:"));
|
|
1560
|
+
console.log("- If HTTP traffic appears above, the app is receiving requests.");
|
|
1561
|
+
console.log("- Full body/token are not available from CF/CDS logs because they are intentionally omitted or masked.");
|
|
1562
|
+
console.log("- Use smdg cf http-watch for stable live tracking.");
|
|
1563
|
+
console.log("- Use deep request-trace only when you accept Inspector/preload limitations in dev/test.");
|
|
1564
|
+
}
|
|
1565
|
+
function buildRequestTraceInjectionExpression(options) {
|
|
1566
|
+
const traceOptions = JSON.stringify(options);
|
|
1567
|
+
const source = `(() => {
|
|
1568
|
+
const options = ${traceOptions};
|
|
1569
|
+
const globalKey = "__SMDG_NETWORK_SPY__";
|
|
1570
|
+
|
|
1571
|
+
const state = globalThis[globalKey] || {
|
|
1572
|
+
installed: false,
|
|
1573
|
+
requestSeq: 0,
|
|
1574
|
+
options,
|
|
1575
|
+
patchedRequests: new WeakSet(),
|
|
1576
|
+
patchedResponses: new WeakSet(),
|
|
1577
|
+
patchedServers: new WeakSet(),
|
|
1578
|
+
activeRequests: new WeakMap(),
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
state.options = options;
|
|
1582
|
+
globalThis[globalKey] = state;
|
|
1583
|
+
|
|
1584
|
+
function write(event) {
|
|
1585
|
+
try {
|
|
1586
|
+
console.log("SMDG_REQUEST_TRACE " + JSON.stringify(event));
|
|
1587
|
+
} catch (error) {
|
|
1588
|
+
console.log("SMDG_REQUEST_TRACE " + JSON.stringify({
|
|
1589
|
+
type: "smdg-request-trace-error",
|
|
1590
|
+
app: options.appName,
|
|
1591
|
+
message: error && error.message ? error.message : String(error),
|
|
1592
|
+
}));
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function currentOptions() {
|
|
1597
|
+
return globalThis[globalKey] && globalThis[globalKey].options ? globalThis[globalKey].options : options;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function shouldCaptureBody() {
|
|
1601
|
+
const mode = currentOptions().mode;
|
|
1602
|
+
return mode === "body" || mode === "response";
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function shouldCaptureResponse() {
|
|
1606
|
+
return currentOptions().mode === "response";
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function maxBodyBytes() {
|
|
1610
|
+
return Number(currentOptions().maxBodyBytes || 20000);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function maskAuthorization(value) {
|
|
1614
|
+
if (!value) return undefined;
|
|
1615
|
+
const authMode = currentOptions().authMode;
|
|
1616
|
+
if (authMode === "omit") return undefined;
|
|
1617
|
+
if (authMode === "full") return String(value);
|
|
1618
|
+
const text = String(value);
|
|
1619
|
+
return text.length <= 24 ? "***" : text.slice(0, 16) + "..." + text.slice(-8);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function normalizeHeaders(headers) {
|
|
1623
|
+
if (currentOptions().mode === "path") return undefined;
|
|
1624
|
+
const output = {};
|
|
1625
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
1626
|
+
const lower = key.toLowerCase();
|
|
1627
|
+
if (lower === "authorization") {
|
|
1628
|
+
const auth = maskAuthorization(value);
|
|
1629
|
+
if (auth !== undefined) output[key] = auth;
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
if (lower === "cookie" || lower === "set-cookie") {
|
|
1633
|
+
output[key] = "***";
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
output[key] = value;
|
|
1637
|
+
}
|
|
1638
|
+
return output;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function appendChunk(record, chunk) {
|
|
1642
|
+
if (!chunk || !shouldCaptureBody()) return;
|
|
1643
|
+
try {
|
|
1644
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
1645
|
+
record.requestBytes += buffer.length;
|
|
1646
|
+
const currentBytes = record.requestChunks.reduce((sum, item) => sum + item.length, 0);
|
|
1647
|
+
const limit = maxBodyBytes();
|
|
1648
|
+
if (currentBytes < limit) {
|
|
1649
|
+
record.requestChunks.push(buffer.subarray(0, Math.max(0, limit - currentBytes)));
|
|
1650
|
+
}
|
|
1651
|
+
} catch {}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function appendResponseChunk(record, chunk) {
|
|
1655
|
+
if (!chunk || !shouldCaptureResponse()) return;
|
|
1656
|
+
try {
|
|
1657
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
1658
|
+
record.responseBytes += buffer.length;
|
|
1659
|
+
const currentBytes = record.responseChunks.reduce((sum, item) => sum + item.length, 0);
|
|
1660
|
+
const limit = maxBodyBytes();
|
|
1661
|
+
if (currentBytes < limit) {
|
|
1662
|
+
record.responseChunks.push(buffer.subarray(0, Math.max(0, limit - currentBytes)));
|
|
1663
|
+
}
|
|
1664
|
+
} catch {}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function chunksToText(chunks) {
|
|
1668
|
+
try {
|
|
1669
|
+
if (!chunks || !chunks.length) return undefined;
|
|
1670
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1671
|
+
} catch {
|
|
1672
|
+
return undefined;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function tryParseContent(text, headers) {
|
|
1677
|
+
if (text === undefined) return undefined;
|
|
1678
|
+
if (!currentOptions().parseBodyJson) return text;
|
|
1679
|
+
const contentType = String((headers && (headers["content-type"] || headers["Content-Type"])) || "");
|
|
1680
|
+
if (contentType.includes("application/json") || /^[\\s]*[\\{\\[]/.test(text)) {
|
|
1681
|
+
try { return JSON.parse(text); } catch { return text; }
|
|
1682
|
+
}
|
|
1683
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1684
|
+
try { return Object.fromEntries(new URLSearchParams(text)); } catch { return text; }
|
|
1685
|
+
}
|
|
1686
|
+
return text;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function getRequestUrl(req) {
|
|
1690
|
+
return req.originalUrl || req.url || req.path || "";
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function patchRequestAndResponse(req, res, source) {
|
|
1694
|
+
if (!req || !res || state.patchedRequests.has(req)) return false;
|
|
1695
|
+
|
|
1696
|
+
state.patchedRequests.add(req);
|
|
1697
|
+
const record = {
|
|
1698
|
+
id: ++state.requestSeq,
|
|
1699
|
+
source,
|
|
1700
|
+
startedAt: Date.now(),
|
|
1701
|
+
requestChunks: [],
|
|
1702
|
+
responseChunks: [],
|
|
1703
|
+
requestBytes: 0,
|
|
1704
|
+
responseBytes: 0,
|
|
1705
|
+
};
|
|
1706
|
+
state.activeRequests.set(req, record);
|
|
1707
|
+
|
|
1708
|
+
try {
|
|
1709
|
+
if (!req.__SMDG_NETWORK_SPY_PUSH_PATCHED__) {
|
|
1710
|
+
const originalPush = req.push;
|
|
1711
|
+
if (typeof originalPush === "function") {
|
|
1712
|
+
req.push = function smdgNetworkTraceRequestPush(chunk, encoding) {
|
|
1713
|
+
appendChunk(record, chunk);
|
|
1714
|
+
return originalPush.call(this, chunk, encoding);
|
|
1715
|
+
};
|
|
1716
|
+
Object.defineProperty(req, "__SMDG_NETWORK_SPY_PUSH_PATCHED__", { value: true, enumerable: false });
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
} catch {}
|
|
1720
|
+
|
|
1721
|
+
try {
|
|
1722
|
+
const originalEmit = req.emit;
|
|
1723
|
+
if (typeof originalEmit === "function" && !req.__SMDG_NETWORK_SPY_EMIT_PATCHED__) {
|
|
1724
|
+
req.emit = function smdgNetworkTraceRequestEmit(eventName, chunk, ...args) {
|
|
1725
|
+
if (eventName === "data") appendChunk(record, chunk);
|
|
1726
|
+
return originalEmit.call(this, eventName, chunk, ...args);
|
|
1727
|
+
};
|
|
1728
|
+
Object.defineProperty(req, "__SMDG_NETWORK_SPY_EMIT_PATCHED__", { value: true, enumerable: false });
|
|
1729
|
+
}
|
|
1730
|
+
} catch {}
|
|
1731
|
+
|
|
1732
|
+
try {
|
|
1733
|
+
if (!state.patchedResponses.has(res)) {
|
|
1734
|
+
state.patchedResponses.add(res);
|
|
1735
|
+
const originalWrite = res.write;
|
|
1736
|
+
const originalEnd = res.end;
|
|
1737
|
+
|
|
1738
|
+
if (typeof originalWrite === "function") {
|
|
1739
|
+
res.write = function smdgNetworkTraceResponseWrite(chunk, ...args) {
|
|
1740
|
+
appendResponseChunk(record, chunk);
|
|
1741
|
+
return originalWrite.call(this, chunk, ...args);
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
if (typeof originalEnd === "function") {
|
|
1746
|
+
res.end = function smdgNetworkTraceResponseEnd(chunk, ...args) {
|
|
1747
|
+
appendResponseChunk(record, chunk);
|
|
1748
|
+
return originalEnd.call(this, chunk, ...args);
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
} catch {}
|
|
1753
|
+
|
|
1754
|
+
const finish = () => {
|
|
1755
|
+
try {
|
|
1756
|
+
const requestBodyText = chunksToText(record.requestChunks);
|
|
1757
|
+
const responseBodyText = chunksToText(record.responseChunks);
|
|
1758
|
+
const headers = req.headers || {};
|
|
1759
|
+
const event = {
|
|
1760
|
+
type: "smdg-request-trace",
|
|
1761
|
+
app: currentOptions().appName,
|
|
1762
|
+
source: record.source,
|
|
1763
|
+
id: record.id,
|
|
1764
|
+
timestamp: new Date(record.startedAt).toISOString(),
|
|
1765
|
+
method: req.method,
|
|
1766
|
+
url: getRequestUrl(req),
|
|
1767
|
+
status: res.statusCode,
|
|
1768
|
+
durationMs: Date.now() - record.startedAt,
|
|
1769
|
+
requestBytes: record.requestBytes,
|
|
1770
|
+
responseBytes: record.responseBytes,
|
|
1771
|
+
headers: normalizeHeaders(headers),
|
|
1772
|
+
body: shouldCaptureBody() ? tryParseContent(requestBodyText, headers) : undefined,
|
|
1773
|
+
responseBody: shouldCaptureResponse() ? tryParseContent(responseBodyText, res.getHeaders ? res.getHeaders() : {}) : undefined,
|
|
1774
|
+
};
|
|
1775
|
+
write(event);
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
write({
|
|
1778
|
+
type: "smdg-request-trace-error",
|
|
1779
|
+
app: currentOptions().appName,
|
|
1780
|
+
message: error && error.message ? error.message : String(error),
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
if (typeof res.once === "function") {
|
|
1786
|
+
res.once("finish", finish);
|
|
1787
|
+
res.once("close", () => {
|
|
1788
|
+
if (!res.writableEnded) finish();
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
return true;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function installDiagnosticsChannelHook() {
|
|
1796
|
+
try {
|
|
1797
|
+
const diagnostics = require("diagnostics_channel");
|
|
1798
|
+
if (!diagnostics || diagnostics.__SMDG_NETWORK_SPY_PATCHED__) return false;
|
|
1799
|
+
const requestStart = diagnostics.channel("http.server.request.start");
|
|
1800
|
+
requestStart.subscribe((message) => {
|
|
1801
|
+
const req = message && (message.request || message.req);
|
|
1802
|
+
const res = message && (message.response || message.res);
|
|
1803
|
+
patchRequestAndResponse(req, res, "diagnostics_channel:http.server.request.start");
|
|
1804
|
+
});
|
|
1805
|
+
Object.defineProperty(diagnostics, "__SMDG_NETWORK_SPY_PATCHED__", { value: true, enumerable: false });
|
|
1806
|
+
return true;
|
|
1807
|
+
} catch {
|
|
1808
|
+
return false;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function installServerEmitHook() {
|
|
1813
|
+
try {
|
|
1814
|
+
const http = require("http");
|
|
1815
|
+
const Server = http && http.Server;
|
|
1816
|
+
if (!Server || !Server.prototype || Server.prototype.__SMDG_NETWORK_SPY_EMIT_PATCHED__) return false;
|
|
1817
|
+
const originalEmit = Server.prototype.emit;
|
|
1818
|
+
Server.prototype.emit = function smdgNetworkTraceServerEmit(eventName, req, res, ...args) {
|
|
1819
|
+
if (eventName === "request") patchRequestAndResponse(req, res, "http.Server.emit");
|
|
1820
|
+
return originalEmit.call(this, eventName, req, res, ...args);
|
|
1821
|
+
};
|
|
1822
|
+
Object.defineProperty(Server.prototype, "__SMDG_NETWORK_SPY_EMIT_PATCHED__", { value: true, enumerable: false });
|
|
1823
|
+
return true;
|
|
1824
|
+
} catch {
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function installCreateServerHook(moduleName) {
|
|
1830
|
+
try {
|
|
1831
|
+
const mod = require(moduleName);
|
|
1832
|
+
if (!mod || mod.__SMDG_NETWORK_SPY_CREATE_SERVER_PATCHED__) return false;
|
|
1833
|
+
const originalCreateServer = mod.createServer;
|
|
1834
|
+
if (typeof originalCreateServer !== "function") return false;
|
|
1835
|
+
mod.createServer = function smdgNetworkTraceCreateServer(...args) {
|
|
1836
|
+
const server = originalCreateServer.apply(this, args);
|
|
1837
|
+
hookServer(server, moduleName + ".createServer");
|
|
1838
|
+
return server;
|
|
1839
|
+
};
|
|
1840
|
+
Object.defineProperty(mod, "__SMDG_NETWORK_SPY_CREATE_SERVER_PATCHED__", { value: true, enumerable: false });
|
|
1841
|
+
return true;
|
|
1842
|
+
} catch {
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function hookServer(server, source) {
|
|
1848
|
+
try {
|
|
1849
|
+
if (!server || state.patchedServers.has(server)) return false;
|
|
1850
|
+
if (typeof server.prependListener === "function") {
|
|
1851
|
+
server.prependListener("request", (req, res) => patchRequestAndResponse(req, res, source));
|
|
1852
|
+
state.patchedServers.add(server);
|
|
1853
|
+
return true;
|
|
1854
|
+
}
|
|
1855
|
+
} catch {}
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function hookActiveServers() {
|
|
1860
|
+
let count = 0;
|
|
1861
|
+
try {
|
|
1862
|
+
const handles = typeof process._getActiveHandles === "function" ? process._getActiveHandles() : [];
|
|
1863
|
+
for (const handle of handles) {
|
|
1864
|
+
if (handle && typeof handle.on === "function" && typeof handle.address === "function") {
|
|
1865
|
+
if (hookServer(handle, "active-handle")) count += 1;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
} catch {}
|
|
1869
|
+
return count;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
const diagnosticsHooked = installDiagnosticsChannelHook();
|
|
1873
|
+
const serverEmitHooked = installServerEmitHook();
|
|
1874
|
+
const httpCreateHooked = installCreateServerHook("http");
|
|
1875
|
+
const httpsCreateHooked = installCreateServerHook("https");
|
|
1876
|
+
const activeServers = hookActiveServers();
|
|
1877
|
+
|
|
1878
|
+
state.installed = true;
|
|
1879
|
+
state.installedAt = state.installedAt || new Date().toISOString();
|
|
1880
|
+
|
|
1881
|
+
write({
|
|
1882
|
+
type: "smdg-request-trace-status",
|
|
1883
|
+
app: options.appName,
|
|
1884
|
+
status: "installed",
|
|
1885
|
+
engine: "network-trace-v4",
|
|
1886
|
+
diagnosticsHooked,
|
|
1887
|
+
serverEmitHooked,
|
|
1888
|
+
httpCreateHooked,
|
|
1889
|
+
httpsCreateHooked,
|
|
1890
|
+
activeServers,
|
|
1891
|
+
mode: options.mode,
|
|
1892
|
+
authMode: options.authMode,
|
|
1893
|
+
maxBodyBytes: options.maxBodyBytes,
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
return "installed:network-trace-v4:" + activeServers;
|
|
1897
|
+
})();`;
|
|
1898
|
+
return source;
|
|
1899
|
+
}
|
|
1900
|
+
async function selectRequestTraceMode() {
|
|
1901
|
+
return (0, prompts_2.searchableSelectChoice)({
|
|
1902
|
+
message: "Select request trace mode",
|
|
1903
|
+
choices: [
|
|
1904
|
+
{ title: "Path only", value: "path", description: "method, URL, status, duration" },
|
|
1905
|
+
{ title: "Headers", value: "headers", description: "include request headers, mask sensitive values" },
|
|
1906
|
+
{ title: "Headers + body", value: "body", description: "include request body up to a safe size limit" },
|
|
1907
|
+
{ title: "Headers + body + response", value: "response", description: "include request body and response body" },
|
|
1908
|
+
],
|
|
1909
|
+
allowCustomValue: false,
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
async function selectRequestTraceAuthMode() {
|
|
1913
|
+
return (0, prompts_2.searchableSelectChoice)({
|
|
1914
|
+
message: "Authorization header handling",
|
|
1915
|
+
choices: [
|
|
1916
|
+
{ title: "Mask token (recommended)", value: "mask" },
|
|
1917
|
+
{ title: "Show full token (dev/test only)", value: "full" },
|
|
1918
|
+
{ title: "Omit Authorization header", value: "omit" },
|
|
1919
|
+
],
|
|
1920
|
+
allowCustomValue: false,
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
async function selectRequestTraceDisplayOptions(options) {
|
|
1924
|
+
const headerPreset = await (0, prompts_2.searchableSelectChoice)({
|
|
1925
|
+
message: "Headers to display in terminal",
|
|
1926
|
+
choices: [
|
|
1927
|
+
{ title: "Minimal headers", value: "minimal", description: "host, content-type, authorization, request/correlation ids" },
|
|
1928
|
+
{ title: "Common debug headers", value: "common", description: "minimal + user-agent, origin, forwarded, CF/B3 headers" },
|
|
1929
|
+
{ title: "All captured headers", value: "all", description: "large output" },
|
|
1930
|
+
{ title: "Custom header list", value: "custom", description: "enter comma-separated headers" },
|
|
1931
|
+
],
|
|
1932
|
+
allowCustomValue: false,
|
|
1933
|
+
});
|
|
1934
|
+
let headerNames = [];
|
|
1935
|
+
if (headerPreset === "minimal")
|
|
1936
|
+
headerNames = getMinimalTraceHeaderNames();
|
|
1937
|
+
if (headerPreset === "common")
|
|
1938
|
+
headerNames = getCommonTraceHeaderNames();
|
|
1939
|
+
if (headerPreset === "custom") {
|
|
1940
|
+
const response = await (0, prompts_1.default)({
|
|
1941
|
+
type: "text",
|
|
1942
|
+
name: "headers",
|
|
1943
|
+
message: "Header names to display",
|
|
1944
|
+
initial: "authorization,content-type,content-length,x-correlationid,x-vcap-request-id,tenantid,user-agent",
|
|
1945
|
+
validate: (value) => value.trim() ? true : "At least one header is required",
|
|
1946
|
+
});
|
|
1947
|
+
headerNames = String(response.headers ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
1948
|
+
}
|
|
1949
|
+
const parseResponse = await (0, prompts_1.default)({
|
|
1950
|
+
type: "select",
|
|
1951
|
+
name: "parseBodyJson",
|
|
1952
|
+
message: "Try parse request/response body as JSON when possible?",
|
|
1953
|
+
choices: [
|
|
1954
|
+
{ title: "Yes, parse JSON/form body when possible", value: true },
|
|
1955
|
+
{ title: "No, keep raw body string", value: false },
|
|
1956
|
+
],
|
|
1957
|
+
initial: 0,
|
|
1958
|
+
});
|
|
1959
|
+
let outputFile = options.out;
|
|
1960
|
+
if (!outputFile) {
|
|
1961
|
+
const outResponse = await (0, prompts_1.default)({
|
|
1962
|
+
type: "select",
|
|
1963
|
+
name: "export",
|
|
1964
|
+
message: "Export captured trace events to JSONL file?",
|
|
1965
|
+
choices: [
|
|
1966
|
+
{ title: "No", value: false },
|
|
1967
|
+
{ title: "Yes", value: true },
|
|
1968
|
+
],
|
|
1969
|
+
initial: 0,
|
|
1970
|
+
});
|
|
1971
|
+
if (outResponse.export) {
|
|
1972
|
+
const fileResponse = await (0, prompts_1.default)({
|
|
1973
|
+
type: "text",
|
|
1974
|
+
name: "file",
|
|
1975
|
+
message: "Trace output file",
|
|
1976
|
+
initial: `smdg-request-trace-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`,
|
|
1977
|
+
validate: (value) => value.trim() ? true : "Output file is required",
|
|
1978
|
+
});
|
|
1979
|
+
outputFile = String(fileResponse.file ?? "").trim();
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (outputFile) {
|
|
1983
|
+
await fs_extra_1.default.ensureDir(node_path_1.default.dirname(node_path_1.default.resolve(outputFile)));
|
|
1984
|
+
await fs_extra_1.default.writeFile(outputFile, "", "utf8");
|
|
1985
|
+
console.log(chalk_1.default.green(`Trace events will be exported to ${node_path_1.default.resolve(outputFile)}`));
|
|
1986
|
+
}
|
|
1987
|
+
return {
|
|
1988
|
+
headerPreset,
|
|
1989
|
+
headerNames,
|
|
1990
|
+
parseBodyJson: Boolean(parseResponse.parseBodyJson),
|
|
1991
|
+
outputFile,
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
async function resolveRequestTraceApps(options) {
|
|
1995
|
+
if (options.app?.trim()) {
|
|
1996
|
+
return uniqueValues(options.app.split(","));
|
|
1997
|
+
}
|
|
1998
|
+
const apps = await getAppsWithCache({ refresh: options.refresh, startBackgroundRefresh: !options.refresh });
|
|
1999
|
+
const selectedApps = [];
|
|
2000
|
+
while (true) {
|
|
2001
|
+
const appName = await (0, prompts_2.searchableSelectChoice)({
|
|
2002
|
+
message: selectedApps.length ? "Add another BTP app to trace, or finish" : "Search/select BTP app to trace",
|
|
2003
|
+
choices: [
|
|
2004
|
+
...apps
|
|
2005
|
+
.filter((app) => !selectedApps.includes(app.name))
|
|
2006
|
+
.map((app) => ({
|
|
2007
|
+
title: [app.name, app.requestedState, app.routes].filter(Boolean).join(" | "),
|
|
2008
|
+
value: app.name,
|
|
2009
|
+
})),
|
|
2010
|
+
...(selectedApps.length ? [{ title: "Done", value: "__DONE__" }] : []),
|
|
2011
|
+
],
|
|
2012
|
+
validateCustomValue: validateRequired,
|
|
2013
|
+
customValueTitle: (value) => `Use typed app name: ${value}`,
|
|
2014
|
+
});
|
|
2015
|
+
if (appName === "__DONE__") {
|
|
2016
|
+
break;
|
|
2017
|
+
}
|
|
2018
|
+
selectedApps.push(appName);
|
|
2019
|
+
await (0, cache_1.rememberSelectedApp)(appName);
|
|
2020
|
+
const moreResponse = await (0, prompts_1.default)({
|
|
2021
|
+
type: "select",
|
|
2022
|
+
name: "more",
|
|
2023
|
+
message: "Trace another app at the same time?",
|
|
2024
|
+
choices: [
|
|
2025
|
+
{ title: "No, start tracing now", value: false },
|
|
2026
|
+
{ title: "Yes, add another app", value: true },
|
|
2027
|
+
],
|
|
2028
|
+
initial: 0,
|
|
2029
|
+
});
|
|
2030
|
+
if (!moreResponse.more) {
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
return selectedApps;
|
|
2035
|
+
}
|
|
2036
|
+
function getMinimalTraceHeaderNames() {
|
|
2037
|
+
return [
|
|
2038
|
+
"host",
|
|
2039
|
+
"content-type",
|
|
2040
|
+
"content-length",
|
|
2041
|
+
"authorization",
|
|
2042
|
+
"x-correlation-id",
|
|
2043
|
+
"x-correlationid",
|
|
2044
|
+
"x-vcap-request-id",
|
|
2045
|
+
"tenantid",
|
|
2046
|
+
];
|
|
2047
|
+
}
|
|
2048
|
+
function getCommonTraceHeaderNames() {
|
|
2049
|
+
return [
|
|
2050
|
+
...getMinimalTraceHeaderNames(),
|
|
2051
|
+
"user-agent",
|
|
2052
|
+
"origin",
|
|
2053
|
+
"referer",
|
|
2054
|
+
"x-forwarded-for",
|
|
2055
|
+
"x-forwarded-host",
|
|
2056
|
+
"x-forwarded-path",
|
|
2057
|
+
"x-forwarded-proto",
|
|
2058
|
+
"x-cf-applicationid",
|
|
2059
|
+
"x-cf-instanceindex",
|
|
2060
|
+
"x-cf-true-client-ip",
|
|
2061
|
+
"x-b3-traceid",
|
|
2062
|
+
"x-b3-spanid",
|
|
2063
|
+
"b3",
|
|
2064
|
+
];
|
|
2065
|
+
}
|
|
2066
|
+
function normalizeHeaderName(value) {
|
|
2067
|
+
return value.trim().toLowerCase();
|
|
2068
|
+
}
|
|
2069
|
+
function filterTraceHeaders(headers, display) {
|
|
2070
|
+
if (!headers || typeof headers !== "object")
|
|
2071
|
+
return undefined;
|
|
2072
|
+
const source = headers;
|
|
2073
|
+
if (display.headerPreset === "all")
|
|
2074
|
+
return source;
|
|
2075
|
+
const names = new Set(display.headerNames.map(normalizeHeaderName));
|
|
2076
|
+
const output = {};
|
|
2077
|
+
for (const [key, value] of Object.entries(source)) {
|
|
2078
|
+
if (names.has(normalizeHeaderName(key)))
|
|
2079
|
+
output[key] = value;
|
|
2080
|
+
}
|
|
2081
|
+
return output;
|
|
2082
|
+
}
|
|
2083
|
+
function stringifyTraceValue(value) {
|
|
2084
|
+
if (value === undefined || value === null)
|
|
2085
|
+
return "";
|
|
2086
|
+
if (typeof value === "string")
|
|
2087
|
+
return value;
|
|
2088
|
+
try {
|
|
2089
|
+
return JSON.stringify(value);
|
|
2090
|
+
}
|
|
2091
|
+
catch {
|
|
2092
|
+
return String(value);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
function traceEventMatchesFilters(event, filters) {
|
|
2096
|
+
if (filters.paused)
|
|
2097
|
+
return false;
|
|
2098
|
+
const method = String(event.method ?? "").toLowerCase();
|
|
2099
|
+
const url = String(event.url ?? "").toLowerCase();
|
|
2100
|
+
const status = String(event.status ?? "").toLowerCase();
|
|
2101
|
+
const body = stringifyTraceValue(event.body).toLowerCase();
|
|
2102
|
+
const responseBody = stringifyTraceValue(event.responseBody).toLowerCase();
|
|
2103
|
+
const all = stringifyTraceValue(event).toLowerCase();
|
|
2104
|
+
if (filters.method && method !== filters.method.toLowerCase())
|
|
2105
|
+
return false;
|
|
2106
|
+
if (filters.path && !url.includes(filters.path.toLowerCase()))
|
|
2107
|
+
return false;
|
|
2108
|
+
if (filters.status && !status.includes(filters.status.toLowerCase()))
|
|
2109
|
+
return false;
|
|
2110
|
+
if (filters.body && !body.includes(filters.body.toLowerCase()) && !responseBody.includes(filters.body.toLowerCase()))
|
|
2111
|
+
return false;
|
|
2112
|
+
if (filters.text && !all.includes(filters.text.toLowerCase()))
|
|
2113
|
+
return false;
|
|
2114
|
+
return true;
|
|
2115
|
+
}
|
|
2116
|
+
function buildPrintableTracePayload(event, display) {
|
|
2117
|
+
const output = {
|
|
2118
|
+
type: event.type,
|
|
2119
|
+
app: event.app,
|
|
2120
|
+
source: event.source,
|
|
2121
|
+
id: event.id,
|
|
2122
|
+
timestamp: event.timestamp,
|
|
2123
|
+
method: event.method,
|
|
2124
|
+
url: event.url,
|
|
2125
|
+
status: event.status,
|
|
2126
|
+
durationMs: event.durationMs,
|
|
2127
|
+
requestBytes: event.requestBytes,
|
|
2128
|
+
responseBytes: event.responseBytes,
|
|
2129
|
+
};
|
|
2130
|
+
const headers = filterTraceHeaders(event.headers, display);
|
|
2131
|
+
if (headers && Object.keys(headers).length > 0)
|
|
2132
|
+
output.headers = headers;
|
|
2133
|
+
if (event.body !== undefined)
|
|
2134
|
+
output.body = event.body;
|
|
2135
|
+
if (event.responseBody !== undefined)
|
|
2136
|
+
output.responseBody = event.responseBody;
|
|
2137
|
+
return output;
|
|
2138
|
+
}
|
|
2139
|
+
function writeTraceEventToFile(outputFile, event) {
|
|
2140
|
+
if (!outputFile)
|
|
2141
|
+
return;
|
|
2142
|
+
try {
|
|
2143
|
+
fs_extra_1.default.appendFileSync(outputFile, `${JSON.stringify(event)}\n`, "utf8");
|
|
2144
|
+
}
|
|
2145
|
+
catch (error) {
|
|
2146
|
+
console.error(chalk_1.default.yellow(`Failed to write trace event to file: ${error instanceof Error ? error.message : String(error)}`));
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
function printRequestTraceEvent(appName, payload, runtime) {
|
|
2150
|
+
const type = String(payload.type ?? "smdg-request-trace");
|
|
2151
|
+
if (type === "smdg-request-trace-status") {
|
|
2152
|
+
console.log(chalk_1.default.green(`[${appName}] ${String(payload.status ?? "trace-status")}`));
|
|
2153
|
+
console.log(chalk_1.default.gray(`engine=${String(payload.engine ?? "unknown")} activeServers=${String(payload.activeServers ?? "?")} mode=${String(payload.mode ?? "")}`));
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
if (type === "smdg-request-trace-error") {
|
|
2157
|
+
console.log(chalk_1.default.red(`[${appName}] trace error: ${String(payload.message ?? "unknown")}`));
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
runtime.events.push(payload);
|
|
2161
|
+
writeTraceEventToFile(runtime.display.outputFile, payload);
|
|
2162
|
+
if (!traceEventMatchesFilters(payload, runtime.filters))
|
|
2163
|
+
return;
|
|
2164
|
+
const time = String(payload.timestamp ?? new Date().toISOString());
|
|
2165
|
+
const method = String(payload.method ?? "");
|
|
2166
|
+
const url = String(payload.url ?? "");
|
|
2167
|
+
const status = String(payload.status ?? "");
|
|
2168
|
+
const duration = String(payload.durationMs ?? "");
|
|
2169
|
+
console.log(chalk_1.default.cyan(`\n[${time}] [${appName}] ${method} ${url} → ${status} ${duration}ms`));
|
|
2170
|
+
console.log(JSON.stringify(buildPrintableTracePayload(payload, runtime.display), null, 2));
|
|
2171
|
+
}
|
|
2172
|
+
function printRequestTraceLine(appName, line, runtime) {
|
|
2173
|
+
const marker = line.includes("SMDG_REQUEST_TRACE ") ? "SMDG_REQUEST_TRACE " : line.includes("SMDG_REQUEST_SPY ") ? "SMDG_REQUEST_SPY " : undefined;
|
|
2174
|
+
if (!marker)
|
|
2175
|
+
return;
|
|
2176
|
+
const markerIndex = line.indexOf(marker);
|
|
2177
|
+
const payloadText = line.slice(markerIndex + marker.length).trim();
|
|
2178
|
+
try {
|
|
2179
|
+
const payload = JSON.parse(payloadText);
|
|
2180
|
+
printRequestTraceEvent(appName, payload, runtime);
|
|
2181
|
+
}
|
|
2182
|
+
catch {
|
|
2183
|
+
console.log(`[${appName}] ${payloadText}`);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
function printTraceRuntimeHelp() {
|
|
2187
|
+
console.log(chalk_1.default.gray("\nRuntime trace commands:"));
|
|
2188
|
+
console.log(chalk_1.default.gray(" /method POST show only one method"));
|
|
2189
|
+
console.log(chalk_1.default.gray(" /path text show only URLs containing text"));
|
|
2190
|
+
console.log(chalk_1.default.gray(" /body text show only request/response body containing text"));
|
|
2191
|
+
console.log(chalk_1.default.gray(" /status 500 show only status containing value"));
|
|
2192
|
+
console.log(chalk_1.default.gray(" /text value search anywhere in the event"));
|
|
2193
|
+
console.log(chalk_1.default.gray(" /headers a,b,c change displayed headers while running"));
|
|
2194
|
+
console.log(chalk_1.default.gray(" /headers all display all captured headers"));
|
|
2195
|
+
console.log(chalk_1.default.gray(" /clear clear active filters"));
|
|
2196
|
+
console.log(chalk_1.default.gray(" /show show active filters"));
|
|
2197
|
+
console.log(chalk_1.default.gray(" /replay print matching events already captured"));
|
|
2198
|
+
console.log(chalk_1.default.gray(" /pause or /resume pause/resume terminal display"));
|
|
2199
|
+
console.log(chalk_1.default.gray(" /help show this help"));
|
|
2200
|
+
}
|
|
2201
|
+
function applyTraceRuntimeCommand(input, runtime) {
|
|
2202
|
+
const trimmed = input.trim();
|
|
2203
|
+
if (!trimmed)
|
|
2204
|
+
return;
|
|
2205
|
+
if (!trimmed.startsWith("/")) {
|
|
2206
|
+
runtime.filters.text = trimmed;
|
|
2207
|
+
console.log(chalk_1.default.yellow(`Search text filter: ${trimmed}`));
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
const [commandRaw, ...restParts] = trimmed.slice(1).split(" ");
|
|
2211
|
+
const command = commandRaw.toLowerCase();
|
|
2212
|
+
const value = restParts.join(" ").trim();
|
|
2213
|
+
if (command === "method")
|
|
2214
|
+
runtime.filters.method = value || undefined;
|
|
2215
|
+
else if (command === "path")
|
|
2216
|
+
runtime.filters.path = value || undefined;
|
|
2217
|
+
else if (command === "body")
|
|
2218
|
+
runtime.filters.body = value || undefined;
|
|
2219
|
+
else if (command === "status")
|
|
2220
|
+
runtime.filters.status = value || undefined;
|
|
2221
|
+
else if (command === "text")
|
|
2222
|
+
runtime.filters.text = value || undefined;
|
|
2223
|
+
else if (command === "pause")
|
|
2224
|
+
runtime.filters.paused = true;
|
|
2225
|
+
else if (command === "resume")
|
|
2226
|
+
runtime.filters.paused = false;
|
|
2227
|
+
else if (command === "clear") {
|
|
2228
|
+
runtime.filters.method = undefined;
|
|
2229
|
+
runtime.filters.path = undefined;
|
|
2230
|
+
runtime.filters.body = undefined;
|
|
2231
|
+
runtime.filters.status = undefined;
|
|
2232
|
+
runtime.filters.text = undefined;
|
|
2233
|
+
runtime.filters.paused = false;
|
|
2234
|
+
}
|
|
2235
|
+
else if (command === "headers") {
|
|
2236
|
+
if (!value || value.toLowerCase() === "common") {
|
|
2237
|
+
runtime.display.headerPreset = "common";
|
|
2238
|
+
runtime.display.headerNames = getCommonTraceHeaderNames();
|
|
2239
|
+
}
|
|
2240
|
+
else if (value.toLowerCase() === "minimal") {
|
|
2241
|
+
runtime.display.headerPreset = "minimal";
|
|
2242
|
+
runtime.display.headerNames = getMinimalTraceHeaderNames();
|
|
2243
|
+
}
|
|
2244
|
+
else if (value.toLowerCase() === "all") {
|
|
2245
|
+
runtime.display.headerPreset = "all";
|
|
2246
|
+
runtime.display.headerNames = [];
|
|
2247
|
+
}
|
|
2248
|
+
else {
|
|
2249
|
+
runtime.display.headerPreset = "custom";
|
|
2250
|
+
runtime.display.headerNames = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
else if (command === "show") {
|
|
2254
|
+
console.log(chalk_1.default.gray(JSON.stringify({ filters: runtime.filters, display: runtime.display, captured: runtime.events.length }, null, 2)));
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
else if (command === "replay") {
|
|
2258
|
+
console.log(chalk_1.default.gray(`Replaying ${runtime.events.length} captured event(s) with current filters...`));
|
|
2259
|
+
for (const event of runtime.events) {
|
|
2260
|
+
if (traceEventMatchesFilters(event, runtime.filters)) {
|
|
2261
|
+
const appName = String(event.app ?? "app");
|
|
2262
|
+
const time = String(event.timestamp ?? "");
|
|
2263
|
+
console.log(chalk_1.default.cyan(`\n[${time}] [${appName}] ${String(event.method ?? "")} ${String(event.url ?? "")} → ${String(event.status ?? "")} ${String(event.durationMs ?? "")}ms`));
|
|
2264
|
+
console.log(JSON.stringify(buildPrintableTracePayload(event, runtime.display), null, 2));
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
else if (command === "help" || command === "?") {
|
|
2270
|
+
printTraceRuntimeHelp();
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
else {
|
|
2274
|
+
console.log(chalk_1.default.yellow(`Unknown runtime command: ${command}`));
|
|
2275
|
+
printTraceRuntimeHelp();
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
console.log(chalk_1.default.yellow(`Trace runtime updated: ${trimmed}`));
|
|
2279
|
+
}
|
|
2280
|
+
function attachTraceRuntimeCommands(runtime) {
|
|
2281
|
+
printTraceRuntimeHelp();
|
|
2282
|
+
process.stdin.setEncoding("utf8");
|
|
2283
|
+
process.stdin.resume();
|
|
2284
|
+
process.stdin.on("data", (chunk) => {
|
|
2285
|
+
for (const line of chunk.split(/\r?\n/)) {
|
|
2286
|
+
applyTraceRuntimeCommand(line, runtime);
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
function startRequestTraceLogStream(appName, runtime) {
|
|
2291
|
+
const childProcess = (0, node_child_process_1.spawn)("cf", ["logs", appName], {
|
|
2292
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2293
|
+
shell: false,
|
|
2294
|
+
windowsHide: true,
|
|
2295
|
+
});
|
|
2296
|
+
childProcess.stdout.on("data", (chunk) => {
|
|
2297
|
+
const lines = chunk.toString("utf8").split(/\r?\n/);
|
|
2298
|
+
for (const line of lines)
|
|
2299
|
+
printRequestTraceLine(appName, line, runtime);
|
|
2300
|
+
});
|
|
2301
|
+
childProcess.stderr.on("data", (chunk) => {
|
|
2302
|
+
const text = chunk.toString("utf8");
|
|
2303
|
+
if (/SMDG_REQUEST_(TRACE|SPY)/.test(text)) {
|
|
2304
|
+
for (const line of text.split(/\r?\n/))
|
|
2305
|
+
printRequestTraceLine(appName, line, runtime);
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
return childProcess;
|
|
2309
|
+
}
|
|
2310
|
+
async function runRequestTraceCommand(options) {
|
|
2311
|
+
await maybeSwitchCloudFoundryTargetForDebug({
|
|
2312
|
+
app: options.app,
|
|
2313
|
+
refresh: options.refresh,
|
|
2314
|
+
instance: options.instance,
|
|
2315
|
+
process: options.process,
|
|
2316
|
+
localPort: options.localPort,
|
|
2317
|
+
remotePort: options.remotePort,
|
|
2318
|
+
skipOrgSelect: options.skipOrgSelect,
|
|
2319
|
+
});
|
|
2320
|
+
await ensureCloudFoundrySessionFromCache();
|
|
2321
|
+
const appNames = await resolveRequestTraceApps(options);
|
|
2322
|
+
if (!appNames.length) {
|
|
2323
|
+
throw new Error("No app selected for request trace");
|
|
2324
|
+
}
|
|
2325
|
+
const engine = await (0, prompts_2.searchableSelectChoice)({
|
|
2326
|
+
message: "Select request trace engine",
|
|
2327
|
+
choices: [
|
|
2328
|
+
{
|
|
2329
|
+
title: "HTTP watch from existing CF/CDS logs (recommended, stable)",
|
|
2330
|
+
value: "http-watch",
|
|
2331
|
+
description: "Shows method/path/status/user/tenant/size. No restart and no source-code change.",
|
|
2332
|
+
},
|
|
2333
|
+
{
|
|
2334
|
+
title: "Deep Node Inspector trace (experimental body capture)",
|
|
2335
|
+
value: "inspector-trace",
|
|
2336
|
+
description: "Attempts runtime injection. May not work for every CAP runtime. Dev/test only.",
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
title: "Doctor: verify traffic, process, and limits",
|
|
2340
|
+
value: "doctor",
|
|
2341
|
+
},
|
|
2342
|
+
],
|
|
2343
|
+
allowCustomValue: false,
|
|
2344
|
+
});
|
|
2345
|
+
if (engine === "http-watch") {
|
|
2346
|
+
await runHttpWatchForApps({ appNames, recent: false, out: undefined });
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
if (engine === "doctor") {
|
|
2350
|
+
await runRequestTraceDoctorCommand({ ...options, app: appNames.join(",") });
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
const traceMode = await selectRequestTraceMode();
|
|
2354
|
+
const authMode = await selectRequestTraceAuthMode();
|
|
2355
|
+
const displayOptions = await selectRequestTraceDisplayOptions(options);
|
|
2356
|
+
const runtime = {
|
|
2357
|
+
display: displayOptions,
|
|
2358
|
+
filters: { paused: false },
|
|
2359
|
+
events: [],
|
|
2360
|
+
};
|
|
2361
|
+
const instanceIndex = await selectDebugInstance({ instance: options.instance });
|
|
2362
|
+
const baseLocalPort = await selectDebugPort({
|
|
2363
|
+
value: options.localPort,
|
|
2364
|
+
message: "Select first local inspector port for request trace",
|
|
2365
|
+
defaultPort: 9329,
|
|
2366
|
+
});
|
|
2367
|
+
const remotePort = parsePositivePort(options.remotePort, 9229);
|
|
2368
|
+
const maxBodyBytes = parsePositivePort(options.maxBodyBytes, 20000);
|
|
2369
|
+
console.log("");
|
|
2370
|
+
console.log(chalk_1.default.yellow("Request trace attaches to the running Node.js app through Node Inspector."));
|
|
2371
|
+
console.log(chalk_1.default.gray("It does not modify your repository source code. It is temporary and disappears after app restart."));
|
|
2372
|
+
const prepareMode = await selectNodeInspectorPrepareMode({ appName: appNames.join(", "), remotePort });
|
|
2373
|
+
const tunnelProcesses = [];
|
|
2374
|
+
const logProcesses = [];
|
|
2375
|
+
const stopAll = () => {
|
|
2376
|
+
for (const child of [...tunnelProcesses, ...logProcesses]) {
|
|
2377
|
+
if (!child.killed)
|
|
2378
|
+
child.kill();
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
process.once("SIGINT", () => {
|
|
2382
|
+
console.log(chalk_1.default.gray("\nStopping request trace..."));
|
|
2383
|
+
stopAll();
|
|
2384
|
+
process.exit(0);
|
|
2385
|
+
});
|
|
2386
|
+
for (const [index, appName] of appNames.entries()) {
|
|
2387
|
+
const localPort = baseLocalPort + index;
|
|
2388
|
+
await ensureSshEnabledForDebug(appName);
|
|
2389
|
+
if (prepareMode === "set-env-restart") {
|
|
2390
|
+
await setNodeInspectorEnvironmentAndRestart({ appName, remotePort });
|
|
2391
|
+
}
|
|
2392
|
+
console.log(chalk_1.default.gray(`Opening inspector tunnel for ${appName}: localhost:${localPort} -> 127.0.0.1:${remotePort}`));
|
|
2393
|
+
const tunnelProcess = (0, node_child_process_1.spawn)("cf", buildCloudFoundryDebugSshArgs({
|
|
2394
|
+
appName,
|
|
2395
|
+
instanceIndex,
|
|
2396
|
+
processName: options.process,
|
|
2397
|
+
localPort,
|
|
2398
|
+
remotePort,
|
|
2399
|
+
prepareMode,
|
|
2400
|
+
}), {
|
|
2401
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2402
|
+
shell: false,
|
|
2403
|
+
windowsHide: true,
|
|
2404
|
+
});
|
|
2405
|
+
tunnelProcesses.push(tunnelProcess);
|
|
2406
|
+
tunnelProcess.stdout.on("data", (chunk) => process.stdout.write(chalk_1.default.gray(`[${appName}:ssh] ${chunk.toString("utf8")}`)));
|
|
2407
|
+
tunnelProcess.stderr.on("data", (chunk) => process.stderr.write(chalk_1.default.yellow(`[${appName}:ssh] ${chunk.toString("utf8")}`)));
|
|
2408
|
+
const webSocketUrl = await waitForNodeInspectorWebSocketUrl(localPort, 20000);
|
|
2409
|
+
if (!webSocketUrl) {
|
|
2410
|
+
console.log(chalk_1.default.red(`Cannot reach Node Inspector for ${appName} on localhost:${localPort}.`));
|
|
2411
|
+
console.log(chalk_1.default.yellow("Try again and choose: Set NODE_OPTIONS and restart app."));
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
const expression = buildRequestTraceInjectionExpression({
|
|
2415
|
+
appName,
|
|
2416
|
+
mode: traceMode,
|
|
2417
|
+
authMode,
|
|
2418
|
+
maxBodyBytes,
|
|
2419
|
+
parseBodyJson: displayOptions.parseBodyJson,
|
|
2420
|
+
});
|
|
2421
|
+
await sendInspectorEvaluateCommand({ webSocketUrl, expression });
|
|
2422
|
+
console.log(chalk_1.default.green(`Request trace injected into ${appName}.`));
|
|
2423
|
+
const logProcess = startRequestTraceLogStream(appName, runtime);
|
|
2424
|
+
logProcesses.push(logProcess);
|
|
2425
|
+
}
|
|
2426
|
+
console.log("");
|
|
2427
|
+
console.log(chalk_1.default.green(`Request trace is watching ${appNames.length} app(s).`));
|
|
2428
|
+
console.log(chalk_1.default.gray("Send requests to your services. Type /help for runtime search commands. Press Ctrl+C to stop tunnels and log streams."));
|
|
2429
|
+
attachTraceRuntimeCommands(runtime);
|
|
2430
|
+
await new Promise((resolve) => {
|
|
2431
|
+
const watchedProcesses = [...tunnelProcesses, ...logProcesses];
|
|
2432
|
+
let closedCount = 0;
|
|
2433
|
+
for (const child of watchedProcesses) {
|
|
2434
|
+
child.on("close", () => {
|
|
2435
|
+
closedCount += 1;
|
|
2436
|
+
if (closedCount >= watchedProcesses.length)
|
|
2437
|
+
resolve();
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
1054
2442
|
async function runDebugCommand(options) {
|
|
1055
2443
|
await maybeSwitchCloudFoundryTargetForDebug(options);
|
|
2444
|
+
await ensureCloudFoundrySessionFromCache();
|
|
1056
2445
|
const appName = await resolveAppSelection({
|
|
1057
2446
|
app: options.app,
|
|
1058
2447
|
refresh: options.refresh,
|
|
@@ -1066,9 +2455,9 @@ async function runDebugCommand(options) {
|
|
|
1066
2455
|
defaultPort: 9229,
|
|
1067
2456
|
});
|
|
1068
2457
|
const remotePort = parsePositivePort(options.remotePort, 9229);
|
|
1069
|
-
const repositoryPath = await resolveRepositoryPath(process.cwd()).catch(() => process.cwd());
|
|
2458
|
+
const repositoryPath = await (0, repository_1.resolveRepositoryPath)(process.cwd()).catch(() => process.cwd());
|
|
1070
2459
|
if (debugMode === "check-ssh") {
|
|
1071
|
-
const result = await runCommand("cf", ["ssh-enabled", appName]);
|
|
2460
|
+
const result = await (0, process_1.runCommand)("cf", ["ssh-enabled", appName]);
|
|
1072
2461
|
if (result.stdout)
|
|
1073
2462
|
console.log(result.stdout);
|
|
1074
2463
|
if (result.stderr)
|
|
@@ -1077,7 +2466,7 @@ async function runDebugCommand(options) {
|
|
|
1077
2466
|
return;
|
|
1078
2467
|
}
|
|
1079
2468
|
if (debugMode === "enable-ssh") {
|
|
1080
|
-
const result = await runCommand("cf", ["enable-ssh", appName]);
|
|
2469
|
+
const result = await (0, process_1.runCommand)("cf", ["enable-ssh", appName]);
|
|
1081
2470
|
if (result.stdout)
|
|
1082
2471
|
console.log(result.stdout);
|
|
1083
2472
|
if (result.stderr)
|
|
@@ -1086,7 +2475,7 @@ async function runDebugCommand(options) {
|
|
|
1086
2475
|
process.exitCode = result.exitCode;
|
|
1087
2476
|
return;
|
|
1088
2477
|
}
|
|
1089
|
-
const restartResponse = await
|
|
2478
|
+
const restartResponse = await (0, prompts_1.default)({
|
|
1090
2479
|
type: "select",
|
|
1091
2480
|
name: "restart",
|
|
1092
2481
|
message: "Restart app now so SSH setting takes effect?",
|
|
@@ -1097,11 +2486,11 @@ async function runDebugCommand(options) {
|
|
|
1097
2486
|
initial: 0,
|
|
1098
2487
|
});
|
|
1099
2488
|
if (restartResponse.restart) {
|
|
1100
|
-
const restartExitCode = await runCommandInherit("cf", ["restart", appName]);
|
|
2489
|
+
const restartExitCode = await (0, process_1.runCommandInherit)("cf", ["restart", appName]);
|
|
1101
2490
|
process.exitCode = restartExitCode;
|
|
1102
2491
|
return;
|
|
1103
2492
|
}
|
|
1104
|
-
console.log(
|
|
2493
|
+
console.log(chalk_1.default.yellow(`SSH was enabled. Restart the app before debugging: cf restart ${appName}`));
|
|
1105
2494
|
return;
|
|
1106
2495
|
}
|
|
1107
2496
|
let launchJsonPath;
|
|
@@ -1112,8 +2501,8 @@ async function runDebugCommand(options) {
|
|
|
1112
2501
|
localPort,
|
|
1113
2502
|
remoteRoot: "/home/vcap/app",
|
|
1114
2503
|
});
|
|
1115
|
-
console.log(
|
|
1116
|
-
const openResponse = await
|
|
2504
|
+
console.log(chalk_1.default.green(`Updated VS Code launch config: ${launchJsonPath}`));
|
|
2505
|
+
const openResponse = await (0, prompts_1.default)({
|
|
1117
2506
|
type: "select",
|
|
1118
2507
|
name: "open",
|
|
1119
2508
|
message: "Open current folder in VS Code?",
|
|
@@ -1132,7 +2521,7 @@ async function runDebugCommand(options) {
|
|
|
1132
2521
|
appName,
|
|
1133
2522
|
instanceIndex,
|
|
1134
2523
|
localPort,
|
|
1135
|
-
launchJsonPath: launchJsonPath ??
|
|
2524
|
+
launchJsonPath: launchJsonPath ?? node_path_1.default.resolve(repositoryPath, ".vscode", "launch.json"),
|
|
1136
2525
|
});
|
|
1137
2526
|
return;
|
|
1138
2527
|
}
|
|
@@ -1142,7 +2531,7 @@ async function runDebugCommand(options) {
|
|
|
1142
2531
|
return;
|
|
1143
2532
|
}
|
|
1144
2533
|
if (options.enableSsh) {
|
|
1145
|
-
const result = await runCommand("cf", ["enable-ssh", appName]);
|
|
2534
|
+
const result = await (0, process_1.runCommand)("cf", ["enable-ssh", appName]);
|
|
1146
2535
|
if (result.stdout)
|
|
1147
2536
|
console.log(result.stdout);
|
|
1148
2537
|
if (result.stderr)
|
|
@@ -1152,28 +2541,28 @@ async function runDebugCommand(options) {
|
|
|
1152
2541
|
return;
|
|
1153
2542
|
}
|
|
1154
2543
|
if (options.restart) {
|
|
1155
|
-
const restartExitCode = await runCommandInherit("cf", ["restart", appName]);
|
|
2544
|
+
const restartExitCode = await (0, process_1.runCommandInherit)("cf", ["restart", appName]);
|
|
1156
2545
|
if (restartExitCode !== 0) {
|
|
1157
2546
|
process.exitCode = restartExitCode;
|
|
1158
2547
|
return;
|
|
1159
2548
|
}
|
|
1160
2549
|
}
|
|
1161
2550
|
else {
|
|
1162
|
-
console.log(
|
|
2551
|
+
console.log(chalk_1.default.yellow("SSH was enabled. If cf ssh still fails, restart the app or run: cf restart " + appName));
|
|
1163
2552
|
}
|
|
1164
2553
|
}
|
|
1165
2554
|
console.log("");
|
|
1166
|
-
console.log(
|
|
1167
|
-
console.log(
|
|
1168
|
-
console.log(
|
|
2555
|
+
console.log(chalk_1.default.cyan("BTP debug works by opening a CF SSH tunnel to the Node.js inspector."));
|
|
2556
|
+
console.log(chalk_1.default.gray("If this is the first time debugging this app, choose: Set NODE_OPTIONS and restart app."));
|
|
2557
|
+
console.log(chalk_1.default.gray("If the app was already restarted with NODE_OPTIONS=--inspect, choose: Inspector is already enabled."));
|
|
1169
2558
|
const prepareMode = await selectNodeInspectorPrepareMode({ appName, remotePort });
|
|
1170
2559
|
await ensureSshEnabledForDebug(appName);
|
|
1171
2560
|
if (prepareMode === "set-env-restart") {
|
|
1172
2561
|
await setNodeInspectorEnvironmentAndRestart({ appName, remotePort });
|
|
1173
2562
|
}
|
|
1174
|
-
console.log(
|
|
1175
|
-
console.log(
|
|
1176
|
-
const childProcess = spawn("cf", buildCloudFoundryDebugSshArgs({
|
|
2563
|
+
console.log(chalk_1.default.gray(`Starting Node.js inspector tunnel for ${appName} instance ${instanceIndex}...`));
|
|
2564
|
+
console.log(chalk_1.default.gray(`Forwarding localhost:${localPort} -> app container 127.0.0.1:${remotePort}`));
|
|
2565
|
+
const childProcess = (0, node_child_process_1.spawn)("cf", buildCloudFoundryDebugSshArgs({
|
|
1177
2566
|
appName,
|
|
1178
2567
|
instanceIndex,
|
|
1179
2568
|
processName: options.process,
|
|
@@ -1194,13 +2583,13 @@ async function runDebugCommand(options) {
|
|
|
1194
2583
|
const debugUrl = await waitForNodeInspectorDebugUrl(localPort);
|
|
1195
2584
|
if (debugMode === "vscode") {
|
|
1196
2585
|
if (!debugUrl) {
|
|
1197
|
-
console.log(
|
|
2586
|
+
console.log(chalk_1.default.yellow("Inspector is not reachable yet on localhost. If you selected running-process mode and see a Node PID error, run debug again and choose 'Set NODE_OPTIONS and restart app'."));
|
|
1198
2587
|
}
|
|
1199
2588
|
printVscodeAttachInstructions({
|
|
1200
2589
|
appName,
|
|
1201
2590
|
instanceIndex,
|
|
1202
2591
|
localPort,
|
|
1203
|
-
launchJsonPath: launchJsonPath ??
|
|
2592
|
+
launchJsonPath: launchJsonPath ?? node_path_1.default.resolve(repositoryPath, ".vscode", "launch.json"),
|
|
1204
2593
|
inspectorReady: Boolean(debugUrl),
|
|
1205
2594
|
});
|
|
1206
2595
|
return;
|
|
@@ -1224,9 +2613,9 @@ async function runDebugCommand(options) {
|
|
|
1224
2613
|
clearTimeout(fallbackTimer);
|
|
1225
2614
|
if (!hasPrintedAttachInfo || (exitCode ?? 0) !== 0) {
|
|
1226
2615
|
console.log("");
|
|
1227
|
-
console.log(
|
|
1228
|
-
console.log(
|
|
1229
|
-
console.log(
|
|
2616
|
+
console.log(chalk_1.default.red("Debug tunnel stopped before a working inspector connection was confirmed."));
|
|
2617
|
+
console.log(chalk_1.default.yellow("Run smdg cf debug again and choose 'Set NODE_OPTIONS and restart app' when asked to prepare Node.js inspector."));
|
|
2618
|
+
console.log(chalk_1.default.gray("After the app restarts, choose VS Code guided debugging and start the attach config from VS Code Run and Debug."));
|
|
1230
2619
|
}
|
|
1231
2620
|
process.exitCode = exitCode ?? 0;
|
|
1232
2621
|
});
|
|
@@ -1235,14 +2624,14 @@ async function runDebugCommand(options) {
|
|
|
1235
2624
|
});
|
|
1236
2625
|
}
|
|
1237
2626
|
async function runTargetCommand() {
|
|
1238
|
-
const target = await readCloudFoundryTarget();
|
|
2627
|
+
const target = await (0, cf_1.readCloudFoundryTarget)();
|
|
1239
2628
|
printTarget(target);
|
|
1240
2629
|
}
|
|
1241
2630
|
async function runCacheCommand() {
|
|
1242
|
-
const cache = await readCache();
|
|
2631
|
+
const cache = await (0, cache_1.readCache)();
|
|
1243
2632
|
console.log(JSON.stringify(cache.cloudFoundry, null, 2));
|
|
1244
2633
|
}
|
|
1245
|
-
|
|
2634
|
+
function registerCloudFoundryCommands(program) {
|
|
1246
2635
|
const cfCommand = program.command("cf").description("Cloud Foundry helper commands for SimpleMDG");
|
|
1247
2636
|
cfCommand
|
|
1248
2637
|
.command("login")
|
|
@@ -1317,10 +2706,48 @@ export function registerCloudFoundryCommands(program) {
|
|
|
1317
2706
|
.option("--open", "Open current folder in VS Code after creating launch.json")
|
|
1318
2707
|
.option("--skip-org-select", "Use current CF org/space without asking")
|
|
1319
2708
|
.action(runDebugCommand);
|
|
2709
|
+
cfCommand
|
|
2710
|
+
.command("http-watch")
|
|
2711
|
+
.alias("watch-http")
|
|
2712
|
+
.description("Watch incoming HTTP requests using existing CF/CDS/RTR logs. Stable and does not modify apps.")
|
|
2713
|
+
.option("--app <appName>", "BTP app name. Use comma-separated names to watch multiple apps")
|
|
2714
|
+
.option("--refresh", "Refresh app list before selecting")
|
|
2715
|
+
.option("--recent", "Parse recent logs and exit")
|
|
2716
|
+
.option("--out <fileName>", "Write parsed HTTP events to a file")
|
|
2717
|
+
.option("--skip-org-select", "Use current CF org/space without asking")
|
|
2718
|
+
.action(runHttpWatchCommand);
|
|
2719
|
+
cfCommand
|
|
2720
|
+
.command("request-trace-doctor")
|
|
2721
|
+
.description("Diagnose why deep request-trace may not capture body/header in a BTP Node.js app")
|
|
2722
|
+
.option("--app <appName>", "BTP app name. Use comma-separated names")
|
|
2723
|
+
.option("--refresh", "Refresh app list before selecting")
|
|
2724
|
+
.option("--instance <index>", "App instance index", "0")
|
|
2725
|
+
.option("--process <processName>", "CF process name for multi-process apps")
|
|
2726
|
+
.option("--local-port <port>", "First local inspector port", "9329")
|
|
2727
|
+
.option("--remote-port <port>", "Remote inspector port in app container", "9229")
|
|
2728
|
+
.option("--max-body-bytes <bytes>", "Maximum request/response body bytes to print", "20000")
|
|
2729
|
+
.option("--skip-org-select", "Use current CF org/space without asking")
|
|
2730
|
+
.action(runRequestTraceDoctorCommand);
|
|
2731
|
+
cfCommand
|
|
2732
|
+
.command("request-trace")
|
|
2733
|
+
.alias("network-trace")
|
|
2734
|
+
.alias("traffic")
|
|
2735
|
+
.description("Watch incoming HTTP requests from BTP Node.js apps without editing backend source code")
|
|
2736
|
+
.option("--app <appName>", "BTP app name. Use comma-separated names to trace multiple apps")
|
|
2737
|
+
.option("--refresh", "Refresh app list before selecting")
|
|
2738
|
+
.option("--instance <index>", "App instance index", "0")
|
|
2739
|
+
.option("--process <processName>", "CF process name for multi-process apps")
|
|
2740
|
+
.option("--local-port <port>", "First local inspector port", "9329")
|
|
2741
|
+
.option("--remote-port <port>", "Remote inspector port in app container", "9229")
|
|
2742
|
+
.option("--max-body-bytes <bytes>", "Maximum request/response body bytes to print", "20000")
|
|
2743
|
+
.option("--out <fileName>", "Export captured trace events to a JSONL file")
|
|
2744
|
+
.option("--skip-org-select", "Use current CF org/space without asking")
|
|
2745
|
+
.action(runRequestTraceCommand);
|
|
1320
2746
|
cfCommand
|
|
1321
2747
|
.command("apps-cache-refresh")
|
|
1322
2748
|
.description("Refresh cached cf apps for current target. Internal command used by smdg cf apps.")
|
|
1323
2749
|
.action(runAppsCacheRefreshCommand);
|
|
2750
|
+
(0, cf_db_command_1.registerCloudFoundryDbCommands)(cfCommand);
|
|
1324
2751
|
cfCommand.command("cache").description("Print cached Cloud Foundry values").action(runCacheCommand);
|
|
1325
2752
|
}
|
|
1326
2753
|
//# sourceMappingURL=cf.command.js.map
|