@saptools/cf-debugger 0.1.8 → 0.1.10

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";
@@ -1084,11 +1155,23 @@ function logStatus(verbose, status, message) {
1084
1155
  `);
1085
1156
  }
1086
1157
  }
1087
- 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
- const app = readRequiredOption(opts.app, "--app");
1158
+ function mergeSelector(selector, opts) {
1159
+ if (selector === void 0) {
1160
+ return opts;
1161
+ }
1162
+ const parts = selector.split("/");
1163
+ if (parts.length === 4) {
1164
+ return { ...opts, region: opts.region ?? parts[0], org: opts.org ?? parts[1], space: opts.space ?? parts[2], app: opts.app ?? parts[3] };
1165
+ }
1166
+ if (parts.length === 1) {
1167
+ return { ...opts, app: opts.app ?? parts[0] };
1168
+ }
1169
+ throw new CfDebuggerError("UNSAFE_INPUT", "Invalid app selector format. Expected <app> or <region>/<org>/<space>/<app>.");
1170
+ }
1171
+ async function handleStart(selector, rawOpts) {
1172
+ const opts = mergeSelector(selector, rawOpts);
1173
+ const app = readRequiredOption(opts.app, "--app or selector");
1174
+ const key = await resolveSessionKey({ ...opts, app });
1092
1175
  const verbose = opts.verbose ?? false;
1093
1176
  const preferredPort = parseOptionalPort(opts.port);
1094
1177
  const tunnelReadyTimeoutMs = parseOptionalTimeout(opts.timeout);
@@ -1109,9 +1192,9 @@ Aborting startup for ${app}...
1109
1192
  let handle;
1110
1193
  try {
1111
1194
  handle = await startDebugger({
1112
- region,
1113
- org,
1114
- space,
1195
+ region: key.region,
1196
+ org: key.org,
1197
+ space: key.space,
1115
1198
  app,
1116
1199
  verbose,
1117
1200
  signal: abortController.signal,
@@ -1126,7 +1209,7 @@ Aborting startup for ${app}...
1126
1209
  process6.off("SIGTERM", startupSigterm);
1127
1210
  }
1128
1211
  process6.stdout.write(
1129
- `Debugger ready for ${app} (${region}/${org}/${space}).
1212
+ `Debugger ready for ${app} (${key.region}/${key.org}/${key.space}).
1130
1213
  Local port: ${handle.session.localPort.toString()}
1131
1214
  Remote port: ${handle.session.remotePort.toString()}
1132
1215
  Session id: ${handle.session.sessionId}
@@ -1164,25 +1247,68 @@ Stopping debugger for ${app}...
1164
1247
  await dispose();
1165
1248
  process6.exit(code ?? 0);
1166
1249
  }
1167
- function resolveKeyFromOpts(opts) {
1168
- if (opts.region !== void 0 && opts.org !== void 0 && opts.space !== void 0 && opts.app !== void 0) {
1250
+ function hasText(value) {
1251
+ return optionalText(value) !== void 0;
1252
+ }
1253
+ function optionalText(value) {
1254
+ const trimmed = value?.trim();
1255
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
1256
+ }
1257
+ function currentCfOptions() {
1258
+ const command = process6.env["CF_DEBUGGER_CF_BIN"];
1259
+ return command === void 0 ? void 0 : { command };
1260
+ }
1261
+ async function resolveSessionKey(opts) {
1262
+ const app = readRequiredOption(opts.app, "--app or selector");
1263
+ const region = optionalText(opts.region);
1264
+ const org = optionalText(opts.org);
1265
+ const space = optionalText(opts.space);
1266
+ if (region !== void 0 && org !== void 0 && space !== void 0) {
1169
1267
  return {
1170
- region: opts.region,
1171
- org: opts.org,
1172
- space: opts.space,
1173
- app: opts.app
1268
+ region,
1269
+ org,
1270
+ space,
1271
+ app
1174
1272
  };
1175
1273
  }
1176
- return void 0;
1274
+ const current = await readCurrentCfTarget(currentCfOptions()).catch((error) => {
1275
+ throw new CfDebuggerError(
1276
+ "CF_TARGET_FAILED",
1277
+ "No current CF target found. Run `cf target -o <org> -s <space>` or pass --region/--org/--space.",
1278
+ error instanceof Error ? error.message : String(error)
1279
+ );
1280
+ });
1281
+ if (current === void 0) {
1282
+ throw new CfDebuggerError(
1283
+ "CF_TARGET_FAILED",
1284
+ "No current CF target found. Run `cf target -o <org> -s <space>` or pass --region/--org/--space."
1285
+ );
1286
+ }
1287
+ return {
1288
+ region: region ?? requireCurrentCfRegion(current),
1289
+ org: org ?? current.org,
1290
+ space: space ?? current.space,
1291
+ app
1292
+ };
1177
1293
  }
1178
- async function handleStop(opts) {
1294
+ async function resolveOptionalSessionKey(opts) {
1295
+ if (!hasText(opts.app)) {
1296
+ if (hasText(opts.region) || hasText(opts.org) || hasText(opts.space)) {
1297
+ readRequiredOption(opts.app, "--app");
1298
+ }
1299
+ return void 0;
1300
+ }
1301
+ return await resolveSessionKey(opts);
1302
+ }
1303
+ async function handleStop(selector, rawOpts) {
1304
+ const opts = mergeSelector(selector, rawOpts);
1179
1305
  if (opts.all === true) {
1180
1306
  const count = await stopAllDebuggers();
1181
1307
  process6.stdout.write(`Stopped ${count.toString()} session(s).
1182
1308
  `);
1183
1309
  return;
1184
1310
  }
1185
- const key = resolveKeyFromOpts(opts);
1311
+ const key = await resolveOptionalSessionKey(opts);
1186
1312
  const result = await stopDebugger({
1187
1313
  ...opts.sessionId === void 0 ? {} : { sessionId: opts.sessionId },
1188
1314
  ...key === void 0 ? {} : { key }
@@ -1201,30 +1327,26 @@ async function handleList() {
1201
1327
  process6.stdout.write(`${JSON.stringify(sessions, null, 2)}
1202
1328
  `);
1203
1329
  }
1204
- 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
- });
1330
+ async function handleStatus(selector, rawOpts) {
1331
+ const opts = mergeSelector(selector, rawOpts);
1332
+ const session = await getSession(await resolveSessionKey(opts));
1211
1333
  process6.stdout.write(`${JSON.stringify(session ?? null, null, 2)}
1212
1334
  `);
1213
1335
  }
1214
1336
  async function main(argv) {
1215
1337
  const program = new Command();
1216
1338
  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) => {
1218
- await handleStart(opts);
1339
+ program.command("start").description("Open a debug tunnel for one app").argument("[selector]", "Optional app selector: `<app>` or `region/org/space/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)").option("--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 (selector, opts) => {
1340
+ await handleStart(selector, opts);
1219
1341
  });
1220
- 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) => {
1221
- await handleStop(opts);
1342
+ program.command("stop").description("Stop one session (by key or id) or all sessions with --all").argument("[selector]", "Optional app selector: `<app>` or `region/org/space/app`").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 (selector, opts) => {
1343
+ await handleStop(selector, opts);
1222
1344
  });
1223
1345
  program.command("list").description("Print every active debugger session as JSON").action(async () => {
1224
1346
  await handleList();
1225
1347
  });
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) => {
1227
- await handleStatus(opts);
1348
+ program.command("status").description("Print one session by key as JSON (null if not active)").argument("[selector]", "Optional app selector: `<app>` or `region/org/space/app`").option("--region <key>").option("--org <name>").option("--space <name>").option("--app <name>").action(async (selector, opts) => {
1349
+ await handleStatus(selector, opts);
1228
1350
  });
1229
1351
  await program.parseAsync([...argv]);
1230
1352
  }