@saptools/cf-debugger 0.1.11 → 0.1.13

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
@@ -12,7 +12,7 @@ Signal the remote process, enable SSH if needed, forward `9229` to a free local
12
12
  [![install size](https://packagephobia.com/badge?p=@saptools/cf-debugger)](https://packagephobia.com/result?p=@saptools/cf-debugger)
13
13
  [![types](https://img.shields.io/npm/types/@saptools/cf-debugger.svg?style=flat&color=3178C6&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
14
14
 
15
- [Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [API](#-programmatic-usage) • [How it works](#-how-it-works) • [FAQ](#-faq)
15
+ [Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [How it works](#-how-it-works) • [FAQ](#-faq)
16
16
 
17
17
  </div>
18
18
 
@@ -136,70 +136,6 @@ cf-debugger status --region eu10 --org my-org --space dev --app my-app
136
136
 
137
137
  ---
138
138
 
139
- ## 🧑‍💻 Programmatic Usage
140
-
141
- ```ts
142
- import {
143
- startDebugger,
144
- stopDebugger,
145
- listSessions,
146
- getSession,
147
- resolveApiEndpoint,
148
- } from "@saptools/cf-debugger";
149
-
150
- const handle = await startDebugger({
151
- region: "eu10",
152
- org: "my-org",
153
- space: "dev",
154
- app: "my-app",
155
- email: process.env["SAP_EMAIL"],
156
- password: process.env["SAP_PASSWORD"],
157
- verbose: true,
158
- onStatus: (status, message) => {
159
- console.log(`[${status}]`, message ?? "");
160
- },
161
- });
162
-
163
- console.log(`Attach your debugger to localhost:${handle.session.localPort}`);
164
-
165
- // Later — shut the tunnel down and clean up state:
166
- await handle.dispose();
167
- ```
168
-
169
- <details>
170
- <summary><b>📚 Full export list</b></summary>
171
-
172
- | Export | Description |
173
- | --- | --- |
174
- | `startDebugger(options)` | Open a tunnel; returns a `DebuggerHandle` |
175
- | `stopDebugger({ sessionId?, key? })` | Stop one session by id or by key |
176
- | `stopAllDebuggers()` | Stop every session owned by this process/machine |
177
- | `listSessions()` | Return every live session as `ActiveSession[]` |
178
- | `getSession(key)` | Return one session matching `{ region, org, space, app }` |
179
- | `resolveApiEndpoint(key, override?)` | Map a region key to its API endpoint |
180
- | `sessionKeyString(key)` | Stable string form of a session key |
181
- | `CfDebuggerError` | Rich error class with typed `code` |
182
-
183
- </details>
184
-
185
- <details>
186
- <summary><b>🧪 Error codes</b></summary>
187
-
188
- | Code | When |
189
- | --- | --- |
190
- | `MISSING_CREDENTIALS` | No `SAP_EMAIL` / `SAP_PASSWORD` in env or options |
191
- | `SESSION_ALREADY_RUNNING` | A session already exists for the same `region/org/space/app` |
192
- | `CF_LOGIN_FAILED` | `cf api` / `cf auth` rejected the credentials |
193
- | `CF_TARGET_FAILED` | Org or space not reachable |
194
- | `SSH_NOT_ENABLED` | SSH disabled at space or app level and could not be enabled |
195
- | `USR1_SIGNAL_FAILED` | Remote `kill -s USR1` failed, timed out, or was terminated by a signal |
196
- | `TUNNEL_NOT_READY` | Inspector didn't respond on port 9229 before timeout |
197
- | `PORT_UNAVAILABLE` | Preferred local port is taken and could not be freed |
198
-
199
- </details>
200
-
201
- ---
202
-
203
139
  ## 🔭 How it works
204
140
 
205
141
  ```
package/dist/cli.js CHANGED
@@ -24,24 +24,41 @@ var REGION_API_ENDPOINTS = {
24
24
  ca10: "https://api.cf.ca10.hana.ondemand.com",
25
25
  ca20: "https://api.cf.ca20.hana.ondemand.com",
26
26
  ch20: "https://api.cf.ch20.hana.ondemand.com",
27
+ cn20: "https://api.cf.cn20.platform.sapcloud.cn",
28
+ cn40: "https://api.cf.cn40.platform.sapcloud.cn",
29
+ eu01: "https://api.cf.eu01.hana.ondemand.com",
30
+ eu02: "https://api.cf.eu02.hana.ondemand.com",
27
31
  eu10: "https://api.cf.eu10.hana.ondemand.com",
32
+ "eu10-002": "https://api.cf.eu10-002.hana.ondemand.com",
33
+ "eu10-003": "https://api.cf.eu10-003.hana.ondemand.com",
34
+ "eu10-004": "https://api.cf.eu10-004.hana.ondemand.com",
35
+ "eu10-005": "https://api.cf.eu10-005.hana.ondemand.com",
28
36
  eu11: "https://api.cf.eu11.hana.ondemand.com",
29
- eu12: "https://api.cf.eu12.hana.ondemand.com",
37
+ eu13: "https://api.cf.eu13.hana.ondemand.com",
30
38
  eu20: "https://api.cf.eu20.hana.ondemand.com",
31
- eu21: "https://api.cf.eu21.hana.ondemand.com",
39
+ "eu20-001": "https://api.cf.eu20-001.hana.ondemand.com",
40
+ "eu20-002": "https://api.cf.eu20-002.hana.ondemand.com",
41
+ eu22: "https://api.cf.eu22.hana.ondemand.com",
32
42
  eu30: "https://api.cf.eu30.hana.ondemand.com",
33
- eu31: "https://api.cf.eu31.hana.ondemand.com",
43
+ il30: "https://api.cf.il30.hana.ondemand.com",
34
44
  in30: "https://api.cf.in30.hana.ondemand.com",
45
+ jp01: "https://api.cf.jp01.hana.ondemand.com",
35
46
  jp10: "https://api.cf.jp10.hana.ondemand.com",
36
47
  jp20: "https://api.cf.jp20.hana.ondemand.com",
37
48
  jp30: "https://api.cf.jp30.hana.ondemand.com",
38
- kr30: "https://api.cf.kr30.hana.ondemand.com",
49
+ jp31: "https://api.cf.jp31.hana.ondemand.com",
50
+ sa30: "https://api.cf.sa30.hana.ondemand.com",
51
+ sa31: "https://api.cf.sa31.hana.ondemand.com",
52
+ uk20: "https://api.cf.uk20.hana.ondemand.com",
53
+ us01: "https://api.cf.us01.hana.ondemand.com",
54
+ us02: "https://api.cf.us02.hana.ondemand.com",
39
55
  us10: "https://api.cf.us10.hana.ondemand.com",
56
+ "us10-001": "https://api.cf.us10-001.hana.ondemand.com",
57
+ "us10-002": "https://api.cf.us10-002.hana.ondemand.com",
40
58
  us11: "https://api.cf.us11.hana.ondemand.com",
41
59
  us20: "https://api.cf.us20.hana.ondemand.com",
42
60
  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"
61
+ us30: "https://api.cf.us30.hana.ondemand.com"
45
62
  };
46
63
  function resolveApiEndpoint(regionKey, override) {
47
64
  if (override !== void 0 && override !== "") {
@@ -245,11 +262,19 @@ function parseTargetFields(stdout) {
245
262
  }
246
263
  function regionKeyForApiEndpoint(apiEndpoint) {
247
264
  const normalized = normalizeApiEndpoint(apiEndpoint);
248
- return listKnownRegionKeys().find((key) => normalizeApiEndpoint(resolveApiEndpoint(key)) === normalized);
265
+ const known = listKnownRegionKeys().find((key) => normalizeApiEndpoint(resolveApiEndpoint(key)) === normalized);
266
+ if (known !== void 0) {
267
+ return known;
268
+ }
269
+ return regionKeyFromSapApiEndpoint(normalized);
249
270
  }
250
271
  function normalizeApiEndpoint(apiEndpoint) {
251
272
  return apiEndpoint.trim().replace(/\/+$/, "").toLowerCase();
252
273
  }
274
+ function regionKeyFromSapApiEndpoint(apiEndpoint) {
275
+ const match = /^https:\/\/api\.cf\.([a-z]{2}\d{2}(?:-\d{3})?)\.(?:hana\.ondemand\.com|platform\.sapcloud\.cn)$/.exec(apiEndpoint);
276
+ return match?.[1];
277
+ }
253
278
  function isPresent(value) {
254
279
  return value !== void 0 && value.length > 0;
255
280
  }
@@ -338,6 +363,7 @@ function sessionCfHomeDir(sessionId) {
338
363
 
339
364
  // src/network/ports.ts
340
365
  import { execFile as execFile3 } from "child_process";
366
+ import { readdir, readFile, readlink } from "fs/promises";
341
367
  import { createConnection, createServer } from "net";
342
368
  import { promisify as promisify3 } from "util";
343
369
  var execFileAsync3 = promisify3(execFile3);
@@ -372,11 +398,75 @@ async function findListeningPidsWithLsof(port) {
372
398
  return [];
373
399
  }
374
400
  }
401
+ async function findListeningPidsWithProc(port) {
402
+ const inodes = await findListeningSocketInodesWithProc(port);
403
+ if (inodes.size === 0) {
404
+ return [];
405
+ }
406
+ try {
407
+ const entries = await readdir("/proc", { withFileTypes: true });
408
+ const pids = /* @__PURE__ */ new Set();
409
+ for (const entry of entries) {
410
+ if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) {
411
+ continue;
412
+ }
413
+ if (await processHasSocketInode(entry.name, inodes)) {
414
+ pids.add(Number.parseInt(entry.name, 10));
415
+ }
416
+ }
417
+ return [...pids];
418
+ } catch {
419
+ return [];
420
+ }
421
+ }
422
+ async function findListeningSocketInodesWithProc(port) {
423
+ const inodes = /* @__PURE__ */ new Set();
424
+ await collectListeningSocketInodes("/proc/net/tcp", port, inodes);
425
+ await collectListeningSocketInodes("/proc/net/tcp6", port, inodes);
426
+ return inodes;
427
+ }
428
+ async function collectListeningSocketInodes(path, port, inodes) {
429
+ try {
430
+ const content = await readFile(path, "utf8");
431
+ for (const line of content.split("\n").slice(1)) {
432
+ const fields = line.trim().split(/\s+/);
433
+ const localAddress = fields[1];
434
+ const state = fields[3];
435
+ const inode = fields[9];
436
+ if (localAddress === void 0 || state === void 0 || inode === void 0) {
437
+ continue;
438
+ }
439
+ const localPort = Number.parseInt(localAddress.split(":")[1] ?? "", 16);
440
+ if (localPort === port && state === "0A") {
441
+ inodes.add(inode);
442
+ }
443
+ }
444
+ } catch {
445
+ }
446
+ }
447
+ async function processHasSocketInode(pid, inodes) {
448
+ try {
449
+ const descriptors = await readdir(`/proc/${pid}/fd`);
450
+ for (const descriptor of descriptors) {
451
+ try {
452
+ const link = await readlink(`/proc/${pid}/fd/${descriptor}`);
453
+ const match = /^socket:\[(\d+)\]$/.exec(link);
454
+ if (match?.[1] !== void 0 && inodes.has(match[1])) {
455
+ return true;
456
+ }
457
+ } catch {
458
+ }
459
+ }
460
+ } catch {
461
+ }
462
+ return false;
463
+ }
375
464
  async function findListeningPids(port) {
376
465
  if (process.platform === "win32") {
377
466
  return await findListeningPidsWithNetstat(port);
378
467
  }
379
- return await findListeningPidsWithLsof(port);
468
+ const lsofPids = await findListeningPidsWithLsof(port);
469
+ return lsofPids.length > 0 ? lsofPids : await findListeningPidsWithProc(port);
380
470
  }
381
471
  async function isPortFree(port) {
382
472
  return await new Promise((resolve) => {
@@ -459,7 +549,7 @@ async function killProcessOnPort(port) {
459
549
 
460
550
  // src/session-state/store.ts
461
551
  import { randomUUID } from "crypto";
462
- import { mkdir as mkdir2, readFile, rename, writeFile } from "fs/promises";
552
+ import { mkdir as mkdir2, readFile as readFile2, rename, writeFile } from "fs/promises";
463
553
  import { hostname as getHostname } from "os";
464
554
  import { dirname as dirname2 } from "path";
465
555
  import process2 from "process";
@@ -516,7 +606,7 @@ async function withFileLock(lockPath, work, options) {
516
606
  async function readJsonFile(path) {
517
607
  let raw;
518
608
  try {
519
- raw = await readFile(path, "utf8");
609
+ raw = await readFile2(path, "utf8");
520
610
  } catch (err) {
521
611
  const code = err.code;
522
612
  if (code === "ENOENT") {