@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.
|
|
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.
|
|
60
|
-
"@michelj/context-guard-workbench": "https://github.com/Michel-Johnson/Context-Guard-Cloud/releases/download/shared-v1.1.
|
|
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
|
-
|
|
784
|
-
if (this.
|
|
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
|
-
|
|
792
|
-
const
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
98
|
+
checkDeadline();
|
|
75
99
|
first = false;
|
|
76
|
-
await
|
|
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
|
|
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
|
|
95
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
? await
|
|
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',
|