@saptools/cf-debugger 0.1.7 → 0.1.9

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 CHANGED
@@ -85,7 +85,7 @@ Open a tunnel for one app and keep running until interrupted.
85
85
  ```bash
86
86
  cf-debugger start --region eu10 --org my-org --space dev --app my-app
87
87
  cf-debugger start --region eu10 --org my-org --space dev --app my-app --port 9230
88
- cf-debugger start --region eu10 --org my-org --space dev --app my-app --timeout 60 --verbose
88
+ cf-debugger start --region eu10 --org my-org --space dev --app my-app --timeout 180 --verbose
89
89
  ```
90
90
 
91
91
  | Flag | Description |
@@ -95,9 +95,13 @@ cf-debugger start --region eu10 --org my-org --space dev --app my-app --timeout
95
95
  | `--space <name>` | **Required.** CF space name |
96
96
  | `--app <name>` | **Required.** CF app name |
97
97
  | `--port <number>` | Preferred local port (auto-assigned in `20000–20999` if omitted) |
98
- | `--timeout <seconds>` | Tunnel-ready timeout (default: `30`) |
98
+ | `--timeout <seconds>` | Tunnel-ready timeout (default: `180`) |
99
99
  | `--verbose` | Print every status transition |
100
100
 
101
+ Cloud Foundry startup commands (`api`, `auth`, `target`, SSH checks, app
102
+ restart, and the one-shot SIGUSR1 SSH command) each allow up to 180 seconds.
103
+ `--timeout` controls the subsequent local tunnel-readiness probe separately.
104
+
101
105
  ### ⏹️ `cf-debugger stop`
102
106
 
103
107
  Stop a specific session or everything at once.
@@ -188,7 +192,7 @@ await handle.dispose();
188
192
  | `CF_LOGIN_FAILED` | `cf api` / `cf auth` rejected the credentials |
189
193
  | `CF_TARGET_FAILED` | Org or space not reachable |
190
194
  | `SSH_NOT_ENABLED` | SSH disabled at space or app level and could not be enabled |
191
- | `USR1_SIGNAL_FAILED` | Remote `kill -s USR1` could not find the node PID |
195
+ | `USR1_SIGNAL_FAILED` | Remote `kill -s USR1` failed, timed out, or was terminated by a signal |
192
196
  | `TUNNEL_NOT_READY` | Inspector didn't respond on port 9229 before timeout |
193
197
  | `PORT_UNAVAILABLE` | Preferred local port is taken and could not be freed |
194
198
 
package/dist/cli.js CHANGED
@@ -4,9 +4,60 @@
4
4
  import process6 from "process";
5
5
  import { Command } from "commander";
6
6
 
7
- // src/debug-session/start.ts
8
- import { mkdir as mkdir3, rm } from "fs/promises";
9
- import process4 from "process";
7
+ // src/cloud-foundry/commands.ts
8
+ import { execFile as execFile2 } from "child_process";
9
+ import { promisify as promisify2 } from "util";
10
+
11
+ // src/regions.ts
12
+ var REGION_API_ENDPOINTS = {
13
+ ae01: "https://api.cf.ae01.hana.ondemand.com",
14
+ ap01: "https://api.cf.ap01.hana.ondemand.com",
15
+ ap10: "https://api.cf.ap10.hana.ondemand.com",
16
+ ap11: "https://api.cf.ap11.hana.ondemand.com",
17
+ ap12: "https://api.cf.ap12.hana.ondemand.com",
18
+ ap20: "https://api.cf.ap20.hana.ondemand.com",
19
+ ap21: "https://api.cf.ap21.hana.ondemand.com",
20
+ ap30: "https://api.cf.ap30.hana.ondemand.com",
21
+ br10: "https://api.cf.br10.hana.ondemand.com",
22
+ br20: "https://api.cf.br20.hana.ondemand.com",
23
+ br30: "https://api.cf.br30.hana.ondemand.com",
24
+ ca10: "https://api.cf.ca10.hana.ondemand.com",
25
+ ca20: "https://api.cf.ca20.hana.ondemand.com",
26
+ ch20: "https://api.cf.ch20.hana.ondemand.com",
27
+ eu10: "https://api.cf.eu10.hana.ondemand.com",
28
+ eu11: "https://api.cf.eu11.hana.ondemand.com",
29
+ eu12: "https://api.cf.eu12.hana.ondemand.com",
30
+ eu20: "https://api.cf.eu20.hana.ondemand.com",
31
+ eu21: "https://api.cf.eu21.hana.ondemand.com",
32
+ eu30: "https://api.cf.eu30.hana.ondemand.com",
33
+ eu31: "https://api.cf.eu31.hana.ondemand.com",
34
+ in30: "https://api.cf.in30.hana.ondemand.com",
35
+ jp10: "https://api.cf.jp10.hana.ondemand.com",
36
+ jp20: "https://api.cf.jp20.hana.ondemand.com",
37
+ jp30: "https://api.cf.jp30.hana.ondemand.com",
38
+ kr30: "https://api.cf.kr30.hana.ondemand.com",
39
+ us10: "https://api.cf.us10.hana.ondemand.com",
40
+ us11: "https://api.cf.us11.hana.ondemand.com",
41
+ us20: "https://api.cf.us20.hana.ondemand.com",
42
+ us21: "https://api.cf.us21.hana.ondemand.com",
43
+ us30: "https://api.cf.us30.hana.ondemand.com",
44
+ us31: "https://api.cf.us31.hana.ondemand.com"
45
+ };
46
+ function resolveApiEndpoint(regionKey, override) {
47
+ if (override !== void 0 && override !== "") {
48
+ return override;
49
+ }
50
+ const endpoint = REGION_API_ENDPOINTS[regionKey];
51
+ if (endpoint === void 0) {
52
+ throw new Error(
53
+ `Unknown region key: ${regionKey}. Pass \`apiEndpoint\` explicitly to override.`
54
+ );
55
+ }
56
+ return endpoint;
57
+ }
58
+ function listKnownRegionKeys() {
59
+ return Object.keys(REGION_API_ENDPOINTS);
60
+ }
10
61
 
11
62
  // src/types.ts
12
63
  var CfDebuggerError = class extends Error {
@@ -27,7 +78,7 @@ import { execFile } from "child_process";
27
78
  import { promisify } from "util";
28
79
  var execFileAsync = promisify(execFile);
29
80
  var MAX_BUFFER = 16 * 1024 * 1024;
30
- var CF_CLI_TIMEOUT_MS = 3e4;
81
+ var DEFAULT_CF_COMMAND_TIMEOUT_MS = 18e4;
31
82
  var REDACTED_ARG = "<redacted>";
32
83
  function buildEnv(cfHome) {
33
84
  return { ...process.env, CF_HOME: cfHome };
@@ -50,7 +101,7 @@ function formatArgsForError(args) {
50
101
  }
51
102
  return args.map((arg, index) => index === 0 ? arg : REDACTED_ARG).join(" ");
52
103
  }
53
- async function runCf(args, context, timeoutMs = CF_CLI_TIMEOUT_MS) {
104
+ async function runCf(args, context, timeoutMs = DEFAULT_CF_COMMAND_TIMEOUT_MS) {
54
105
  try {
55
106
  const { stdout } = await execFileAsync(resolveBin(context), [...args], {
56
107
  env: buildEnv(context.cfHome),
@@ -72,8 +123,9 @@ async function runCf(args, context, timeoutMs = CF_CLI_TIMEOUT_MS) {
72
123
  }
73
124
 
74
125
  // src/cloud-foundry/commands.ts
75
- var CF_RESTART_TIMEOUT_MS = 12e4;
126
+ var execFileAsync2 = promisify2(execFile2);
76
127
  var CF_AUTH_MAX_ATTEMPTS = 3;
128
+ var CURRENT_TARGET_TIMEOUT_MS = 3e4;
77
129
  async function cfApi(apiEndpoint, context) {
78
130
  await runCf(["api", apiEndpoint], context);
79
131
  }
@@ -137,13 +189,74 @@ async function cfEnableSsh(appName, context) {
137
189
  }
138
190
  }
139
191
  async function cfRestartApp(appName, context) {
140
- await runCf(["restart", appName], context, CF_RESTART_TIMEOUT_MS);
192
+ await runCf(["restart", appName], context);
193
+ }
194
+ async function readCurrentCfTarget(options = {}) {
195
+ try {
196
+ const { stdout } = await execFileAsync2(options.command ?? process.env["CF_DEBUGGER_CF_BIN"] ?? "cf", ["target"], {
197
+ env: options.env ?? process.env,
198
+ maxBuffer: 16 * 1024 * 1024,
199
+ timeout: options.timeoutMs ?? CURRENT_TARGET_TIMEOUT_MS
200
+ });
201
+ return parseCurrentCfTarget(stdout);
202
+ } catch (error) {
203
+ const message = error instanceof Error ? error.message : String(error);
204
+ throw new CfDebuggerError("CF_TARGET_FAILED", `cf target failed: ${message}`);
205
+ }
206
+ }
207
+ function parseCurrentCfTarget(stdout) {
208
+ const fields = parseTargetFields(stdout);
209
+ const apiEndpoint = fields.get("api endpoint");
210
+ const org = fields.get("org");
211
+ const space = fields.get("space");
212
+ if (!isPresent(apiEndpoint) || !isPresent(org) || !isPresent(space)) {
213
+ return void 0;
214
+ }
215
+ const region = regionKeyForApiEndpoint(apiEndpoint);
216
+ return {
217
+ apiEndpoint,
218
+ ...region === void 0 ? {} : { region },
219
+ org,
220
+ space
221
+ };
222
+ }
223
+ function requireCurrentCfRegion(target, instruction = "Pass --region explicitly.") {
224
+ if (target.region !== void 0) {
225
+ return target.region;
226
+ }
227
+ throw new CfDebuggerError(
228
+ "CF_TARGET_FAILED",
229
+ `Current CF API endpoint "${target.apiEndpoint}" does not match a known SAP region. ${instruction}`
230
+ );
231
+ }
232
+ function parseTargetFields(stdout) {
233
+ return new Map(
234
+ stdout.split("\n").map((line) => {
235
+ const separator = line.indexOf(":");
236
+ if (separator < 0) {
237
+ return void 0;
238
+ }
239
+ return [
240
+ line.slice(0, separator).trim().toLowerCase(),
241
+ line.slice(separator + 1).trim()
242
+ ];
243
+ }).filter((field) => field !== void 0)
244
+ );
245
+ }
246
+ function regionKeyForApiEndpoint(apiEndpoint) {
247
+ const normalized = normalizeApiEndpoint(apiEndpoint);
248
+ return listKnownRegionKeys().find((key) => normalizeApiEndpoint(resolveApiEndpoint(key)) === normalized);
249
+ }
250
+ function normalizeApiEndpoint(apiEndpoint) {
251
+ return apiEndpoint.trim().replace(/\/+$/, "").toLowerCase();
252
+ }
253
+ function isPresent(value) {
254
+ return value !== void 0 && value.length > 0;
141
255
  }
142
256
 
143
257
  // src/cloud-foundry/ssh.ts
144
258
  import { spawn } from "child_process";
145
- var CF_SSH_SIGNAL_TIMEOUT_MS = 15e3;
146
- async function cfSshOneShot(appName, command, context) {
259
+ async function cfSshOneShot(appName, command, context, timeoutMs = DEFAULT_CF_COMMAND_TIMEOUT_MS) {
147
260
  return await new Promise((resolve) => {
148
261
  const child = spawn(resolveBin(context), ["ssh", appName, "-c", command], {
149
262
  env: buildEnv(context.cfHome),
@@ -160,18 +273,20 @@ async function cfSshOneShot(appName, command, context) {
160
273
  child.kill();
161
274
  } catch {
162
275
  }
163
- resolve({ exitCode: null, stderr: stderrBuf });
164
- }, CF_SSH_SIGNAL_TIMEOUT_MS);
276
+ resolve({ exitCode: null, stderr: stderrBuf, timedOutAfterMs: timeoutMs });
277
+ }, timeoutMs);
165
278
  child.stderr.on("data", (data) => {
166
279
  stderrBuf += data.toString();
167
280
  });
168
- child.on("close", (code) => {
281
+ child.on("close", (code, signal) => {
169
282
  if (settled) {
170
283
  return;
171
284
  }
172
285
  settled = true;
173
286
  clearTimeout(timeout);
174
- resolve({ exitCode: code, stderr: stderrBuf });
287
+ resolve(
288
+ signal === null ? { exitCode: code, stderr: stderrBuf } : { exitCode: code, stderr: stderrBuf, signal }
289
+ );
175
290
  });
176
291
  child.on("error", (err) => {
177
292
  if (settled) {
@@ -197,6 +312,10 @@ function spawnSshTunnel(appName, localPort, remotePort, context) {
197
312
  });
198
313
  }
199
314
 
315
+ // src/debug-session/start.ts
316
+ import { mkdir as mkdir3, rm } from "fs/promises";
317
+ import process4 from "process";
318
+
200
319
  // src/paths.ts
201
320
  import { homedir } from "os";
202
321
  import { join } from "path";
@@ -218,13 +337,13 @@ function sessionCfHomeDir(sessionId) {
218
337
  }
219
338
 
220
339
  // src/network/ports.ts
221
- import { execFile as execFile2 } from "child_process";
340
+ import { execFile as execFile3 } from "child_process";
222
341
  import { createConnection, createServer } from "net";
223
- import { promisify as promisify2 } from "util";
224
- var execFileAsync2 = promisify2(execFile2);
342
+ import { promisify as promisify3 } from "util";
343
+ var execFileAsync3 = promisify3(execFile3);
225
344
  async function findListeningPidsWithNetstat(port) {
226
345
  try {
227
- const { stdout } = await execFileAsync2("netstat", ["-ano"]);
346
+ const { stdout } = await execFileAsync3("netstat", ["-ano"]);
228
347
  const pids = /* @__PURE__ */ new Set();
229
348
  for (const line of stdout.split("\n")) {
230
349
  if (!line.includes(`:${port.toString()}`) || !line.includes("LISTENING")) {
@@ -247,7 +366,7 @@ async function findListeningPidsWithNetstat(port) {
247
366
  }
248
367
  async function findListeningPidsWithLsof(port) {
249
368
  try {
250
- const { stdout } = await execFileAsync2("lsof", ["-nP", "-t", "-i", `tcp:${port.toString()}`, "-sTCP:LISTEN"]);
369
+ const { stdout } = await execFileAsync3("lsof", ["-nP", "-t", "-i", `tcp:${port.toString()}`, "-sTCP:LISTEN"]);
251
370
  return stdout.trim().split("\n").filter((line) => line.length > 0).map((line) => Number.parseInt(line, 10)).filter((pid) => !Number.isNaN(pid));
252
371
  } catch {
253
372
  return [];
@@ -313,7 +432,7 @@ async function killProcessOnPort(port) {
313
432
  const pids = await findListeningPidsWithNetstat(port);
314
433
  for (const pid of pids) {
315
434
  try {
316
- await execFileAsync2("taskkill", ["/F", "/PID", pid.toString()]);
435
+ await execFileAsync3("taskkill", ["/F", "/PID", pid.toString()]);
317
436
  } catch {
318
437
  }
319
438
  }
@@ -322,7 +441,7 @@ async function killProcessOnPort(port) {
322
441
  return;
323
442
  }
324
443
  try {
325
- const { stdout } = await execFileAsync2("lsof", ["-t", "-i", `tcp:${portStr}`]);
444
+ const { stdout } = await execFileAsync3("lsof", ["-t", "-i", `tcp:${portStr}`]);
326
445
  const lines = stdout.trim().split("\n").filter((line) => line.length > 0);
327
446
  for (const line of lines) {
328
447
  const pid = Number.parseInt(line, 10);
@@ -338,54 +457,6 @@ async function killProcessOnPort(port) {
338
457
  }
339
458
  }
340
459
 
341
- // src/regions.ts
342
- var REGION_API_ENDPOINTS = {
343
- ae01: "https://api.cf.ae01.hana.ondemand.com",
344
- ap01: "https://api.cf.ap01.hana.ondemand.com",
345
- ap10: "https://api.cf.ap10.hana.ondemand.com",
346
- ap11: "https://api.cf.ap11.hana.ondemand.com",
347
- ap12: "https://api.cf.ap12.hana.ondemand.com",
348
- ap20: "https://api.cf.ap20.hana.ondemand.com",
349
- ap21: "https://api.cf.ap21.hana.ondemand.com",
350
- ap30: "https://api.cf.ap30.hana.ondemand.com",
351
- br10: "https://api.cf.br10.hana.ondemand.com",
352
- br20: "https://api.cf.br20.hana.ondemand.com",
353
- br30: "https://api.cf.br30.hana.ondemand.com",
354
- ca10: "https://api.cf.ca10.hana.ondemand.com",
355
- ca20: "https://api.cf.ca20.hana.ondemand.com",
356
- ch20: "https://api.cf.ch20.hana.ondemand.com",
357
- eu10: "https://api.cf.eu10.hana.ondemand.com",
358
- eu11: "https://api.cf.eu11.hana.ondemand.com",
359
- eu12: "https://api.cf.eu12.hana.ondemand.com",
360
- eu20: "https://api.cf.eu20.hana.ondemand.com",
361
- eu21: "https://api.cf.eu21.hana.ondemand.com",
362
- eu30: "https://api.cf.eu30.hana.ondemand.com",
363
- eu31: "https://api.cf.eu31.hana.ondemand.com",
364
- in30: "https://api.cf.in30.hana.ondemand.com",
365
- jp10: "https://api.cf.jp10.hana.ondemand.com",
366
- jp20: "https://api.cf.jp20.hana.ondemand.com",
367
- jp30: "https://api.cf.jp30.hana.ondemand.com",
368
- kr30: "https://api.cf.kr30.hana.ondemand.com",
369
- us10: "https://api.cf.us10.hana.ondemand.com",
370
- us11: "https://api.cf.us11.hana.ondemand.com",
371
- us20: "https://api.cf.us20.hana.ondemand.com",
372
- us21: "https://api.cf.us21.hana.ondemand.com",
373
- us30: "https://api.cf.us30.hana.ondemand.com",
374
- us31: "https://api.cf.us31.hana.ondemand.com"
375
- };
376
- function resolveApiEndpoint(regionKey, override) {
377
- if (override !== void 0 && override !== "") {
378
- return override;
379
- }
380
- const endpoint = REGION_API_ENDPOINTS[regionKey];
381
- if (endpoint === void 0) {
382
- throw new Error(
383
- `Unknown region key: ${regionKey}. Pass \`apiEndpoint\` explicitly to override.`
384
- );
385
- }
386
- return endpoint;
387
- }
388
-
389
460
  // src/session-state/store.ts
390
461
  import { randomUUID } from "crypto";
391
462
  import { mkdir as mkdir2, readFile, rename, writeFile } from "fs/promises";
@@ -680,7 +751,7 @@ async function removeSession(sessionId) {
680
751
  }
681
752
 
682
753
  // src/debug-session/constants.ts
683
- var DEFAULT_TUNNEL_READY_TIMEOUT_MS = 3e4;
754
+ var DEFAULT_TUNNEL_READY_TIMEOUT_MS = 18e4;
684
755
  var POST_USR1_DELAY_MS = 300;
685
756
  var PORT_CLEANUP_DELAY_MS = 600;
686
757
  var CHILD_SIGTERM_GRACE_MS = 2e3;
@@ -743,6 +814,19 @@ async function killProcessGroupOrProc(child, timeoutMs = CHILD_SIGTERM_GRACE_MS)
743
814
  }
744
815
 
745
816
  // src/debug-session/start.ts
817
+ function signalFailureDetail(result) {
818
+ if (result.timedOutAfterMs !== void 0) {
819
+ return `timed out after ${(result.timedOutAfterMs / 1e3).toString()}s`;
820
+ }
821
+ const stderr = result.stderr.trim();
822
+ if (stderr.length > 0) {
823
+ return stderr;
824
+ }
825
+ if (result.signal !== void 0) {
826
+ return `terminated by signal ${result.signal}`;
827
+ }
828
+ return `exit code ${String(result.exitCode)}`;
829
+ }
746
830
  function checkAbort(signal) {
747
831
  if (signal?.aborted) {
748
832
  throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
@@ -802,10 +886,9 @@ async function signalRemoteNode(options, context, sessionId, emit) {
802
886
  if (signalResult.exitCode === 0) {
803
887
  return;
804
888
  }
805
- const detail = signalResult.stderr.trim().length > 0 ? signalResult.stderr.trim() : `exit code ${String(signalResult.exitCode)}`;
806
889
  throw new CfDebuggerError(
807
890
  "USR1_SIGNAL_FAILED",
808
- `Failed to send SIGUSR1 to the Node.js process on ${options.app}: ${detail}`,
891
+ `Failed to send SIGUSR1 to the Node.js process on ${options.app}: ${signalFailureDetail(signalResult)}`,
809
892
  signalResult.stderr
810
893
  );
811
894
  }
@@ -832,10 +915,9 @@ async function retryRemoteSignal(options, context, sessionId, emit) {
832
915
  if (retrySignalResult.exitCode === 0) {
833
916
  return;
834
917
  }
835
- const detail = retrySignalResult.stderr.trim().length > 0 ? retrySignalResult.stderr.trim() : `exit code ${String(retrySignalResult.exitCode)}`;
836
918
  throw new CfDebuggerError(
837
919
  "USR1_SIGNAL_FAILED",
838
- `Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${detail}`,
920
+ `Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${signalFailureDetail(retrySignalResult)}`,
839
921
  retrySignalResult.stderr
840
922
  );
841
923
  }
@@ -1074,10 +1156,8 @@ function logStatus(verbose, status, message) {
1074
1156
  }
1075
1157
  }
1076
1158
  async function handleStart(opts) {
1077
- const region = readRequiredOption(opts.region, "--region");
1078
- const org = readRequiredOption(opts.org, "--org");
1079
- const space = readRequiredOption(opts.space, "--space");
1080
1159
  const app = readRequiredOption(opts.app, "--app");
1160
+ const key = await resolveSessionKey({ ...opts, app });
1081
1161
  const verbose = opts.verbose ?? false;
1082
1162
  const preferredPort = parseOptionalPort(opts.port);
1083
1163
  const tunnelReadyTimeoutMs = parseOptionalTimeout(opts.timeout);
@@ -1098,9 +1178,9 @@ Aborting startup for ${app}...
1098
1178
  let handle;
1099
1179
  try {
1100
1180
  handle = await startDebugger({
1101
- region,
1102
- org,
1103
- space,
1181
+ region: key.region,
1182
+ org: key.org,
1183
+ space: key.space,
1104
1184
  app,
1105
1185
  verbose,
1106
1186
  signal: abortController.signal,
@@ -1115,7 +1195,7 @@ Aborting startup for ${app}...
1115
1195
  process6.off("SIGTERM", startupSigterm);
1116
1196
  }
1117
1197
  process6.stdout.write(
1118
- `Debugger ready for ${app} (${region}/${org}/${space}).
1198
+ `Debugger ready for ${app} (${key.region}/${key.org}/${key.space}).
1119
1199
  Local port: ${handle.session.localPort.toString()}
1120
1200
  Remote port: ${handle.session.remotePort.toString()}
1121
1201
  Session id: ${handle.session.sessionId}
@@ -1153,16 +1233,58 @@ Stopping debugger for ${app}...
1153
1233
  await dispose();
1154
1234
  process6.exit(code ?? 0);
1155
1235
  }
1156
- function resolveKeyFromOpts(opts) {
1157
- if (opts.region !== void 0 && opts.org !== void 0 && opts.space !== void 0 && opts.app !== void 0) {
1236
+ function hasText(value) {
1237
+ return optionalText(value) !== void 0;
1238
+ }
1239
+ function optionalText(value) {
1240
+ const trimmed = value?.trim();
1241
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
1242
+ }
1243
+ function currentCfOptions() {
1244
+ const command = process6.env["CF_DEBUGGER_CF_BIN"];
1245
+ return command === void 0 ? void 0 : { command };
1246
+ }
1247
+ async function resolveSessionKey(opts) {
1248
+ const app = readRequiredOption(opts.app, "--app");
1249
+ const region = optionalText(opts.region);
1250
+ const org = optionalText(opts.org);
1251
+ const space = optionalText(opts.space);
1252
+ if (region !== void 0 && org !== void 0 && space !== void 0) {
1158
1253
  return {
1159
- region: opts.region,
1160
- org: opts.org,
1161
- space: opts.space,
1162
- app: opts.app
1254
+ region,
1255
+ org,
1256
+ space,
1257
+ app
1163
1258
  };
1164
1259
  }
1165
- return void 0;
1260
+ const current = await readCurrentCfTarget(currentCfOptions()).catch((error) => {
1261
+ throw new CfDebuggerError(
1262
+ "CF_TARGET_FAILED",
1263
+ "No current CF target found. Run `cf target -o <org> -s <space>` or pass --region/--org/--space.",
1264
+ error instanceof Error ? error.message : String(error)
1265
+ );
1266
+ });
1267
+ if (current === void 0) {
1268
+ throw new CfDebuggerError(
1269
+ "CF_TARGET_FAILED",
1270
+ "No current CF target found. Run `cf target -o <org> -s <space>` or pass --region/--org/--space."
1271
+ );
1272
+ }
1273
+ return {
1274
+ region: region ?? requireCurrentCfRegion(current),
1275
+ org: org ?? current.org,
1276
+ space: space ?? current.space,
1277
+ app
1278
+ };
1279
+ }
1280
+ async function resolveOptionalSessionKey(opts) {
1281
+ if (!hasText(opts.app)) {
1282
+ if (hasText(opts.region) || hasText(opts.org) || hasText(opts.space)) {
1283
+ readRequiredOption(opts.app, "--app");
1284
+ }
1285
+ return void 0;
1286
+ }
1287
+ return await resolveSessionKey(opts);
1166
1288
  }
1167
1289
  async function handleStop(opts) {
1168
1290
  if (opts.all === true) {
@@ -1171,7 +1293,7 @@ async function handleStop(opts) {
1171
1293
  `);
1172
1294
  return;
1173
1295
  }
1174
- const key = resolveKeyFromOpts(opts);
1296
+ const key = await resolveOptionalSessionKey(opts);
1175
1297
  const result = await stopDebugger({
1176
1298
  ...opts.sessionId === void 0 ? {} : { sessionId: opts.sessionId },
1177
1299
  ...key === void 0 ? {} : { key }
@@ -1191,19 +1313,14 @@ async function handleList() {
1191
1313
  `);
1192
1314
  }
1193
1315
  async function handleStatus(opts) {
1194
- const session = await getSession({
1195
- region: opts.region,
1196
- org: opts.org,
1197
- space: opts.space,
1198
- app: opts.app
1199
- });
1316
+ const session = await getSession(await resolveSessionKey(opts));
1200
1317
  process6.stdout.write(`${JSON.stringify(session ?? null, null, 2)}
1201
1318
  `);
1202
1319
  }
1203
1320
  async function main(argv) {
1204
1321
  const program = new Command();
1205
1322
  program.name("cf-debugger").description("Open an SSH debug tunnel to a SAP BTP Cloud Foundry app's Node.js inspector");
1206
- program.command("start").description("Open a debug tunnel for one app").requiredOption("--region <key>", "CF region key (e.g. eu10)").requiredOption("--org <name>", "CF org name").requiredOption("--space <name>", "CF space name").requiredOption("--app <name>", "CF app name").option("--port <number>", "Preferred local port (auto-assigned if omitted)").option("--timeout <seconds>", "Tunnel-ready timeout in seconds (default: 30)").option("--verbose", "Print status transitions", false).action(async (opts) => {
1323
+ program.command("start").description("Open a debug tunnel for one app").option("--region <key>", "CF region key (default: current cf target)").option("--org <name>", "CF org name (default: current cf target)").option("--space <name>", "CF space name (default: current cf target)").requiredOption("--app <name>", "CF app name").option("--port <number>", "Preferred local port (auto-assigned if omitted)").option("--timeout <seconds>", "Tunnel-ready timeout in seconds (default: 180)").option("--verbose", "Print status transitions", false).action(async (opts) => {
1207
1324
  await handleStart(opts);
1208
1325
  });
1209
1326
  program.command("stop").description("Stop one session (by key or id) or all sessions with --all").option("--region <key>").option("--org <name>").option("--space <name>").option("--app <name>").option("--session-id <id>").option("--all", "Stop every active session", false).action(async (opts) => {
@@ -1212,7 +1329,7 @@ async function main(argv) {
1212
1329
  program.command("list").description("Print every active debugger session as JSON").action(async () => {
1213
1330
  await handleList();
1214
1331
  });
1215
- program.command("status").description("Print one session by key as JSON (null if not active)").requiredOption("--region <key>").requiredOption("--org <name>").requiredOption("--space <name>").requiredOption("--app <name>").action(async (opts) => {
1332
+ program.command("status").description("Print one session by key as JSON (null if not active)").option("--region <key>").option("--org <name>").option("--space <name>").requiredOption("--app <name>").action(async (opts) => {
1216
1333
  await handleStatus(opts);
1217
1334
  });
1218
1335
  await program.parseAsync([...argv]);