@saptools/cf-debugger 0.1.16 โ†’ 0.2.0

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
@@ -1,323 +1,374 @@
1
- <div align="center">
1
+ # `@saptools/cf-debugger`
2
2
 
3
- # ๐Ÿ› `@saptools/cf-debugger`
3
+ Open a verified Node.js inspector tunnel to an SAP BTP Cloud Foundry app from one
4
+ command. Each concurrent session has its own local port, isolated `CF_HOME`, and
5
+ lock-guarded state entry.
4
6
 
5
- **Open a Node.js inspector tunnel to any SAP BTP Cloud Foundry app โ€” in one command.**
6
-
7
- Signal the remote process, enable SSH if needed, forward `9229` to a free local port, and hand you back a ready-to-attach debugger โ€” with first-class support for **multiple concurrent tunnels** across terminals.
8
-
9
- [![npm version](https://img.shields.io/npm/v/@saptools/cf-debugger.svg?style=flat&color=CB3837&logo=npm)](https://www.npmjs.com/package/@saptools/cf-debugger)
10
- [![license](https://img.shields.io/npm/l/@saptools/cf-debugger.svg?style=flat&color=blue)](./LICENSE)
11
- [![node](https://img.shields.io/node/v/@saptools/cf-debugger.svg?style=flat&color=339933&logo=node.js&logoColor=white)](https://nodejs.org)
12
- [![install size](https://packagephobia.com/badge?p=@saptools/cf-debugger)](https://packagephobia.com/result?p=@saptools/cf-debugger)
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
-
15
- [Install](#-install) โ€ข [Quick Start](#-quick-start) โ€ข [CLI](#-cli) โ€ข [How it works](#-how-it-works) โ€ข [FAQ](#-faq)
16
-
17
- </div>
18
-
19
- ---
20
-
21
- ## โœจ Features
22
-
23
- - ๐Ÿš€ **One-shot tunnel** โ€” auth, target, SSH-enable, USR1 signal, port forward, readiness probe โ€” all hidden behind `cf-debugger start`
24
- - ๐Ÿงต **Multi-debugger concurrency** โ€” run N debuggers for N apps at once; each session gets its own local port, isolated `CF_HOME`, and an entry in the shared state file
25
- - ๐ŸŽฏ **Exact process targeting** โ€” select a CF process, instance, and optional Node PID; when several Node processes exist the one owning the app's `$PORT` listening socket is auto-selected, otherwise selection fails closed
26
- - ๐Ÿ›ก๏ธ **Duplicate-session protection** โ€” the same `region/org/space/app/process/instance` cannot be debugged twice simultaneously (returns `SESSION_ALREADY_RUNNING`)
27
- - ๐Ÿงน **Crash-proof state** โ€” provably dead entries are pruned, while ownership mismatches are retained for safe recovery instead of being deleted blindly
28
- - ๐Ÿ”Œ **Deterministic ports** โ€” auto-assigned from a safe range (`20000โ€“20999`), or pick your own with `--port`
29
- - ๐Ÿงฉ **CLI & typed API** โ€” every command has a zero-config Node.js equivalent with full TypeScript definitions
30
- - ๐Ÿชถ **Small + boring** โ€” one runtime dep (`commander`), no daemons, no magic
31
-
32
- ---
33
-
34
- ## ๐Ÿ“ฆ Install
7
+ ## Install
35
8
 
36
9
  ```bash
37
- # Global CLI
38
10
  npm install -g @saptools/cf-debugger
39
-
40
- # Or as a dependency
41
- npm install @saptools/cf-debugger
42
- # pnpm add @saptools/cf-debugger
43
- # yarn add @saptools/cf-debugger
44
11
  ```
45
12
 
46
- > [!NOTE]
47
- > Requires **Node.js โ‰ฅ 20** and the official **`cf` CLI** on `PATH`. Default `web`
48
- > process targets work with CF CLI v6+; non-web process targeting uses `--process` and requires
49
- > CF CLI v7+ (v8 recommended).
50
-
51
- ---
13
+ Node.js 20+ and the official `cf` CLI are required. CF CLI v8 is recommended.
14
+ Non-`web` process targeting uses `cf ssh --process` and requires CF CLI v7+.
15
+ On macOS, `lsof` is a soft dependency used to prove which PID owns a local
16
+ listening port. Install it if `cf-debugger` reports `TUNNEL_OWNER_UNVERIFIED`.
52
17
 
53
- ## ๐Ÿš€ Quick Start
18
+ ## Quick start
54
19
 
55
20
  ```bash
56
- # 1. Export your SAP SSO credentials (used for `cf auth` under the hood)
57
21
  export SAP_EMAIL="you@company.com"
58
22
  export SAP_PASSWORD="your-sap-password"
59
23
 
60
- # 2. Open a debug tunnel for one app
61
24
  cf-debugger start \
62
25
  --region eu10 \
63
26
  --org my-org \
64
27
  --space dev \
65
28
  --app my-app \
66
29
  --verbose
67
-
68
- # โ†’ Debugger ready for my-app (eu10/my-org/dev).
69
- # Local port: 20142
70
- # Remote port: 9229
71
- # Session id: 01HXYZ...
72
- # Tunnel PID: 83421
73
- # Press Ctrl+C to stop.
74
-
75
- # 3. Attach your IDE (VSCode, Chrome DevTools, ...) to localhost:20142
76
30
  ```
77
31
 
78
- Ctrl+C cleans everything up after tunnel termination is confirmed. If the PID or process group does
79
- not terminate, `cf-debugger` reports an error and retains its state and isolated CF home for safe
80
- recovery instead of hiding an unmanaged tunnel.
32
+ Successful output includes the complete attachment target:
81
33
 
82
- ---
34
+ ```text
35
+ Debugger ready for my-app (eu10/my-org/dev).
36
+ Process: web
37
+ Instance: 0
38
+ Local port: 20142
39
+ Remote port: 9229
40
+ Session id: 550e8400-e29b-41d4-a716-446655440000
41
+ Tunnel PID: 83421
42
+ Node PID: 4312
43
+ Press Ctrl+C to stop.
44
+ ```
83
45
 
84
- ## ๐Ÿงฐ CLI
46
+ Attach the IDE to `127.0.0.1:20142`. The remote inspector advertises its own
47
+ `ws://127.0.0.1:9229/...` URL through the tunnel; `cf-debugger` validates that
48
+ the URL exists but does not print or follow that remote-authority URL.
85
49
 
86
- ### โ–ถ๏ธ `cf-debugger start`
50
+ ## CLI
87
51
 
88
- Open a tunnel for one app and keep running until interrupted.
52
+ ### `start`
89
53
 
90
54
  ```bash
91
55
  cf-debugger start --region eu10 --org my-org --space dev --app my-app
92
- cf-debugger start --region eu10 --org my-org --space dev --app my-app --port 9230
93
- cf-debugger start --region eu10 --org my-org --space dev --app my-app --process worker --instance 2 --node-pid 4312
94
- cf-debugger start --region eu10 --org my-org --space dev --app my-app --timeout 180 --verbose
56
+ cf-debugger start eu10/my-org/dev/my-app --remote-port 9230
57
+ cf-debugger start my-app --process worker --instance 2 --node-pid 4312
58
+ cf-debugger start my-app --startup-timeout 300 --timeout 180 --verbose
95
59
  ```
96
60
 
97
- | Flag | Description |
61
+ When region, org, or space is omitted, the current `cf target` supplies it.
62
+
63
+ | Flag | Meaning |
98
64
  | --- | --- |
99
- | `--region <key>` | **Required.** CF region key (e.g. `eu10`, `ap10`, `us10`) |
100
- | `--org <name>` | **Required.** CF org name |
101
- | `--space <name>` | **Required.** CF space name |
102
- | `--app <name>` | **Required.** CF app name |
103
- | `--process <name>` | CF process name (default: `web`) |
104
- | `-i, --instance <index>` | Zero-based CF process instance (default: `0`) |
105
- | `--node-pid <pid>` | Exact remote Node.js PID; otherwise the app-`$PORT` listener is auto-selected, else one unambiguous PID is discovered |
106
- | `--port <number>` | Preferred local port (auto-assigned in `20000โ€“20999` if omitted) |
107
- | `--timeout <seconds>` | Tunnel-ready timeout (default: `180`) |
108
- | `--verbose` | Print every status transition |
109
-
110
- Cloud Foundry startup commands (`api`, `auth`, `target`, SSH checks, app
111
- restart, and the one-shot SIGUSR1 SSH command) each allow up to 300 seconds.
112
- `--timeout` controls the subsequent local tunnel-readiness probe separately.
113
-
114
- When `--node-pid` is explicit and SSH is disabled, `cf-debugger` fails with
115
- `NODE_PID_RESTART_UNSAFE` before enabling SSH or restarting the app. A restart replaces the
116
- container process identity, so enable SSH and restart manually, then resolve and pass the new PID.
117
-
118
- ### โน๏ธ `cf-debugger stop`
119
-
120
- Stop a specific session or everything at once. A stop received during startup records an atomic
121
- stop intent and asks the startup owner to cancel and clean up; the command reports `Stop requested`
122
- instead of claiming the tunnel has already stopped. If a matching entry is provably stale,
123
- `stop --session-id` removes it idempotently. For bare app names, matching still uses the current CF
124
- target; use `--session-id` or the full `region/org/space/app` selector when the listed session belongs
125
- to a different target.
65
+ | `--region <key>` | SAP region key; defaults to the current CF target |
66
+ | `--api-endpoint <url>` | Exact endpoint override, including nonstandard landscapes |
67
+ | `--org <name>`, `--space <name>`, `--app <name>` | CF target and app |
68
+ | `--process <name>` | CF process, default `web` |
69
+ | `-i, --instance <index>` | Zero-based process instance, default `0` |
70
+ | `--node-pid <pid>` | Exact remote Node PID |
71
+ | `--port <number>` | Preferred local port; otherwise choose from `20000โ€“20999` |
72
+ | `--remote-port <number>` | Remote inspector port, default `9229` |
73
+ | `--timeout <seconds>` | Local-tunnel and inspector-readiness budget, default `180` |
74
+ | `--startup-timeout <seconds>` | Overall startup deadline, default `300`, maximum `1800` |
75
+ | `--allow-ssh-enable-restart` | Permit app-level SSH enablement and one app restart |
76
+ | `--no-ssh-enable-restart` | Explicitly forbid that mutation, overriding flag/environment |
77
+ | `--verbose` | Print status, retry, and redacted tunnel transport diagnostics |
78
+
79
+ > [!IMPORTANT]
80
+ > App restart is opt-in in 0.2.0. By default, a disabled-SSH app fails with
81
+ > `SSH_NOT_ENABLED` and no deployment mutation. Use
82
+ > `--allow-ssh-enable-restart`, or set `CF_DEBUGGER_ALLOW_RESTART=1`, only when
83
+ > restarting the named app is acceptable. `CF_DEBUGGER_ALLOW_RESTART=0` keeps
84
+ > the guard enabled for a production shell.
85
+
86
+ Even with permission, a restart occurs only when all of these are true:
87
+
88
+ 1. the one-shot SSH error specifically says SSH support is disabled;
89
+ 2. `cf ssh-enabled <app>` proves app-level SSH is disabled;
90
+ 3. this invocation runs `cf enable-ssh <app>`;
91
+ 4. a second probe proves app-level SSH is now enabled.
92
+
93
+ An unknown probe result, an already-enabled app, a permission error, or a
94
+ space-level SSH policy never causes a restart. Before an allowed restart,
95
+ stderr names the app and org/space. With an explicit `--node-pid`, automatic
96
+ restart is always rejected because the container replacement invalidates that
97
+ PID.
98
+
99
+ The per-attempt CF command timeout is 60 seconds. Transient transport errors
100
+ retry with bounded exponential delays while the single overall startup budget
101
+ still has time. `cf auth` has no outer retry loop, and rejected credentials fail
102
+ after one attempt; repeated manual failures may count toward a tenant's
103
+ identity-provider lockout policy. The one-shot SSH signal and readiness wait are
104
+ also clamped to the same overall deadline. No retry configuration multiplies
105
+ startup work past `--startup-timeout`. After a timeout, fail-closed tunnel and
106
+ state cleanup still runs; that safety teardown can briefly extend the observed
107
+ command wall time rather than abandoning an unverified child.
108
+
109
+ ### `stop`
126
110
 
127
111
  ```bash
128
- cf-debugger stop --region eu10 --org my-org --space dev --app my-app
129
- cf-debugger stop --region eu10 --org my-org --space dev --app my-app --process worker --instance 2
130
- cf-debugger stop --session-id 01HXYZABCD...
112
+ cf-debugger stop --session-id 550e8400-e29b-41d4-a716-446655440000
113
+ cf-debugger stop eu10/my-org/dev/my-app
114
+ cf-debugger stop my-app --api-endpoint https://api.cf.eu10.hana.ondemand.com
115
+ cf-debugger stop my-app --node-pid 4312
131
116
  cf-debugger stop --all
117
+ cf-debugger stop --session-id 550e8400-e29b-41d4-a716-446655440000 --force
132
118
  ```
133
119
 
134
- | Flag | Description |
135
- | --- | --- |
136
- | `--region` / `--org` / `--space` / `--app` | Match session by key (all four required together) |
137
- | `--process <name>` / `--instance <index>` | Match the process-instance target (defaults: `web` / `0`) |
138
- | `--session-id <id>` | Match session by its ID |
139
- | `--all` | Stop every active session on this machine |
120
+ `stop --all` attempts every local session, reports stopped, stale, pending, and
121
+ failed outcomes separately, and exits nonzero if any session failed. It cannot
122
+ be combined with a positional selector, `--session-id`, or target selector
123
+ options; ambiguous scope is rejected instead of widening to every session.
140
124
 
141
- ### ๐Ÿ“‹ `cf-debugger list`
125
+ `stop --force` means โ€œforget safely,โ€ not โ€œkill harder.โ€ It never signals a PID
126
+ whose tunnel ownership is unproven. It removes the state record and only the
127
+ exact derived v2 `CF_HOME`, then warns with the abandoned PID and port so they
128
+ can be investigated manually. This is the supported recovery for a dead tunnel
129
+ whose old port is now occupied by another program, or an abandoned startup
130
+ record that cannot be verified. If a damaged record names a non-owned home
131
+ path, that path is left untouched and named in the warning while the record is
132
+ still forgotten.
142
133
 
143
- Print active and conservatively retained sessions as JSON. During startup, a live controller retains
144
- the record; after `ready`, tunnel process-group and port ownership determine health. An entry is
145
- pruned only when the owners relevant to its phase are gone and the recorded port is closed. If a
146
- relevant PID/group is alive or the port has an unexpected owner, the entry is retained so a later
147
- command cannot target or delete an unrelated process.
134
+ ### `list` and `status`
148
135
 
149
136
  ```bash
150
- cf-debugger list | jq '.[] | {app, localPort, status}'
137
+ cf-debugger list
138
+ cf-debugger status --session-id 550e8400-e29b-41d4-a716-446655440000
139
+ cf-debugger status eu10/my-org/dev/my-app --api-endpoint https://api.example
151
140
  ```
152
141
 
153
- ### ๐Ÿ” `cf-debugger status`
142
+ `list` prunes only records proven stale. PID liveness is paired with an optional
143
+ OS process-start token to detect PID reuse; older records without the additive
144
+ token retain the compatible PID-only check. A ready record is healthy only when
145
+ the complete listener-owner set contains its recorded tunnel PID. Ownership
146
+ that cannot be inspected remains explicit rather than being guessed.
147
+
148
+ `status` prints `null` only when no exact session matches. Ambiguity is a coded
149
+ `SESSION_AMBIGUOUS` failure; refine with session ID, API endpoint, or Node PID.
154
150
 
155
- Print one retained session by key (or `null` if no session matches after the same safe pruning used
156
- by `list`).
151
+ ### `doctor`
157
152
 
158
153
  ```bash
159
- cf-debugger status --region eu10 --org my-org --space dev --app my-app
160
- cf-debugger status --region eu10 --org my-org --space dev --app my-app --process worker --instance 2
154
+ cf-debugger doctor
155
+ cf-debugger doctor --cleanup
161
156
  ```
162
157
 
163
- ---
164
-
165
- ## ๐Ÿ”ญ How it works
158
+ The default is read-only JSON reporting. It includes:
159
+
160
+ - every local state record with its health verdict and reason;
161
+ - v2 session homes with no state record;
162
+ - listeners in `20000โ€“20999` that no state record claims;
163
+ - leftover state temp, lock, recovery, stop-intent, and corrupt-backup files;
164
+ - legacy v1 state/homes and their credential-retention risk.
165
+
166
+ `--cleanup` removes only canonical orphan v2 homes and sufficiently old,
167
+ package-owned temp/lock/recovery/stop-intent artifacts. It revalidates orphan
168
+ homes against lock-guarded state immediately before deletion. It never signals
169
+ an unclaimed listener, removes corrupt evidence, or removes legacy v1 artifacts.
170
+
171
+ ## Region resolution
172
+
173
+ Curated region keys are available from `listKnownRegionKeys()`. Keys matching
174
+ `aa00` or `aa00-000` also work before the curated table is updated:
175
+ `cf-debugger` synthesizes `https://api.cf.<key>.hana.ondemand.com`, or the
176
+ `.platform.sapcloud.cn` domain for `cn*`, and warns with the synthesized URL.
177
+ This supports new regions but makes a syntactically valid typo such as `eu99`
178
+ reach DNS; verify the warning or use `--api-endpoint`. Malformed keys fail early
179
+ with `UNKNOWN_REGION`.
180
+
181
+ ## How it works
182
+
183
+ 1. Resolve and validate the endpoint, deadline, target, and credentials.
184
+ 2. Prune only provably stale state and register a unique session/port.
185
+ 3. Run `cf api`, environment-only `cf auth`, `cf target`, and an app existence check.
186
+ 4. Probe the selected instance through one-shot `cf ssh`.
187
+ 5. If an inspector already owns the remote port and is Node, reuse that PID.
188
+ 6. Otherwise:
189
+ - choose the sole Node PID when exactly one exists;
190
+ - with several Node PIDs, use the app `$PORT` listener only as a tiebreaker;
191
+ - otherwise fail `NODE_PROCESS_AMBIGUOUS`.
192
+ 7. Send `SIGUSR1` when needed and prove the selected PID owns the configured
193
+ remote inspector port.
194
+ 8. Spawn detached `cf ssh -N -L <local>:localhost:<remote>`.
195
+ 9. Prove the spawned PID is among all owners of the local listener.
196
+ 10. Request `GET /json/list` through the tunnel and require HTTP 200 plus a
197
+ non-empty target array whose first entry has a well-formed
198
+ `webSocketDebuggerUrl`.
199
+ 11. Persist `ready` state and return a `DebuggerHandle`.
200
+
201
+ Node's Linux `cluster` default can leave the app `$PORT` socket owned by the
202
+ cluster primary rather than a request-handling worker. When several Node
203
+ processes exist, inspect the reported candidates and pass `--node-pid`
204
+ explicitly if a worker is the intended target.
205
+
206
+ The remote probe reads `/proc/<pid>/exe`, not argv. Its marker lines are filtered
207
+ by a strict `saptools-inspector-*` prefix, so CF SSH banners cannot impersonate
208
+ results.
209
+
210
+ ### Status sequence
211
+
212
+ The public `onStatus` sequence is:
166
213
 
214
+ ```text
215
+ starting โ†’ logging-in โ†’ targeting โ†’ signaling
216
+ โ†’ [ssh-enabling โ†’ ssh-restarting โ†’ signaling]
217
+ โ†’ tunneling โ†’ ready โ†’ stopping โ†’ stopped
167
218
  ```
168
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 1. cf api + cf auth (retry x3)
169
- โ”‚ cf-debugger start โ”‚ 2. cf target -o <org> -s <space>
170
- โ”‚ region/org/ โ”‚ 3. Probe one exact/unambiguous Node PID through cf ssh
171
- โ”‚ space/app โ”‚ โ”€โ–บ 4. Signal/verify inspector ownership on remote port 9229
172
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 5. If SSH is disabled: enable + restart only for automatic PID selection
173
- โ”‚ 6. Retry the probe, then record its verified remote Node PID
174
- โ”‚ 7. Open cf ssh [--process <non-web>] -i <instance> with -L
175
- โ–ผ 8. Verify the local listener PID and TCP readiness
176
- DebuggerHandle 9. Save ready state to ~/.saptools/cf-debugger-state-v2.json
177
- ```
178
-
179
- Each step emits a status update (`logging-in`, `targeting`, `ssh-enabling`, `signaling`, `tunneling`, `ready`, โ€ฆ). `--verbose` prints them live; the programmatic API exposes the same stream via `onStatus`.
180
-
181
- ### Concurrency model
182
-
183
- - **Atomic state** โ€” `~/.saptools/cf-debugger-state-v2.json` is written via temp-file + `rename`, guarded by a short-lived v2 lock file (`open(..., "wx")`).
184
- - **Port allocation** โ€” on register, ports already used by other sessions are excluded; the first free port in `20000โ€“20999` wins.
185
- - **Isolated CF homes** โ€” each session runs with its own `CF_HOME` (`~/.saptools/cf-debugger-homes-v2/<sessionId>/`), so `cf target` in one terminal can't clobber another.
186
- - **Ownership-aware lifecycle** โ€” startup records separate controller and tunnel PIDs, polls an atomic stop intent, and lets the startup owner clean up. A tunnel PID is signalled only after exact listener ownership is verified.
187
- - **Conservative stale pruning** โ€” state is removed automatically only when recorded owners are dead and the port is closed. Unknown or mismatched ownership is retained and reported instead of risking an unrelated process.
188
- - **Exact Node selection** โ€” the fixed remote probe reads numeric `/proc` entries, never reads command lines, verifies the chosen PID owns inspector port `9229`, and fails closed when automatic selection finds zero or multiple Node processes.
189
- - **Duplicate guard** โ€” trying to start a second healthy tunnel for the same `region/org/space/app/process/instance` fails fast with `SESSION_ALREADY_RUNNING` instead of racing for the port; stale same-key entries are pruned so a fresh tunnel can recover.
190
-
191
- ---
192
219
 
193
- ## ๐Ÿ“ Output Files
220
+ Verbose retries repeat the current phase with a message and remaining budget.
221
+ After `starting` is emitted, startup failure emits terminal `error` and never
222
+ follows it with `stopped`. Option/credential/region validation can reject before
223
+ the status stream begins, in which case no callback is emitted.
224
+ Unexpected tunnel failure after readiness likewise emits terminal `error`;
225
+ cleanup still runs without claiming a normal stop sequence.
194
226
 
195
- All state lives under your home directory:
227
+ ## State and security
196
228
 
197
229
  ```text
198
- ~/.saptools/cf-debugger-state-v2.json # active sessions (atomic JSON)
199
- ~/.saptools/cf-debugger-state-v2.lock # short-lived lock file
200
- ~/.saptools/cf-debugger-homes-v2/<id>/ # per-session isolated CF_HOME
230
+ ~/.saptools/cf-debugger-state-v2.json
231
+ ~/.saptools/cf-debugger-state-v2.lock
232
+ ~/.saptools/cf-debugger-homes-v2/<sessionId>/
201
233
  ```
202
234
 
203
- <details>
204
- <summary><b>๐Ÿ”ฌ Shape of <code>cf-debugger-state-v2.json</code></b></summary>
205
-
206
- ```jsonc
207
- {
208
- "version": "2",
209
- "sessions": [
210
- {
211
- "sessionId": "01HXYZABCD...",
212
- "region": "eu10",
213
- "org": "my-org",
214
- "space": "dev",
215
- "app": "my-app",
216
- "process": "web",
217
- "instance": 0,
218
- "hostname": "developer-host",
219
- "apiEndpoint": "https://api.cf.eu10.hana.ondemand.com",
220
- "localPort": 20142,
221
- "remotePort": 9229,
222
- "pid": 83421,
223
- "controllerPid": 83390,
224
- "tunnelPid": 83421,
225
- "remoteNodePid": 4312,
226
- "cfHomeDir": "/home/developer/.saptools/cf-debugger-homes-v2/01HXYZABCD...",
227
- "status": "ready",
228
- "startedAt": "2026-04-18T00:00:00.000Z"
229
- }
230
- ]
231
- }
232
- ```
233
-
234
- </details>
235
-
236
- > [!IMPORTANT]
237
- > Prefer the CLI commands (`list` / `status`) or the exported APIs over parsing these files โ€” the on-disk format is an implementation detail.
238
-
239
- > [!WARNING]
240
- > State v2 intentionally does not adopt or modify legacy `cf-debugger-state.json` and
241
- > `cf-debugger-homes/` artifacts. Stop all sessions owned by the older CLI before upgrading or
242
- > downgrading. Separate namespaces prevent cross-version state overwrite and cross-stop, but an old
243
- > CLI can still compete for the same local ports because it does not understand v2 reservations.
244
-
245
- ---
246
-
247
- ## โ“ FAQ
235
+ State is written as a `0600` temp file plus atomic rename while a token-owned
236
+ `open(..., "wx")` lock is held. The parent and session-home directories are
237
+ `0700`. Keep `~/.saptools` on a local filesystem: exclusive-create locking is
238
+ not reliably atomic on every NFS/SMB implementation. A generous age fallback
239
+ recovers foreign-host lock files, but it cannot make a network filesystem's
240
+ locking semantics safe.
241
+
242
+ Each isolated `CF_HOME` contains CF CLI credentials, including a live refresh
243
+ token. A clean stop removes it. If `TUNNEL_TERMINATION_FAILED` retains state and
244
+ the home, recover the process first, then run `stop --force`. If a record has
245
+ already disappeared but its canonical home remains, use `doctor --cleanup`.
246
+ Do not leave recovered homes indefinitely.
247
+
248
+ A damaged entry no longer erases healthy sessions. Invalid entries are dropped
249
+ individually, and the original state file is moved to a private
250
+ `.corrupt-<timestamp>-<uuid>` backup before any repair. Completely invalid JSON,
251
+ version, or root shape is likewise preserved before a fresh empty v2 file is
252
+ written.
253
+
254
+ Legacy `~/.saptools/cf-debugger-homes/` directories may still contain live
255
+ refresh and access tokens. `doctor` reports but never deletes them. After
256
+ confirming no v1 tunnel is running, remove them explicitly:
248
257
 
249
- <details>
250
- <summary><b>Can I run multiple debuggers at once?</b></summary>
251
-
252
- Yes โ€” that's a core feature. Open two terminals, pick different apps or process instances, and the tunnels come up on separate local ports. `cf-debugger list` shows everything at once. One exact app/process/instance target can have only one healthy tunnel.
253
-
254
- </details>
258
+ ```bash
259
+ rm -rf "$HOME/.saptools/cf-debugger-homes" \
260
+ "$HOME/.saptools/cf-debugger-state.json" \
261
+ "$HOME/.saptools/cf-debugger-state.lock"
262
+ ```
255
263
 
256
- <details>
257
- <summary><b>Does this modify the remote app?</b></summary>
264
+ The v2 CLI never adopts or mutates v1 sessions. Prefer CLI/API operations over
265
+ editing the v2 JSON by hand.
258
266
 
259
- Only if SSH is disabled and Node PID selection is automatic. In that case, `cf-debugger` runs
260
- `cf enable-ssh` + `cf restart`; otherwise it only sends `SIGUSR1` to the selected Node.js process.
261
- With an explicit `--node-pid`, automatic restart is rejected because the old PID cannot identify the
262
- new process after restart. No code, environment variable, or manifest is changed.
267
+ Credentials are passed to `cf auth` as `CF_USERNAME` and `CF_PASSWORD` in the
268
+ child environment, never argv. CF command and tunnel diagnostics are bounded
269
+ and redact sensitive values before they reach errors or verbose output.
270
+ `CF_COLOR=false` is forced so ANSI escapes cannot corrupt output parsing.
263
271
 
264
- </details>
272
+ ## Environment variables
265
273
 
266
- <details>
267
- <summary><b>What if my app crashes while the tunnel is open?</b></summary>
274
+ | Variable | Meaning |
275
+ | --- | --- |
276
+ | `SAP_EMAIL`, `SAP_PASSWORD` | CF authentication credentials |
277
+ | `CF_DEBUGGER_ALLOW_RESTART=0\|1` | Shell-level restart guard/default |
278
+ | `CF_DEBUGGER_CF_BIN` | Replace the `cf` executable; useful for wrappers and deterministic test stubs |
268
279
 
269
- The TCP probe will fail on reconnect and the CLI will exit with the SSH child's code. The state entry is removed on exit, so the next `start` for the same app works immediately.
280
+ A caller-provided child environment cannot override the per-session `CF_HOME`.
270
281
 
271
- </details>
282
+ ## Errors and exit status
272
283
 
273
- <details>
274
- <summary><b>Is there a way to reserve a specific local port?</b></summary>
284
+ Documented domain failures use `Error [CODE]: ...`. An unexpected operating
285
+ system or filesystem failure can still surface as a plain `Error` without a
286
+ domain code; it is never converted into success.
275
287
 
276
- Yes โ€” pass `--port 9230` (CLI) or `preferredPort: 9230` (API). If any process already owns that port, `cf-debugger` fails closed with `PORT_UNAVAILABLE`; it never terminates an unrelated listener to claim a port.
288
+ | Code | Meaning |
289
+ | --- | --- |
290
+ | `UNKNOWN_REGION`, `UNSAFE_INPUT`, `MISSING_CREDENTIALS` | Invalid endpoint/key, selector, numeric input, or credentials |
291
+ | `CF_LOGIN_FAILED`, `CF_AUTH_FAILED`, `CF_TARGET_FAILED`, `CF_CLI_FAILED`, `CF_CLI_TIMEOUT` | CF command failure; rejected auth is not retried |
292
+ | `STARTUP_TIMEOUT`, `ABORTED` | Overall deadline expired, or caller/stop intent cancelled startup |
293
+ | `APP_NOT_FOUND` | The targeted app does not exist in the selected org/space |
294
+ | `SSH_NOT_ENABLED`, `SSH_PERMISSION_DENIED`, `SSH_STATE_UNKNOWN` | Restart refused/not useful, permission denied, or SSH state ambiguous |
295
+ | `NODE_PID_RESTART_UNSAFE` | Explicit PID cannot survive an app restart |
296
+ | `NODE_PROCESS_NOT_FOUND`, `NODE_PROCESS_AMBIGUOUS`, `NODE_PID_INVALID` | Remote Node selection failed closed |
297
+ | `USR1_SIGNAL_FAILED`, `INSPECTOR_NOT_READY`, `INSPECTOR_OWNER_MISMATCH`, `INSPECTOR_OUTPUT_TOO_LARGE` | Remote signal/ownership protocol failed |
298
+ | `TUNNEL_NOT_READY` | The spawned local forward did not bind |
299
+ | `PORT_UNAVAILABLE`, `TUNNEL_PROCESS_MISSING` | Local port allocation failed, or the spawned tunnel exposed no PID |
300
+ | `TUNNEL_OWNER_UNVERIFIED`, `TUNNEL_OWNER_MISMATCH` | Local listener ownership could not be proved |
301
+ | `INSPECTOR_UNREACHABLE` | Local forward bound, but `/json/list` did not prove an attachable remote inspector |
302
+ | `TUNNEL_EXITED`, `TUNNEL_TERMINATION_FAILED`, `TUNNEL_OWNERSHIP_UNVERIFIED` | Tunnel died, cleanup could not terminate it, or stop could not safely signal it |
303
+ | `SESSION_ALREADY_RUNNING`, `SESSION_AMBIGUOUS`, `SESSION_NOT_FOUND` | Session selection conflict |
304
+ | `SESSION_STATE_LOST`, `SESSION_STATE_CONFLICT`, `STATE_LOCK_TIMEOUT` | Atomic state/lifecycle invariant failed |
305
+ | `STOP_FAILED` | One entry in a batch stop failed with a non-coded internal error |
306
+
307
+ | Exit | Meaning |
308
+ | --- | --- |
309
+ | `0` | Clean completion |
310
+ | `1` | Coded operational/startup error, including `STARTUP_TIMEOUT` |
311
+ | `70` | Cleanup/termination failed and state was retained |
312
+ | SSH child's nonzero code | An unexpected numeric SSH exit is preserved, for example `255` |
313
+ | `128 + signal` | Tunnel child died from a signal, for example `137` for `SIGKILL` |
314
+ | `130`, `143` | User interrupted with `SIGINT` or `SIGTERM` |
315
+
316
+ ## Programmatic API
317
+
318
+ ```ts
319
+ import {
320
+ startDebugger,
321
+ stopDebugger,
322
+ stopAllDebuggers,
323
+ listSessions,
324
+ runDoctor,
325
+ } from "@saptools/cf-debugger";
326
+
327
+ const handle = await startDebugger({
328
+ region: "eu10",
329
+ org: "my-org",
330
+ space: "dev",
331
+ app: "my-app",
332
+ startupTimeoutMs: 300_000,
333
+ allowSshEnableRestart: false,
334
+ });
335
+
336
+ // Attach to handle.session.localPort.
337
+ await handle.dispose();
338
+
339
+ const summary = await stopAllDebuggers();
340
+ // 0.2.0 returns per-session outcomes plus stopped/stale/pending/failed counts.
341
+ ```
277
342
 
278
- </details>
343
+ Persisted schema additions in 0.2.0 are optional. Absence means compatible
344
+ fallback behavior because an older mixed-version writer may strip fields it
345
+ does not know. State remains version `"2"`.
279
346
 
280
- <details>
281
- <summary><b>Can I use this in CI for integration tests?</b></summary>
347
+ ## FAQ
282
348
 
283
- You can, but it's designed for interactive debugging. CI usually wants a short-lived request against the running app, not a persistent inspector tunnel โ€” consider `cf ssh -L` directly for that case.
349
+ ### Does this modify the remote app?
284
350
 
285
- </details>
351
+ Not by default. Inspector activation sends `SIGUSR1` to the verified Node
352
+ process, but app-level SSH enablement and `cf restart` require explicit
353
+ `--allow-ssh-enable-restart` permission. Even with permission, restart occurs
354
+ only after this run changes app SSH from proven disabled to proven enabled; an
355
+ already-enabled, unknown, permission-denied, or space-policy result never
356
+ restarts the app.
286
357
 
287
- ---
358
+ ### Is session state always removed when the CLI exits?
288
359
 
289
- ## ๐Ÿ› ๏ธ Development
360
+ Only after cleanup is verified. `TUNNEL_TERMINATION_FAILED` deliberately retains
361
+ state and the isolated credential-bearing `CF_HOME`; recover it with `doctor`
362
+ and `stop --force` instead of assuming the next `start` can proceed.
290
363
 
291
- From the monorepo root:
364
+ ## Development
292
365
 
293
366
  ```bash
294
- pnpm install
295
- pnpm --filter @saptools/cf-debugger build
367
+ pnpm --filter @saptools/cf-debugger lint
296
368
  pnpm --filter @saptools/cf-debugger typecheck
297
369
  pnpm --filter @saptools/cf-debugger test:unit
298
- pnpm --filter @saptools/cf-debugger test:e2e
370
+ pnpm --filter @saptools/cf-debugger build
299
371
  ```
300
372
 
301
- The e2e suite hits live SAP BTP CF. Set `CF_DEBUGGER_E2E_REGIONS=eu10,ap10` (plus `SAP_EMAIL` / `SAP_PASSWORD`) to restrict which regions it searches for a running app.
302
-
303
- ---
304
-
305
- ## ๐ŸŒ Related
306
-
307
- - โ˜๏ธ [`@saptools/cf-sync`](https://www.npmjs.com/package/@saptools/cf-sync) โ€” snapshot every region / org / space / app you can reach into one JSON file
308
- - ๐Ÿ” [`@saptools/cf-xsuaa`](https://www.npmjs.com/package/@saptools/cf-xsuaa) โ€” fetch XSUAA credentials and cached OAuth2 tokens for any CF app
309
- - ๐Ÿ—‚๏ธ [saptools monorepo](https://github.com/dongitran/saptools) โ€” the full toolbox
310
-
311
- ---
312
-
313
- ## ๐Ÿ‘จโ€๐Ÿ’ป Author
314
-
315
- **dongtran** โœจ
316
-
317
- ## ๐Ÿ“„ License
318
-
319
- MIT
320
-
321
- ---
322
-
323
- Made with โค๏ธ to make your work life easier!
373
+ The fake-backed tests set `CF_DEBUGGER_CF_BIN`. Live E2E tests require a safe
374
+ non-production CF target and credentials; do not run them against production.