@saptools/cf-debugger 0.1.8 → 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/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 {
@@ -72,7 +123,9 @@ async function runCf(args, context, timeoutMs = DEFAULT_CF_COMMAND_TIMEOUT_MS) {
72
123
  }
73
124
 
74
125
  // src/cloud-foundry/commands.ts
126
+ var execFileAsync2 = promisify2(execFile2);
75
127
  var CF_AUTH_MAX_ATTEMPTS = 3;
128
+ var CURRENT_TARGET_TIMEOUT_MS = 3e4;
76
129
  async function cfApi(apiEndpoint, context) {
77
130
  await runCf(["api", apiEndpoint], context);
78
131
  }
@@ -138,6 +191,68 @@ async function cfEnableSsh(appName, context) {
138
191
  async function cfRestartApp(appName, context) {
139
192
  await runCf(["restart", appName], context);
140
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;
255
+ }
141
256
 
142
257
  // src/cloud-foundry/ssh.ts
143
258
  import { spawn } from "child_process";
@@ -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";
@@ -1085,10 +1156,8 @@ function logStatus(verbose, status, message) {
1085
1156
  }
1086
1157
  }
1087
1158
  async function handleStart(opts) {
1088
- const region = readRequiredOption(opts.region, "--region");
1089
- const org = readRequiredOption(opts.org, "--org");
1090
- const space = readRequiredOption(opts.space, "--space");
1091
1159
  const app = readRequiredOption(opts.app, "--app");
1160
+ const key = await resolveSessionKey({ ...opts, app });
1092
1161
  const verbose = opts.verbose ?? false;
1093
1162
  const preferredPort = parseOptionalPort(opts.port);
1094
1163
  const tunnelReadyTimeoutMs = parseOptionalTimeout(opts.timeout);
@@ -1109,9 +1178,9 @@ Aborting startup for ${app}...
1109
1178
  let handle;
1110
1179
  try {
1111
1180
  handle = await startDebugger({
1112
- region,
1113
- org,
1114
- space,
1181
+ region: key.region,
1182
+ org: key.org,
1183
+ space: key.space,
1115
1184
  app,
1116
1185
  verbose,
1117
1186
  signal: abortController.signal,
@@ -1126,7 +1195,7 @@ Aborting startup for ${app}...
1126
1195
  process6.off("SIGTERM", startupSigterm);
1127
1196
  }
1128
1197
  process6.stdout.write(
1129
- `Debugger ready for ${app} (${region}/${org}/${space}).
1198
+ `Debugger ready for ${app} (${key.region}/${key.org}/${key.space}).
1130
1199
  Local port: ${handle.session.localPort.toString()}
1131
1200
  Remote port: ${handle.session.remotePort.toString()}
1132
1201
  Session id: ${handle.session.sessionId}
@@ -1164,16 +1233,58 @@ Stopping debugger for ${app}...
1164
1233
  await dispose();
1165
1234
  process6.exit(code ?? 0);
1166
1235
  }
1167
- function resolveKeyFromOpts(opts) {
1168
- 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) {
1169
1253
  return {
1170
- region: opts.region,
1171
- org: opts.org,
1172
- space: opts.space,
1173
- app: opts.app
1254
+ region,
1255
+ org,
1256
+ space,
1257
+ app
1174
1258
  };
1175
1259
  }
1176
- 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);
1177
1288
  }
1178
1289
  async function handleStop(opts) {
1179
1290
  if (opts.all === true) {
@@ -1182,7 +1293,7 @@ async function handleStop(opts) {
1182
1293
  `);
1183
1294
  return;
1184
1295
  }
1185
- const key = resolveKeyFromOpts(opts);
1296
+ const key = await resolveOptionalSessionKey(opts);
1186
1297
  const result = await stopDebugger({
1187
1298
  ...opts.sessionId === void 0 ? {} : { sessionId: opts.sessionId },
1188
1299
  ...key === void 0 ? {} : { key }
@@ -1202,19 +1313,14 @@ async function handleList() {
1202
1313
  `);
1203
1314
  }
1204
1315
  async function handleStatus(opts) {
1205
- const session = await getSession({
1206
- region: opts.region,
1207
- org: opts.org,
1208
- space: opts.space,
1209
- app: opts.app
1210
- });
1316
+ const session = await getSession(await resolveSessionKey(opts));
1211
1317
  process6.stdout.write(`${JSON.stringify(session ?? null, null, 2)}
1212
1318
  `);
1213
1319
  }
1214
1320
  async function main(argv) {
1215
1321
  const program = new Command();
1216
1322
  program.name("cf-debugger").description("Open an SSH debug tunnel to a SAP BTP Cloud Foundry app's Node.js inspector");
1217
- 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: 180)").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) => {
1218
1324
  await handleStart(opts);
1219
1325
  });
1220
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) => {
@@ -1223,7 +1329,7 @@ async function main(argv) {
1223
1329
  program.command("list").description("Print every active debugger session as JSON").action(async () => {
1224
1330
  await handleList();
1225
1331
  });
1226
- 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) => {
1227
1333
  await handleStatus(opts);
1228
1334
  });
1229
1335
  await program.parseAsync([...argv]);