@saptools/cf-events 0.1.4 → 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
@@ -2,9 +2,9 @@
2
2
 
3
3
  # ☁️ `@saptools/cf-events`
4
4
 
5
- **Inspect SAP BTP Cloud Foundry application audit events and detect active SSH/debug sessions from the command line.**
5
+ **Inspect SAP BTP Cloud Foundry application and space audit events and detect active SSH/debug sessions from the command line.**
6
6
 
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?".
7
+ Point it at a `region/org/space/app`, `region/org/space`, or a bare app name and instantly answer: "what just happened?", "is anyone SSH'd in right now?", or "why did it crash?".
8
8
 
9
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
10
  [![license](https://img.shields.io/npm/l/@saptools/cf-events.svg?style=flat&color=blue)](./LICENSE)
@@ -20,16 +20,15 @@ Point it at a `region/org/space/app` (or a bare app name) and instantly answer:
20
20
 
21
21
  ## ✨ Features
22
22
 
23
- - 📜 **Audit event inspection** — list recent `audit.app.*` events (start/stop, scale, restarts, restage, route changes, env views, crashes, SSH authz)
23
+ - 📜 **Audit event inspection** — list recent `audit.app.*` events for one app or all apps in a space
24
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
25
+ - 💥 **Crash summaries** — count, last crash time/reason/exit code, and per-instance detail for an app or grouped by app across a space
26
26
  - ❤️ **One-glance status** — requested state, per-instance CPU/mem/uptime, SSH flag, and the most recent audit event
27
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)
28
+ - 🧭 **Smart selectors** — full app selectors (`region/org/space/app`), space selectors (`region/org/space`), or bare app names resolved from the current `cf target`
30
29
  - 🧪 **Isolated sessions** — every invocation uses a fresh ephemeral `CF_HOME`; never touches your interactive login
31
30
  - 🧩 **CLI + typed library** — full TypeScript exports for `CfEventsRuntime`, parsers, formatters, and types
32
- - 🪶 **Zero daemon, tiny deps** — commander + cf-sync only
31
+ - 🪶 **Zero daemon, tiny deps** — commander only
33
32
 
34
33
  ---
35
34
 
@@ -59,12 +58,10 @@ npm install @saptools/cf-events
59
58
  export SAP_EMAIL="you@company.com"
60
59
  export SAP_PASSWORD="your-sap-password"
61
60
 
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
61
+ # 2. Inspect events
66
62
  cf-events events orders-srv
67
63
  cf-events events ap10/my-org/dev/orders-srv --limit 100 --since 6h
64
+ cf-events events ap10/my-org/dev --limit 200 --since 6h
68
65
 
69
66
  # 4. Check for active SSH/debug sessions
70
67
  cf-events ssh-status orders-srv --since 7d
@@ -79,16 +76,17 @@ cf-events status orders-srv
79
76
  cf-events watch orders-srv --type crash
80
77
  ```
81
78
 
82
- After a `cf-sync` snapshot exists, bare app names are resolved automatically (with current `cf target` expansion when no `/` present).
79
+ Bare app names use the current `cf target` for org/space/API context. Explicit app and space selectors do not require a topology snapshot.
83
80
 
84
81
  ---
85
82
 
86
83
  ## 🧰 CLI
87
84
 
88
- All commands accept a single positional **selector**:
85
+ All commands accept a single positional **selector**. `events`, `watch`, and `crashes` accept app or space selectors; `status` and `ssh-status` require an app selector.
89
86
 
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.
87
+ - App path: `ap10/my-org/dev/orders-srv`
88
+ - Space path: `ap10/my-org/dev`
89
+ - Bare app name: `orders-srv` — resolved against the current `cf target`. Bare single-segment selectors are always app names, never spaces.
92
90
 
93
91
  Common credential flags (all commands):
94
92
 
@@ -100,13 +98,14 @@ Common credential flags (all commands):
100
98
 
101
99
  ### `events <selector>`
102
100
 
103
- List recent audit events for the app (deployments, restarts, scaling, crashes, SSH, routes, ...).
101
+ List recent audit events for the app or all apps in a space (deployments, restarts, scaling, crashes, SSH, routes, ...).
104
102
 
105
103
  ```bash
106
104
  cf-events events ap10/my-org/dev/orders-srv
107
105
  cf-events events orders-srv --limit 100 --since 6h
108
106
  cf-events events orders-srv --type ssh --json
109
107
  cf-events events orders-srv --type audit.app.start,audit.app.stop
108
+ cf-events events ap10/my-org/dev --type ssh --json
110
109
  ```
111
110
 
112
111
  | Flag | Description |
@@ -133,11 +132,12 @@ cf-events ssh-status orders-srv --since 7d --json
133
132
 
134
133
  ### `crashes <selector>`
135
134
 
136
- Count and detail recent crash events (both `audit.app.crash` and `audit.app.process.crash`).
135
+ Count and detail recent crash events for an app or grouped by target app across a space (both `audit.app.crash` and `audit.app.process.crash`).
137
136
 
138
137
  ```bash
139
138
  cf-events crashes orders-srv
140
139
  cf-events crashes orders-srv --since 24h --json
140
+ cf-events crashes ap10/my-org/dev --since 24h --json
141
141
  ```
142
142
 
143
143
  | Flag | Description |
@@ -156,11 +156,12 @@ cf-events status orders-srv --json
156
156
 
157
157
  ### `watch <selector>`
158
158
 
159
- Poll `/v3/audit_events` repeatedly and print (or emit NDJSON) fresh events as they arrive. Press Ctrl+C to stop.
159
+ Poll `/v3/audit_events` repeatedly for an app or space and print (or emit NDJSON) fresh events as they arrive. Press Ctrl+C to stop.
160
160
 
161
161
  ```bash
162
162
  cf-events watch orders-srv
163
163
  cf-events watch orders-srv --interval 30000 --type crash --lookback 5m
164
+ cf-events watch ap10/my-org/dev --lookback 5m --type crash --json
164
165
  ```
165
166
 
166
167
  | Flag | Description |
@@ -172,24 +173,22 @@ cf-events watch orders-srv --interval 30000 --type crash --lookback 5m
172
173
 
173
174
  ---
174
175
 
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.
176
+ ## 📁 Prerequisites & Selector Resolution
178
177
 
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.
178
+ `cf-events` uses direct CF CLI/API calls and an isolated throw-away `CF_HOME`; it does not read or validate a `cf-sync` snapshot.
182
179
 
183
- All CF operations run inside a throw-away `CF_HOME`. Your interactive `cf` login is never affected.
180
+ - Explicit app selectors (`region/org/space/app`) and space selectors (`region/org/space`) use the built-in SAP BTP region-to-API map, then authenticate and target that org/space.
181
+ - Bare app names use your current interactive `cf target` only to discover API endpoint, org, and space, then resolve the app GUID inside the isolated session.
182
+ - Space-wide audit event queries use the Cloud Foundry v3 `space_guids` audit-event filter after resolving org and space GUIDs with `/v3/organizations?names=...` and `/v3/spaces?names=...&organization_guids=...`.
184
183
 
185
- ---
184
+ For JSON output, `events --json` returns the same raw audit-event array shape for app and space selectors. Space events include target, space, and organization references from the CF response. `watch --json` emits newline-delimited JSON events.
186
185
 
187
186
  ## ❓ FAQ
188
187
 
189
188
  <details>
190
189
  <summary><b>Do I need to run cf target first?</b></summary>
191
190
 
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`.
191
+ Not required for explicit selectors. Bare app names require a current `cf target` so `cf-events` can discover the API endpoint, org, and space.
193
192
 
194
193
  </details>
195
194
 
@@ -217,7 +216,7 @@ Yes: `--type crash` or `--type ssh`. You can also pass comma-separated full type
217
216
  <details>
218
217
  <summary><b>Is the snapshot required even for JSON output?</b></summary>
219
218
 
220
- Yes for safety and consistent resolution. The snapshot is the single source of truth for which apps exist in which spaces.
219
+ No. `cf-events` does not read a snapshot. Explicit selectors are resolved with the region map and live CF API/CLI calls; bare app names use the current `cf target`.
221
220
 
222
221
  </details>
223
222
 
@@ -243,7 +242,6 @@ E2E tests are fully fake-backed (no real CF or credentials needed).
243
242
 
244
243
  ## 🌐 Related
245
244
 
246
- - 🗺️ [`@saptools/cf-sync`](https://www.npmjs.com/package/@saptools/cf-sync) — the topology snapshot and current-target helpers that power selector resolution
247
245
  - 🔐 [`@saptools/cf-xsuaa`](https://www.npmjs.com/package/@saptools/cf-xsuaa)
248
246
  - 🐛 [`@saptools/cf-debugger`](https://www.npmjs.com/package/@saptools/cf-debugger)
249
247
  - Full toolbox: [saptools monorepo](https://github.com/dongitran/saptools)
package/dist/cli.js CHANGED
@@ -88,6 +88,39 @@ function summarizeCrashes(appName, events) {
88
88
  crashes
89
89
  };
90
90
  }
91
+ function crashAppName(event) {
92
+ if (event.target.name.length > 0) {
93
+ return event.target.name;
94
+ }
95
+ if (event.target.guid.length > 0) {
96
+ return event.target.guid;
97
+ }
98
+ return "(unknown app)";
99
+ }
100
+ function summarizeSpaceCrashes(selector, events) {
101
+ const byApp = /* @__PURE__ */ new Map();
102
+ for (const event of events.filter(isCrashEvent)) {
103
+ const appName = crashAppName(event);
104
+ byApp.set(appName, [...byApp.get(appName) ?? [], event]);
105
+ }
106
+ const apps = [...byApp.entries()].map(([appName, appEvents]) => summarizeCrashes(appName, appEvents)).map(({ appName, crashCount, lastCrashAt, lastCrashReason, crashes }) => ({
107
+ appName,
108
+ crashCount,
109
+ lastCrashAt,
110
+ lastCrashReason,
111
+ crashes
112
+ })).sort((left, right) => {
113
+ const byTime = (right.lastCrashAt ?? "").localeCompare(left.lastCrashAt ?? "");
114
+ return byTime === 0 ? left.appName.localeCompare(right.appName) : byTime;
115
+ });
116
+ return {
117
+ scope: "space",
118
+ selector,
119
+ crashCount: apps.reduce((sum, app) => sum + app.crashCount, 0),
120
+ lastCrashAt: apps[0]?.lastCrashAt,
121
+ apps
122
+ };
123
+ }
91
124
 
92
125
  // src/format.ts
93
126
  var EVENT_LABELS = {
@@ -188,6 +221,34 @@ function formatEventsReport(appName, events, now) {
188
221
  ...events.map((event) => formatEventLine(event, now))
189
222
  ].join("\n");
190
223
  }
224
+ function eventTargetLabel(event) {
225
+ if (event.target.name.length > 0) {
226
+ return event.target.name;
227
+ }
228
+ if (event.target.type.length > 0) {
229
+ return event.target.type;
230
+ }
231
+ return event.target.guid.length > 0 ? event.target.guid : "(unknown target)";
232
+ }
233
+ function formatSpaceEventLine(event, now) {
234
+ const actor = event.actor.name.length > 0 ? event.actor.name : event.actor.type;
235
+ return [
236
+ ` ${event.createdAt.padEnd(26)}`,
237
+ formatRelativeTime(event.createdAt, now).padEnd(11),
238
+ describeEventType(event.type).padEnd(24),
239
+ eventTargetLabel(event).padEnd(18),
240
+ actor.length > 0 ? actor : "(unknown actor)"
241
+ ].join(" ");
242
+ }
243
+ function formatSpaceEventsReport(selector, events, now) {
244
+ if (events.length === 0) {
245
+ return `No audit events found for space ${selector}.`;
246
+ }
247
+ return [
248
+ `Audit events for space ${selector} (${events.length.toString()}):`,
249
+ ...events.map((event) => formatSpaceEventLine(event, now))
250
+ ].join("\n");
251
+ }
191
252
  function formatSshStatusReport(status, now) {
192
253
  const sessionLines = status.sessions.map((session) => {
193
254
  const tag = session.likelyActive ? "[active]" : "[past] ";
@@ -212,7 +273,7 @@ function formatSshStatusReport(status, now) {
212
273
  " are inferred from recent ssh-authorized audit events."
213
274
  ].join("\n");
214
275
  }
215
- function formatCrashReport(summary, now) {
276
+ function formatAppCrashReport(summary, now) {
216
277
  if (summary.crashCount === 0) {
217
278
  return `No crashes found for ${summary.appName}.`;
218
279
  }
@@ -233,6 +294,35 @@ function formatCrashReport(summary, now) {
233
294
  ...crashLines
234
295
  ].join("\n");
235
296
  }
297
+ function formatSpaceCrashReport(summary, now) {
298
+ if (summary.crashCount === 0) {
299
+ return `No crashes found for space ${summary.selector}.`;
300
+ }
301
+ const appLines = summary.apps.flatMap((app) => [
302
+ ` ${app.appName} (${app.crashCount.toString()})`,
303
+ ...app.crashes.map((crash) => {
304
+ const index = crash.index === void 0 ? "-" : `#${crash.index.toString()}`;
305
+ const reason = crash.reason ?? "unknown";
306
+ const exit = crash.exitStatus === void 0 ? "" : ` exit ${crash.exitStatus.toString()}`;
307
+ return ` ${crash.at} instance ${index} ${reason}${exit}`;
308
+ })
309
+ ]);
310
+ return [
311
+ `Crash report for space ${summary.selector}`,
312
+ "",
313
+ ` Crashes: ${summary.crashCount.toString()}`,
314
+ ` Apps affected: ${summary.apps.length.toString()}`,
315
+ ...summary.lastCrashAt === void 0 ? [] : [` Last crash: ${summary.lastCrashAt} (${formatRelativeTime(summary.lastCrashAt, now)})`],
316
+ "",
317
+ ...appLines
318
+ ].join("\n");
319
+ }
320
+ function formatCrashReport(summary, now) {
321
+ if ("scope" in summary) {
322
+ return formatSpaceCrashReport(summary, now);
323
+ }
324
+ return formatAppCrashReport(summary, now);
325
+ }
236
326
  function formatInstanceLine(instance) {
237
327
  return [
238
328
  ` #${instance.index.toString()}`.padEnd(6),
@@ -327,7 +417,7 @@ function mapProcessStat(value) {
327
417
  }
328
418
  function buildAuditEventsPath(input, perPage) {
329
419
  const params = new URLSearchParams();
330
- params.set("target_guids", input.appGuid);
420
+ applyAuditEventScope(params, input.scope);
331
421
  params.set("order_by", "-created_at");
332
422
  params.set("per_page", perPage.toString());
333
423
  if (input.types !== void 0 && input.types.length > 0) {
@@ -338,6 +428,36 @@ function buildAuditEventsPath(input, perPage) {
338
428
  }
339
429
  return `/v3/audit_events?${params.toString()}`;
340
430
  }
431
+ function applyAuditEventScope(params, scope) {
432
+ if (scope.kind === "app") {
433
+ params.set("target_guids", scope.appGuid);
434
+ return;
435
+ }
436
+ params.set("space_guids", scope.spaceGuid);
437
+ }
438
+ async function resolveOrganizationGuid(orgName, curl) {
439
+ const params = new URLSearchParams();
440
+ params.set("names", orgName);
441
+ const body = asRecord(parseJson(await curl(`/v3/organizations?${params.toString()}`)));
442
+ const first = asRecord(asArray(body["resources"])[0]);
443
+ const guid = asString(first["guid"]);
444
+ if (guid.length === 0) {
445
+ throw new Error(`Could not resolve the GUID for organization "${orgName}".`);
446
+ }
447
+ return guid;
448
+ }
449
+ async function resolveSpaceGuid(spaceName, orgGuid, curl) {
450
+ const params = new URLSearchParams();
451
+ params.set("names", spaceName);
452
+ params.set("organization_guids", orgGuid);
453
+ const body = asRecord(parseJson(await curl(`/v3/spaces?${params.toString()}`)));
454
+ const first = asRecord(asArray(body["resources"])[0]);
455
+ const guid = asString(first["guid"]);
456
+ if (guid.length === 0) {
457
+ throw new Error(`Could not resolve the GUID for space "${spaceName}".`);
458
+ }
459
+ return guid;
460
+ }
341
461
  function nextPagePath(body) {
342
462
  const next = asRecord(asRecord(asRecord(body)["pagination"])["next"]);
343
463
  const href = next["href"];
@@ -626,7 +746,12 @@ function extractSafeCliDetail(error) {
626
746
  }
627
747
 
628
748
  // src/selector.ts
629
- var SELECTOR_USAGE = 'use "region/org/space/app" or a bare app name';
749
+ var SELECTOR_USAGE = 'use "region/org/space", "region/org/space/app", or a bare app name';
750
+ function assertNonEmpty(raw, parts, shape) {
751
+ if (parts.some((part) => part.length === 0)) {
752
+ throw new Error(`Invalid selector "${raw}": every ${shape} segment must be non-empty.`);
753
+ }
754
+ }
630
755
  function parseSelector(raw) {
631
756
  const trimmed = raw.trim();
632
757
  if (trimmed.length === 0) {
@@ -640,12 +765,21 @@ function parseSelector(raw) {
640
765
  }
641
766
  return { kind: "appName", appName };
642
767
  }
768
+ if (parts.length === 3) {
769
+ assertNonEmpty(raw, parts, "region/org/space");
770
+ const [regionKey, orgName, spaceName] = parts;
771
+ return { kind: "spacePath", regionKey: regionKey ?? "", orgName: orgName ?? "", spaceName: spaceName ?? "" };
772
+ }
643
773
  if (parts.length === 4) {
774
+ assertNonEmpty(raw, parts, "region/org/space/app");
644
775
  const [regionKey, orgName, spaceName, appName] = parts;
645
- if (regionKey === void 0 || orgName === void 0 || spaceName === void 0 || appName === void 0 || regionKey.length === 0 || orgName.length === 0 || spaceName.length === 0 || appName.length === 0) {
646
- throw new Error(`Invalid selector "${raw}": every region/org/space/app segment must be non-empty.`);
647
- }
648
- return { kind: "explicit", regionKey, orgName, spaceName, appName };
776
+ return {
777
+ kind: "appPath",
778
+ regionKey: regionKey ?? "",
779
+ orgName: orgName ?? "",
780
+ spaceName: spaceName ?? "",
781
+ appName: appName ?? ""
782
+ };
649
783
  }
650
784
  throw new Error(`Invalid selector "${raw}": ${SELECTOR_USAGE}.`);
651
785
  }
@@ -659,6 +793,7 @@ async function resolveSelector(raw) {
659
793
  );
660
794
  }
661
795
  return {
796
+ kind: "app",
662
797
  raw,
663
798
  regionKey: current.regionKey ?? "",
664
799
  apiEndpoint: current.apiEndpoint,
@@ -673,7 +808,18 @@ async function resolveSelector(raw) {
673
808
  `Unknown region "${parsed.regionKey}". Use a bare app name (requires current CF target) or a known region key.`
674
809
  );
675
810
  }
811
+ if (parsed.kind === "spacePath") {
812
+ return {
813
+ kind: "space",
814
+ raw,
815
+ regionKey: parsed.regionKey,
816
+ apiEndpoint,
817
+ orgName: parsed.orgName,
818
+ spaceName: parsed.spaceName
819
+ };
820
+ }
676
821
  return {
822
+ kind: "app",
677
823
  raw,
678
824
  regionKey: parsed.regionKey,
679
825
  apiEndpoint,
@@ -724,10 +870,12 @@ function createCfClient(curl) {
724
870
  fetchAuditEvents: (input) => fetchAuditEvents(input, curl),
725
871
  fetchApp: (appGuid) => fetchApp(appGuid, curl),
726
872
  fetchSshEnabled: (appGuid) => fetchSshEnabled(appGuid, curl),
727
- fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl)
873
+ fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl),
874
+ resolveOrganizationGuid: (orgName) => resolveOrganizationGuid(orgName, curl),
875
+ resolveSpaceGuid: (spaceName, orgGuid) => resolveSpaceGuid(spaceName, orgGuid, curl)
728
876
  };
729
877
  }
730
- async function defaultWithCfApp(selector, credentials, work) {
878
+ async function defaultWithCfTarget(selector, credentials, work) {
731
879
  return await withCfSession(async (ctx) => {
732
880
  await prepareCfCliSession(
733
881
  {
@@ -738,15 +886,14 @@ async function defaultWithCfApp(selector, credentials, work) {
738
886
  },
739
887
  ctx
740
888
  );
741
- const appGuid = await cfAppGuid(selector.appName, ctx);
742
889
  const client = createCfClient((path) => cfCurl(path, ctx));
743
- return await work({ appGuid, client });
890
+ return await work({ selector, client, resolveAppGuid: (appName) => cfAppGuid(appName, ctx) });
744
891
  });
745
892
  }
746
893
  function createDefaultDependencies() {
747
894
  return {
748
895
  resolveSelector,
749
- withCfApp: defaultWithCfApp,
896
+ withCfTarget: defaultWithCfTarget,
750
897
  now: () => /* @__PURE__ */ new Date()
751
898
  };
752
899
  }
@@ -756,6 +903,24 @@ async function delay(ms, signal) {
756
903
  } catch {
757
904
  }
758
905
  }
906
+ function requireAppSelector(selector, command) {
907
+ if (selector.kind === "app") {
908
+ return selector;
909
+ }
910
+ throw new Error(
911
+ `The ${command} command requires an app selector. Use region/org/space/app or a bare app name.`
912
+ );
913
+ }
914
+ async function resolveAuditScope(session) {
915
+ if (session.selector.kind === "app") {
916
+ return { kind: "app", appGuid: await session.resolveAppGuid(session.selector.appName) };
917
+ }
918
+ const orgGuid = await session.client.resolveOrganizationGuid(session.selector.orgName);
919
+ return {
920
+ kind: "space",
921
+ spaceGuid: await session.client.resolveSpaceGuid(session.selector.spaceName, orgGuid)
922
+ };
923
+ }
759
924
  var CfEventsRuntime = class {
760
925
  deps;
761
926
  constructor(deps = createDefaultDependencies()) {
@@ -763,9 +928,9 @@ var CfEventsRuntime = class {
763
928
  }
764
929
  async fetchEvents(raw, credentials, options) {
765
930
  const selector = await this.deps.resolveSelector(raw);
766
- return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
767
- return await client.fetchAuditEvents({
768
- appGuid,
931
+ return await this.deps.withCfTarget(selector, credentials, async (session) => {
932
+ return await session.client.fetchAuditEvents({
933
+ scope: await resolveAuditScope(session),
769
934
  types: options.types.length > 0 ? options.types : void 0,
770
935
  createdAfter: this.resolveCreatedAfter(options.since),
771
936
  limit: options.limit
@@ -773,12 +938,13 @@ var CfEventsRuntime = class {
773
938
  });
774
939
  }
775
940
  async getSshStatus(raw, credentials, since) {
776
- const selector = await this.deps.resolveSelector(raw);
777
- return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
941
+ const selector = requireAppSelector(await this.deps.resolveSelector(raw), "ssh-status");
942
+ return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
943
+ const appGuid = await resolveAppGuid(selector.appName);
778
944
  const [sshEnabled, events] = await Promise.all([
779
945
  client.fetchSshEnabled(appGuid),
780
946
  client.fetchAuditEvents({
781
- appGuid,
947
+ scope: { kind: "app", appGuid },
782
948
  types: [...SSH_EVENT_TYPES],
783
949
  createdAfter: durationToCreatedAfter(since, this.deps.now()),
784
950
  limit: SSH_STATUS_EVENT_LIMIT
@@ -795,24 +961,28 @@ var CfEventsRuntime = class {
795
961
  }
796
962
  async getCrashes(raw, credentials, options) {
797
963
  const selector = await this.deps.resolveSelector(raw);
798
- return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
799
- const events = await client.fetchAuditEvents({
800
- appGuid,
964
+ return await this.deps.withCfTarget(selector, credentials, async (session) => {
965
+ const events = await session.client.fetchAuditEvents({
966
+ scope: await resolveAuditScope(session),
801
967
  types: [...CRASH_EVENT_TYPES],
802
968
  createdAfter: this.resolveCreatedAfter(options.since),
803
969
  limit: options.limit
804
970
  });
805
- return summarizeCrashes(selector.appName, events);
971
+ if (selector.kind === "app") {
972
+ return summarizeCrashes(selector.appName, events);
973
+ }
974
+ return summarizeSpaceCrashes(`${selector.regionKey}/${selector.orgName}/${selector.spaceName}`, events);
806
975
  });
807
976
  }
808
977
  async getStatus(raw, credentials) {
809
- const selector = await this.deps.resolveSelector(raw);
810
- return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
978
+ const selector = requireAppSelector(await this.deps.resolveSelector(raw), "status");
979
+ return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
980
+ const appGuid = await resolveAppGuid(selector.appName);
811
981
  const [app, instances, sshEnabled, recent] = await Promise.all([
812
982
  client.fetchApp(appGuid),
813
983
  client.fetchWebProcessStats(appGuid),
814
984
  client.fetchSshEnabled(appGuid),
815
- client.fetchAuditEvents({ appGuid, types: void 0, createdAfter: void 0, limit: 1 })
985
+ client.fetchAuditEvents({ scope: { kind: "app", appGuid }, types: void 0, createdAfter: void 0, limit: 1 })
816
986
  ]);
817
987
  return {
818
988
  appName: selector.appName,
@@ -826,12 +996,13 @@ var CfEventsRuntime = class {
826
996
  }
827
997
  async watchEvents(raw, credentials, options, onEvent, signal) {
828
998
  const selector = await this.deps.resolveSelector(raw);
829
- await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
999
+ await this.deps.withCfTarget(selector, credentials, async (session) => {
1000
+ const scope = await resolveAuditScope(session);
830
1001
  const seen = /* @__PURE__ */ new Set();
831
1002
  let cursor = durationToCreatedAfter(options.lookback, this.deps.now());
832
1003
  while (!signal.aborted) {
833
- const events = await client.fetchAuditEvents({
834
- appGuid,
1004
+ const events = await session.client.fetchAuditEvents({
1005
+ scope,
835
1006
  types: options.types.length > 0 ? options.types : void 0,
836
1007
  createdAfter: cursor,
837
1008
  limit: WATCH_FETCH_LIMIT
@@ -893,6 +1064,13 @@ function parsePositiveInteger(value, optionName) {
893
1064
  function appLabel(selector) {
894
1065
  return (selector.split("/").at(-1) ?? selector).trim();
895
1066
  }
1067
+ function isSpaceSelector(selector) {
1068
+ return parseSelector(selector).kind === "spacePath";
1069
+ }
1070
+ function spaceLabel(selector) {
1071
+ const parsed = parseSelector(selector);
1072
+ return parsed.kind === "spacePath" ? `${parsed.regionKey}/${parsed.orgName}/${parsed.spaceName}` : selector;
1073
+ }
896
1074
  function resolveSelectorArgument(selector) {
897
1075
  return selector;
898
1076
  }
@@ -920,7 +1098,7 @@ async function runEvents(selector, flags) {
920
1098
  writeJson(events);
921
1099
  return;
922
1100
  }
923
- writeOut(formatEventsReport(appLabel(resolvedSelector), events, /* @__PURE__ */ new Date()));
1101
+ writeOut(isSpaceSelector(resolvedSelector) ? formatSpaceEventsReport(spaceLabel(resolvedSelector), events, /* @__PURE__ */ new Date()) : formatEventsReport(appLabel(resolvedSelector), events, /* @__PURE__ */ new Date()));
924
1102
  }
925
1103
  async function runSshStatus(selector, flags) {
926
1104
  const resolvedSelector = resolveSelectorArgument(selector);
@@ -959,12 +1137,12 @@ async function runStatus(selector, flags) {
959
1137
  }
960
1138
  writeOut(formatStatusReport(health, /* @__PURE__ */ new Date()));
961
1139
  }
962
- function emitWatchEvent(event, asJson) {
1140
+ function emitWatchEvent(event, asJson, spaceScope) {
963
1141
  if (asJson) {
964
1142
  writeJsonLine(event);
965
1143
  return;
966
1144
  }
967
- writeOut(formatEventLine(event, /* @__PURE__ */ new Date()));
1145
+ writeOut(spaceScope ? formatSpaceEventLine(event, /* @__PURE__ */ new Date()) : formatEventLine(event, /* @__PURE__ */ new Date()));
968
1146
  }
969
1147
  async function runWatch(selector, flags) {
970
1148
  const resolvedSelector = resolveSelectorArgument(selector);
@@ -978,6 +1156,7 @@ async function runWatch(selector, flags) {
978
1156
  controller.abort();
979
1157
  });
980
1158
  const asJson = flags.json === true;
1159
+ const spaceScope = isSpaceSelector(resolvedSelector);
981
1160
  try {
982
1161
  process2.stderr.write(`Watching ${resolvedSelector} for new audit events. Press Ctrl+C to stop.
983
1162
  `);
@@ -990,7 +1169,7 @@ async function runWatch(selector, flags) {
990
1169
  types: parseTypeFilter(flags.type)
991
1170
  },
992
1171
  (event) => {
993
- emitWatchEvent(event, asJson);
1172
+ emitWatchEvent(event, asJson, spaceScope);
994
1173
  },
995
1174
  controller.signal
996
1175
  );
@@ -1036,7 +1215,7 @@ function buildProgram() {
1036
1215
  const program = new Command();
1037
1216
  program.name("cf-events").description("Inspect SAP BTP Cloud Foundry application audit events and SSH/debug sessions").version(readPackageVersion(), "-V, --version", "Print the cf-events package version");
1038
1217
  addCommonOptions(
1039
- program.command("events").description("List recent audit events for an app").argument("<selector>", "region/org/space/app or a bare app name")
1218
+ program.command("events").description("List recent audit events for an app or all apps in a space").argument("<selector>", "region/org/space, region/org/space/app, or a bare app name")
1040
1219
  ).option(
1041
1220
  "--limit <count>",
1042
1221
  "Maximum number of events to return",
@@ -1050,7 +1229,7 @@ function buildProgram() {
1050
1229
  await runSshStatus(selector, flags);
1051
1230
  });
1052
1231
  addCommonOptions(
1053
- program.command("crashes").description("Summarize recent crash events for an app").argument("<selector>", "region/org/space/app or a bare app name")
1232
+ program.command("crashes").description("Summarize recent crash events for an app or all apps in a space").argument("<selector>", "region/org/space, region/org/space/app, or a bare app name")
1054
1233
  ).option(
1055
1234
  "--limit <count>",
1056
1235
  "Maximum number of crash events to inspect",
@@ -1064,7 +1243,7 @@ function buildProgram() {
1064
1243
  await runStatus(selector, flags);
1065
1244
  });
1066
1245
  addCommonOptions(
1067
- program.command("watch").description("Poll for new audit events and print them as they appear").argument("<selector>", "region/org/space/app or a bare app name")
1246
+ program.command("watch").description("Poll for new audit events for an app or all apps in a space").argument("<selector>", "region/org/space, region/org/space/app, or a bare app name")
1068
1247
  ).option(
1069
1248
  "--interval <ms>",
1070
1249
  "Polling interval in milliseconds",