@michelj/context-guard 0.6.2 → 0.6.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michelj/context-guard",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Next-generation collaboration layer for humans and coding agents on Codex, Cursor, and Claude.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,8 +56,8 @@
56
56
  "node": ">=18"
57
57
  },
58
58
  "devDependencies": {
59
- "@michelj/context-guard-core": "https://github.com/Michel-Johnson/Context-Guard-Cloud/releases/download/shared-v1.1.0/michelj-context-guard-core-1.1.0.tgz",
60
- "@michelj/context-guard-workbench": "https://github.com/Michel-Johnson/Context-Guard-Cloud/releases/download/shared-v1.1.0/michelj-context-guard-workbench-1.1.0.tgz",
59
+ "@michelj/context-guard-core": "https://github.com/Michel-Johnson/Context-Guard-Cloud/releases/download/shared-v1.1.1/michelj-context-guard-core-1.1.1.tgz",
60
+ "@michelj/context-guard-workbench": "https://github.com/Michel-Johnson/Context-Guard-Cloud/releases/download/shared-v1.1.1/michelj-context-guard-workbench-1.1.1.tgz",
61
61
  "marked": "15.0.12",
62
62
  "playwright": "1.62.1"
63
63
  }
@@ -74,7 +74,7 @@ const I18N = {
74
74
  remove:"移除", addModule:"接入模块",
75
75
  noOpenBugs:"没有 Bug。", unnamedBug:"未命名 Bug", noOpenTodos:"没有 TODO。", unnamedTodo:"未命名 TODO",
76
76
  bugSending:"发送中", bugSendFailed:"发送失败", taskQueued:"Cloud 已排队", taskCloudQueued:"等待本地接收", taskBlocked:"等待前序任务完成", taskReceived:"Codex 已收到", taskUncertain:"接收结果待确认", taskInterrupted:"执行中断", taskWaitingReview:"等待 Plan 审核",
77
- allSessions:"主工作台 · 工作中 Session", globalSessionView:"仅跟随 Main",
77
+ allSessions:"主工作台 · 工作中 Session", globalSessionView:"仅跟随 Main", showOtherSessions:"显示其他会话", hideOtherSessions:"仅显示工作中会话",
78
78
  projectOverview:"项目总览",
79
79
  scopeRequired:"该 Session 尚未获得当前节点权限。确认后将授权当前节点、所有上级和直接关联节点,共 {n} 个新节点。",
80
80
  bugWaiting:"待处理", bugProcessing:"处理中", bugHandoff:"待接手",
@@ -178,7 +178,7 @@ const I18N = {
178
178
  remove:"Remove", addModule:"Attach module",
179
179
  noOpenBugs:"No bugs.", unnamedBug:"Untitled bug", noOpenTodos:"No TODOs.", unnamedTodo:"Untitled TODO",
180
180
  bugSending:"Sending", bugSendFailed:"Send failed", taskQueued:"Queued in Cloud", taskCloudQueued:"Waiting for local host", taskReceived:"Received by Codex", taskUncertain:"Receipt uncertain", taskInterrupted:"Interrupted", taskWaitingReview:"Waiting for Plan review",
181
- allSessions:"Main workbench · Working sessions", globalSessionView:"Main branch only",
181
+ allSessions:"Main workbench · Working sessions", globalSessionView:"Main branch only", showOtherSessions:"Show other sessions", hideOtherSessions:"Show working sessions only",
182
182
  projectOverview:"Projects",
183
183
  scopeRequired:"This session cannot access the current node. Confirm to authorize the node, its ancestors, and direct relations ({n} new nodes).",
184
184
  bugWaiting:"Waiting", bugProcessing:"In progress", bugHandoff:"Needs handoff",
@@ -376,6 +376,7 @@ let bugPathReturn = null;
376
376
  let activeWorkPanelKind = "bug";
377
377
  let foldDormant = false;
378
378
  let foldMemoryDoc = false;
379
+ let showOtherSessions = false;
379
380
  let foldIdea = false;
380
381
  let foldTodo = false;
381
382
  let foldBug = false;
@@ -1307,7 +1308,7 @@ function isWorkingSession(meta){
1307
1308
  function visibleSessionChoices(){
1308
1309
  const sessions=normalizeSessions(workbenchSync?.sessions||[]);
1309
1310
  const selected=String(workbenchSync?.activeSession||"__all__");
1310
- return sessions.filter(meta=>isWorkingSession(meta)||(selected!=="__all__"&&sessionIdOf(meta)===selected));
1311
+ return sessions.filter(meta=>showOtherSessions||isWorkingSession(meta)||(selected!=="__all__"&&sessionIdOf(meta)===selected));
1311
1312
  }
1312
1313
  function browserCurrentSessionId(){
1313
1314
  const id=String(new URLSearchParams(location.search).get("session")||"").trim();
@@ -1469,7 +1470,12 @@ function renderSessionMenu(){
1469
1470
  <span class="session-option-copy"><span class="session-option-name">${esc(label.primary)}</span>${label.secondary?`<span class="session-option-context">${esc(label.secondary)}</span>`:""}</span>
1470
1471
  <span class="session-option-state ${lifecycle.state}">${esc(lifecycle.label)}</span><span class="session-status ${lifecycle.state}" aria-label="${lifecycle.label}"></span>
1471
1472
  </button>`;
1472
- }).join("");
1473
+ }).join("") + `<button type="button" data-show-other-sessions aria-pressed="${showOtherSessions}">${esc(t(showOtherSessions?"hideOtherSessions":"showOtherSessions"))}</button>`;
1474
+ menu.querySelector('[data-show-other-sessions]').onclick = ()=>{
1475
+ showOtherSessions = !showOtherSessions;
1476
+ syncSessionSelect();
1477
+ renderSessionMenu();
1478
+ };
1473
1479
  menu.querySelectorAll("[data-session]").forEach(button=>{
1474
1480
  button.onclick = async ()=>{
1475
1481
  try {
@@ -780,18 +780,26 @@ export class WorkbenchSync {
780
780
  if (this.switchingSession) return false;
781
781
  if (this.retrying) await this.retrying;
782
782
  if (this.switchingSession) return false;
783
- if (sessionId !== ALL_SESSIONS && !this.sessions.some(item => item.id === sessionId && item.bindingState !== 'unavailable')) return false;
784
- if (this.dirty()) {
785
- await this.flush();
786
- if (this.dirty()) { this.setStatus(this.status, '当前视图仍有未保存内容,暂不能切换'); return false; }
787
- }
788
- if (this.switchingSession) return false;
783
+ const unavailable = ['unavailable', 'stale', 'closed', 'published', 'expired', 'deleted', 'invalid'];
784
+ if (sessionId !== ALL_SESSIONS && !this.sessions.some(item => item.id === sessionId && !unavailable.includes(String(item.bindingState || '').toLowerCase()) && !unavailable.includes(String(item.status || '').toLowerCase()))) return false;
789
785
  const privateCloud = this.config?.root?.startsWith('cloud:') && this.config.root !== 'cloud:overview';
790
786
  const nextView = sessionId === ALL_SESSIONS || (!privateCloud && this.project?.kind !== 'git') ? 'main' : `session:${sessionId}`;
791
- const previous = Object.fromEntries(['activeSession', 'pendingSession', 'manualSession', 'viewId', 'captureKey', 'doc', 'version', 'source', 'baseTree', 'ready', 'initializationRequired', 'pendingRequest', 'inputDraft', 'serverRecovery', 'revision', 'sessionUnavailable'].map(key => [key, this[key]]));
792
- const previousTree = copy(this.a.getRoot());
787
+ let previous, previousTree;
788
+ const previousUrl = location.href;
793
789
  this.switchingSession = true;
794
790
  try {
791
+ // A missing Cloud snapshot must not flush the current view's private draft.
792
+ // This read only checks availability; reload still obtains the final baseline.
793
+ if (privateCloud && nextView.startsWith('session:') && nextView !== this.viewId) {
794
+ const target = await this.call('/api/state', undefined, 'GET', nextView);
795
+ if (target.error || target.recovery || !target.doc?.root) throw Object.assign(new Error(target.error?.message || target.recovery?.message || '地图尚未同步'), { code: target.error?.code || (!target.doc?.root ? 'UNKNOWN_VIEW' : undefined) });
796
+ }
797
+ if (this.dirty()) {
798
+ await this.flush();
799
+ if (this.dirty()) { this.setStatus(this.status, '当前视图仍有未保存内容,暂不能切换'); return false; }
800
+ }
801
+ previous = Object.fromEntries(['activeSession', 'pendingSession', 'manualSession', 'viewId', 'captureKey', 'doc', 'version', 'source', 'baseTree', 'ready', 'initializationRequired', 'pendingRequest', 'inputDraft', 'serverRecovery', 'recoveryBlocked', 'revision', 'cachedDiff', 'sessionUnavailable'].map(key => [key, this[key]]));
802
+ previousTree = copy(this.a.getRoot());
795
803
  this.activeSession = sessionId;
796
804
  this.sessionUnavailable = false;
797
805
  this.pendingSession = '';
@@ -809,12 +817,15 @@ export class WorkbenchSync {
809
817
  setTimeout(() => this.refreshAccess().catch(error => this.setStatus('error', error.message)), 0);
810
818
  return true;
811
819
  } catch (error) {
812
- Object.assign(this, previous);
813
- if (previous.doc) this.a.apply({ ...previous.doc, root: previousTree });
814
- this.panel.querySelector('#cg-sync-initialize').hidden = !previous.initializationRequired || (previous.viewId === 'main' && previous.source?.status !== 'local-folder');
815
- this.loadGeneration = (this.loadGeneration || 0) + 1;
816
- if (!this.sessionUnavailable) this.connect();
817
- this.setStatus('error', '无法切换地图:' + error.message);
820
+ if (previous) {
821
+ Object.assign(this, previous);
822
+ history.replaceState(null, '', previousUrl);
823
+ if (previous.doc) this.a.apply({ ...previous.doc, root: previousTree });
824
+ this.panel.querySelector('#cg-sync-initialize').hidden = !previous.initializationRequired || (previous.viewId === 'main' && previous.source?.status !== 'local-folder');
825
+ this.loadGeneration = (this.loadGeneration || 0) + 1;
826
+ if (!this.sessionUnavailable) this.connect();
827
+ }
828
+ this.setStatus('error', error.code === 'UNKNOWN_VIEW' ? '地图尚未同步;原地图和草稿已保留' : '无法切换地图,原地图和草稿已保留:' + error.message);
818
829
  return false;
819
830
  } finally { this.switchingSession = false; }
820
831
  }
@@ -1,13 +1,14 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
3
  import { randomBytes, randomUUID } from 'node:crypto';
4
+ import { performance } from 'node:perf_hooks';
4
5
  import { atomicWrite, encode, readJSON, withFileLock, pause } from '../shared/io.mjs';
5
6
  import { fail } from '../shared/protocol.mjs';
6
7
 
7
- async function request(origin, route, input, fetcher) {
8
+ async function request(origin, route, input, fetcher, timeoutMs = 15000) {
8
9
  let response;
9
10
  try {
10
- response = await fetcher(new URL(route, origin), { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(15000),
11
+ response = await fetcher(new URL(route, origin), { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(timeoutMs),
11
12
  headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) });
12
13
  } catch { fail('UNAVAILABLE', 'Cloud authorization is unavailable; retry the same connection command'); }
13
14
  if ([404, 405, 426].includes(response.status)) fail('UPGRADE_REQUIRED', 'Cloud does not support browser authorization; upgrade Cloud or use the explicit private-input login');
@@ -25,17 +26,32 @@ async function request(origin, route, input, fetcher) {
25
26
 
26
27
  // The model sees only the verification URL/code. The backend keeps the claim
27
28
  // secret and final credential private, including across a stopped CLI invocation.
28
- export async function browserLogin(device, { repository, repositoryId, wait = false, onPending = () => {}, fetcher = fetch, label = os.hostname() }) {
29
+ export async function browserLogin(device, { repository, repositoryId, wait = false, onPending = () => {}, fetcher = fetch, label = os.hostname(), clock = () => performance.now(), sleep = pause }) {
29
30
  const origin = new URL(device.origin);
30
31
  if (origin.username || origin.password || origin.pathname !== '/' || origin.search || origin.hash || origin.protocol !== 'https:' && !(device.allowLoopback && origin.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(origin.hostname))) fail('FORBIDDEN', 'Cloud authorization requires an HTTPS origin');
31
32
  const file = path.join(device.directory, 'browser-login.json');
33
+ let deadline = clock() + 600000, grant;
34
+ const checkDeadline = () => {
35
+ if (wait && clock() >= deadline) fail('UNAUTHORIZED', 'Browser authorization expired; retry the connection command');
36
+ };
37
+ const authorize = async (route, input) => {
38
+ checkDeadline();
39
+ try {
40
+ const result = await request(device.origin, route, input, fetcher, wait ? Math.max(1, Math.min(15000, Math.ceil(deadline - clock()))) : 15000);
41
+ checkDeadline();
42
+ return result;
43
+ } catch (error) { checkDeadline(); throw error; }
44
+ };
32
45
  let first = true;
33
46
  for (;;) {
47
+ checkDeadline();
34
48
  const outcome = await withFileLock(file + '.lock', async () => {
49
+ checkDeadline();
35
50
  const connected = await readJSON(device.file, null);
36
51
  if (connected?.origin === device.origin && connected.repositoryId === repositoryId && connected.credential) {
37
52
  try {
38
53
  await device.transmit({ v: 2, id: randomUUID(), type: 'sync.heartbeat', payload: { sessions: [] } });
54
+ checkDeadline();
39
55
  const { credential: _secret, origin: _origin, ...data } = connected;
40
56
  return { connected: true, ...data };
41
57
  } catch (error) {
@@ -43,23 +59,31 @@ export async function browserLogin(device, { repository, repositoryId, wait = fa
43
59
  }
44
60
  }
45
61
  let pending = await readJSON(file, null);
46
- if (!pending?.deviceCode || pending.origin !== device.origin || pending.repository !== repository || pending.repositoryId !== repositoryId || Date.parse(pending.expiresAt) <= Date.now()) {
62
+ checkDeadline();
63
+ if (grant && ['deviceCode', 'verificationUrl', 'userCode', 'expiresAt', 'origin', 'repository', 'repositoryId'].some(key => pending?.[key] !== grant[key])) fail('CONFLICT', 'Browser authorization changed; retry the connection command');
64
+ if (!pending?.deviceCode || pending.origin !== device.origin || pending.repository !== repository || pending.repositoryId !== repositoryId || !Number.isFinite(Date.parse(pending.expiresAt)) || !grant && Date.parse(pending.expiresAt) <= Date.now()) {
47
65
  pending = { origin: device.origin, repository, repositoryId, deviceCode: randomBytes(32).toString('base64url'), label: String(label).slice(0, 80),
48
66
  expiresAt: new Date(Date.now() + 600000).toISOString() };
49
67
  await atomicWrite(file, encode(pending));
50
68
  }
51
69
  if (!pending.verificationUrl) {
52
70
  const identity = await device.identity();
53
- const { data } = await request(device.origin, '/api/auth/device/start', { repository, clientId: identity.clientId, deviceCode: pending.deviceCode, label: pending.label }, fetcher);
71
+ const { data } = await authorize('/api/auth/device/start', { repository, clientId: identity.clientId, deviceCode: pending.deviceCode, label: pending.label });
54
72
  const verification = new URL(data.verificationPath, device.origin);
55
73
  if (verification.origin !== device.origin || verification.pathname !== '/connect' || !/^[A-F0-9]{4}-[A-F0-9]{4}$/.test(data.userCode) || verification.searchParams.get('code') !== data.userCode || !Number.isFinite(Date.parse(data.expiresAt)) || !Number.isInteger(data.expiresIn) || data.expiresIn <= 0 || data.expiresIn > 600) fail('UNAVAILABLE', 'Cloud returned an invalid authorization link');
56
74
  pending = { ...pending, verificationUrl: verification.href, userCode: data.userCode, expiresAt: new Date(Date.now() + data.expiresIn * 1000).toISOString() };
57
75
  await atomicWrite(file, encode(pending));
58
76
  }
59
77
  const safe = { connected: false, authorizationRequired: true, verificationUrl: pending.verificationUrl, userCode: pending.userCode, expiresAt: pending.expiresAt };
78
+ if (wait && !grant) {
79
+ // A displayed grant and this invocation's budget never renew in a wait loop.
80
+ deadline = Math.min(deadline, clock() + Math.max(0, Date.parse(pending.expiresAt) - Date.now()));
81
+ grant = { ...pending };
82
+ }
83
+ checkDeadline();
60
84
  if (first) await onPending(safe);
61
85
  let result;
62
- try { result = await request(device.origin, '/api/auth/device/poll', { deviceCode: pending.deviceCode }, fetcher); }
86
+ try { result = await authorize('/api/auth/device/poll', { deviceCode: pending.deviceCode }); }
63
87
  catch (error) {
64
88
  if (['UNAUTHORIZED', 'FORBIDDEN'].includes(error.code)) await atomicWrite(file, encode({ status: error.code }));
65
89
  throw error;
@@ -71,8 +95,9 @@ export async function browserLogin(device, { repository, repositoryId, wait = fa
71
95
  return { connected: true, ...result.data };
72
96
  });
73
97
  if (outcome.connected || !wait) return outcome;
74
- if (Date.now() >= Date.parse(outcome.expiresAt)) fail('UNAUTHORIZED', 'Browser authorization expired; retry the connection command');
98
+ checkDeadline();
75
99
  first = false;
76
- await pause(5000);
100
+ await sleep(Math.min(5000, Math.max(1, deadline - clock())));
101
+ checkDeadline();
77
102
  }
78
103
  }
@@ -11,7 +11,7 @@ const digest = value => createHash('sha256').update(String(value)).digest('hex')
11
11
  export const nonInteractiveGitEnvironment = (parent = process.env) => ({ ...parent, GIT_TERMINAL_PROMPT: '0' });
12
12
  export const optionalGitMiss = error => Number.isInteger(error?.code) && error.code !== 0 && !error.killed && error.signal == null;
13
13
 
14
- async function git(root, args, { optional = false } = {}) {
14
+ async function git(root, args, { optional = false, rawOutput = false } = {}) {
15
15
  try {
16
16
  const { stdout } = await execFileAsync('git', args, {
17
17
  cwd: root,
@@ -21,7 +21,7 @@ async function git(root, args, { optional = false } = {}) {
21
21
  timeout: 10000,
22
22
  maxBuffer: 16 * 1024 * 1024,
23
23
  });
24
- return stdout.trim();
24
+ return rawOutput ? stdout : stdout.trim();
25
25
  } catch (error) {
26
26
  if (optional && optionalGitMiss(error)) return '';
27
27
  throw error;
@@ -62,13 +62,40 @@ async function defaultBranch(root, remote = 'origin') {
62
62
  return '';
63
63
  }
64
64
 
65
- async function worktreeMetadata(root, main = null) {
65
+ async function physicalGitDirectories(root) {
66
+ let output;
67
+ try {
68
+ output = await git(root, ['rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir', '--git-dir'], { rawOutput: true });
69
+ } catch (error) {
70
+ if (!optionalGitMiss(error)) throw error;
71
+ // Supported Git reports these misses without any directory records. Other
72
+ // failures may be older-option incompatibilities; keep the legacy path.
73
+ if (/fatal: (?:not a git repository|this operation must be run in a work tree)/.test(String(error.stderr || ''))) return null;
74
+ }
75
+ if (output !== undefined) {
76
+ const records = (output.endsWith('\n') ? output.slice(0, -1) : output).split('\n');
77
+ if (records.length === 3 && records.every(value => value && !value.includes('\r') && path.isAbsolute(value))) {
78
+ return { topLevel: records[0], commonDir: records[1], gitDir: records[2] };
79
+ }
80
+ }
81
+ const topLevel = await git(root, ['rev-parse', '--show-toplevel'], { optional: true });
82
+ if (!topLevel) return null;
83
+ const worktreeRoot = await fs.realpath(topLevel);
84
+ let commonDir = await git(worktreeRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir'], { optional: true });
85
+ if (!commonDir) {
86
+ const relative = await git(worktreeRoot, ['rev-parse', '--git-common-dir']);
87
+ commonDir = path.resolve(worktreeRoot, relative);
88
+ }
89
+ return { topLevel, commonDir };
90
+ }
91
+
92
+ async function worktreeMetadata(root, main = null, physicalGitDir) {
66
93
  const mainBranch = main?.branch || '';
67
94
  const mainRef = main?.ref || '';
68
95
  const [head, currentBranch, rawGitDir, mainSha] = await Promise.all([
69
96
  git(root, ['rev-parse', 'HEAD'], { optional: true }),
70
97
  git(root, ['branch', '--show-current'], { optional: true }),
71
- git(root, ['rev-parse', '--path-format=absolute', '--git-dir'], { optional: true }),
98
+ physicalGitDir === undefined ? git(root, ['rev-parse', '--path-format=absolute', '--git-dir'], { optional: true }) : physicalGitDir,
72
99
  mainRef ? git(root, ['rev-parse', '--verify', '--quiet', `${mainRef}^{commit}`], { optional: true }) : '',
73
100
  ]);
74
101
  const gitDir = rawGitDir ? await fs.realpath(rawGitDir).catch(() => path.resolve(root, rawGitDir)) : '';
@@ -91,8 +118,8 @@ function mainTarget({ mode = 'remote', remote = 'origin', branch } = {}) {
91
118
 
92
119
  export async function resolveProject(openedRoot) {
93
120
  const requestedRoot = await fs.realpath(path.resolve(openedRoot));
94
- const topLevel = await git(requestedRoot, ['rev-parse', '--show-toplevel'], { optional: true });
95
- if (!topLevel) {
121
+ const directories = await physicalGitDirectories(requestedRoot);
122
+ if (!directories) {
96
123
  const projectId = `folder-${digest(requestedRoot).slice(0, 20)}`;
97
124
  return {
98
125
  projectId,
@@ -111,27 +138,28 @@ export async function resolveProject(openedRoot) {
111
138
  branch: '', head: '', gitDir: '', mainBranch: '', mainRef: '', mainSha: '',
112
139
  };
113
140
  }
114
- const worktreeRoot = await fs.realpath(topLevel);
115
- let commonDir = await git(worktreeRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir'], { optional: true });
116
- if (!commonDir) {
117
- const relative = await git(worktreeRoot, ['rev-parse', '--git-common-dir']);
118
- commonDir = path.resolve(worktreeRoot, relative);
119
- }
120
- commonDir = await fs.realpath(commonDir);
141
+ const worktreeRoot = await fs.realpath(directories.topLevel);
142
+ const commonDir = await fs.realpath(directories.commonDir);
121
143
  const projectId = `git-${digest(commonDir).slice(0, 20)}`;
122
144
  const sharedDir = path.join(commonDir, 'context-guard');
123
145
  const stored = await readJSON(path.join(sharedDir, 'project-binding.json'), null);
124
146
  if (stored && (stored.projectId !== projectId || !stored.main?.branch)) throw new Error('Invalid project binding; repair it explicitly, do not recreate it');
125
- const originUrl = await git(worktreeRoot, ['config', '--get', 'remote.origin.url'], { optional: true });
126
- const originGithub = githubRepository(originUrl);
127
- const automaticBranch = originGithub ? await defaultBranch(worktreeRoot) : '';
128
- const automatic = automaticBranch ? mainTarget({ remote: 'origin', branch: automaticBranch }) : null;
129
- const selected = stored?.projectId === projectId && stored?.main?.branch ? mainTarget(stored.main) : automatic;
130
- const remote = selected?.mode === 'remote'
131
- ? await git(worktreeRoot, ['config', '--get', `remote.${selected.remote}.url`], { optional: true })
132
- : '';
147
+ let selected;
148
+ if (stored) {
149
+ selected = mainTarget(stored.main);
150
+ } else {
151
+ const originUrl = await git(worktreeRoot, ['config', '--get', 'remote.origin.url'], { optional: true });
152
+ const originGithub = githubRepository(originUrl);
153
+ const automaticBranch = originGithub ? await defaultBranch(worktreeRoot) : '';
154
+ selected = automaticBranch ? mainTarget({ remote: 'origin', branch: automaticBranch }) : null;
155
+ }
156
+ const [remote, metadata] = await Promise.all([
157
+ selected?.mode === 'remote'
158
+ ? git(worktreeRoot, ['config', '--get', `remote.${selected.remote}.url`], { optional: true })
159
+ : '',
160
+ worktreeMetadata(worktreeRoot, selected, directories.gitDir),
161
+ ]);
133
162
  const github = githubRepository(remote);
134
- const metadata = await worktreeMetadata(worktreeRoot, selected);
135
163
  return {
136
164
  projectId,
137
165
  kind: 'git',