@lantharos/pult 0.11.0 → 0.13.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/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as traceOrigins, L as findActionDef, M as valuesIn, S as kindOf, _ as normalizeFilter, a as collect, c as guards, d as Transport, h as hasPendingAttachments, i as validate, j as shownValue, k as eachBlock, r as toJsonSchema, x as humanize } from "./schema-DuTVNptF.mjs";
2
+ import { C as traceOrigins, L as findActionDef, M as valuesIn, S as kindOf, _ as normalizeFilter, a as collect, c as guards, d as Transport, h as hasPendingAttachments, i as validate, j as shownValue, k as eachBlock, r as toJsonSchema, x as humanize } from "./schema-DCS7N4sm.mjs";
3
3
  import { basename, dirname, join, relative, resolve } from "node:path";
4
4
  import { parseArgs, styleText } from "node:util";
5
5
  import { Cron } from "croner";
@@ -184,7 +184,7 @@ function actionChanges(prefix) {
184
184
  }
185
185
  //#endregion
186
186
  //#region package.json
187
- var version = "0.11.0";
187
+ var version = "0.13.0";
188
188
  //#endregion
189
189
  //#region ../protocol/src/checks.ts
190
190
  const COLUMNS = [
@@ -274,6 +274,7 @@ function checkManifest(manifest) {
274
274
  }
275
275
  for (const [name, page] of Object.entries(manifest.pages)) if (page.blocks) checkBlocks(manifest, page.source, `page ${name}`, page.blocks, problems);
276
276
  if (manifest.status) checkStatus(manifest, problems);
277
+ for (const [name, rule] of Object.entries(manifest.notify)) checkNotify(manifest, name, rule, problems);
277
278
  for (const [name, flag] of Object.entries(manifest.flags)) {
278
279
  if (flag.target && !manifest.resources[flag.target]) problems.push({
279
280
  source: flag.source,
@@ -390,6 +391,32 @@ function checkSchedule(name, action, problems) {
390
391
  message: `${name} runs on a schedule, so it can't need a second approver`
391
392
  });
392
393
  }
394
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
395
+ const MIN_ESCALATION_MS = 6e4;
396
+ function checkRecipients(manifest, recipients, problem) {
397
+ for (const recipient of recipients) {
398
+ if ("role" in recipient && !manifest.roles[recipient.role]) problem(`role "${recipient.role}", which doesn't exist`);
399
+ if ("person" in recipient && !EMAIL.test(recipient.person)) problem(`"${recipient.person}", which isn't an email address`);
400
+ }
401
+ }
402
+ function checkNotify(manifest, name, rule, problems) {
403
+ const problem = (message) => problems.push({
404
+ source: rule.source,
405
+ message: `${name} ${message}`
406
+ });
407
+ if (!rule.on.length) problem("doesn't say when to notify");
408
+ if (!rule.to.length) problem("doesn't say who to notify");
409
+ for (const inbox of rule.inboxes) if (!manifest.inboxes[inbox]) problem(`watches unknown inbox "${inbox}"`);
410
+ checkRecipients(manifest, rule.to, (message) => problem(`notifies ${message}`));
411
+ let previous = 0;
412
+ for (const step of rule.escalate) {
413
+ if (step.after < MIN_ESCALATION_MS) problem("escalates sooner than a minute. Use 1m or more.");
414
+ else if (step.after <= previous) problem("escalates out of order. Each step should come later than the one before.");
415
+ if (!step.to.length) problem("has an escalation step that doesn't say who to notify");
416
+ checkRecipients(manifest, step.to, (message) => problem(`escalates to ${message}`));
417
+ previous = step.after;
418
+ }
419
+ }
393
420
  const COMPONENT = /^[a-z0-9][a-z0-9-]*$/;
394
421
  const WEB_ADDRESS = /^https?:\/\/[^\s/$.?#].\S*$/;
395
422
  const CSS_LIMIT = 5e4;
@@ -406,7 +433,7 @@ function checkStatus(manifest, problems) {
406
433
  if (component.monitored && component.check) problem(`component ${name} can follow your app or a check, not both`);
407
434
  if (component.check && !WEB_ADDRESS.test(component.check)) problem(`component ${name} checks "${component.check}", which isn't a web address`);
408
435
  }
409
- for (const recipient of status.alert) if ("role" in recipient && !manifest.roles[recipient.role]) problem(`alerts role "${recipient.role}", which doesn't exist`);
436
+ checkRecipients(manifest, status.alert, (message) => problem(`alerts ${message}`));
410
437
  if (status.after < 6e4) problem("waits less than a minute before calling your app down. Use 1m or more.");
411
438
  for (const [what, url] of [
412
439
  ["logo", status.logo?.light],
@@ -726,15 +753,32 @@ function byDefinition(a, b) {
726
753
  return a.origin.file.localeCompare(b.origin.file) || a.origin.line - b.origin.line;
727
754
  }
728
755
  function notifyDef(locator, rule) {
756
+ const { options } = rule;
729
757
  return {
730
758
  source: locator.call("notify", rule.origin, rule.name),
731
- inboxes: rule.options.inboxes.map((inbox) => inbox.name),
732
- on: rule.options.on,
733
- to: recipients(rule.options.to)
759
+ inboxes: options.inboxes.map((inbox) => inbox.name),
760
+ on: options.on,
761
+ where: options.where ? whereDef(options.where) : null,
762
+ to: recipients(options.to),
763
+ escalate: (options.escalate ?? []).map((step) => ({
764
+ after: milliseconds(step.after),
765
+ to: recipients(step.to)
766
+ }))
767
+ };
768
+ }
769
+ function whereDef(where) {
770
+ return {
771
+ tags: where.tags ?? [],
772
+ data: Object.fromEntries(Object.entries(where.data ?? {}).map(([field, value]) => [field, Array.isArray(value) ? value : [value]]))
734
773
  };
735
774
  }
736
775
  function recipients(list) {
737
- return list.map((recipient) => "channel" in recipient ? { channel: recipient.channel } : { role: recipient.name });
776
+ return list.map((recipient) => {
777
+ if ("channel" in recipient) return { channel: recipient.channel };
778
+ if ("person" in recipient) return { person: recipient.person };
779
+ if ("assignee" in recipient) return { assignee: true };
780
+ return { role: recipient.name };
781
+ });
738
782
  }
739
783
  function logo(value) {
740
784
  if (value === void 0) return null;
package/index.d.mts CHANGED
@@ -92,6 +92,7 @@ interface ItemSummary {
92
92
  triage: "pending" | "done" | null;
93
93
  autoHandled: boolean;
94
94
  dueAt: number | null;
95
+ urgent: boolean;
95
96
  fields: Record<string, FieldValue>;
96
97
  }
97
98
  //#endregion
@@ -102,6 +103,8 @@ type FieldValue = string | number | boolean | null | {
102
103
  record: RecordRef;
103
104
  } | {
104
105
  code: string;
106
+ } | {
107
+ link: Link;
105
108
  } | {
106
109
  live: LiveStat;
107
110
  } | {
@@ -115,6 +118,10 @@ interface Editable {
115
118
  record: string | null;
116
119
  field: string | null;
117
120
  }
121
+ interface Link {
122
+ url: string;
123
+ title?: string;
124
+ }
118
125
  interface Sensitive {
119
126
  value?: string;
120
127
  hint?: string;
@@ -184,6 +191,7 @@ type Block = {
184
191
  }[];
185
192
  } | {
186
193
  type: "table";
194
+ title?: string;
187
195
  columns: string[];
188
196
  rows: FieldValue[][];
189
197
  } | {
@@ -396,19 +404,34 @@ interface FlagGuard {
396
404
  perHour: number;
397
405
  then: "pause" | "revert";
398
406
  }
399
- type NotifyTrigger = "created" | "reopened" | "overdue" | {
407
+ type NotifyTrigger = "created" | "reopened" | "overdue" | "urgent" | {
400
408
  spike: number;
401
409
  };
402
- type Recipient = {
410
+ type Recipient$1 = {
403
411
  role: string;
404
412
  } | {
405
413
  channel: string;
414
+ } | {
415
+ person: string;
416
+ } | {
417
+ assignee: true;
406
418
  };
419
+ type MatchValue = string | number | boolean;
420
+ interface NotifyWhere$1 {
421
+ tags: string[];
422
+ data: Record<string, MatchValue[]>;
423
+ }
424
+ interface Escalation {
425
+ after: number;
426
+ to: Recipient$1[];
427
+ }
407
428
  interface NotifyDef {
408
429
  source: string;
409
430
  inboxes: string[];
410
431
  on: NotifyTrigger[];
411
- to: Recipient[];
432
+ where: NotifyWhere$1 | null;
433
+ to: Recipient$1[];
434
+ escalate: Escalation[];
412
435
  }
413
436
  interface StatusComponentDef {
414
437
  label: string;
@@ -427,7 +450,7 @@ interface StatusDef<Component extends string = string> {
427
450
  website: string | null;
428
451
  css: string | null;
429
452
  components: Record<Component, StatusComponentDef>;
430
- alert: Recipient[];
453
+ alert: Recipient$1[];
431
454
  after: number;
432
455
  }
433
456
  type Delivery = {
@@ -796,7 +819,11 @@ export declare const b: {
796
819
  at: Date | number;
797
820
  text: string;
798
821
  }[]) => Block;
799
- table: (rows: Record<string, FieldInput>[], columns?: string[]) => Block;
822
+ link: (url: string, title?: string) => FieldValue;
823
+ table: (rows: Record<string, FieldInput>[], { title, columns }?: {
824
+ title?: string;
825
+ columns?: string[];
826
+ }) => Block;
800
827
  stats: (stats: Record<string, FieldInput | {
801
828
  value: FieldInput;
802
829
  note?: string;
@@ -891,11 +918,30 @@ export declare function space(name: string, options: SpaceOptions): Space;
891
918
  interface Channel {
892
919
  readonly channel: string;
893
920
  }
921
+ interface Person {
922
+ readonly person: string;
923
+ }
924
+ interface Assignee {
925
+ readonly assignee: true;
926
+ }
927
+ type Recipient = Role | Channel | Person | Assignee;
894
928
  export declare function slack(channel: string): Channel;
929
+ export declare function person(email: string): Person;
930
+ export declare const assignee: Assignee;
931
+ interface NotifyWhere {
932
+ tags?: string[];
933
+ data?: Record<string, MatchValue | MatchValue[]>;
934
+ }
935
+ interface EscalationStep {
936
+ after: Duration;
937
+ to: Recipient[];
938
+ }
895
939
  interface NotifyOptions {
896
940
  inboxes: Inbox[];
897
941
  on: NotifyTrigger[];
898
- to: (Role | Channel)[];
942
+ where?: NotifyWhere;
943
+ to: Recipient[];
944
+ escalate?: EscalationStep[];
899
945
  }
900
946
  interface Notify extends Located {
901
947
  readonly name: string;
@@ -919,7 +965,7 @@ interface StatusOptions {
919
965
  website?: string;
920
966
  css?: string;
921
967
  components: Record<string, string | StatusComponent>;
922
- alert?: (Role | Channel)[];
968
+ alert?: (Role | Channel | Person)[];
923
969
  after?: Duration;
924
970
  }
925
971
  interface Status extends Located {
@@ -965,6 +1011,8 @@ interface SendOptions<S extends Schema | undefined, Suggested extends unknown[]
965
1011
  data: InboxInput<S>;
966
1012
  tags?: string[];
967
1013
  suggest?: { [K in keyof Suggested]: Suggestion<Suggested[K]>; };
1014
+ urgent?: boolean;
1015
+ assign?: string;
968
1016
  }
969
1017
  interface ReceivedMessage extends Omit<IncomingMessage, "attachments"> {
970
1018
  attachments?: BlockInput[];
@@ -995,4 +1043,4 @@ export declare function createPult<Component extends string = string>({ key, pro
995
1043
  };
996
1044
  type Pult = ReturnType<typeof createPult>;
997
1045
  //#endregion
998
- export type { Action, ActionContext, ActionOptions, ActionRef, Actor, Block, BlockInput, ButtonInput, Channel, ComponentState, Config, ConfigOptions, Duration, FieldInput, Flag, FlagContext, FlagOptions, Guard, Inbox, InboxOptions, ItemView, ListOptions, ListQuery, ListenOptions, Manifest, Notify, NotifyOptions, OutgoingEmail, Page, PageContext, PageOptions, Project, Pult, PultOptions, Question, ReceivedMessage, Relation, Resource, ResourceOptions, Role, RoleOptions, Rule, SendOptions, SendResult, Space, SpaceOptions, StandaloneOptions, StateChange, Status, StatusOptions, Suggestion, Triage, View, ViewOptions };
1046
+ export type { Action, ActionContext, ActionOptions, ActionRef, Actor, Assignee, Block, BlockInput, ButtonInput, Channel, ComponentState, Config, ConfigOptions, Duration, EscalationStep, FieldInput, Flag, FlagContext, FlagOptions, Guard, Inbox, InboxOptions, ItemView, ListOptions, ListQuery, ListenOptions, Manifest, Notify, NotifyOptions, NotifyWhere, OutgoingEmail, Page, PageContext, PageOptions, Person, Project, Pult, PultOptions, Question, ReceivedMessage, Recipient, Relation, Resource, ResourceOptions, Role, RoleOptions, Rule, SendOptions, SendResult, Space, SpaceOptions, StandaloneOptions, StateChange, Status, StatusOptions, Suggestion, Triage, View, ViewOptions };
package/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as mapBlocks, D as sign, E as NonceWindow, F as HOME, I as actionId, L as findActionDef, M as valuesIn, N as DEFAULT_STATES, O as verify, S as kindOf, T as refKey, a as collect, b as captureOrigin, d as Transport, f as b, g as isPendingAttachment, h as hasPendingAttachments, i as validate, k as eachBlock, m as fieldValues, n as reviveDates, o as findAction, p as bindActions, s as flag, t as ValidationError, u as PultError, v as view, w as HEADER, y as KIND } from "./schema-DuTVNptF.mjs";
1
+ import { A as mapBlocks, D as sign, E as NonceWindow, F as HOME, I as actionId, L as findActionDef, M as valuesIn, N as DEFAULT_STATES, O as verify, S as kindOf, T as refKey, a as collect, b as captureOrigin, d as Transport, f as b, g as isPendingAttachment, h as hasPendingAttachments, i as validate, k as eachBlock, m as fieldValues, n as reviveDates, o as findAction, p as bindActions, s as flag, t as ValidationError, u as PultError, v as view, w as HEADER, y as KIND } from "./schema-DCS7N4sm.mjs";
2
2
  //#region ../protocol/src/flags.ts
3
3
  function rampAt(ramp, now = Date.now()) {
4
4
  if (now >= ramp.end) return ramp.to;
@@ -843,6 +843,8 @@ function createPult({ key = env("PULT_KEY"), project, url = env("PULT_URL") ?? "
843
843
  inbox: inbox.name,
844
844
  fingerprint: item.fingerprint,
845
845
  tags: item.tags,
846
+ urgent: item.urgent,
847
+ assign: item.assign,
846
848
  suggest: item.suggest?.map(({ action, ...rest }) => ({
847
849
  action: action.id,
848
850
  ...rest
@@ -1039,6 +1041,10 @@ function status(options) {
1039
1041
  function slack(channel) {
1040
1042
  return { channel: channel.replace(/^#/, "") };
1041
1043
  }
1044
+ function person(email) {
1045
+ return { person: email.trim().toLowerCase() };
1046
+ }
1047
+ const assignee = { assignee: true };
1042
1048
  function notify(name, options) {
1043
1049
  return {
1044
1050
  [KIND]: "notify",
@@ -1048,4 +1054,4 @@ function notify(name, options) {
1048
1054
  };
1049
1055
  }
1050
1056
  //#endregion
1051
- export { PultError, ValidationError, action, b, choice, createPult, defineConfig, flag, home, inbox, notify, noul, page, resource, role, run, score, setState, slack, space, status, tag, triage, view };
1057
+ export { PultError, ValidationError, action, assignee, b, choice, createPult, defineConfig, flag, home, inbox, notify, noul, page, person, resource, role, run, score, setState, slack, space, status, tag, triage, view };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lantharos/pult",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "The other side of your app",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -361,8 +361,13 @@ const b = {
361
361
  text
362
362
  }))
363
363
  }),
364
- table: (rows, columns = Object.keys(rows[0] ?? {})) => ({
364
+ link: (url, title) => ({ link: {
365
+ url,
366
+ ...title ? { title } : {}
367
+ } }),
368
+ table: (rows, { title, columns = Object.keys(rows[0] ?? {}) } = {}) => ({
365
369
  type: "table",
370
+ ...title ? { title } : {},
366
371
  columns,
367
372
  rows: rows.map((row) => columns.map((column) => field(row[column])))
368
373
  }),
@@ -585,6 +590,7 @@ function collect(modules) {
585
590
  claim(found.notify, rule, "notifications");
586
591
  for (const inbox of rule.options.inboxes) add(inbox);
587
592
  for (const recipient of rule.options.to) add(recipient);
593
+ for (const step of rule.options.escalate ?? []) for (const recipient of step.to) add(recipient);
588
594
  return;
589
595
  }
590
596
  case "status": {