simplemdg-dev-cli 1.3.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -74,6 +74,71 @@ async function ensureCloudFoundrySessionFromCache() {
74
74
  });
75
75
  return readCloudFoundryTarget();
76
76
  }
77
+ function sortCloudFoundryProfilesForEndpoint(options) {
78
+ const profilesWithPassword = options.profiles.filter((profile) => profile.password?.trim());
79
+ return [
80
+ ...profilesWithPassword.filter((profile) => profile.apiEndpoint === options.apiEndpoint && profile.org === options.preferredOrg),
81
+ ...profilesWithPassword.filter((profile) => profile.apiEndpoint === options.apiEndpoint && profile.org !== options.preferredOrg),
82
+ ...profilesWithPassword.filter((profile) => profile.apiEndpoint !== options.apiEndpoint && profile.org === options.preferredOrg),
83
+ ...profilesWithPassword.filter((profile) => profile.apiEndpoint !== options.apiEndpoint && profile.org !== options.preferredOrg),
84
+ ].filter((profile, index, array) => {
85
+ return array.findIndex((item) => {
86
+ return item.apiEndpoint === profile.apiEndpoint
87
+ && item.username === profile.username
88
+ && item.password === profile.password
89
+ && item.org === profile.org
90
+ && item.space === profile.space;
91
+ }) === index;
92
+ });
93
+ }
94
+ async function ensureCloudFoundryAuthenticatedForApiEndpoint(options) {
95
+ const apiExitCode = await setCloudFoundryApiEndpoint(options.apiEndpoint);
96
+ if (apiExitCode !== 0) {
97
+ process.exitCode = apiExitCode;
98
+ throw new Error(`Cannot set CF API endpoint: ${options.apiEndpoint}`);
99
+ }
100
+ const orgsCheck = await runCommand("cf", ["orgs"]);
101
+ if (orgsCheck.exitCode === 0) {
102
+ return undefined;
103
+ }
104
+ const cache = await readCache();
105
+ const profiles = sortCloudFoundryProfilesForEndpoint({
106
+ profiles: cache.cloudFoundry.loginProfiles,
107
+ apiEndpoint: options.apiEndpoint,
108
+ preferredOrg: options.preferredOrg,
109
+ });
110
+ if (!profiles.length) {
111
+ console.log(chalk.yellow(`Not logged in to ${inferCloudFoundryRegionFromApiEndpoint(options.apiEndpoint)} and no cached password was found for automatic login.`));
112
+ console.log(chalk.gray("Run smdg cf login once, choose to save password, then retry this command."));
113
+ throw new Error("Cloud Foundry automatic login is required");
114
+ }
115
+ let lastError = orgsCheck.stderr || orgsCheck.stdout || "cf orgs failed";
116
+ for (const profile of profiles) {
117
+ console.log(chalk.gray(`Auto auth CF ${inferCloudFoundryRegionFromApiEndpoint(options.apiEndpoint)} as ${profile.username}...`));
118
+ const authExitCode = await authenticateCloudFoundry({
119
+ username: profile.username,
120
+ password: profile.password,
121
+ });
122
+ if (authExitCode !== 0) {
123
+ lastError = `cf auth failed for ${profile.username}`;
124
+ continue;
125
+ }
126
+ const nextOrgsCheck = await runCommand("cf", ["orgs"]);
127
+ if (nextOrgsCheck.exitCode === 0) {
128
+ const updatedProfile = {
129
+ ...profile,
130
+ apiEndpoint: options.apiEndpoint,
131
+ org: options.preferredOrg || profile.org,
132
+ space: options.preferredSpace || profile.space,
133
+ updatedAt: new Date().toISOString(),
134
+ };
135
+ await rememberCloudFoundryLoginProfile(updatedProfile);
136
+ return updatedProfile;
137
+ }
138
+ lastError = nextOrgsCheck.stderr || nextOrgsCheck.stdout || lastError;
139
+ }
140
+ throw new Error(`CF automatic login failed for ${options.apiEndpoint}. ${lastError}`);
141
+ }
77
142
  function buildCloudFoundryLogsArgs(options) {
78
143
  const args = ["logs", options.appName];
79
144
  if (options.recent) {
@@ -96,15 +161,28 @@ function buildNodeInspectorRemoteCommand(remotePort) {
96
161
  return [
97
162
  `echo "Remote port ${remotePort} was requested."`,
98
163
  `echo "SIGUSR1 starts the Node.js inspector on its default port 9229 for a running Node process."`,
99
- `echo "Use remote port 9229, or start the app process with NODE_OPTIONS=--inspect=127.0.0.1:${remotePort}."`,
164
+ `echo "Use remote port 9229, or start the app process with NODE_OPTIONS=--inspect=0.0.0.0:${remotePort}."`,
100
165
  `exit 2`,
101
166
  ].join("; ");
102
167
  }
168
+ const detectNodePidScript = [
169
+ `PID=""`,
170
+ `if command -v pgrep >/dev/null 2>&1; then PID=$(pgrep -f "(^|/| )node( |$)" 2>/dev/null | head -n 1 || true); fi`,
171
+ `if [ -z "$PID" ]; then PID=$(ps -eo pid=,args= 2>/dev/null | awk '/[n]ode/ && $0 !~ /awk/ && $0 !~ /pgrep/ {print $1; exit}'); fi`,
172
+ `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`,
173
+ `echo "Detected Node.js PID: $PID"`,
174
+ `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`,
175
+ `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`,
176
+ `kill -USR1 "$PID" || { echo "Cannot send SIGUSR1 to Node.js PID $PID. Use prepare mode: Set NODE_OPTIONS and restart app." >&2; exit 1; }`,
177
+ `echo "Requested Node inspector for PID $PID on 127.0.0.1:9229"`,
178
+ `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`,
179
+ `tail -f /dev/null`,
180
+ ];
181
+ return detectNodePidScript.join("; ");
182
+ }
183
+ function buildKeepAliveRemoteCommand() {
103
184
  return [
104
- `PID=$(pgrep -xo node || pgrep -x node | head -n 1)`,
105
- `if [ -z "$PID" ]; then echo "No Node.js PID found in app container" >&2; exit 1; fi`,
106
- `if command -v ss >/dev/null 2>&1 && ss -H -ntl "sport = :9229" | grep -q .; then echo "Node inspector already listening on 127.0.0.1:9229"`,
107
- `else kill -s SIGUSR1 "$PID" && echo "Started Node inspector for PID $PID on 127.0.0.1:9229"; fi`,
185
+ `echo "SSH tunnel is open. Keep this terminal running."`,
108
186
  `tail -f /dev/null`,
109
187
  ].join("; ");
110
188
  }
@@ -118,9 +196,69 @@ function buildCloudFoundryDebugSshArgs(options) {
118
196
  if (options.processName?.trim()) {
119
197
  args.push("--process", options.processName.trim());
120
198
  }
121
- args.push("-T", "-L", `${options.localPort}:127.0.0.1:${options.remotePort}`, "-c", buildNodeInspectorRemoteCommand(options.remotePort));
199
+ const remoteCommand = options.prepareMode === "running-process"
200
+ ? buildNodeInspectorRemoteCommand(options.remotePort)
201
+ : buildKeepAliveRemoteCommand();
202
+ args.push("-T", "-L", `${options.localPort}:127.0.0.1:${options.remotePort}`, "-c", remoteCommand);
122
203
  return args;
123
204
  }
205
+ async function selectNodeInspectorPrepareMode(options) {
206
+ return searchableSelectChoice({
207
+ message: "Prepare Node.js inspector",
208
+ choices: [
209
+ {
210
+ title: "Set NODE_OPTIONS and restart app (recommended first time)",
211
+ value: "set-env-restart",
212
+ description: `Runs cf set-env ${options.appName} NODE_OPTIONS --inspect=0.0.0.0:${options.remotePort} and cf restart`,
213
+ },
214
+ {
215
+ title: "Try start inspector on running Node process without restart",
216
+ value: "running-process",
217
+ description: "Uses cf ssh + SIGUSR1. Fast, but may fail if Node PID cannot be detected.",
218
+ },
219
+ {
220
+ title: "Inspector is already enabled, only open SSH tunnel",
221
+ value: "already-enabled",
222
+ description: "Use when NODE_OPTIONS already contains --inspect and app was restarted.",
223
+ },
224
+ ],
225
+ allowCustomValue: false,
226
+ });
227
+ }
228
+ async function ensureSshEnabledForDebug(appName) {
229
+ const sshEnabledResult = await runCommand("cf", ["ssh-enabled", appName]);
230
+ const combinedOutput = `${sshEnabledResult.stdout}
231
+ ${sshEnabledResult.stderr}`;
232
+ if (sshEnabledResult.exitCode === 0 && /enabled/i.test(combinedOutput) && !/not enabled/i.test(combinedOutput)) {
233
+ return;
234
+ }
235
+ console.log(chalk.yellow("SSH is not enabled for this app. Enabling SSH..."));
236
+ const enableResult = await runCommand("cf", ["enable-ssh", appName]);
237
+ if (enableResult.stdout)
238
+ console.log(enableResult.stdout);
239
+ if (enableResult.stderr)
240
+ console.error(enableResult.stderr);
241
+ if (enableResult.exitCode !== 0) {
242
+ throw new Error("cf enable-ssh failed. You may not have permission to enable SSH for this app.");
243
+ }
244
+ }
245
+ async function setNodeInspectorEnvironmentAndRestart(options) {
246
+ const nodeOptions = `--inspect=0.0.0.0:${options.remotePort} --enable-source-maps`;
247
+ console.log(chalk.gray(`Setting NODE_OPTIONS for ${options.appName}: ${nodeOptions}`));
248
+ const setEnvResult = await runCommand("cf", ["set-env", options.appName, "NODE_OPTIONS", nodeOptions]);
249
+ if (setEnvResult.stdout)
250
+ console.log(setEnvResult.stdout);
251
+ if (setEnvResult.stderr)
252
+ console.error(setEnvResult.stderr);
253
+ if (setEnvResult.exitCode !== 0) {
254
+ throw new Error("cf set-env NODE_OPTIONS failed");
255
+ }
256
+ console.log(chalk.yellow("Restarting app so NODE_OPTIONS takes effect..."));
257
+ const restartExitCode = await runCommandInherit("cf", ["restart", options.appName]);
258
+ if (restartExitCode !== 0) {
259
+ throw new Error(`cf restart ${options.appName} failed`);
260
+ }
261
+ }
124
262
  async function getNodeInspectorDebugUrl(localPort) {
125
263
  const response = await fetch(`http://127.0.0.1:${localPort}/json/list`);
126
264
  if (!response.ok) {
@@ -240,18 +378,34 @@ async function writeVscodeLaunchConfiguration(options) {
240
378
  }
241
379
  function printVscodeAttachInstructions(options) {
242
380
  console.log("");
243
- console.log(chalk.green(`VS Code debug config is ready for ${options.appName} instance ${options.instanceIndex}.`));
381
+ if (options.inspectorReady === false) {
382
+ console.log(chalk.yellow(`VS Code config was created, but the Node inspector is not reachable yet for ${options.appName} instance ${options.instanceIndex}.`));
383
+ console.log(chalk.yellow("The VS Code debug toolbar appears only after the inspector is reachable and you start the attach config."));
384
+ }
385
+ else {
386
+ console.log(chalk.green(`VS Code debug config is ready for ${options.appName} instance ${options.instanceIndex}.`));
387
+ }
244
388
  console.log(`Launch file: ${chalk.cyan(options.launchJsonPath)}`);
245
389
  console.log(`Attach config: ${chalk.cyan(`Attach BTP ${options.appName}`)}`);
246
390
  console.log(`Inspector: ${chalk.cyan(`127.0.0.1:${options.localPort}`)}`);
247
391
  console.log("");
248
- console.log(chalk.gray("Keep this terminal open, then open VS Code Run and Debug and choose the attach config above."));
392
+ console.log(chalk.cyan("How to start debugging in VS Code:"));
393
+ console.log("1. Keep this terminal open. It owns the CF SSH tunnel.");
394
+ console.log("2. Open VS Code Run and Debug panel with Ctrl+Shift+D.");
395
+ console.log(`3. Select ${chalk.cyan(`Attach BTP ${options.appName}`)}.`);
396
+ console.log("4. Press F5 or the green Start Debugging button.");
397
+ console.log("5. Debug buttons such as pause, step over, step into, restart, and stop appear only after attach succeeds.");
398
+ console.log("");
249
399
  console.log(chalk.gray("Press Ctrl+C in this terminal to close the tunnel."));
250
400
  }
251
401
  async function openVisualStudioCode(options) {
252
- const result = await runCommand("code", [options.cwd]);
402
+ const args = options.debugPanel
403
+ ? ["--reuse-window", options.cwd, "--command", "workbench.view.debug"]
404
+ : [options.cwd];
405
+ const result = await runCommand("code", args);
253
406
  if (result.exitCode !== 0) {
254
407
  console.log(chalk.yellow("Could not open VS Code automatically. Open this folder manually in VS Code."));
408
+ console.log(chalk.gray("Then open Run and Debug with Ctrl+Shift+D."));
255
409
  if (result.stderr)
256
410
  console.log(chalk.gray(result.stderr));
257
411
  }
@@ -273,9 +427,9 @@ async function selectDebugMode(options) {
273
427
  message: "Select debug mode",
274
428
  choices: [
275
429
  {
276
- title: "VS Code attach debugger (recommended)",
430
+ title: "VS Code guided debugging (recommended)",
277
431
  value: "vscode",
278
- description: "Create/update .vscode/launch.json and open a CF SSH tunnel",
432
+ description: "Create launch.json, open VS Code Debug panel, prepare inspector, and open CF SSH tunnel",
279
433
  },
280
434
  {
281
435
  title: "Chrome DevTools / chrome://inspect",
@@ -430,6 +584,7 @@ function refreshAppsCacheInDetachedProcess() {
430
584
  childProcess.unref();
431
585
  }
432
586
  async function getAppsWithCache(options) {
587
+ await ensureCloudFoundrySessionFromCache();
433
588
  if (options.refresh) {
434
589
  return refreshAppsCacheForCurrentTarget();
435
590
  }
@@ -480,15 +635,24 @@ function printTarget(target) {
480
635
  const DEFAULT_CLOUD_FOUNDRY_API_ENDPOINTS = [
481
636
  "https://api.cf.br10.hana.ondemand.com",
482
637
  "https://api.cf.eu10.hana.ondemand.com",
638
+ "https://api.cf.eu10-004.hana.ondemand.com",
639
+ "https://api.cf.eu10-005.hana.ondemand.com",
640
+ "https://api.cf.eu20.hana.ondemand.com",
641
+ "https://api.cf.eu20-001.hana.ondemand.com",
642
+ "https://api.cf.eu20-002.hana.ondemand.com",
483
643
  "https://api.cf.us10.hana.ondemand.com",
644
+ "https://api.cf.us10-001.hana.ondemand.com",
645
+ "https://api.cf.us11.hana.ondemand.com",
646
+ "https://api.cf.us20.hana.ondemand.com",
647
+ "https://api.cf.us21.hana.ondemand.com",
484
648
  "https://api.cf.ap10.hana.ondemand.com",
485
649
  "https://api.cf.ap11.hana.ondemand.com",
650
+ "https://api.cf.ap20.hana.ondemand.com",
486
651
  "https://api.cf.ap21.hana.ondemand.com",
487
652
  "https://api.cf.jp10.hana.ondemand.com",
488
653
  "https://api.cf.ca10.hana.ondemand.com",
489
654
  "https://api.cf.ch20.hana.ondemand.com",
490
- "https://api.cf.eu20.hana.ondemand.com",
491
- "https://api.cf.us20.hana.ondemand.com",
655
+ "https://api.cf.sa10.hana.ondemand.com",
492
656
  ];
493
657
  function uniqueValues(values) {
494
658
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
@@ -689,6 +853,7 @@ function getCloudFoundryApiEndpointsForOrgSearch(options, target, cache) {
689
853
  options.api ?? "",
690
854
  target.apiEndpoint ?? "",
691
855
  ...cache.cloudFoundry.loginProfiles.map((item) => item.apiEndpoint),
856
+ ...cache.cloudFoundry.orgsAcrossRegions.map((item) => item.apiEndpoint),
692
857
  ...DEFAULT_CLOUD_FOUNDRY_API_ENDPOINTS,
693
858
  ]);
694
859
  }
@@ -696,8 +861,12 @@ async function getCloudFoundryOrganizationsAcrossRegions(options) {
696
861
  const target = await readCloudFoundryTarget();
697
862
  const cache = await readCache();
698
863
  const cachedEntries = cache.cloudFoundry.orgsAcrossRegions ?? [];
699
- if (!options.refresh && cachedEntries.length) {
700
- return cachedEntries;
864
+ const cachedRegionCount = new Set(cachedEntries.map((entry) => entry.region)).size;
865
+ if (!options.refresh && cachedEntries.length && cachedRegionCount > 1) {
866
+ return cachedEntries.sort((left, right) => {
867
+ const byOrg = left.org.localeCompare(right.org);
868
+ return byOrg !== 0 ? byOrg : left.region.localeCompare(right.region);
869
+ });
701
870
  }
702
871
  const apiEndpoints = getCloudFoundryApiEndpointsForOrgSearch(options, target, cache);
703
872
  console.log(chalk.gray(`Searching CF orgs across ${apiEndpoints.length} region endpoint(s)...`));
@@ -708,6 +877,8 @@ async function getCloudFoundryOrganizationsAcrossRegions(options) {
708
877
  }));
709
878
  const entries = await scanCloudFoundryOrganizationsAcrossRegions(apiEndpoints, credentials);
710
879
  if (entries.length) {
880
+ const regionCount = new Set(entries.map((entry) => entry.region)).size;
881
+ console.log(chalk.green(`Found ${entries.length} org(s) across ${regionCount} region(s).`));
711
882
  await rememberCloudFoundryOrgEntries(entries);
712
883
  return entries;
713
884
  }
@@ -747,14 +918,17 @@ async function runOrgCommand(options) {
747
918
  api: options.api,
748
919
  refresh: options.refresh,
749
920
  });
921
+ const organizationRegionCount = new Set(organizationEntries.map((entry) => entry.region)).size;
750
922
  const latestTarget = await readCloudFoundryTarget();
751
923
  if (action === "list") {
752
924
  printTarget(latestTarget);
753
925
  console.log("");
754
926
  if (!organizationEntries.length) {
755
- console.log(chalk.yellow("No orgs found for current CF user. Try smdg cf org --list --refresh after login."));
927
+ console.log(chalk.yellow("No orgs found for current CF user. Run smdg cf login, save the password, then run smdg cf org again."));
756
928
  return;
757
929
  }
930
+ console.log(chalk.gray(`Showing ${organizationEntries.length} org(s) across ${organizationRegionCount} region(s).`));
931
+ console.log("");
758
932
  for (const entry of organizationEntries) {
759
933
  const marker = entry.apiEndpoint === latestTarget.apiEndpoint && entry.org === latestTarget.org ? chalk.green("*") : " ";
760
934
  console.log(`${marker} ${formatCloudFoundryOrgEntry(entry, latestTarget)}`);
@@ -781,7 +955,7 @@ async function runOrgCommand(options) {
781
955
  return;
782
956
  }
783
957
  const selectedIndex = await searchableSelectChoice({
784
- message: "Search CF org across regions",
958
+ message: `Search CF org across ${organizationRegionCount} region(s)`,
785
959
  choices: organizationEntries.map((entry, index) => ({
786
960
  title: formatCloudFoundryOrgEntry(entry, latestTarget),
787
961
  value: String(index),
@@ -799,14 +973,16 @@ async function runOrgCommand(options) {
799
973
  if (!selectedEntry.apiEndpoint) {
800
974
  throw new Error("Cannot determine CF API endpoint for selected org.");
801
975
  }
802
- const apiExitCode = await setCloudFoundryApiEndpoint(selectedEntry.apiEndpoint);
803
- if (apiExitCode !== 0) {
804
- process.exitCode = apiExitCode;
805
- return;
806
- }
976
+ const authenticatedProfile = await ensureCloudFoundryAuthenticatedForApiEndpoint({
977
+ apiEndpoint: selectedEntry.apiEndpoint,
978
+ preferredOrg: selectedEntry.org,
979
+ preferredSpace: options.space,
980
+ reason: "switch-org",
981
+ });
807
982
  const orgExitCode = await targetCloudFoundryOrg(selectedEntry.org);
808
983
  if (orgExitCode !== 0) {
809
- console.log(chalk.yellow("Cannot switch to this org with current CF session. Run smdg cf login for this region, then try again."));
984
+ console.log(chalk.yellow("Cannot switch to this org after automatic authentication."));
985
+ console.log(chalk.gray("Run smdg cf login, save the password, then try again."));
810
986
  process.exitCode = orgExitCode;
811
987
  return;
812
988
  }
@@ -833,6 +1009,15 @@ async function runOrgCommand(options) {
833
1009
  return;
834
1010
  }
835
1011
  }
1012
+ if (authenticatedProfile?.password) {
1013
+ await rememberCloudFoundryLoginProfile({
1014
+ ...authenticatedProfile,
1015
+ apiEndpoint: selectedEntry.apiEndpoint,
1016
+ org: selectedEntry.org,
1017
+ space,
1018
+ updatedAt: new Date().toISOString(),
1019
+ });
1020
+ }
836
1021
  const switchedTarget = await readCloudFoundryTarget();
837
1022
  console.log(chalk.green("CF org/space switched."));
838
1023
  printTarget(switchedTarget);
@@ -962,6 +1147,7 @@ async function runLogsCommand(options) {
962
1147
  }
963
1148
  async function runDebugCommand(options) {
964
1149
  await maybeSwitchCloudFoundryTargetForDebug(options);
1150
+ await ensureCloudFoundrySessionFromCache();
965
1151
  const appName = await resolveAppSelection({
966
1152
  app: options.app,
967
1153
  refresh: options.refresh,
@@ -1033,7 +1219,7 @@ async function runDebugCommand(options) {
1033
1219
  initial: options.open ? 1 : 0,
1034
1220
  });
1035
1221
  if (openResponse.open) {
1036
- await openVisualStudioCode({ cwd: repositoryPath });
1222
+ await openVisualStudioCode({ cwd: repositoryPath, debugPanel: debugMode === "vscode" });
1037
1223
  }
1038
1224
  }
1039
1225
  if (debugMode === "config-only") {
@@ -1071,6 +1257,15 @@ async function runDebugCommand(options) {
1071
1257
  console.log(chalk.yellow("SSH was enabled. If cf ssh still fails, restart the app or run: cf restart " + appName));
1072
1258
  }
1073
1259
  }
1260
+ console.log("");
1261
+ console.log(chalk.cyan("BTP debug works by opening a CF SSH tunnel to the Node.js inspector."));
1262
+ console.log(chalk.gray("If this is the first time debugging this app, choose: Set NODE_OPTIONS and restart app."));
1263
+ console.log(chalk.gray("If the app was already restarted with NODE_OPTIONS=--inspect, choose: Inspector is already enabled."));
1264
+ const prepareMode = await selectNodeInspectorPrepareMode({ appName, remotePort });
1265
+ await ensureSshEnabledForDebug(appName);
1266
+ if (prepareMode === "set-env-restart") {
1267
+ await setNodeInspectorEnvironmentAndRestart({ appName, remotePort });
1268
+ }
1074
1269
  console.log(chalk.gray(`Starting Node.js inspector tunnel for ${appName} instance ${instanceIndex}...`));
1075
1270
  console.log(chalk.gray(`Forwarding localhost:${localPort} -> app container 127.0.0.1:${remotePort}`));
1076
1271
  const childProcess = spawn("cf", buildCloudFoundryDebugSshArgs({
@@ -1079,6 +1274,7 @@ async function runDebugCommand(options) {
1079
1274
  processName: options.process,
1080
1275
  localPort,
1081
1276
  remotePort,
1277
+ prepareMode,
1082
1278
  }), {
1083
1279
  stdio: ["ignore", "pipe", "pipe"],
1084
1280
  shell: false,
@@ -1090,16 +1286,20 @@ async function runDebugCommand(options) {
1090
1286
  return;
1091
1287
  }
1092
1288
  hasPrintedAttachInfo = true;
1289
+ const debugUrl = await waitForNodeInspectorDebugUrl(localPort);
1093
1290
  if (debugMode === "vscode") {
1291
+ if (!debugUrl) {
1292
+ console.log(chalk.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'."));
1293
+ }
1094
1294
  printVscodeAttachInstructions({
1095
1295
  appName,
1096
1296
  instanceIndex,
1097
1297
  localPort,
1098
1298
  launchJsonPath: launchJsonPath ?? path.resolve(repositoryPath, ".vscode", "launch.json"),
1299
+ inspectorReady: Boolean(debugUrl),
1099
1300
  });
1100
1301
  return;
1101
1302
  }
1102
- const debugUrl = await waitForNodeInspectorDebugUrl(localPort);
1103
1303
  printNodeInspectorAttachInfo({ appName, instanceIndex, localPort, debugUrl });
1104
1304
  };
1105
1305
  childProcess.stdout?.on("data", (chunk) => {
@@ -1117,6 +1317,12 @@ async function runDebugCommand(options) {
1117
1317
  }, 3000);
1118
1318
  childProcess.on("close", (exitCode) => {
1119
1319
  clearTimeout(fallbackTimer);
1320
+ if (!hasPrintedAttachInfo || (exitCode ?? 0) !== 0) {
1321
+ console.log("");
1322
+ console.log(chalk.red("Debug tunnel stopped before a working inspector connection was confirmed."));
1323
+ console.log(chalk.yellow("Run smdg cf debug again and choose 'Set NODE_OPTIONS and restart app' when asked to prepare Node.js inspector."));
1324
+ console.log(chalk.gray("After the app restarts, choose VS Code guided debugging and start the attach config from VS Code Run and Debug."));
1325
+ }
1120
1326
  process.exitCode = exitCode ?? 0;
1121
1327
  });
1122
1328
  await new Promise((resolve) => {