@saptools/cf-events 0.1.4 → 0.2.1

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/dist/index.d.ts CHANGED
@@ -10,8 +10,9 @@ interface CfCredentials {
10
10
  readonly email: string;
11
11
  readonly password: string;
12
12
  }
13
- /** A fully resolved `region/org/space/app` target. */
14
- interface ResolvedSelector {
13
+ /** A fully resolved application target. */
14
+ interface ResolvedAppSelector {
15
+ readonly kind: "app";
15
16
  readonly raw: string;
16
17
  readonly regionKey: RegionKey;
17
18
  readonly apiEndpoint: string;
@@ -19,6 +20,16 @@ interface ResolvedSelector {
19
20
  readonly spaceName: string;
20
21
  readonly appName: string;
21
22
  }
23
+ /** A fully resolved space target for space-wide audit-event queries. */
24
+ interface ResolvedSpaceSelector {
25
+ readonly kind: "space";
26
+ readonly raw: string;
27
+ readonly regionKey: RegionKey;
28
+ readonly apiEndpoint: string;
29
+ readonly orgName: string;
30
+ readonly spaceName: string;
31
+ }
32
+ type ResolvedSelector = ResolvedAppSelector | ResolvedSpaceSelector;
22
33
  interface CfSessionInput {
23
34
  readonly apiEndpoint: string;
24
35
  readonly orgName: string;
@@ -104,8 +115,30 @@ interface AppHealth {
104
115
  readonly instances: readonly ProcessInstanceStat[];
105
116
  readonly lastEvent: AuditEvent | undefined;
106
117
  }
107
- interface FetchAuditEventsInput {
118
+ interface AppCrashSummary {
119
+ readonly appName: string;
120
+ readonly crashCount: number;
121
+ readonly lastCrashAt: string | undefined;
122
+ readonly lastCrashReason: string | undefined;
123
+ readonly crashes: readonly CrashRecord[];
124
+ }
125
+ interface SpaceCrashSummary {
126
+ readonly scope: "space";
127
+ readonly selector: string;
128
+ readonly crashCount: number;
129
+ readonly lastCrashAt: string | undefined;
130
+ readonly apps: readonly AppCrashSummary[];
131
+ }
132
+ type CrashReportSummary = CrashSummary | SpaceCrashSummary;
133
+ type AuditEventScope = {
134
+ readonly kind: "app";
108
135
  readonly appGuid: string;
136
+ } | {
137
+ readonly kind: "space";
138
+ readonly spaceGuid: string;
139
+ };
140
+ interface FetchAuditEventsInput {
141
+ readonly scope: AuditEventScope;
109
142
  readonly types: readonly string[] | undefined;
110
143
  readonly createdAfter: string | undefined;
111
144
  readonly limit: number;
@@ -113,8 +146,11 @@ interface FetchAuditEventsInput {
113
146
 
114
147
  /** Calls a Cloud Foundry v3 API path and returns the raw response body. */
115
148
  type CurlFn = (path: string) => Promise<string>;
149
+ declare function mapAuditEvent(value: unknown): AuditEvent;
116
150
  declare function buildAuditEventsPath(input: FetchAuditEventsInput, perPage: number): string;
117
- /** Fetches audit events for an app, following pagination up to `input.limit`. */
151
+ declare function resolveOrganizationGuid(orgName: string, curl: CurlFn): Promise<string>;
152
+ declare function resolveSpaceGuid(spaceName: string, orgGuid: string, curl: CurlFn): Promise<string>;
153
+ /** Fetches audit events for an app or space, following pagination up to `input.limit`. */
118
154
  declare function fetchAuditEvents(input: FetchAuditEventsInput, curl: CurlFn): Promise<readonly AuditEvent[]>;
119
155
  declare function fetchApp(appGuid: string, curl: CurlFn): Promise<AppSummary>;
120
156
  declare function fetchSshEnabled(appGuid: string, curl: CurlFn): Promise<SshEnabled>;
@@ -149,7 +185,7 @@ declare function filterByTypes(events: readonly AuditEvent[], types: readonly st
149
185
  declare function sortEventsNewestFirst(events: readonly AuditEvent[]): readonly AuditEvent[];
150
186
  /** Parses a human duration such as `30m`, `6h`, or `7d` into milliseconds. */
151
187
  declare function parseDuration(raw: string): number;
152
- /** Converts a duration into an ISO timestamp suitable for `created_ats[gt]`. */
188
+ /** Converts a duration into a CF audit timestamp suitable for `created_ats[gt]`. */
153
189
  declare function durationToCreatedAfter(raw: string, now: Date): string;
154
190
  /**
155
191
  * Parses a comma-separated `--type` filter. Accepts the shorthands `ssh` and
@@ -158,6 +194,7 @@ declare function durationToCreatedAfter(raw: string, now: Date): string;
158
194
  declare function parseTypeFilter(raw: string | undefined): readonly string[];
159
195
  declare function toCrashRecord(event: AuditEvent): CrashRecord;
160
196
  declare function summarizeCrashes(appName: string, events: readonly AuditEvent[]): CrashSummary;
197
+ declare function summarizeSpaceCrashes(selector: string, events: readonly AuditEvent[]): SpaceCrashSummary;
161
198
 
162
199
  declare function describeEventType(type: string): string;
163
200
  declare function formatUptime(seconds: number | undefined): string;
@@ -166,8 +203,10 @@ declare function formatCpu(cpu: number | undefined): string;
166
203
  declare function formatRelativeTime(timestamp: string, now: Date): string;
167
204
  declare function formatEventLine(event: AuditEvent, now: Date): string;
168
205
  declare function formatEventsReport(appName: string, events: readonly AuditEvent[], now: Date): string;
206
+ declare function formatSpaceEventLine(event: AuditEvent, now: Date): string;
207
+ declare function formatSpaceEventsReport(selector: string, events: readonly AuditEvent[], now: Date): string;
169
208
  declare function formatSshStatusReport(status: SshStatus, now: Date): string;
170
- declare function formatCrashReport(summary: CrashSummary, now: Date): string;
209
+ declare function formatCrashReport(summary: CrashReportSummary, now: Date): string;
171
210
  declare function formatStatusReport(health: AppHealth, now: Date): string;
172
211
 
173
212
  /** Typed accessor for the Cloud Foundry v3 API used by the runtime. */
@@ -176,15 +215,18 @@ interface CfClient {
176
215
  readonly fetchApp: (appGuid: string) => Promise<AppSummary>;
177
216
  readonly fetchSshEnabled: (appGuid: string) => Promise<SshEnabled>;
178
217
  readonly fetchWebProcessStats: (appGuid: string) => Promise<readonly ProcessInstanceStat[]>;
218
+ readonly resolveOrganizationGuid: (orgName: string) => Promise<string>;
219
+ readonly resolveSpaceGuid: (spaceName: string, orgGuid: string) => Promise<string>;
179
220
  }
180
- interface CfAppSession {
181
- readonly appGuid: string;
221
+ interface CfTargetSession {
222
+ readonly selector: ResolvedSelector;
182
223
  readonly client: CfClient;
224
+ readonly resolveAppGuid: (appName: string) => Promise<string>;
183
225
  }
184
226
  /** Injection seam so the orchestration can be unit-tested without a real CF. */
185
227
  interface CfEventsDependencies {
186
228
  readonly resolveSelector: (raw: string) => Promise<ResolvedSelector>;
187
- readonly withCfApp: <T>(selector: ResolvedSelector, credentials: CfCredentials, work: (session: CfAppSession) => Promise<T>) => Promise<T>;
229
+ readonly withCfTarget: <T>(selector: ResolvedSelector, credentials: CfCredentials, work: (session: CfTargetSession) => Promise<T>) => Promise<T>;
188
230
  readonly now: () => Date;
189
231
  }
190
232
  interface EventQueryOptions {
@@ -208,7 +250,7 @@ declare class CfEventsRuntime {
208
250
  constructor(deps?: CfEventsDependencies);
209
251
  fetchEvents(raw: string, credentials: CfCredentials, options: EventQueryOptions): Promise<readonly AuditEvent[]>;
210
252
  getSshStatus(raw: string, credentials: CfCredentials, since: string): Promise<SshStatus>;
211
- getCrashes(raw: string, credentials: CfCredentials, options: CrashQueryOptions): Promise<CrashSummary>;
253
+ getCrashes(raw: string, credentials: CfCredentials, options: CrashQueryOptions): Promise<CrashReportSummary>;
212
254
  getStatus(raw: string, credentials: CfCredentials): Promise<AppHealth>;
213
255
  watchEvents(raw: string, credentials: CfCredentials, options: WatchOptions, onEvent: (event: AuditEvent) => void, signal: AbortSignal): Promise<void>;
214
256
  private resolveCreatedAfter;
@@ -218,20 +260,26 @@ interface ParsedAppName {
218
260
  readonly kind: "appName";
219
261
  readonly appName: string;
220
262
  }
221
- interface ParsedExplicit {
222
- readonly kind: "explicit";
263
+ interface ParsedAppPath {
264
+ readonly kind: "appPath";
223
265
  readonly regionKey: string;
224
266
  readonly orgName: string;
225
267
  readonly spaceName: string;
226
268
  readonly appName: string;
227
269
  }
228
- type ParsedSelector = ParsedAppName | ParsedExplicit;
270
+ interface ParsedSpacePath {
271
+ readonly kind: "spacePath";
272
+ readonly regionKey: string;
273
+ readonly orgName: string;
274
+ readonly spaceName: string;
275
+ }
276
+ type ParsedSelector = ParsedAppName | ParsedAppPath | ParsedSpacePath;
229
277
  /** Parses a raw selector string into its structural parts without any I/O. */
230
278
  declare function parseSelector(raw: string): ParsedSelector;
231
279
  /**
232
280
  * Resolves a raw selector.
233
281
  * - Bare app name: based strictly on the CURRENT CF target (no global search).
234
- * - Full path: uses known region-to-api mapping; trusts the provided org/space/app (no snapshot validation).
282
+ * - Explicit paths: use known region-to-api mapping; trust the provided org/space/app.
235
283
  */
236
284
  declare function resolveSelector(raw: string): Promise<ResolvedSelector>;
237
285
 
@@ -246,4 +294,4 @@ interface SshStatusInput {
246
294
  }
247
295
  declare function buildSshStatus(input: SshStatusInput): SshStatus;
248
296
 
249
- export { APP_EVENT_TYPES, type AppEventType, type AppHealth, type AppSummary, type AuditEvent, CRASH_EVENT_TYPES, type CfAppSession, type CfCliContext, type CfClient, type CfCredentials, type CfEntityRef, type CfEventsDependencies, CfEventsRuntime, type CfSessionInput, type CrashQueryOptions, type CrashRecord, type CrashSummary, type CurlFn, type CurrentCfTarget, type EventQueryOptions, type FetchAuditEventsInput, type ParsedAppName, type ParsedExplicit, type ParsedSelector, type ProcessInstanceStat, type RegionKey, type ResolvedSelector, SSH_EVENT_TYPES, type SshEnabled, type SshSession, type SshStatus, type SshStatusInput, type WatchOptions, buildAuditEventsPath, buildSshStatus, cfAppGuid, cfCurl, createDefaultDependencies, describeEventType, durationToCreatedAfter, fetchApp, fetchAuditEvents, fetchSshEnabled, fetchWebProcessStats, filterByTypes, formatBytes, formatCpu, formatCrashReport, formatEventLine, formatEventsReport, formatRelativeTime, formatSshStatusReport, formatStatusReport, formatUptime, getApiEndpointForRegion, getRegionKeyForApi, inferSshSessions, isCrashEvent, isSshEvent, parseCfTargetOutput, parseDuration, parseSelector, parseTypeFilter, prepareCfCliSession, readCurrentCfTarget, resolveSelector, runCfCommand, sortEventsNewestFirst, summarizeCrashes, toCrashRecord, withCfSession };
297
+ export { APP_EVENT_TYPES, type AppCrashSummary, type AppEventType, type AppHealth, type AppSummary, type AuditEvent, type AuditEventScope, CRASH_EVENT_TYPES, type CfCliContext, type CfClient, type CfCredentials, type CfEntityRef, type CfEventsDependencies, CfEventsRuntime, type CfSessionInput, type CfTargetSession, type CrashQueryOptions, type CrashRecord, type CrashReportSummary, type CrashSummary, type CurlFn, type CurrentCfTarget, type EventQueryOptions, type FetchAuditEventsInput, type ParsedAppName, type ParsedAppPath, type ParsedSelector, type ParsedSpacePath, type ProcessInstanceStat, type RegionKey, type ResolvedAppSelector, type ResolvedSelector, type ResolvedSpaceSelector, SSH_EVENT_TYPES, type SpaceCrashSummary, type SshEnabled, type SshSession, type SshStatus, type SshStatusInput, type WatchOptions, buildAuditEventsPath, buildSshStatus, cfAppGuid, cfCurl, createDefaultDependencies, describeEventType, durationToCreatedAfter, fetchApp, fetchAuditEvents, fetchSshEnabled, fetchWebProcessStats, filterByTypes, formatBytes, formatCpu, formatCrashReport, formatEventLine, formatEventsReport, formatRelativeTime, formatSpaceEventLine, formatSpaceEventsReport, formatSshStatusReport, formatStatusReport, formatUptime, getApiEndpointForRegion, getRegionKeyForApi, inferSshSessions, isCrashEvent, isSshEvent, mapAuditEvent, parseCfTargetOutput, parseDuration, parseSelector, parseTypeFilter, prepareCfCliSession, readCurrentCfTarget, resolveOrganizationGuid, resolveSelector, resolveSpaceGuid, runCfCommand, sortEventsNewestFirst, summarizeCrashes, summarizeSpaceCrashes, toCrashRecord, withCfSession };
package/dist/index.js CHANGED
@@ -18,6 +18,12 @@ function asRecord(value) {
18
18
  function asArray(value) {
19
19
  return Array.isArray(value) ? value : [];
20
20
  }
21
+ function requireArray(value, message) {
22
+ if (!Array.isArray(value)) {
23
+ throw new Error(message);
24
+ }
25
+ return value;
26
+ }
21
27
  function asString(value) {
22
28
  return typeof value === "string" ? value : "";
23
29
  }
@@ -67,7 +73,7 @@ function mapProcessStat(value) {
67
73
  }
68
74
  function buildAuditEventsPath(input, perPage) {
69
75
  const params = new URLSearchParams();
70
- params.set("target_guids", input.appGuid);
76
+ applyAuditEventScope(params, input.scope);
71
77
  params.set("order_by", "-created_at");
72
78
  params.set("per_page", perPage.toString());
73
79
  if (input.types !== void 0 && input.types.length > 0) {
@@ -78,6 +84,60 @@ function buildAuditEventsPath(input, perPage) {
78
84
  }
79
85
  return `/v3/audit_events?${params.toString()}`;
80
86
  }
87
+ function applyAuditEventScope(params, scope) {
88
+ if (scope.kind === "app") {
89
+ params.set("target_guids", scope.appGuid);
90
+ return;
91
+ }
92
+ params.set("space_guids", scope.spaceGuid);
93
+ }
94
+ async function resolveOrganizationGuid(orgName, curl) {
95
+ const params = new URLSearchParams();
96
+ params.set("names", orgName);
97
+ const body = asRecord(parseJson(await curl(`/v3/organizations?${params.toString()}`)));
98
+ const first = asRecord(asArray(body["resources"])[0]);
99
+ const guid = asString(first["guid"]);
100
+ if (guid.length === 0) {
101
+ throw new Error(`Could not resolve the GUID for organization "${orgName}".`);
102
+ }
103
+ return guid;
104
+ }
105
+ async function resolveSpaceGuid(spaceName, orgGuid, curl) {
106
+ const params = new URLSearchParams();
107
+ params.set("names", spaceName);
108
+ params.set("organization_guids", orgGuid);
109
+ const body = asRecord(parseJson(await curl(`/v3/spaces?${params.toString()}`)));
110
+ const first = asRecord(asArray(body["resources"])[0]);
111
+ const guid = asString(first["guid"]);
112
+ if (guid.length === 0) {
113
+ throw new Error(`Could not resolve the GUID for space "${spaceName}".`);
114
+ }
115
+ return guid;
116
+ }
117
+ function formatCfApiError(error) {
118
+ const record = asRecord(error);
119
+ const title = asString(record["title"]);
120
+ const detail = asString(record["detail"]);
121
+ if (title.length > 0 && detail.length > 0) {
122
+ return `${title}: ${detail}`;
123
+ }
124
+ if (title.length > 0) {
125
+ return title;
126
+ }
127
+ if (detail.length > 0) {
128
+ return detail;
129
+ }
130
+ const code = asNumber(record["code"]);
131
+ return code === void 0 ? "unknown CF API error" : `code ${code.toString()}`;
132
+ }
133
+ function throwIfCfApiErrors(body) {
134
+ const errors = asRecord(body)["errors"];
135
+ if (errors === void 0) {
136
+ return;
137
+ }
138
+ const details = requireArray(errors, "The CF API returned an invalid errors response.").map(formatCfApiError).filter((detail) => detail.length > 0);
139
+ throw new Error(`CF API error ${details.length > 0 ? details.join("; ") : "unknown CF API error"}`);
140
+ }
81
141
  function nextPagePath(body) {
82
142
  const next = asRecord(asRecord(asRecord(body)["pagination"])["next"]);
83
143
  const href = next["href"];
@@ -98,7 +158,11 @@ async function fetchAuditEvents(input, curl) {
98
158
  let path = buildAuditEventsPath(input, perPage);
99
159
  while (path !== void 0 && events.length < limit) {
100
160
  const body = parseJson(await curl(path));
101
- for (const resource of asArray(asRecord(body)["resources"])) {
161
+ throwIfCfApiErrors(body);
162
+ for (const resource of requireArray(
163
+ asRecord(body)["resources"],
164
+ "The CF API returned an invalid audit events response: missing resources array."
165
+ )) {
102
166
  events.push(mapAuditEvent(resource));
103
167
  if (events.length >= limit) {
104
168
  break;
@@ -426,8 +490,11 @@ function parseDuration(raw) {
426
490
  }
427
491
  return amount * unitMs;
428
492
  }
493
+ function toCfAuditTimestamp(date) {
494
+ return date.toISOString().replace(/\.\d{3}Z$/, "Z");
495
+ }
429
496
  function durationToCreatedAfter(raw, now) {
430
- return new Date(now.getTime() - parseDuration(raw)).toISOString();
497
+ return toCfAuditTimestamp(new Date(now.getTime() - parseDuration(raw)));
431
498
  }
432
499
  function parseTypeFilter(raw) {
433
500
  if (raw === void 0) {
@@ -475,6 +542,39 @@ function summarizeCrashes(appName, events) {
475
542
  crashes
476
543
  };
477
544
  }
545
+ function crashAppName(event) {
546
+ if (event.target.name.length > 0) {
547
+ return event.target.name;
548
+ }
549
+ if (event.target.guid.length > 0) {
550
+ return event.target.guid;
551
+ }
552
+ return "(unknown app)";
553
+ }
554
+ function summarizeSpaceCrashes(selector, events) {
555
+ const byApp = /* @__PURE__ */ new Map();
556
+ for (const event of events.filter(isCrashEvent)) {
557
+ const appName = crashAppName(event);
558
+ byApp.set(appName, [...byApp.get(appName) ?? [], event]);
559
+ }
560
+ const apps = [...byApp.entries()].map(([appName, appEvents]) => summarizeCrashes(appName, appEvents)).map(({ appName, crashCount, lastCrashAt, lastCrashReason, crashes }) => ({
561
+ appName,
562
+ crashCount,
563
+ lastCrashAt,
564
+ lastCrashReason,
565
+ crashes
566
+ })).sort((left, right) => {
567
+ const byTime = (right.lastCrashAt ?? "").localeCompare(left.lastCrashAt ?? "");
568
+ return byTime === 0 ? left.appName.localeCompare(right.appName) : byTime;
569
+ });
570
+ return {
571
+ scope: "space",
572
+ selector,
573
+ crashCount: apps.reduce((sum, app) => sum + app.crashCount, 0),
574
+ lastCrashAt: apps[0]?.lastCrashAt,
575
+ apps
576
+ };
577
+ }
478
578
 
479
579
  // src/format.ts
480
580
  var EVENT_LABELS = {
@@ -575,6 +675,34 @@ function formatEventsReport(appName, events, now) {
575
675
  ...events.map((event) => formatEventLine(event, now))
576
676
  ].join("\n");
577
677
  }
678
+ function eventTargetLabel(event) {
679
+ if (event.target.name.length > 0) {
680
+ return event.target.name;
681
+ }
682
+ if (event.target.type.length > 0) {
683
+ return event.target.type;
684
+ }
685
+ return event.target.guid.length > 0 ? event.target.guid : "(unknown target)";
686
+ }
687
+ function formatSpaceEventLine(event, now) {
688
+ const actor = event.actor.name.length > 0 ? event.actor.name : event.actor.type;
689
+ return [
690
+ ` ${event.createdAt.padEnd(26)}`,
691
+ formatRelativeTime(event.createdAt, now).padEnd(11),
692
+ describeEventType(event.type).padEnd(24),
693
+ eventTargetLabel(event).padEnd(18),
694
+ actor.length > 0 ? actor : "(unknown actor)"
695
+ ].join(" ");
696
+ }
697
+ function formatSpaceEventsReport(selector, events, now) {
698
+ if (events.length === 0) {
699
+ return `No audit events found for space ${selector}.`;
700
+ }
701
+ return [
702
+ `Audit events for space ${selector} (${events.length.toString()}):`,
703
+ ...events.map((event) => formatSpaceEventLine(event, now))
704
+ ].join("\n");
705
+ }
578
706
  function formatSshStatusReport(status, now) {
579
707
  const sessionLines = status.sessions.map((session) => {
580
708
  const tag = session.likelyActive ? "[active]" : "[past] ";
@@ -599,7 +727,7 @@ function formatSshStatusReport(status, now) {
599
727
  " are inferred from recent ssh-authorized audit events."
600
728
  ].join("\n");
601
729
  }
602
- function formatCrashReport(summary, now) {
730
+ function formatAppCrashReport(summary, now) {
603
731
  if (summary.crashCount === 0) {
604
732
  return `No crashes found for ${summary.appName}.`;
605
733
  }
@@ -620,6 +748,35 @@ function formatCrashReport(summary, now) {
620
748
  ...crashLines
621
749
  ].join("\n");
622
750
  }
751
+ function formatSpaceCrashReport(summary, now) {
752
+ if (summary.crashCount === 0) {
753
+ return `No crashes found for space ${summary.selector}.`;
754
+ }
755
+ const appLines = summary.apps.flatMap((app) => [
756
+ ` ${app.appName} (${app.crashCount.toString()})`,
757
+ ...app.crashes.map((crash) => {
758
+ const index = crash.index === void 0 ? "-" : `#${crash.index.toString()}`;
759
+ const reason = crash.reason ?? "unknown";
760
+ const exit = crash.exitStatus === void 0 ? "" : ` exit ${crash.exitStatus.toString()}`;
761
+ return ` ${crash.at} instance ${index} ${reason}${exit}`;
762
+ })
763
+ ]);
764
+ return [
765
+ `Crash report for space ${summary.selector}`,
766
+ "",
767
+ ` Crashes: ${summary.crashCount.toString()}`,
768
+ ` Apps affected: ${summary.apps.length.toString()}`,
769
+ ...summary.lastCrashAt === void 0 ? [] : [` Last crash: ${summary.lastCrashAt} (${formatRelativeTime(summary.lastCrashAt, now)})`],
770
+ "",
771
+ ...appLines
772
+ ].join("\n");
773
+ }
774
+ function formatCrashReport(summary, now) {
775
+ if ("scope" in summary) {
776
+ return formatSpaceCrashReport(summary, now);
777
+ }
778
+ return formatAppCrashReport(summary, now);
779
+ }
623
780
  function formatInstanceLine(instance) {
624
781
  return [
625
782
  ` #${instance.index.toString()}`.padEnd(6),
@@ -648,7 +805,12 @@ function formatStatusReport(health, now) {
648
805
  import { setTimeout as delayPromise } from "timers/promises";
649
806
 
650
807
  // src/selector.ts
651
- var SELECTOR_USAGE = 'use "region/org/space/app" or a bare app name';
808
+ var SELECTOR_USAGE = 'use "region/org/space", "region/org/space/app", or a bare app name';
809
+ function assertNonEmpty(raw, parts, shape) {
810
+ if (parts.some((part) => part.length === 0)) {
811
+ throw new Error(`Invalid selector "${raw}": every ${shape} segment must be non-empty.`);
812
+ }
813
+ }
652
814
  function parseSelector(raw) {
653
815
  const trimmed = raw.trim();
654
816
  if (trimmed.length === 0) {
@@ -662,12 +824,21 @@ function parseSelector(raw) {
662
824
  }
663
825
  return { kind: "appName", appName };
664
826
  }
827
+ if (parts.length === 3) {
828
+ assertNonEmpty(raw, parts, "region/org/space");
829
+ const [regionKey, orgName, spaceName] = parts;
830
+ return { kind: "spacePath", regionKey: regionKey ?? "", orgName: orgName ?? "", spaceName: spaceName ?? "" };
831
+ }
665
832
  if (parts.length === 4) {
833
+ assertNonEmpty(raw, parts, "region/org/space/app");
666
834
  const [regionKey, orgName, spaceName, appName] = parts;
667
- 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) {
668
- throw new Error(`Invalid selector "${raw}": every region/org/space/app segment must be non-empty.`);
669
- }
670
- return { kind: "explicit", regionKey, orgName, spaceName, appName };
835
+ return {
836
+ kind: "appPath",
837
+ regionKey: regionKey ?? "",
838
+ orgName: orgName ?? "",
839
+ spaceName: spaceName ?? "",
840
+ appName: appName ?? ""
841
+ };
671
842
  }
672
843
  throw new Error(`Invalid selector "${raw}": ${SELECTOR_USAGE}.`);
673
844
  }
@@ -681,6 +852,7 @@ async function resolveSelector(raw) {
681
852
  );
682
853
  }
683
854
  return {
855
+ kind: "app",
684
856
  raw,
685
857
  regionKey: current.regionKey ?? "",
686
858
  apiEndpoint: current.apiEndpoint,
@@ -695,7 +867,18 @@ async function resolveSelector(raw) {
695
867
  `Unknown region "${parsed.regionKey}". Use a bare app name (requires current CF target) or a known region key.`
696
868
  );
697
869
  }
870
+ if (parsed.kind === "spacePath") {
871
+ return {
872
+ kind: "space",
873
+ raw,
874
+ regionKey: parsed.regionKey,
875
+ apiEndpoint,
876
+ orgName: parsed.orgName,
877
+ spaceName: parsed.spaceName
878
+ };
879
+ }
698
880
  return {
881
+ kind: "app",
699
882
  raw,
700
883
  regionKey: parsed.regionKey,
701
884
  apiEndpoint,
@@ -746,10 +929,12 @@ function createCfClient(curl) {
746
929
  fetchAuditEvents: (input) => fetchAuditEvents(input, curl),
747
930
  fetchApp: (appGuid) => fetchApp(appGuid, curl),
748
931
  fetchSshEnabled: (appGuid) => fetchSshEnabled(appGuid, curl),
749
- fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl)
932
+ fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl),
933
+ resolveOrganizationGuid: (orgName) => resolveOrganizationGuid(orgName, curl),
934
+ resolveSpaceGuid: (spaceName, orgGuid) => resolveSpaceGuid(spaceName, orgGuid, curl)
750
935
  };
751
936
  }
752
- async function defaultWithCfApp(selector, credentials, work) {
937
+ async function defaultWithCfTarget(selector, credentials, work) {
753
938
  return await withCfSession(async (ctx) => {
754
939
  await prepareCfCliSession(
755
940
  {
@@ -760,15 +945,14 @@ async function defaultWithCfApp(selector, credentials, work) {
760
945
  },
761
946
  ctx
762
947
  );
763
- const appGuid = await cfAppGuid(selector.appName, ctx);
764
948
  const client = createCfClient((path) => cfCurl(path, ctx));
765
- return await work({ appGuid, client });
949
+ return await work({ selector, client, resolveAppGuid: (appName) => cfAppGuid(appName, ctx) });
766
950
  });
767
951
  }
768
952
  function createDefaultDependencies() {
769
953
  return {
770
954
  resolveSelector,
771
- withCfApp: defaultWithCfApp,
955
+ withCfTarget: defaultWithCfTarget,
772
956
  now: () => /* @__PURE__ */ new Date()
773
957
  };
774
958
  }
@@ -778,6 +962,24 @@ async function delay(ms, signal) {
778
962
  } catch {
779
963
  }
780
964
  }
965
+ function requireAppSelector(selector, command) {
966
+ if (selector.kind === "app") {
967
+ return selector;
968
+ }
969
+ throw new Error(
970
+ `The ${command} command requires an app selector. Use region/org/space/app or a bare app name.`
971
+ );
972
+ }
973
+ async function resolveAuditScope(session) {
974
+ if (session.selector.kind === "app") {
975
+ return { kind: "app", appGuid: await session.resolveAppGuid(session.selector.appName) };
976
+ }
977
+ const orgGuid = await session.client.resolveOrganizationGuid(session.selector.orgName);
978
+ return {
979
+ kind: "space",
980
+ spaceGuid: await session.client.resolveSpaceGuid(session.selector.spaceName, orgGuid)
981
+ };
982
+ }
781
983
  var CfEventsRuntime = class {
782
984
  deps;
783
985
  constructor(deps = createDefaultDependencies()) {
@@ -785,9 +987,9 @@ var CfEventsRuntime = class {
785
987
  }
786
988
  async fetchEvents(raw, credentials, options) {
787
989
  const selector = await this.deps.resolveSelector(raw);
788
- return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
789
- return await client.fetchAuditEvents({
790
- appGuid,
990
+ return await this.deps.withCfTarget(selector, credentials, async (session) => {
991
+ return await session.client.fetchAuditEvents({
992
+ scope: await resolveAuditScope(session),
791
993
  types: options.types.length > 0 ? options.types : void 0,
792
994
  createdAfter: this.resolveCreatedAfter(options.since),
793
995
  limit: options.limit
@@ -795,12 +997,13 @@ var CfEventsRuntime = class {
795
997
  });
796
998
  }
797
999
  async getSshStatus(raw, credentials, since) {
798
- const selector = await this.deps.resolveSelector(raw);
799
- return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
1000
+ const selector = requireAppSelector(await this.deps.resolveSelector(raw), "ssh-status");
1001
+ return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
1002
+ const appGuid = await resolveAppGuid(selector.appName);
800
1003
  const [sshEnabled, events] = await Promise.all([
801
1004
  client.fetchSshEnabled(appGuid),
802
1005
  client.fetchAuditEvents({
803
- appGuid,
1006
+ scope: { kind: "app", appGuid },
804
1007
  types: [...SSH_EVENT_TYPES],
805
1008
  createdAfter: durationToCreatedAfter(since, this.deps.now()),
806
1009
  limit: SSH_STATUS_EVENT_LIMIT
@@ -817,24 +1020,28 @@ var CfEventsRuntime = class {
817
1020
  }
818
1021
  async getCrashes(raw, credentials, options) {
819
1022
  const selector = await this.deps.resolveSelector(raw);
820
- return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
821
- const events = await client.fetchAuditEvents({
822
- appGuid,
1023
+ return await this.deps.withCfTarget(selector, credentials, async (session) => {
1024
+ const events = await session.client.fetchAuditEvents({
1025
+ scope: await resolveAuditScope(session),
823
1026
  types: [...CRASH_EVENT_TYPES],
824
1027
  createdAfter: this.resolveCreatedAfter(options.since),
825
1028
  limit: options.limit
826
1029
  });
827
- return summarizeCrashes(selector.appName, events);
1030
+ if (selector.kind === "app") {
1031
+ return summarizeCrashes(selector.appName, events);
1032
+ }
1033
+ return summarizeSpaceCrashes(`${selector.regionKey}/${selector.orgName}/${selector.spaceName}`, events);
828
1034
  });
829
1035
  }
830
1036
  async getStatus(raw, credentials) {
831
- const selector = await this.deps.resolveSelector(raw);
832
- return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
1037
+ const selector = requireAppSelector(await this.deps.resolveSelector(raw), "status");
1038
+ return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
1039
+ const appGuid = await resolveAppGuid(selector.appName);
833
1040
  const [app, instances, sshEnabled, recent] = await Promise.all([
834
1041
  client.fetchApp(appGuid),
835
1042
  client.fetchWebProcessStats(appGuid),
836
1043
  client.fetchSshEnabled(appGuid),
837
- client.fetchAuditEvents({ appGuid, types: void 0, createdAfter: void 0, limit: 1 })
1044
+ client.fetchAuditEvents({ scope: { kind: "app", appGuid }, types: void 0, createdAfter: void 0, limit: 1 })
838
1045
  ]);
839
1046
  return {
840
1047
  appName: selector.appName,
@@ -848,12 +1055,13 @@ var CfEventsRuntime = class {
848
1055
  }
849
1056
  async watchEvents(raw, credentials, options, onEvent, signal) {
850
1057
  const selector = await this.deps.resolveSelector(raw);
851
- await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
1058
+ await this.deps.withCfTarget(selector, credentials, async (session) => {
1059
+ const scope = await resolveAuditScope(session);
852
1060
  const seen = /* @__PURE__ */ new Set();
853
1061
  let cursor = durationToCreatedAfter(options.lookback, this.deps.now());
854
1062
  while (!signal.aborted) {
855
- const events = await client.fetchAuditEvents({
856
- appGuid,
1063
+ const events = await session.client.fetchAuditEvents({
1064
+ scope,
857
1065
  types: options.types.length > 0 ? options.types : void 0,
858
1066
  createdAfter: cursor,
859
1067
  limit: WATCH_FETCH_LIMIT
@@ -903,6 +1111,8 @@ export {
903
1111
  formatEventLine,
904
1112
  formatEventsReport,
905
1113
  formatRelativeTime,
1114
+ formatSpaceEventLine,
1115
+ formatSpaceEventsReport,
906
1116
  formatSshStatusReport,
907
1117
  formatStatusReport,
908
1118
  formatUptime,
@@ -911,16 +1121,20 @@ export {
911
1121
  inferSshSessions,
912
1122
  isCrashEvent,
913
1123
  isSshEvent,
1124
+ mapAuditEvent,
914
1125
  parseCfTargetOutput,
915
1126
  parseDuration,
916
1127
  parseSelector,
917
1128
  parseTypeFilter,
918
1129
  prepareCfCliSession,
919
1130
  readCurrentCfTarget,
1131
+ resolveOrganizationGuid,
920
1132
  resolveSelector,
1133
+ resolveSpaceGuid,
921
1134
  runCfCommand,
922
1135
  sortEventsNewestFirst,
923
1136
  summarizeCrashes,
1137
+ summarizeSpaceCrashes,
924
1138
  toCrashRecord,
925
1139
  withCfSession
926
1140
  };