@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 CHANGED
@@ -1,135 +1,263 @@
1
- # @saptools/cf-events
1
+ <div align="center">
2
2
 
3
- Inspect SAP BTP Cloud Foundry application **audit events** and detect active
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
- `cf-events` reuses [`@saptools/cf-sync`](https://github.com/dongitran/saptools/tree/main/packages/cf-sync)
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
- ## Prerequisites
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
- - Node.js >= 20.
15
- - The [Cloud Foundry CLI v8+](https://github.com/cloudfoundry/cli) on `PATH`.
16
- - A topology snapshot produced by `cf-sync`. Run `cf-sync sync` (or a targeted
17
- `cf-sync space <region> <org> <space>`) once so `cf-events` can resolve and
18
- validate selectors. The snapshot lives at `~/.saptools/cf-structure.json`.
9
+ [![npm version](https://img.shields.io/npm/v/@saptools/cf-events.svg?style=flat&color=CB3837&logo=npm)](https://www.npmjs.com/package/@saptools/cf-events)
10
+ [![license](https://img.shields.io/npm/l/@saptools/cf-events.svg?style=flat&color=blue)](./LICENSE)
11
+ [![node](https://img.shields.io/node/v/@saptools/cf-events.svg?style=flat&color=339933&logo=node.js&logoColor=white)](https://nodejs.org)
12
+ [![install size](https://packagephobia.com/badge?p=@saptools/cf-events)](https://packagephobia.com/result?p=@saptools/cf-events)
13
+ [![types](https://img.shields.io/npm/types/@saptools/cf-events.svg?style=flat&color=3178C6&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
19
14
 
20
- ## Install
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
- # or run it on demand
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
- ## Authentication
50
+ > [!NOTE]
51
+ > Requires **Node.js ≥ 20** and the official **Cloud Foundry CLI (v8+)** on `PATH`.
52
+
53
+ ---
29
54
 
30
- Credentials are read from environment variables (preferred) or flags:
55
+ ## 🚀 Quick Start
31
56
 
32
57
  ```bash
33
- export SAP_EMAIL="you@example.com"
34
- export SAP_PASSWORD="your-password"
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
- Every command also accepts `--email` and `--password`. Credentials are passed
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
- ## Selectors
84
+ ---
42
85
 
43
- Every command takes a single positional selector:
86
+ ## 🧰 CLI
44
87
 
45
- - A full path: `ap10/my-org/dev/orders-srv`.
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
- ## Commands
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 an app (deployments, restarts, scaling, crashes,
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
- Options: `--limit <count>` (default 50), `--since <duration>` (e.g. `30m`,
64
- `6h`, `7d`), `--type <types>` (comma-separated CF event types, or the
65
- shorthand `ssh` / `crash`), `--json`.
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 whether SSH is enabled for the app and surface recent SSH / debug
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
- Options: `--since <duration>` (default `24h`), `--json`.
127
+ | Flag | Description |
128
+ |---------------|-----------------------------------------|
129
+ | `--since <dur>` | Look-back window (default `24h`) |
78
130
 
79
- > Cloud Foundry exposes no live-session API and emits no event when an SSH
80
- > session closes. `cf-events` therefore *infers* "likely active" sessions from
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
- Summarize recent crash events: how many, the most recent one, and the exit
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
- Options: `--limit <count>` (default 50), `--since <duration>`, `--json`.
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
- A one-glance health view: requested state, per-instance state / uptime / CPU /
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` on an interval and print new events as they appear.
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
- Options: `--interval <ms>` (default 15000, minimum 2000), `--lookback
115
- <duration>` (initial window on start, default `2m`), `--type <types>`,
116
- `--json` (line-delimited JSON).
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
- ## Programmatic use
189
+ <details>
190
+ <summary><b>Do I need to run cf target first?</b></summary>
119
191
 
120
- The package also ships a typed library entry:
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
- ```ts
123
- import { CfEventsRuntime } from "@saptools/cf-events";
194
+ </details>
124
195
 
125
- const runtime = new CfEventsRuntime();
126
- const events = await runtime.fetchEvents("orders-srv", credentials, {
127
- limit: 50,
128
- since: "6h",
129
- types: [],
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
- ## License
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 resolveExplicit(raw, parsed, structure) {
554
- const region = structure.regions.find((entry) => entry.key === parsed.regionKey);
555
- if (region === void 0) {
556
- throw new Error(
557
- `Region "${parsed.regionKey}" is not in the CF topology snapshot. Run \`cf-sync sync\` first (or \`cf-sync regions\` to list valid region keys).`
558
- );
559
- }
560
- const org = region.orgs.find((entry) => entry.name === parsed.orgName);
561
- if (org === void 0) {
562
- throw new Error(
563
- `Org "${parsed.orgName}" was not found in region ${parsed.regionKey}. Run \`cf-sync orgs ${parsed.regionKey}\` to refresh it.`
564
- );
565
- }
566
- const space = org.spaces.find((entry) => entry.name === parsed.spaceName);
567
- if (space === void 0) {
568
- throw new Error(
569
- `Space "${parsed.spaceName}" was not found in ${parsed.regionKey}/${parsed.orgName}. Run \`cf-sync org ${parsed.regionKey} ${parsed.orgName}\` to refresh it.`
570
- );
571
- }
572
- const app = space.apps.find((entry) => entry.name === parsed.appName);
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
- `App "${parsed.appName}" was not found in ${parsed.regionKey}/${parsed.orgName}/${parsed.spaceName}. Run \`cf-sync space ${parsed.regionKey} ${parsed.orgName} ${parsed.spaceName}\` to refresh it.`
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: region.key,
581
- apiEndpoint: region.apiEndpoint,
582
- orgName: org.name,
583
- spaceName: space.name,
584
- appName: app.name
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
- async function resolveSelectorArgument(selector) {
851
- if (selector.includes("/")) {
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 = await resolveSelectorArgument(selector);
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 = await resolveSelectorArgument(selector);
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 = await resolveSelectorArgument(selector);
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 = await resolveSelectorArgument(selector);
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 = await resolveSelectorArgument(selector);
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.`);