@saptools/cf-events 0.1.1 → 0.1.4
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 +191 -63
- package/dist/cli.js +137 -109
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +15 -7
- package/dist/index.js +134 -80
- package/dist/index.js.map +1 -1
- package/package.json +11 -12
package/README.md
CHANGED
|
@@ -1,135 +1,263 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
**SSH / debug sessions** from the command line. Point it at a
|
|
5
|
-
`region/org/space/app` target and it answers questions like "what just happened
|
|
6
|
-
to this app?", "is anyone SSH'd into it right now?", and "why did it crash?".
|
|
3
|
+
# ☁️ `@saptools/cf-events`
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
for the synced `region/org/space/app` topology, then calls the Cloud Foundry v3
|
|
10
|
-
API (`/v3/audit_events`, process stats, SSH status) through the `cf` CLI.
|
|
5
|
+
**Inspect SAP BTP Cloud Foundry application audit events and detect active SSH/debug sessions from the command line.**
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
Point it at a `region/org/space/app` (or a bare app name) and instantly answer: "what just happened?", "is anyone SSH'd in right now?", or "why did it crash?".
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/@saptools/cf-events)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
[](https://packagephobia.com/result?p=@saptools/cf-events)
|
|
13
|
+
[](https://www.typescriptlang.org)
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
[Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [FAQ](#-faq)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ✨ Features
|
|
22
|
+
|
|
23
|
+
- 📜 **Audit event inspection** — list recent `audit.app.*` events (start/stop, scale, restarts, restage, route changes, env views, crashes, SSH authz)
|
|
24
|
+
- 🔐 **SSH & debug session detection** — surface authorized sessions + denied attempts; infer "likely active" ones from recent `ssh-authorized` events (CF has no live session or close events)
|
|
25
|
+
- 💥 **Crash summaries** — count, last crash time/reason/exit code, and per-instance detail
|
|
26
|
+
- ❤️ **One-glance status** — requested state, per-instance CPU/mem/uptime, SSH flag, and the most recent audit event
|
|
27
|
+
- 👀 **Live watch** — poll `/v3/audit_events` on an interval and stream new events (Ctrl+C to stop)
|
|
28
|
+
- 🧭 **Smart selectors** — full `region/org/space/app` or bare app name (resolved via the `cf-sync` snapshot; falls back to current `cf target` for bare names)
|
|
29
|
+
- 🔄 **Reuses cf-sync** — topology snapshot + current-target helpers from [`@saptools/cf-sync`](https://www.npmjs.com/package/@saptools/cf-sync)
|
|
30
|
+
- 🧪 **Isolated sessions** — every invocation uses a fresh ephemeral `CF_HOME`; never touches your interactive login
|
|
31
|
+
- 🧩 **CLI + typed library** — full TypeScript exports for `CfEventsRuntime`, parsers, formatters, and types
|
|
32
|
+
- 🪶 **Zero daemon, tiny deps** — commander + cf-sync only
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 📦 Install
|
|
21
37
|
|
|
22
38
|
```bash
|
|
39
|
+
# Global CLI
|
|
23
40
|
npm install -g @saptools/cf-events
|
|
24
|
-
|
|
41
|
+
|
|
42
|
+
# Or use on demand
|
|
25
43
|
npx @saptools/cf-events --help
|
|
44
|
+
|
|
45
|
+
# As a library
|
|
46
|
+
npm install @saptools/cf-events
|
|
47
|
+
# pnpm add @saptools/cf-events
|
|
26
48
|
```
|
|
27
49
|
|
|
28
|
-
|
|
50
|
+
> [!NOTE]
|
|
51
|
+
> Requires **Node.js ≥ 20** and the official **Cloud Foundry CLI (v8+)** on `PATH`.
|
|
52
|
+
|
|
53
|
+
---
|
|
29
54
|
|
|
30
|
-
|
|
55
|
+
## 🚀 Quick Start
|
|
31
56
|
|
|
32
57
|
```bash
|
|
33
|
-
|
|
34
|
-
export
|
|
58
|
+
# 1. Provide SAP SSO credentials (used only for live CF calls)
|
|
59
|
+
export SAP_EMAIL="you@company.com"
|
|
60
|
+
export SAP_PASSWORD="your-sap-password"
|
|
61
|
+
|
|
62
|
+
# 2. Make sure you have a topology snapshot (or be logged into a CF target)
|
|
63
|
+
cf-sync sync --only ap10 # or cf-sync space ap10 my-org dev
|
|
64
|
+
|
|
65
|
+
# 3. Inspect events
|
|
66
|
+
cf-events events orders-srv
|
|
67
|
+
cf-events events ap10/my-org/dev/orders-srv --limit 100 --since 6h
|
|
68
|
+
|
|
69
|
+
# 4. Check for active SSH/debug sessions
|
|
70
|
+
cf-events ssh-status orders-srv --since 7d
|
|
71
|
+
|
|
72
|
+
# 5. Summarize recent crashes
|
|
73
|
+
cf-events crashes orders-srv
|
|
74
|
+
|
|
75
|
+
# 6. Quick health view
|
|
76
|
+
cf-events status orders-srv
|
|
77
|
+
|
|
78
|
+
# 7. Stream new events live
|
|
79
|
+
cf-events watch orders-srv --type crash
|
|
35
80
|
```
|
|
36
81
|
|
|
37
|
-
|
|
38
|
-
to the `cf` CLI through the environment, never on the command line, and each
|
|
39
|
-
invocation runs in an isolated, ephemeral `CF_HOME`.
|
|
82
|
+
After a `cf-sync` snapshot exists, bare app names are resolved automatically (with current `cf target` expansion when no `/` present).
|
|
40
83
|
|
|
41
|
-
|
|
84
|
+
---
|
|
42
85
|
|
|
43
|
-
|
|
86
|
+
## 🧰 CLI
|
|
44
87
|
|
|
45
|
-
|
|
46
|
-
- A bare app name: `orders-srv` — resolved against the `cf-sync` snapshot. If
|
|
47
|
-
the name is ambiguous across spaces, `cf-events` lists the candidates and
|
|
48
|
-
asks for a full path.
|
|
88
|
+
All commands accept a single positional **selector**:
|
|
49
89
|
|
|
50
|
-
|
|
90
|
+
- Full path: `ap10/my-org/dev/orders-srv`
|
|
91
|
+
- Bare app name: `orders-srv` — resolved against the cf-sync snapshot (and current `cf target` when the name is unique within the target scope). Ambiguous names list the candidates and fail.
|
|
92
|
+
|
|
93
|
+
Common credential flags (all commands):
|
|
94
|
+
|
|
95
|
+
| Flag | Description |
|
|
96
|
+
|-----------------|--------------------------------------------------|
|
|
97
|
+
| `--email` | SAP email (falls back to `SAP_EMAIL` env) |
|
|
98
|
+
| `--password` | SAP password (falls back to `SAP_PASSWORD` env) |
|
|
99
|
+
| `--json` | Emit JSON instead of human text |
|
|
51
100
|
|
|
52
101
|
### `events <selector>`
|
|
53
102
|
|
|
54
|
-
List recent audit events for
|
|
55
|
-
SSH activity, ...).
|
|
103
|
+
List recent audit events for the app (deployments, restarts, scaling, crashes, SSH, routes, ...).
|
|
56
104
|
|
|
57
105
|
```bash
|
|
58
106
|
cf-events events ap10/my-org/dev/orders-srv
|
|
59
107
|
cf-events events orders-srv --limit 100 --since 6h
|
|
60
108
|
cf-events events orders-srv --type ssh --json
|
|
109
|
+
cf-events events orders-srv --type audit.app.start,audit.app.stop
|
|
61
110
|
```
|
|
62
111
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
112
|
+
| Flag | Description |
|
|
113
|
+
|----------|--------------------------------------------------|
|
|
114
|
+
| `--limit <n>` | Max events (default 50) |
|
|
115
|
+
| `--since <dur>` | Only newer than duration (e.g. `30m`, `6h`, `7d`) |
|
|
116
|
+
| `--type <types>` | Comma list of types or shorthands `ssh` / `crash` |
|
|
66
117
|
|
|
67
118
|
### `ssh-status <selector>`
|
|
68
119
|
|
|
69
|
-
Show
|
|
70
|
-
activity: who opened a session and when, plus any denied attempts.
|
|
120
|
+
Show the SSH-enabled flag for the app + recent SSH / debug activity (who authorized when) and denied attempts. Marks sessions that are "likely active" (within the last 60 minutes of an `ssh-authorized` event).
|
|
71
121
|
|
|
72
122
|
```bash
|
|
73
123
|
cf-events ssh-status orders-srv
|
|
74
124
|
cf-events ssh-status orders-srv --since 7d --json
|
|
75
125
|
```
|
|
76
126
|
|
|
77
|
-
|
|
127
|
+
| Flag | Description |
|
|
128
|
+
|---------------|-----------------------------------------|
|
|
129
|
+
| `--since <dur>` | Look-back window (default `24h`) |
|
|
78
130
|
|
|
79
|
-
>
|
|
80
|
-
>
|
|
81
|
-
> recent `ssh-authorized` audit events — treat it as a strong hint, not proof.
|
|
131
|
+
> [!IMPORTANT]
|
|
132
|
+
> Cloud Foundry does not expose live sessions or emit `ssh-unauthorized` / close events. `cf-events` **infers** likely-active sessions from recent `audit.app.ssh-authorized` records. Treat as a strong signal, not definitive proof.
|
|
82
133
|
|
|
83
134
|
### `crashes <selector>`
|
|
84
135
|
|
|
85
|
-
|
|
86
|
-
reason.
|
|
136
|
+
Count and detail recent crash events (both `audit.app.crash` and `audit.app.process.crash`).
|
|
87
137
|
|
|
88
138
|
```bash
|
|
89
139
|
cf-events crashes orders-srv
|
|
90
140
|
cf-events crashes orders-srv --since 24h --json
|
|
91
141
|
```
|
|
92
142
|
|
|
93
|
-
|
|
143
|
+
| Flag | Description |
|
|
144
|
+
|---------------|------------------------------------------|
|
|
145
|
+
| `--limit <n>` | Max crash events to inspect (default 50) |
|
|
146
|
+
| `--since <dur>` | Filter window |
|
|
94
147
|
|
|
95
148
|
### `status <selector>`
|
|
96
149
|
|
|
97
|
-
|
|
98
|
-
memory, the SSH-enabled flag, and the most recent audit event.
|
|
150
|
+
Compact health snapshot: GUID, requested state, SSH flag, instance table (index / state / uptime / cpu / mem), and the single most recent audit event.
|
|
99
151
|
|
|
100
152
|
```bash
|
|
153
|
+
cf-events status orders-srv
|
|
101
154
|
cf-events status orders-srv --json
|
|
102
155
|
```
|
|
103
156
|
|
|
104
157
|
### `watch <selector>`
|
|
105
158
|
|
|
106
|
-
Poll `/v3/audit_events`
|
|
107
|
-
Press `Ctrl+C` to stop.
|
|
159
|
+
Poll `/v3/audit_events` repeatedly and print (or emit NDJSON) fresh events as they arrive. Press Ctrl+C to stop.
|
|
108
160
|
|
|
109
161
|
```bash
|
|
110
162
|
cf-events watch orders-srv
|
|
111
|
-
cf-events watch orders-srv --interval 30000 --type crash
|
|
163
|
+
cf-events watch orders-srv --interval 30000 --type crash --lookback 5m
|
|
112
164
|
```
|
|
113
165
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
`--
|
|
166
|
+
| Flag | Description |
|
|
167
|
+
|------------------|--------------------------------------------------|
|
|
168
|
+
| `--interval <ms>` | Poll interval (default 15000, min 2000) |
|
|
169
|
+
| `--lookback <dur>` | Initial fetch window on start (default `2m`) |
|
|
170
|
+
| `--type <types>` | Filter (supports `ssh` / `crash` shorthands) |
|
|
171
|
+
| `--json` | Line-delimited JSON output |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 📁 Prerequisites & Snapshots
|
|
176
|
+
|
|
177
|
+
`cf-events` relies on a `cf-sync` snapshot at `~/.saptools/cf-structure.json` for bare-app-name resolution and validation of explicit paths.
|
|
178
|
+
|
|
179
|
+
- Run `cf-sync sync`, `cf-sync space ...`, or `cf-sync org ...` at least once.
|
|
180
|
+
- When you pass a bare app name, `cf-events` will also consult your current `cf target` (via `cf target` output) to build a full selector before validating against the snapshot.
|
|
181
|
+
- Pure `--json` + explicit full selectors still require the snapshot for safety.
|
|
182
|
+
|
|
183
|
+
All CF operations run inside a throw-away `CF_HOME`. Your interactive `cf` login is never affected.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## ❓ FAQ
|
|
117
188
|
|
|
118
|
-
|
|
189
|
+
<details>
|
|
190
|
+
<summary><b>Do I need to run cf target first?</b></summary>
|
|
119
191
|
|
|
120
|
-
|
|
192
|
+
Not required. Pass a full selector or make sure a cf-sync snapshot exists. Bare names will try to use your current `cf target` output to expand into `region/org/space/app`.
|
|
121
193
|
|
|
122
|
-
|
|
123
|
-
import { CfEventsRuntime } from "@saptools/cf-events";
|
|
194
|
+
</details>
|
|
124
195
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
196
|
+
<details>
|
|
197
|
+
<summary><b>Why "likely active" instead of real sessions?</b></summary>
|
|
198
|
+
|
|
199
|
+
Cloud Foundry's audit log only records authorization. There is no "session closed" or live-process table exposed via the APIs `cf-events` consumes. The 60-minute window after `ssh-authorized` is a documented heuristic.
|
|
200
|
+
|
|
201
|
+
</details>
|
|
202
|
+
|
|
203
|
+
<details>
|
|
204
|
+
<summary><b>What durations are accepted?</b></summary>
|
|
205
|
+
|
|
206
|
+
`30s`, `5m`, `6h`, `7d` (positive integers + s/m/h/d). Used by `--since` and `--lookback`.
|
|
207
|
+
|
|
208
|
+
</details>
|
|
209
|
+
|
|
210
|
+
<details>
|
|
211
|
+
<summary><b>Can I filter only crashes or only SSH?</b></summary>
|
|
212
|
+
|
|
213
|
+
Yes: `--type crash` or `--type ssh`. You can also pass comma-separated full types: `--type audit.app.crash,audit.app.process.crash`.
|
|
214
|
+
|
|
215
|
+
</details>
|
|
216
|
+
|
|
217
|
+
<details>
|
|
218
|
+
<summary><b>Is the snapshot required even for JSON output?</b></summary>
|
|
219
|
+
|
|
220
|
+
Yes for safety and consistent resolution. The snapshot is the single source of truth for which apps exist in which spaces.
|
|
221
|
+
|
|
222
|
+
</details>
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 🛠️ Development
|
|
227
|
+
|
|
228
|
+
From monorepo root:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
pnpm install
|
|
232
|
+
pnpm --filter @saptools/cf-events build
|
|
233
|
+
pnpm --filter @saptools/cf-events typecheck
|
|
234
|
+
pnpm --filter @saptools/cf-events lint
|
|
235
|
+
pnpm --filter @saptools/cf-events test:unit
|
|
236
|
+
pnpm --filter @saptools/cf-events test:e2e
|
|
237
|
+
pnpm --filter @saptools/cf-events check # cspell + lint + type + unit + e2e
|
|
131
238
|
```
|
|
132
239
|
|
|
133
|
-
|
|
240
|
+
E2E tests are fully fake-backed (no real CF or credentials needed).
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 🌐 Related
|
|
245
|
+
|
|
246
|
+
- 🗺️ [`@saptools/cf-sync`](https://www.npmjs.com/package/@saptools/cf-sync) — the topology snapshot and current-target helpers that power selector resolution
|
|
247
|
+
- 🔐 [`@saptools/cf-xsuaa`](https://www.npmjs.com/package/@saptools/cf-xsuaa)
|
|
248
|
+
- 🐛 [`@saptools/cf-debugger`](https://www.npmjs.com/package/@saptools/cf-debugger)
|
|
249
|
+
- Full toolbox: [saptools monorepo](https://github.com/dongitran/saptools)
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 👨💻 Author
|
|
254
|
+
|
|
255
|
+
**Dong Tran**
|
|
256
|
+
|
|
257
|
+
## 📄 License
|
|
134
258
|
|
|
135
259
|
MIT
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
Made with ❤️ for SAP BTP CF developers.
|
package/dist/cli.js
CHANGED
|
@@ -5,10 +5,6 @@ import { readFileSync, realpathSync } from "fs";
|
|
|
5
5
|
import { dirname, join as join2, resolve } from "path";
|
|
6
6
|
import process2 from "process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
-
import {
|
|
9
|
-
formatCurrentCfAppSelector,
|
|
10
|
-
readCurrentCfTarget
|
|
11
|
-
} from "@saptools/cf-sync";
|
|
12
8
|
import { Command } from "commander";
|
|
13
9
|
|
|
14
10
|
// src/types.ts
|
|
@@ -397,6 +393,110 @@ import { execFile } from "child_process";
|
|
|
397
393
|
import { mkdtemp, rm } from "fs/promises";
|
|
398
394
|
import { tmpdir } from "os";
|
|
399
395
|
import { join } from "path";
|
|
396
|
+
import { promisify } from "util";
|
|
397
|
+
var execFileAsync = promisify(execFile);
|
|
398
|
+
var REGION_API_MAP = {
|
|
399
|
+
ae01: "https://api.cf.ae01.hana.ondemand.com",
|
|
400
|
+
ap01: "https://api.cf.ap01.hana.ondemand.com",
|
|
401
|
+
ap10: "https://api.cf.ap10.hana.ondemand.com",
|
|
402
|
+
ap11: "https://api.cf.ap11.hana.ondemand.com",
|
|
403
|
+
ap12: "https://api.cf.ap12.hana.ondemand.com",
|
|
404
|
+
ap20: "https://api.cf.ap20.hana.ondemand.com",
|
|
405
|
+
ap21: "https://api.cf.ap21.hana.ondemand.com",
|
|
406
|
+
ap30: "https://api.cf.ap30.hana.ondemand.com",
|
|
407
|
+
br10: "https://api.cf.br10.hana.ondemand.com",
|
|
408
|
+
br20: "https://api.cf.br20.hana.ondemand.com",
|
|
409
|
+
br30: "https://api.cf.br30.hana.ondemand.com",
|
|
410
|
+
ca10: "https://api.cf.ca10.hana.ondemand.com",
|
|
411
|
+
ca20: "https://api.cf.ca20.hana.ondemand.com",
|
|
412
|
+
ch20: "https://api.cf.ch20.hana.ondemand.com",
|
|
413
|
+
cn20: "https://api.cf.cn20.platform.sapcloud.cn",
|
|
414
|
+
cn40: "https://api.cf.cn40.platform.sapcloud.cn",
|
|
415
|
+
eu01: "https://api.cf.eu01.hana.ondemand.com",
|
|
416
|
+
eu02: "https://api.cf.eu02.hana.ondemand.com",
|
|
417
|
+
eu10: "https://api.cf.eu10.hana.ondemand.com",
|
|
418
|
+
"eu10-002": "https://api.cf.eu10-002.hana.ondemand.com",
|
|
419
|
+
"eu10-003": "https://api.cf.eu10-003.hana.ondemand.com",
|
|
420
|
+
"eu10-004": "https://api.cf.eu10-004.hana.ondemand.com",
|
|
421
|
+
"eu10-005": "https://api.cf.eu10-005.hana.ondemand.com",
|
|
422
|
+
"eu10-006": "https://api.cf.eu10-006.hana.ondemand.com",
|
|
423
|
+
eu11: "https://api.cf.eu11.hana.ondemand.com",
|
|
424
|
+
eu13: "https://api.cf.eu13.hana.ondemand.com",
|
|
425
|
+
eu20: "https://api.cf.eu20.hana.ondemand.com",
|
|
426
|
+
"eu20-001": "https://api.cf.eu20-001.hana.ondemand.com",
|
|
427
|
+
eu30: "https://api.cf.eu30.hana.ondemand.com",
|
|
428
|
+
in30: "https://api.cf.in30.hana.ondemand.com",
|
|
429
|
+
jp10: "https://api.cf.jp10.hana.ondemand.com",
|
|
430
|
+
jp20: "https://api.cf.jp20.hana.ondemand.com",
|
|
431
|
+
jp30: "https://api.cf.jp30.hana.ondemand.com",
|
|
432
|
+
jp31: "https://api.cf.jp31.hana.ondemand.com",
|
|
433
|
+
sa30: "https://api.cf.sa30.hana.ondemand.com",
|
|
434
|
+
us10: "https://api.cf.us10.hana.ondemand.com",
|
|
435
|
+
"us10-001": "https://api.cf.us10-001.hana.ondemand.com",
|
|
436
|
+
"us10-002": "https://api.cf.us10-002.hana.ondemand.com",
|
|
437
|
+
us20: "https://api.cf.us20.hana.ondemand.com",
|
|
438
|
+
us21: "https://api.cf.us21.hana.ondemand.com",
|
|
439
|
+
us30: "https://api.cf.us30.hana.ondemand.com"
|
|
440
|
+
// add more if needed, this covers common SAP BTP regions
|
|
441
|
+
};
|
|
442
|
+
function getApiEndpointForRegion(regionKey) {
|
|
443
|
+
return REGION_API_MAP[regionKey];
|
|
444
|
+
}
|
|
445
|
+
function getRegionKeyForApi(apiEndpoint) {
|
|
446
|
+
const normalized = apiEndpoint.trim().replace(/\/+$/, "").toLowerCase();
|
|
447
|
+
for (const [key, api] of Object.entries(REGION_API_MAP)) {
|
|
448
|
+
if (api.toLowerCase() === normalized) {
|
|
449
|
+
return key;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return void 0;
|
|
453
|
+
}
|
|
454
|
+
async function readCurrentCfTarget() {
|
|
455
|
+
const cfBin = process.env["CF_EVENTS_CF_BIN"] ?? "cf";
|
|
456
|
+
const isScript = cfBin.endsWith(".mjs") || cfBin.endsWith(".js");
|
|
457
|
+
const file = isScript ? "node" : cfBin;
|
|
458
|
+
const args = isScript ? [cfBin, "target"] : ["target"];
|
|
459
|
+
try {
|
|
460
|
+
const { stdout } = await execFileAsync(file, args, {
|
|
461
|
+
maxBuffer: 1024 * 1024,
|
|
462
|
+
timeout: 1e4
|
|
463
|
+
});
|
|
464
|
+
return parseCfTargetOutput(stdout);
|
|
465
|
+
} catch {
|
|
466
|
+
return void 0;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function parseCfTargetOutput(stdout) {
|
|
470
|
+
const fields = parseCfTargetFields(stdout);
|
|
471
|
+
const apiEndpoint = fields.get("api endpoint");
|
|
472
|
+
const orgName = fields.get("org");
|
|
473
|
+
const spaceName = fields.get("space");
|
|
474
|
+
if (!apiEndpoint || !orgName || !spaceName) {
|
|
475
|
+
return void 0;
|
|
476
|
+
}
|
|
477
|
+
const regionKey = getRegionKeyForApi(apiEndpoint);
|
|
478
|
+
return {
|
|
479
|
+
apiEndpoint,
|
|
480
|
+
orgName,
|
|
481
|
+
spaceName,
|
|
482
|
+
...regionKey ? { regionKey } : {}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function parseCfTargetFields(stdout) {
|
|
486
|
+
const fields = /* @__PURE__ */ new Map();
|
|
487
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
488
|
+
const sep = line.indexOf(":");
|
|
489
|
+
if (sep < 0) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const key = line.slice(0, sep).trim().toLowerCase();
|
|
493
|
+
const value = line.slice(sep + 1).trim();
|
|
494
|
+
if (key && value) {
|
|
495
|
+
fields.set(key, value);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return fields;
|
|
499
|
+
}
|
|
400
500
|
var CF_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
401
501
|
var CF_COMMAND_TIMEOUT_MS = 3e4;
|
|
402
502
|
var CF_RETRY_MAX_ATTEMPTS = 3;
|
|
@@ -526,7 +626,6 @@ function extractSafeCliDetail(error) {
|
|
|
526
626
|
}
|
|
527
627
|
|
|
528
628
|
// src/selector.ts
|
|
529
|
-
import { readStructure } from "@saptools/cf-sync";
|
|
530
629
|
var SELECTOR_USAGE = 'use "region/org/space/app" or a bare app name';
|
|
531
630
|
function parseSelector(raw) {
|
|
532
631
|
const trimmed = raw.trim();
|
|
@@ -550,92 +649,39 @@ function parseSelector(raw) {
|
|
|
550
649
|
}
|
|
551
650
|
throw new Error(`Invalid selector "${raw}": ${SELECTOR_USAGE}.`);
|
|
552
651
|
}
|
|
553
|
-
function
|
|
554
|
-
const
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if (app === void 0) {
|
|
652
|
+
async function resolveSelector(raw) {
|
|
653
|
+
const parsed = parseSelector(raw);
|
|
654
|
+
if (parsed.kind === "appName") {
|
|
655
|
+
const current = await readCurrentCfTarget();
|
|
656
|
+
if (!current) {
|
|
657
|
+
throw new Error(
|
|
658
|
+
"No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector."
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
raw,
|
|
663
|
+
regionKey: current.regionKey ?? "",
|
|
664
|
+
apiEndpoint: current.apiEndpoint,
|
|
665
|
+
orgName: current.orgName,
|
|
666
|
+
spaceName: current.spaceName,
|
|
667
|
+
appName: parsed.appName
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const apiEndpoint = getApiEndpointForRegion(parsed.regionKey);
|
|
671
|
+
if (!apiEndpoint) {
|
|
574
672
|
throw new Error(
|
|
575
|
-
`
|
|
673
|
+
`Unknown region "${parsed.regionKey}". Use a bare app name (requires current CF target) or a known region key.`
|
|
576
674
|
);
|
|
577
675
|
}
|
|
578
676
|
return {
|
|
579
677
|
raw,
|
|
580
|
-
regionKey:
|
|
581
|
-
apiEndpoint
|
|
582
|
-
orgName:
|
|
583
|
-
spaceName:
|
|
584
|
-
appName:
|
|
678
|
+
regionKey: parsed.regionKey,
|
|
679
|
+
apiEndpoint,
|
|
680
|
+
orgName: parsed.orgName,
|
|
681
|
+
spaceName: parsed.spaceName,
|
|
682
|
+
appName: parsed.appName
|
|
585
683
|
};
|
|
586
684
|
}
|
|
587
|
-
function resolveByAppName(raw, appName, structure) {
|
|
588
|
-
const matches = [];
|
|
589
|
-
for (const region of structure.regions) {
|
|
590
|
-
for (const org of region.orgs) {
|
|
591
|
-
for (const space of org.spaces) {
|
|
592
|
-
for (const app of space.apps) {
|
|
593
|
-
if (app.name === appName) {
|
|
594
|
-
matches.push({
|
|
595
|
-
raw,
|
|
596
|
-
regionKey: region.key,
|
|
597
|
-
apiEndpoint: region.apiEndpoint,
|
|
598
|
-
orgName: org.name,
|
|
599
|
-
spaceName: space.name,
|
|
600
|
-
appName: app.name
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
if (matches.length === 0) {
|
|
608
|
-
throw new Error(
|
|
609
|
-
`App "${appName}" was not found in the CF topology snapshot. Run \`cf-sync sync\` first, or pass a full region/org/space/app selector.`
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
if (matches.length > 1) {
|
|
613
|
-
const candidates = matches.map((match) => ` ${match.regionKey}/${match.orgName}/${match.spaceName}/${match.appName}`).join("\n");
|
|
614
|
-
throw new Error(
|
|
615
|
-
`App "${appName}" is ambiguous - it exists in multiple spaces:
|
|
616
|
-
${candidates}
|
|
617
|
-
Pass a full region/org/space/app selector to disambiguate.`
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
const onlyMatch = matches[0];
|
|
621
|
-
if (onlyMatch === void 0) {
|
|
622
|
-
throw new Error(`App "${appName}" could not be resolved.`);
|
|
623
|
-
}
|
|
624
|
-
return onlyMatch;
|
|
625
|
-
}
|
|
626
|
-
async function resolveSelector(raw) {
|
|
627
|
-
const parsed = parseSelector(raw);
|
|
628
|
-
const structure = await readStructure();
|
|
629
|
-
if (structure === void 0) {
|
|
630
|
-
throw new Error(
|
|
631
|
-
"No CF topology snapshot found. Run `cf-sync sync` (or `cf-sync space ...`) first."
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
if (parsed.kind === "explicit") {
|
|
635
|
-
return resolveExplicit(raw, parsed, structure);
|
|
636
|
-
}
|
|
637
|
-
return resolveByAppName(raw, parsed.appName, structure);
|
|
638
|
-
}
|
|
639
685
|
|
|
640
686
|
// src/ssh.ts
|
|
641
687
|
var SSH_AUTHORIZED_TYPE = "audit.app.ssh-authorized";
|
|
@@ -847,26 +893,8 @@ function parsePositiveInteger(value, optionName) {
|
|
|
847
893
|
function appLabel(selector) {
|
|
848
894
|
return (selector.split("/").at(-1) ?? selector).trim();
|
|
849
895
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return selector;
|
|
853
|
-
}
|
|
854
|
-
const current = await readCurrentCfTarget(currentCfContext()).catch((error) => {
|
|
855
|
-
throw new Error(
|
|
856
|
-
"No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector.",
|
|
857
|
-
{ cause: error }
|
|
858
|
-
);
|
|
859
|
-
});
|
|
860
|
-
if (current === void 0) {
|
|
861
|
-
throw new Error(
|
|
862
|
-
"No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector."
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
return formatCurrentCfAppSelector(current, selector);
|
|
866
|
-
}
|
|
867
|
-
function currentCfContext() {
|
|
868
|
-
const command = process2.env["CF_EVENTS_CF_BIN"];
|
|
869
|
-
return command === void 0 ? void 0 : { command };
|
|
896
|
+
function resolveSelectorArgument(selector) {
|
|
897
|
+
return selector;
|
|
870
898
|
}
|
|
871
899
|
function writeOut(text) {
|
|
872
900
|
process2.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
@@ -881,7 +909,7 @@ function writeJsonLine(value) {
|
|
|
881
909
|
`);
|
|
882
910
|
}
|
|
883
911
|
async function runEvents(selector, flags) {
|
|
884
|
-
const resolvedSelector =
|
|
912
|
+
const resolvedSelector = resolveSelectorArgument(selector);
|
|
885
913
|
const runtime = new CfEventsRuntime();
|
|
886
914
|
const events = await runtime.fetchEvents(resolvedSelector, buildCredentials(flags), {
|
|
887
915
|
limit: flags.limit ?? DEFAULT_EVENT_LIMIT,
|
|
@@ -895,7 +923,7 @@ async function runEvents(selector, flags) {
|
|
|
895
923
|
writeOut(formatEventsReport(appLabel(resolvedSelector), events, /* @__PURE__ */ new Date()));
|
|
896
924
|
}
|
|
897
925
|
async function runSshStatus(selector, flags) {
|
|
898
|
-
const resolvedSelector =
|
|
926
|
+
const resolvedSelector = resolveSelectorArgument(selector);
|
|
899
927
|
const runtime = new CfEventsRuntime();
|
|
900
928
|
const status = await runtime.getSshStatus(
|
|
901
929
|
resolvedSelector,
|
|
@@ -909,7 +937,7 @@ async function runSshStatus(selector, flags) {
|
|
|
909
937
|
writeOut(formatSshStatusReport(status, /* @__PURE__ */ new Date()));
|
|
910
938
|
}
|
|
911
939
|
async function runCrashes(selector, flags) {
|
|
912
|
-
const resolvedSelector =
|
|
940
|
+
const resolvedSelector = resolveSelectorArgument(selector);
|
|
913
941
|
const runtime = new CfEventsRuntime();
|
|
914
942
|
const summary = await runtime.getCrashes(resolvedSelector, buildCredentials(flags), {
|
|
915
943
|
limit: flags.limit ?? DEFAULT_CRASH_LIMIT,
|
|
@@ -922,7 +950,7 @@ async function runCrashes(selector, flags) {
|
|
|
922
950
|
writeOut(formatCrashReport(summary, /* @__PURE__ */ new Date()));
|
|
923
951
|
}
|
|
924
952
|
async function runStatus(selector, flags) {
|
|
925
|
-
const resolvedSelector =
|
|
953
|
+
const resolvedSelector = resolveSelectorArgument(selector);
|
|
926
954
|
const runtime = new CfEventsRuntime();
|
|
927
955
|
const health = await runtime.getStatus(resolvedSelector, buildCredentials(flags));
|
|
928
956
|
if (flags.json === true) {
|
|
@@ -939,7 +967,7 @@ function emitWatchEvent(event, asJson) {
|
|
|
939
967
|
writeOut(formatEventLine(event, /* @__PURE__ */ new Date()));
|
|
940
968
|
}
|
|
941
969
|
async function runWatch(selector, flags) {
|
|
942
|
-
const resolvedSelector =
|
|
970
|
+
const resolvedSelector = resolveSelectorArgument(selector);
|
|
943
971
|
const interval = flags.interval ?? DEFAULT_WATCH_INTERVAL_MS;
|
|
944
972
|
if (interval < MIN_WATCH_INTERVAL_MS) {
|
|
945
973
|
throw new Error(`--interval must be at least ${MIN_WATCH_INTERVAL_MS.toString()}ms.`);
|