@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 +51 -7
- package/index.d.mts +56 -8
- package/index.mjs +8 -2
- package/package.json +1 -1
- package/{schema-DuTVNptF.mjs → schema-DCS7N4sm.mjs} +7 -1
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-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
732
|
-
on:
|
|
733
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
@@ -361,8 +361,13 @@ const b = {
|
|
|
361
361
|
text
|
|
362
362
|
}))
|
|
363
363
|
}),
|
|
364
|
-
|
|
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": {
|