@open-tomato/rafa 0.33.0 → 0.34.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.
@@ -321,6 +321,7 @@ import {
321
321
  sessionLine,
322
322
  sessionLogDir,
323
323
  setUpBoard,
324
+ setUpLabels,
324
325
  severalRoadmapsMessage,
325
326
  shellQuote,
326
327
  show_default,
@@ -356,7 +357,7 @@ import {
356
357
  worktreeAt,
357
358
  writeSession,
358
359
  writeShippedFile
359
- } from "./index-w3vyb08t.js";
360
+ } from "./index-s8ff86xt.js";
360
361
  import {
361
362
  PROBE_TIMEOUT_MS,
362
363
  loadPlanPrerequisites,
@@ -368,7 +369,7 @@ import {
368
369
  runPreflight,
369
370
  splitBlockerComment,
370
371
  trackerPathFor
371
- } from "./index-zn08ywsv.js";
372
+ } from "./index-v8d7c6sw.js";
372
373
  import {
373
374
  ACTION_HEADING,
374
375
  CAUSE_HEADING,
@@ -388,6 +389,7 @@ import {
388
389
  MOVE_TO_SQLITE,
389
390
  MergeRefusal,
390
391
  POSITION_FILE,
392
+ RAFA_VERSION,
391
393
  RebuildRefusal,
392
394
  SCOPE_DIR,
393
395
  SKILL_FILE,
@@ -421,6 +423,7 @@ import {
421
423
  isFile,
422
424
  issueLine,
423
425
  loadConfig,
426
+ mainCheckoutOf,
424
427
  mergeFrontmatter,
425
428
  mergeStore,
426
429
  migrateStore,
@@ -461,7 +464,7 @@ import {
461
464
  versionLine,
462
465
  writeFrontmatter,
463
466
  writePositionFile
464
- } from "./index-kz7e4gtt.js";
467
+ } from "./index-bebv92ha.js";
465
468
  import {
466
469
  CONFIG_DEFAULTS,
467
470
  CONFIG_FILE,
@@ -476,7 +479,7 @@ import {
476
479
  readItemBytes,
477
480
  readRafaBlocks,
478
481
  resolveTiers
479
- } from "./index-dv0ycmp8.js";
482
+ } from "./index-vp0xghk2.js";
480
483
  import {
481
484
  promotable
482
485
  } from "./index-d6d0942q.js";
@@ -10584,6 +10587,9 @@ function bugLine(criterion, bug) {
10584
10587
  case "commented": {
10585
10588
  return { text: `${which}: recurs in ${bug.ref === null ? "an issue" : issueName2(bug.ref)} on the ${bug.channel} tracker; commented on it.`, warn: false };
10586
10589
  }
10590
+ case "inherited": {
10591
+ return { text: `${which}: not filed: a test failure the run started with.`, warn: false };
10592
+ }
10587
10593
  case "skipped": {
10588
10594
  return { text: `${which}: not filed: ${bug.problem ?? "no reason given"}.`, warn: true };
10589
10595
  }
@@ -10621,7 +10627,8 @@ async function fileFailures(run, epic, failures, commit) {
10621
10627
  tracker,
10622
10628
  privateTracker: run.seams.privateTracker,
10623
10629
  secrets,
10624
- seams: run.seams.findings
10630
+ seams: run.seams.findings,
10631
+ similarity: run.config
10625
10632
  });
10626
10633
  for (const bug of result2.bugs) {
10627
10634
  const line = bugLine(criterion, bug);
@@ -11682,6 +11689,10 @@ var CONFIG_SETTINGS_LINES = Object.freeze([
11682
11689
  "# claims:",
11683
11690
  `# staleAfter: ${CONFIG_DEFAULTS.claimsStaleAfter} # 36h | 3d | disabled; how long a rafa:claimed claim stands before a takeover`,
11684
11691
  `# ahead: ${CONFIG_DEFAULTS.claimsAhead} # off | allow; whether a claim may reach one issue ahead`,
11692
+ "# triage:",
11693
+ "# similarity:",
11694
+ `# threshold: ${String(CONFIG_DEFAULTS.triageSimilarityThreshold)} # above 0 and at most 1 | false; the least score at which triage comments on the nearest open bug`,
11695
+ `# candidates: ${String(CONFIG_DEFAULTS.triageSimilarityCandidates)} # 1..10, the nearest issues a new bug lists as Possible duplicates`,
11685
11696
  "# release:",
11686
11697
  `# enabled: ${String(CONFIG_DEFAULTS.releaseEnabled)} # true | false | auto, which is on when both files below exist`,
11687
11698
  `# versionFile: ${CONFIG_DEFAULTS.releaseVersionFile}`,
@@ -15209,8 +15220,16 @@ async function guardBeforeMerge(step) {
15209
15220
  var NO_WORKFLOW_WARNING = "nothing on GitHub has tested this branch; you are relying on the checks run locally";
15210
15221
  var WORKFLOWS_EXIST_WARNING = "CI may not have started (a path filter, a draft, Actions disabled, or it has not registered yet); this is probably not what you want";
15211
15222
  var UNCHECKED_MERGE_SENTENCE = "Merged with no checks reported, by rafa pr merge --skip-checks.";
15212
- function uncheckedCaseOf(workflowCount) {
15213
- return workflowCount === 0 ? "no-workflow" : "workflows-exist";
15223
+ function uncheckedCaseOf(workflowCount, baseWorkflows = null) {
15224
+ if (workflowCount === 0)
15225
+ return "no-workflow";
15226
+ if (workflowCount !== null && baseWorkflows !== null && !baseWorkflows.runsOnPullRequests) {
15227
+ return "no-pull-request-workflow";
15228
+ }
15229
+ return "workflows-exist";
15230
+ }
15231
+ function noPullRequestWorkflowLine(base) {
15232
+ return `no workflow runs on pull requests into ${base}`;
15214
15233
  }
15215
15234
  function workflowCountLine(workflowCount) {
15216
15235
  if (workflowCount === null)
@@ -15224,26 +15243,181 @@ function skipChecksCommand(number) {
15224
15243
  function uncheckedQuestion(number) {
15225
15244
  return `Merge #${number} with no checks? [y/N]`;
15226
15245
  }
15227
- function uncheckedComment(workflowCount) {
15228
- return `${UNCHECKED_MERGE_SENTENCE}
15246
+ function uncheckedComment(workflowCount, baseLine = null) {
15247
+ const countLine2 = workflowCountLine(workflowCount);
15248
+ const paragraphs = baseLine === null ? [UNCHECKED_MERGE_SENTENCE, countLine2] : [UNCHECKED_MERGE_SENTENCE, countLine2, baseLine];
15249
+ return paragraphs.join(`
15229
15250
 
15230
- ${workflowCountLine(workflowCount)}`;
15251
+ `);
15231
15252
  }
15232
- function readUnchecked(number, workflowCount) {
15233
- const which = uncheckedCaseOf(workflowCount);
15234
- const warning = which === "no-workflow" ? NO_WORKFLOW_WARNING : WORKFLOWS_EXIST_WARNING;
15253
+ function warningOf(which, baseWorkflows) {
15254
+ if (which === "no-workflow")
15255
+ return NO_WORKFLOW_WARNING;
15256
+ if (which === "no-pull-request-workflow" && baseWorkflows !== null)
15257
+ return noPullRequestWorkflowLine(baseWorkflows.base);
15258
+ return WORKFLOWS_EXIST_WARNING;
15259
+ }
15260
+ function readUnchecked(number, workflowCount, baseWorkflows = null) {
15261
+ const which = uncheckedCaseOf(workflowCount, baseWorkflows);
15262
+ const warning = warningOf(which, baseWorkflows);
15263
+ const isBaseCase = which === "no-pull-request-workflow";
15235
15264
  return Object.freeze({
15236
15265
  case: which,
15237
15266
  warning: Object.freeze([workflowCountLine(workflowCount), warning]),
15238
- yesMayAnswer: which === "no-workflow",
15267
+ yesMayAnswer: which !== "workflows-exist",
15239
15268
  question: uncheckedQuestion(number),
15240
- comment: uncheckedComment(workflowCount)
15269
+ comment: uncheckedComment(workflowCount, isBaseCase ? warning : null)
15241
15270
  });
15242
15271
  }
15243
15272
 
15273
+ // src/pr/workflow-triggers.ts
15274
+ var PULL_REQUEST_EVENT = "pull_request";
15275
+ var UNKNOWN = "unknown";
15276
+ var BRACKET_BODY = /^[A-Za-z0-9-]+$/;
15277
+ var REGEX_SPECIAL = /[.*+?^${}()|[\]\\/]/g;
15278
+ function workflowsRunOnPullRequestsInto(workflowTexts, base) {
15279
+ return workflowTexts.some((text) => workflowNamesBase(text, base));
15280
+ }
15281
+ function workflowNamesBase(workflowText, base) {
15282
+ let document;
15283
+ try {
15284
+ document = Bun.YAML.parse(workflowText);
15285
+ } catch {
15286
+ return true;
15287
+ }
15288
+ return readWorkflow(document, base) !== false;
15289
+ }
15290
+ function readWorkflow(document, base) {
15291
+ if (!isPlainObject(document) || !("on" in document))
15292
+ return UNKNOWN;
15293
+ const on = document.on;
15294
+ if (typeof on === "string")
15295
+ return on === PULL_REQUEST_EVENT;
15296
+ if (Array.isArray(on)) {
15297
+ if (!on.every((event) => typeof event === "string"))
15298
+ return UNKNOWN;
15299
+ return on.includes(PULL_REQUEST_EVENT);
15300
+ }
15301
+ if (!isPlainObject(on))
15302
+ return UNKNOWN;
15303
+ if (!(PULL_REQUEST_EVENT in on))
15304
+ return false;
15305
+ return readPullRequestTrigger(on[PULL_REQUEST_EVENT], base);
15306
+ }
15307
+ function readPullRequestTrigger(trigger, base) {
15308
+ if (trigger === null || trigger === undefined)
15309
+ return true;
15310
+ if (!isPlainObject(trigger))
15311
+ return UNKNOWN;
15312
+ const branches = trigger.branches;
15313
+ const ignored = trigger["branches-ignore"];
15314
+ if (branches !== undefined && ignored !== undefined)
15315
+ return UNKNOWN;
15316
+ if (branches !== undefined)
15317
+ return readBranches(branches, base);
15318
+ if (ignored !== undefined)
15319
+ return readBranchesIgnore(ignored, base);
15320
+ return true;
15321
+ }
15322
+ function readBranches(filter, base) {
15323
+ const patterns = patternListOf(filter);
15324
+ if (patterns === null)
15325
+ return UNKNOWN;
15326
+ let named2 = false;
15327
+ for (const pattern of patterns) {
15328
+ const negated = pattern.startsWith("!");
15329
+ const matches = branchPatternMatches(negated ? pattern.slice(1) : pattern, base);
15330
+ if (matches === null)
15331
+ return UNKNOWN;
15332
+ if (matches)
15333
+ named2 = !negated;
15334
+ }
15335
+ return named2;
15336
+ }
15337
+ function readBranchesIgnore(filter, base) {
15338
+ const patterns = patternListOf(filter);
15339
+ if (patterns === null || patterns.some((pattern) => pattern.startsWith("!")))
15340
+ return UNKNOWN;
15341
+ let anyMatch = false;
15342
+ for (const pattern of patterns) {
15343
+ const matches = branchPatternMatches(pattern, base);
15344
+ if (matches === null)
15345
+ return UNKNOWN;
15346
+ anyMatch = anyMatch || matches;
15347
+ }
15348
+ return !anyMatch;
15349
+ }
15350
+ function patternListOf(filter) {
15351
+ if (typeof filter === "string")
15352
+ return [filter];
15353
+ if (Array.isArray(filter) && filter.every((pattern) => typeof pattern === "string"))
15354
+ return filter;
15355
+ return null;
15356
+ }
15357
+ function branchPatternMatches(pattern, branch) {
15358
+ const source = regexSourceOf(pattern);
15359
+ if (source === null)
15360
+ return null;
15361
+ try {
15362
+ return new RegExp(`^${source}$`).test(branch);
15363
+ } catch {
15364
+ return null;
15365
+ }
15366
+ }
15367
+ function regexSourceOf(pattern) {
15368
+ let source = "";
15369
+ let quantifiable = false;
15370
+ let index = 0;
15371
+ while (index < pattern.length) {
15372
+ const char = pattern.charAt(index);
15373
+ if (char === "*") {
15374
+ const isDouble = pattern.charAt(index + 1) === "*";
15375
+ source += isDouble ? ".*" : "[^/]*";
15376
+ index += isDouble ? 2 : 1;
15377
+ quantifiable = false;
15378
+ } else if (char === "?" || char === "+") {
15379
+ if (!quantifiable)
15380
+ return null;
15381
+ source += char;
15382
+ index += 1;
15383
+ quantifiable = false;
15384
+ } else if (char === "[") {
15385
+ const close = pattern.indexOf("]", index + 1);
15386
+ if (close === -1)
15387
+ return null;
15388
+ const body = pattern.slice(index + 1, close);
15389
+ if (!BRACKET_BODY.test(body))
15390
+ return null;
15391
+ source += `[${body}]`;
15392
+ index = close + 1;
15393
+ quantifiable = true;
15394
+ } else if (char === "\\") {
15395
+ if (index + 1 >= pattern.length)
15396
+ return null;
15397
+ source += escapeRegex(pattern.charAt(index + 1));
15398
+ index += 2;
15399
+ quantifiable = true;
15400
+ } else {
15401
+ source += escapeRegex(char);
15402
+ index += 1;
15403
+ quantifiable = true;
15404
+ }
15405
+ }
15406
+ return source;
15407
+ }
15408
+ function escapeRegex(char) {
15409
+ return char.replace(REGEX_SPECIAL, "\\$&");
15410
+ }
15411
+ function isPlainObject(value) {
15412
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15413
+ }
15414
+
15244
15415
  // src/commands/pr/merge-unchecked.ts
15245
15416
  var INDENT4 = " ";
15246
15417
  var YES_ANSWERS3 = ["y", "yes"];
15418
+ var WORKFLOWS_DIR = ".github/workflows/";
15419
+ var WORKFLOW_ENDINGS = [".yml", ".yaml"];
15420
+ var LS_TREE_ENTRY = /^\d+ (\w+) ([0-9a-f]+)\t(.+)$/s;
15247
15421
  function refusal10(head, reading, summary, tail) {
15248
15422
  return new CommandExit(1, [
15249
15423
  `\u274C ${head}`,
@@ -15260,10 +15434,37 @@ async function workflowCountOf(pulls) {
15260
15434
  return null;
15261
15435
  }
15262
15436
  }
15437
+ function workflowBlobsOf(listing) {
15438
+ return listing.split("\x00").map((entry) => LS_TREE_ENTRY.exec(entry)).filter((match) => match !== null).filter(([, type, , path]) => type === "blob" && WORKFLOW_ENDINGS.some((ending) => path?.endsWith(ending) === true)).map(([, , oid]) => oid ?? "");
15439
+ }
15440
+ function readBaseWorkflows(git, base) {
15441
+ if (base === "" || base.startsWith("-"))
15442
+ return null;
15443
+ if (!git(["fetch", "origin", base]).ok)
15444
+ return null;
15445
+ const listed2 = git(["ls-tree", "--full-tree", "-z", `origin/${base}`, WORKFLOWS_DIR]);
15446
+ if (!listed2.ok)
15447
+ return null;
15448
+ const blobs = workflowBlobsOf(listed2.stdout);
15449
+ if (blobs.length === 0)
15450
+ return null;
15451
+ const shown = blobs.map((oid) => git(["cat-file", "blob", oid]));
15452
+ if (shown.some((result2) => !result2.ok))
15453
+ return null;
15454
+ const texts = shown.map((result2) => result2.stdout);
15455
+ return Object.freeze({ base, runsOnPullRequests: workflowsRunOnPullRequestsInto(texts, base) });
15456
+ }
15457
+ function baseWorkflowsOf(options, workflowCount) {
15458
+ const { git, base } = options;
15459
+ if (workflowCount === null || workflowCount === 0 || git === undefined || base === undefined)
15460
+ return null;
15461
+ return readBaseWorkflows(git, base);
15462
+ }
15263
15463
  async function readUncheckedMerge(options) {
15264
15464
  const { pulls, number, yes, summary } = options;
15265
15465
  const workflowCount = await workflowCountOf(pulls);
15266
- const reading = readUnchecked(number, workflowCount);
15466
+ const baseWorkflows = baseWorkflowsOf(options, workflowCount);
15467
+ const reading = readUnchecked(number, workflowCount, baseWorkflows);
15267
15468
  if (yes && !reading.yesMayAnswer) {
15268
15469
  throw refusal10(`rafa pr merge refuses --yes for #${number} with no checks: workflows exist, or their count could not be read.`, reading, summary, "That is more likely a fault than a choice, so a person has to answer. Run it again without --yes on a terminal.");
15269
15470
  }
@@ -15272,7 +15473,7 @@ async function readUncheckedMerge(options) {
15272
15473
  const tail = reading.yesMayAnswer ? "Merge it without the question with --yes." : "--yes is refused here, so run it on a terminal and answer it.";
15273
15474
  throw refusal10(`rafa pr merge asks before merging #${number} with no checks, and standard input is no terminal.`, reading, summary, tail);
15274
15475
  }
15275
- return Object.freeze({ workflowCount, reading, yes });
15476
+ return Object.freeze({ workflowCount, baseWorkflows, reading, yes });
15276
15477
  }
15277
15478
  function isYes(answer2) {
15278
15479
  return answer2 !== null && YES_ANSWERS3.includes(answer2.trim().toLowerCase());
@@ -15454,8 +15655,8 @@ async function reportAfterCleanUp(context, pr, seams, detail) {
15454
15655
  return { unblocked: null, freed };
15455
15656
  }
15456
15657
  async function askToMerge(context, pr, seams, ask) {
15457
- const { number, summary, yes, skipChecks } = ask;
15458
- const unchecked = skipChecks ? await readUncheckedMerge({ pulls: pr.pulls, number, yes, summary, isTerminal: seams.isTerminal }) : null;
15658
+ const { number, summary, yes, skipChecks, base, git } = ask;
15659
+ const unchecked = skipChecks ? await readUncheckedMerge({ pulls: pr.pulls, number, yes, summary, isTerminal: seams.isTerminal, base, git }) : null;
15459
15660
  if (unchecked === null && !yes)
15460
15661
  requireTerminal(seams, summary);
15461
15662
  context.output.info(summary);
@@ -15520,7 +15721,7 @@ async function runMerge2(context, seams) {
15520
15721
  openPrompter: prompterOf(seams)
15521
15722
  });
15522
15723
  const summary = summaryLine2(detail, method);
15523
- const answer2 = guard.go ? await askToMerge(context, pr, seams, { number: detail.number, summary, yes, skipChecks }) : { go: false, unchecked: null };
15724
+ const answer2 = guard.go ? await askToMerge(context, pr, seams, { number: detail.number, summary, yes, skipChecks, base: detail.baseRefName, git }) : { go: false, unchecked: null };
15524
15725
  const answered3 = {
15525
15726
  number: detail.number,
15526
15727
  source: pick.source,
@@ -15584,7 +15785,7 @@ function createPrMergeCommand(seams = DEFAULT_MERGE_SEAMS) {
15584
15785
  subject: "pr",
15585
15786
  action: "merge",
15586
15787
  summary: "merge a pull request and clean up both branches",
15587
- description: "Merges one pull request through the GitHub CLI and then, in code, switches to the base branch," + " pulls it fast-forward only, deletes the head branch locally and on the remote where it is still there," + " and prunes, reporting each step; a head branch this checkout has no local branch for is reported as a" + " skipped local delete, and the rest still run. Refuses before it asks anything on a dirty working tree, on a pull" + " request that is not green or does not merge, and on a head branch checked out in another worktree. It" + " shows the pull request, its branches and the method and asks `Merge? [y/N]`; `--yes` skips the question," + " and without a terminal and without `--yes` it refuses. A pull request that reports no checks at all is" + " refused unless `--skip-checks` is given, which is refused on any pull request that does report checks;" + " with it the command reads how many workflows the repository defines, prints a warning for that case, asks" + " `Merge #<n> with no checks? [y/N]` (which `--yes` answers only where the repository defines no workflow)," + " and after the merge posts one comment on the pull request saying so. Where the release runs, the release" + " guard reads the branch before the question: a `collision` is refused unless" + " `dangerous.acceptVersionCollision` is true, and `pr.versionCollision` sets whether a `missing` or" + " `stale` branch merges silently, with a warning, after its own question, or not at all. A step that fails never undoes the merge: it" + " prints what is left as commands to paste and exits 1. After the merge it ticks the `Closes #<n>` line of" + " every issue the pull request closes on every open board whose checklist lists it, or on the roadmap issue while no issue carries `type:roadmap`, warning rather than failing when that write" + ' does not land. It ends by reading every open issue whose "Blocked by:" line names an issue this pull' + " request closes, asking whether to remove `spec:blocked` from each one whose blockers have all closed;" + " `--yes` does not answer that question, and every failure of that reading is a warning. With" + " board.relationships set to native it ticks no epic checklist and, in place of that reading, prints the" + " open issues the merge freed, whose blockers GitHub clears by itself, asking nothing and writing nothing. With" + " `--output=json` the pull request, the method, the steps that ran, the follow-ups, the roadmap tick and" + " the unblock reading, or the freed issues in native mode, are the data of the terminal result event. Refuses with exit code 2 where" + " `pr.provider` is not `gh`.",
15788
+ description: "Merges one pull request through the GitHub CLI and then, in code, switches to the base branch," + " pulls it fast-forward only, deletes the head branch locally and on the remote where it is still there," + " and prunes, reporting each step; a head branch this checkout has no local branch for is reported as a" + " skipped local delete, and the rest still run. Refuses before it asks anything on a dirty working tree, on a pull" + " request that is not green or does not merge, and on a head branch checked out in another worktree. It" + " shows the pull request, its branches and the method and asks `Merge? [y/N]`; `--yes` skips the question," + " and without a terminal and without `--yes` it refuses. A pull request that reports no checks at all is" + " refused unless `--skip-checks` is given, which is refused on any pull request that does report checks;" + " with it the command reads how many workflows the repository defines, prints a warning for that case, asks" + " `Merge #<n> with no checks? [y/N]` (which `--yes` answers only where the repository defines no workflow, or" + " where no workflow file on the base has a `pull_request` trigger naming it)," + " and after the merge posts one comment on the pull request saying so. Where the release runs, the release" + " guard reads the branch before the question: a `collision` is refused unless" + " `dangerous.acceptVersionCollision` is true, and `pr.versionCollision` sets whether a `missing` or" + " `stale` branch merges silently, with a warning, after its own question, or not at all. A step that fails never undoes the merge: it" + " prints what is left as commands to paste and exits 1. After the merge it ticks the `Closes #<n>` line of" + " every issue the pull request closes on every open board whose checklist lists it, or on the roadmap issue while no issue carries `type:roadmap`, warning rather than failing when that write" + ' does not land. It ends by reading every open issue whose "Blocked by:" line names an issue this pull' + " request closes, asking whether to remove `spec:blocked` from each one whose blockers have all closed;" + " `--yes` does not answer that question, and every failure of that reading is a warning. With" + " board.relationships set to native it ticks no epic checklist and, in place of that reading, prints the" + " open issues the merge freed, whose blockers GitHub clears by itself, asking nothing and writing nothing. With" + " `--output=json` the pull request, the method, the steps that ran, the follow-ups, the roadmap tick and" + " the unblock reading, or the freed issues in native mode, are the data of the terminal result event. Refuses with exit code 2 where" + " `pr.provider` is not `gh`.",
15588
15789
  args: [
15589
15790
  {
15590
15791
  name: "n",
@@ -15600,7 +15801,7 @@ function createPrMergeCommand(seams = DEFAULT_MERGE_SEAMS) {
15600
15801
  },
15601
15802
  {
15602
15803
  name: "skip-checks",
15603
- description: "Merge a pull request that reports no checks at all, after a warning and its own question." + " Refused where the pull request reports any check; `--yes` beside it is refused where the repository" + " defines a workflow or its workflow count could not be read.",
15804
+ description: "Merge a pull request that reports no checks at all, after a warning and its own question." + " Refused where the pull request reports any check; `--yes` beside it is refused where the repository" + " defines a workflow that may run on pull requests into the base, or its workflow count could not be read.",
15604
15805
  type: "boolean"
15605
15806
  },
15606
15807
  {
@@ -21880,6 +22081,445 @@ function createStatusCommand(seams = DEFAULT_STATUS_SEAMS) {
21880
22081
  }
21881
22082
  var status_default3 = createStatusCommand();
21882
22083
 
22084
+ // src/commands/update/stub.ts
22085
+ var ISSUES_URL = "https://github.com/open-tomato/rafa/issues";
22086
+ function stubMessage(action, issue2) {
22087
+ return `rafa update ${action} is a feature in development, tracked in ${ISSUES_URL}/${String(issue2)}.` + " Nothing was changed.";
22088
+ }
22089
+ function createUpdateStub(spec) {
22090
+ const command = {
22091
+ name: `update ${spec.action}`,
22092
+ subject: "update",
22093
+ action: spec.action,
22094
+ summary: `${spec.summary} (in development, #${String(spec.issue)})`,
22095
+ description: `${spec.summary}. In development: it refuses with exit code 1, naming #${String(spec.issue)},` + " and changes nothing.",
22096
+ args: [],
22097
+ flags: [],
22098
+ examples: [{ cmd: `rafa update ${spec.action}`, note: `Refuses, naming #${String(spec.issue)}; changes nothing.` }],
22099
+ outputs: ["text", "json"],
22100
+ needsProject: false,
22101
+ ...spec.hidden === true ? { hidden: true } : {},
22102
+ run: () => Promise.reject(new CommandExit(1, stubMessage(spec.action, spec.issue)))
22103
+ };
22104
+ return Object.freeze(command);
22105
+ }
22106
+
22107
+ // src/commands/update/board.ts
22108
+ var board_default = createUpdateStub({ action: "board", summary: "apply the board's newest setup and its migrations", issue: 717 });
22109
+
22110
+ // src/project/lock.ts
22111
+ import { existsSync as existsSync15, lstatSync as lstatSync2, readFileSync as readFileSync28, writeFileSync as writeFileSync12 } from "fs";
22112
+ import { join as join40 } from "path";
22113
+ var LOCK_FILE = "rafa.lock";
22114
+ var LOCKFILE_VERSION = 1;
22115
+
22116
+ class ProjectLockError extends Error {
22117
+ name = "ProjectLockError";
22118
+ }
22119
+ function lockPath(root) {
22120
+ return join40(root, LOCK_FILE);
22121
+ }
22122
+ function projectLockText(version2) {
22123
+ const lock = { lockfileVersion: LOCKFILE_VERSION, rafa: version2 };
22124
+ return `${JSON.stringify(lock, null, 2)}
22125
+ `;
22126
+ }
22127
+ function isLink(path) {
22128
+ try {
22129
+ return lstatSync2(path).isSymbolicLink();
22130
+ } catch {
22131
+ return false;
22132
+ }
22133
+ }
22134
+ function refuse8(problem2) {
22135
+ throw new ProjectLockError(`${LOCK_FILE} ${problem2}`);
22136
+ }
22137
+ function lockOf(value) {
22138
+ if (typeof value !== "object" || value === null || Array.isArray(value))
22139
+ refuse8("holds no JSON object");
22140
+ const record = value;
22141
+ const format = record["lockfileVersion"];
22142
+ if (format !== LOCKFILE_VERSION) {
22143
+ refuse8(`is in lockfileVersion ${String(format)}, and this rafa reads ${String(LOCKFILE_VERSION)}: a newer rafa wrote it`);
22144
+ }
22145
+ const rafa = record["rafa"];
22146
+ if (typeof rafa !== "string" || parseSemanticVersion(rafa) === null) {
22147
+ refuse8(`names rafa ${JSON.stringify(rafa)}, which is no version`);
22148
+ }
22149
+ return { lockfileVersion: LOCKFILE_VERSION, rafa };
22150
+ }
22151
+ function readProjectLock(root) {
22152
+ const path = lockPath(root);
22153
+ if (!existsSync15(path)) {
22154
+ if (isLink(path))
22155
+ refuse8("is a link to nothing");
22156
+ return null;
22157
+ }
22158
+ let text;
22159
+ try {
22160
+ text = readFileSync28(path, "utf8");
22161
+ } catch (error) {
22162
+ refuse8(`could not be read: ${messageOf(error)}`);
22163
+ }
22164
+ let value;
22165
+ try {
22166
+ value = JSON.parse(text);
22167
+ } catch {
22168
+ refuse8("is not JSON");
22169
+ }
22170
+ return lockOf(value);
22171
+ }
22172
+ function writeProjectLock(root, version2) {
22173
+ writeFileSync12(lockPath(root), projectLockText(version2), "utf8");
22174
+ }
22175
+
22176
+ // src/project/update-current.ts
22177
+ import { existsSync as existsSync16 } from "fs";
22178
+ import { join as join41 } from "path";
22179
+ var DEPRECATIONS = Object.freeze([]);
22180
+ function missingFolders(root) {
22181
+ return PROJECT_TREE.map((name) => join41(SCOPE_DIR, name)).filter((relative9) => !existsSync16(join41(root, relative9)));
22182
+ }
22183
+ async function readBoard3(access) {
22184
+ if (access.gh === null)
22185
+ return { kind: "skipped", reason: access.reason };
22186
+ try {
22187
+ const held = await listBoardLabels(access.gh);
22188
+ return { kind: "labels", missing: missingBoardLabels(held).map((label) => label.name) };
22189
+ } catch (error) {
22190
+ return { kind: "skipped", reason: messageOf(error) };
22191
+ }
22192
+ }
22193
+ function lockChangeOf(range) {
22194
+ if (range.kind === "adopt")
22195
+ return "created";
22196
+ return range.kind === "patch" ? "updated" : "unchanged";
22197
+ }
22198
+ function fromOf(range) {
22199
+ if (range.kind === "patch")
22200
+ return range.from;
22201
+ if (range.kind === "same")
22202
+ return range.to;
22203
+ return null;
22204
+ }
22205
+ async function readUpdatePlan(roots, range, board) {
22206
+ const { root, lockRoot } = roots;
22207
+ return {
22208
+ root,
22209
+ lockRoot,
22210
+ from: fromOf(range),
22211
+ to: range.to,
22212
+ folders: missingFolders(root),
22213
+ board: await readBoard3(board),
22214
+ deprecations: DEPRECATIONS,
22215
+ lock: lockChangeOf(range)
22216
+ };
22217
+ }
22218
+ function planChanges(plan2) {
22219
+ const labels = plan2.board.kind === "labels" ? plan2.board.missing.length : 0;
22220
+ return plan2.folders.length > 0 || labels > 0 || plan2.deprecations.length > 0 || plan2.lock !== "unchanged";
22221
+ }
22222
+
22223
+ class UpdateStepError extends Error {
22224
+ step;
22225
+ written;
22226
+ name = "UpdateStepError";
22227
+ constructor(step, message, written) {
22228
+ super(message);
22229
+ this.step = step;
22230
+ this.written = written;
22231
+ }
22232
+ }
22233
+ function foldersNowThere(plan2) {
22234
+ return plan2.folders.filter((relative9) => existsSync16(join41(plan2.root, relative9)));
22235
+ }
22236
+ function writeFolders(plan2) {
22237
+ if (plan2.folders.length === 0)
22238
+ return [];
22239
+ try {
22240
+ return writeProjectScope(plan2.root).filter((write) => write.change === "created").map((write) => write.path);
22241
+ } catch (error) {
22242
+ throw new UpdateStepError("folders", messageOf(error), foldersNowThere(plan2));
22243
+ }
22244
+ }
22245
+ function createdLabels(parts) {
22246
+ return parts.filter((part) => part.outcome === "created").map((part) => `label ${part.name}`);
22247
+ }
22248
+ async function applyUpdatePlan(plan2, gh) {
22249
+ const folders = writeFolders(plan2);
22250
+ const labels = gh !== null && plan2.board.kind === "labels" && plan2.board.missing.length > 0 ? await setUpLabels(gh) : [];
22251
+ if (plan2.lock !== "unchanged") {
22252
+ try {
22253
+ writeProjectLock(plan2.lockRoot, plan2.to);
22254
+ } catch (error) {
22255
+ throw new UpdateStepError("lock", messageOf(error), [...folders, ...createdLabels(labels)]);
22256
+ }
22257
+ }
22258
+ return { folders, labels, lock: plan2.lock };
22259
+ }
22260
+
22261
+ // src/project/update-range.ts
22262
+ function refused2(reason, message) {
22263
+ return { kind: "refused", reason, message };
22264
+ }
22265
+ function readCurrentRange(recorded, installed) {
22266
+ const to = parseSemanticVersion(installed);
22267
+ if (to === null)
22268
+ return refused2("unreadable", `the installed rafa answers "${installed}", which is no version`);
22269
+ if (recorded === null)
22270
+ return { kind: "adopt", to: installed };
22271
+ const from = parseSemanticVersion(recorded);
22272
+ if (from === null)
22273
+ return refused2("unreadable", `the project records rafa "${recorded}", which is no version`);
22274
+ const order = compareVersions(to, from);
22275
+ if (order < 0) {
22276
+ return refused2("older", `the installed rafa ${installed} is older than the project's ${recorded}, and no downgrade runs before 1.0.0`);
22277
+ }
22278
+ if (to.major !== from.major) {
22279
+ return refused2("newer-major", `the installed rafa ${installed} is a newer major than the project's ${recorded}; that move is rafa update latest`);
22280
+ }
22281
+ if (to.minor !== from.minor) {
22282
+ return refused2("newer-minor", `the installed rafa ${installed} is a newer minor than the project's ${recorded}; that move is rafa update next`);
22283
+ }
22284
+ return order === 0 ? { kind: "same", to: installed } : { kind: "patch", from: recorded, to: installed };
22285
+ }
22286
+
22287
+ // src/commands/update/current.ts
22288
+ var USAGE42 = "rafa update current [--dry-run] [--yes]";
22289
+ var APPLY_QUESTION = "Apply these changes? [y/N] ";
22290
+ var YES_ANSWERS5 = ["y", "yes"];
22291
+ function checkoutLockRoot(root, dir) {
22292
+ const run = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], { cwd: dir, stdout: "pipe", stderr: "pipe" });
22293
+ if (run.exitCode !== 0)
22294
+ return root;
22295
+ const top = run.stdout.toString().trim();
22296
+ return top !== "" && mainCheckoutOf(top) === root ? top : root;
22297
+ }
22298
+ var DEFAULT_UPDATE_CURRENT_SEAMS = Object.freeze({
22299
+ installed: RAFA_VERSION,
22300
+ openGh: (root) => createGhRunner({ cwd: root }),
22301
+ readRemote: gitRemoteUrl,
22302
+ isTerminal: () => process.stdin.isTTY === true,
22303
+ openPrompter: () => createLinePrompter(process.stdin, process.stderr),
22304
+ lockRootOf: (root) => checkoutLockRoot(root, process.cwd())
22305
+ });
22306
+ function readSwitch4(context, name) {
22307
+ const value = context.flags[name];
22308
+ if (value === undefined || value === false || value === "false")
22309
+ return false;
22310
+ if (value === true || value === "true")
22311
+ return true;
22312
+ throw new CommandExit(1, `\u274C --${name} takes no value, and read "${String(value)}" as one
22313
+ Usage: ${USAGE42}`);
22314
+ }
22315
+ function couldNotRun(problem2) {
22316
+ return new CommandExit(2, `FAIL \u2014 ${problem2}
22317
+ nothing was changed.`);
22318
+ }
22319
+ function projectOf15(context) {
22320
+ if (context.project === null)
22321
+ throw new Error("rafa update current runs inside a project, and was handed none");
22322
+ return context.project;
22323
+ }
22324
+ function boardAccess(project, seams) {
22325
+ let configured;
22326
+ try {
22327
+ configured = loadConfig({ root: project.root, home: project.home }, {}, () => {}).config.prProvider;
22328
+ } catch (error) {
22329
+ throw couldNotRun(`config: ${messageOf(error)}`);
22330
+ }
22331
+ const provider = resolvePrProvider({ configured, dir: project.root, readRemote: seams.readRemote }).provider;
22332
+ return provider === "gh" ? { gh: seams.openGh(project.root), reason: "" } : { gh: null, reason: `pr.provider is ${provider}, so there is no GitHub board` };
22333
+ }
22334
+ function foldersText(plan2) {
22335
+ if (plan2.folders.length === 0)
22336
+ return "nothing missing";
22337
+ return `create ${plan2.folders.join(", ")}`;
22338
+ }
22339
+ function boardText2(plan2) {
22340
+ if (plan2.board.kind === "skipped")
22341
+ return `skipped: ${plan2.board.reason}`;
22342
+ if (plan2.board.missing.length === 0)
22343
+ return "every label present";
22344
+ return `create labels ${plan2.board.missing.join(", ")}`;
22345
+ }
22346
+ function deprecationsText(plan2) {
22347
+ if (plan2.deprecations.length === 0)
22348
+ return "none registered yet (#718)";
22349
+ return `${String(plan2.deprecations.length)} to run`;
22350
+ }
22351
+ function lockText(plan2) {
22352
+ const where = plan2.lockRoot === plan2.root ? "" : ` in ${plan2.lockRoot}`;
22353
+ if (plan2.lock === "unchanged")
22354
+ return `unchanged at ${plan2.to}${where}`;
22355
+ if (plan2.lock === "created")
22356
+ return `create at ${plan2.to}${where}`;
22357
+ return `update ${String(plan2.from)} \u2192 ${plan2.to}${where}`;
22358
+ }
22359
+ function planLines2(plan2) {
22360
+ const from = plan2.from ?? `no ${LOCK_FILE}`;
22361
+ return [
22362
+ `rafa update current: ${plan2.root}, from ${from} to ${plan2.to}`,
22363
+ ` folders ${foldersText(plan2)}`,
22364
+ ` board ${boardText2(plan2)}`,
22365
+ ` deprecations ${deprecationsText(plan2)}`,
22366
+ ` ${LOCK_FILE} ${lockText(plan2)}`
22367
+ ];
22368
+ }
22369
+ function appliedLines(applied, plan2, context) {
22370
+ for (const path of applied.folders)
22371
+ context.output.info(`created ${path}`);
22372
+ for (const part of applied.labels) {
22373
+ if (part.outcome === "created")
22374
+ context.output.info(`created label ${part.name}`);
22375
+ if (part.outcome === "refused")
22376
+ context.output.warn(`label ${part.name} was not created: ${part.detail}`);
22377
+ }
22378
+ if (applied.lock !== "unchanged")
22379
+ context.output.info(`wrote ${LOCK_FILE} at ${plan2.to}`);
22380
+ }
22381
+ async function confirmed2(yes, seams) {
22382
+ if (yes)
22383
+ return true;
22384
+ if (!seams.isTerminal()) {
22385
+ throw new CommandExit(1, "REFUSED \u2014 rafa update current asks before it changes anything, and there is no" + " terminal to ask on. Run it with --dry-run to see the changes, then with --yes to apply them." + `
22386
+ nothing was changed.`);
22387
+ }
22388
+ const prompter = seams.openPrompter();
22389
+ try {
22390
+ const answer2 = await prompter.ask(APPLY_QUESTION);
22391
+ return answer2 !== null && YES_ANSWERS5.includes(answer2.trim().toLowerCase());
22392
+ } finally {
22393
+ prompter.close();
22394
+ }
22395
+ }
22396
+ function readRange(root, installed) {
22397
+ let recorded;
22398
+ try {
22399
+ recorded = readProjectLock(root)?.rafa ?? null;
22400
+ } catch (error) {
22401
+ throw couldNotRun(messageOf(error));
22402
+ }
22403
+ const range = readCurrentRange(recorded, installed);
22404
+ if (range.kind === "refused")
22405
+ throw new CommandExit(1, `REFUSED \u2014 ${range.message}
22406
+ nothing was changed.`);
22407
+ return range;
22408
+ }
22409
+ function refuseConflicts(project) {
22410
+ const conflicts2 = scaffoldConflicts(project.root, project.home);
22411
+ if (conflicts2.length === 0)
22412
+ return;
22413
+ throw couldNotRun(`the project scope cannot be written:
22414
+ ${conflicts2.map((conflict) => ` ${conflict}`).join(`
22415
+ `)}`);
22416
+ }
22417
+ function stoppedPartWay(error) {
22418
+ const kept = error.written.length === 0 ? "Nothing was written before it." : `Already written, and kept: ${error.written.join(", ")}.`;
22419
+ return new CommandExit(2, `FAIL \u2014 ${error.step}: ${error.message}
22420
+ stopped part-way. ${kept}` + `
22421
+ Fix the cause and run rafa update current again: each step writes only what is missing.`);
22422
+ }
22423
+ async function applyAndReport(plan2, gh, context) {
22424
+ let applied;
22425
+ try {
22426
+ applied = await applyUpdatePlan(plan2, gh);
22427
+ } catch (error) {
22428
+ if (error instanceof UpdateStepError)
22429
+ throw stoppedPartWay(error);
22430
+ throw error;
22431
+ }
22432
+ appliedLines(applied, plan2, context);
22433
+ return applied;
22434
+ }
22435
+ function refusedLabels(applied) {
22436
+ const refused3 = applied.labels.filter((part) => part.outcome === "refused");
22437
+ if (refused3.length === 0)
22438
+ return null;
22439
+ return new CommandExit(1, `${String(refused3.length)} board label(s) were not created; everything else was applied.` + " Run rafa update current again once gh can create them.");
22440
+ }
22441
+ function stopLine2(plan2, dryRun) {
22442
+ if (!planChanges(plan2))
22443
+ return "Nothing to change.";
22444
+ if (dryRun)
22445
+ return "Dry run: nothing was written. Run rafa update current to apply it.";
22446
+ return null;
22447
+ }
22448
+ async function runUpdateCurrent(context, seams) {
22449
+ expectNoArgument(context.args, USAGE42);
22450
+ const dryRun = readSwitch4(context, "dry-run");
22451
+ const yes = readSwitch4(context, "yes");
22452
+ const project = projectOf15(context);
22453
+ const lockRoot = seams.lockRootOf(project.root);
22454
+ const range = readRange(lockRoot, seams.installed);
22455
+ refuseConflicts(project);
22456
+ const access = boardAccess(project, seams);
22457
+ const plan2 = await readUpdatePlan({ root: project.root, lockRoot }, range, access);
22458
+ for (const line of planLines2(plan2))
22459
+ context.output.info(line);
22460
+ const finish = (applied2) => {
22461
+ if (context.outputMode === "json")
22462
+ context.output.result({ plan: plan2, applied: applied2 });
22463
+ };
22464
+ const stop = stopLine2(plan2, dryRun);
22465
+ if (stop !== null) {
22466
+ context.output.info(stop);
22467
+ finish(null);
22468
+ return;
22469
+ }
22470
+ context.output.warn("rafa cannot roll these changes back. Run it with --dry-run first to only see them.");
22471
+ if (!await confirmed2(yes, seams)) {
22472
+ context.output.info("Nothing was changed.");
22473
+ finish(null);
22474
+ return;
22475
+ }
22476
+ const applied = await applyAndReport(plan2, access.gh, context);
22477
+ finish(applied);
22478
+ const refusal13 = refusedLabels(applied);
22479
+ if (refusal13 !== null)
22480
+ throw refusal13;
22481
+ }
22482
+ function createUpdateCurrentCommand(seams = DEFAULT_UPDATE_CURRENT_SEAMS) {
22483
+ const command = {
22484
+ name: "update current",
22485
+ subject: "update",
22486
+ action: "current",
22487
+ summary: "bring this project to the installed rafa, within its patch range",
22488
+ description: "Brings the project to the installed rafa when the installed version shares the major and" + ` minor of the version \`${LOCK_FILE}\` records at the project root, and is the same patch or newer.` + " A project with no lock is adopted at the installed version. It creates the missing `.rafa/` folders" + " and the missing board labels on a GitHub board, runs the deprecations step (none registered yet)," + ` and writes \`${LOCK_FILE}\`. It prints every change first; \`--dry-run\` stops there. Otherwise it` + " warns that the changes cannot be rolled back and asks once, `--yes` answering; with no terminal and" + " no `--yes` it refuses with exit code 1. A newer minor or major, or an installed rafa older than the" + " lock, refuses with exit code 1, and so does a board label `gh` would not create, once everything" + " else is applied. A lock or config that cannot be read, or a `.rafa/` path holding something else," + " exits 2 before anything is asked; a folder or the lock that cannot be written exits 2 naming the" + " step and what was already written. In a linked worktree the lock is the worktree's, on its branch." + " With `--output=json` the plan and what was applied are the data of the terminal result event.",
22489
+ args: [],
22490
+ flags: [
22491
+ { name: "dry-run", description: "Print every change and write nothing.", type: "boolean" },
22492
+ { name: "yes", description: "Apply the changes without asking.", type: "boolean" }
22493
+ ],
22494
+ examples: [
22495
+ { cmd: "rafa update current --dry-run", note: "Lists the folders, labels and lock it would write, and writes nothing." },
22496
+ { cmd: "rafa update current --yes", note: "Applies them without asking, as a script or a loop host runs it." }
22497
+ ],
22498
+ outputs: ["text", "json"],
22499
+ run: async (context) => runUpdateCurrent(context, seams)
22500
+ };
22501
+ return Object.freeze(command);
22502
+ }
22503
+ var current_default2 = createUpdateCurrentCommand();
22504
+
22505
+ // src/commands/update/latest.ts
22506
+ var latest_default = createUpdateStub({ action: "latest", summary: "move this project to the newest version", issue: 716 });
22507
+
22508
+ // src/commands/update/next.ts
22509
+ var next_default2 = createUpdateStub({ action: "next", summary: "move this project to the newest minor of its major", issue: 716 });
22510
+
22511
+ // src/commands/update/port.ts
22512
+ var port_default = createUpdateStub({ action: "port", summary: "move this project to a version, as update project does", issue: 716, hidden: true });
22513
+
22514
+ // src/commands/update/project.ts
22515
+ var project_default = createUpdateStub({ action: "project", summary: "move this project to a version, with its migrations", issue: 716 });
22516
+
22517
+ // src/commands/update/rafa.ts
22518
+ var rafa_default = createUpdateStub({ action: "rafa", summary: "move this project to a version, as update project does", issue: 716, hidden: true });
22519
+
22520
+ // src/commands/update/self.ts
22521
+ var self_default = createUpdateStub({ action: "self", summary: "update rafa itself, from npm, a pinned version or another source", issue: 715 });
22522
+
21883
22523
  // src/commands/usage.ts
21884
22524
  var usage_default = wrapPhaseZeroCommand({
21885
22525
  name: "usage",
@@ -21912,7 +22552,8 @@ var CORE_SUBJECTS = Object.freeze([
21912
22552
  { name: "release", summary: "read the release state of the project; settle the waiting fragments into a version; tag the commit that set it" },
21913
22553
  { name: "board", summary: "list the boards with their owner, epic count, and which is current and home" },
21914
22554
  { name: "epic", summary: "show one epic's issues as the Roadmap table; create an epic; defer or promote it; move an issue to it; close it through the gate or cancel it" },
21915
- { name: "claim", summary: "give up this device's claim on an issue; hand it to another store or withdraw the offer; accept a handover; take over a stale claim" }
22555
+ { name: "claim", summary: "give up this device's claim on an issue; hand it to another store or withdraw the offer; accept a handover; take over a stale claim" },
22556
+ { name: "update", summary: "bring this project to the installed rafa; the other updates are in development" }
21916
22557
  ]);
21917
22558
  var CORE_COMMANDS = Object.freeze([
21918
22559
  create_default2,
@@ -21984,6 +22625,14 @@ var CORE_COMMANDS = Object.freeze([
21984
22625
  hand_default,
21985
22626
  accept_default,
21986
22627
  take_default,
22628
+ current_default2,
22629
+ self_default,
22630
+ project_default,
22631
+ rafa_default,
22632
+ port_default,
22633
+ board_default,
22634
+ next_default2,
22635
+ latest_default,
21987
22636
  status_default3,
21988
22637
  next_default,
21989
22638
  roadmap_default,