brainfile 0.21.0 → 0.21.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 (38) hide show
  1. package/dist/{chunk-FOLBAGJS.mjs → chunk-MUV7XTFA.mjs} +211 -96
  2. package/dist/cli.mjs +324 -207
  3. package/dist/commands/brief.js +1 -1
  4. package/dist/commands/brief.js.map +1 -1
  5. package/dist/commands/log.d.ts.map +1 -1
  6. package/dist/commands/log.js +4 -1
  7. package/dist/commands/log.js.map +1 -1
  8. package/dist/commands/sync.d.ts +3 -1
  9. package/dist/commands/sync.d.ts.map +1 -1
  10. package/dist/commands/sync.js +23 -14
  11. package/dist/commands/sync.js.map +1 -1
  12. package/dist/commands/where.d.ts +4 -1
  13. package/dist/commands/where.d.ts.map +1 -1
  14. package/dist/commands/where.js +9 -3
  15. package/dist/commands/where.js.map +1 -1
  16. package/dist/mcp/helpers.d.ts +3 -2
  17. package/dist/mcp/helpers.d.ts.map +1 -1
  18. package/dist/mcp/helpers.js +5 -6
  19. package/dist/mcp/helpers.js.map +1 -1
  20. package/dist/mcp/tools/brief_tool.js +1 -1
  21. package/dist/mcp/tools/brief_tool.js.map +1 -1
  22. package/dist/{tui-M2KWIG2E.mjs → tui-5TFQR53I.mjs} +1 -1
  23. package/dist/utils/actor.d.ts +22 -0
  24. package/dist/utils/actor.d.ts.map +1 -0
  25. package/dist/utils/actor.js +200 -0
  26. package/dist/utils/actor.js.map +1 -0
  27. package/dist/utils/board-commit-hooks.d.ts.map +1 -1
  28. package/dist/utils/board-commit-hooks.js +3 -2
  29. package/dist/utils/board-commit-hooks.js.map +1 -1
  30. package/dist/utils/board-repo.d.ts +33 -9
  31. package/dist/utils/board-repo.d.ts.map +1 -1
  32. package/dist/utils/board-repo.js +162 -61
  33. package/dist/utils/board-repo.js.map +1 -1
  34. package/dist/utils/board-sync.d.ts +26 -11
  35. package/dist/utils/board-sync.d.ts.map +1 -1
  36. package/dist/utils/board-sync.js +141 -56
  37. package/dist/utils/board-sync.js.map +1 -1
  38. package/package.json +2 -2
@@ -1003,7 +1003,7 @@ var require_ansi_styles = __commonJS({
1003
1003
  });
1004
1004
  };
1005
1005
  var colorConvert;
1006
- var makeDynamicStyles = (wrap, targetSpace, identity2, isBackground) => {
1006
+ var makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => {
1007
1007
  if (colorConvert === void 0) {
1008
1008
  colorConvert = require_color_convert();
1009
1009
  }
@@ -1012,7 +1012,7 @@ var require_ansi_styles = __commonJS({
1012
1012
  for (const [sourceSpace, suite] of Object.entries(colorConvert)) {
1013
1013
  const name = sourceSpace === "ansi16" ? "ansi" : sourceSpace;
1014
1014
  if (sourceSpace === targetSpace) {
1015
- styles[name] = wrap(identity2, offset);
1015
+ styles[name] = wrap(identity, offset);
1016
1016
  } else if (typeof suite === "object") {
1017
1017
  styles[name] = wrap(suite[targetSpace], offset);
1018
1018
  }
@@ -11549,18 +11549,34 @@ import * as path2 from "path";
11549
11549
  import { spawnSync } from "child_process";
11550
11550
  var DEFAULT_BOARD_BRANCH = "brainfile";
11551
11551
  var BOARD_BRANCH_CONFIG_KEY = "brainfile.branch";
11552
+ var BOARD_REF_NAMESPACE = "refs/brainfile";
11553
+ var BOARD_NAME_CONFIG_KEY = "brainfile.boardName";
11554
+ var LEGACY_REMOTE_BRANCH_CONFIG_KEY = "brainfile.remoteBranch";
11555
+ var DEFAULT_BOARD_NAME = "board";
11556
+ function remoteBoardRef(name) {
11557
+ return `${BOARD_REF_NAMESPACE}/${name}`;
11558
+ }
11559
+ function boardTrackingRef(remote, name) {
11560
+ return `${BOARD_REF_NAMESPACE}/remotes/${remote}/${name}`;
11561
+ }
11562
+ var NETWORK_TIMEOUT_MS = 8e3;
11563
+ var NO_PROMPT_ENV = { GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo", SSH_ASKPASS: "echo" };
11564
+ function gitNetwork(args, cwd) {
11565
+ return git(args, cwd, NO_PROMPT_ENV, NETWORK_TIMEOUT_MS);
11566
+ }
11552
11567
  var EXCLUDE_ENTRY = ".brainfile/";
11553
11568
  var EXCLUDE_MARKER = "# brainfile: board worktree (managed by `brainfile`)";
11554
11569
  var FALLBACK_IDENTITY_NAME = "brainfile";
11555
11570
  var FALLBACK_IDENTITY_EMAIL = "brainfile@localhost";
11556
11571
  var MAX_MESSAGE_LENGTH = 72;
11557
11572
  var BOARD_SUBDIRS = ["board", "logs"];
11558
- function git(args, cwd, env) {
11573
+ function git(args, cwd, env, timeoutMs) {
11559
11574
  const result = spawnSync("git", args, {
11560
11575
  cwd,
11561
11576
  encoding: "utf-8",
11562
11577
  env: env ? { ...process.env, ...env } : process.env,
11563
- stdio: ["ignore", "pipe", "pipe"]
11578
+ stdio: ["ignore", "pipe", "pipe"],
11579
+ ...timeoutMs ? { timeout: timeoutMs, killSignal: "SIGKILL" } : {}
11564
11580
  });
11565
11581
  return {
11566
11582
  ok: result.status === 0,
@@ -11625,25 +11641,75 @@ function findTrackedBoardDir(cwd) {
11625
11641
  if (!hit) return null;
11626
11642
  return fs2.existsSync(path2.join(hit.path, "brainfile.md")) ? hit.path : null;
11627
11643
  }
11628
- function materializeBoardWorktree(cwd) {
11629
- const branch = boardBranch(cwd);
11630
- const hasLocal = git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], cwd).ok;
11631
- const configuredRemote = git(["config", "--get", "brainfile.remote"], cwd).stdout || null;
11632
- const configuredBranch = git(["config", "--get", "brainfile.remoteBranch"], cwd).stdout || branch;
11633
- let upstream = null;
11634
- if (!hasLocal) {
11635
- if (configuredRemote) {
11636
- const ref = `${configuredRemote}/${configuredBranch}`;
11637
- if (!git(["rev-parse", "--verify", "--quiet", `refs/remotes/${ref}`], cwd).ok) {
11638
- git(["fetch", "--quiet", configuredRemote, configuredBranch], cwd);
11639
- }
11640
- if (git(["rev-parse", "--verify", "--quiet", `refs/remotes/${ref}`], cwd).ok) upstream = ref;
11641
- }
11642
- if (!upstream && git(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}`], cwd).ok) {
11643
- upstream = `origin/${branch}`;
11644
- }
11644
+ var PROBE_MISS_TTL_MS = 10 * 6e4;
11645
+ function probeMissFile(cwd) {
11646
+ const common = gitCommonDir(cwd);
11647
+ return common ? path2.join(common, "brainfile-probe.json") : null;
11648
+ }
11649
+ function recentProbeMiss(cwd, remote) {
11650
+ const file = probeMissFile(cwd);
11651
+ if (!file) return false;
11652
+ try {
11653
+ const miss = JSON.parse(fs2.readFileSync(file, "utf-8"));
11654
+ return miss.remote === remote && typeof miss.at === "number" && Date.now() - miss.at < PROBE_MISS_TTL_MS;
11655
+ } catch {
11656
+ return false;
11657
+ }
11658
+ }
11659
+ function recordProbeMiss(cwd, remote) {
11660
+ const file = probeMissFile(cwd);
11661
+ if (!file) return;
11662
+ try {
11663
+ fs2.writeFileSync(file, JSON.stringify({ remote, at: Date.now() }), "utf-8");
11664
+ } catch {
11665
+ }
11666
+ }
11667
+ function clearProbeMiss(cwd) {
11668
+ const file = probeMissFile(cwd);
11669
+ if (file) fs2.rmSync(file, { force: true });
11670
+ }
11671
+ function listRemoteBoards(cwd, remote) {
11672
+ const r = gitNetwork(["ls-remote", "--quiet", remote, `${BOARD_REF_NAMESPACE}/*`], cwd);
11673
+ if (!r.ok) return null;
11674
+ const names = [];
11675
+ for (const line of r.stdout.split("\n")) {
11676
+ const ref = line.split(" ")[1] ?? "";
11677
+ const name = ref.slice(BOARD_REF_NAMESPACE.length + 1);
11678
+ if (ref.startsWith(`${BOARD_REF_NAMESPACE}/`) && name && !name.includes("/")) names.push(name);
11645
11679
  }
11646
- if (!hasLocal && !upstream) return null;
11680
+ return names;
11681
+ }
11682
+ function findBoardStart(cwd, branch, options) {
11683
+ if (git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], cwd).ok) return { ref: branch, remote: null };
11684
+ const configuredRemote = git(["config", "--get", "brainfile.remote"], cwd).stdout || null;
11685
+ const remote = configuredRemote ?? (git(["remote", "get-url", "origin"], cwd).ok ? "origin" : null);
11686
+ if (!remote) return null;
11687
+ const configuredName = git(["config", "--get", BOARD_NAME_CONFIG_KEY], cwd).stdout || git(["config", "--get", LEGACY_REMOTE_BRANCH_CONFIG_KEY], cwd).stdout || null;
11688
+ const knownName = configuredName ?? DEFAULT_BOARD_NAME;
11689
+ const known = boardTrackingRef(remote, knownName);
11690
+ if (git(["rev-parse", "--verify", "--quiet", known], cwd).ok) return { ref: known, remote, name: knownName };
11691
+ if (options.fresh || !recentProbeMiss(cwd, remote)) {
11692
+ const names = listRemoteBoards(cwd, remote);
11693
+ const name = names === null ? null : configuredName ? names.includes(configuredName) ? configuredName : null : names.includes(DEFAULT_BOARD_NAME) ? DEFAULT_BOARD_NAME : names.length === 1 ? names[0] : null;
11694
+ if (name) {
11695
+ const tracking = boardTrackingRef(remote, name);
11696
+ if (gitNetwork(["fetch", "--quiet", "--no-tags", remote, `+${remoteBoardRef(name)}:${tracking}`], cwd).ok) {
11697
+ clearProbeMiss(cwd);
11698
+ return { ref: tracking, remote, name };
11699
+ }
11700
+ }
11701
+ }
11702
+ const legacyBranch = configuredName ?? branch;
11703
+ const legacy = `refs/remotes/${remote}/${legacyBranch}`;
11704
+ if (!git(["rev-parse", "--verify", "--quiet", legacy], cwd).ok && configuredRemote && (options.fresh || !recentProbeMiss(cwd, remote))) {
11705
+ gitNetwork(["fetch", "--quiet", "--no-tags", remote, `+refs/heads/${legacyBranch}:${legacy}`], cwd);
11706
+ }
11707
+ if (git(["rev-parse", "--verify", "--quiet", legacy], cwd).ok) return { ref: legacy, remote };
11708
+ recordProbeMiss(cwd, remote);
11709
+ return null;
11710
+ }
11711
+ function materializeBoardWorktree(cwd, options = {}) {
11712
+ const branch = boardBranch(cwd);
11647
11713
  const worktrees = listWorktrees(cwd);
11648
11714
  if (worktrees.length === 0) return null;
11649
11715
  if (worktrees.some((w) => w.branch === `refs/heads/${branch}`)) return null;
@@ -11657,15 +11723,21 @@ function materializeBoardWorktree(cwd) {
11657
11723
  target = path2.join(main.path, ".brainfile");
11658
11724
  }
11659
11725
  if (fs2.existsSync(target)) return null;
11660
- const r = hasLocal ? git(["worktree", "add", "--quiet", target, branch], cwd) : git(["worktree", "add", "--quiet", "--track", "-b", branch, target, upstream], cwd);
11726
+ const start = findBoardStart(cwd, branch, options);
11727
+ if (!start) return null;
11728
+ const r = start.remote ? git(["worktree", "add", "--quiet", "--no-track", "-b", branch, target, start.ref], cwd) : git(["worktree", "add", "--quiet", target, branch], cwd);
11661
11729
  if (!r.ok) return null;
11662
11730
  ensureExcludeEntry(cwd);
11663
11731
  for (const sub of BOARD_SUBDIRS) fs2.mkdirSync(path2.join(target, sub), { recursive: true });
11664
- const cameFrom = upstream ? upstream.slice(0, upstream.indexOf("/")) : null;
11665
- if (cameFrom && !configuredRemote) git(["config", "brainfile.remote", cameFrom], cwd);
11732
+ if (start.remote) {
11733
+ if (!git(["config", "--get", "brainfile.remote"], cwd).stdout) git(["config", "brainfile.remote", start.remote], cwd);
11734
+ if (start.name && start.name !== DEFAULT_BOARD_NAME && !git(["config", "--get", BOARD_NAME_CONFIG_KEY], cwd).stdout) {
11735
+ git(["config", BOARD_NAME_CONFIG_KEY, start.name], cwd);
11736
+ }
11737
+ }
11666
11738
  const relative3 = path2.relative(process.cwd(), target) || ".";
11667
11739
  process.stderr.write(
11668
- cameFrom ? `Checked out the shared board from ${cameFrom} into ${relative3}/
11740
+ start.remote ? `Checked out the shared board from ${start.remote} into ${relative3}/
11669
11741
  ` : `Checked out the board from the '${branch}' branch into ${relative3}/
11670
11742
  `
11671
11743
  );
@@ -11731,9 +11803,6 @@ function identityArgs(dotDir) {
11731
11803
  if (!git(["config", "--get", "user.email"], dotDir).stdout) args.push("-c", `user.email=${FALLBACK_IDENTITY_EMAIL}`);
11732
11804
  return args;
11733
11805
  }
11734
- function sanitizeAgent(agent) {
11735
- return agent.replace(/[<>\r\n]/g, "").trim();
11736
- }
11737
11806
  var commitListeners = [];
11738
11807
  function onBoardCommitted(listener) {
11739
11808
  if (!commitListeners.includes(listener)) commitListeners.push(listener);
@@ -11768,17 +11837,12 @@ function commitBoard(dotDir, options) {
11768
11837
  if (unmergedFiles(dotDir).length > 0) return false;
11769
11838
  if (!git(["add", "-A"], dotDir).ok) return false;
11770
11839
  if (!boardIsDirty(dotDir)) return false;
11771
- const env = {};
11772
- const agent = options.agent ? sanitizeAgent(options.agent) : "";
11773
- if (agent) {
11774
- env.GIT_AUTHOR_NAME = agent;
11775
- env.GIT_AUTHOR_EMAIL = `${agent}@brainfile.local`;
11776
- }
11777
- const message = oneLine(options.message);
11840
+ const agent = options.agent?.replace(/[[\]<>\r\n]/g, "").trim();
11841
+ const suffix = agent ? ` [${agent}]` : "";
11842
+ const message = oneLine(options.message, suffix);
11778
11843
  const r = git(
11779
11844
  [...identityArgs(dotDir), "-c", "commit.gpgsign=false", "commit", "--quiet", "--no-verify", "-m", message],
11780
- dotDir,
11781
- env
11845
+ dotDir
11782
11846
  );
11783
11847
  if (r.ok) notifyCommitted(dotDir);
11784
11848
  return r.ok;
@@ -11794,13 +11858,6 @@ function commitHandEdits(dotDir) {
11794
11858
  const summary = files.length <= 3 ? files.join(", ") : `${files.length} files`;
11795
11859
  return commitBoard(dotDir, { message: `edit: ${summary}` });
11796
11860
  }
11797
- function resolveAgentName(argv = process.argv) {
11798
- if (process.env.BRAINFILE_AGENT) return process.env.BRAINFILE_AGENT;
11799
- const flag = argv.indexOf("--agent");
11800
- if (flag >= 0 && argv[flag + 1] && !argv[flag + 1].startsWith("-")) return argv[flag + 1];
11801
- const inline = argv.find((a) => a.startsWith("--agent="));
11802
- return inline ? inline.slice("--agent=".length) : null;
11803
- }
11804
11861
  function homeBoardDir() {
11805
11862
  return path2.join(os.homedir(), ".brainfile");
11806
11863
  }
@@ -11808,9 +11865,10 @@ function isGlobalBoardRequested(argv = process.argv) {
11808
11865
  if (process.env.BRAINFILE_GLOBAL === "1") return true;
11809
11866
  return argv.includes("-g") || argv.includes("--global");
11810
11867
  }
11811
- function oneLine(message) {
11868
+ function oneLine(message, suffix = "") {
11812
11869
  const flat = message.replace(/\s+/g, " ").trim();
11813
- return flat.length > MAX_MESSAGE_LENGTH ? `${flat.slice(0, MAX_MESSAGE_LENGTH - 1)}\u2026` : flat;
11870
+ const room = Math.max(1, MAX_MESSAGE_LENGTH - suffix.length);
11871
+ return `${flat.length > room ? `${flat.slice(0, room - 1)}\u2026` : flat}${suffix}`;
11814
11872
  }
11815
11873
  function describeCommandForCommit(command) {
11816
11874
  const names = [];
@@ -11833,7 +11891,7 @@ var exitHookInstalled = false;
11833
11891
  function scheduleBoardCommit(brainfilePath, message, delayMs = 2e3) {
11834
11892
  const dotDir = path2.dirname(path2.resolve(brainfilePath));
11835
11893
  if (!isTrackedBoard(dotDir)) return;
11836
- const entry = pendingCommits.get(dotDir) ?? { messages: [], agent: resolveAgentName() };
11894
+ const entry = pendingCommits.get(dotDir) ?? { messages: [] };
11837
11895
  entry.messages.push(message);
11838
11896
  pendingCommits.set(dotDir, entry);
11839
11897
  if (flushTimer) clearTimeout(flushTimer);
@@ -11852,7 +11910,7 @@ function flushBoardCommits() {
11852
11910
  for (const [dotDir, entry] of pendingCommits) {
11853
11911
  const [first, ...rest] = entry.messages;
11854
11912
  const message = rest.length === 0 ? first : `${first} (+${rest.length} more)`;
11855
- commitBoard(dotDir, { message, agent: entry.agent });
11913
+ commitBoard(dotDir, { message });
11856
11914
  }
11857
11915
  pendingCommits.clear();
11858
11916
  }
@@ -12130,7 +12188,7 @@ import * as fs7 from "fs";
12130
12188
  import * as os3 from "os";
12131
12189
  import * as path7 from "path";
12132
12190
  var REMOTE_CONFIG_KEY = "brainfile.remote";
12133
- var REMOTE_BRANCH_CONFIG_KEY = "brainfile.remoteBranch";
12191
+ var LEGACY_CHECKED_CONFIG_KEY = "brainfile.legacyBranchChecked";
12134
12192
  var AUTOSYNC_CONFIG_KEY = "brainfile.autosync";
12135
12193
  var URL_REMOTE_NAME = "board";
12136
12194
  var MERGE_DRIVER_NAME = "brainfile";
@@ -12152,14 +12210,23 @@ function configSet(dotDir, key, value) {
12152
12210
  function boardRemote(dotDir) {
12153
12211
  return configGet(dotDir, REMOTE_CONFIG_KEY);
12154
12212
  }
12155
- function boardRemoteBranch(dotDir) {
12156
- const configured = configGet(dotDir, REMOTE_BRANCH_CONFIG_KEY);
12213
+ function boardRemoteName(dotDir) {
12214
+ const configured = configGet(dotDir, BOARD_NAME_CONFIG_KEY) ?? configGet(dotDir, LEGACY_REMOTE_BRANCH_CONFIG_KEY);
12157
12215
  if (configured) return configured;
12216
+ return defaultName(dotDir, DEFAULT_BOARD_NAME);
12217
+ }
12218
+ function defaultName(dotDir, inRepo) {
12158
12219
  if (path7.resolve(dotDir) === path7.resolve(homeBoardDir())) return "home";
12159
12220
  if (boardRepoKind(dotDir) === "standalone" && path7.basename(path7.resolve(dotDir)) === ".brainfile") {
12160
- return path7.basename(path7.dirname(path7.resolve(dotDir))) || boardBranch(dotDir);
12221
+ return path7.basename(path7.dirname(path7.resolve(dotDir))) || inRepo;
12161
12222
  }
12162
- return boardBranch(dotDir);
12223
+ return inRepo;
12224
+ }
12225
+ function boardRemoteRef(dotDir) {
12226
+ return remoteBoardRef(boardRemoteName(dotDir));
12227
+ }
12228
+ function legacyRemoteBranch(dotDir) {
12229
+ return configGet(dotDir, LEGACY_REMOTE_BRANCH_CONFIG_KEY) ?? defaultName(dotDir, boardBranch(dotDir));
12163
12230
  }
12164
12231
  function boardAutosync(dotDir) {
12165
12232
  const value = configGet(dotDir, AUTOSYNC_CONFIG_KEY);
@@ -12176,7 +12243,7 @@ function looksLikeRemoteUrl(target) {
12176
12243
  if (target.includes("/") && fs7.existsSync(target)) return true;
12177
12244
  return false;
12178
12245
  }
12179
- function setBoardRemote(cwd, target, remoteBranch) {
12246
+ function setBoardRemote(cwd, target, boardName) {
12180
12247
  const trimmed = target.trim();
12181
12248
  if (!trimmed) throw new Error("remote is required");
12182
12249
  let remote;
@@ -12194,13 +12261,16 @@ function setBoardRemote(cwd, target, remoteBranch) {
12194
12261
  throw new Error(`'${remote}' is not a git remote here.${hint} Pass a URL to add one.`);
12195
12262
  }
12196
12263
  }
12264
+ if (boardName !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(boardName)) {
12265
+ throw new Error(`'${boardName}' is not a usable board name: use letters, digits, '.', '_' or '-'.`);
12266
+ }
12197
12267
  configSet(cwd, REMOTE_CONFIG_KEY, remote);
12198
- if (remoteBranch) configSet(cwd, REMOTE_BRANCH_CONFIG_KEY, remoteBranch);
12268
+ if (boardName) configSet(cwd, BOARD_NAME_CONFIG_KEY, boardName);
12199
12269
  registerMergeDriver(cwd);
12200
12270
  return {
12201
12271
  remote,
12202
12272
  url: git(["remote", "get-url", remote], cwd).stdout || null,
12203
- remoteBranch: remoteBranch ?? boardRemoteBranch(cwd)
12273
+ remoteRef: boardRemoteRef(cwd)
12204
12274
  };
12205
12275
  }
12206
12276
  function registerMergeDriver(cwd, command) {
@@ -12255,18 +12325,12 @@ function commitCount(dotDir, range) {
12255
12325
  const r = git(["rev-list", "--count", range], dotDir);
12256
12326
  return r.ok ? Number.parseInt(r.stdout, 10) || 0 : 0;
12257
12327
  }
12258
- function identity(dotDir, agent) {
12328
+ function identityArgs2(dotDir) {
12259
12329
  const args = ["-c", "commit.gpgsign=false"];
12260
12330
  if (!git(["config", "--get", "user.email"], dotDir).ok) {
12261
12331
  args.push("-c", "user.name=brainfile", "-c", "user.email=brainfile@localhost");
12262
12332
  }
12263
- const env = {};
12264
- const name = agent?.replace(/[\r\n<>]/g, "").trim();
12265
- if (name) {
12266
- env.GIT_AUTHOR_NAME = name;
12267
- env.GIT_AUTHOR_EMAIL = `${name}@brainfile.local`;
12268
- }
12269
- return { args, env };
12333
+ return args;
12270
12334
  }
12271
12335
  function resolveCompleteVersusEdit(dotDir) {
12272
12336
  const status = git(["status", "--porcelain", "-z"], dotDir);
@@ -12310,16 +12374,49 @@ ${entryLine}
12310
12374
  }
12311
12375
  return resolved;
12312
12376
  }
12377
+ function mergeIncoming(dotDir, incoming, result) {
12378
+ if (git(["merge-base", "--is-ancestor", incoming, "HEAD"], dotDir).ok) return { ok: true, pulled: 0 };
12379
+ const before = git(["rev-parse", "HEAD"], dotDir).stdout;
12380
+ const id = identityArgs2(dotDir);
12381
+ const merge = git([...id, "merge", "--quiet", "--no-edit", incoming], dotDir);
12382
+ if (!merge.ok) {
12383
+ resolveCompleteVersusEdit(dotDir);
12384
+ const remaining = unmergedFiles(dotDir);
12385
+ if (remaining.length > 0) {
12386
+ return {
12387
+ ok: false,
12388
+ result: {
12389
+ ...result,
12390
+ ok: false,
12391
+ failure: "conflict",
12392
+ conflicts: remaining,
12393
+ warning: `merge left conflicts in ${remaining.join(", ")} \u2014 resolve them in ${dotDir}, then run sync again`
12394
+ }
12395
+ };
12396
+ }
12397
+ const commit = git([...id, "commit", "--quiet", "--no-edit", "--no-verify"], dotDir);
12398
+ if (!commit.ok) {
12399
+ return { ok: false, result: { ...result, ok: false, failure: "error", warning: `merge commit failed: ${firstLine(commit.stderr)}` } };
12400
+ }
12401
+ }
12402
+ return { ok: true, pulled: commitCount(dotDir, `${before}..${incoming}`) };
12403
+ }
12404
+ function legacyChecked(dotDir, remote) {
12405
+ return configGet(dotDir, LEGACY_CHECKED_CONFIG_KEY) === remote;
12406
+ }
12313
12407
  function syncBoard(dotDir, options = {}) {
12314
- const pull = options.pull ?? true;
12315
12408
  const push = options.push ?? true;
12316
12409
  const result = { ok: true, pulled: 0, pushed: 0, conflicts: [] };
12317
12410
  if (!isTrackedBoard(dotDir)) return { ...result, skipped: "not-tracked" };
12318
12411
  const remote = boardRemote(dotDir);
12319
12412
  if (!remote) return { ...result, skipped: "no-remote" };
12320
- const remoteBranch = boardRemoteBranch(dotDir);
12413
+ const name = boardRemoteName(dotDir);
12414
+ const remoteRef = remoteBoardRef(name);
12415
+ const tracking = boardTrackingRef(remote, name);
12321
12416
  result.remote = remote;
12322
- result.remoteBranch = remoteBranch;
12417
+ result.remoteRef = remoteRef;
12418
+ const checkLegacy = !legacyChecked(dotDir, remote);
12419
+ const pull = (options.pull ?? true) || checkLegacy;
12323
12420
  const stuck = unmergedFiles(dotDir);
12324
12421
  if (stuck.length > 0) {
12325
12422
  return finish(dotDir, { ...result, ok: false, failure: "conflict", conflicts: stuck, warning: `unresolved conflicts: ${stuck.join(", ")}` });
@@ -12328,41 +12425,38 @@ function syncBoard(dotDir, options = {}) {
12328
12425
  if (ensureBoardAttributes(dotDir)) commitBoard(dotDir, { message: "chore: merge attributes" });
12329
12426
  registerMergeDriver(dotDir);
12330
12427
  let fetched = false;
12428
+ let legacy = null;
12429
+ let legacyAbsent = false;
12331
12430
  if (pull) {
12332
- const fetch2 = git(["fetch", "--quiet", remote, remoteBranch], dotDir);
12431
+ const fetch2 = git(["fetch", "--quiet", "--no-tags", remote, `+${remoteRef}:${tracking}`], dotDir);
12333
12432
  if (fetch2.ok) {
12334
12433
  fetched = true;
12335
12434
  } else if (!isMissingRemoteRef(fetch2.stderr)) {
12336
12435
  return finish(dotDir, { ...result, ok: false, failure: "offline", warning: `fetch from ${remote} failed: ${firstLine(fetch2.stderr)}` });
12436
+ } else {
12437
+ git(["update-ref", "-d", tracking], dotDir);
12337
12438
  }
12338
- if (fetched && !git(["merge-base", "--is-ancestor", "FETCH_HEAD", "HEAD"], dotDir).ok) {
12339
- const before = git(["rev-parse", "HEAD"], dotDir).stdout;
12340
- const id = identity(dotDir, options.agent);
12341
- const merge = git([...id.args, "merge", "--quiet", "--no-edit", "FETCH_HEAD"], dotDir, id.env);
12342
- if (!merge.ok) {
12343
- resolveCompleteVersusEdit(dotDir);
12344
- const remaining = unmergedFiles(dotDir);
12345
- if (remaining.length > 0) {
12346
- return finish(dotDir, {
12347
- ...result,
12348
- ok: false,
12349
- failure: "conflict",
12350
- conflicts: remaining,
12351
- warning: `merge left conflicts in ${remaining.join(", ")} \u2014 resolve them in ${dotDir}, then run sync again`
12352
- });
12353
- }
12354
- const commit = git([...id.args, "commit", "--quiet", "--no-edit", "--no-verify"], dotDir, id.env);
12355
- if (!commit.ok) {
12356
- return finish(dotDir, { ...result, ok: false, failure: "error", warning: `merge commit failed: ${firstLine(commit.stderr)}` });
12357
- }
12439
+ if (checkLegacy) {
12440
+ const branch = legacyRemoteBranch(dotDir);
12441
+ const old = git(["fetch", "--quiet", "--no-tags", remote, `refs/heads/${branch}`], dotDir);
12442
+ if (old.ok) {
12443
+ legacy = { branch, sha: git(["rev-parse", "FETCH_HEAD"], dotDir).stdout };
12444
+ } else if (isMissingRemoteRef(old.stderr)) {
12445
+ legacyAbsent = true;
12358
12446
  }
12359
- result.pulled = commitCount(dotDir, `${before}..FETCH_HEAD`);
12447
+ }
12448
+ for (const incoming of [fetched ? tracking : null, legacy?.sha ?? null]) {
12449
+ if (!incoming) continue;
12450
+ const merged = mergeIncoming(dotDir, incoming, result);
12451
+ if (!merged.ok) return finish(dotDir, merged.result);
12452
+ result.pulled += merged.pulled;
12360
12453
  }
12361
12454
  }
12362
12455
  if (push) {
12363
- const ahead = fetched ? commitCount(dotDir, "FETCH_HEAD..HEAD") : pendingChanges(dotDir);
12456
+ const known = git(["rev-parse", "--verify", "--quiet", tracking], dotDir).ok;
12457
+ const ahead = commitCount(dotDir, known ? `${tracking}..HEAD` : "HEAD");
12364
12458
  if (ahead > 0) {
12365
- const pushed = git(["push", "--quiet", remote, `HEAD:refs/heads/${remoteBranch}`], dotDir);
12459
+ const pushed = git(["push", "--quiet", remote, `HEAD:${remoteRef}`], dotDir);
12366
12460
  if (!pushed.ok) {
12367
12461
  const rejected = /rejected|non-fast-forward|fetch first/i.test(pushed.stderr);
12368
12462
  return finish(dotDir, {
@@ -12372,8 +12466,24 @@ function syncBoard(dotDir, options = {}) {
12372
12466
  warning: rejected ? `push to ${remote} rejected (the remote moved) \u2014 run sync again` : `push to ${remote} failed: ${firstLine(pushed.stderr)}`
12373
12467
  });
12374
12468
  }
12469
+ git(["update-ref", tracking, "HEAD"], dotDir);
12375
12470
  result.pushed = ahead;
12376
12471
  }
12472
+ if (legacy && git(["merge-base", "--is-ancestor", legacy.sha, tracking], dotDir).ok) {
12473
+ const removed = git(
12474
+ ["push", "--quiet", `--force-with-lease=refs/heads/${legacy.branch}:${legacy.sha}`, remote, `:refs/heads/${legacy.branch}`],
12475
+ dotDir
12476
+ );
12477
+ if (removed.ok) {
12478
+ git(["update-ref", "-d", `refs/remotes/${remote}/${legacy.branch}`], dotDir);
12479
+ result.movedFromBranch = legacy.branch;
12480
+ configSet(dotDir, LEGACY_CHECKED_CONFIG_KEY, remote);
12481
+ }
12482
+ }
12483
+ }
12484
+ if (legacyAbsent) {
12485
+ git(["update-ref", "-d", `refs/remotes/${remote}/${legacyRemoteBranch(dotDir)}`], dotDir);
12486
+ configSet(dotDir, LEGACY_CHECKED_CONFIG_KEY, remote);
12377
12487
  }
12378
12488
  return finish(dotDir, result);
12379
12489
  }
@@ -12381,7 +12491,7 @@ function finish(dotDir, result) {
12381
12491
  writeSyncState(dotDir, {
12382
12492
  at: (/* @__PURE__ */ new Date()).toISOString(),
12383
12493
  remote: result.remote ?? "",
12384
- remoteBranch: result.remoteBranch ?? "",
12494
+ remoteRef: result.remoteRef ?? "",
12385
12495
  ok: result.ok,
12386
12496
  ...result.warning ? { warning: result.warning } : {}
12387
12497
  });
@@ -12406,10 +12516,15 @@ function syncStatusLabel(dotDir, now = Date.now()) {
12406
12516
  function pendingChanges(dotDir) {
12407
12517
  const remote = boardRemote(dotDir);
12408
12518
  if (!remote) return 0;
12409
- const tracking = `refs/remotes/${remote}/${boardRemoteBranch(dotDir)}`;
12519
+ const tracking = boardTrackingRef(remote, boardRemoteName(dotDir));
12410
12520
  const known = git(["rev-parse", "--verify", "--quiet", tracking], dotDir).ok;
12411
12521
  return commitCount(dotDir, known ? `${tracking}..HEAD` : "HEAD");
12412
12522
  }
12523
+ function boardWebUrl(remoteUrl, remoteRef) {
12524
+ if (!remoteUrl) return null;
12525
+ const m = /^(?:https:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(remoteUrl);
12526
+ return m ? `https://github.com/${m[1]}/${m[2]}/commits/${remoteRef}` : null;
12527
+ }
12413
12528
  var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
12414
12529
  function syncMessage(result) {
12415
12530
  if (result.skipped === "not-tracked") {
@@ -13442,7 +13557,6 @@ export {
13442
13557
  boardIsDirty,
13443
13558
  commitBoard,
13444
13559
  commitHandEdits,
13445
- resolveAgentName,
13446
13560
  homeBoardDir,
13447
13561
  isGlobalBoardRequested,
13448
13562
  describeCommandForCommit,
@@ -13456,7 +13570,7 @@ export {
13456
13570
  import_core5 as import_core,
13457
13571
  mergeDriverCommand,
13458
13572
  boardRemote,
13459
- boardRemoteBranch,
13573
+ boardRemoteRef,
13460
13574
  boardAutosync,
13461
13575
  setBoardAutosync,
13462
13576
  setBoardRemote,
@@ -13467,6 +13581,7 @@ export {
13467
13581
  secondsSinceSync,
13468
13582
  syncStatusLabel,
13469
13583
  pendingChanges,
13584
+ boardWebUrl,
13470
13585
  syncMessage,
13471
13586
  effectiveAutosync,
13472
13587
  clearPushLock,