@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/cli.js CHANGED
@@ -39,8 +39,11 @@ function parseDuration(raw) {
39
39
  }
40
40
  return amount * unitMs;
41
41
  }
42
+ function toCfAuditTimestamp(date) {
43
+ return date.toISOString().replace(/\.\d{3}Z$/, "Z");
44
+ }
42
45
  function durationToCreatedAfter(raw, now) {
43
- return new Date(now.getTime() - parseDuration(raw)).toISOString();
46
+ return toCfAuditTimestamp(new Date(now.getTime() - parseDuration(raw)));
44
47
  }
45
48
  function parseTypeFilter(raw) {
46
49
  if (raw === void 0) {
@@ -88,6 +91,39 @@ function summarizeCrashes(appName, events) {
88
91
  crashes
89
92
  };
90
93
  }
94
+ function crashAppName(event) {
95
+ if (event.target.name.length > 0) {
96
+ return event.target.name;
97
+ }
98
+ if (event.target.guid.length > 0) {
99
+ return event.target.guid;
100
+ }
101
+ return "(unknown app)";
102
+ }
103
+ function summarizeSpaceCrashes(selector, events) {
104
+ const byApp = /* @__PURE__ */ new Map();
105
+ for (const event of events.filter(isCrashEvent)) {
106
+ const appName = crashAppName(event);
107
+ byApp.set(appName, [...byApp.get(appName) ?? [], event]);
108
+ }
109
+ const apps = [...byApp.entries()].map(([appName, appEvents]) => summarizeCrashes(appName, appEvents)).map(({ appName, crashCount, lastCrashAt, lastCrashReason, crashes }) => ({
110
+ appName,
111
+ crashCount,
112
+ lastCrashAt,
113
+ lastCrashReason,
114
+ crashes
115
+ })).sort((left, right) => {
116
+ const byTime = (right.lastCrashAt ?? "").localeCompare(left.lastCrashAt ?? "");
117
+ return byTime === 0 ? left.appName.localeCompare(right.appName) : byTime;
118
+ });
119
+ return {
120
+ scope: "space",
121
+ selector,
122
+ crashCount: apps.reduce((sum, app) => sum + app.crashCount, 0),
123
+ lastCrashAt: apps[0]?.lastCrashAt,
124
+ apps
125
+ };
126
+ }
91
127
 
92
128
  // src/format.ts
93
129
  var EVENT_LABELS = {
@@ -188,6 +224,34 @@ function formatEventsReport(appName, events, now) {
188
224
  ...events.map((event) => formatEventLine(event, now))
189
225
  ].join("\n");
190
226
  }
227
+ function eventTargetLabel(event) {
228
+ if (event.target.name.length > 0) {
229
+ return event.target.name;
230
+ }
231
+ if (event.target.type.length > 0) {
232
+ return event.target.type;
233
+ }
234
+ return event.target.guid.length > 0 ? event.target.guid : "(unknown target)";
235
+ }
236
+ function formatSpaceEventLine(event, now) {
237
+ const actor = event.actor.name.length > 0 ? event.actor.name : event.actor.type;
238
+ return [
239
+ ` ${event.createdAt.padEnd(26)}`,
240
+ formatRelativeTime(event.createdAt, now).padEnd(11),
241
+ describeEventType(event.type).padEnd(24),
242
+ eventTargetLabel(event).padEnd(18),
243
+ actor.length > 0 ? actor : "(unknown actor)"
244
+ ].join(" ");
245
+ }
246
+ function formatSpaceEventsReport(selector, events, now) {
247
+ if (events.length === 0) {
248
+ return `No audit events found for space ${selector}.`;
249
+ }
250
+ return [
251
+ `Audit events for space ${selector} (${events.length.toString()}):`,
252
+ ...events.map((event) => formatSpaceEventLine(event, now))
253
+ ].join("\n");
254
+ }
191
255
  function formatSshStatusReport(status, now) {
192
256
  const sessionLines = status.sessions.map((session) => {
193
257
  const tag = session.likelyActive ? "[active]" : "[past] ";
@@ -212,7 +276,7 @@ function formatSshStatusReport(status, now) {
212
276
  " are inferred from recent ssh-authorized audit events."
213
277
  ].join("\n");
214
278
  }
215
- function formatCrashReport(summary, now) {
279
+ function formatAppCrashReport(summary, now) {
216
280
  if (summary.crashCount === 0) {
217
281
  return `No crashes found for ${summary.appName}.`;
218
282
  }
@@ -233,6 +297,35 @@ function formatCrashReport(summary, now) {
233
297
  ...crashLines
234
298
  ].join("\n");
235
299
  }
300
+ function formatSpaceCrashReport(summary, now) {
301
+ if (summary.crashCount === 0) {
302
+ return `No crashes found for space ${summary.selector}.`;
303
+ }
304
+ const appLines = summary.apps.flatMap((app) => [
305
+ ` ${app.appName} (${app.crashCount.toString()})`,
306
+ ...app.crashes.map((crash) => {
307
+ const index = crash.index === void 0 ? "-" : `#${crash.index.toString()}`;
308
+ const reason = crash.reason ?? "unknown";
309
+ const exit = crash.exitStatus === void 0 ? "" : ` exit ${crash.exitStatus.toString()}`;
310
+ return ` ${crash.at} instance ${index} ${reason}${exit}`;
311
+ })
312
+ ]);
313
+ return [
314
+ `Crash report for space ${summary.selector}`,
315
+ "",
316
+ ` Crashes: ${summary.crashCount.toString()}`,
317
+ ` Apps affected: ${summary.apps.length.toString()}`,
318
+ ...summary.lastCrashAt === void 0 ? [] : [` Last crash: ${summary.lastCrashAt} (${formatRelativeTime(summary.lastCrashAt, now)})`],
319
+ "",
320
+ ...appLines
321
+ ].join("\n");
322
+ }
323
+ function formatCrashReport(summary, now) {
324
+ if ("scope" in summary) {
325
+ return formatSpaceCrashReport(summary, now);
326
+ }
327
+ return formatAppCrashReport(summary, now);
328
+ }
236
329
  function formatInstanceLine(instance) {
237
330
  return [
238
331
  ` #${instance.index.toString()}`.padEnd(6),
@@ -278,6 +371,12 @@ function asRecord(value) {
278
371
  function asArray(value) {
279
372
  return Array.isArray(value) ? value : [];
280
373
  }
374
+ function requireArray(value, message) {
375
+ if (!Array.isArray(value)) {
376
+ throw new Error(message);
377
+ }
378
+ return value;
379
+ }
281
380
  function asString(value) {
282
381
  return typeof value === "string" ? value : "";
283
382
  }
@@ -327,7 +426,7 @@ function mapProcessStat(value) {
327
426
  }
328
427
  function buildAuditEventsPath(input, perPage) {
329
428
  const params = new URLSearchParams();
330
- params.set("target_guids", input.appGuid);
429
+ applyAuditEventScope(params, input.scope);
331
430
  params.set("order_by", "-created_at");
332
431
  params.set("per_page", perPage.toString());
333
432
  if (input.types !== void 0 && input.types.length > 0) {
@@ -338,6 +437,60 @@ function buildAuditEventsPath(input, perPage) {
338
437
  }
339
438
  return `/v3/audit_events?${params.toString()}`;
340
439
  }
440
+ function applyAuditEventScope(params, scope) {
441
+ if (scope.kind === "app") {
442
+ params.set("target_guids", scope.appGuid);
443
+ return;
444
+ }
445
+ params.set("space_guids", scope.spaceGuid);
446
+ }
447
+ async function resolveOrganizationGuid(orgName, curl) {
448
+ const params = new URLSearchParams();
449
+ params.set("names", orgName);
450
+ const body = asRecord(parseJson(await curl(`/v3/organizations?${params.toString()}`)));
451
+ const first = asRecord(asArray(body["resources"])[0]);
452
+ const guid = asString(first["guid"]);
453
+ if (guid.length === 0) {
454
+ throw new Error(`Could not resolve the GUID for organization "${orgName}".`);
455
+ }
456
+ return guid;
457
+ }
458
+ async function resolveSpaceGuid(spaceName, orgGuid, curl) {
459
+ const params = new URLSearchParams();
460
+ params.set("names", spaceName);
461
+ params.set("organization_guids", orgGuid);
462
+ const body = asRecord(parseJson(await curl(`/v3/spaces?${params.toString()}`)));
463
+ const first = asRecord(asArray(body["resources"])[0]);
464
+ const guid = asString(first["guid"]);
465
+ if (guid.length === 0) {
466
+ throw new Error(`Could not resolve the GUID for space "${spaceName}".`);
467
+ }
468
+ return guid;
469
+ }
470
+ function formatCfApiError(error) {
471
+ const record = asRecord(error);
472
+ const title = asString(record["title"]);
473
+ const detail = asString(record["detail"]);
474
+ if (title.length > 0 && detail.length > 0) {
475
+ return `${title}: ${detail}`;
476
+ }
477
+ if (title.length > 0) {
478
+ return title;
479
+ }
480
+ if (detail.length > 0) {
481
+ return detail;
482
+ }
483
+ const code = asNumber(record["code"]);
484
+ return code === void 0 ? "unknown CF API error" : `code ${code.toString()}`;
485
+ }
486
+ function throwIfCfApiErrors(body) {
487
+ const errors = asRecord(body)["errors"];
488
+ if (errors === void 0) {
489
+ return;
490
+ }
491
+ const details = requireArray(errors, "The CF API returned an invalid errors response.").map(formatCfApiError).filter((detail) => detail.length > 0);
492
+ throw new Error(`CF API error ${details.length > 0 ? details.join("; ") : "unknown CF API error"}`);
493
+ }
341
494
  function nextPagePath(body) {
342
495
  const next = asRecord(asRecord(asRecord(body)["pagination"])["next"]);
343
496
  const href = next["href"];
@@ -358,7 +511,11 @@ async function fetchAuditEvents(input, curl) {
358
511
  let path = buildAuditEventsPath(input, perPage);
359
512
  while (path !== void 0 && events.length < limit) {
360
513
  const body = parseJson(await curl(path));
361
- for (const resource of asArray(asRecord(body)["resources"])) {
514
+ throwIfCfApiErrors(body);
515
+ for (const resource of requireArray(
516
+ asRecord(body)["resources"],
517
+ "The CF API returned an invalid audit events response: missing resources array."
518
+ )) {
362
519
  events.push(mapAuditEvent(resource));
363
520
  if (events.length >= limit) {
364
521
  break;
@@ -626,7 +783,12 @@ function extractSafeCliDetail(error) {
626
783
  }
627
784
 
628
785
  // src/selector.ts
629
- var SELECTOR_USAGE = 'use "region/org/space/app" or a bare app name';
786
+ var SELECTOR_USAGE = 'use "region/org/space", "region/org/space/app", or a bare app name';
787
+ function assertNonEmpty(raw, parts, shape) {
788
+ if (parts.some((part) => part.length === 0)) {
789
+ throw new Error(`Invalid selector "${raw}": every ${shape} segment must be non-empty.`);
790
+ }
791
+ }
630
792
  function parseSelector(raw) {
631
793
  const trimmed = raw.trim();
632
794
  if (trimmed.length === 0) {
@@ -640,12 +802,21 @@ function parseSelector(raw) {
640
802
  }
641
803
  return { kind: "appName", appName };
642
804
  }
805
+ if (parts.length === 3) {
806
+ assertNonEmpty(raw, parts, "region/org/space");
807
+ const [regionKey, orgName, spaceName] = parts;
808
+ return { kind: "spacePath", regionKey: regionKey ?? "", orgName: orgName ?? "", spaceName: spaceName ?? "" };
809
+ }
643
810
  if (parts.length === 4) {
811
+ assertNonEmpty(raw, parts, "region/org/space/app");
644
812
  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 };
813
+ return {
814
+ kind: "appPath",
815
+ regionKey: regionKey ?? "",
816
+ orgName: orgName ?? "",
817
+ spaceName: spaceName ?? "",
818
+ appName: appName ?? ""
819
+ };
649
820
  }
650
821
  throw new Error(`Invalid selector "${raw}": ${SELECTOR_USAGE}.`);
651
822
  }
@@ -659,6 +830,7 @@ async function resolveSelector(raw) {
659
830
  );
660
831
  }
661
832
  return {
833
+ kind: "app",
662
834
  raw,
663
835
  regionKey: current.regionKey ?? "",
664
836
  apiEndpoint: current.apiEndpoint,
@@ -673,7 +845,18 @@ async function resolveSelector(raw) {
673
845
  `Unknown region "${parsed.regionKey}". Use a bare app name (requires current CF target) or a known region key.`
674
846
  );
675
847
  }
848
+ if (parsed.kind === "spacePath") {
849
+ return {
850
+ kind: "space",
851
+ raw,
852
+ regionKey: parsed.regionKey,
853
+ apiEndpoint,
854
+ orgName: parsed.orgName,
855
+ spaceName: parsed.spaceName
856
+ };
857
+ }
676
858
  return {
859
+ kind: "app",
677
860
  raw,
678
861
  regionKey: parsed.regionKey,
679
862
  apiEndpoint,
@@ -724,10 +907,12 @@ function createCfClient(curl) {
724
907
  fetchAuditEvents: (input) => fetchAuditEvents(input, curl),
725
908
  fetchApp: (appGuid) => fetchApp(appGuid, curl),
726
909
  fetchSshEnabled: (appGuid) => fetchSshEnabled(appGuid, curl),
727
- fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl)
910
+ fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl),
911
+ resolveOrganizationGuid: (orgName) => resolveOrganizationGuid(orgName, curl),
912
+ resolveSpaceGuid: (spaceName, orgGuid) => resolveSpaceGuid(spaceName, orgGuid, curl)
728
913
  };
729
914
  }
730
- async function defaultWithCfApp(selector, credentials, work) {
915
+ async function defaultWithCfTarget(selector, credentials, work) {
731
916
  return await withCfSession(async (ctx) => {
732
917
  await prepareCfCliSession(
733
918
  {
@@ -738,15 +923,14 @@ async function defaultWithCfApp(selector, credentials, work) {
738
923
  },
739
924
  ctx
740
925
  );
741
- const appGuid = await cfAppGuid(selector.appName, ctx);
742
926
  const client = createCfClient((path) => cfCurl(path, ctx));
743
- return await work({ appGuid, client });
927
+ return await work({ selector, client, resolveAppGuid: (appName) => cfAppGuid(appName, ctx) });
744
928
  });
745
929
  }
746
930
  function createDefaultDependencies() {
747
931
  return {
748
932
  resolveSelector,
749
- withCfApp: defaultWithCfApp,
933
+ withCfTarget: defaultWithCfTarget,
750
934
  now: () => /* @__PURE__ */ new Date()
751
935
  };
752
936
  }
@@ -756,6 +940,24 @@ async function delay(ms, signal) {
756
940
  } catch {
757
941
  }
758
942
  }
943
+ function requireAppSelector(selector, command) {
944
+ if (selector.kind === "app") {
945
+ return selector;
946
+ }
947
+ throw new Error(
948
+ `The ${command} command requires an app selector. Use region/org/space/app or a bare app name.`
949
+ );
950
+ }
951
+ async function resolveAuditScope(session) {
952
+ if (session.selector.kind === "app") {
953
+ return { kind: "app", appGuid: await session.resolveAppGuid(session.selector.appName) };
954
+ }
955
+ const orgGuid = await session.client.resolveOrganizationGuid(session.selector.orgName);
956
+ return {
957
+ kind: "space",
958
+ spaceGuid: await session.client.resolveSpaceGuid(session.selector.spaceName, orgGuid)
959
+ };
960
+ }
759
961
  var CfEventsRuntime = class {
760
962
  deps;
761
963
  constructor(deps = createDefaultDependencies()) {
@@ -763,9 +965,9 @@ var CfEventsRuntime = class {
763
965
  }
764
966
  async fetchEvents(raw, credentials, options) {
765
967
  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,
968
+ return await this.deps.withCfTarget(selector, credentials, async (session) => {
969
+ return await session.client.fetchAuditEvents({
970
+ scope: await resolveAuditScope(session),
769
971
  types: options.types.length > 0 ? options.types : void 0,
770
972
  createdAfter: this.resolveCreatedAfter(options.since),
771
973
  limit: options.limit
@@ -773,12 +975,13 @@ var CfEventsRuntime = class {
773
975
  });
774
976
  }
775
977
  async getSshStatus(raw, credentials, since) {
776
- const selector = await this.deps.resolveSelector(raw);
777
- return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
978
+ const selector = requireAppSelector(await this.deps.resolveSelector(raw), "ssh-status");
979
+ return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
980
+ const appGuid = await resolveAppGuid(selector.appName);
778
981
  const [sshEnabled, events] = await Promise.all([
779
982
  client.fetchSshEnabled(appGuid),
780
983
  client.fetchAuditEvents({
781
- appGuid,
984
+ scope: { kind: "app", appGuid },
782
985
  types: [...SSH_EVENT_TYPES],
783
986
  createdAfter: durationToCreatedAfter(since, this.deps.now()),
784
987
  limit: SSH_STATUS_EVENT_LIMIT
@@ -795,24 +998,28 @@ var CfEventsRuntime = class {
795
998
  }
796
999
  async getCrashes(raw, credentials, options) {
797
1000
  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,
1001
+ return await this.deps.withCfTarget(selector, credentials, async (session) => {
1002
+ const events = await session.client.fetchAuditEvents({
1003
+ scope: await resolveAuditScope(session),
801
1004
  types: [...CRASH_EVENT_TYPES],
802
1005
  createdAfter: this.resolveCreatedAfter(options.since),
803
1006
  limit: options.limit
804
1007
  });
805
- return summarizeCrashes(selector.appName, events);
1008
+ if (selector.kind === "app") {
1009
+ return summarizeCrashes(selector.appName, events);
1010
+ }
1011
+ return summarizeSpaceCrashes(`${selector.regionKey}/${selector.orgName}/${selector.spaceName}`, events);
806
1012
  });
807
1013
  }
808
1014
  async getStatus(raw, credentials) {
809
- const selector = await this.deps.resolveSelector(raw);
810
- return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
1015
+ const selector = requireAppSelector(await this.deps.resolveSelector(raw), "status");
1016
+ return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
1017
+ const appGuid = await resolveAppGuid(selector.appName);
811
1018
  const [app, instances, sshEnabled, recent] = await Promise.all([
812
1019
  client.fetchApp(appGuid),
813
1020
  client.fetchWebProcessStats(appGuid),
814
1021
  client.fetchSshEnabled(appGuid),
815
- client.fetchAuditEvents({ appGuid, types: void 0, createdAfter: void 0, limit: 1 })
1022
+ client.fetchAuditEvents({ scope: { kind: "app", appGuid }, types: void 0, createdAfter: void 0, limit: 1 })
816
1023
  ]);
817
1024
  return {
818
1025
  appName: selector.appName,
@@ -826,12 +1033,13 @@ var CfEventsRuntime = class {
826
1033
  }
827
1034
  async watchEvents(raw, credentials, options, onEvent, signal) {
828
1035
  const selector = await this.deps.resolveSelector(raw);
829
- await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
1036
+ await this.deps.withCfTarget(selector, credentials, async (session) => {
1037
+ const scope = await resolveAuditScope(session);
830
1038
  const seen = /* @__PURE__ */ new Set();
831
1039
  let cursor = durationToCreatedAfter(options.lookback, this.deps.now());
832
1040
  while (!signal.aborted) {
833
- const events = await client.fetchAuditEvents({
834
- appGuid,
1041
+ const events = await session.client.fetchAuditEvents({
1042
+ scope,
835
1043
  types: options.types.length > 0 ? options.types : void 0,
836
1044
  createdAfter: cursor,
837
1045
  limit: WATCH_FETCH_LIMIT
@@ -893,6 +1101,13 @@ function parsePositiveInteger(value, optionName) {
893
1101
  function appLabel(selector) {
894
1102
  return (selector.split("/").at(-1) ?? selector).trim();
895
1103
  }
1104
+ function isSpaceSelector(selector) {
1105
+ return parseSelector(selector).kind === "spacePath";
1106
+ }
1107
+ function spaceLabel(selector) {
1108
+ const parsed = parseSelector(selector);
1109
+ return parsed.kind === "spacePath" ? `${parsed.regionKey}/${parsed.orgName}/${parsed.spaceName}` : selector;
1110
+ }
896
1111
  function resolveSelectorArgument(selector) {
897
1112
  return selector;
898
1113
  }
@@ -920,7 +1135,7 @@ async function runEvents(selector, flags) {
920
1135
  writeJson(events);
921
1136
  return;
922
1137
  }
923
- writeOut(formatEventsReport(appLabel(resolvedSelector), events, /* @__PURE__ */ new Date()));
1138
+ writeOut(isSpaceSelector(resolvedSelector) ? formatSpaceEventsReport(spaceLabel(resolvedSelector), events, /* @__PURE__ */ new Date()) : formatEventsReport(appLabel(resolvedSelector), events, /* @__PURE__ */ new Date()));
924
1139
  }
925
1140
  async function runSshStatus(selector, flags) {
926
1141
  const resolvedSelector = resolveSelectorArgument(selector);
@@ -959,12 +1174,12 @@ async function runStatus(selector, flags) {
959
1174
  }
960
1175
  writeOut(formatStatusReport(health, /* @__PURE__ */ new Date()));
961
1176
  }
962
- function emitWatchEvent(event, asJson) {
1177
+ function emitWatchEvent(event, asJson, spaceScope) {
963
1178
  if (asJson) {
964
1179
  writeJsonLine(event);
965
1180
  return;
966
1181
  }
967
- writeOut(formatEventLine(event, /* @__PURE__ */ new Date()));
1182
+ writeOut(spaceScope ? formatSpaceEventLine(event, /* @__PURE__ */ new Date()) : formatEventLine(event, /* @__PURE__ */ new Date()));
968
1183
  }
969
1184
  async function runWatch(selector, flags) {
970
1185
  const resolvedSelector = resolveSelectorArgument(selector);
@@ -978,6 +1193,7 @@ async function runWatch(selector, flags) {
978
1193
  controller.abort();
979
1194
  });
980
1195
  const asJson = flags.json === true;
1196
+ const spaceScope = isSpaceSelector(resolvedSelector);
981
1197
  try {
982
1198
  process2.stderr.write(`Watching ${resolvedSelector} for new audit events. Press Ctrl+C to stop.
983
1199
  `);
@@ -990,7 +1206,7 @@ async function runWatch(selector, flags) {
990
1206
  types: parseTypeFilter(flags.type)
991
1207
  },
992
1208
  (event) => {
993
- emitWatchEvent(event, asJson);
1209
+ emitWatchEvent(event, asJson, spaceScope);
994
1210
  },
995
1211
  controller.signal
996
1212
  );
@@ -1036,7 +1252,7 @@ function buildProgram() {
1036
1252
  const program = new Command();
1037
1253
  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
1254
  addCommonOptions(
1039
- program.command("events").description("List recent audit events for an app").argument("<selector>", "region/org/space/app or a bare app name")
1255
+ 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
1256
  ).option(
1041
1257
  "--limit <count>",
1042
1258
  "Maximum number of events to return",
@@ -1050,7 +1266,7 @@ function buildProgram() {
1050
1266
  await runSshStatus(selector, flags);
1051
1267
  });
1052
1268
  addCommonOptions(
1053
- program.command("crashes").description("Summarize recent crash events for an app").argument("<selector>", "region/org/space/app or a bare app name")
1269
+ 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
1270
  ).option(
1055
1271
  "--limit <count>",
1056
1272
  "Maximum number of crash events to inspect",
@@ -1064,7 +1280,7 @@ function buildProgram() {
1064
1280
  await runStatus(selector, flags);
1065
1281
  });
1066
1282
  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")
1283
+ 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
1284
  ).option(
1069
1285
  "--interval <ms>",
1070
1286
  "Polling interval in milliseconds",