@junctionpanel/server 0.1.28 → 0.1.29

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 (61) hide show
  1. package/dist/server/client/daemon-client.d.ts +39 -4
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +83 -3
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-sdk-types.d.ts +1 -0
  6. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  7. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  8. package/dist/server/server/agent/providers/codex-app-server-agent.js +1 -39
  9. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  10. package/dist/server/server/agent/providers/gemini-agent.d.ts +4 -1
  11. package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
  12. package/dist/server/server/agent/providers/gemini-agent.js +36 -8
  13. package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
  14. package/dist/server/server/agent/providers/image-attachments.d.ts +8 -0
  15. package/dist/server/server/agent/providers/image-attachments.d.ts.map +1 -0
  16. package/dist/server/server/agent/providers/image-attachments.js +47 -0
  17. package/dist/server/server/agent/providers/image-attachments.js.map +1 -0
  18. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +3 -0
  19. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  20. package/dist/server/server/daemon-doctor.d.ts +39 -0
  21. package/dist/server/server/daemon-doctor.d.ts.map +1 -0
  22. package/dist/server/server/daemon-doctor.js +260 -0
  23. package/dist/server/server/daemon-doctor.js.map +1 -0
  24. package/dist/server/server/daemon-provider-settings.d.ts +42 -0
  25. package/dist/server/server/daemon-provider-settings.d.ts.map +1 -0
  26. package/dist/server/server/daemon-provider-settings.js +207 -0
  27. package/dist/server/server/daemon-provider-settings.js.map +1 -0
  28. package/dist/server/server/file-explorer/service.d.ts +4 -2
  29. package/dist/server/server/file-explorer/service.d.ts.map +1 -1
  30. package/dist/server/server/file-explorer/service.js +104 -2
  31. package/dist/server/server/file-explorer/service.js.map +1 -1
  32. package/dist/server/server/persisted-config.d.ts +24 -24
  33. package/dist/server/server/session.d.ts +10 -1
  34. package/dist/server/server/session.d.ts.map +1 -1
  35. package/dist/server/server/session.js +421 -60
  36. package/dist/server/server/session.js.map +1 -1
  37. package/dist/server/server/worktree-bootstrap.d.ts +1 -0
  38. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -1
  39. package/dist/server/server/worktree-bootstrap.js +4 -0
  40. package/dist/server/server/worktree-bootstrap.js.map +1 -1
  41. package/dist/server/shared/messages.d.ts +3673 -22
  42. package/dist/server/shared/messages.d.ts.map +1 -1
  43. package/dist/server/shared/messages.js +151 -0
  44. package/dist/server/shared/messages.js.map +1 -1
  45. package/dist/server/utils/checkout-git.d.ts +23 -4
  46. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  47. package/dist/server/utils/checkout-git.js +298 -79
  48. package/dist/server/utils/checkout-git.js.map +1 -1
  49. package/dist/server/utils/directory-suggestions.d.ts +4 -0
  50. package/dist/server/utils/directory-suggestions.d.ts.map +1 -1
  51. package/dist/server/utils/directory-suggestions.js +83 -5
  52. package/dist/server/utils/directory-suggestions.js.map +1 -1
  53. package/dist/server/utils/workspace-ref-files.d.ts +31 -0
  54. package/dist/server/utils/workspace-ref-files.d.ts.map +1 -0
  55. package/dist/server/utils/workspace-ref-files.js +207 -0
  56. package/dist/server/utils/workspace-ref-files.js.map +1 -0
  57. package/dist/server/utils/worktree.d.ts +6 -3
  58. package/dist/server/utils/worktree.d.ts.map +1 -1
  59. package/dist/server/utils/worktree.js +46 -45
  60. package/dist/server/utils/worktree.js.map +1 -1
  61. package/package.json +2 -2
@@ -12,10 +12,16 @@ const READ_ONLY_GIT_ENV = {
12
12
  ...process.env,
13
13
  GIT_OPTIONAL_LOCKS: "0",
14
14
  };
15
+ const SAFE_GIT_REMOTE_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
15
16
  const SMALL_OUTPUT_MAX_BUFFER = 20 * 1024 * 1024; // 20MB
16
17
  async function execGit(command, options) {
17
18
  return execAsync(command, { ...options, maxBuffer: SMALL_OUTPUT_MAX_BUFFER });
18
19
  }
20
+ function assertSafeRemoteName(remoteName) {
21
+ if (!SAFE_GIT_REMOTE_NAME_PATTERN.test(remoteName)) {
22
+ throw new Error(`Invalid remote name: ${remoteName}`);
23
+ }
24
+ }
19
25
  async function spawnLimitedText(params) {
20
26
  const accept = new Set(params.acceptExitCodes ?? [0]);
21
27
  return new Promise((resolvePromise, rejectPromise) => {
@@ -72,7 +78,7 @@ async function spawnLimitedText(params) {
72
78
  });
73
79
  });
74
80
  }
75
- function normalizeBranchSuggestionName(raw) {
81
+ function normalizeBranchSuggestionName(raw, source) {
76
82
  const trimmed = raw.trim();
77
83
  if (!trimmed) {
78
84
  return null;
@@ -84,8 +90,12 @@ function normalizeBranchSuggestionName(raw) {
84
90
  else if (normalized.startsWith("refs/remotes/")) {
85
91
  normalized = normalized.slice("refs/remotes/".length);
86
92
  }
87
- if (normalized.startsWith("origin/")) {
88
- normalized = normalized.slice("origin/".length);
93
+ if (source === "remote") {
94
+ const slashIndex = normalized.indexOf("/");
95
+ if (slashIndex <= 0) {
96
+ return null;
97
+ }
98
+ normalized = normalized.slice(slashIndex + 1);
89
99
  }
90
100
  if (!normalized || normalized === "HEAD") {
91
101
  return null;
@@ -128,13 +138,19 @@ export async function listBranchSuggestions(cwd, options) {
128
138
  const requestedLimit = options?.limit ?? 50;
129
139
  const limit = Math.max(1, Math.min(200, requestedLimit));
130
140
  const query = options?.query?.trim().toLowerCase() ?? "";
141
+ if (options?.remoteName?.trim()) {
142
+ assertSafeRemoteName(options.remoteName.trim());
143
+ }
144
+ const remoteRefPrefix = options?.remoteName?.trim()
145
+ ? `refs/remotes/${options.remoteName.trim()}`
146
+ : "refs/remotes";
131
147
  const [localRefs, remoteRefs] = await Promise.all([
132
148
  listGitRefs(cwd, "refs/heads"),
133
- listGitRefs(cwd, "refs/remotes/origin"),
149
+ listGitRefs(cwd, remoteRefPrefix),
134
150
  ]);
135
151
  const merged = new Map();
136
152
  for (const localRef of localRefs) {
137
- const normalized = normalizeBranchSuggestionName(localRef);
153
+ const normalized = normalizeBranchSuggestionName(localRef, "local");
138
154
  if (!normalized) {
139
155
  continue;
140
156
  }
@@ -143,7 +159,7 @@ export async function listBranchSuggestions(cwd, options) {
143
159
  merged.set(normalized, origins);
144
160
  }
145
161
  for (const remoteRef of remoteRefs) {
146
- const normalized = normalizeBranchSuggestionName(remoteRef);
162
+ const normalized = normalizeBranchSuggestionName(remoteRef, "remote");
147
163
  if (!normalized) {
148
164
  continue;
149
165
  }
@@ -482,9 +498,10 @@ async function isWorkingTreeDirty(cwd) {
482
498
  });
483
499
  return stdout.trim().length > 0;
484
500
  }
485
- async function getOriginRemoteUrl(cwd) {
501
+ async function getRemoteUrl(cwd, remoteName) {
502
+ assertSafeRemoteName(remoteName);
486
503
  try {
487
- const { stdout } = await execAsync("git config --get remote.origin.url", {
504
+ const { stdout } = await execAsync(`git config --get remote.${remoteName}.url`, {
488
505
  cwd,
489
506
  env: READ_ONLY_GIT_ENV,
490
507
  });
@@ -495,20 +512,171 @@ async function getOriginRemoteUrl(cwd) {
495
512
  return null;
496
513
  }
497
514
  }
498
- async function hasOriginRemote(cwd) {
499
- const url = await getOriginRemoteUrl(cwd);
500
- return url !== null;
515
+ function parseSymbolicHeadRef(ref) {
516
+ const trimmed = ref.trim();
517
+ if (!trimmed.startsWith("ref: ")) {
518
+ return null;
519
+ }
520
+ const headRef = trimmed.slice("ref: ".length).trim();
521
+ const match = /^refs\/heads\/(.+)$/.exec(headRef);
522
+ return match?.[1] ?? null;
501
523
  }
502
- export async function resolveBaseRef(repoRoot) {
524
+ async function getLocalRemoteHeadBranch(remoteUrl) {
525
+ let localPath = null;
503
526
  try {
504
- const { stdout } = await execAsync("git symbolic-ref --quiet refs/remotes/origin/HEAD", {
505
- cwd: repoRoot,
527
+ if (remoteUrl.startsWith("file://")) {
528
+ localPath = realpathSync(new URL(remoteUrl));
529
+ }
530
+ else if (remoteUrl.startsWith("/")) {
531
+ localPath = realpathSync(remoteUrl);
532
+ }
533
+ }
534
+ catch {
535
+ localPath = null;
536
+ }
537
+ if (!localPath) {
538
+ return null;
539
+ }
540
+ for (const candidateHeadPath of [`${localPath}/HEAD`, `${localPath}/.git/HEAD`]) {
541
+ try {
542
+ const headFile = await openFile(candidateHeadPath, "r");
543
+ try {
544
+ const contents = await headFile.readFile("utf8");
545
+ const branch = parseSymbolicHeadRef(contents);
546
+ if (branch) {
547
+ return branch;
548
+ }
549
+ }
550
+ finally {
551
+ await headFile.close();
552
+ }
553
+ }
554
+ catch {
555
+ // try next location
556
+ }
557
+ }
558
+ return null;
559
+ }
560
+ async function listRemoteNames(cwd) {
561
+ const { stdout } = await execAsync("git remote", {
562
+ cwd,
563
+ env: READ_ONLY_GIT_ENV,
564
+ });
565
+ return stdout
566
+ .split("\n")
567
+ .map((line) => line.trim())
568
+ .filter((line) => line.length > 0)
569
+ .sort((left, right) => left.localeCompare(right));
570
+ }
571
+ async function getRemoteHeadBranch(cwd, remoteName) {
572
+ assertSafeRemoteName(remoteName);
573
+ try {
574
+ const { stdout } = await execAsync(`git symbolic-ref --quiet refs/remotes/${remoteName}/HEAD`, {
575
+ cwd,
506
576
  env: READ_ONLY_GIT_ENV,
507
577
  });
508
578
  const ref = stdout.trim();
579
+ if (!ref) {
580
+ const remoteUrl = await getRemoteUrl(cwd, remoteName);
581
+ return remoteUrl ? getLocalRemoteHeadBranch(remoteUrl) : null;
582
+ }
583
+ const shortRef = ref.replace(/^refs\/remotes\//, "");
584
+ return shortRef.startsWith(`${remoteName}/`)
585
+ ? shortRef.slice(`${remoteName}/`.length)
586
+ : shortRef;
587
+ }
588
+ catch {
589
+ try {
590
+ const { stdout } = await execAsync(`git ls-remote --symref ${remoteName} HEAD`, {
591
+ cwd,
592
+ env: READ_ONLY_GIT_ENV,
593
+ });
594
+ const headLine = stdout
595
+ .split("\n")
596
+ .map((line) => line.trim())
597
+ .find((line) => line.startsWith("ref: refs/heads/") && line.endsWith("\tHEAD"));
598
+ if (!headLine) {
599
+ const remoteUrl = await getRemoteUrl(cwd, remoteName);
600
+ return remoteUrl ? getLocalRemoteHeadBranch(remoteUrl) : null;
601
+ }
602
+ const match = /^ref:\s+refs\/heads\/(.+)\tHEAD$/.exec(headLine);
603
+ return match?.[1] ?? null;
604
+ }
605
+ catch {
606
+ const remoteUrl = await getRemoteUrl(cwd, remoteName);
607
+ return remoteUrl ? getLocalRemoteHeadBranch(remoteUrl) : null;
608
+ }
609
+ }
610
+ }
611
+ export async function listGitRemotes(cwd) {
612
+ await requireGitRepo(cwd);
613
+ const remoteNames = await listRemoteNames(cwd);
614
+ const remotes = await Promise.all(remoteNames.map(async (name) => {
615
+ const [url, headBranch] = await Promise.all([
616
+ getRemoteUrl(cwd, name),
617
+ getRemoteHeadBranch(cwd, name),
618
+ ]);
619
+ return {
620
+ name,
621
+ url,
622
+ headBranch,
623
+ isGitHub: typeof url === "string" && parseGitHubRepoFromRemoteUrl(url) !== null,
624
+ };
625
+ }));
626
+ return remotes;
627
+ }
628
+ async function resolveEffectiveRemoteName(cwd, requestedRemoteName) {
629
+ const remotes = await listRemoteNames(cwd);
630
+ if (remotes.length === 0) {
631
+ return null;
632
+ }
633
+ const trimmedRequested = requestedRemoteName?.trim();
634
+ if (trimmedRequested && remotes.includes(trimmedRequested)) {
635
+ assertSafeRemoteName(trimmedRequested);
636
+ return trimmedRequested;
637
+ }
638
+ if (remotes.includes("origin")) {
639
+ return "origin";
640
+ }
641
+ return remotes[0] ?? null;
642
+ }
643
+ export async function resolveBaseRef(repoRoot, options) {
644
+ const remoteName = await resolveEffectiveRemoteName(repoRoot, options?.remoteName);
645
+ if (remoteName) {
646
+ try {
647
+ const { stdout } = await execAsync(`git symbolic-ref --quiet refs/remotes/${remoteName}/HEAD`, {
648
+ cwd: repoRoot,
649
+ env: READ_ONLY_GIT_ENV,
650
+ });
651
+ const ref = stdout.trim();
652
+ if (ref) {
653
+ const remoteShort = ref.replace(/^refs\/remotes\//, "");
654
+ const localName = remoteShort.startsWith(`${remoteName}/`)
655
+ ? remoteShort.slice(`${remoteName}/`.length)
656
+ : remoteShort;
657
+ try {
658
+ await execAsync(`git show-ref --verify --quiet refs/heads/${localName}`, {
659
+ cwd: repoRoot,
660
+ env: READ_ONLY_GIT_ENV,
661
+ });
662
+ return localName;
663
+ }
664
+ catch {
665
+ return remoteShort;
666
+ }
667
+ }
668
+ }
669
+ catch {
670
+ // ignore
671
+ }
672
+ }
673
+ try {
674
+ const fallbackOriginRef = await execAsync("git symbolic-ref --quiet refs/remotes/origin/HEAD", {
675
+ cwd: repoRoot,
676
+ env: READ_ONLY_GIT_ENV,
677
+ });
678
+ const ref = fallbackOriginRef.stdout.trim();
509
679
  if (ref) {
510
- // Prefer a local branch name (e.g. "main") over the remote-tracking ref (e.g. "origin/main")
511
- // so that status/diff/merge all operate against the same base ref.
512
680
  const remoteShort = ref.replace(/^refs\/remotes\//, "");
513
681
  const localName = remoteShort.startsWith("origin/")
514
682
  ? remoteShort.slice("origin/".length)
@@ -545,7 +713,15 @@ export async function resolveBaseRef(repoRoot) {
545
713
  return null;
546
714
  }
547
715
  function normalizeLocalBranchRefName(input) {
548
- return input.startsWith("origin/") ? input.slice("origin/".length) : input;
716
+ if (input.startsWith("origin/")) {
717
+ return input.slice("origin/".length);
718
+ }
719
+ if (input.startsWith("refs/remotes/")) {
720
+ const stripped = input.slice("refs/remotes/".length);
721
+ const slashIndex = stripped.indexOf("/");
722
+ return slashIndex > 0 ? stripped.slice(slashIndex + 1) : stripped;
723
+ }
724
+ return input;
549
725
  }
550
726
  async function doesGitRefExist(cwd, fullRef) {
551
727
  try {
@@ -559,28 +735,34 @@ async function doesGitRefExist(cwd, fullRef) {
559
735
  return false;
560
736
  }
561
737
  }
562
- async function resolveBestBaseRefForMerge(cwd, normalizedBaseRef) {
738
+ async function resolveBestBaseRefForMerge(cwd, normalizedBaseRef, options) {
739
+ const remoteName = await resolveEffectiveRemoteName(cwd, options?.remoteName);
740
+ const remoteRef = remoteName ? `refs/remotes/${remoteName}/${normalizedBaseRef}` : null;
563
741
  const [hasLocal, hasOrigin] = await Promise.all([
564
742
  doesGitRefExist(cwd, `refs/heads/${normalizedBaseRef}`),
565
- doesGitRefExist(cwd, `refs/remotes/origin/${normalizedBaseRef}`),
743
+ remoteRef ? doesGitRefExist(cwd, remoteRef) : Promise.resolve(false),
566
744
  ]);
567
745
  if (hasLocal && !hasOrigin) {
568
746
  return normalizedBaseRef;
569
747
  }
570
748
  if (!hasLocal && hasOrigin) {
571
- return `origin/${normalizedBaseRef}`;
749
+ return `${remoteName}/${normalizedBaseRef}`;
572
750
  }
573
751
  if (!hasLocal && !hasOrigin) {
574
- throw new Error(`Base branch not found locally or on origin: ${normalizedBaseRef}`);
752
+ const remoteLabel = remoteName ?? "any remote";
753
+ throw new Error(`Base branch not found locally or on ${remoteLabel}: ${normalizedBaseRef}`);
575
754
  }
576
755
  // Both exist: choose the ref with more unique commits compared to the other.
577
756
  try {
578
- const { stdout } = await execAsync(`git rev-list --left-right --count ${normalizedBaseRef}...origin/${normalizedBaseRef}`, { cwd, env: READ_ONLY_GIT_ENV });
757
+ if (!remoteName) {
758
+ return normalizedBaseRef;
759
+ }
760
+ const { stdout } = await execAsync(`git rev-list --left-right --count ${normalizedBaseRef}...${remoteName}/${normalizedBaseRef}`, { cwd, env: READ_ONLY_GIT_ENV });
579
761
  const [localOnlyRaw, originOnlyRaw] = stdout.trim().split(/\s+/);
580
762
  const localOnly = Number.parseInt(localOnlyRaw ?? "0", 10);
581
763
  const originOnly = Number.parseInt(originOnlyRaw ?? "0", 10);
582
764
  if (!Number.isNaN(localOnly) && !Number.isNaN(originOnly) && originOnly > localOnly) {
583
- return `origin/${normalizedBaseRef}`;
765
+ return `${remoteName}/${normalizedBaseRef}`;
584
766
  }
585
767
  }
586
768
  catch {
@@ -634,11 +816,12 @@ async function inspectCheckoutContext(cwd, context) {
634
816
  if (!root) {
635
817
  return null;
636
818
  }
637
- const [currentBranch, remoteUrl, configured] = await Promise.all([
819
+ const [currentBranch, configured, effectiveRemoteName] = await Promise.all([
638
820
  getCurrentBranch(cwd),
639
- getOriginRemoteUrl(cwd),
640
821
  getConfiguredBaseRefForCwd(cwd, context),
822
+ resolveEffectiveRemoteName(cwd),
641
823
  ]);
824
+ const remoteUrl = effectiveRemoteName ? await getRemoteUrl(cwd, effectiveRemoteName) : null;
642
825
  return {
643
826
  worktreeRoot: root,
644
827
  currentBranch,
@@ -1130,7 +1313,7 @@ export async function mergeFromBase(cwd, options = {}, context) {
1130
1313
  throw new Error("Unable to determine current branch for merge");
1131
1314
  }
1132
1315
  const configured = await getConfiguredBaseRefForCwd(cwd, context);
1133
- const baseRef = configured.baseRef ?? options.baseRef ?? (await resolveBaseRef(cwd));
1316
+ const baseRef = configured.baseRef ?? options.baseRef ?? (await resolveBaseRef(cwd, { remoteName: options.remoteName }));
1134
1317
  if (!baseRef) {
1135
1318
  throw new Error("Unable to determine base branch for merge");
1136
1319
  }
@@ -1148,7 +1331,9 @@ export async function mergeFromBase(cwd, options = {}, context) {
1148
1331
  }
1149
1332
  }
1150
1333
  const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
1151
- const bestBaseRef = await resolveBestBaseRefForMerge(cwd, normalizedBaseRef);
1334
+ const bestBaseRef = await resolveBestBaseRefForMerge(cwd, normalizedBaseRef, {
1335
+ remoteName: options.remoteName,
1336
+ });
1152
1337
  if (bestBaseRef === currentBranch) {
1153
1338
  return;
1154
1339
  }
@@ -1207,17 +1392,17 @@ export async function mergeFromBase(cwd, options = {}, context) {
1207
1392
  throw error;
1208
1393
  }
1209
1394
  }
1210
- export async function pushCurrentBranch(cwd) {
1395
+ export async function pushCurrentBranch(cwd, options) {
1211
1396
  await requireGitRepo(cwd);
1212
1397
  const currentBranch = await getCurrentBranch(cwd);
1213
1398
  if (!currentBranch || currentBranch === "HEAD") {
1214
1399
  throw new Error("Unable to determine current branch for push");
1215
1400
  }
1216
- const hasRemote = await hasOriginRemote(cwd);
1217
- if (!hasRemote) {
1218
- throw new Error("Remote 'origin' is not configured.");
1401
+ const remoteName = await resolveEffectiveRemoteName(cwd, options?.remoteName);
1402
+ if (!remoteName) {
1403
+ throw new Error("No git remotes are configured.");
1219
1404
  }
1220
- await execAsync(`git push -u origin ${currentBranch}`, { cwd });
1405
+ await execAsync(`git push -u ${remoteName} ${currentBranch}`, { cwd });
1221
1406
  }
1222
1407
  const GH_JSON_MAX_BYTES = 512 * 1024;
1223
1408
  const GH_FAILED_LOG_MAX_BYTES = 200 * 1024;
@@ -1241,54 +1426,65 @@ function getCommandErrorText(error) {
1241
1426
  function isGhAuthError(error) {
1242
1427
  const text = getCommandErrorText(error);
1243
1428
  return (text.includes("gh auth login") ||
1429
+ text.includes("gh auth status") ||
1244
1430
  text.includes("not logged into any github hosts") ||
1431
+ text.includes("not logged into github.com") ||
1245
1432
  text.includes("authentication failed") ||
1246
1433
  text.includes("authentication required") ||
1247
1434
  text.includes("bad credentials") ||
1248
1435
  text.includes("http 401"));
1249
1436
  }
1250
- async function resolveGitHubRepo(cwd) {
1251
- try {
1252
- const { stdout } = await execAsync("git config --get remote.origin.url", {
1253
- cwd,
1254
- env: READ_ONLY_GIT_ENV,
1255
- });
1256
- const url = stdout.trim();
1257
- if (!url) {
1258
- return null;
1259
- }
1260
- let cleaned = url;
1261
- if (cleaned.startsWith("git@github.com:")) {
1262
- cleaned = cleaned.slice("git@github.com:".length);
1263
- }
1264
- else if (cleaned.startsWith("https://github.com/")) {
1265
- cleaned = cleaned.slice("https://github.com/".length);
1266
- }
1267
- else if (cleaned.startsWith("http://github.com/")) {
1268
- cleaned = cleaned.slice("http://github.com/".length);
1437
+ function parseGitHubRepoFromRemoteUrl(url) {
1438
+ let cleaned = url.trim();
1439
+ if (!cleaned) {
1440
+ return null;
1441
+ }
1442
+ if (cleaned.startsWith("git@github.com:")) {
1443
+ cleaned = cleaned.slice("git@github.com:".length);
1444
+ }
1445
+ else if (cleaned.startsWith("https://github.com/")) {
1446
+ cleaned = cleaned.slice("https://github.com/".length);
1447
+ }
1448
+ else if (cleaned.startsWith("http://github.com/")) {
1449
+ cleaned = cleaned.slice("http://github.com/".length);
1450
+ }
1451
+ else {
1452
+ const marker = "github.com/";
1453
+ const index = cleaned.indexOf(marker);
1454
+ if (index !== -1) {
1455
+ cleaned = cleaned.slice(index + marker.length);
1269
1456
  }
1270
1457
  else {
1271
- const marker = "github.com/";
1272
- const index = cleaned.indexOf(marker);
1273
- if (index !== -1) {
1274
- cleaned = cleaned.slice(index + marker.length);
1275
- }
1276
- else {
1277
- return null;
1278
- }
1279
- }
1280
- if (cleaned.endsWith(".git")) {
1281
- cleaned = cleaned.slice(0, -".git".length);
1282
- }
1283
- if (!cleaned.includes("/")) {
1284
1458
  return null;
1285
1459
  }
1286
- return cleaned;
1287
1460
  }
1288
- catch {
1289
- // ignore
1461
+ if (cleaned.endsWith(".git")) {
1462
+ cleaned = cleaned.slice(0, -".git".length);
1463
+ }
1464
+ if (!cleaned.includes("/")) {
1465
+ return null;
1466
+ }
1467
+ return cleaned;
1468
+ }
1469
+ async function resolveGitHubRepo(cwd, options) {
1470
+ const remoteName = await resolveEffectiveRemoteName(cwd, options?.remoteName);
1471
+ if (!remoteName) {
1472
+ return null;
1473
+ }
1474
+ const remoteUrl = await getRemoteUrl(cwd, remoteName);
1475
+ return remoteUrl ? parseGitHubRepoFromRemoteUrl(remoteUrl) : null;
1476
+ }
1477
+ async function hasAuthenticatedGitHubCliSession(cwd) {
1478
+ try {
1479
+ await execFileAsync("gh", ["auth", "status", "--hostname", "github.com"], { cwd });
1480
+ return true;
1481
+ }
1482
+ catch (error) {
1483
+ if (isGhAuthError(error)) {
1484
+ return false;
1485
+ }
1486
+ throw error;
1290
1487
  }
1291
- return null;
1292
1488
  }
1293
1489
  async function listPullRequestsForHead(options) {
1294
1490
  const { stdout } = await execFileAsync("gh", [
@@ -1666,11 +1862,10 @@ function appendBoundedText(parts, next, currentBytes) {
1666
1862
  parts.push(`${truncated}\n\n[combined log output truncated]`);
1667
1863
  return GH_FAILED_LOG_TOTAL_MAX_BYTES;
1668
1864
  }
1669
- async function loadCurrentPullRequest(cwd) {
1865
+ async function loadCurrentPullRequest(cwd, options) {
1670
1866
  await requireGitRepo(cwd);
1671
- const repo = await resolveGitHubRepo(cwd);
1672
1867
  const head = await getCurrentBranch(cwd);
1673
- if (!repo || !head) {
1868
+ if (!head) {
1674
1869
  return {
1675
1870
  status: null,
1676
1871
  githubFeaturesEnabled: false,
@@ -1689,6 +1884,26 @@ async function loadCurrentPullRequest(cwd) {
1689
1884
  headSha: null,
1690
1885
  };
1691
1886
  }
1887
+ const remoteName = await resolveEffectiveRemoteName(cwd, options?.remoteName);
1888
+ const remoteUrl = remoteName ? await getRemoteUrl(cwd, remoteName) : null;
1889
+ if (!remoteUrl) {
1890
+ const hasAuthenticatedGhSession = await hasAuthenticatedGitHubCliSession(cwd);
1891
+ return {
1892
+ status: null,
1893
+ githubFeaturesEnabled: hasAuthenticatedGhSession,
1894
+ checks: [],
1895
+ headSha: null,
1896
+ };
1897
+ }
1898
+ const repo = parseGitHubRepoFromRemoteUrl(remoteUrl);
1899
+ if (!repo) {
1900
+ return {
1901
+ status: null,
1902
+ githubFeaturesEnabled: false,
1903
+ checks: [],
1904
+ headSha: null,
1905
+ };
1906
+ }
1692
1907
  const owner = repo.split("/")[0];
1693
1908
  let openPulls;
1694
1909
  try {
@@ -1794,13 +2009,14 @@ async function loadCurrentPullRequest(cwd) {
1794
2009
  export async function createPullRequest(cwd, options) {
1795
2010
  await requireGitRepo(cwd);
1796
2011
  await ensureGhAvailable(cwd);
1797
- const repo = await resolveGitHubRepo(cwd);
2012
+ const remoteName = await resolveEffectiveRemoteName(cwd, options.remoteName);
2013
+ const repo = await resolveGitHubRepo(cwd, { remoteName });
1798
2014
  if (!repo) {
1799
2015
  throw new Error("Unable to determine GitHub repo from git remote");
1800
2016
  }
1801
2017
  const head = options.head ?? (await getCurrentBranch(cwd));
1802
2018
  const configured = await getConfiguredBaseRefForCwd(cwd);
1803
- const base = configured.baseRef ?? options.base ?? (await resolveBaseRef(cwd));
2019
+ const base = configured.baseRef ?? options.base ?? (await resolveBaseRef(cwd, { remoteName }));
1804
2020
  if (!head) {
1805
2021
  throw new Error("Unable to determine head branch for PR");
1806
2022
  }
@@ -1811,7 +2027,10 @@ export async function createPullRequest(cwd, options) {
1811
2027
  if (configured.isJunctionOwnedWorktree && options.base && options.base !== base) {
1812
2028
  throw new Error(`Base ref mismatch: expected ${base}, got ${options.base}`);
1813
2029
  }
1814
- await execAsync(`git push -u origin ${head}`, { cwd });
2030
+ if (!remoteName) {
2031
+ throw new Error("No git remotes are configured.");
2032
+ }
2033
+ await execAsync(`git push -u ${remoteName} ${head}`, { cwd });
1815
2034
  const args = ["api", "-X", "POST", `repos/${repo}/pulls`, "-f", `title=${options.title}`];
1816
2035
  args.push("-f", `head=${head}`);
1817
2036
  args.push("-f", `base=${normalizedBase}`);
@@ -1825,15 +2044,15 @@ export async function createPullRequest(cwd, options) {
1825
2044
  }
1826
2045
  return { url: parsed.url, number: parsed.number };
1827
2046
  }
1828
- export async function getPullRequestStatus(cwd) {
1829
- const lookup = await loadCurrentPullRequest(cwd);
2047
+ export async function getPullRequestStatus(cwd, options) {
2048
+ const lookup = await loadCurrentPullRequest(cwd, options);
1830
2049
  return {
1831
2050
  status: lookup.status,
1832
2051
  githubFeaturesEnabled: lookup.githubFeaturesEnabled,
1833
2052
  };
1834
2053
  }
1835
- export async function getPullRequestFailureLogs(cwd) {
1836
- const lookup = await loadCurrentPullRequest(cwd);
2054
+ export async function getPullRequestFailureLogs(cwd, options) {
2055
+ const lookup = await loadCurrentPullRequest(cwd, options);
1837
2056
  if (!lookup.githubFeaturesEnabled) {
1838
2057
  return {
1839
2058
  logs: null,
@@ -1910,7 +2129,7 @@ export async function mergePullRequest(cwd, options = {}) {
1910
2129
  if (method !== "squash") {
1911
2130
  throw new Error("Only squash merge is supported");
1912
2131
  }
1913
- const statusResult = await getPullRequestStatus(cwd);
2132
+ const statusResult = await getPullRequestStatus(cwd, { remoteName: options.remoteName });
1914
2133
  if (!statusResult.githubFeaturesEnabled) {
1915
2134
  throw new Error("GitHub CLI (gh) is not available or not authenticated");
1916
2135
  }