forges 0.0.0 → 0.1.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.
Files changed (152) hide show
  1. package/LICENCE +21 -0
  2. package/README.md +72 -0
  3. package/dist/azure-devops/index.d.mts +2 -0
  4. package/dist/azure-devops/index.mjs +1 -0
  5. package/dist/azure-devops-DwQvL4TV.mjs +1355 -0
  6. package/dist/bitbucket/index.d.mts +2 -0
  7. package/dist/bitbucket/index.mjs +1 -0
  8. package/dist/bitbucket-CpcfJlYi.mjs +1171 -0
  9. package/dist/crypto-CQNEyYHD.mjs +156 -0
  10. package/dist/cursor-origin/index.d.mts +2 -0
  11. package/dist/cursor-origin/index.mjs +1 -0
  12. package/dist/cursor-origin-BhqYx2oW.mjs +980 -0
  13. package/dist/define-B2_4ob2i.d.mts +307 -0
  14. package/dist/define-D3yEXtKK.mjs +2276 -0
  15. package/dist/env.d.mts +46 -0
  16. package/dist/env.mjs +283 -0
  17. package/dist/fake/index.d.mts +227 -0
  18. package/dist/fake/index.mjs +1001 -0
  19. package/dist/fetch-D4pSRvvA.d.mts +1723 -0
  20. package/dist/forgejo/index.d.mts +2 -0
  21. package/dist/forgejo/index.mjs +1 -0
  22. package/dist/forgejo-BCefbHUN.mjs +2005 -0
  23. package/dist/gitea/index.d.mts +2 -0
  24. package/dist/gitea/index.mjs +22 -0
  25. package/dist/gitee/index.d.mts +2 -0
  26. package/dist/gitee/index.mjs +1 -0
  27. package/dist/gitee-CA-f7Rs8.mjs +1272 -0
  28. package/dist/github/index.d.mts +2 -0
  29. package/dist/github/index.mjs +1 -0
  30. package/dist/github-B0umL8_f.mjs +3259 -0
  31. package/dist/gitlab/index.d.mts +2 -0
  32. package/dist/gitlab/index.mjs +1 -0
  33. package/dist/gitlab-Bsr__Z0C.mjs +2378 -0
  34. package/dist/graphql-D7uGzZTf.mjs +366 -0
  35. package/dist/index-BQ6VS31s.d.mts +32 -0
  36. package/dist/index-C-jz9Mo3.d.mts +16 -0
  37. package/dist/index-C4fYzmK3.d.mts +102 -0
  38. package/dist/index-CQiIX4SH.d.mts +27 -0
  39. package/dist/index-CTj5gQzl.d.mts +35 -0
  40. package/dist/index-CliBWzOF.d.mts +23 -0
  41. package/dist/index-Cz_y-t3d.d.mts +36 -0
  42. package/dist/index-DEt25E1w.d.mts +77 -0
  43. package/dist/index-DIuS0EOy.d.mts +43 -0
  44. package/dist/index-uSpsMCrC.d.mts +26 -0
  45. package/dist/index.d.mts +41 -0
  46. package/dist/index.mjs +13 -0
  47. package/dist/kit.d.mts +183 -0
  48. package/dist/kit.mjs +4 -0
  49. package/dist/provider-CoZ7cCxp.d.mts +1439 -0
  50. package/dist/provider-Ed1YhkQN.mjs +169 -0
  51. package/dist/pushin/index.d.mts +2 -0
  52. package/dist/pushin/index.mjs +1 -0
  53. package/dist/pushin-CkxgJ2cU.mjs +343 -0
  54. package/dist/schema/Actor.json +96 -0
  55. package/dist/schema/AlertSupport.json +49 -0
  56. package/dist/schema/AuthKind.json +15 -0
  57. package/dist/schema/Branch.json +31 -0
  58. package/dist/schema/ChangedFile.json +58 -0
  59. package/dist/schema/Check.json +169 -0
  60. package/dist/schema/CheckRef.json +114 -0
  61. package/dist/schema/CheckState.json +14 -0
  62. package/dist/schema/ChecksSummary.json +34 -0
  63. package/dist/schema/CiJob.json +186 -0
  64. package/dist/schema/CiJobRef.json +132 -0
  65. package/dist/schema/CiRun.json +234 -0
  66. package/dist/schema/CiRunRef.json +103 -0
  67. package/dist/schema/CodeFragment.json +19 -0
  68. package/dist/schema/CodeMatch.json +128 -0
  69. package/dist/schema/Collaborator.json +169 -0
  70. package/dist/schema/Comment.json +336 -0
  71. package/dist/schema/CommentRef.json +158 -0
  72. package/dist/schema/Commit.json +307 -0
  73. package/dist/schema/CommitRef.json +104 -0
  74. package/dist/schema/CommitSignature.json +118 -0
  75. package/dist/schema/Comparison.json +355 -0
  76. package/dist/schema/Cursor.json +21 -0
  77. package/dist/schema/EventAction.json +33 -0
  78. package/dist/schema/EventDetail.json +649 -0
  79. package/dist/schema/EventKind.json +26 -0
  80. package/dist/schema/FileContent.json +73 -0
  81. package/dist/schema/FileStatus.json +15 -0
  82. package/dist/schema/ForgeCapabilities.json +737 -0
  83. package/dist/schema/ForgeEvent.json +789 -0
  84. package/dist/schema/ForgeInstance.json +7 -0
  85. package/dist/schema/ForgeKind.json +32 -0
  86. package/dist/schema/ForgeWarning.json +49 -0
  87. package/dist/schema/ForgeWarningCode.json +7 -0
  88. package/dist/schema/GetManyResult.json +699 -0
  89. package/dist/schema/Installation.json +131 -0
  90. package/dist/schema/InstallationToken.json +31 -0
  91. package/dist/schema/KnownForgeKind.json +19 -0
  92. package/dist/schema/Label.json +22 -0
  93. package/dist/schema/MergeMethod.json +14 -0
  94. package/dist/schema/Milestone.json +40 -0
  95. package/dist/schema/Notification.json +498 -0
  96. package/dist/schema/NotificationReason.json +28 -0
  97. package/dist/schema/NotificationRef.json +60 -0
  98. package/dist/schema/NotificationSubject.json +376 -0
  99. package/dist/schema/PerKind.json +49 -0
  100. package/dist/schema/PullBranches.json +125 -0
  101. package/dist/schema/PushCommit.json +24 -0
  102. package/dist/schema/Reaction.json +148 -0
  103. package/dist/schema/ReactionContent.json +17 -0
  104. package/dist/schema/ReactionSummary.json +70 -0
  105. package/dist/schema/Release.json +286 -0
  106. package/dist/schema/ReleaseAsset.json +169 -0
  107. package/dist/schema/ReleaseAssetRef.json +138 -0
  108. package/dist/schema/ReleaseRef.json +107 -0
  109. package/dist/schema/Repo.json +354 -0
  110. package/dist/schema/RepoFeatures.json +35 -0
  111. package/dist/schema/RepoPermissions.json +35 -0
  112. package/dist/schema/RepoRef.json +77 -0
  113. package/dist/schema/RepoRole.json +15 -0
  114. package/dist/schema/RepoVisibility.json +12 -0
  115. package/dist/schema/Review.json +371 -0
  116. package/dist/schema/ReviewComment.json +280 -0
  117. package/dist/schema/ReviewCommentInput.json +34 -0
  118. package/dist/schema/ReviewRef.json +158 -0
  119. package/dist/schema/ReviewState.json +15 -0
  120. package/dist/schema/Reviewer.json +133 -0
  121. package/dist/schema/SecurityAlert.json +219 -0
  122. package/dist/schema/SecurityAlertKind.json +14 -0
  123. package/dist/schema/SecurityAlertRef.json +122 -0
  124. package/dist/schema/SecuritySeverity.json +14 -0
  125. package/dist/schema/SourceKind.json +12 -0
  126. package/dist/schema/SubscriptionState.json +12 -0
  127. package/dist/schema/Support.json +23 -0
  128. package/dist/schema/Tag.json +25 -0
  129. package/dist/schema/TextLimits.json +18 -0
  130. package/dist/schema/Thread.json +653 -0
  131. package/dist/schema/ThreadKind.json +14 -0
  132. package/dist/schema/ThreadRef.json +131 -0
  133. package/dist/schema/ThreadStack.json +146 -0
  134. package/dist/schema/ThreadState.json +13 -0
  135. package/dist/schema/TreeEntry.json +46 -0
  136. package/dist/schema/TreeEntryType.json +13 -0
  137. package/dist/schema/UpsertCommentResult.json +355 -0
  138. package/dist/schema/User.json +123 -0
  139. package/dist/schema/Webhook.json +188 -0
  140. package/dist/schema/WebhookDeliveryRecord.json +173 -0
  141. package/dist/schema/WebhookDeliveryRef.json +135 -0
  142. package/dist/schema/WebhookRef.json +105 -0
  143. package/dist/schema/index.d.mts +4 -0
  144. package/dist/schema/index.mjs +3219 -0
  145. package/dist/tangled/index.d.mts +2 -0
  146. package/dist/tangled/index.mjs +1 -0
  147. package/dist/tangled-ChdPmiv-.mjs +1570 -0
  148. package/dist/testing/index.d.mts +123 -0
  149. package/dist/testing/index.mjs +242 -0
  150. package/dist/webhooks-rn9-8lxT.mjs +70 -0
  151. package/package.json +115 -3
  152. package/dist/.gitkeep +0 -0
@@ -0,0 +1,1001 @@
1
+ import { N as capabilityOf, f as getManyConcurrently, ft as toFileContent, gt as ForgeApiError, ht as ForbiddenError, n as perKind, r as verb, st as completeEvent, t as defineForgeProvider, wt as NotFoundError } from "../define-D3yEXtKK.mjs";
2
+ import { n as headerValue, t as bodyText } from "../crypto-CQNEyYHD.mjs";
3
+ import { a as verifyHmacSignature } from "../webhooks-rn9-8lxT.mjs";
4
+ //#region src/fake/index.ts
5
+ /** Every normalised kind the fake forge records, so a test can subscribe to any of them. */
6
+ const FAKE_WEBHOOK_EVENTS = [
7
+ { kind: "comment" },
8
+ { kind: "state_change" },
9
+ { kind: "label" },
10
+ { kind: "assignment" },
11
+ { kind: "review" },
12
+ { kind: "push" }
13
+ ];
14
+ const ALL_KINDS = {
15
+ issue: true,
16
+ pull_request: true,
17
+ discussion: true,
18
+ commit: true
19
+ };
20
+ const WRITABLE = {
21
+ issue: true,
22
+ pull_request: true,
23
+ discussion: true
24
+ };
25
+ const PULLS = { pull_request: true };
26
+ const SIGNATURE_HEADER = "x-fake-signature";
27
+ function pageOf(items, options = {}) {
28
+ const offset = Number(options.cursor?.token ?? 0);
29
+ const next = offset + (options.perPage ?? 30);
30
+ return {
31
+ items: items.slice(offset, next),
32
+ cursor: next < items.length ? { token: String(next) } : void 0
33
+ };
34
+ }
35
+ function splitRepo(slug) {
36
+ const index = slug.lastIndexOf("/");
37
+ return {
38
+ owner: slug.slice(0, index),
39
+ name: slug.slice(index + 1)
40
+ };
41
+ }
42
+ /** Reads a stored file key, `<ref>:<path>`, where the ref `*` stands for every ref. */
43
+ function splitFileKey(key) {
44
+ const index = key.indexOf(":");
45
+ return {
46
+ ref: key.slice(0, index),
47
+ path: key.slice(index + 1)
48
+ };
49
+ }
50
+ /**
51
+ * An in-memory forge implementing the whole provider surface, for testing
52
+ * code that writes. Every write changes `store` and appends a `ForgeEvent`
53
+ * that `threads.events()` and `sources.subscribe()` return. Webhook
54
+ * deliveries carry normalised events as JSON, signed as `signDelivery('fake', ...)`
55
+ * from `forges/testing` signs them.
56
+ * @param options The data the forge starts with, and the support of individual operations.
57
+ * @example
58
+ * ```ts
59
+ * import { createForges } from 'forges'
60
+ * import { fake } from 'forges/fake'
61
+ *
62
+ * const factory = fake({
63
+ * seed: {
64
+ * repos: [{ repo: 'acme/widgets' }],
65
+ * threads: [{ repo: 'acme/widgets', kind: 'issue', title: 'Broken build' }],
66
+ * },
67
+ * })
68
+ * const forges = createForges([factory])
69
+ * ```
70
+ */
71
+ function fake(options = {}) {
72
+ const forge = options.forge ?? "fake";
73
+ const instance = options.instance ?? "fake.test";
74
+ const origin = {
75
+ forge,
76
+ instance
77
+ };
78
+ const viewerLogin = options.viewer ?? "fake-user";
79
+ const store = {
80
+ repos: /* @__PURE__ */ new Map(),
81
+ notifications: [],
82
+ events: [],
83
+ webhooks: []
84
+ };
85
+ const secrets = /* @__PURE__ */ new Map();
86
+ const listeners = /* @__PURE__ */ new Set();
87
+ const notFound = (message) => new NotFoundError(message, 404, "", origin);
88
+ const actor = (login) => ({
89
+ ...origin,
90
+ login,
91
+ id: login,
92
+ isBotHint: login.endsWith("[bot]")
93
+ });
94
+ const label = (name) => ({ name });
95
+ const repoRef = (slug) => ({
96
+ ...origin,
97
+ ...splitRepo(slug)
98
+ });
99
+ const slugOf = (repo) => `${repo.owner}/${repo.name}`;
100
+ function repoState(repo) {
101
+ const state = store.repos.get(slugOf(repo));
102
+ if (!state) throw notFound(`No repository ${slugOf(repo)}`);
103
+ return state;
104
+ }
105
+ function addRepo(seed) {
106
+ const ref = repoRef(seed.repo);
107
+ store.repos.set(seed.repo, {
108
+ repo: {
109
+ ref,
110
+ description: seed.description,
111
+ defaultBranch: seed.defaultBranch ?? "main",
112
+ visibility: seed.visibility ?? "public",
113
+ isFork: false,
114
+ isArchived: false,
115
+ topics: [],
116
+ url: `https://${instance}/${seed.repo}`,
117
+ raw: seed
118
+ },
119
+ labels: seed.labels ?? [],
120
+ threads: /* @__PURE__ */ new Map(),
121
+ releases: [],
122
+ statuses: /* @__PURE__ */ new Map(),
123
+ runs: [],
124
+ files: /* @__PURE__ */ new Map(),
125
+ commits: [],
126
+ assets: /* @__PURE__ */ new Map()
127
+ });
128
+ }
129
+ function releaseState(ref) {
130
+ const release = repoState(ref.repo).releases.find((item) => item.ref.id === ref.id || ref.tag && item.tag === ref.tag);
131
+ if (!release) throw notFound(`No release ${ref.tag ?? ref.id}`);
132
+ return release;
133
+ }
134
+ function requireFreeTag(releases, tag) {
135
+ if (releases.some((release) => release.tag === tag)) throw new ForgeApiError(`A release tagged ${tag} already exists`, 422, "", origin);
136
+ }
137
+ function webhookState(ref) {
138
+ const hook = store.webhooks.find((item) => item.ref.id === ref.id);
139
+ if (!hook) throw notFound(`No webhook ${ref.id}`);
140
+ return hook;
141
+ }
142
+ function threadState(ref) {
143
+ const state = repoState(ref.repo).threads.get(String(ref.number));
144
+ if (!state || state.thread.kind !== ref.kind) throw notFound(`No ${ref.kind} ${slugOf(ref.repo)}#${ref.number}`);
145
+ return state;
146
+ }
147
+ function addThread(seed) {
148
+ const repo = store.repos.get(seed.repo) ?? (addRepo({ repo: seed.repo }), store.repos.get(seed.repo));
149
+ const number = String(seed.number ?? repo.threads.size + 1);
150
+ const ref = {
151
+ ...origin,
152
+ repo: repo.repo.ref,
153
+ kind: seed.kind,
154
+ number
155
+ };
156
+ const now = /* @__PURE__ */ new Date();
157
+ const thread = {
158
+ ref,
159
+ kind: seed.kind,
160
+ title: seed.title,
161
+ body: seed.body,
162
+ state: seed.state ?? "open",
163
+ isDraft: seed.isDraft ?? false,
164
+ author: actor(seed.author ?? viewerLogin),
165
+ assignees: (seed.assignees ?? []).map(actor),
166
+ reviewers: [],
167
+ labels: (seed.labels ?? []).map(label),
168
+ url: `https://${instance}/${seed.repo}/${seed.kind === "pull_request" ? "pull" : seed.kind === "discussion" ? "discussions" : "issues"}/${number}`,
169
+ createdAt: now,
170
+ updatedAt: now,
171
+ commentCount: seed.comments?.length ?? 0,
172
+ raw: seed
173
+ };
174
+ const comments = (seed.comments ?? []).map((comment, index) => ({
175
+ ref: {
176
+ ...origin,
177
+ thread: ref,
178
+ id: `${number}-${index + 1}`
179
+ },
180
+ body: comment.body,
181
+ author: actor(comment.author ?? viewerLogin),
182
+ createdAt: now,
183
+ raw: comment
184
+ }));
185
+ const checks = (seed.checks ?? []).map((check, index) => ({
186
+ ref: {
187
+ ...origin,
188
+ repo: ref.repo,
189
+ id: `${number}-check-${index + 1}`,
190
+ type: "check_run"
191
+ },
192
+ name: check.name,
193
+ state: check.state,
194
+ stateRaw: check.state,
195
+ raw: check
196
+ }));
197
+ if (checks.length) thread.checks = {
198
+ state: summaryState(checks),
199
+ stateRaw: summaryState(checks),
200
+ total: checks.length,
201
+ failed: checks.filter((check) => check.state === "failure").length
202
+ };
203
+ const files = (seed.files ?? []).map((file) => ({
204
+ path: file.path,
205
+ status: file.status ?? "modified",
206
+ statusRaw: file.status ?? "modified",
207
+ additions: file.additions ?? 0,
208
+ deletions: file.deletions ?? 0,
209
+ patch: file.patch
210
+ }));
211
+ repo.threads.set(number, {
212
+ thread,
213
+ files,
214
+ commits: seed.commits ?? [],
215
+ comments,
216
+ reactions: /* @__PURE__ */ new Map(),
217
+ checks,
218
+ reviews: [],
219
+ subscription: "subscribed"
220
+ });
221
+ return thread;
222
+ }
223
+ function summaryState(checks) {
224
+ if (checks.some((check) => check.state === "failure")) return "failure";
225
+ if (checks.some((check) => check.state === "pending")) return "pending";
226
+ return checks.every((check) => check.state === "success" || check.state === "neutral") ? "success" : "unknown";
227
+ }
228
+ function record(thread, kind, action, summary, detail) {
229
+ const event = completeEvent({
230
+ ...origin,
231
+ id: String(store.events.length + 1),
232
+ kind,
233
+ action,
234
+ actionRaw: action,
235
+ summary: `${viewerLogin} ${summary}`,
236
+ occurredAt: /* @__PURE__ */ new Date(),
237
+ actor: actor(viewerLogin),
238
+ repo: thread.repo,
239
+ thread,
240
+ detail,
241
+ source: "subscribe",
242
+ payload: {
243
+ kind,
244
+ action,
245
+ detail
246
+ }
247
+ });
248
+ store.events.push(event);
249
+ const state = repoState(thread.repo).threads.get(String(thread.number));
250
+ if (state) {
251
+ state.thread.updatedAt = event.occurredAt;
252
+ state.thread.lastActivityAt = event.occurredAt;
253
+ }
254
+ for (const listener of listeners) listener(event);
255
+ }
256
+ for (const seed of options.seed?.repos ?? []) addRepo(seed);
257
+ for (const seed of options.seed?.threads ?? []) addThread(seed);
258
+ for (const seed of options.seed?.releases ?? []) {
259
+ const repo = store.repos.get(seed.repo) ?? (addRepo({ repo: seed.repo }), store.repos.get(seed.repo));
260
+ const releaseRef = {
261
+ ...origin,
262
+ repo: repo.repo.ref,
263
+ id: String(repo.releases.length + 1),
264
+ tag: seed.tag
265
+ };
266
+ for (const asset of seed.assets ?? []) repo.assets.set(`${releaseRef.id}:${asset.name}`, typeof asset.content === "string" ? new TextEncoder().encode(asset.content) : asset.content);
267
+ repo.releases.push({
268
+ ref: releaseRef,
269
+ assets: (seed.assets ?? []).map((asset) => ({
270
+ ref: {
271
+ ...origin,
272
+ repo: repo.repo.ref,
273
+ release: releaseRef,
274
+ id: asset.name
275
+ },
276
+ name: asset.name,
277
+ url: `https://${instance}/${seed.repo}/releases/download/${seed.tag}/${asset.name}`,
278
+ size: (typeof asset.content === "string" ? new TextEncoder().encode(asset.content) : asset.content).byteLength,
279
+ contentType: asset.contentType
280
+ })),
281
+ tag: seed.tag,
282
+ name: seed.name,
283
+ body: seed.body,
284
+ isDraft: seed.isDraft ?? false,
285
+ isPrerelease: seed.isPrerelease ?? false,
286
+ immutable: seed.immutable,
287
+ publishedAt: seed.publishedAt ?? /* @__PURE__ */ new Date(),
288
+ raw: seed
289
+ });
290
+ }
291
+ for (const seed of options.seed?.runs ?? []) {
292
+ const repo = store.repos.get(seed.repo) ?? (addRepo({ repo: seed.repo }), store.repos.get(seed.repo));
293
+ const id = String(repo.runs.length + 1);
294
+ const ref = {
295
+ ...origin,
296
+ repo: repo.repo.ref,
297
+ id
298
+ };
299
+ repo.runs.push({
300
+ run: {
301
+ ref,
302
+ name: seed.name,
303
+ state: seed.state ?? "success",
304
+ stateRaw: seed.state ?? "success",
305
+ number: id,
306
+ branch: seed.branch ?? repo.repo.defaultBranch,
307
+ sha: seed.sha,
308
+ createdAt: /* @__PURE__ */ new Date(),
309
+ raw: seed
310
+ },
311
+ jobs: (seed.jobs ?? []).map((job, index) => ({
312
+ job: {
313
+ ref: {
314
+ ...origin,
315
+ repo: repo.repo.ref,
316
+ id: `${id}-${index + 1}`,
317
+ run: ref
318
+ },
319
+ name: job.name,
320
+ state: job.state ?? seed.state ?? "success",
321
+ stateRaw: job.state ?? seed.state ?? "success",
322
+ raw: job
323
+ },
324
+ log: job.log ?? ""
325
+ }))
326
+ });
327
+ }
328
+ for (const seed of options.seed?.files ?? []) (store.repos.get(seed.repo) ?? (addRepo({ repo: seed.repo }), store.repos.get(seed.repo))).files.set(`${seed.ref ?? "*"}:${seed.path}`, typeof seed.content === "string" ? new TextEncoder().encode(seed.content) : seed.content);
329
+ for (const seed of options.seed?.commits ?? []) {
330
+ const repo = store.repos.get(seed.repo) ?? (addRepo({ repo: seed.repo }), store.repos.get(seed.repo));
331
+ const files = (seed.files ?? []).map((file) => ({
332
+ path: file.path,
333
+ status: file.status ?? "modified",
334
+ statusRaw: file.status ?? "modified",
335
+ additions: file.additions ?? 0,
336
+ deletions: file.deletions ?? 0,
337
+ patch: file.patch
338
+ }));
339
+ repo.commits.push({
340
+ ref: {
341
+ ...origin,
342
+ repo: repo.repo.ref,
343
+ sha: seed.sha
344
+ },
345
+ sha: seed.sha,
346
+ message: seed.message,
347
+ author: {
348
+ actor: actor(seed.author ?? viewerLogin),
349
+ name: seed.author ?? viewerLogin,
350
+ date: seed.committedAt ?? /* @__PURE__ */ new Date()
351
+ },
352
+ parents: seed.parents ?? [],
353
+ stats: {
354
+ additions: files.reduce((total, file) => total + (file.additions ?? 0), 0),
355
+ deletions: files.reduce((total, file) => total + (file.deletions ?? 0), 0)
356
+ },
357
+ files,
358
+ raw: seed
359
+ });
360
+ }
361
+ for (const [index, seed] of (options.seed?.notifications ?? []).entries()) {
362
+ const state = store.repos.get(seed.repo)?.threads.get(String(seed.number));
363
+ if (!state) throw new Error(`Notification seed references unknown thread ${seed.repo}#${seed.number}`);
364
+ store.notifications.push({
365
+ ref: {
366
+ ...origin,
367
+ id: String(index + 1)
368
+ },
369
+ subject: {
370
+ type: "thread",
371
+ thread: state.thread.ref
372
+ },
373
+ reason: seed.reason ?? "subscribed",
374
+ reasonRaw: seed.reason ?? "subscribed",
375
+ unread: seed.unread ?? true,
376
+ title: state.thread.title,
377
+ subjectState: state.thread.state,
378
+ updatedAt: /* @__PURE__ */ new Date(),
379
+ raw: seed
380
+ });
381
+ }
382
+ const givenSupport = (name) => options.support?.[name] ?? (options.capabilities && capabilityOf(options.capabilities, name));
383
+ const support = (name, fallback) => {
384
+ const given = givenSupport(name);
385
+ return given === void 0 || typeof given === "object" ? fallback : given;
386
+ };
387
+ const kinds = (name, fallback) => {
388
+ const given = givenSupport(name);
389
+ if (given === void 0) return fallback;
390
+ return typeof given === "object" ? given : Object.fromEntries(Object.keys(fallback).map((kind) => [kind, given]));
391
+ };
392
+ function setState(ref, state, action) {
393
+ const current = threadState(ref).thread;
394
+ current.state = state;
395
+ current.closedAt = state === "open" ? void 0 : /* @__PURE__ */ new Date();
396
+ record(ref, "state_change", action, action, {
397
+ type: "state_change",
398
+ state
399
+ });
400
+ }
401
+ function runState(ref) {
402
+ const found = repoState(ref.repo).runs.find((item) => item.run.ref.id === ref.id);
403
+ if (!found) throw notFound(`No run ${ref.id}`);
404
+ return found;
405
+ }
406
+ const REVIEW_STATES = {
407
+ approve: "approved",
408
+ request_changes: "changes_requested",
409
+ comment: "commented"
410
+ };
411
+ function createReview(ref, input) {
412
+ const state = threadState(ref);
413
+ const id = String(state.reviews.length + 1);
414
+ const comments = (input.comments ?? []).map((comment, index) => ({
415
+ ref: {
416
+ ...origin,
417
+ thread: state.thread.ref,
418
+ id: `${ref.number}-review-${id}-${index + 1}`
419
+ },
420
+ body: comment.body,
421
+ author: actor(viewerLogin),
422
+ path: comment.path,
423
+ line: comment.line,
424
+ side: comment.side ?? "right",
425
+ thread: {
426
+ id: `${ref.number}-thread-${id}-${index + 1}`,
427
+ resolved: false
428
+ },
429
+ createdAt: /* @__PURE__ */ new Date(),
430
+ raw: comment
431
+ }));
432
+ const review = {
433
+ ref: {
434
+ ...origin,
435
+ thread: state.thread.ref,
436
+ id
437
+ },
438
+ author: actor(viewerLogin),
439
+ state: input.event ? REVIEW_STATES[input.event] : "pending",
440
+ stateRaw: input.event ?? "pending",
441
+ body: input.body,
442
+ submittedAt: input.event ? /* @__PURE__ */ new Date() : void 0,
443
+ comments,
444
+ raw: input
445
+ };
446
+ state.reviews.push(review);
447
+ if (input.event) record(state.thread.ref, "review", "submitted", `reviewed (${review.state})`, {
448
+ type: "review",
449
+ state: review.state,
450
+ body: input.body
451
+ });
452
+ return review;
453
+ }
454
+ function setReviewThreadResolved(ref, id, resolved) {
455
+ const comment = threadState(ref).reviews.flatMap((review) => review.comments === false ? [] : review.comments).find((item) => item.thread?.id === id);
456
+ if (!comment?.thread) throw notFound(`No review thread ${id}`);
457
+ comment.thread = {
458
+ id,
459
+ resolved
460
+ };
461
+ }
462
+ const factory = defineForgeProvider({
463
+ forge,
464
+ baseUrl: `https://${instance}`,
465
+ webhooks: () => ({
466
+ verify: (delivery) => verifyHmacSignature(delivery, options.webhookSecret, { header: SIGNATURE_HEADER }),
467
+ translate: (delivery) => {
468
+ const parsed = JSON.parse(bodyText(delivery.body));
469
+ const deliveryId = headerValue(delivery.headers, "x-fake-delivery");
470
+ return (Array.isArray(parsed) ? parsed : [parsed]).map((event, index) => ({
471
+ ...event,
472
+ ...origin,
473
+ id: event.id ?? `${deliveryId ?? "delivery"}:${index}`,
474
+ occurredAt: new Date(event.occurredAt),
475
+ source: "webhook"
476
+ }));
477
+ },
478
+ events: FAKE_WEBHOOK_EVENTS
479
+ }),
480
+ setup: () => ({
481
+ traits: {
482
+ eventKinds: "native",
483
+ authKinds: ["token"]
484
+ },
485
+ users: { me: verb(support("users.me", true), async () => ({
486
+ ...actor(viewerLogin),
487
+ raw: void 0
488
+ })) },
489
+ repos: {
490
+ get: verb(support("repos.get", true), async (repo) => repoState(repo).repo),
491
+ listPage: verb(support("repos.list", true), async (listOptions = {}) => pageOf([...store.repos.values()].map((state) => state.repo), listOptions))
492
+ },
493
+ search: {
494
+ threadsPage: verb(support("search.threads", true), async (query = {}) => {
495
+ const text = query.text?.toLowerCase();
496
+ const state = query.state ?? "all";
497
+ const items = [...store.repos.values()].filter((entry) => !query.repo || slugOf(entry.repo.ref) === slugOf(query.repo)).flatMap((entry) => [...entry.threads.values()].map((thread) => thread.thread)).filter((thread) => (thread.kind === "issue" || thread.kind === "pull_request") && (!query.kind || thread.kind === query.kind) && (state === "all" || (state === "open" ? thread.state === "open" : thread.state !== "open")) && (!text || thread.title.toLowerCase().includes(text) || (thread.body ?? "").toLowerCase().includes(text)) && (!query.author || thread.author?.login === query.author) && (!query.assignee || thread.assignees.some((item) => item.login === query.assignee)) && (!query.involves || thread.author?.login === query.involves || thread.assignees.some((item) => item.login === query.involves)) && (!query.labels?.length || query.labels.every((name) => thread.labels.some((item) => item.name === name))) && (!query.since || (thread.updatedAt?.getTime() ?? 0) >= query.since.getTime()));
498
+ return pageOf(query.direction === "asc" ? items : items.toReversed(), query);
499
+ }),
500
+ reposPage: verb(support("search.repos", true), async (query = {}) => {
501
+ const text = query.text?.toLowerCase();
502
+ return pageOf([...store.repos.values()].map((entry) => entry.repo).filter((repo) => (!query.owner || repo.ref.owner === query.owner) && (!text || `${repo.ref.owner}/${repo.ref.name}`.toLowerCase().includes(text) || (repo.description ?? "").toLowerCase().includes(text))), query);
503
+ }),
504
+ commitsPage: verb(support("search.commits", true), async (query = {}) => {
505
+ const text = query.text?.toLowerCase();
506
+ const items = [...store.repos.values()].filter((entry) => !query.repo || slugOf(entry.repo.ref) === slugOf(query.repo)).flatMap((entry) => entry.commits).filter((commit) => (!text || commit.message.toLowerCase().includes(text)) && (!query.author || commit.author?.name === query.author) && (!query.committer || commit.committer?.name === query.committer));
507
+ return pageOf(query.direction === "asc" ? items : items.toReversed(), query);
508
+ }),
509
+ codePage: verb(support("search.code", true), async (query = {}) => {
510
+ const text = query.text?.toLowerCase();
511
+ if (!text) return pageOf([], query);
512
+ const decoder = new TextDecoder();
513
+ return pageOf([...store.repos.values()].filter((entry) => (!query.repo || slugOf(entry.repo.ref) === slugOf(query.repo)) && (!query.owner || entry.repo.ref.owner === query.owner)).flatMap((entry) => [...entry.files].flatMap(([key, bytes]) => {
514
+ const { ref, path } = splitFileKey(key);
515
+ const fragments = decoder.decode(bytes).split("\n").flatMap((line, index) => line.toLowerCase().includes(text) ? [{
516
+ text: line,
517
+ line: index + 1
518
+ }] : []);
519
+ return fragments.length ? [{
520
+ repo: entry.repo.ref,
521
+ path,
522
+ ref: ref === "*" ? void 0 : ref,
523
+ fragments,
524
+ raw: void 0
525
+ }] : [];
526
+ })), query);
527
+ })
528
+ },
529
+ threads: {
530
+ get: perKind(kinds("threads.get", ALL_KINDS), async (ref) => threadState(ref).thread),
531
+ listPage: perKind(kinds("threads.list", ALL_KINDS), async (repo, query = {}) => {
532
+ const state = query.state ?? "open";
533
+ const items = [...repoState(repo).threads.values()].map((entry) => entry.thread).filter((thread) => (!query.kind || thread.kind === query.kind) && (state === "all" || (state === "open" ? thread.state === "open" : state === "merged" ? thread.state === "merged" : thread.state !== "open")) && (!query.labels?.length || query.labels.every((name) => thread.labels.some((item) => item.name === name))) && (!query.author || thread.author?.login === query.author) && (!query.assignee || thread.assignees.some((item) => item.login === query.assignee)));
534
+ return pageOf(query.direction === "asc" ? items : items.toReversed(), query);
535
+ }),
536
+ getMany: verb(support("threads.getMany", true), (refs) => getManyConcurrently(refs, async (ref) => threadState(ref).thread)),
537
+ eventsPage: verb(true, async (ref, listOptions = {}) => pageOf(store.events.filter((event) => event.thread?.repo.owner === ref.repo.owner && event.thread.repo.name === ref.repo.name && event.thread.number === ref.number), listOptions)),
538
+ commentsPage: perKind(kinds("threads.comments", ALL_KINDS), async (ref, listOptions = {}) => pageOf(threadState(ref).comments, listOptions)),
539
+ reactionsPage: perKind(kinds("threads.reactions", ALL_KINDS), async (target, listOptions = {}) => {
540
+ return pageOf(threadState("thread" in target ? target.thread : target).reactions.get("thread" in target ? target.id : "") ?? [], listOptions);
541
+ }),
542
+ reactions: perKind(kinds("threads.react", WRITABLE), {
543
+ async react(target, reaction) {
544
+ const thread = "thread" in target ? target.thread : target;
545
+ const state = threadState(thread);
546
+ const key = "thread" in target ? target.id : "";
547
+ const left = state.reactions.get(key) ?? [];
548
+ if (!left.some((item) => item.content === reaction && item.actor.login === viewerLogin)) left.push({
549
+ content: reaction,
550
+ contentRaw: reaction,
551
+ actor: actor(viewerLogin),
552
+ createdAt: /* @__PURE__ */ new Date(),
553
+ raw: { content: reaction }
554
+ });
555
+ state.reactions.set(key, left);
556
+ record(thread, "reaction", "created", `reacted ${reaction}`);
557
+ },
558
+ async unreact(target, reaction) {
559
+ const thread = "thread" in target ? target.thread : target;
560
+ const state = threadState(thread);
561
+ const key = "thread" in target ? target.id : "";
562
+ const left = (state.reactions.get(key) ?? []).filter((item) => !(item.content === reaction && item.actor.login === viewerLogin));
563
+ state.reactions.set(key, left);
564
+ record(thread, "reaction", "deleted", `removed a ${reaction} reaction`);
565
+ }
566
+ }),
567
+ comment: perKind(kinds("threads.comment", WRITABLE), async (ref, body) => {
568
+ const state = threadState(ref);
569
+ const comment = {
570
+ ref: {
571
+ ...origin,
572
+ thread: state.thread.ref,
573
+ id: `${ref.number}-${state.comments.length + 1}`
574
+ },
575
+ body,
576
+ author: actor(viewerLogin),
577
+ createdAt: /* @__PURE__ */ new Date(),
578
+ raw: { body }
579
+ };
580
+ state.comments.push(comment);
581
+ state.thread.commentCount = state.comments.length;
582
+ record(state.thread.ref, "comment", "created", "commented", {
583
+ type: "comment",
584
+ comment: comment.ref,
585
+ body
586
+ });
587
+ return comment;
588
+ }),
589
+ editComment: perKind(kinds("threads.editComment", WRITABLE), async (ref, body) => {
590
+ const comment = threadState(ref.thread).comments.find((item) => item.ref.id === ref.id);
591
+ if (!comment) throw notFound(`No comment ${ref.id}`);
592
+ comment.body = body;
593
+ comment.updatedAt = /* @__PURE__ */ new Date();
594
+ record(ref.thread, "comment", "edited", "edited a comment", {
595
+ type: "comment",
596
+ comment: comment.ref,
597
+ body
598
+ });
599
+ return comment;
600
+ }),
601
+ deleteComment: perKind(kinds("threads.deleteComment", WRITABLE), async (ref) => {
602
+ const state = threadState(ref.thread);
603
+ const index = state.comments.findIndex((item) => item.ref.id === ref.id);
604
+ if (index < 0) throw notFound(`No comment ${ref.id}`);
605
+ state.comments.splice(index, 1);
606
+ state.thread.commentCount = state.comments.length;
607
+ record(ref.thread, "comment", "deleted", "deleted a comment", {
608
+ type: "comment",
609
+ comment: ref
610
+ });
611
+ }),
612
+ create: perKind(kinds("threads.create", {
613
+ issue: true,
614
+ pull_request: true,
615
+ discussion: true
616
+ }), async (repo, input) => {
617
+ const thread = addThread({
618
+ repo: slugOf(repo),
619
+ kind: input.kind,
620
+ title: input.title,
621
+ body: input.body,
622
+ labels: input.labels,
623
+ assignees: input.assignees?.map((item) => typeof item === "string" ? item : item.login)
624
+ });
625
+ record(thread.ref, "state_change", "opened", `opened ${thread.kind === "pull_request" ? "a pull request" : `a ${thread.kind}`}`, {
626
+ type: "state_change",
627
+ state: "open"
628
+ });
629
+ return thread;
630
+ }),
631
+ update: perKind(kinds("threads.update", WRITABLE), async (ref, input) => {
632
+ const thread = threadState(ref).thread;
633
+ Object.assign(thread, input.title === void 0 ? {} : { title: input.title }, input.body === void 0 ? {} : { body: input.body });
634
+ record(ref, "other", "edited", "edited the thread");
635
+ return thread;
636
+ }),
637
+ close: perKind(kinds("threads.close", WRITABLE), async (ref, closeOptions = {}) => {
638
+ threadState(ref).thread.stateReason = closeOptions.reasonRaw ?? closeOptions.reason;
639
+ setState(ref, "closed", "closed");
640
+ }),
641
+ reopen: perKind(kinds("threads.reopen", WRITABLE), async (ref) => setState(ref, "open", "reopened")),
642
+ addLabels: perKind(kinds("threads.addLabels", WRITABLE), async (ref, labels) => {
643
+ const thread = threadState(ref).thread;
644
+ for (const name of labels.filter((name) => !thread.labels.some((item) => item.name === name))) {
645
+ thread.labels = [...thread.labels, label(name)];
646
+ record(ref, "label", "labelled", `added label ${name}`, {
647
+ type: "label",
648
+ label: name
649
+ });
650
+ }
651
+ }),
652
+ removeLabels: perKind(kinds("threads.removeLabels", WRITABLE), async (ref, labels) => {
653
+ const thread = threadState(ref).thread;
654
+ for (const name of labels.filter((name) => thread.labels.some((item) => item.name === name))) {
655
+ thread.labels = thread.labels.filter((item) => item.name !== name);
656
+ record(ref, "label", "unlabelled", `removed label ${name}`, {
657
+ type: "label",
658
+ label: name
659
+ });
660
+ }
661
+ }),
662
+ setLabels: perKind(kinds("threads.setLabels", WRITABLE), async (ref, labels) => {
663
+ const thread = threadState(ref).thread;
664
+ const before = new Set(thread.labels.map((item) => item.name));
665
+ const after = new Set(labels);
666
+ thread.labels = labels.map(label);
667
+ for (const name of [...after].filter((name) => !before.has(name))) record(ref, "label", "labelled", `added label ${name}`, {
668
+ type: "label",
669
+ label: name
670
+ });
671
+ for (const name of [...before].filter((name) => !after.has(name))) record(ref, "label", "unlabelled", `removed label ${name}`, {
672
+ type: "label",
673
+ label: name
674
+ });
675
+ }),
676
+ setAssignees: perKind(kinds("threads.setAssignees", WRITABLE), async (ref, assignees) => {
677
+ const thread = threadState(ref).thread;
678
+ thread.assignees = assignees.map((item) => typeof item === "string" ? actor(item) : item);
679
+ for (const assignee of thread.assignees) record(ref, "assignment", "assigned", `assigned ${assignee.login}`, {
680
+ type: "assignment",
681
+ assignee
682
+ });
683
+ }),
684
+ requestReview: perKind(kinds("threads.requestReview", PULLS), async (ref, reviewers) => {
685
+ const thread = threadState(ref).thread;
686
+ for (const reviewer of reviewers.map((item) => typeof item === "string" ? actor(item) : item)) {
687
+ thread.reviewers.push({
688
+ actor: reviewer,
689
+ state: "pending"
690
+ });
691
+ record(ref, "assignment", "review_requested", `requested review from ${reviewer.login}`, {
692
+ type: "assignment",
693
+ assignee: reviewer
694
+ });
695
+ }
696
+ }),
697
+ ...givenSupport("threads.approveAndMerge") === void 0 ? {} : { approveAndMerge: { support: support("threads.approveAndMerge", true) } },
698
+ merge: verb(support("threads.merge", true), async (ref, _mergeOptions = {}, hooks = {}) => {
699
+ if (threadState(ref).thread.kind !== "pull_request") throw notFound(`No pull request ${slugOf(ref.repo)}#${ref.number}`);
700
+ await hooks.beforeMerge?.();
701
+ setState(ref, "merged", "merged");
702
+ }),
703
+ subscriptions: perKind(kinds("threads.subscription", WRITABLE), {
704
+ subscription: async (ref) => threadState(ref).subscription,
705
+ subscribe: async (ref) => {
706
+ threadState(ref).subscription = "subscribed";
707
+ },
708
+ unsubscribe: async (ref) => {
709
+ threadState(ref).subscription = "none";
710
+ }
711
+ }),
712
+ checks: perKind(kinds("threads.checks", PULLS), async (ref) => ({ items: threadState(ref).checks })),
713
+ reviewsPage: verb(support("threads.reviews", true), async (ref, listOptions = {}) => pageOf(threadState(ref).reviews, listOptions)),
714
+ createReview: verb(support("threads.createReview", true), async (ref, input) => createReview(ref, input)),
715
+ submitReview: verb(support("threads.submitReview", true), async (ref, event, body) => {
716
+ const review = threadState(ref.thread).reviews.find((item) => item.ref.id === ref.id);
717
+ if (!review || review.state !== "pending") throw notFound(`No pending review ${ref.id}`);
718
+ review.state = REVIEW_STATES[event];
719
+ review.stateRaw = event;
720
+ review.submittedAt = /* @__PURE__ */ new Date();
721
+ if (body !== void 0) review.body = body;
722
+ record(ref.thread, "review", "submitted", `reviewed (${review.state})`, {
723
+ type: "review",
724
+ state: review.state,
725
+ body: review.body
726
+ });
727
+ return review;
728
+ }),
729
+ filesPage: verb(support("threads.files", true), async (ref, listOptions = {}) => pageOf(threadState(ref).files, listOptions)),
730
+ commitsPage: verb(support("threads.commits", true), async (ref, listOptions = {}) => {
731
+ const state = repoState(ref.repo);
732
+ const shas = new Set(threadState(ref).commits);
733
+ return pageOf(state.commits.filter((commit) => shas.has(commit.sha)), listOptions);
734
+ }),
735
+ reviewThreads: verb(support("threads.resolveReviewThread", true), {
736
+ resolveReviewThread: async (ref, id) => setReviewThreadResolved(ref, id, true),
737
+ unresolveReviewThread: async (ref, id) => setReviewThreadResolved(ref, id, false)
738
+ })
739
+ },
740
+ contents: {
741
+ file: verb(support("contents.file", true), async (repo, path, fileOptions = {}) => {
742
+ const state = repoState(repo);
743
+ const bytes = state.files.get(`${fileOptions.ref ?? "*"}:${path}`) ?? state.files.get(`*:${path}`);
744
+ if (!bytes) throw notFound(`No file ${path} in ${slugOf(repo)}`);
745
+ return toFileContent(bytes, {
746
+ path,
747
+ size: bytes.byteLength
748
+ }, fileOptions, origin);
749
+ }),
750
+ treePage: verb(support("contents.tree", true), async (repo, treeOptions = {}) => {
751
+ const prefix = treeOptions.path?.replace(/^\/|\/$/g, "");
752
+ const paths = [...repoState(repo).files.keys()].flatMap((key) => {
753
+ const { ref, path } = splitFileKey(key);
754
+ return ref === "*" || ref === treeOptions.ref ? [path] : [];
755
+ }).filter((path) => !prefix || path.startsWith(`${prefix}/`));
756
+ return pageOf((treeOptions.recursive ? paths : [...new Set(paths.map((path) => {
757
+ const rest = prefix ? path.slice(prefix.length + 1) : path;
758
+ return prefix ? `${prefix}/${rest.split("/")[0]}` : rest.split("/")[0];
759
+ }))]).toSorted().map((path) => ({
760
+ path,
761
+ type: paths.includes(path) ? "file" : "directory",
762
+ size: repoState(repo).files.get(`*:${path}`)?.byteLength
763
+ })), treeOptions);
764
+ }),
765
+ branchesPage: verb(support("contents.branches", true), async (repo, listOptions = {}) => {
766
+ const state = repoState(repo);
767
+ const name = state.repo.defaultBranch ?? "main";
768
+ return pageOf([{
769
+ name,
770
+ sha: state.commits.at(-1)?.sha ?? "0".repeat(40),
771
+ isDefault: true,
772
+ raw: { name }
773
+ }], listOptions);
774
+ }),
775
+ tagsPage: verb(support("contents.tags", true), async (repo, listOptions = {}) => pageOf(repoState(repo).releases.map((release) => ({
776
+ name: release.tag,
777
+ sha: "0".repeat(40),
778
+ raw: release.raw
779
+ })), listOptions)),
780
+ resolveRef: verb(support("contents.resolveRef", true), async (repo, ref) => {
781
+ const state = repoState(repo);
782
+ const commit = state.commits.find((item) => item.sha === ref) ?? state.commits.at(-1);
783
+ if (!commit) throw notFound(`No commit for ${ref} in ${slugOf(repo)}`);
784
+ return commit.sha;
785
+ }),
786
+ commitsPage: verb(support("contents.commits", true), async (repo, query = {}) => pageOf(repoState(repo).commits.filter((commit) => (!query.since || (commit.author?.date ?? /* @__PURE__ */ new Date(0)) >= query.since) && (!query.path || commit.files?.some((file) => file.path === query.path)) && (!query.author || commit.author?.actor?.login === query.author)).toReversed(), query)),
787
+ commit: verb(support("contents.commit", true), async (repo, sha) => {
788
+ const commit = repoState(repo).commits.find((item) => item.sha === sha);
789
+ if (!commit) throw notFound(`No commit ${sha} in ${slugOf(repo)}`);
790
+ return commit;
791
+ }),
792
+ compare: verb(support("contents.compare", true), async (repo, base, head) => {
793
+ const commits = repoState(repo).commits;
794
+ const from = commits.findIndex((commit) => commit.sha === base);
795
+ const to = commits.findIndex((commit) => commit.sha === head);
796
+ const between = commits.slice(from + 1, to < 0 ? void 0 : to + 1);
797
+ return {
798
+ base,
799
+ head,
800
+ aheadBy: between.length,
801
+ behindBy: 0,
802
+ commits: between,
803
+ files: between.flatMap((commit) => commit.files ?? []),
804
+ raw: {
805
+ base,
806
+ head
807
+ }
808
+ };
809
+ })
810
+ },
811
+ checks: {
812
+ list: verb(support("checks.list", true), async (repo, sha) => ({ items: repoState(repo).statuses.get(sha) ?? [] })),
813
+ report: verb(support("checks.report", true), async (repo, sha, input) => {
814
+ const state = repoState(repo);
815
+ const existing = state.statuses.get(sha) ?? [];
816
+ const check = {
817
+ ref: {
818
+ ...origin,
819
+ repo: state.repo.ref,
820
+ id: input.externalId ?? `${sha}-${existing.length + 1}`,
821
+ type: "status"
822
+ },
823
+ name: input.name,
824
+ state: input.state,
825
+ stateRaw: input.state,
826
+ url: input.url,
827
+ startedAt: /* @__PURE__ */ new Date(),
828
+ raw: input
829
+ };
830
+ state.statuses.set(sha, [...existing.filter((item) => item.name !== input.name), check]);
831
+ return check;
832
+ })
833
+ },
834
+ ci: {
835
+ runsPage: verb(support("ci.runs", true), async (repo, query = {}) => pageOf(repoState(repo).runs.map((item) => item.run).filter((run) => (!query.branch || run.branch === query.branch) && (!query.state || run.state === query.state)).toReversed(), query)),
836
+ run: verb(support("ci.run", true), async (ref) => runState(ref).run),
837
+ jobsPage: verb(support("ci.jobs", true), async (ref, listOptions = {}) => pageOf(runState(ref).jobs.map((item) => item.job), listOptions)),
838
+ log: verb(support("ci.log", true), async (ref) => {
839
+ const job = repoState(ref.repo).runs.flatMap((item) => item.jobs).find((item) => item.job.ref.id === ref.id);
840
+ if (!job) throw notFound(`No job ${ref.id}`);
841
+ return new Blob([job.log]).stream();
842
+ })
843
+ },
844
+ releases: {
845
+ listPage: verb(support("releases.list", true), async (repo, listOptions = {}) => pageOf(repoState(repo).releases.toReversed(), listOptions)),
846
+ get: verb(support("releases.get", true), async (ref) => releaseState(ref)),
847
+ getByTag: verb(support("releases.getByTag", true), async (repo, tag) => {
848
+ const release = repoState(repo).releases.find((item) => item.tag === tag);
849
+ if (!release) throw notFound(`No release tagged ${tag}`);
850
+ return release;
851
+ }),
852
+ downloadAsset: verb(support("releases.downloadAsset", true), async (ref) => {
853
+ const bytes = repoState(ref.repo).assets.get(`${ref.release.id}:${ref.id}`);
854
+ if (!bytes) throw notFound(`No asset ${ref.id} on release ${ref.release.id}`);
855
+ return new Blob([bytes]).stream();
856
+ }),
857
+ latest: verb(support("releases.latest", true), async (repo) => repoState(repo).releases.findLast((release) => !release.isDraft && !release.isPrerelease)),
858
+ create: verb(support("releases.create", true), async (repo, input) => {
859
+ const state = repoState(repo);
860
+ requireFreeTag(state.releases, input.tag);
861
+ const now = /* @__PURE__ */ new Date();
862
+ const release = {
863
+ ref: {
864
+ ...origin,
865
+ repo: state.repo.ref,
866
+ id: String(state.releases.length + 1),
867
+ tag: input.tag
868
+ },
869
+ tag: input.tag,
870
+ name: input.name,
871
+ body: input.body,
872
+ isDraft: input.draft ?? false,
873
+ isPrerelease: input.prerelease ?? false,
874
+ author: actor(viewerLogin),
875
+ createdAt: now,
876
+ publishedAt: input.draft ? void 0 : now,
877
+ assets: [],
878
+ raw: input
879
+ };
880
+ state.releases.push(release);
881
+ return release;
882
+ }),
883
+ update: verb(support("releases.update", true), async (ref, update) => {
884
+ const release = releaseState(ref);
885
+ if (release.immutable && (update.tag !== void 0 && update.tag !== release.tag || update.target !== void 0 || update.draft !== void 0 && update.draft !== release.isDraft)) throw new ForbiddenError(`Release ${release.tag} is immutable; its tag, target and draft state are locked`, 422, "", "immutable_release", origin);
886
+ if (update.tag !== void 0 && update.tag !== release.tag) {
887
+ requireFreeTag(repoState(ref.repo).releases, update.tag);
888
+ release.tag = update.tag;
889
+ release.ref = {
890
+ ...release.ref,
891
+ tag: update.tag
892
+ };
893
+ }
894
+ release.name = update.name ?? release.name;
895
+ release.body = update.body ?? release.body;
896
+ release.isDraft = update.draft ?? release.isDraft;
897
+ release.isPrerelease = update.prerelease ?? release.isPrerelease;
898
+ release.publishedAt = release.isDraft ? void 0 : release.publishedAt ?? /* @__PURE__ */ new Date();
899
+ return release;
900
+ })
901
+ },
902
+ notifications: {
903
+ listPage: verb(support("notifications.list", true), async (listOptions = {}) => pageOf(store.notifications.filter((item) => listOptions.all || item.unread), listOptions)),
904
+ markRead: verb(support("notifications.markRead", true), async (ref) => {
905
+ const notification = store.notifications.find((item) => item.ref.id === ref.id);
906
+ if (notification) notification.unread = false;
907
+ }),
908
+ markDone: verb(support("notifications.markDone", true), async (ref) => {
909
+ store.notifications = store.notifications.filter((item) => item.ref.id !== ref.id);
910
+ }),
911
+ unsubscribe: verb(support("notifications.unsubscribe", true), async (ref) => {
912
+ const notification = store.notifications.find((item) => item.ref.id === ref.id);
913
+ if (notification?.subject.type === "thread") threadState(notification.subject.thread).subscription = "none";
914
+ }),
915
+ markAllRead: verb(support("notifications.markAllRead", true), async () => {
916
+ for (const notification of store.notifications) notification.unread = false;
917
+ }),
918
+ markAllDone: verb(support("notifications.markAllDone", true), async () => {
919
+ store.notifications = [];
920
+ }),
921
+ unreadCount: verb(support("notifications.unreadCount", true), async () => store.notifications.filter((item) => item.unread).length)
922
+ },
923
+ sources: { subscribe: verb(support("sources.subscribe", true), (subscribeOptions = {}) => subscribeTo(subscribeOptions)) },
924
+ webhooks: {
925
+ listPage: verb(support("webhooks.list", true), async (target, listOptions = {}) => pageOf(store.webhooks.filter((hook) => slugOf(hook.ref.target) === slugOf(target)), listOptions)),
926
+ create: verb(support("webhooks.create", true), async (target, input) => {
927
+ const hook = {
928
+ ref: {
929
+ ...origin,
930
+ target,
931
+ id: String(store.webhooks.length + 1)
932
+ },
933
+ url: input.url,
934
+ events: input.events ?? [],
935
+ nativeEvents: input.nativeEvents ?? (input.events ?? []).map((kind) => kind),
936
+ active: input.active ?? true,
937
+ contentType: input.contentType ?? "json",
938
+ createdAt: /* @__PURE__ */ new Date(),
939
+ raw: input
940
+ };
941
+ secrets.set(hook.ref.id, input.secret);
942
+ store.webhooks.push(hook);
943
+ return hook;
944
+ }),
945
+ update: verb(support("webhooks.update", true), async (ref, update) => {
946
+ const hook = webhookState(ref);
947
+ Object.assign(hook, {
948
+ ...update.url ? { url: update.url } : {},
949
+ ...update.events ? { events: update.events } : {},
950
+ ...update.nativeEvents ? { nativeEvents: update.nativeEvents } : {},
951
+ ...update.active === void 0 ? {} : { active: update.active },
952
+ ...update.contentType ? { contentType: update.contentType } : {},
953
+ updatedAt: /* @__PURE__ */ new Date()
954
+ });
955
+ if (update.secret) secrets.set(ref.id, update.secret);
956
+ return hook;
957
+ }),
958
+ delete: verb(support("webhooks.delete", true), async (ref) => {
959
+ store.webhooks.splice(store.webhooks.indexOf(webhookState(ref)), 1);
960
+ secrets.delete(ref.id);
961
+ }),
962
+ rotateSecret: verb(support("webhooks.rotateSecret", true), async (ref, secret) => {
963
+ const hook = webhookState(ref);
964
+ secrets.set(ref.id, secret);
965
+ hook.updatedAt = /* @__PURE__ */ new Date();
966
+ return hook;
967
+ })
968
+ }
969
+ })
970
+ });
971
+ async function* subscribeTo(subscribeOptions) {
972
+ const queue = store.events.slice(Number(subscribeOptions.cursor ?? store.events.length));
973
+ let wake;
974
+ const listener = (event) => {
975
+ queue.push(event);
976
+ wake?.();
977
+ };
978
+ const aborted = new Promise((resolve) => subscribeOptions.signal?.addEventListener("abort", () => resolve(), { once: true }));
979
+ listeners.add(listener);
980
+ try {
981
+ while (!subscribeOptions.signal?.aborted) {
982
+ const event = queue.shift();
983
+ if (event) {
984
+ yield {
985
+ event,
986
+ cursor: event.id
987
+ };
988
+ continue;
989
+ }
990
+ await Promise.race([new Promise((resolve) => {
991
+ wake = resolve;
992
+ }), aborted]);
993
+ }
994
+ } finally {
995
+ listeners.delete(listener);
996
+ }
997
+ }
998
+ return Object.assign(factory(options), { store });
999
+ }
1000
+ //#endregion
1001
+ export { fake };