@vercel/board 0.27.0 → 0.29.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/dist/{a2-B8P8hjOT.mjs → a2-HUbQJ0RD.mjs} +16 -192
- package/dist/access.d.mts +1 -1
- package/dist/access.mjs +3 -3
- package/dist/{actors-BLXVp4oZ.mjs → actors-DcJhFbwJ.mjs} +2 -2
- package/dist/{auth-lTkOgvcE.mjs → auth-JO22iIwr.mjs} +1 -1
- package/dist/{backend-ZKYTe3xa.d.mts → backend-CPC_vyOo.d.mts} +5 -5
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +1 -1
- package/dist/backend-a2/contract.mjs +3 -3
- package/dist/backend-a2/store-memory.d.mts +1 -1
- package/dist/backend-a2/store-memory.mjs +2 -2
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +1 -1
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +1 -1
- package/dist/backend-a2/store-redis.d.mts +1 -1
- package/dist/backend-a2/store-redis.mjs +1 -1
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +2 -2
- package/dist/backend-a2.d.mts +8 -5
- package/dist/backend-a2.mjs +2 -1
- package/dist/{backend-http-nppEk8gC.mjs → backend-http-Bmp0IgD3.mjs} +2 -2
- package/dist/{backend-http-E7YU0qhn.d.mts → backend-http-CqfKhrnn.d.mts} +5 -3
- package/dist/{backend-http-DEMvX2ht.mjs → backend-http-CzIJMm58.mjs} +3 -3
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{backend-vercel-CIRObA0t.d.mts → backend-vercel-4tvcPKqX.d.mts} +7 -3
- package/dist/{backend-vercel-K5rDrMxZ.mjs → backend-vercel-BNauAs7w.mjs} +68 -10
- package/dist/{backend-vercel-CnViBZYT.mjs → backend-vercel-DQc_TaHp.mjs} +5 -3
- package/dist/backend-vercel.d.mts +1 -1
- package/dist/backend-vercel.mjs +1 -1
- package/dist/backend-vercel.node.d.mts +1 -1
- package/dist/backend-vercel.node.mjs +1 -1
- package/dist/blob-DdbpJqN1.mjs +182 -0
- package/dist/{blob-B-9AvYis.d.mts → blob-DmiQjrYy.d.mts} +2 -2
- package/dist/{chat-provider-DsNEBQiA.d.mts → chat-provider-BWHbChcQ.d.mts} +5 -3
- package/dist/{chat-provider-DnHibqnM.mjs → chat-provider-COtsU5jB.mjs} +2 -2
- package/dist/cli-commands.mjs +1 -1
- package/dist/cli-embedded.d.mts +5 -3
- package/dist/cli-embedded.mjs +6 -4
- package/dist/cli.mjs +5 -5
- package/dist/{client-xLxeDz0f.d.mts → client-BqAjk0Q4.d.mts} +8 -1
- package/dist/{commands-BFookFtn.mjs → commands-C3R1AWgm.mjs} +4 -1
- package/dist/conformance.d.mts +1 -1
- package/dist/{engine-Bfe6pcXb.mjs → engine-DQxXk7dX.mjs} +20 -12
- package/dist/{errors-CB4tnZGz.d.mts → errors-DDktVeUa.d.mts} +1 -1
- package/dist/{events-CewD4kw7.mjs → events-qf5XG5VQ.mjs} +2 -2
- package/dist/{exports-uiRQD2-i.mjs → exports-BG-0Wyn-.mjs} +9 -9
- package/dist/exports-mhAYiU4i.d.mts +5 -0
- package/dist/{external-BV_cUncs.mjs → external-D3p5T57R.mjs} +4 -2
- package/dist/{folds-Dyjput8M.mjs → folds-BDMSxBUf.mjs} +2 -2
- package/dist/{folds-jS9wlh9_.d.mts → folds-ZkHM4M-v.d.mts} +2 -0
- package/dist/{index-C05it0mC.d.mts → index-D3VvNfh_.d.mts} +2 -2
- package/dist/index.d.mts +5 -5
- package/dist/{index.default-RaGMBzNu.mjs → index.default-rB8sJ9RX.mjs} +1 -1
- package/dist/index.mjs +4 -4
- package/dist/index.node.d.mts +5 -5
- package/dist/index.node.mjs +2 -2
- package/dist/inspect/app.js +3 -3
- package/dist/inspect/engine.js +5 -5
- package/dist/{main-CRHQQFgN.mjs → main-CVBqe3VO.mjs} +14 -9
- package/dist/{middleware-ChKM1zuu.mjs → middleware-CXjEeef4.mjs} +1 -1
- package/dist/{node-Ybi9E6bO.mjs → node-Zd_C-gxP.mjs} +34 -17
- package/dist/{project-DMDST9kl.mjs → project-CBXlLftU.mjs} +1 -1
- package/dist/{provider-poIkXapd.mjs → provider-BaVmi4Hi.mjs} +3 -1
- package/dist/{provider-Cdg1qadh.d.mts → provider-DmSTp9lI.d.mts} +9 -2
- package/dist/{redis-tables-DN9PfIDi.mjs → redis-tables-BJQVUspu.mjs} +2 -2
- package/dist/{reducers-BcmSs4qf.mjs → reducers-DJzVpXwM.mjs} +3 -3
- package/dist/{schemas-Be08nyfW.mjs → schemas-DBmUO6pr.mjs} +2 -1
- package/dist/{serve-BsbHOJFN.mjs → serve-G9RuaDSr.mjs} +5 -4
- package/dist/{server-C0DVYiHm.mjs → server-BixV_byP.mjs} +4 -4
- package/dist/{serving-CvOWEym2.mjs → serving-B75Y4BF-.mjs} +1 -1
- package/dist/{sql-U9WTxrwJ.mjs → sql-D4Z_efJX.mjs} +2 -2
- package/dist/{sqlite-db-Ds4majtT.mjs → sqlite-db-Bz2JkfCU.mjs} +1 -1
- package/dist/sync-github.d.mts +4 -2
- package/dist/sync-github.mjs +4 -14
- package/dist/sync-kapso.d.mts +3 -2
- package/dist/sync-kapso.mjs +3 -3
- package/dist/sync-linear.d.mts +12 -2
- package/dist/sync-linear.mjs +189 -9
- package/dist/sync-slack.d.mts +41 -5
- package/dist/sync-slack.mjs +70 -11
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.chat.mjs +1 -1
- package/dist/sync.d.mts +2 -2
- package/dist/sync.mjs +3 -3
- package/dist/tags-BpFo6q0_.mjs +14 -0
- package/dist/{targets-C-wdR5wP.mjs → targets-Cgjs29T0.mjs} +1 -1
- package/dist/{tokens-BuCU-61R.mjs → tokens-CC3izSNr.mjs} +1 -1
- package/dist/{types-aCAf6lGU.d.mts → types-No0WdhE5.d.mts} +1 -1
- package/dist/{watchers-hash-qSMYlljN.mjs → watchers-hash-BDhLVkV5.mjs} +7 -5
- package/docs/02-sync/03-slack.md +6 -2
- package/docs/02-sync/07-linear.md +28 -6
- package/docs/04-reference/01-cli.md +7 -2
- package/package.json +1 -1
- package/dist/exports-D22M68rk.d.mts +0 -5
package/dist/sync-linear.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { i as UnreadableError, s as channels } from "./provider-
|
|
2
|
-
import { i as eventSchema, r as envelope } from "./events-
|
|
3
|
-
import { r as markdownOf, t as ChatSyncProvider } from "./chat-provider-
|
|
1
|
+
import { i as UnreadableError, s as channels } from "./provider-BaVmi4Hi.mjs";
|
|
2
|
+
import { i as eventSchema, r as envelope } from "./events-qf5XG5VQ.mjs";
|
|
3
|
+
import { r as markdownOf, t as ChatSyncProvider } from "./chat-provider-COtsU5jB.mjs";
|
|
4
|
+
import { n as tagOf, t as slug } from "./tags-BpFo6q0_.mjs";
|
|
4
5
|
import "zod";
|
|
5
6
|
import { connectLinearAdapter } from "@vercel/connect/chat";
|
|
6
7
|
import { createLinearAdapter } from "@chat-adapter/linear";
|
|
@@ -30,6 +31,41 @@ const author = (user, isBot) => ({
|
|
|
30
31
|
});
|
|
31
32
|
/** `ENG` from `ENG-123`, or from a Linear URL that names an issue (`…/issue/ENG-123/…`). */
|
|
32
33
|
const teamOf = (identifierOrUrl) => /(?:^|\/issue\/)([A-Z0-9_]+)-\d+/i.exec(identifierOrUrl ?? "")?.[1]?.toUpperCase() ?? null;
|
|
34
|
+
/** An issue's description, or null when it has none. */
|
|
35
|
+
const bodyOf = (description) => description?.trim() || null;
|
|
36
|
+
/**
|
|
37
|
+
* An issue's properties as board labels: `priority=high`, `project=v0-accessibility`, a label per
|
|
38
|
+
* Linear label (`tag.bug`), and its assignee (`assignee.ada`). Only what the payload says: a
|
|
39
|
+
* property it carries empty is unset, and one it names without its details is left as it was.
|
|
40
|
+
* `priority` and `project` are single keys, so they're unset by name; `tag.` and `assignee.` are
|
|
41
|
+
* prefixes the mirror owns, so a key under them that Linear doesn't have goes.
|
|
42
|
+
*/
|
|
43
|
+
function propertiesOf(data) {
|
|
44
|
+
const labels = {};
|
|
45
|
+
const unset = [];
|
|
46
|
+
const owns = [];
|
|
47
|
+
if (data.priority === 0 || data.priorityLabel === "No priority") unset.push("priority");
|
|
48
|
+
else if (data.priorityLabel && slug(data.priorityLabel)) labels.priority = slug(data.priorityLabel).slice(0, 256);
|
|
49
|
+
if (data.project?.name && slug(data.project.name)) labels.project = slug(data.project.name).slice(0, 256);
|
|
50
|
+
else if (data.project === null || data.projectId === null) unset.push("project");
|
|
51
|
+
if (Array.isArray(data.labels)) {
|
|
52
|
+
owns.push("tag.");
|
|
53
|
+
for (const label of data.labels) {
|
|
54
|
+
const tag = tagOf("tag", label.name);
|
|
55
|
+
if (tag) labels[tag[0]] = tag[1];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (data.assignee) {
|
|
59
|
+
owns.push("assignee.");
|
|
60
|
+
const tag = tagOf("assignee", handleOf(data.assignee));
|
|
61
|
+
if (tag) labels[tag[0]] = tag[1];
|
|
62
|
+
} else if (data.assignee === null || data.assigneeId === null) owns.push("assignee.");
|
|
63
|
+
return {
|
|
64
|
+
labels,
|
|
65
|
+
unset,
|
|
66
|
+
owns
|
|
67
|
+
};
|
|
68
|
+
}
|
|
33
69
|
/** The copy of an issue the board opened: the issue itself, not a comment. */
|
|
34
70
|
const OPENED = "issue";
|
|
35
71
|
/** A root's copy: one the board opened, or imported (`lifecycle:<event>`, `root:<event>`). Comment IDs are UUIDs. */
|
|
@@ -52,6 +88,51 @@ function titleOf(markdown) {
|
|
|
52
88
|
function stateOf(state) {
|
|
53
89
|
return state?.type ?? state?.name?.toLowerCase() ?? null;
|
|
54
90
|
}
|
|
91
|
+
const ISSUE_FIELDS = `id identifier title description url createdAt priority priorityLabel
|
|
92
|
+
state { name } assignee { name displayName url } creator { name displayName url } project { name }
|
|
93
|
+
labels(first: 10) { nodes { name } }`;
|
|
94
|
+
const ISSUES_PER_PAGE = 100;
|
|
95
|
+
const DESCRIPTION_IN_LIST = 1500;
|
|
96
|
+
const MAX_COMMENTS = 1e3;
|
|
97
|
+
/** What a read names: a project (by its link), a team (by its key), or an issue (by its ID or link). */
|
|
98
|
+
function readTargetOf(target) {
|
|
99
|
+
const project = /\/project\/(?:[^/?#]*-)?([0-9a-f]{8,})(?:[/?#]|$)/i.exec(target);
|
|
100
|
+
if (project) return {
|
|
101
|
+
kind: "project",
|
|
102
|
+
slugId: project[1]
|
|
103
|
+
};
|
|
104
|
+
const issue = /(?:^|\/issue\/)([A-Z0-9_]+-\d+)(?:[/?#]|$)/i.exec(target.trim());
|
|
105
|
+
if (issue) return {
|
|
106
|
+
kind: "issue",
|
|
107
|
+
id: issue[1].toUpperCase()
|
|
108
|
+
};
|
|
109
|
+
if (/^[A-Z0-9_]+$/i.test(target.trim())) return {
|
|
110
|
+
kind: "team",
|
|
111
|
+
key: target.trim().toUpperCase()
|
|
112
|
+
};
|
|
113
|
+
throw new UnreadableError("invalid_target", `can't read ${target} from Linear: name a project by its link, a team by its key (ENG), or an issue (ENG-123)`);
|
|
114
|
+
}
|
|
115
|
+
/** An issue as a read shows it: its title, its properties on one line, its link, and what it says. */
|
|
116
|
+
function issueMessage(issue, descriptionMax) {
|
|
117
|
+
const properties = [
|
|
118
|
+
issue.state?.name,
|
|
119
|
+
issue.priority ? issue.priorityLabel : void 0,
|
|
120
|
+
issue.assignee ? `assigned to ${handleOf(issue.assignee)}` : "unassigned",
|
|
121
|
+
issue.project?.name,
|
|
122
|
+
...(issue.labels?.nodes ?? []).map((label) => label.name)
|
|
123
|
+
].filter(Boolean);
|
|
124
|
+
const description = bodyOf(issue.description);
|
|
125
|
+
const shown = description && descriptionMax !== null && description.length > descriptionMax ? `${description.slice(0, descriptionMax)}…` : description;
|
|
126
|
+
return {
|
|
127
|
+
id: issue.identifier,
|
|
128
|
+
thread: issue.identifier,
|
|
129
|
+
author: handleOf(issue.creator ?? void 0) ?? "unknown",
|
|
130
|
+
text: [`${issue.identifier}: ${issue.title}\n${properties.join(" · ")}\n${issue.url}`, shown].filter(Boolean).join("\n\n"),
|
|
131
|
+
createdAt: issue.createdAt,
|
|
132
|
+
replies: issue.comments?.nodes.length ?? 0,
|
|
133
|
+
files: []
|
|
134
|
+
};
|
|
135
|
+
}
|
|
55
136
|
var LinearProvider = class extends ChatSyncProvider {
|
|
56
137
|
linear;
|
|
57
138
|
teamIds = /* @__PURE__ */ new Map();
|
|
@@ -133,13 +214,14 @@ var LinearProvider = class extends ChatSyncProvider {
|
|
|
133
214
|
const { data } = payload;
|
|
134
215
|
const created = payload.action === "create";
|
|
135
216
|
if (!created && payload.action !== "update") return [];
|
|
136
|
-
const retitled = payload.action === "update" && payload.updatedFrom?.title !== void 0;
|
|
217
|
+
const retitled = payload.action === "update" && (payload.updatedFrom?.title !== void 0 || payload.updatedFrom?.description !== void 0);
|
|
137
218
|
const team = data.team?.key ?? teamOf(data.identifier);
|
|
138
219
|
const state = stateOf(data.state);
|
|
139
220
|
const bot = adapter.defaultBotUserId;
|
|
140
221
|
const own = payload.actor?.id !== void 0 && payload.actor.id === bot;
|
|
141
222
|
const opens = created ? !own : !own && (bot === null || data.creatorId !== bot) && payload.actor?.type === "user" && !data.archivedAt;
|
|
142
223
|
const delivery = headers.get("linear-delivery") ?? `${payload.webhookId}:${payload.webhookTimestamp}`;
|
|
224
|
+
const properties = propertiesOf(data);
|
|
143
225
|
return [{
|
|
144
226
|
event: {
|
|
145
227
|
kind: "lifecycle",
|
|
@@ -148,11 +230,14 @@ var LinearProvider = class extends ChatSyncProvider {
|
|
|
148
230
|
type: created ? "issue.created" : "issue.updated",
|
|
149
231
|
title: `${data.identifier}: ${data.title}`,
|
|
150
232
|
url: data.url ?? payload.url ?? null,
|
|
233
|
+
body: bodyOf(data.description),
|
|
151
234
|
labels: {
|
|
152
235
|
issue: data.identifier,
|
|
153
|
-
...state ? { state } : {}
|
|
236
|
+
...state ? { state } : {},
|
|
237
|
+
...properties.labels
|
|
154
238
|
},
|
|
155
|
-
unset:
|
|
239
|
+
unset: properties.unset,
|
|
240
|
+
owns: properties.owns,
|
|
156
241
|
reply: null,
|
|
157
242
|
createRoot: opens,
|
|
158
243
|
...retitled ? { retitled: true } : {},
|
|
@@ -173,6 +258,84 @@ var LinearProvider = class extends ChatSyncProvider {
|
|
|
173
258
|
await this.ready();
|
|
174
259
|
return this.linear.isConnectMode() ? this.linear.withConnectClient(void 0, fn) : fn();
|
|
175
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* A project's or team's issues, newest first, or one issue and its comments, read on demand:
|
|
263
|
+
* nothing is synced or stored. `next` pages past a read's `limit`.
|
|
264
|
+
*/
|
|
265
|
+
async read(target, { limit, after }) {
|
|
266
|
+
const wanted = readTargetOf(target);
|
|
267
|
+
const query = (text, variables) => this.api(async () => (await this.client().client.rawRequest(text, variables)).data);
|
|
268
|
+
if (wanted.kind === "issue") {
|
|
269
|
+
let issue = null;
|
|
270
|
+
const comments = [];
|
|
271
|
+
let cursor = null;
|
|
272
|
+
do {
|
|
273
|
+
const data = await query(`query BoardReadIssue($id: String!, $first: Int!, $after: String) { issue(id: $id) { ${ISSUE_FIELDS}
|
|
274
|
+
comments(first: $first, after: $after) { nodes { id body createdAt user { name displayName url } } pageInfo { hasNextPage endCursor } } } }`, {
|
|
275
|
+
id: wanted.id,
|
|
276
|
+
first: ISSUES_PER_PAGE,
|
|
277
|
+
after: cursor
|
|
278
|
+
});
|
|
279
|
+
if (!data?.issue) throw new UnreadableError("channel_not_found", `no Linear issue ${wanted.id}`);
|
|
280
|
+
issue = data.issue;
|
|
281
|
+
comments.push(...issue.comments?.nodes ?? []);
|
|
282
|
+
const page = issue.comments?.pageInfo;
|
|
283
|
+
cursor = page?.hasNextPage ? page.endCursor ?? null : null;
|
|
284
|
+
} while (cursor !== null && comments.length < MAX_COMMENTS);
|
|
285
|
+
comments.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
286
|
+
const from = after === null ? 0 : Math.max(0, Number.parseInt(after, 10) || 0);
|
|
287
|
+
const shown = comments.slice(from, from + Math.min(limit, ISSUES_PER_PAGE));
|
|
288
|
+
const to = from + shown.length;
|
|
289
|
+
return {
|
|
290
|
+
channel: teamOf(issue.identifier) ?? wanted.id,
|
|
291
|
+
name: null,
|
|
292
|
+
thread: issue.identifier,
|
|
293
|
+
messages: [...from === 0 ? [{
|
|
294
|
+
...issueMessage(issue, null),
|
|
295
|
+
replies: comments.length
|
|
296
|
+
}] : [], ...shown.map((comment) => ({
|
|
297
|
+
id: comment.id,
|
|
298
|
+
thread: issue.identifier,
|
|
299
|
+
author: handleOf(comment.user ?? void 0) ?? "unknown",
|
|
300
|
+
text: comment.body ?? "",
|
|
301
|
+
createdAt: comment.createdAt,
|
|
302
|
+
replies: 0,
|
|
303
|
+
files: []
|
|
304
|
+
}))],
|
|
305
|
+
more: to < comments.length,
|
|
306
|
+
next: to < comments.length ? String(to) : null
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
let filter;
|
|
310
|
+
let channel;
|
|
311
|
+
let name;
|
|
312
|
+
if (wanted.kind === "project") {
|
|
313
|
+
const project = (await query("query BoardReadProject($slugId: String!) { projects(filter: { slugId: { eq: $slugId } }, first: 1) { nodes { id name url } } }", { slugId: wanted.slugId }))?.projects.nodes[0];
|
|
314
|
+
if (!project) throw new UnreadableError("channel_not_found", `no Linear project ${target}`);
|
|
315
|
+
filter = { project: { id: { eq: project.id } } };
|
|
316
|
+
channel = project.url;
|
|
317
|
+
name = project.name;
|
|
318
|
+
} else {
|
|
319
|
+
await this.team(wanted.key);
|
|
320
|
+
filter = { team: { key: { eq: wanted.key } } };
|
|
321
|
+
channel = wanted.key;
|
|
322
|
+
name = null;
|
|
323
|
+
}
|
|
324
|
+
const issues = (await query(`query BoardReadIssues($filter: IssueFilter, $first: Int!, $after: String) {
|
|
325
|
+
issues(filter: $filter, first: $first, after: $after) { nodes { ${ISSUE_FIELDS} comments(first: 20) { nodes { id createdAt } } } pageInfo { hasNextPage endCursor } } }`, {
|
|
326
|
+
filter,
|
|
327
|
+
first: Math.min(limit, ISSUES_PER_PAGE),
|
|
328
|
+
after
|
|
329
|
+
}))?.issues;
|
|
330
|
+
return {
|
|
331
|
+
channel,
|
|
332
|
+
name,
|
|
333
|
+
thread: null,
|
|
334
|
+
messages: (issues?.nodes ?? []).map((issue) => issueMessage(issue, DESCRIPTION_IN_LIST)),
|
|
335
|
+
more: issues?.pageInfo.hasNextPage ?? false,
|
|
336
|
+
next: issues?.pageInfo.hasNextPage ? issues.pageInfo.endCursor ?? null : null
|
|
337
|
+
};
|
|
338
|
+
}
|
|
176
339
|
async resolveChannel(key) {
|
|
177
340
|
await this.team(key);
|
|
178
341
|
return key.toUpperCase();
|
|
@@ -207,10 +370,26 @@ var LinearProvider = class extends ChatSyncProvider {
|
|
|
207
370
|
async fetchRoot(thread) {
|
|
208
371
|
return this.api(async () => {
|
|
209
372
|
const issue = await this.client().issue(parseThread(thread).issue);
|
|
210
|
-
const [state, creator] = await Promise.all([
|
|
373
|
+
const [state, creator, project, assignee, tags] = await Promise.all([
|
|
374
|
+
issue.state,
|
|
375
|
+
issue.creator,
|
|
376
|
+
issue.project,
|
|
377
|
+
issue.assignee,
|
|
378
|
+
issue.labels?.()
|
|
379
|
+
]);
|
|
211
380
|
const found = stateOf(state);
|
|
381
|
+
const properties = propertiesOf({
|
|
382
|
+
id: issue.id,
|
|
383
|
+
identifier: issue.identifier,
|
|
384
|
+
title: issue.title,
|
|
385
|
+
priority: issue.priority,
|
|
386
|
+
priorityLabel: issue.priorityLabel,
|
|
387
|
+
project,
|
|
388
|
+
assignee,
|
|
389
|
+
labels: tags?.nodes
|
|
390
|
+
});
|
|
212
391
|
return {
|
|
213
|
-
text: `${issue.identifier}: ${issue.title}\n${issue.url}`,
|
|
392
|
+
text: [`${issue.identifier}: ${issue.title}\n${issue.url}`, bodyOf(issue.description)].filter(Boolean).join("\n\n"),
|
|
214
393
|
raw: { issue: {
|
|
215
394
|
id: issue.id,
|
|
216
395
|
identifier: issue.identifier,
|
|
@@ -220,7 +399,8 @@ var LinearProvider = class extends ChatSyncProvider {
|
|
|
220
399
|
author: author(creator, false),
|
|
221
400
|
labels: {
|
|
222
401
|
issue: issue.identifier,
|
|
223
|
-
...found ? { state: found } : {}
|
|
402
|
+
...found ? { state: found } : {},
|
|
403
|
+
...properties.labels
|
|
224
404
|
}
|
|
225
405
|
};
|
|
226
406
|
});
|
package/dist/sync-slack.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { f as ExternalLink } from "./folds-
|
|
2
|
-
import { C as channels, _ as withUserTokens, d as ResolvedTarget, f as SyncContext, h as UserTokens, o as ProviderPost, r as ProviderDelivery, y as SyncFn } from "./provider-
|
|
3
|
-
import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-
|
|
1
|
+
import { f as ExternalLink } from "./folds-ZkHM4M-v.mjs";
|
|
2
|
+
import { C as channels, _ as withUserTokens, d as ResolvedTarget, f as SyncContext, h as UserTokens, o as ProviderPost, r as ProviderDelivery, y as SyncFn } from "./provider-DmSTp9lI.mjs";
|
|
3
|
+
import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-BWHbChcQ.mjs";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { Adapter } from "chat";
|
|
6
6
|
//#region src/sync/slack.d.ts
|
|
@@ -67,6 +67,31 @@ export type SlackClient = {
|
|
|
67
67
|
delete(args: {
|
|
68
68
|
file: string;
|
|
69
69
|
}): Promise<unknown>;
|
|
70
|
+
/** Uploads files and shares them in one message, with `initial_comment` as its text; needs `files:write`. */
|
|
71
|
+
uploadV2?(args: {
|
|
72
|
+
channel_id: string;
|
|
73
|
+
initial_comment: string;
|
|
74
|
+
file_uploads: {
|
|
75
|
+
file: Buffer;
|
|
76
|
+
filename: string;
|
|
77
|
+
}[];
|
|
78
|
+
}): Promise<{
|
|
79
|
+
files?: {
|
|
80
|
+
files?: {
|
|
81
|
+
id?: string;
|
|
82
|
+
}[];
|
|
83
|
+
}[];
|
|
84
|
+
}>;
|
|
85
|
+
/** Where a file was shared (`shares`), to find the message an upload made; needs `files:read`. */
|
|
86
|
+
info?(args: {
|
|
87
|
+
file: string;
|
|
88
|
+
}): Promise<{
|
|
89
|
+
file?: {
|
|
90
|
+
shares?: Partial<Record<'public' | 'private', Record<string, {
|
|
91
|
+
ts?: string;
|
|
92
|
+
}[]>>>;
|
|
93
|
+
};
|
|
94
|
+
}>;
|
|
70
95
|
};
|
|
71
96
|
/** Posts an agent's message under its own name; needs `chat:write.customize` for `username` and `icon_url`. */
|
|
72
97
|
chat?: {
|
|
@@ -296,6 +321,7 @@ export declare const SlackEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
296
321
|
type: z.ZodString;
|
|
297
322
|
title: z.ZodString;
|
|
298
323
|
url: z.ZodNullable<z.ZodString>;
|
|
324
|
+
body: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
299
325
|
labels: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
300
326
|
unset: z.ZodArray<z.ZodString>;
|
|
301
327
|
owns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -339,7 +365,7 @@ export type SlackOptions = {
|
|
|
339
365
|
export declare function slack(app: string | SlackOptions, sync: SyncFn<SlackEvent>): SlackProvider;
|
|
340
366
|
export declare class SlackProvider extends ChatSyncProvider<SlackEvent> {
|
|
341
367
|
private readonly web;
|
|
342
|
-
private
|
|
368
|
+
private scopes;
|
|
343
369
|
constructor(given: SlackOptions, sync: SyncFn<SlackEvent>);
|
|
344
370
|
/**
|
|
345
371
|
* An agent's message goes out under its own name (`clever-lemur via vboard-factory`), and its
|
|
@@ -347,11 +373,21 @@ export declare class SlackProvider extends ChatSyncProvider<SlackEvent> {
|
|
|
347
373
|
* `**name**: ` label dropped. Without `chat:write.customize` it posts as the app, label and all.
|
|
348
374
|
*/
|
|
349
375
|
post(target: string, kind: ResolvedTarget['kind'], body: ProviderPost): Promise<ProviderDelivery>;
|
|
376
|
+
/**
|
|
377
|
+
* A new thread whose root shows its files under its text, as one message: Slack makes
|
|
378
|
+
* that message from an upload's `initial_comment`, and names it only on the file, a
|
|
379
|
+
* moment later. A file upload can't set a username, so it goes out as the app, label and all.
|
|
380
|
+
* Null, before anything is sent, where the client can't, or the app can't read files
|
|
381
|
+
* (`files:read`) to find that message.
|
|
382
|
+
*/
|
|
383
|
+
private postWithFiles;
|
|
350
384
|
/** An edit keeps the label out only where the message went out under the agent's name. */
|
|
351
385
|
edit(thread: string, message: string, body: ProviderPost): Promise<void>;
|
|
352
386
|
private postNamed;
|
|
353
|
-
/** Whether the app may set a message's username and icon (`chat:write.customize`)
|
|
387
|
+
/** Whether the app may set a message's username and icon (`chat:write.customize`). */
|
|
354
388
|
private canCustomize;
|
|
389
|
+
/** Whether the app has a scope, from its scopes, asked once. */
|
|
390
|
+
private hasScope;
|
|
355
391
|
/** Whether a message Slack has went out under `name`. */
|
|
356
392
|
private sentAs;
|
|
357
393
|
/**
|
package/dist/sync-slack.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { m as MESSAGE_TEXT_MAX } from "./schemas-
|
|
2
|
-
import "./external-
|
|
3
|
-
import { i as UnreadableError, s as channels } from "./provider-
|
|
4
|
-
import { i as eventSchema, r as envelope } from "./events-
|
|
1
|
+
import { m as MESSAGE_TEXT_MAX } from "./schemas-DBmUO6pr.mjs";
|
|
2
|
+
import "./external-D3p5T57R.mjs";
|
|
3
|
+
import { i as UnreadableError, s as channels } from "./provider-BaVmi4Hi.mjs";
|
|
4
|
+
import { i as eventSchema, r as envelope } from "./events-qf5XG5VQ.mjs";
|
|
5
5
|
import { r as withUserTokens, t as connectUserTokens } from "./users-yryHKFMo.mjs";
|
|
6
|
-
import { r as markdownOf, t as ChatSyncProvider } from "./chat-provider-
|
|
6
|
+
import { r as markdownOf, t as ChatSyncProvider } from "./chat-provider-COtsU5jB.mjs";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { stringifyMarkdown } from "chat";
|
|
9
9
|
import { createSlackAdapter } from "@chat-adapter/slack";
|
|
@@ -220,6 +220,8 @@ const SlackEvent = eventSchema("slack", "message");
|
|
|
220
220
|
function slack(app, sync) {
|
|
221
221
|
return new SlackProvider(typeof app === "string" ? { adapter: app } : app, sync);
|
|
222
222
|
}
|
|
223
|
+
/** How many times to look for the message an upload made, about 15 seconds in all. */
|
|
224
|
+
const SHARE_POLLS = 10;
|
|
223
225
|
/**
|
|
224
226
|
* Slack's client retries 10 times over about 30 minutes by default, which outlives any function:
|
|
225
227
|
* here a call gets 3 more tries. Each still waits out Slack's `Retry-After` (about a minute at
|
|
@@ -250,7 +252,7 @@ function fromConnector(connector) {
|
|
|
250
252
|
}
|
|
251
253
|
var SlackProvider = class extends ChatSyncProvider {
|
|
252
254
|
web;
|
|
253
|
-
|
|
255
|
+
scopes;
|
|
254
256
|
constructor(given, sync) {
|
|
255
257
|
const connected = typeof given.adapter === "string" ? fromConnector(given.adapter) : null;
|
|
256
258
|
const options = connected ? {
|
|
@@ -386,6 +388,7 @@ var SlackProvider = class extends ChatSyncProvider {
|
|
|
386
388
|
platform: "slack",
|
|
387
389
|
adapter: options.adapter,
|
|
388
390
|
canStartThreads: true,
|
|
391
|
+
startsThreadsWithFiles: true,
|
|
389
392
|
async uploadFiles(thread, files) {
|
|
390
393
|
const sent = await options.adapter.postMessage(thread, {
|
|
391
394
|
markdown: "",
|
|
@@ -681,8 +684,60 @@ var SlackProvider = class extends ChatSyncProvider {
|
|
|
681
684
|
...given,
|
|
682
685
|
markdown: withoutHtml(given.markdown)
|
|
683
686
|
};
|
|
687
|
+
if (kind === "channel" && post.files?.length && post.markdown.trim()) {
|
|
688
|
+
const sent = await this.postWithFiles(target, post.markdown, post.files);
|
|
689
|
+
if (sent) return sent;
|
|
690
|
+
const { files: _files, ...text } = post;
|
|
691
|
+
return (as ? await this.postNamed(target, as, text.markdown) : null) ?? super.post(target, kind, text);
|
|
692
|
+
}
|
|
684
693
|
return (as && !post.files?.length ? await this.postNamed(target, as, post.markdown) : null) ?? super.post(target, kind, post);
|
|
685
694
|
}
|
|
695
|
+
/**
|
|
696
|
+
* A new thread whose root shows its files under its text, as one message: Slack makes
|
|
697
|
+
* that message from an upload's `initial_comment`, and names it only on the file, a
|
|
698
|
+
* moment later. A file upload can't set a username, so it goes out as the app, label and all.
|
|
699
|
+
* Null, before anything is sent, where the client can't, or the app can't read files
|
|
700
|
+
* (`files:read`) to find that message.
|
|
701
|
+
*/
|
|
702
|
+
async postWithFiles(target, markdown, files) {
|
|
703
|
+
const adapter = await this.ready();
|
|
704
|
+
const web = await this.web();
|
|
705
|
+
const toMrkdwn = adapter.formatConverter?.toResponseUrlText;
|
|
706
|
+
if (!web?.files?.uploadV2 || !web.files.info || !toMrkdwn || !await this.hasScope(web, "files:read")) return null;
|
|
707
|
+
const threadId = this.toChat(target);
|
|
708
|
+
const { channel } = adapter.decodeThreadId(threadId);
|
|
709
|
+
const message = { markdown };
|
|
710
|
+
const resolved = await adapter.resolveMessageMentions?.(message, threadId) ?? message;
|
|
711
|
+
const ids = ((await web.files.uploadV2({
|
|
712
|
+
channel_id: channel,
|
|
713
|
+
initial_comment: toMrkdwn.call(adapter.formatConverter, resolved),
|
|
714
|
+
file_uploads: files.map((file) => ({
|
|
715
|
+
file: Buffer.from(file.data),
|
|
716
|
+
filename: slackFileName(file.filename, file.mimeType)
|
|
717
|
+
}))
|
|
718
|
+
})).files?.[0]?.files ?? []).map((file) => file.id).filter((id) => !!id);
|
|
719
|
+
const found = async () => {
|
|
720
|
+
for (let attempt = 0; attempt < SHARE_POLLS; attempt++) {
|
|
721
|
+
const shares = (await web.files.info({ file: ids[0] })).file?.shares;
|
|
722
|
+
const ts = shares?.public?.[channel]?.[0]?.ts ?? shares?.private?.[channel]?.[0]?.ts;
|
|
723
|
+
if (ts) return ts;
|
|
724
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(250 * 2 ** attempt, 2e3)));
|
|
725
|
+
}
|
|
726
|
+
throw new Error("never shared");
|
|
727
|
+
};
|
|
728
|
+
if (!ids[0]) throw new Error("slack: files.uploadV2 returned no file IDs");
|
|
729
|
+
let ts;
|
|
730
|
+
try {
|
|
731
|
+
ts = await found();
|
|
732
|
+
} catch (error) {
|
|
733
|
+
throw new Error(`slack: couldn't find the message uploading ${ids[0]} made in ${channel} (${slackError(error)})`);
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
thread: this.toNative(`${adapter.name}:${channel}:${ts}`),
|
|
737
|
+
message: ts,
|
|
738
|
+
files: ids
|
|
739
|
+
};
|
|
740
|
+
}
|
|
686
741
|
/** An edit keeps the label out only where the message went out under the agent's name. */
|
|
687
742
|
async edit(thread, message, body) {
|
|
688
743
|
const { as, ...given } = body;
|
|
@@ -720,14 +775,18 @@ var SlackProvider = class extends ChatSyncProvider {
|
|
|
720
775
|
message: sent.ts
|
|
721
776
|
};
|
|
722
777
|
}
|
|
723
|
-
/** Whether the app may set a message's username and icon (`chat:write.customize`)
|
|
778
|
+
/** Whether the app may set a message's username and icon (`chat:write.customize`). */
|
|
724
779
|
canCustomize(web) {
|
|
725
|
-
this.
|
|
726
|
-
|
|
780
|
+
return this.hasScope(web, "chat:write.customize");
|
|
781
|
+
}
|
|
782
|
+
/** Whether the app has a scope, from its scopes, asked once. */
|
|
783
|
+
async hasScope(web, scope) {
|
|
784
|
+
this.scopes ??= (async () => (await web.auth?.test())?.response_metadata?.scopes ?? [])().catch((error) => {
|
|
785
|
+
this.scopes = void 0;
|
|
727
786
|
console.warn(`[board] slack: couldn't check the app's scopes (${slackError(error)}); posting as the app`);
|
|
728
|
-
return
|
|
787
|
+
return [];
|
|
729
788
|
});
|
|
730
|
-
return this.
|
|
789
|
+
return (await this.scopes).includes(scope);
|
|
731
790
|
}
|
|
732
791
|
/** Whether a message Slack has went out under `name`. */
|
|
733
792
|
async sentAs(thread, message, name) {
|
package/dist/sync.chat.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as externalAuthor, n as ChatSyncProvider, r as ChatTransport, t as ChatInput } from "./chat-provider-
|
|
1
|
+
import { i as externalAuthor, n as ChatSyncProvider, r as ChatTransport, t as ChatInput } from "./chat-provider-BWHbChcQ.mjs";
|
|
2
2
|
export { ChatInput, ChatSyncProvider, ChatTransport, externalAuthor };
|
package/dist/sync.chat.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as externalAuthor, t as ChatSyncProvider } from "./chat-provider-
|
|
1
|
+
import { n as externalAuthor, t as ChatSyncProvider } from "./chat-provider-COtsU5jB.mjs";
|
|
2
2
|
export { ChatSyncProvider, externalAuthor };
|
package/dist/sync.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { d as ExternalFile, l as ExternalAuthor, u as ExternalEvent } from "./folds-
|
|
2
|
-
import { A as SyncOverrides, C as channels, D as EventBase, E as render, M as SyncRoot, N as envelope, O as ProviderEvent, P as eventSchema, S as SyncUnit, T as mirror, _ as withUserTokens, a as ProviderOptions, b as SyncIn, c as ReceivedEvent, d as ResolvedTarget, f as SyncContext, g as connectUserTokens, h as UserTokens, i as ProviderFile, j as SyncResult, k as RootOverride, l as RenderInput, m as UnreadableError, n as FileDelivery, o as ProviderPost, p as SyncProvider, r as ProviderDelivery, s as Receive, u as Renderer, v as Sync, w as into, x as SyncOut, y as SyncFn } from "./provider-
|
|
1
|
+
import { d as ExternalFile, l as ExternalAuthor, u as ExternalEvent } from "./folds-ZkHM4M-v.mjs";
|
|
2
|
+
import { A as SyncOverrides, C as channels, D as EventBase, E as render, M as SyncRoot, N as envelope, O as ProviderEvent, P as eventSchema, S as SyncUnit, T as mirror, _ as withUserTokens, a as ProviderOptions, b as SyncIn, c as ReceivedEvent, d as ResolvedTarget, f as SyncContext, g as connectUserTokens, h as UserTokens, i as ProviderFile, j as SyncResult, k as RootOverride, l as RenderInput, m as UnreadableError, n as FileDelivery, o as ProviderPost, p as SyncProvider, r as ProviderDelivery, s as Receive, u as Renderer, v as Sync, w as into, x as SyncOut, y as SyncFn } from "./provider-DmSTp9lI.mjs";
|
|
3
3
|
export { EventBase, type ExternalAuthor, ExternalEvent, type ExternalFile, type FileDelivery, type ProviderDelivery, type ProviderEvent, type ProviderFile, type ProviderOptions, type ProviderPost, type Receive, ReceivedEvent, type RenderInput, type Renderer, ResolvedTarget, type RootOverride, type Sync, type SyncContext, type SyncFn, type SyncIn, type SyncOut, SyncOverrides, SyncProvider, type SyncResult, type SyncRoot, type SyncUnit, UnreadableError, type UserTokens, channels, connectUserTokens, envelope, eventSchema, into, mirror, render, withUserTokens };
|
package/dist/sync.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as ExternalEvent } from "./external-
|
|
2
|
-
import { c as into, i as UnreadableError, l as mirror, n as ResolvedTarget, r as SyncProvider, s as channels, t as ReceivedEvent, u as render } from "./provider-
|
|
3
|
-
import { i as eventSchema, n as SyncOverrides, r as envelope, t as EventBase } from "./events-
|
|
1
|
+
import { n as ExternalEvent } from "./external-D3p5T57R.mjs";
|
|
2
|
+
import { c as into, i as UnreadableError, l as mirror, n as ResolvedTarget, r as SyncProvider, s as channels, t as ReceivedEvent, u as render } from "./provider-BaVmi4Hi.mjs";
|
|
3
|
+
import { i as eventSchema, n as SyncOverrides, r as envelope, t as EventBase } from "./events-qf5XG5VQ.mjs";
|
|
4
4
|
import { r as withUserTokens, t as connectUserTokens } from "./users-yryHKFMo.mjs";
|
|
5
5
|
export { EventBase, ExternalEvent, ReceivedEvent, ResolvedTarget, SyncOverrides, SyncProvider, UnreadableError, channels, connectUserTokens, envelope, eventSchema, into, mirror, render, withUserTokens };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/sync/tags.ts
|
|
2
|
+
/** `good first issue` → `good-first-issue`: the part of a label key a name gives. Empty when nothing's left. */
|
|
3
|
+
const slug = (name) => name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^[-_.]+|-+$/g, "");
|
|
4
|
+
/**
|
|
5
|
+
* A platform's label or assignee as a board label: `bug` → `tag.bug=bug`,
|
|
6
|
+
* `julianbenegas` → `assignee.julianbenegas=julianbenegas`. The keys don't
|
|
7
|
+
* name the platform, so a filter like `--label tag.bug` holds for any of them.
|
|
8
|
+
*/
|
|
9
|
+
function tagOf(prefix, name) {
|
|
10
|
+
const key = name ? slug(name) : "";
|
|
11
|
+
return key ? [`${prefix}.${key}`.slice(0, 64), name.slice(0, 256)] : null;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { tagOf as n, slug as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as isThreadTarget } from "./external-
|
|
1
|
+
import { l as isThreadTarget } from "./external-D3p5T57R.mjs";
|
|
2
2
|
import { n as replaceMentions, t as extractMentions } from "./mentions-CB6ShY4R.mjs";
|
|
3
3
|
//#region src/server/targets.ts
|
|
4
4
|
/** A target as a post or a receipt names it, resolved: a plain one's kind goes by its shape. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { l as unauthorized } from "./errors-CusN4x2l.mjs";
|
|
2
|
-
import { f as markVerified, m as trustedIssuers, n as TOKENS, r as VERCEL_ISSUER, u as issuerName } from "./auth-
|
|
2
|
+
import { f as markVerified, m as trustedIssuers, n as TOKENS, r as VERCEL_ISSUER, u as issuerName } from "./auth-JO22iIwr.mjs";
|
|
3
3
|
import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
|
|
4
4
|
//#region src/server/identity.ts
|
|
5
5
|
let fromDisk;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ExternalCopy, c as SyncedThread, g as BoardEvent, o as Message } from "./folds-
|
|
1
|
+
import { a as ExternalCopy, c as SyncedThread, g as BoardEvent, o as Message } from "./folds-ZkHM4M-v.mjs";
|
|
2
2
|
import { t as A2Store } from "./store-D0MhBvg6-4Z2L0Kqd.mjs";
|
|
3
3
|
//#region src/store/types.d.ts
|
|
4
4
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
|
-
import { S as storable, a as AttachmentId, c as ChannelPath, d as LabelKey, g as MessageText, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, p as Labels, x as platformAvatar } from "./schemas-
|
|
3
|
-
import { a as Link, i as ExternalTarget, n as ExternalEvent, o as Origin, t as ExternalAuthor } from "./external-
|
|
2
|
+
import { S as storable, a as AttachmentId, c as ChannelPath, d as LabelKey, g as MessageText, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, p as Labels, x as platformAvatar } from "./schemas-DBmUO6pr.mjs";
|
|
3
|
+
import { a as Link, i as ExternalTarget, n as ExternalEvent, o as Origin, t as ExternalAuthor } from "./external-D3p5T57R.mjs";
|
|
4
4
|
import { r as badRequest, t as HttpError } from "./errors-CusN4x2l.mjs";
|
|
5
|
-
import { o as options } from "./commands-
|
|
5
|
+
import { o as options } from "./commands-C3R1AWgm.mjs";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
//#region src/api/routes.ts
|
|
8
8
|
/**
|
|
@@ -172,7 +172,9 @@ const AckBody = z.object({
|
|
|
172
172
|
*/
|
|
173
173
|
const SourceReadQuery = z.object({
|
|
174
174
|
target: z.string().min(1).max(4e3),
|
|
175
|
-
limit: z.coerce.number().int().min(1).max(200).optional()
|
|
175
|
+
limit: z.coerce.number().int().min(1).max(200).optional(),
|
|
176
|
+
/** Where the last read left off: its `next`. */
|
|
177
|
+
after: z.string().min(1).max(400).optional()
|
|
176
178
|
});
|
|
177
179
|
/** A channel to add the board's app to, so it can be read on demand: `#name`, an ID, or a link into it. */
|
|
178
180
|
const SourceJoinBody = z.object({ target: z.string().min(1).max(4e3) });
|
|
@@ -662,7 +664,7 @@ const routes = {
|
|
|
662
664
|
const SYNC = "@sync";
|
|
663
665
|
//#endregion
|
|
664
666
|
//#region src/version.ts
|
|
665
|
-
const VERSION = "0.
|
|
667
|
+
const VERSION = "0.29.0";
|
|
666
668
|
//#endregion
|
|
667
669
|
//#region src/server/routing.ts
|
|
668
670
|
/** board()'s own routes: the files it serves, and sync's, which run where board() does. */
|
package/docs/02-sync/03-slack.md
CHANGED
|
@@ -156,8 +156,12 @@ who can see it there:
|
|
|
156
156
|
📎 [shot.png](https://files.slack.com/…) (7 B) didn't come over: this board keeps no files
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
-
**To Slack:**
|
|
160
|
-
|
|
159
|
+
**To Slack:** a new thread's root goes out as one message, its files under its text
|
|
160
|
+
(it needs `files:write`, and `files:read` to find the message the upload made;
|
|
161
|
+
without `files:read`, the files follow in a message of their own). A
|
|
162
|
+
file upload can't set a username, so that root goes out as the app, with the
|
|
163
|
+
agent's name in front. A reply's text posts first, then its files upload into the
|
|
164
|
+
same thread as a message of their own. A failed upload retries alone, and
|
|
161
165
|
deleting the message on the board deletes the files in Slack. If Slack refuses
|
|
162
166
|
files, the text is edited to add a line per file:
|
|
163
167
|
|
|
@@ -46,13 +46,16 @@ in the map stays off the board, except threads someone syncs by hand.
|
|
|
46
46
|
## An issue's life in one thread
|
|
47
47
|
|
|
48
48
|
1. **An issue opens.** The board creates a root in the team's channel,
|
|
49
|
-
`ENG-123: <title>` with its link,
|
|
50
|
-
workflow state's type
|
|
49
|
+
`ENG-123: <title>` with its link and, under them, its description. It's
|
|
50
|
+
labeled `issue=ENG-123`, `state` with its workflow state's type, and the
|
|
51
|
+
issue's properties: `priority=high`, `project=<project>`, `tag.<label>` for
|
|
52
|
+
each Linear label, and `assignee.<handle>`.
|
|
51
53
|
2. **Comments** arrive as replies. A reply to a comment stays in that comment's
|
|
52
54
|
thread on Linear, and a board reply to one goes back into it. Edits and
|
|
53
55
|
deletes sync too.
|
|
54
|
-
3. **Moving the issue** to another state updates `state
|
|
55
|
-
|
|
56
|
+
3. **Moving the issue** to another state updates `state`, and changing a
|
|
57
|
+
property updates its label. A new title or description edits a root that
|
|
58
|
+
Linear opened to say it. An issue that was open before the mirror
|
|
56
59
|
gets its root the first time a person edits or comments on it. Linear's own
|
|
57
60
|
automations and archiving don't bring old issues in.
|
|
58
61
|
4. **A root posted on the board** in a mirrored channel opens an issue in that
|
|
@@ -79,8 +82,8 @@ the board follows the team it's in now. The old thread stays as it was.
|
|
|
79
82
|
|
|
80
83
|
| Event | What the mirror does |
|
|
81
84
|
| --- | --- |
|
|
82
|
-
| `issue.created` | A root labeled `issue=ENG-123
|
|
83
|
-
| `issue.updated` | A new state sets `state
|
|
85
|
+
| `issue.created` | A root with the description, labeled `issue=ENG-123`, `state=<type>`, and the issue's properties |
|
|
86
|
+
| `issue.updated` | A new state sets `state`, and properties update their labels; a new title or description edits a root Linear opened. A person's edit to an issue the board hasn't seen opens its root, so older issues come in when they're next touched (a bulk edit brings in a whole view). Automations and archiving don't |
|
|
84
87
|
| `comment` | A reply. Edits and deletes sync too |
|
|
85
88
|
|
|
86
89
|
`state` is the workflow state's type: `triage`, `backlog`, `unstarted`,
|
|
@@ -96,6 +99,25 @@ A comment on an issue that opened before you mapped its team still lands: the
|
|
|
96
99
|
board fetches the issue through Linear's API, creates its root, then posts the
|
|
97
100
|
comment as a reply.
|
|
98
101
|
|
|
102
|
+
## Read on demand
|
|
103
|
+
|
|
104
|
+
`vb sources <key> read` reads Linear without syncing anything, so an agent can
|
|
105
|
+
work through a project's issues without a thread for each one:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
vb sources linear read https://linear.app/acme/project/checkout-a1b2c3d4e5f6 # a project's issues
|
|
109
|
+
vb sources linear read ENG # a team's issues
|
|
110
|
+
vb sources linear read ENG-123 # one issue and its comments
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A project or team read lists issues newest first, each with its state,
|
|
114
|
+
priority, assignee, project, labels, link, and description (cut at 1,500
|
|
115
|
+
characters). An issue read shows the whole description, then its comments,
|
|
116
|
+
oldest first. A page holds at most 100 issues or comments: when there are more,
|
|
117
|
+
the read ends with the `--after` cursor that reads the next page. It reads with
|
|
118
|
+
the Linear app's token, so it needs `manage` in every channel, as every source
|
|
119
|
+
read does.
|
|
120
|
+
|
|
99
121
|
## Reactions
|
|
100
122
|
|
|
101
123
|
A reaction on the board goes to its comment on Linear, while it's added: Linear
|