@lantharos/pult 0.12.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-D7N0bd_N.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.12.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
@@ -403,19 +404,34 @@ interface FlagGuard {
403
404
  perHour: number;
404
405
  then: "pause" | "revert";
405
406
  }
406
- type NotifyTrigger = "created" | "reopened" | "overdue" | {
407
+ type NotifyTrigger = "created" | "reopened" | "overdue" | "urgent" | {
407
408
  spike: number;
408
409
  };
409
- type Recipient = {
410
+ type Recipient$1 = {
410
411
  role: string;
411
412
  } | {
412
413
  channel: string;
414
+ } | {
415
+ person: string;
416
+ } | {
417
+ assignee: true;
413
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
+ }
414
428
  interface NotifyDef {
415
429
  source: string;
416
430
  inboxes: string[];
417
431
  on: NotifyTrigger[];
418
- to: Recipient[];
432
+ where: NotifyWhere$1 | null;
433
+ to: Recipient$1[];
434
+ escalate: Escalation[];
419
435
  }
420
436
  interface StatusComponentDef {
421
437
  label: string;
@@ -434,7 +450,7 @@ interface StatusDef<Component extends string = string> {
434
450
  website: string | null;
435
451
  css: string | null;
436
452
  components: Record<Component, StatusComponentDef>;
437
- alert: Recipient[];
453
+ alert: Recipient$1[];
438
454
  after: number;
439
455
  }
440
456
  type Delivery = {
@@ -902,11 +918,30 @@ export declare function space(name: string, options: SpaceOptions): Space;
902
918
  interface Channel {
903
919
  readonly channel: string;
904
920
  }
921
+ interface Person {
922
+ readonly person: string;
923
+ }
924
+ interface Assignee {
925
+ readonly assignee: true;
926
+ }
927
+ type Recipient = Role | Channel | Person | Assignee;
905
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
+ }
906
939
  interface NotifyOptions {
907
940
  inboxes: Inbox[];
908
941
  on: NotifyTrigger[];
909
- to: (Role | Channel)[];
942
+ where?: NotifyWhere;
943
+ to: Recipient[];
944
+ escalate?: EscalationStep[];
910
945
  }
911
946
  interface Notify extends Located {
912
947
  readonly name: string;
@@ -930,7 +965,7 @@ interface StatusOptions {
930
965
  website?: string;
931
966
  css?: string;
932
967
  components: Record<string, string | StatusComponent>;
933
- alert?: (Role | Channel)[];
968
+ alert?: (Role | Channel | Person)[];
934
969
  after?: Duration;
935
970
  }
936
971
  interface Status extends Located {
@@ -976,6 +1011,8 @@ interface SendOptions<S extends Schema | undefined, Suggested extends unknown[]
976
1011
  data: InboxInput<S>;
977
1012
  tags?: string[];
978
1013
  suggest?: { [K in keyof Suggested]: Suggestion<Suggested[K]>; };
1014
+ urgent?: boolean;
1015
+ assign?: string;
979
1016
  }
980
1017
  interface ReceivedMessage extends Omit<IncomingMessage, "attachments"> {
981
1018
  attachments?: BlockInput[];
@@ -1006,4 +1043,4 @@ export declare function createPult<Component extends string = string>({ key, pro
1006
1043
  };
1007
1044
  type Pult = ReturnType<typeof createPult>;
1008
1045
  //#endregion
1009
- 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-D7N0bd_N.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.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "The other side of your app",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -590,6 +590,7 @@ function collect(modules) {
590
590
  claim(found.notify, rule, "notifications");
591
591
  for (const inbox of rule.options.inboxes) add(inbox);
592
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);
593
594
  return;
594
595
  }
595
596
  case "status": {