opencode-forgekeeper 2.0.0 → 2.1.1

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 (4) hide show
  1. package/README.md +10 -3
  2. package/dist/index.js +252 -217
  3. package/dist/tui.js +444 -101
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -185,10 +185,14 @@ class ForgeError extends Error {
185
185
  }
186
186
  }
187
187
 
188
+ // packages/forgekeeper/src/graphql.ts
189
+ var query = (text) => text;
190
+ var typed = (transport) => async (host, query2, variables) => await transport(host, query2, variables);
191
+
188
192
  // packages/forgekeeper/src/gitlab-feedback.ts
189
193
  var PAGE_SIZE = 100;
190
194
  var MAX_COMMENT_PAGES = 20;
191
- var COMMENTS_QUERY = `query($project: ID!, $iid: String!, $before: String) {
195
+ var COMMENTS_QUERY = query(`query($project: ID!, $iid: String!, $before: String) {
192
196
  currentUser { id }
193
197
  project(fullPath: $project) {
194
198
  mergeRequest(iid: $iid) {
@@ -202,7 +206,7 @@ var COMMENTS_QUERY = `query($project: ID!, $iid: String!, $before: String) {
202
206
  }
203
207
  }
204
208
  }
205
- }`;
209
+ }`);
206
210
  var noteNumber = (id) => id.slice(id.lastIndexOf("/") + 1);
207
211
  var timestamp = (node) => Date.parse(node.lastEditedAt ?? node.updatedAt ?? node.createdAt);
208
212
  function toComment(node) {
@@ -221,7 +225,8 @@ function toComment(node) {
221
225
  username: node.author.username
222
226
  };
223
227
  }
224
- async function fetchComments(graphql, host, project, iid) {
228
+ async function fetchComments(transport, host, project, iid) {
229
+ const graphql = typed(transport);
225
230
  const pages = [];
226
231
  let viewerID;
227
232
  let before;
@@ -235,8 +240,8 @@ async function fetchComments(graphql, host, project, iid) {
235
240
  if (before)
236
241
  variables.before = before;
237
242
  const data = await graphql(host, COMMENTS_QUERY, variables);
238
- viewerID ??= data?.currentUser?.id ?? undefined;
239
- const connection = data?.project?.mergeRequest?.notes;
243
+ viewerID ??= data.currentUser?.id ?? undefined;
244
+ const connection = data.project?.mergeRequest?.notes;
240
245
  if (!connection)
241
246
  return page === 0 ? undefined : snapshot(false);
242
247
  pages.push(connection.nodes.flatMap((node) => toComment(node) ?? []));
@@ -268,17 +273,17 @@ function mergeRequestsQuery(projectCount) {
268
273
  mergeRequests(sourceBranches: [$branch], state: opened, first: 20) { nodes { ${MERGE_REQUEST_FIELDS} } }
269
274
  }`).join(`
270
275
  `);
271
- return `query($branch: String!, ${variables}) {
276
+ return query(`query($branch: String!, ${variables}) {
272
277
  ${projects}
273
- }`;
278
+ }`);
274
279
  }
275
280
  function mergeRequestQuery(projectCount) {
276
281
  const variables = Array.from({ length: projectCount }, (_, index) => `$p${index}: ID!`).join(", ");
277
282
  const projects = Array.from({ length: projectCount }, (_, index) => `p${index}: project(fullPath: $p${index}) { mergeRequest(iid: $iid) { ${MERGE_REQUEST_FIELDS} } }`).join(`
278
283
  `);
279
- return `query($iid: String!, ${variables}) {
284
+ return query(`query($iid: String!, ${variables}) {
280
285
  ${projects}
281
- }`;
286
+ }`);
282
287
  }
283
288
  function mergeRequestNumbersQuery(projectCount) {
284
289
  const variables = Array.from({ length: projectCount }, (_, index) => `$p${index}: ID!`).join(", ");
@@ -286,18 +291,18 @@ function mergeRequestNumbersQuery(projectCount) {
286
291
  mergeRequests(sourceBranches: [$branch], state: opened, first: 20) { nodes { iid updatedAt sourceProject { fullPath } } }
287
292
  }`).join(`
288
293
  `);
289
- return `query($branch: String!, ${variables}) {
294
+ return query(`query($branch: String!, ${variables}) {
290
295
  ${projects}
291
- }`;
296
+ }`);
292
297
  }
293
- var SOURCE_BRANCH_QUERY = `query($project: ID!, $iid: String!) {
298
+ var SOURCE_BRANCH_QUERY = query(`query($project: ID!, $iid: String!) {
294
299
  project(fullPath: $project) { mergeRequest(iid: $iid) { sourceBranch } }
295
- }`;
296
- var DISCUSSIONS_QUERY = `query($project: ID!, $iid: String!, $after: String) {
300
+ }`);
301
+ var DISCUSSIONS_QUERY = query(`query($project: ID!, $iid: String!, $after: String) {
297
302
  project(fullPath: $project) {
298
303
  mergeRequest(iid: $iid) { discussions(first: ${PAGE_SIZE2}, after: $after) { ${DISCUSSIONS} } }
299
304
  }
300
- }`;
305
+ }`);
301
306
  function classifyError(message, code) {
302
307
  const text = message.trim().replace(/^glab:\s*/, "") || `glab exited with code ${code}`;
303
308
  if (/ENOENT|command not found/i.test(text))
@@ -312,8 +317,8 @@ function classifyError(message, code) {
312
317
  }
313
318
  var isResponse = (body) => typeof body === "object" && body !== null && (!("errors" in body) || Array.isArray(body.errors)) && (!("data" in body) || typeof body.data === "object");
314
319
  function glabGraphQL(run, cwd) {
315
- return async (host, query, variables) => {
316
- const args = ["api", "graphql", "--hostname", host, "-f", `query=${query}`];
320
+ return async (host, query2, variables) => {
321
+ const args = ["api", "graphql", "--hostname", host, "-f", `query=${query2}`];
317
322
  for (const [name, value] of Object.entries(variables))
318
323
  args.push("-f", `${name}=${value}`);
319
324
  const result = await run("glab", args, cwd);
@@ -381,10 +386,10 @@ function duoState(state) {
381
386
  }
382
387
 
383
388
  class GitLabForge {
384
- graphql;
385
389
  traits = gitlabTraits;
390
+ graphql;
386
391
  constructor(graphql) {
387
- this.graphql = graphql;
392
+ this.graphql = typed(graphql);
388
393
  }
389
394
  async findByBranch(repository) {
390
395
  const { source, targets, sourceBranch } = repository;
@@ -395,7 +400,7 @@ class GitLabForge {
395
400
  const seen = new Set;
396
401
  const nodes = [];
397
402
  targets.forEach((_, index) => {
398
- for (const node of data?.[`p${index}`]?.mergeRequests?.nodes ?? []) {
403
+ for (const node of data[`p${index}`]?.mergeRequests?.nodes ?? []) {
399
404
  if (node.sourceProject?.fullPath.toLowerCase() !== sourcePath)
400
405
  continue;
401
406
  const key = `${node.targetProject.fullPath}!${node.iid}`;
@@ -414,7 +419,7 @@ class GitLabForge {
414
419
  targets.forEach((target, index) => variables[`p${index}`] = target.path);
415
420
  const data = await this.graphql(source.host, mergeRequestNumbersQuery(targets.length), variables);
416
421
  const sourcePath = source.path.toLowerCase();
417
- const nodes = targets.flatMap((_, index) => data?.[`p${index}`]?.mergeRequests?.nodes ?? []);
422
+ const nodes = targets.flatMap((_, index) => data[`p${index}`]?.mergeRequests?.nodes ?? []);
418
423
  return nodes.filter((node) => node.sourceProject?.fullPath.toLowerCase() === sourcePath).sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))[0]?.iid;
419
424
  }
420
425
  async sourceBranch(ref) {
@@ -422,7 +427,7 @@ class GitLabForge {
422
427
  project: ref.project,
423
428
  iid: ref.iid
424
429
  });
425
- const branch = data?.project?.mergeRequest?.sourceBranch;
430
+ const branch = data.project?.mergeRequest?.sourceBranch;
426
431
  return typeof branch === "string" && branch ? branch : undefined;
427
432
  }
428
433
  async findByNumber(projects, iid) {
@@ -433,7 +438,7 @@ class GitLabForge {
433
438
  projects.forEach((project, index) => variables[`p${index}`] = project.path);
434
439
  const data = await this.graphql(host, mergeRequestQuery(projects.length), variables);
435
440
  for (let index = 0;index < projects.length; index++) {
436
- const node = data?.[`p${index}`]?.mergeRequest ?? undefined;
441
+ const node = data[`p${index}`]?.mergeRequest;
437
442
  if (node)
438
443
  return this.normalize(host, node);
439
444
  }
@@ -449,7 +454,7 @@ class GitLabForge {
449
454
  iid: node.iid,
450
455
  after: page.endCursor
451
456
  });
452
- const next = data?.project?.mergeRequest?.discussions;
457
+ const next = data.project?.mergeRequest?.discussions;
453
458
  if (!next)
454
459
  break;
455
460
  count += countUnresolved(next);
@@ -505,8 +510,8 @@ var fields = `number title url state isDraft mergeable reviewDecision headRefOid
505
510
  } } } } }`;
506
511
  var MAX_PAGES = 50;
507
512
  function ghGraphQL(run, cwd) {
508
- return async (host, query, variables) => {
509
- const args = ["api", "graphql", "--hostname", host, "-f", `query=${query}`];
513
+ return async (host, query2, variables) => {
514
+ const args = ["api", "graphql", "--hostname", host, "-f", `query=${query2}`];
510
515
  for (const [name, value] of Object.entries(variables))
511
516
  args.push(typeof value === "number" ? "-F" : "-f", `${name}=${value}`);
512
517
  const result = await run("gh", args, cwd);
@@ -596,17 +601,17 @@ var githubTraits = {
596
601
  };
597
602
 
598
603
  class GitHubForge {
599
- graphql;
600
604
  traits = githubTraits;
605
+ graphql;
601
606
  constructor(graphql) {
602
- this.graphql = graphql;
607
+ this.graphql = typed(graphql);
603
608
  }
604
609
  async findByNumber(projects, number) {
605
610
  for (const project of projects) {
606
611
  const [owner, repo] = project.path.split("/");
607
- const data = await this.graphql(project.host, `query($owner:String!,$repo:String!,$number:Int!) {
612
+ const data = await this.graphql(project.host, query(`query($owner:String!,$repo:String!,$number:Int!) {
608
613
  repository(owner:$owner,name:$repo) { pullRequest(number:$number) { ${fields} } }
609
- }`, { owner, repo, number: Number(number) });
614
+ }`), { owner, repo, number: Number(number) });
610
615
  const node = data.repository?.pullRequest;
611
616
  if (node)
612
617
  return this.normalize(project.host, node);
@@ -617,10 +622,10 @@ class GitHubForge {
617
622
  let newest;
618
623
  for (const target of repository.targets) {
619
624
  const [owner, repo] = target.path.split("/");
620
- const data = await this.graphql(target.host, `query($owner:String!,$repo:String!,$branch:String!) {
625
+ const data = await this.graphql(target.host, query(`query($owner:String!,$repo:String!,$branch:String!) {
621
626
  repository(owner:$owner,name:$repo) { pullRequests(first:100,states:OPEN,headRefName:$branch,orderBy:{field:UPDATED_AT,direction:DESC}) {
622
627
  nodes { number updatedAt headRepository { nameWithOwner } }
623
- } } }`, { owner, repo, branch: repository.sourceBranch });
628
+ } } }`), { owner, repo, branch: repository.sourceBranch });
624
629
  const nodes = data.repository?.pullRequests?.nodes ?? [];
625
630
  for (const node of nodes) {
626
631
  if (node.headRepository?.nameWithOwner.toLowerCase() !== repository.source.path.toLowerCase())
@@ -633,11 +638,10 @@ class GitHubForge {
633
638
  }
634
639
  async sourceBranch(ref) {
635
640
  const [owner, repo] = ref.project.split("/");
636
- const data = await this.graphql(ref.host, `query($owner:String!,$repo:String!,$number:Int!) {
641
+ const data = await this.graphql(ref.host, query(`query($owner:String!,$repo:String!,$number:Int!) {
637
642
  repository(owner:$owner,name:$repo) { pullRequest(number:$number) { headRefName } }
638
- }`, { owner, repo, number: Number(ref.iid) });
639
- const branch = data.repository?.pullRequest?.headRefName;
640
- return typeof branch === "string" && branch ? branch : undefined;
643
+ }`), { owner, repo, number: Number(ref.iid) });
644
+ return data.repository?.pullRequest?.headRefName || undefined;
641
645
  }
642
646
  async findByBranch(repository) {
643
647
  const found = new Map;
@@ -646,10 +650,10 @@ class GitHubForge {
646
650
  let after;
647
651
  let complete = false;
648
652
  for (let page = 0;page < MAX_PAGES; page++) {
649
- const data = await this.graphql(target.host, `query($owner:String!,$repo:String!,$branch:String!${after ? ",$after:String!" : ""}) {
653
+ const data = await this.graphql(target.host, query(`query($owner:String!,$repo:String!,$branch:String!${after ? ",$after:String!" : ""}) {
650
654
  repository(owner:$owner,name:$repo) { pullRequests(first:100,states:OPEN,headRefName:$branch${after ? ",after:$after" : ""}) {
651
655
  nodes { ${fields} } pageInfo { hasNextPage endCursor }
652
- } } }`, { owner, repo, branch: repository.sourceBranch, ...after ? { after } : {} });
656
+ } } }`), { owner, repo, branch: repository.sourceBranch, ...after ? { after } : {} });
653
657
  const pulls = data.repository?.pullRequests;
654
658
  if (!pulls)
655
659
  throw new ForgeError("request", "GitHub repository unavailable");
@@ -677,10 +681,10 @@ class GitHubForge {
677
681
  const threads = [...node.reviewThreads.nodes];
678
682
  let page = node.reviewThreads.pageInfo;
679
683
  for (let count = 1;page?.hasNextPage && page.endCursor && count < MAX_PAGES; count++) {
680
- const data = await this.graphql(host, `query($owner:String!,$repo:String!,$number:Int!,$after:String!) {
684
+ const data = await this.graphql(host, query(`query($owner:String!,$repo:String!,$number:Int!,$after:String!) {
681
685
  repository(owner:$owner,name:$repo) { pullRequest(number:$number) {
682
686
  reviewThreads(first:100,after:$after) { nodes { isResolved } pageInfo { hasNextPage endCursor } }
683
- } } }`, { owner, repo, number: node.number, after: page.endCursor });
687
+ } } }`), { owner, repo, number: node.number, after: page.endCursor });
684
688
  const next = data.repository?.pullRequest?.reviewThreads;
685
689
  if (!next)
686
690
  break;
@@ -698,10 +702,10 @@ class GitHubForge {
698
702
  const contexts = [...rollup?.nodes ?? []];
699
703
  let cursor = rollup?.pageInfo;
700
704
  for (let count = 1;cursor?.hasNextPage && cursor.endCursor && count < MAX_PAGES; count++) {
701
- const data = await this.graphql(host, `query($owner:String!,$repo:String!,$oid:GitObjectID!,$after:String!) {
705
+ const data = await this.graphql(host, query(`query($owner:String!,$repo:String!,$oid:GitObjectID!,$after:String!) {
702
706
  repository(owner:$owner,name:$repo) { object(oid:$oid) { ... on Commit { statusCheckRollup {
703
707
  contexts(first:100,after:$after) { nodes { __typename ... on CheckRun { status conclusion detailsUrl } ... on StatusContext { state targetUrl } } pageInfo { hasNextPage endCursor } }
704
- } } } } }`, { owner, repo, oid: node.headRefOid, after: cursor.endCursor });
708
+ } } } } }`), { owner, repo, oid: node.headRefOid, after: cursor.endCursor });
705
709
  const next = data.repository?.object?.statusCheckRollup?.contexts;
706
710
  if (!next)
707
711
  break;
@@ -910,11 +914,12 @@ var target = {
910
914
  additionalProperties: false
911
915
  };
912
916
  var key = { type: "string" };
913
- var snapshot = { type: "object" };
917
+ var snapshot = { type: "object", title: "Snapshot" };
914
918
  var status = {
915
919
  type: "object",
916
920
  properties: { enabled: { type: "boolean" }, snapshot },
917
- required: ["enabled", "snapshot"]
921
+ required: ["enabled", "snapshot"],
922
+ additionalProperties: false
918
923
  };
919
924
  var ForgeSessionRpc = {
920
925
  id: "opencode-forgekeeper",
@@ -941,8 +946,12 @@ var ForgeSessionRpc = {
941
946
  },
942
947
  output: {
943
948
  type: "object",
944
- properties: { enabled: { type: "boolean" }, statuses: { type: "object" } },
945
- required: ["enabled", "statuses"]
949
+ properties: {
950
+ enabled: { type: "boolean" },
951
+ statuses: { type: "object", additionalProperties: snapshot }
952
+ },
953
+ required: ["enabled", "statuses"],
954
+ additionalProperties: false
946
955
  }
947
956
  },
948
957
  release: {
@@ -952,7 +961,7 @@ var ForgeSessionRpc = {
952
961
  required: ["clientID"],
953
962
  additionalProperties: false
954
963
  },
955
- output: { type: "object" }
964
+ output: { type: "object", additionalProperties: false }
956
965
  },
957
966
  refresh: {
958
967
  input: {
@@ -977,7 +986,8 @@ var ForgeSessionRpc = {
977
986
  schema: {
978
987
  type: "object",
979
988
  properties: { directory: { type: "string" }, key, snapshot },
980
- required: ["directory", "key", "snapshot"]
989
+ required: ["directory", "key", "snapshot"],
990
+ additionalProperties: false
981
991
  }
982
992
  },
983
993
  notice: {
@@ -989,7 +999,8 @@ var ForgeSessionRpc = {
989
999
  message: { type: "string" },
990
1000
  variant: { type: "string", enum: ["info", "error"] }
991
1001
  },
992
- required: ["message", "variant"]
1002
+ required: ["message", "variant"],
1003
+ additionalProperties: false
993
1004
  }
994
1005
  }
995
1006
  }
@@ -997,6 +1008,12 @@ var ForgeSessionRpc = {
997
1008
  var HOME = "";
998
1009
  var HEARTBEAT = 60000;
999
1010
  var LEASE_TIME = 3 * HEARTBEAT;
1011
+ var implement = (handlers) => ({
1012
+ target: (input) => handlers.target(input),
1013
+ watch: (input) => handlers.watch(input),
1014
+ release: (input) => handlers.release(input),
1015
+ refresh: (input) => handlers.refresh(input)
1016
+ });
1000
1017
 
1001
1018
  // packages/forgekeeper/src/automated-review-watch.ts
1002
1019
  var requests = (lookup) => lookup?.kind === "found" ? lookup.requests : [];
@@ -1238,6 +1255,175 @@ function createFeedbackWatcher(options) {
1238
1255
  };
1239
1256
  }
1240
1257
 
1258
+ // packages/forgekeeper/src/store.ts
1259
+ class TargetLookupError extends Error {
1260
+ cause;
1261
+ constructor(cause) {
1262
+ super(cause instanceof Error ? cause.message : String(cause));
1263
+ this.cause = cause;
1264
+ }
1265
+ }
1266
+ function createStatusStore(options) {
1267
+ const now = options.now ?? Date.now;
1268
+ const setTimer = options.setTimer ?? ((callback, delay) => setTimeout(callback, delay));
1269
+ const clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
1270
+ const interval = options.interval ?? 120000;
1271
+ const maxBackoff = options.maxBackoff ?? 900000;
1272
+ const minEventInterval = options.minEventInterval ?? 15000;
1273
+ const entries = new Map;
1274
+ let disposed = false;
1275
+ let revision = now() * 1000;
1276
+ const entry = (key2) => {
1277
+ let value = entries.get(key2);
1278
+ if (!value) {
1279
+ value = { snapshot: { loading: false }, refs: 0, failures: 0 };
1280
+ entries.set(key2, value);
1281
+ }
1282
+ return value;
1283
+ };
1284
+ const update = (key2, value, patch) => {
1285
+ value.snapshot = { ...value.snapshot, ...patch, revision: ++revision };
1286
+ if (!disposed)
1287
+ options.onChange(key2);
1288
+ };
1289
+ const cancelTimer = (value) => {
1290
+ if (value.timer === undefined)
1291
+ return;
1292
+ clearTimer(value.timer);
1293
+ value.timer = undefined;
1294
+ };
1295
+ const baseInterval = (value) => options.active?.(value.snapshot.lookup) ? options.activeInterval ?? interval : interval;
1296
+ const pollDelay = (value) => Math.min(baseInterval(value) * 2 ** value.failures, maxBackoff);
1297
+ const lastAttempt = (value) => Math.max(value.snapshot.fetchedAt ?? -Infinity, value.snapshot.error?.at ?? -Infinity);
1298
+ const schedule = (key2, value, delay = pollDelay(value)) => {
1299
+ cancelTimer(value);
1300
+ if (disposed || value.refs === 0)
1301
+ return;
1302
+ const lookup = value.snapshot.lookup;
1303
+ if (!value.snapshot.error && (lookup?.kind !== "found" || lookup.requests.length === 0))
1304
+ return;
1305
+ value.timer = setTimer(() => {
1306
+ value.timer = undefined;
1307
+ refresh(key2);
1308
+ }, delay);
1309
+ };
1310
+ function refresh(key2) {
1311
+ if (disposed)
1312
+ return Promise.resolve({ loading: false });
1313
+ const value = entry(key2);
1314
+ if (value.inflight)
1315
+ return value.inflight;
1316
+ value.stale = false;
1317
+ update(key2, value, { loading: true });
1318
+ value.inflight = options.load(key2).then((lookup) => {
1319
+ if (disposed || value.invalidated)
1320
+ return;
1321
+ const previous = value.snapshot.lookup;
1322
+ value.failures = 0;
1323
+ update(key2, value, {
1324
+ lookup,
1325
+ fetchedAt: now(),
1326
+ error: undefined,
1327
+ targeted: lookup.explicitTarget === true ? true : undefined,
1328
+ loading: false
1329
+ });
1330
+ try {
1331
+ if (!disposed)
1332
+ options.onLoad?.(key2, previous, lookup);
1333
+ } catch (error) {
1334
+ try {
1335
+ options.onLoadError?.(error);
1336
+ } catch {}
1337
+ }
1338
+ }, (error) => {
1339
+ if (disposed || value.invalidated)
1340
+ return;
1341
+ value.failures++;
1342
+ const target2 = error instanceof TargetLookupError;
1343
+ const cause = target2 ? error.cause : error;
1344
+ update(key2, value, {
1345
+ loading: false,
1346
+ error: {
1347
+ kind: cause instanceof ForgeError ? cause.kind : "request",
1348
+ message: cause instanceof Error ? cause.message : String(cause),
1349
+ at: now()
1350
+ },
1351
+ ...target2 ? { targeted: true } : {}
1352
+ });
1353
+ }).then(() => {
1354
+ value.inflight = undefined;
1355
+ if (value.invalidated && !disposed) {
1356
+ value.invalidated = false;
1357
+ return refresh(key2);
1358
+ }
1359
+ schedule(key2, value);
1360
+ return value.snapshot;
1361
+ });
1362
+ return value.inflight;
1363
+ }
1364
+ return {
1365
+ get(key2) {
1366
+ return entries.get(key2)?.snapshot ?? { loading: false };
1367
+ },
1368
+ acquire(key2) {
1369
+ const value = entry(key2);
1370
+ value.refs++;
1371
+ if (value.refs === 1 && !value.inflight) {
1372
+ const remaining = lastAttempt(value) + pollDelay(value) - now();
1373
+ if (value.stale || remaining <= 0)
1374
+ refresh(key2);
1375
+ else
1376
+ schedule(key2, value, remaining);
1377
+ }
1378
+ let released = false;
1379
+ return () => {
1380
+ if (released)
1381
+ return;
1382
+ released = true;
1383
+ value.refs--;
1384
+ if (value.refs === 0)
1385
+ cancelTimer(value);
1386
+ };
1387
+ },
1388
+ refresh,
1389
+ keys() {
1390
+ return [...entries.keys()];
1391
+ },
1392
+ notify(key2) {
1393
+ const value = entries.get(key2);
1394
+ if (!value || value.refs === 0 || value.inflight)
1395
+ return;
1396
+ const last = Math.max(value.snapshot.fetchedAt ?? 0, value.snapshot.error?.at ?? 0);
1397
+ if (now() - last < minEventInterval)
1398
+ return;
1399
+ refresh(key2);
1400
+ },
1401
+ invalidate(key2) {
1402
+ const value = entries.get(key2);
1403
+ if (!value)
1404
+ return;
1405
+ if (value.inflight)
1406
+ value.invalidated = true;
1407
+ else if (value.refs > 0)
1408
+ refresh(key2);
1409
+ else
1410
+ value.stale = true;
1411
+ },
1412
+ setTargeted(key2, targeted) {
1413
+ const value = entries.get(key2);
1414
+ if (!value || disposed || value.snapshot.targeted === true === targeted)
1415
+ return;
1416
+ update(key2, value, { targeted: targeted ? true : undefined });
1417
+ },
1418
+ dispose() {
1419
+ disposed = true;
1420
+ for (const value of entries.values())
1421
+ cancelTimer(value);
1422
+ entries.clear();
1423
+ }
1424
+ };
1425
+ }
1426
+
1241
1427
  // packages/forgekeeper/src/target.ts
1242
1428
  var MAX_TARGETS = 50;
1243
1429
  var PREFIX_REFERENCES = 4;
@@ -1449,11 +1635,12 @@ async function locateTargets(forges, directory, targets, previous) {
1449
1635
  });
1450
1636
  });
1451
1637
  if (error !== undefined && !outcomes.some((outcome) => ("request" in outcome)))
1452
- throw error;
1638
+ throw new TargetLookupError(error);
1453
1639
  if (found.length === 0)
1454
1640
  return {
1455
1641
  kind: "none",
1456
- reason: requests2.length === 1 ? failed[0].reason : "No session target PR/MR could be resolved"
1642
+ reason: requests2.length === 1 ? failed[0].reason : "No session target PR/MR could be resolved",
1643
+ explicitTarget: true
1457
1644
  };
1458
1645
  return {
1459
1646
  kind: "found",
@@ -1499,154 +1686,6 @@ async function locate(forges, hints) {
1499
1686
  };
1500
1687
  }
1501
1688
 
1502
- // packages/forgekeeper/src/store.ts
1503
- function createStatusStore(options) {
1504
- const now = options.now ?? Date.now;
1505
- const setTimer = options.setTimer ?? ((callback, delay) => setTimeout(callback, delay));
1506
- const clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
1507
- const interval = options.interval ?? 120000;
1508
- const maxBackoff = options.maxBackoff ?? 900000;
1509
- const minEventInterval = options.minEventInterval ?? 15000;
1510
- const entries = new Map;
1511
- let disposed = false;
1512
- let revision = now() * 1000;
1513
- const entry = (key2) => {
1514
- let value = entries.get(key2);
1515
- if (!value) {
1516
- value = { snapshot: { loading: false }, refs: 0, failures: 0 };
1517
- entries.set(key2, value);
1518
- }
1519
- return value;
1520
- };
1521
- const update = (key2, value, patch) => {
1522
- value.snapshot = { ...value.snapshot, ...patch, revision: ++revision };
1523
- if (!disposed)
1524
- options.onChange(key2);
1525
- };
1526
- const cancelTimer = (value) => {
1527
- if (value.timer === undefined)
1528
- return;
1529
- clearTimer(value.timer);
1530
- value.timer = undefined;
1531
- };
1532
- const baseInterval = (value) => options.active?.(value.snapshot.lookup) ? options.activeInterval ?? interval : interval;
1533
- const pollDelay = (value) => Math.min(baseInterval(value) * 2 ** value.failures, maxBackoff);
1534
- const lastAttempt = (value) => Math.max(value.snapshot.fetchedAt ?? -Infinity, value.snapshot.error?.at ?? -Infinity);
1535
- const schedule = (key2, value, delay = pollDelay(value)) => {
1536
- cancelTimer(value);
1537
- if (disposed || value.refs === 0)
1538
- return;
1539
- const lookup = value.snapshot.lookup;
1540
- if (!value.snapshot.error && (lookup?.kind !== "found" || lookup.requests.length === 0))
1541
- return;
1542
- value.timer = setTimer(() => {
1543
- value.timer = undefined;
1544
- refresh(key2);
1545
- }, delay);
1546
- };
1547
- function refresh(key2) {
1548
- if (disposed)
1549
- return Promise.resolve({ loading: false });
1550
- const value = entry(key2);
1551
- if (value.inflight)
1552
- return value.inflight;
1553
- value.stale = false;
1554
- update(key2, value, { loading: true });
1555
- value.inflight = options.load(key2).then((lookup) => {
1556
- if (disposed || value.invalidated)
1557
- return;
1558
- const previous = value.snapshot.lookup;
1559
- value.failures = 0;
1560
- update(key2, value, { lookup, fetchedAt: now(), error: undefined, loading: false });
1561
- try {
1562
- if (!disposed)
1563
- options.onLoad?.(key2, previous, lookup);
1564
- } catch (error) {
1565
- try {
1566
- options.onLoadError?.(error);
1567
- } catch {}
1568
- }
1569
- }, (error) => {
1570
- if (disposed || value.invalidated)
1571
- return;
1572
- value.failures++;
1573
- const known = error instanceof ForgeError;
1574
- update(key2, value, {
1575
- loading: false,
1576
- error: {
1577
- kind: known ? error.kind : "request",
1578
- message: error instanceof Error ? error.message : String(error),
1579
- at: now()
1580
- }
1581
- });
1582
- }).then(() => {
1583
- value.inflight = undefined;
1584
- if (value.invalidated && !disposed) {
1585
- value.invalidated = false;
1586
- return refresh(key2);
1587
- }
1588
- schedule(key2, value);
1589
- return value.snapshot;
1590
- });
1591
- return value.inflight;
1592
- }
1593
- return {
1594
- get(key2) {
1595
- return entries.get(key2)?.snapshot ?? { loading: false };
1596
- },
1597
- acquire(key2) {
1598
- const value = entry(key2);
1599
- value.refs++;
1600
- if (value.refs === 1 && !value.inflight) {
1601
- const remaining = lastAttempt(value) + pollDelay(value) - now();
1602
- if (value.stale || remaining <= 0)
1603
- refresh(key2);
1604
- else
1605
- schedule(key2, value, remaining);
1606
- }
1607
- let released = false;
1608
- return () => {
1609
- if (released)
1610
- return;
1611
- released = true;
1612
- value.refs--;
1613
- if (value.refs === 0)
1614
- cancelTimer(value);
1615
- };
1616
- },
1617
- refresh,
1618
- keys() {
1619
- return [...entries.keys()];
1620
- },
1621
- notify(key2) {
1622
- const value = entries.get(key2);
1623
- if (!value || value.refs === 0 || value.inflight)
1624
- return;
1625
- const last = Math.max(value.snapshot.fetchedAt ?? 0, value.snapshot.error?.at ?? 0);
1626
- if (now() - last < minEventInterval)
1627
- return;
1628
- refresh(key2);
1629
- },
1630
- invalidate(key2) {
1631
- const value = entries.get(key2);
1632
- if (!value)
1633
- return;
1634
- if (value.inflight)
1635
- value.invalidated = true;
1636
- else if (value.refs > 0)
1637
- refresh(key2);
1638
- else
1639
- value.stale = true;
1640
- },
1641
- dispose() {
1642
- disposed = true;
1643
- for (const value of entries.values())
1644
- cancelTimer(value);
1645
- entries.clear();
1646
- }
1647
- };
1648
- }
1649
-
1650
1689
  // packages/forgekeeper/src/status-service.ts
1651
1690
  function createStatusService(options) {
1652
1691
  const now = options.now ?? Date.now;
@@ -1774,6 +1813,7 @@ function createStatusService(options) {
1774
1813
  if (!disposed && !holds.has(key2) && liveLeases(key2).length > 0)
1775
1814
  store.refresh(key2);
1776
1815
  },
1816
+ setTargeted: (key2, targeted) => store.setTargeted(key2, targeted),
1777
1817
  notify: (key2) => store.notify(key2),
1778
1818
  keys: () => store.keys(),
1779
1819
  polling: (key2) => holds.has(key2),
@@ -1978,7 +2018,11 @@ async function setupStatus(ctx, settings, deps, host) {
1978
2018
  });
1979
2019
  return {
1980
2020
  service,
1981
- targetsChanged: (sessionID) => service.invalidate(sessionID),
2021
+ targetsChanged(sessionID, targets) {
2022
+ const classified = classifyTargets(targetOutput(targets)) ?? [];
2023
+ service.setTargeted(sessionID, classified.some((target2) => target2.kind === "merge-request"));
2024
+ service.invalidate(sessionID);
2025
+ },
1982
2026
  async titleChanged(sessionID) {
1983
2027
  if (!service.keys().includes(sessionID))
1984
2028
  return;
@@ -2112,24 +2156,15 @@ async function setupServer(ctx, lookups, status2) {
2112
2156
  }
2113
2157
  };
2114
2158
  let watcher;
2115
- const rpc = await ctx.rpc.register(ForgeSessionRpc, {
2116
- target: async (input) => {
2117
- const { sessionID } = input;
2118
- return targetOutput((await stateFor(sessionID)).targets);
2119
- },
2120
- watch: async (input) => {
2121
- const { clientID, keys, visible } = input;
2122
- return watcher ? { enabled: true, statuses: json(watcher.service.watch(clientID, keys, visible)) } : { enabled: false, statuses: {} };
2123
- },
2124
- release: async (input) => {
2125
- watcher?.service.release(input.clientID);
2159
+ const rpc = await ctx.rpc.register(ForgeSessionRpc, implement({
2160
+ target: async ({ sessionID }) => targetOutput((await stateFor(sessionID)).targets),
2161
+ watch: async ({ clientID, keys, visible }) => watcher ? { enabled: true, statuses: json(watcher.service.watch(clientID, keys, visible)) } : { enabled: false, statuses: {} },
2162
+ release: async ({ clientID }) => {
2163
+ watcher?.service.release(clientID);
2126
2164
  return {};
2127
2165
  },
2128
- refresh: async (input) => {
2129
- const { key: key2 } = input;
2130
- return watcher ? { enabled: true, snapshot: json(await watcher.service.refresh(key2)) } : DISABLED;
2131
- }
2132
- });
2166
+ refresh: async ({ key: key2 }) => watcher ? { enabled: true, snapshot: json(await watcher.service.refresh(key2)) } : DISABLED
2167
+ }));
2133
2168
  const settings = statusOptions(ctx.options);
2134
2169
  if (status2 && settings.enabled) {
2135
2170
  watcher = await setupStatus(ctx, settings, status2, {
@@ -2235,7 +2270,7 @@ async function setupServer(ctx, lookups, status2) {
2235
2270
  targets: result.targets,
2236
2271
  ...state.prefix ? { prefix: state.prefix } : {}
2237
2272
  });
2238
- watcher?.targetsChanged(sessionID);
2273
+ watcher?.targetsChanged(sessionID, result.targets);
2239
2274
  await rpc.events.emit("targetChanged", { sessionID, ...targetOutput(result.targets) });
2240
2275
  await update(sessionID);
2241
2276
  });