@lantharos/pult 0.0.0-stage → 0.8.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/LICENSE +21 -0
- package/README.md +61 -2
- package/cli.d.mts +1 -0
- package/cli.mjs +917 -0
- package/index.d.mts +950 -0
- package/index.mjs +950 -0
- package/package.json +23 -4
- package/schema-CzG8J_9_.mjs +638 -0
- package/stub.d.mts +11 -0
- package/stub.mjs +26 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
import { A as mapBlocks, C as HEADER, D as sign, O as verify, P as findActionDef, T as NonceWindow, a as collect, b as KIND, d as Transport, f as b, g as home, h as isPendingAttachment, i as validate, j as DEFAULT_STATES, 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 page, w as refKey, x as captureOrigin, y as view } from "./schema-CzG8J_9_.mjs";
|
|
2
|
+
//#region ../protocol/src/flags.ts
|
|
3
|
+
function rampAt(ramp, now = Date.now()) {
|
|
4
|
+
if (now >= ramp.end) return ramp.to;
|
|
5
|
+
if (now <= ramp.start) return ramp.from;
|
|
6
|
+
const elapsed = ramp.every ? Math.floor((now - ramp.start) / ramp.every) * ramp.every : now - ramp.start;
|
|
7
|
+
const value = ramp.from + (ramp.to - ramp.from) * elapsed / (ramp.end - ramp.start);
|
|
8
|
+
return ramp.whole ? Math.round(value) : value;
|
|
9
|
+
}
|
|
10
|
+
function currentRollout(state, now = Date.now()) {
|
|
11
|
+
return state.ramp?.of === "rollout" ? rampAt(state.ramp, now) : state.rollout;
|
|
12
|
+
}
|
|
13
|
+
function currentValue(state, now = Date.now()) {
|
|
14
|
+
return state.ramp?.of === "value" ? rampAt(state.ramp, now) : state.value;
|
|
15
|
+
}
|
|
16
|
+
function resolveFlag(name, state, context = {}, now = Date.now()) {
|
|
17
|
+
const { key, attributes = {} } = context;
|
|
18
|
+
if (key !== void 0 && key in state.targets) return state.targets[key];
|
|
19
|
+
const rule = state.rules.find((candidate) => candidate.conditions.every((condition) => holds(condition, attributes[condition.attribute])));
|
|
20
|
+
if (rule) return rule.rollout === null ? rule.value : share(name, key, rule.rollout);
|
|
21
|
+
const rollout = currentRollout(state, now);
|
|
22
|
+
if (rollout !== null) return share(name, key, rollout);
|
|
23
|
+
return currentValue(state, now);
|
|
24
|
+
}
|
|
25
|
+
function holds(condition, value) {
|
|
26
|
+
if (value === null || value === void 0) return condition.op === "is not";
|
|
27
|
+
switch (condition.op) {
|
|
28
|
+
case "is": return condition.values.includes(value);
|
|
29
|
+
case "is not": return !condition.values.includes(value);
|
|
30
|
+
case "above": return typeof value === "number" && value > Number(condition.values[0]);
|
|
31
|
+
case "below": return typeof value === "number" && value < Number(condition.values[0]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function share(name, key, rollout) {
|
|
35
|
+
return key === void 0 ? rollout >= 100 : bucket(name, key) < rollout;
|
|
36
|
+
}
|
|
37
|
+
function bucket(name, key) {
|
|
38
|
+
let hash = 2166136261;
|
|
39
|
+
for (const char of `${name}:${key}`) {
|
|
40
|
+
hash ^= char.codePointAt(0);
|
|
41
|
+
hash = Math.imul(hash, 16777619);
|
|
42
|
+
}
|
|
43
|
+
return (hash >>> 0) % 1e4 / 100;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/client/runtime.ts
|
|
47
|
+
const onWorkers = globalThis.navigator?.userAgent === "Cloudflare-Workers";
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/client/batch.ts
|
|
50
|
+
const FLUSH_MS = 8;
|
|
51
|
+
const MAX_ITEMS = 200;
|
|
52
|
+
const MAX_BYTES = 15e5;
|
|
53
|
+
const schedule = onWorkers ? queueMicrotask : (flush) => setTimeout(flush, FLUSH_MS);
|
|
54
|
+
var Batcher = class {
|
|
55
|
+
transport;
|
|
56
|
+
sync;
|
|
57
|
+
#queue = [];
|
|
58
|
+
#bytes = 0;
|
|
59
|
+
#scheduled = false;
|
|
60
|
+
constructor(transport, sync) {
|
|
61
|
+
this.transport = transport;
|
|
62
|
+
this.sync = sync;
|
|
63
|
+
}
|
|
64
|
+
send(item) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const size = JSON.stringify(item).length;
|
|
67
|
+
if (this.#bytes + size > MAX_BYTES) this.#flush();
|
|
68
|
+
this.#queue.push({
|
|
69
|
+
item,
|
|
70
|
+
size,
|
|
71
|
+
resolve,
|
|
72
|
+
reject
|
|
73
|
+
});
|
|
74
|
+
this.#bytes += size;
|
|
75
|
+
if (this.#queue.length >= MAX_ITEMS) this.#flush();
|
|
76
|
+
else if (!this.#scheduled) {
|
|
77
|
+
this.#scheduled = true;
|
|
78
|
+
schedule(() => {
|
|
79
|
+
this.#scheduled = false;
|
|
80
|
+
this.#flush();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
#flush() {
|
|
86
|
+
const batch = this.#queue;
|
|
87
|
+
this.#queue = [];
|
|
88
|
+
this.#bytes = 0;
|
|
89
|
+
if (batch.length) this.#post(batch);
|
|
90
|
+
}
|
|
91
|
+
async #post(batch) {
|
|
92
|
+
const body = { items: batch.map((entry) => entry.item) };
|
|
93
|
+
try {
|
|
94
|
+
let response;
|
|
95
|
+
try {
|
|
96
|
+
response = await this.transport.request("POST", "/v1/items", body);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (!(error instanceof PultError && error.code === "manifest_required")) throw error;
|
|
99
|
+
await this.sync();
|
|
100
|
+
response = await this.transport.request("POST", "/v1/items", body);
|
|
101
|
+
}
|
|
102
|
+
response.results.forEach((outcome, index) => {
|
|
103
|
+
const entry = batch[index];
|
|
104
|
+
if ("error" in outcome) entry.reject(new PultError(outcome.error, 400, outcome.code));
|
|
105
|
+
else entry.resolve(outcome);
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
for (const entry of batch) entry.reject(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/define/inbox.ts
|
|
114
|
+
function inbox(name, options) {
|
|
115
|
+
const states = options.states ?? DEFAULT_STATES;
|
|
116
|
+
const closed = options.closed ?? (options.states ? states.slice(1) : ["handled"]);
|
|
117
|
+
for (const state of closed) if (!states.includes(state)) throw new Error(`Inbox "${name}" closes on "${state}", which isn't one of its states`);
|
|
118
|
+
for (const state of Object.keys(options.sla ?? {})) if (!states.includes(state)) throw new Error(`Inbox "${name}" has a response time for "${state}", which isn't one of its states`);
|
|
119
|
+
return {
|
|
120
|
+
[KIND]: "inbox",
|
|
121
|
+
origin: captureOrigin(),
|
|
122
|
+
name,
|
|
123
|
+
options,
|
|
124
|
+
states,
|
|
125
|
+
closed
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function liveView(title, data, record) {
|
|
129
|
+
return {
|
|
130
|
+
title,
|
|
131
|
+
data,
|
|
132
|
+
record,
|
|
133
|
+
occurrences: { live: "occurrences" },
|
|
134
|
+
users: { live: "users" },
|
|
135
|
+
firstSeen: { live: "firstSeen" },
|
|
136
|
+
lastSeen: { live: "lastSeen" },
|
|
137
|
+
due: { live: "due" }
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/define/triage.ts
|
|
142
|
+
function noul(prompt, criteria) {
|
|
143
|
+
return {
|
|
144
|
+
type: "noul",
|
|
145
|
+
prompt,
|
|
146
|
+
...criteria ? { criteria } : {}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function choice(options, prompt) {
|
|
150
|
+
return {
|
|
151
|
+
type: "choice",
|
|
152
|
+
prompt,
|
|
153
|
+
options: Array.isArray(options) ? Object.fromEntries(options.map((option) => [option, option])) : options
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function score(levels, prompt) {
|
|
157
|
+
return {
|
|
158
|
+
type: "score",
|
|
159
|
+
prompt,
|
|
160
|
+
levels: [...levels]
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function triage(options) {
|
|
164
|
+
return {
|
|
165
|
+
instructions: options.instructions ?? "",
|
|
166
|
+
questions: options.questions,
|
|
167
|
+
rules: options.rules ?? []
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const setState = (state) => ({
|
|
171
|
+
type: "state",
|
|
172
|
+
state
|
|
173
|
+
});
|
|
174
|
+
const tag = (name) => ({
|
|
175
|
+
type: "tag",
|
|
176
|
+
tag: name
|
|
177
|
+
});
|
|
178
|
+
const run = (action, input) => ({
|
|
179
|
+
type: "run",
|
|
180
|
+
action: action.id,
|
|
181
|
+
input
|
|
182
|
+
});
|
|
183
|
+
function decide(rules, answers) {
|
|
184
|
+
const index = rules.findIndex((rule) => rule.when(answers));
|
|
185
|
+
if (index === -1) return null;
|
|
186
|
+
const then = rules[index].then;
|
|
187
|
+
return {
|
|
188
|
+
rule: index,
|
|
189
|
+
decisions: Array.isArray(then) ? then : [then]
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/define/resource.ts
|
|
194
|
+
function action(name, options) {
|
|
195
|
+
return {
|
|
196
|
+
[KIND]: "action",
|
|
197
|
+
origin: captureOrigin(),
|
|
198
|
+
options,
|
|
199
|
+
resource: null,
|
|
200
|
+
name,
|
|
201
|
+
id: name
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const identity = (value) => value;
|
|
205
|
+
function resource(name, options) {
|
|
206
|
+
const origin = captureOrigin();
|
|
207
|
+
const actions = { "*": { id: `${name}.*` } };
|
|
208
|
+
for (const [key, action] of Object.entries(options.actions?.(identity) ?? {})) actions[key] = {
|
|
209
|
+
[KIND]: "action",
|
|
210
|
+
origin,
|
|
211
|
+
options: action,
|
|
212
|
+
resource: name,
|
|
213
|
+
name: key,
|
|
214
|
+
id: `${name}.${key}`
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
[KIND]: "resource",
|
|
218
|
+
origin,
|
|
219
|
+
name,
|
|
220
|
+
options,
|
|
221
|
+
actions
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function recordId(resource, record) {
|
|
225
|
+
return resource.options.id ? resource.options.id(record) : String(record.id);
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/client/records.ts
|
|
229
|
+
function resourceOf(definitions, name) {
|
|
230
|
+
const resource = definitions.resources.get(name);
|
|
231
|
+
if (!resource) throw new Error(`Unknown resource "${name}"`);
|
|
232
|
+
return resource;
|
|
233
|
+
}
|
|
234
|
+
async function target(definitions, action, id) {
|
|
235
|
+
if (action.resource === null) return null;
|
|
236
|
+
const resource = resourceOf(definitions, action.resource);
|
|
237
|
+
const record = id === null ? null : await resource.options.get(id);
|
|
238
|
+
if (!record) throw new Error(`${resource.name} ${id} doesn't exist`);
|
|
239
|
+
return record;
|
|
240
|
+
}
|
|
241
|
+
function found(resource, record) {
|
|
242
|
+
return {
|
|
243
|
+
id: recordId(resource, record),
|
|
244
|
+
title: resource.options.title(record),
|
|
245
|
+
subtitle: resource.options.subtitle?.(record)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function listed(resource, record) {
|
|
249
|
+
return {
|
|
250
|
+
...found(resource, record),
|
|
251
|
+
fields: fieldValues(resource.options.fields?.(record) ?? {})
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
async function find(definitions, request) {
|
|
255
|
+
const resource = resourceOf(definitions, request.resource);
|
|
256
|
+
if (!resource.options.find) return [];
|
|
257
|
+
return (await resource.options.find(request.query, request.limit)).slice(0, request.limit).map((record) => found(resource, record));
|
|
258
|
+
}
|
|
259
|
+
async function get(definitions, request, upload) {
|
|
260
|
+
const resource = resourceOf(definitions, request.resource);
|
|
261
|
+
const record = await resource.options.get(request.id);
|
|
262
|
+
if (!record) return null;
|
|
263
|
+
const blocks = resource.options.render ? await resource.options.render(record) : [];
|
|
264
|
+
return {
|
|
265
|
+
...found(resource, record),
|
|
266
|
+
url: resource.options.url?.(record),
|
|
267
|
+
blocks: await settle(definitions, await upload(blocks), {
|
|
268
|
+
resource,
|
|
269
|
+
record
|
|
270
|
+
})
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
async function list(definitions, request) {
|
|
274
|
+
const resource = resourceOf(definitions, request.resource);
|
|
275
|
+
const options = resource.options.list;
|
|
276
|
+
if (!options) throw new Error(`${resource.name} can't be listed`);
|
|
277
|
+
const filters = options.filters ? await validate(options.filters, request.filters) : {};
|
|
278
|
+
const page = await options.run({
|
|
279
|
+
query: request.query,
|
|
280
|
+
filters,
|
|
281
|
+
cursor: request.cursor,
|
|
282
|
+
limit: request.limit
|
|
283
|
+
});
|
|
284
|
+
return {
|
|
285
|
+
records: page.records.slice(0, request.limit).map((record) => listed(resource, record)),
|
|
286
|
+
cursor: page.cursor ?? null
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async function related(definitions, request) {
|
|
290
|
+
const resource = resourceOf(definitions, request.resource);
|
|
291
|
+
const relation = resource.options.related?.[request.relation];
|
|
292
|
+
if (!relation) throw new Error(`${resource.name} has no relation "${request.relation}"`);
|
|
293
|
+
const record = await resource.options.get(request.id);
|
|
294
|
+
if (!record) return [];
|
|
295
|
+
const target = resourceOf(definitions, relation.resource);
|
|
296
|
+
return (await relation.list(record, request.limit)).slice(0, request.limit).map((entry) => listed(target, entry));
|
|
297
|
+
}
|
|
298
|
+
async function choices(definitions, request) {
|
|
299
|
+
const action = findAction(definitions, request.action);
|
|
300
|
+
if (!action) throw new Error(`Unknown action "${request.action}"`);
|
|
301
|
+
const record = await target(definitions, action, request.record).catch(() => void 0);
|
|
302
|
+
if (record === void 0) return {};
|
|
303
|
+
const entries = Object.entries(action.options.choices ?? {});
|
|
304
|
+
const resolved = await Promise.all(entries.map(async ([field, options]) => [field, (await options(record)).map(toChoice)]));
|
|
305
|
+
return Object.fromEntries(resolved);
|
|
306
|
+
}
|
|
307
|
+
function toChoice(choice) {
|
|
308
|
+
return typeof choice === "string" ? {
|
|
309
|
+
value: choice,
|
|
310
|
+
label: choice
|
|
311
|
+
} : choice;
|
|
312
|
+
}
|
|
313
|
+
async function titles(definitions, request) {
|
|
314
|
+
const resolved = await Promise.all(request.refs.map(async (ref) => {
|
|
315
|
+
const resource = definitions.resources.get(ref.resource);
|
|
316
|
+
const record = resource && await resource.options.get(ref.id);
|
|
317
|
+
return record ? [refKey(ref), resource.options.title(record)] : null;
|
|
318
|
+
}));
|
|
319
|
+
return Object.fromEntries(resolved.filter((entry) => entry !== null));
|
|
320
|
+
}
|
|
321
|
+
async function available(definitions, request) {
|
|
322
|
+
const availability = {};
|
|
323
|
+
for (const ref of request.refs) {
|
|
324
|
+
const resource = definitions.resources.get(ref.resource);
|
|
325
|
+
const record = resource && await resource.options.get(ref.id);
|
|
326
|
+
if (!resource || !record) continue;
|
|
327
|
+
const actions = {};
|
|
328
|
+
for (const [name, action] of Object.entries(resource.actions)) {
|
|
329
|
+
if (!("options" in action)) continue;
|
|
330
|
+
const { when, defaults } = action.options;
|
|
331
|
+
if (when && !when(record)) continue;
|
|
332
|
+
actions[name] = defaults ? JSON.parse(JSON.stringify(defaults(record))) : null;
|
|
333
|
+
}
|
|
334
|
+
availability[`${ref.resource}:${ref.id}`] = actions;
|
|
335
|
+
}
|
|
336
|
+
return availability;
|
|
337
|
+
}
|
|
338
|
+
async function settle(definitions, blocks, scope) {
|
|
339
|
+
const bound = scope ? bindActions(blocks, scope.resource, recordId(scope.resource, scope.record)) : blocks;
|
|
340
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
341
|
+
if (scope) loaded.set(`${scope.resource.name}:${recordId(scope.resource, scope.record)}`, Promise.resolve(scope.record));
|
|
342
|
+
const load = (resource, id) => {
|
|
343
|
+
const key = `${resource.name}:${id}`;
|
|
344
|
+
if (!loaded.has(key)) loaded.set(key, Promise.resolve(resource.options.get(id)));
|
|
345
|
+
return loaded.get(key);
|
|
346
|
+
};
|
|
347
|
+
const verdict = async ({ action: id, record: target }) => {
|
|
348
|
+
const action = findAction(definitions, id);
|
|
349
|
+
if (!action) return { applies: true };
|
|
350
|
+
const record = action.resource && target !== null ? await load(resourceOf(definitions, action.resource), target) : null;
|
|
351
|
+
if (action.resource && target !== null && !record) return { applies: false };
|
|
352
|
+
if (record && action.options.when && !action.options.when(record)) return { applies: false };
|
|
353
|
+
const defaults = action.options.input ? action.options.defaults?.(record) : void 0;
|
|
354
|
+
return {
|
|
355
|
+
applies: true,
|
|
356
|
+
...defaults && { defaults: JSON.parse(JSON.stringify(defaults)) }
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
const targets = /* @__PURE__ */ new Map();
|
|
360
|
+
const keyOf = (target) => `${target.action}@${target.record ?? ""}`;
|
|
361
|
+
const visit = (target) => {
|
|
362
|
+
if (!targets.has(keyOf(target))) targets.set(keyOf(target), verdict(target));
|
|
363
|
+
};
|
|
364
|
+
const editsIn = (value) => {
|
|
365
|
+
if (value && typeof value === "object" && "edit" in value) visit(value.edit);
|
|
366
|
+
};
|
|
367
|
+
eachBlock(bound, (block) => {
|
|
368
|
+
if (block.type === "actions") for (const button of block.actions) visit(button);
|
|
369
|
+
if (block.type === "form") visit(block.form);
|
|
370
|
+
if (block.type === "fields") for (const field of block.fields) editsIn(field.value);
|
|
371
|
+
if (block.type === "stats") for (const stat of block.stats) editsIn(stat.value);
|
|
372
|
+
if (block.type === "table") for (const row of block.rows) for (const value of row) editsIn(value);
|
|
373
|
+
});
|
|
374
|
+
if (!targets.size) return bound;
|
|
375
|
+
const verdicts = new Map(await Promise.all([...targets].map(async ([key, pending]) => [key, await pending])));
|
|
376
|
+
const editable = (value) => value && typeof value === "object" && "edit" in value && !verdicts.get(keyOf(value.edit))?.applies ? value.edit.value : value;
|
|
377
|
+
const settled = (button) => {
|
|
378
|
+
const found = verdicts.get(keyOf(button));
|
|
379
|
+
if (!found.applies) return null;
|
|
380
|
+
return found.defaults ? {
|
|
381
|
+
...button,
|
|
382
|
+
input: {
|
|
383
|
+
...found.defaults,
|
|
384
|
+
...button.input
|
|
385
|
+
}
|
|
386
|
+
} : button;
|
|
387
|
+
};
|
|
388
|
+
return mapBlocks(bound, (block) => {
|
|
389
|
+
if (block.type === "actions") {
|
|
390
|
+
const actions = block.actions.flatMap((button) => settled(button) ?? []);
|
|
391
|
+
return actions.length ? {
|
|
392
|
+
...block,
|
|
393
|
+
actions
|
|
394
|
+
} : null;
|
|
395
|
+
}
|
|
396
|
+
if (block.type === "form") {
|
|
397
|
+
const form = settled(block.form);
|
|
398
|
+
return form ? {
|
|
399
|
+
...block,
|
|
400
|
+
form
|
|
401
|
+
} : null;
|
|
402
|
+
}
|
|
403
|
+
if (block.type === "fields") return {
|
|
404
|
+
...block,
|
|
405
|
+
fields: block.fields.map((field) => ({
|
|
406
|
+
...field,
|
|
407
|
+
value: editable(field.value)
|
|
408
|
+
}))
|
|
409
|
+
};
|
|
410
|
+
if (block.type === "stats") return {
|
|
411
|
+
...block,
|
|
412
|
+
stats: block.stats.map((stat) => ({
|
|
413
|
+
...stat,
|
|
414
|
+
value: editable(stat.value)
|
|
415
|
+
}))
|
|
416
|
+
};
|
|
417
|
+
if (block.type === "table") return {
|
|
418
|
+
...block,
|
|
419
|
+
rows: block.rows.map((row) => row.map(editable))
|
|
420
|
+
};
|
|
421
|
+
return block;
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region src/client/actions.ts
|
|
426
|
+
const ignoreProgress = async () => {};
|
|
427
|
+
async function runAction(definitions, manifest, { id, request, actor }, upload, jobs) {
|
|
428
|
+
const action = findAction(definitions, request.action);
|
|
429
|
+
if (!action) throw new Error(`Unknown action "${request.action}"`);
|
|
430
|
+
const def = findActionDef(manifest, request.action);
|
|
431
|
+
const record = await target(definitions, action, request.record);
|
|
432
|
+
if (action.options.when && !action.options.when(record)) {
|
|
433
|
+
const title = resourceOf(definitions, action.resource).options.title(record);
|
|
434
|
+
throw new Error(`${def?.label ?? action.name} doesn't apply to ${title} right now`);
|
|
435
|
+
}
|
|
436
|
+
const raw = reviveDates(def?.input ?? null, request.input);
|
|
437
|
+
const input = action.options.input ? await validate(action.options.input, raw) : {};
|
|
438
|
+
const background = Boolean(action.options.background) && !request.preview;
|
|
439
|
+
const progress = background ? (fraction, note) => jobs.report(id, {
|
|
440
|
+
progress: fraction,
|
|
441
|
+
note
|
|
442
|
+
}) : ignoreProgress;
|
|
443
|
+
const context = {
|
|
444
|
+
record,
|
|
445
|
+
input,
|
|
446
|
+
actor,
|
|
447
|
+
item: request.item,
|
|
448
|
+
progress
|
|
449
|
+
};
|
|
450
|
+
if (request.preview) {
|
|
451
|
+
if (!action.options.preview) return {};
|
|
452
|
+
return { blocks: await upload(await action.options.preview(context)) };
|
|
453
|
+
}
|
|
454
|
+
if (!background) return toResult(await action.options.run(context), upload);
|
|
455
|
+
jobs.defer((async () => {
|
|
456
|
+
try {
|
|
457
|
+
await jobs.report(id, { result: await toResult(await action.options.run(context), upload) });
|
|
458
|
+
} catch (error) {
|
|
459
|
+
await jobs.report(id, { error: error.message });
|
|
460
|
+
}
|
|
461
|
+
})());
|
|
462
|
+
return { running: true };
|
|
463
|
+
}
|
|
464
|
+
async function toResult(value, upload) {
|
|
465
|
+
if (!value) return {};
|
|
466
|
+
if (typeof value === "string") return { message: value };
|
|
467
|
+
return {
|
|
468
|
+
message: value.message,
|
|
469
|
+
blocks: value.blocks && await upload(value.blocks)
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/client/dispatch.ts
|
|
474
|
+
var Dispatcher = class {
|
|
475
|
+
project;
|
|
476
|
+
upload;
|
|
477
|
+
report;
|
|
478
|
+
#definitions = null;
|
|
479
|
+
constructor(project, upload, report) {
|
|
480
|
+
this.project = project;
|
|
481
|
+
this.upload = upload;
|
|
482
|
+
this.report = report;
|
|
483
|
+
}
|
|
484
|
+
definitions() {
|
|
485
|
+
this.#definitions ??= Promise.all(Object.values(this.project.modules).map((load) => load())).then(collect);
|
|
486
|
+
return this.#definitions;
|
|
487
|
+
}
|
|
488
|
+
async handle(envelope, defer = () => {}) {
|
|
489
|
+
const { request } = envelope;
|
|
490
|
+
const definitions = await this.definitions();
|
|
491
|
+
switch (request.kind) {
|
|
492
|
+
case "ping": return null;
|
|
493
|
+
case "action": return runAction(definitions, this.project.manifest, envelope, this.upload, {
|
|
494
|
+
report: this.report,
|
|
495
|
+
defer
|
|
496
|
+
});
|
|
497
|
+
case "find": return find(definitions, request);
|
|
498
|
+
case "get": return get(definitions, request, this.upload);
|
|
499
|
+
case "list": return list(definitions, request);
|
|
500
|
+
case "related": return related(definitions, request);
|
|
501
|
+
case "choices": return choices(definitions, request);
|
|
502
|
+
case "titles": return titles(definitions, request);
|
|
503
|
+
case "available": return available(definitions, request);
|
|
504
|
+
case "page": return this.#page(definitions, envelope);
|
|
505
|
+
case "render": return renderItem(this.#inbox(definitions, request.inbox), request, this.upload);
|
|
506
|
+
case "triage": return this.#triage(definitions, request);
|
|
507
|
+
case "replay": return this.#replay(definitions, request);
|
|
508
|
+
case "state": return this.#state(definitions, envelope);
|
|
509
|
+
case "reply": return this.#reply(definitions, envelope);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async #page(definitions, { request, actor }) {
|
|
513
|
+
const page = definitions.pages.get(request.page);
|
|
514
|
+
if (!page) throw new Error(`Unknown page "${request.page}"`);
|
|
515
|
+
const { params, render } = page.options;
|
|
516
|
+
const parsed = params ? await validate(params, request.params) : {};
|
|
517
|
+
const blocks = typeof render === "function" ? await render({
|
|
518
|
+
params: parsed,
|
|
519
|
+
actor
|
|
520
|
+
}) : render;
|
|
521
|
+
return settle(definitions, await this.upload(blocks), null);
|
|
522
|
+
}
|
|
523
|
+
#inbox(definitions, name) {
|
|
524
|
+
const inbox = definitions.inboxes.get(name);
|
|
525
|
+
if (!inbox) throw new Error(`Unknown inbox "${name}"`);
|
|
526
|
+
return inbox;
|
|
527
|
+
}
|
|
528
|
+
async #state(definitions, { request, actor }) {
|
|
529
|
+
const inbox = this.#inbox(definitions, request.inbox);
|
|
530
|
+
const item = {
|
|
531
|
+
id: request.item,
|
|
532
|
+
title: request.title,
|
|
533
|
+
record: request.record,
|
|
534
|
+
data: request.data
|
|
535
|
+
};
|
|
536
|
+
await inbox.options.onState?.({
|
|
537
|
+
item,
|
|
538
|
+
from: request.from,
|
|
539
|
+
to: request.to,
|
|
540
|
+
actor
|
|
541
|
+
});
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
async #reply(definitions, { request, actor }) {
|
|
545
|
+
const reply = this.#inbox(definitions, request.inbox).options.reply;
|
|
546
|
+
if (!reply) throw new Error(`${request.inbox} can't send email`);
|
|
547
|
+
const { item, to, subject, text, thread } = request;
|
|
548
|
+
await reply.send({
|
|
549
|
+
item,
|
|
550
|
+
to,
|
|
551
|
+
subject,
|
|
552
|
+
text,
|
|
553
|
+
thread,
|
|
554
|
+
actor
|
|
555
|
+
});
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
#replay(definitions, request) {
|
|
559
|
+
const rules = this.#inbox(definitions, request.inbox).options.triage?.rules ?? [];
|
|
560
|
+
return request.answers.map((answers) => decide(rules, answers));
|
|
561
|
+
}
|
|
562
|
+
#triage(definitions, request) {
|
|
563
|
+
const inbox = this.#inbox(definitions, request.inbox);
|
|
564
|
+
return inbox.options.triage ? decide(inbox.options.triage.rules, request.answers) : null;
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
async function renderItem(inbox, item, upload) {
|
|
568
|
+
const data = inbox.options.data ? await validate(inbox.options.data, item.data) : item.data;
|
|
569
|
+
const title = item.title || inbox.options.title?.(data) || inbox.options.label || inbox.name;
|
|
570
|
+
const view = liveView(title, data, item.record ?? null);
|
|
571
|
+
const blocks = inbox.options.render ? await inbox.options.render(view) : defaultBlocks(data);
|
|
572
|
+
return {
|
|
573
|
+
title,
|
|
574
|
+
record: item.record ?? void 0,
|
|
575
|
+
data: JSON.parse(JSON.stringify(data)),
|
|
576
|
+
blocks: bindActions(await upload(blocks), inbox.options.resource ?? null, item.record ?? null),
|
|
577
|
+
replyTo: inbox.options.reply?.to(view)
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function defaultBlocks(data) {
|
|
581
|
+
if (!data || typeof data !== "object") return [];
|
|
582
|
+
const fields = Object.entries(data).filter(([, value]) => value !== null && typeof value !== "object").map(([label, value]) => ({
|
|
583
|
+
label,
|
|
584
|
+
value
|
|
585
|
+
}));
|
|
586
|
+
return fields.length ? [{
|
|
587
|
+
type: "fields",
|
|
588
|
+
fields
|
|
589
|
+
}] : [];
|
|
590
|
+
}
|
|
591
|
+
//#endregion
|
|
592
|
+
//#region src/client/flags.ts
|
|
593
|
+
const FRESH_MS = 1e4;
|
|
594
|
+
const EMPTY = {
|
|
595
|
+
version: -1,
|
|
596
|
+
flags: {}
|
|
597
|
+
};
|
|
598
|
+
var Flags = class {
|
|
599
|
+
transport;
|
|
600
|
+
#snapshot = null;
|
|
601
|
+
#checkedAt = 0;
|
|
602
|
+
#live = false;
|
|
603
|
+
#refreshing = null;
|
|
604
|
+
#parsed = /* @__PURE__ */ new Map();
|
|
605
|
+
#reads = /* @__PURE__ */ new Map();
|
|
606
|
+
constructor(transport) {
|
|
607
|
+
this.transport = transport;
|
|
608
|
+
}
|
|
609
|
+
receive(snapshot) {
|
|
610
|
+
this.#live = snapshot !== null;
|
|
611
|
+
if (snapshot) this.#store(snapshot);
|
|
612
|
+
}
|
|
613
|
+
async value(flag, context) {
|
|
614
|
+
this.#reads.set(flag.name, (this.#reads.get(flag.name) ?? 0) + 1);
|
|
615
|
+
const state = (await this.#current()).flags[flag.name];
|
|
616
|
+
const raw = state === void 0 ? void 0 : resolveFlag(flag.name, state, contextOf(context));
|
|
617
|
+
if (raw === void 0 || raw === null) return flag.options.default;
|
|
618
|
+
return this.#validated(flag, raw);
|
|
619
|
+
}
|
|
620
|
+
takeReads() {
|
|
621
|
+
if (!this.#reads.size) return null;
|
|
622
|
+
const reads = Object.fromEntries(this.#reads);
|
|
623
|
+
this.#reads.clear();
|
|
624
|
+
return reads;
|
|
625
|
+
}
|
|
626
|
+
#store(snapshot) {
|
|
627
|
+
if (snapshot.version !== this.#snapshot?.version) this.#parsed.clear();
|
|
628
|
+
this.#snapshot = snapshot;
|
|
629
|
+
this.#checkedAt = Date.now();
|
|
630
|
+
}
|
|
631
|
+
async #current() {
|
|
632
|
+
const fresh = this.#live || Date.now() - this.#checkedAt < FRESH_MS;
|
|
633
|
+
if (this.#snapshot && fresh) return this.#snapshot;
|
|
634
|
+
if (onWorkers) return this.#refresh();
|
|
635
|
+
this.#refreshing ??= this.#refresh().finally(() => {
|
|
636
|
+
this.#refreshing = null;
|
|
637
|
+
});
|
|
638
|
+
return this.#refreshing;
|
|
639
|
+
}
|
|
640
|
+
async #refresh() {
|
|
641
|
+
const since = this.#snapshot?.version ?? -1;
|
|
642
|
+
const reads = this.takeReads();
|
|
643
|
+
const counted = reads ? `&reads=${encodeURIComponent(Object.entries(reads).map(([name, count]) => `${name}:${count}`).join(","))}` : "";
|
|
644
|
+
try {
|
|
645
|
+
const answer = await this.transport.request("GET", `/v1/flags?since=${since}${counted}`);
|
|
646
|
+
if ("unchanged" in answer) this.#checkedAt = Date.now();
|
|
647
|
+
else this.#store(answer);
|
|
648
|
+
} catch {
|
|
649
|
+
this.#checkedAt = Date.now();
|
|
650
|
+
}
|
|
651
|
+
return this.#snapshot ?? EMPTY;
|
|
652
|
+
}
|
|
653
|
+
#validated(flag, raw) {
|
|
654
|
+
const key = `${flag.name}\u0000${JSON.stringify(raw)}`;
|
|
655
|
+
let parsed = this.#parsed.get(key);
|
|
656
|
+
if (!parsed) {
|
|
657
|
+
const schema = flag.options.value;
|
|
658
|
+
parsed = schema ? validate(schema, raw).catch(() => flag.options.default) : Promise.resolve(typeof raw === "boolean" ? raw : flag.options.default);
|
|
659
|
+
this.#parsed.set(key, parsed);
|
|
660
|
+
}
|
|
661
|
+
return parsed;
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
function contextOf(context) {
|
|
665
|
+
if (context === void 0 || typeof context === "string") return { key: context };
|
|
666
|
+
const { key, ...attributes } = context;
|
|
667
|
+
return {
|
|
668
|
+
key,
|
|
669
|
+
attributes
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region src/client/socket.ts
|
|
674
|
+
const HEARTBEAT_MS = 25e3;
|
|
675
|
+
const MAX_BACKOFF_MS = 3e4;
|
|
676
|
+
function connect({ transport, version, onRequest, onSyncManifest, onFlags, reads, onStatus }) {
|
|
677
|
+
const nonces = new NonceWindow();
|
|
678
|
+
let socket = null;
|
|
679
|
+
let heartbeat;
|
|
680
|
+
let attempt = 0;
|
|
681
|
+
let closed = false;
|
|
682
|
+
const send = (message) => socket?.send(JSON.stringify(message));
|
|
683
|
+
const respond = async (response) => {
|
|
684
|
+
const body = JSON.stringify(response);
|
|
685
|
+
send({
|
|
686
|
+
type: "response",
|
|
687
|
+
signature: await sign(transport.key.secret, "response", body),
|
|
688
|
+
body
|
|
689
|
+
});
|
|
690
|
+
};
|
|
691
|
+
const handle = async (message) => {
|
|
692
|
+
if (message.type === "welcome") {
|
|
693
|
+
attempt = 0;
|
|
694
|
+
onStatus?.("connected");
|
|
695
|
+
const manifest = message.syncManifest ? onSyncManifest(message.environment) : null;
|
|
696
|
+
if (manifest) send(manifest);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (message.type === "flags") {
|
|
700
|
+
onFlags(message.snapshot);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (await verify(transport.key.secret, "request", message.body, message.signature) || !nonces.claim(message.signature.nonce)) return;
|
|
704
|
+
const envelope = JSON.parse(message.body);
|
|
705
|
+
try {
|
|
706
|
+
await respond({
|
|
707
|
+
id: envelope.id,
|
|
708
|
+
ok: true,
|
|
709
|
+
result: await onRequest(envelope) ?? null
|
|
710
|
+
});
|
|
711
|
+
} catch (error) {
|
|
712
|
+
await respond({
|
|
713
|
+
id: envelope.id,
|
|
714
|
+
ok: false,
|
|
715
|
+
error: { message: error.message }
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
const open = async () => {
|
|
720
|
+
const signature = await sign(transport.key.secret, "connect", version);
|
|
721
|
+
const params = new URLSearchParams({
|
|
722
|
+
environment: transport.key.environment,
|
|
723
|
+
version,
|
|
724
|
+
...signature
|
|
725
|
+
});
|
|
726
|
+
socket = new WebSocket(`${transport.url.replace(/^http/, "ws")}/v1/connect?${params}`);
|
|
727
|
+
socket.addEventListener("message", (event) => {
|
|
728
|
+
if (event.data !== "pong") handle(JSON.parse(String(event.data)));
|
|
729
|
+
});
|
|
730
|
+
socket.addEventListener("open", () => {
|
|
731
|
+
heartbeat = setInterval(() => {
|
|
732
|
+
socket?.send("ping");
|
|
733
|
+
const counted = reads();
|
|
734
|
+
if (counted) send({
|
|
735
|
+
type: "reads",
|
|
736
|
+
reads: counted
|
|
737
|
+
});
|
|
738
|
+
}, HEARTBEAT_MS);
|
|
739
|
+
});
|
|
740
|
+
socket.addEventListener("close", () => {
|
|
741
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
742
|
+
onFlags(null);
|
|
743
|
+
onStatus?.("disconnected");
|
|
744
|
+
if (closed) return;
|
|
745
|
+
const delay = Math.min(MAX_BACKOFF_MS, 500 * 2 ** attempt++) * (.5 + Math.random() / 2);
|
|
746
|
+
setTimeout(open, delay);
|
|
747
|
+
});
|
|
748
|
+
};
|
|
749
|
+
open();
|
|
750
|
+
return { close() {
|
|
751
|
+
closed = true;
|
|
752
|
+
socket?.close();
|
|
753
|
+
} };
|
|
754
|
+
}
|
|
755
|
+
//#endregion
|
|
756
|
+
//#region src/client/pult.ts
|
|
757
|
+
function createPult({ key = env("PULT_KEY"), project, url = env("PULT_URL") ?? "https://app.pult.sh", fetch = globalThis.fetch }) {
|
|
758
|
+
if (!key) throw new Error("Pult needs a key: pass `key` or set PULT_KEY");
|
|
759
|
+
const transport = new Transport(key, url, (input, init) => fetch(input, init));
|
|
760
|
+
const upload = async (blocks) => Promise.all(blocks.map(async (block) => {
|
|
761
|
+
if (!isPendingAttachment(block)) return block;
|
|
762
|
+
const data = block.data instanceof Blob ? block.data : new Uint8Array(block.data);
|
|
763
|
+
const { key } = await transport.upload(block.name, block.contentType, data);
|
|
764
|
+
return {
|
|
765
|
+
type: "attachment",
|
|
766
|
+
name: block.name,
|
|
767
|
+
key,
|
|
768
|
+
contentType: block.contentType,
|
|
769
|
+
size: byteLength(data)
|
|
770
|
+
};
|
|
771
|
+
}));
|
|
772
|
+
const report = async (job, update) => {
|
|
773
|
+
await transport.request("POST", `/v1/jobs/${job}`, update);
|
|
774
|
+
};
|
|
775
|
+
const dispatcher = new Dispatcher(project, upload, report);
|
|
776
|
+
const { manifest } = project;
|
|
777
|
+
const deploy = () => transport.request("POST", "/v1/manifest", manifest);
|
|
778
|
+
const sync = () => transport.request("POST", "/v1/manifest/sync", manifest);
|
|
779
|
+
const batcher = new Batcher(transport, sync);
|
|
780
|
+
const flags = new Flags(transport);
|
|
781
|
+
async function send(inbox, item) {
|
|
782
|
+
const rendered = await renderItem(inbox, item, upload);
|
|
783
|
+
return batcher.send({
|
|
784
|
+
inbox: inbox.name,
|
|
785
|
+
fingerprint: item.fingerprint,
|
|
786
|
+
tags: item.tags,
|
|
787
|
+
suggest: item.suggest?.map(({ action, ...rest }) => ({
|
|
788
|
+
action: action.id,
|
|
789
|
+
...rest
|
|
790
|
+
})),
|
|
791
|
+
version: manifest.version,
|
|
792
|
+
...rendered
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
async function log(resource, id, text, options = {}) {
|
|
796
|
+
await transport.request("POST", "/v1/history", {
|
|
797
|
+
resource: resource.name,
|
|
798
|
+
id,
|
|
799
|
+
text,
|
|
800
|
+
by: options.by,
|
|
801
|
+
blocks: options.blocks && await upload(options.blocks)
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
async function receive(message) {
|
|
805
|
+
try {
|
|
806
|
+
return await transport.request("POST", "/v1/messages", message);
|
|
807
|
+
} catch (error) {
|
|
808
|
+
if (error instanceof PultError && error.code === "no_thread") return null;
|
|
809
|
+
throw error;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function listen(options = {}) {
|
|
813
|
+
return connect({
|
|
814
|
+
transport,
|
|
815
|
+
version: manifest.version,
|
|
816
|
+
onRequest: (envelope) => dispatcher.handle(envelope),
|
|
817
|
+
onSyncManifest: (environment) => environment === manifest.sync ? {
|
|
818
|
+
type: "manifest",
|
|
819
|
+
manifest
|
|
820
|
+
} : null,
|
|
821
|
+
onFlags: (snapshot) => flags.receive(snapshot),
|
|
822
|
+
reads: () => flags.takeReads(),
|
|
823
|
+
onStatus: options.onStatus
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
function handler() {
|
|
827
|
+
const nonces = new NonceWindow();
|
|
828
|
+
return async (request, context) => {
|
|
829
|
+
const body = await request.text();
|
|
830
|
+
const signature = signedHeaders(request.headers);
|
|
831
|
+
const failure = signature && await verify(transport.key.secret, "request", body, signature);
|
|
832
|
+
if (!signature || failure || !nonces.claim(signature.nonce)) return Response.json({ error: "invalid signature" }, { status: 401 });
|
|
833
|
+
const envelope = JSON.parse(body);
|
|
834
|
+
let response;
|
|
835
|
+
try {
|
|
836
|
+
const result = await dispatcher.handle(envelope, (work) => context?.waitUntil(work));
|
|
837
|
+
response = {
|
|
838
|
+
id: envelope.id,
|
|
839
|
+
ok: true,
|
|
840
|
+
result: result ?? null
|
|
841
|
+
};
|
|
842
|
+
} catch (error) {
|
|
843
|
+
response = {
|
|
844
|
+
id: envelope.id,
|
|
845
|
+
ok: false,
|
|
846
|
+
error: { message: error.message }
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
const payload = JSON.stringify(response);
|
|
850
|
+
const signed = await sign(transport.key.secret, "response", payload);
|
|
851
|
+
return new Response(payload, { headers: {
|
|
852
|
+
"content-type": "application/json",
|
|
853
|
+
[HEADER.timestamp]: signed.timestamp,
|
|
854
|
+
[HEADER.nonce]: signed.nonce,
|
|
855
|
+
[HEADER.signature]: signed.signature
|
|
856
|
+
} });
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
const status = { async set(component, state) {
|
|
860
|
+
if (!manifest.status?.components[component]) throw new Error(`Your status page has no component "${component}"`);
|
|
861
|
+
await transport.request("POST", "/v1/status", {
|
|
862
|
+
component,
|
|
863
|
+
state
|
|
864
|
+
});
|
|
865
|
+
} };
|
|
866
|
+
function flag(definition, context) {
|
|
867
|
+
return flags.value(definition, context);
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
send,
|
|
871
|
+
log,
|
|
872
|
+
receive,
|
|
873
|
+
flag,
|
|
874
|
+
status,
|
|
875
|
+
listen,
|
|
876
|
+
handler,
|
|
877
|
+
deploy
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function signedHeaders(headers) {
|
|
881
|
+
const timestamp = headers.get(HEADER.timestamp);
|
|
882
|
+
const nonce = headers.get(HEADER.nonce);
|
|
883
|
+
const signature = headers.get(HEADER.signature);
|
|
884
|
+
return timestamp && nonce && signature ? {
|
|
885
|
+
timestamp,
|
|
886
|
+
nonce,
|
|
887
|
+
signature
|
|
888
|
+
} : null;
|
|
889
|
+
}
|
|
890
|
+
function byteLength(data) {
|
|
891
|
+
return data instanceof Blob ? data.size : data.byteLength;
|
|
892
|
+
}
|
|
893
|
+
function env(name) {
|
|
894
|
+
return globalThis.process?.env[name];
|
|
895
|
+
}
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/define/config.ts
|
|
898
|
+
function defineConfig(options) {
|
|
899
|
+
const environments = options.environments ?? [
|
|
900
|
+
"dev",
|
|
901
|
+
"staging",
|
|
902
|
+
"prod"
|
|
903
|
+
];
|
|
904
|
+
const sync = options.sync ?? environments[0];
|
|
905
|
+
if (!environments.includes(sync)) throw new Error(`sync is "${sync}", which isn't one of your environments (${environments.join(", ")})`);
|
|
906
|
+
return {
|
|
907
|
+
[KIND]: "config",
|
|
908
|
+
origin: captureOrigin(),
|
|
909
|
+
options: {
|
|
910
|
+
delivery: { mode: "socket" },
|
|
911
|
+
...options,
|
|
912
|
+
environments,
|
|
913
|
+
sync
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region src/define/notify.ts
|
|
919
|
+
function slack(channel) {
|
|
920
|
+
return { channel: channel.replace(/^#/, "") };
|
|
921
|
+
}
|
|
922
|
+
function notify(name, options) {
|
|
923
|
+
return {
|
|
924
|
+
[KIND]: "notify",
|
|
925
|
+
origin: captureOrigin(),
|
|
926
|
+
name,
|
|
927
|
+
options
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region src/define/role.ts
|
|
932
|
+
function role(name, options) {
|
|
933
|
+
return {
|
|
934
|
+
[KIND]: "role",
|
|
935
|
+
origin: captureOrigin(),
|
|
936
|
+
name,
|
|
937
|
+
options
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
//#endregion
|
|
941
|
+
//#region src/define/status.ts
|
|
942
|
+
function status(options) {
|
|
943
|
+
return {
|
|
944
|
+
[KIND]: "status",
|
|
945
|
+
origin: captureOrigin(),
|
|
946
|
+
options
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
//#endregion
|
|
950
|
+
export { PultError, ValidationError, action, b, choice, createPult, defineConfig, flag, home, inbox, notify, noul, page, resource, role, run, score, setState, slack, status, tag, triage, view };
|