circuschief 0.8.0 → 1.1.0
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 +1 -1
- package/packages/server/src/agents/AgentGateway.js +2 -0
- package/packages/server/src/agents/adapters/GeminiAdapter.js +105 -0
- package/packages/server/src/agents/adapters/cliUtils.js +15 -0
- package/packages/server/src/agents/adapters/codexCliRunner.js +1 -8
- package/packages/server/src/agents/adapters/geminiCliRunner.js +183 -0
- package/packages/server/src/agents/adapters/geminiEventMapper.js +195 -0
- package/packages/server/src/api/commandButtons.js +16 -15
- package/packages/server/src/api/projects-commandButtons.js +6 -6
- package/packages/server/src/api/projects-session-create.js +109 -0
- package/packages/server/src/api/projects-session-defaults.js +51 -0
- package/packages/server/src/api/projects-session-helpers.js +47 -1
- package/packages/server/src/api/projects-templates.js +38 -0
- package/packages/server/src/api/projects.js +28 -180
- package/packages/server/src/api/sessions-commands.js +21 -18
- package/packages/server/src/api/sessions-patch.js +41 -1
- package/packages/server/src/db/ProviderRepository.js +4 -2
- package/packages/server/src/db/SessionRepository.js +1 -1
- package/packages/server/src/db/SessionTemplateRepository.js +23 -2
- package/packages/server/src/db/migrations/canvasItemsMigrations.js +109 -0
- package/packages/server/src/db/migrations/conversationsMigrations.js +187 -0
- package/packages/server/src/db/migrations/index.js +234 -6
- package/packages/server/src/db/migrations/kanbanMigrations.js +99 -0
- package/packages/server/src/db/migrations/miscMigrations.js +244 -0
- package/packages/server/src/db/migrations/projectsMigrations.js +130 -0
- package/packages/server/src/db/migrations/providerCommitAttributionMigrations.js +30 -0
- package/packages/server/src/db/migrations/providerMigrations.js +250 -0
- package/packages/server/src/db/migrations/sessionTableRecreate.js +136 -0
- package/packages/server/src/db/migrations/sessionsMigrations.js +300 -0
- package/packages/server/src/db/seedBaselineData.js +23 -1
- package/packages/server/src/db/session-helpers.js +26 -1
- package/packages/server/src/schema.sql +5 -1
- package/packages/server/src/services/commandButtonPrompts.js +9 -7
- package/packages/server/src/services/e2eSpawnCapture.js +47 -6
- package/packages/server/src/services/geminiSpawnHelper.js +47 -0
- package/packages/server/src/services/gitCommitAttribution.js +38 -8
- package/packages/server/src/services/gitDiff.js +107 -0
- package/packages/server/src/services/gitRepoUrl.js +174 -0
- package/packages/server/src/services/gitService.js +43 -311
- package/packages/server/src/services/gitWorktree.js +127 -0
- package/packages/server/src/services/providerTestService.js +59 -1
- package/packages/server/src/services/queryParamBuilder.js +33 -1
- package/packages/server/src/services/sessionExecution.js +4 -0
- package/packages/server/src/services/sessionPrompts.js +23 -1
- package/packages/server/src/services/sessionProvider.js +41 -1
- package/packages/shared/src/constants.js +1 -1
- package/packages/shared/src/contracts/providers.js +1 -1
- package/packages/shared/src/contracts/sessions.js +27 -1
- package/packages/shared/src/contracts/templates.js +10 -0
- package/packages/shared/src/types.js +7 -0
- package/packages/web/dist/assets/{ActiveSessionsView-B0XHqLmv.js → ActiveSessionsView-EdNxmPmZ.js} +1 -1
- package/packages/web/dist/assets/{AgentLogsView-DmsjUMlB.js → AgentLogsView-C2wX0JPP.js} +2 -2
- package/packages/web/dist/assets/ApiClient-DfbJwzpz.js +1 -0
- package/packages/web/dist/assets/ArchiveConfirmModal-DJERn5XO.js +1 -0
- package/packages/web/dist/assets/CommandButtonDetailView-CBPI8-US.js +1 -0
- package/packages/web/dist/assets/CommandButtonDetailView-D9zjx9ME.css +1 -0
- package/packages/web/dist/assets/EffortLevelSelector-PaBpUveC.js +1 -0
- package/packages/web/dist/assets/{GeneralSettingsView-D1nI8_zk.js → GeneralSettingsView-Dw-x83R0.js} +1 -1
- package/packages/web/dist/assets/{InputWithButton-CAkttyqx.js → InputWithButton-CHHcpF4I.js} +1 -1
- package/packages/web/dist/assets/{InterpolationHelp-BO1j9Z3_.js → InterpolationHelp-CLNPz8s8.js} +1 -1
- package/packages/web/dist/assets/MarkdownEditor-DYi1igfT.js +2 -0
- package/packages/web/dist/assets/ModelSelector-Cko_yTO5.js +1 -0
- package/packages/web/dist/assets/{ModelSelector-BSxKUSus.css → ModelSelector-Dtwe5xLH.css} +1 -1
- package/packages/web/dist/assets/{NewSessionView-BDPb-1qr.css → NewSessionView-DBl7T2Xp.css} +1 -1
- package/packages/web/dist/assets/NewSessionView-DwUfBg70.js +3 -0
- package/packages/web/dist/assets/ProjectEditView-CSbsea3U.js +1 -0
- package/packages/web/dist/assets/ProjectEditView-DbqTbA0q.css +1 -0
- package/packages/web/dist/assets/{ProjectListView-DcNyuINs.js → ProjectListView-CEc_LWZL.js} +1 -1
- package/packages/web/dist/assets/{ProjectNewView-B5YV62hv.js → ProjectNewView-D4U0uRlp.js} +1 -1
- package/packages/web/dist/assets/ProvidersView-2KCOiY6Q.css +1 -0
- package/packages/web/dist/assets/ProvidersView-CD1j8BOv.js +1 -0
- package/packages/web/dist/assets/QuickResponsesPanel-Dp39f12o.js +1 -0
- package/packages/web/dist/assets/QuickResponsesPanel-dk-Rj8xx.css +1 -0
- package/packages/web/dist/assets/ResizableTextarea-BWywIqOv.js +1 -0
- package/packages/web/dist/assets/ResizableTextarea-DERSH3Wz.css +1 -0
- package/packages/web/dist/assets/SessionCard-B6d5ijDW.js +1 -0
- package/packages/web/dist/assets/SessionDetailView-DWbXdx7A.js +36 -0
- package/packages/web/dist/assets/SessionDetailView-ULeIkWS0.css +1 -0
- package/packages/web/dist/assets/{SessionFormOptions-B6AxyREh.js → SessionFormOptions-Dz9ik4Fo.js} +1 -1
- package/packages/web/dist/assets/{SessionListView-B5_6gW49.css → SessionListView-3-xx6EVs.css} +1 -1
- package/packages/web/dist/assets/SessionListView-C129buBe.js +1 -0
- package/packages/web/dist/assets/{SessionLogStream-LlZ3z_Xj.js → SessionLogStream-BvXUNNBZ.js} +6 -6
- package/packages/web/dist/assets/{SettingsView-CTGiGvR2.js → SettingsView-DW1NvpX_.js} +1 -1
- package/packages/web/dist/assets/SlashCommandWizard-DleYBxrE.js +1 -0
- package/packages/web/dist/assets/{SummarySettingsView-BR2ZjEa3.js → SummarySettingsView-CLUfcWvf.js} +1 -1
- package/packages/web/dist/assets/TemplateDetailView-B5NI2oTR.css +1 -0
- package/packages/web/dist/assets/TemplateDetailView-Cukb205e.js +1 -0
- package/packages/web/dist/assets/{commandButtons-BfqR-fqq.js → commandButtons-DejH0rVN.js} +1 -1
- package/packages/web/dist/assets/index-BD7Y3rBE.js +3 -0
- package/packages/web/dist/assets/{index-BY174HVJ.css → index-Bd20AzX1.css} +1 -1
- package/packages/web/dist/assets/index-BgJiarKe.js +1 -0
- package/packages/web/dist/assets/index-Bk32fSSG.js +1 -0
- package/packages/web/dist/assets/index-BkA6pF2Z.js +1 -0
- package/packages/web/dist/assets/index-Cltr-Ldt.js +7 -0
- package/packages/web/dist/assets/index-Co-46Tp3.js +1 -0
- package/packages/web/dist/assets/index-Cpykk857.js +1 -0
- package/packages/web/dist/assets/index-CtABl0D1.js +1 -0
- package/packages/web/dist/assets/index-Cuqk5m9S.js +1 -0
- package/packages/web/dist/assets/{index-fK8FIZgP.js → index-CvXApbVC.js} +15 -15
- package/packages/web/dist/assets/index-D2gN-xEH.js +1 -0
- package/packages/web/dist/assets/index-Dd3WpmyQ.js +1 -0
- package/packages/web/dist/assets/index-Dk6--9rj.js +1 -0
- package/packages/web/dist/assets/{index-DgkC10TW.js → index-MZf7MlPX.js} +3 -3
- package/packages/web/dist/assets/{index-DtfUt785.js → index-NShCcwfj.js} +1 -1
- package/packages/web/dist/assets/index-hA3VEuSq.js +1 -0
- package/packages/web/dist/assets/index-p0mp3nca.js +1 -0
- package/packages/web/dist/assets/index-qntNa5r_.js +1 -0
- package/packages/web/dist/assets/index-qq9ceNSK.js +1 -0
- package/packages/web/dist/assets/projectDefaults-D9xkp2XR.js +1 -0
- package/packages/web/dist/assets/{projects-DXYQNJIi.js → projects-BvLADGKx.js} +1 -1
- package/packages/web/dist/assets/{providers-1bnH-exJ.js → providers-DZ-fOa4G.js} +1 -1
- package/packages/web/dist/assets/{sessions-6zGUlFrt.js → sessions-DETEyjPI.js} +1 -1
- package/packages/web/dist/assets/{settings-MbfRir0d.js → settings-TWfbahn5.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +0 -1
- package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +0 -1
- package/packages/web/dist/assets/CommandButtonDetailView-CdSCPp78.js +0 -1
- package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +0 -1
- package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +0 -1
- package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +0 -2
- package/packages/web/dist/assets/ModelSelector-CwTz8ZWO.js +0 -1
- package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +0 -3
- package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +0 -1
- package/packages/web/dist/assets/ProjectEditView-J15mcsWz.css +0 -1
- package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +0 -1
- package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +0 -1
- package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +0 -1
- package/packages/web/dist/assets/QuickResponseSettings-BQwQXuL7.js +0 -1
- package/packages/web/dist/assets/QuickResponsesPanel-BlFDvnZ2.css +0 -1
- package/packages/web/dist/assets/QuickResponsesPanel-BzSYcCSP.js +0 -1
- package/packages/web/dist/assets/ResizableTextarea-B3YIdIXv.js +0 -1
- package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +0 -1
- package/packages/web/dist/assets/SessionCard-CjE1tXiT.js +0 -1
- package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +0 -36
- package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +0 -1
- package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +0 -1
- package/packages/web/dist/assets/SlashCommandWizard-Cy04d7-o.js +0 -1
- package/packages/web/dist/assets/TemplateDetailView-DH6Oswsp.js +0 -1
- package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +0 -1
- package/packages/web/dist/assets/index-1zziPL6l.js +0 -1
- package/packages/web/dist/assets/index-7kzHPxSF.js +0 -1
- package/packages/web/dist/assets/index-B0N_obMc.js +0 -1
- package/packages/web/dist/assets/index-BNk_gdfI.js +0 -1
- package/packages/web/dist/assets/index-CSqaAH-0.js +0 -1
- package/packages/web/dist/assets/index-C_q4WlK8.js +0 -1
- package/packages/web/dist/assets/index-D1wpU4y0.js +0 -7
- package/packages/web/dist/assets/index-D5zCA8sD.js +0 -1
- package/packages/web/dist/assets/index-DGR8ELWY.js +0 -1
- package/packages/web/dist/assets/index-DHga8pXo.js +0 -1
- package/packages/web/dist/assets/index-DSby02Wl.js +0 -1
- package/packages/web/dist/assets/index-DqjXJTVI.js +0 -1
- package/packages/web/dist/assets/index-_4S2uLDI.js +0 -1
- package/packages/web/dist/assets/index-gmiZeFXN.js +0 -1
- package/packages/web/dist/assets/index-irD539ZM.js +0 -3
- package/packages/web/dist/assets/index-yq-E1Y00.js +0 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { git } from './gitService.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get diff for a directory
|
|
5
|
+
* @param {string} directory
|
|
6
|
+
* @returns {Promise<string>}
|
|
7
|
+
*/
|
|
8
|
+
export async function getDiff(directory) {
|
|
9
|
+
return git(directory, 'diff');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get staged diff for a directory
|
|
14
|
+
* @param {string} directory
|
|
15
|
+
* @returns {Promise<string>}
|
|
16
|
+
*/
|
|
17
|
+
export async function getStagedDiff(directory) {
|
|
18
|
+
return git(directory, 'diff --cached');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get list of untracked files
|
|
23
|
+
* @param {string} directory
|
|
24
|
+
* @returns {Promise<string[]>}
|
|
25
|
+
*/
|
|
26
|
+
export async function getUntrackedFiles(directory) {
|
|
27
|
+
try {
|
|
28
|
+
const output = await git(directory, 'ls-files --others --exclude-standard');
|
|
29
|
+
if (!output) return [];
|
|
30
|
+
return output.split('\n').filter((line) => line.trim());
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.warn(`Failed to get untracked files for ${directory}:`, error.message);
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get diff for a directory compared to a specific branch
|
|
39
|
+
* @param {string} directory
|
|
40
|
+
* @param {string} branch - Branch to compare against (e.g., 'origin/main')
|
|
41
|
+
* @returns {Promise<string>}
|
|
42
|
+
*/
|
|
43
|
+
export async function getDiffAgainstBranch(directory, branch) {
|
|
44
|
+
return git(directory, `diff ${branch}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get staged diff for a directory compared to a specific branch
|
|
49
|
+
* @param {string} directory
|
|
50
|
+
* @param {string} branch - Branch to compare against (e.g., 'origin/main')
|
|
51
|
+
* @returns {Promise<string>}
|
|
52
|
+
*/
|
|
53
|
+
export async function getStagedDiffAgainstBranch(directory, branch) {
|
|
54
|
+
return git(directory, `diff --cached ${branch}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get diff between two git refs (e.g., comparing HEAD to origin/main)
|
|
59
|
+
* This shows the committed changes between two refs, ignoring working tree state
|
|
60
|
+
* @param {string} directory
|
|
61
|
+
* @param {string} fromRef - Base ref (e.g., 'origin/main')
|
|
62
|
+
* @param {string} toRef - Target ref (e.g., 'HEAD')
|
|
63
|
+
* @returns {Promise<string>}
|
|
64
|
+
*/
|
|
65
|
+
export async function getDiffBetweenRefs(directory, fromRef, toRef) {
|
|
66
|
+
return git(directory, `diff ${fromRef} ${toRef}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function addGitPathOutput(files, output) {
|
|
70
|
+
if (!output) return;
|
|
71
|
+
|
|
72
|
+
output.split('\n').forEach((file) => {
|
|
73
|
+
const trimmed = file.trim();
|
|
74
|
+
if (trimmed) files.add(trimmed);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get count of files modified/added compared to a branch
|
|
80
|
+
* Includes committed changes + staged + unstaged + untracked files
|
|
81
|
+
* @param {string} directory - The git repository directory
|
|
82
|
+
* @param {string} branch - Branch to compare against (e.g., 'origin/main')
|
|
83
|
+
* @returns {Promise<number>} - Total count of unique files modified/added
|
|
84
|
+
*/
|
|
85
|
+
export async function getModifiedFilesCount(directory, branch) {
|
|
86
|
+
try {
|
|
87
|
+
const committed = await git(
|
|
88
|
+
directory,
|
|
89
|
+
`diff --name-only ${branch}...HEAD`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const staged = await git(directory, 'diff --cached --name-only');
|
|
93
|
+
const unstaged = await git(directory, 'diff --name-only');
|
|
94
|
+
const untracked = await getUntrackedFiles(directory);
|
|
95
|
+
|
|
96
|
+
const allFiles = new Set();
|
|
97
|
+
addGitPathOutput(allFiles, committed);
|
|
98
|
+
addGitPathOutput(allFiles, staged);
|
|
99
|
+
addGitPathOutput(allFiles, unstaged);
|
|
100
|
+
untracked.forEach((file) => allFiles.add(file));
|
|
101
|
+
|
|
102
|
+
return allFiles.size;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.warn(`Failed to get modified files count for ${directory}:`, error.message);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { realpath, access } from 'fs/promises';
|
|
3
|
+
import { isGitRepo, getWorktrees, git } from './gitService.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a git remote URL into a clean HTTPS browser URL.
|
|
7
|
+
*
|
|
8
|
+
* Supported forms:
|
|
9
|
+
* - https://github.com/owner/repo.git -> https://github.com/owner/repo
|
|
10
|
+
* - https://github.com/owner/repo -> https://github.com/owner/repo (already clean)
|
|
11
|
+
* - git@github.com:owner/repo.git -> https://github.com/owner/repo
|
|
12
|
+
* - ssh://git@github.com/owner/repo.git -> https://github.com/owner/repo
|
|
13
|
+
* - git@gitlab.com:owner/repo.git -> https://gitlab.com/owner/repo
|
|
14
|
+
* - https://gitlab.com/owner/repo.git -> https://gitlab.com/owner/repo
|
|
15
|
+
* - https://bitbucket.org/owner/repo.git -> https://bitbucket.org/owner/repo
|
|
16
|
+
* - http://git.example.com/owner/repo.git -> http://git.example.com/owner/repo
|
|
17
|
+
* - git://github.com/owner/repo.git -> https://github.com/owner/repo
|
|
18
|
+
*
|
|
19
|
+
* Query strings and fragments are stripped before matching.
|
|
20
|
+
*
|
|
21
|
+
* Returns null for empty, null, undefined, or unrecognizable inputs.
|
|
22
|
+
*
|
|
23
|
+
* @param {string|null|undefined} remoteUrl
|
|
24
|
+
* @returns {string|null}
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeGitRemoteUrl(remoteUrl) {
|
|
27
|
+
if (!remoteUrl || typeof remoteUrl !== 'string') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const trimmed = remoteUrl.trim();
|
|
32
|
+
if (!trimmed) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Strip query strings and fragments before matching
|
|
37
|
+
// (defensive: malformed remote configs shouldn't silently fail)
|
|
38
|
+
const cleaned = trimmed.replace(/[?#].*$/, '');
|
|
39
|
+
|
|
40
|
+
// SSH form: git@host:owner/repo.git or git@host:owner/repo
|
|
41
|
+
const sshMatch = cleaned.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
|
42
|
+
if (sshMatch) {
|
|
43
|
+
return `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// SSH protocol form: ssh://git@host/owner/repo.git
|
|
47
|
+
const sshProtocolMatch = cleaned.match(/^ssh:\/\/git@([^/]+)\/(.+?)(?:\.git)?$/);
|
|
48
|
+
if (sshProtocolMatch) {
|
|
49
|
+
return `https://${sshProtocolMatch[1]}/${sshProtocolMatch[2]}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// HTTPS form: https://host/owner/repo.git or https://host/owner/repo
|
|
53
|
+
const httpsMatch = cleaned.match(/^https:\/\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
54
|
+
if (httpsMatch) {
|
|
55
|
+
return `https://${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// HTTP form: http://host/owner/repo.git or http://host/owner/repo
|
|
59
|
+
// Preserve the http:// scheme (unlike SSH->HTTPS conversion, HTTP remotes
|
|
60
|
+
// are intentional and the server may not support HTTPS).
|
|
61
|
+
const httpMatch = cleaned.match(/^http:\/\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
62
|
+
if (httpMatch) {
|
|
63
|
+
return `http://${httpMatch[1]}/${httpMatch[2]}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// git:// protocol form: git://host/owner/repo.git or git://host/owner/repo
|
|
67
|
+
// Convert to HTTPS (same output as SSH).
|
|
68
|
+
const gitProtocolMatch = cleaned.match(/^git:\/\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
69
|
+
if (gitProtocolMatch) {
|
|
70
|
+
return `https://${gitProtocolMatch[1]}/${gitProtocolMatch[2]}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Unrecognized format
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse the first remote URL from `git remote -v` output.
|
|
79
|
+
* @param {string} remoteVerbose - Raw output of `git remote -v`
|
|
80
|
+
* @returns {string|null} The extracted URL, or null if not parseable
|
|
81
|
+
*/
|
|
82
|
+
function parseFirstRemoteUrl(remoteVerbose) {
|
|
83
|
+
const firstLine = remoteVerbose.split('\n').find((r) => r.trim());
|
|
84
|
+
if (!firstLine) return null;
|
|
85
|
+
|
|
86
|
+
// git remote -v outputs: "remote_name\turl (fetch)"
|
|
87
|
+
const parts = firstLine.split('\t');
|
|
88
|
+
if (parts.length < 2) return null;
|
|
89
|
+
|
|
90
|
+
return parts[1].replace(/ \((?:fetch|push)\)$/, '');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Auto-detect the repository URL from a directory's git remotes.
|
|
95
|
+
*
|
|
96
|
+
* Prefers the "origin" remote. Falls back to the first configured remote.
|
|
97
|
+
* Normalizes SSH and HTTPS URLs into clean HTTPS browser URLs.
|
|
98
|
+
* Returns null if the directory is not a git repo or has no usable remotes.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} directory
|
|
101
|
+
* @returns {Promise<string|null>}
|
|
102
|
+
*/
|
|
103
|
+
export async function getRepositoryUrl(directory) {
|
|
104
|
+
try {
|
|
105
|
+
// Fast-path: skip entirely if the directory is not a git repo.
|
|
106
|
+
// Uses a filesystem check (.git existence) instead of spawning a git process,
|
|
107
|
+
// which avoids unnecessary child_process overhead for non-git directories.
|
|
108
|
+
try {
|
|
109
|
+
await access(path.join(directory, '.git'));
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Try origin first (with timeout to prevent indefinite blocking under load)
|
|
115
|
+
let rawUrl;
|
|
116
|
+
try {
|
|
117
|
+
rawUrl = await git(directory, 'config --get remote.origin.url', { timeout: 5000 });
|
|
118
|
+
} catch {
|
|
119
|
+
// No origin remote, try listing all remotes
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Fall back to first remote if origin doesn't exist
|
|
123
|
+
if (!rawUrl) {
|
|
124
|
+
try {
|
|
125
|
+
const remoteVerbose = await git(directory, 'remote -v', { timeout: 5000 });
|
|
126
|
+
rawUrl = parseFirstRemoteUrl(remoteVerbose);
|
|
127
|
+
} catch {
|
|
128
|
+
// No remotes at all
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!rawUrl) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return normalizeGitRemoteUrl(rawUrl);
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Detect the worktree path for a directory by inspecting existing worktrees.
|
|
144
|
+
* If external worktrees exist, uses the parent directory of the first one.
|
|
145
|
+
* Otherwise, falls back to {directory}/.worktrees.
|
|
146
|
+
* @param {string} directory - The git repository directory
|
|
147
|
+
* @returns {Promise<{worktreePath: string, source: 'detected' | 'default'}>}
|
|
148
|
+
*/
|
|
149
|
+
export async function detectWorktreePath(directory) {
|
|
150
|
+
const isRepo = await isGitRepo(directory);
|
|
151
|
+
if (!isRepo) {
|
|
152
|
+
return { worktreePath: path.join(directory, '.worktrees'), source: 'default' };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Resolve symlinks for consistent path comparison (e.g., /var -> /private/var on macOS)
|
|
156
|
+
let resolvedDir;
|
|
157
|
+
try {
|
|
158
|
+
resolvedDir = await realpath(directory);
|
|
159
|
+
} catch {
|
|
160
|
+
resolvedDir = path.resolve(directory);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const worktrees = await getWorktrees(directory);
|
|
164
|
+
// Filter out the main worktree (its path === directory or resolves to it)
|
|
165
|
+
const externalWorktrees = worktrees.filter(wt => path.resolve(wt.path) !== resolvedDir);
|
|
166
|
+
|
|
167
|
+
if (externalWorktrees.length > 0) {
|
|
168
|
+
// Use the parent directory of the first external worktree
|
|
169
|
+
const parentDir = path.dirname(path.resolve(externalWorktrees[0].path));
|
|
170
|
+
return { worktreePath: parentDir, source: 'detected' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { worktreePath: path.join(resolvedDir, '.worktrees'), source: 'default' };
|
|
174
|
+
}
|
|
@@ -1,26 +1,43 @@
|
|
|
1
1
|
import { exec } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { realpath } from 'fs/promises';
|
|
5
3
|
export {
|
|
4
|
+
_setManagedHooksPath,
|
|
6
5
|
clearWorktreeCommitAttribution,
|
|
7
6
|
configureWorktreeCommitAttribution,
|
|
8
7
|
ensureWorktreeCommitAttributionHook,
|
|
8
|
+
getManagedHooksPath,
|
|
9
9
|
} from './gitCommitAttribution.js';
|
|
10
|
+
export {
|
|
11
|
+
normalizeGitRemoteUrl,
|
|
12
|
+
getRepositoryUrl,
|
|
13
|
+
detectWorktreePath,
|
|
14
|
+
} from './gitRepoUrl.js';
|
|
15
|
+
export {
|
|
16
|
+
getDiff,
|
|
17
|
+
getStagedDiff,
|
|
18
|
+
getUntrackedFiles,
|
|
19
|
+
getDiffAgainstBranch,
|
|
20
|
+
getStagedDiffAgainstBranch,
|
|
21
|
+
getDiffBetweenRefs,
|
|
22
|
+
getModifiedFilesCount,
|
|
23
|
+
} from './gitDiff.js';
|
|
24
|
+
export {
|
|
25
|
+
branchExists,
|
|
26
|
+
checkoutBranch,
|
|
27
|
+
createWorktree,
|
|
28
|
+
removeWorktree,
|
|
29
|
+
createWorktreeForBranch,
|
|
30
|
+
} from './gitWorktree.js';
|
|
10
31
|
|
|
11
32
|
const execAsync = promisify(exec);
|
|
33
|
+
export const DEFAULT_GIT_MAX_BUFFER = 100 * 1024 * 1024;
|
|
12
34
|
|
|
13
|
-
// Cache for default branch detection
|
|
14
|
-
// Key: directory path, Value: { branch: string, timestamp: number }
|
|
35
|
+
// Cache for default branch detection: directory -> { branch, timestamp }
|
|
15
36
|
const defaultBranchCache = new Map();
|
|
16
37
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
17
38
|
const MAX_CACHE_SIZE = 100; // Maximum number of repositories to cache
|
|
18
39
|
|
|
19
|
-
|
|
20
|
-
// Can be overridden via setLogger() for custom logging behavior
|
|
21
|
-
let logger = {
|
|
22
|
-
warn: (...args) => console.warn(...args),
|
|
23
|
-
};
|
|
40
|
+
import { _setWorktreeLogger } from './gitWorktree.js';
|
|
24
41
|
|
|
25
42
|
/**
|
|
26
43
|
* Set a custom logger for git service warnings.
|
|
@@ -29,19 +46,15 @@ let logger = {
|
|
|
29
46
|
* @param {Function} customLogger.warn - Function to handle warning messages
|
|
30
47
|
*/
|
|
31
48
|
export function setLogger(customLogger) {
|
|
32
|
-
|
|
49
|
+
_setWorktreeLogger(customLogger);
|
|
33
50
|
}
|
|
34
51
|
|
|
35
|
-
/**
|
|
36
|
-
* Evict oldest entries from cache if it exceeds MAX_CACHE_SIZE.
|
|
37
|
-
* Uses LRU-like eviction based on timestamp.
|
|
38
|
-
*/
|
|
52
|
+
/** Evict oldest cache entries if size exceeds MAX_CACHE_SIZE (LRU-like). */
|
|
39
53
|
function evictOldestCacheEntries() {
|
|
40
54
|
if (defaultBranchCache.size <= MAX_CACHE_SIZE) {
|
|
41
55
|
return;
|
|
42
56
|
}
|
|
43
57
|
|
|
44
|
-
// Sort entries by timestamp and remove oldest ones
|
|
45
58
|
const entries = [...defaultBranchCache.entries()];
|
|
46
59
|
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
47
60
|
|
|
@@ -51,32 +64,23 @@ function evictOldestCacheEntries() {
|
|
|
51
64
|
}
|
|
52
65
|
}
|
|
53
66
|
|
|
54
|
-
/**
|
|
55
|
-
* Safely fetch from origin remote.
|
|
56
|
-
* Logs a warning if fetch fails but does not throw.
|
|
57
|
-
* @param {string} directory - The git repository directory
|
|
58
|
-
* @returns {Promise<boolean>} - True if fetch succeeded, false otherwise
|
|
59
|
-
*/
|
|
60
|
-
async function safeFetchOrigin(directory) {
|
|
61
|
-
try {
|
|
62
|
-
await git(directory, 'fetch origin');
|
|
63
|
-
return true;
|
|
64
|
-
} catch (err) {
|
|
65
|
-
// No origin or network unavailable, proceed without fetch
|
|
66
|
-
logger.warn('Could not fetch from origin, proceeding with local refs:', err.message);
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
67
|
/**
|
|
72
68
|
* Execute a git command in a directory
|
|
73
69
|
* @param {string} directory
|
|
74
70
|
* @param {string} command
|
|
71
|
+
* @param {Object} [opts]
|
|
72
|
+
* @param {Object} [opts.env]
|
|
73
|
+
* @param {number} [opts.timeout]
|
|
74
|
+
* @param {number} [opts.maxBuffer]
|
|
75
75
|
* @returns {Promise<string>}
|
|
76
76
|
*/
|
|
77
|
-
async function git(directory, command, opts = {}) {
|
|
78
|
-
const execOpts = {
|
|
77
|
+
export async function git(directory, command, opts = {}) {
|
|
78
|
+
const execOpts = {
|
|
79
|
+
cwd: directory,
|
|
80
|
+
maxBuffer: opts.maxBuffer ?? DEFAULT_GIT_MAX_BUFFER,
|
|
81
|
+
};
|
|
79
82
|
if (opts.env) execOpts.env = opts.env;
|
|
83
|
+
if (opts.timeout) execOpts.timeout = opts.timeout;
|
|
80
84
|
const { stdout } = await execAsync(`git ${command}`, execOpts);
|
|
81
85
|
return stdout.trim();
|
|
82
86
|
}
|
|
@@ -246,242 +250,9 @@ export async function getCurrentBranch(directory) {
|
|
|
246
250
|
}
|
|
247
251
|
}
|
|
248
252
|
|
|
249
|
-
/**
|
|
250
|
-
* Create a new worktree
|
|
251
|
-
* @param {string} directory
|
|
252
|
-
* @param {string} branch
|
|
253
|
-
* @param {string} worktreePath
|
|
254
|
-
* @param {Object} options
|
|
255
|
-
* @param {boolean} options.skipFetch - Skip fetching from origin (default: false)
|
|
256
|
-
* @returns {Promise<{path: string, branch: string}>}
|
|
257
|
-
*/
|
|
258
|
-
export async function createWorktree(directory, branch, worktreePath, options = {}) {
|
|
259
|
-
const { skipFetch = false } = options;
|
|
260
|
-
|
|
261
|
-
// Fetch latest from origin to ensure we have up-to-date default branch
|
|
262
|
-
if (!skipFetch) {
|
|
263
|
-
await safeFetchOrigin(directory);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Get the default branch from origin (main or master)
|
|
267
|
-
const defaultBranch = await getOriginDefaultBranch(directory);
|
|
268
|
-
// Base new branch on origin's default branch to avoid including unrelated commits from HEAD
|
|
269
|
-
// Use --no-track to prevent the new branch from tracking the start-point (main/master)
|
|
270
|
-
await git(directory, `worktree add --no-track "${worktreePath}" -b "${branch}" ${defaultBranch}`);
|
|
271
|
-
return { path: worktreePath, branch };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Remove a worktree
|
|
276
|
-
* @param {string} directory
|
|
277
|
-
* @param {string} path
|
|
278
|
-
* @param {boolean} force - Force removal even if worktree has uncommitted changes
|
|
279
|
-
*/
|
|
280
|
-
export async function removeWorktree(directory, worktreePath, force = false) {
|
|
281
|
-
const forceFlag = force ? '--force' : '';
|
|
282
|
-
await git(directory, `worktree remove ${forceFlag} "${worktreePath}"`);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Get diff for a directory
|
|
287
|
-
* @param {string} directory
|
|
288
|
-
* @returns {Promise<string>}
|
|
289
|
-
*/
|
|
290
|
-
export async function getDiff(directory) {
|
|
291
|
-
try {
|
|
292
|
-
return await git(directory, 'diff');
|
|
293
|
-
} catch {
|
|
294
|
-
return '';
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Get staged diff for a directory
|
|
300
|
-
* @param {string} directory
|
|
301
|
-
* @returns {Promise<string>}
|
|
302
|
-
*/
|
|
303
|
-
export async function getStagedDiff(directory) {
|
|
304
|
-
try {
|
|
305
|
-
return await git(directory, 'diff --cached');
|
|
306
|
-
} catch {
|
|
307
|
-
return '';
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Check if a branch exists
|
|
313
|
-
* @param {string} directory
|
|
314
|
-
* @param {string} branch
|
|
315
|
-
* @returns {Promise<boolean>}
|
|
316
|
-
*/
|
|
317
|
-
export async function branchExists(directory, branch) {
|
|
318
|
-
try {
|
|
319
|
-
await git(directory, `rev-parse --verify refs/heads/${branch}`);
|
|
320
|
-
return true;
|
|
321
|
-
} catch {
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Checkout a branch, creating it if it doesn't exist
|
|
328
|
-
* @param {string} directory
|
|
329
|
-
* @param {string} branch
|
|
330
|
-
* @returns {Promise<void>}
|
|
331
|
-
*/
|
|
332
|
-
export async function checkoutBranch(directory, branch) {
|
|
333
|
-
const exists = await branchExists(directory, branch);
|
|
334
|
-
if (exists) {
|
|
335
|
-
await git(directory, `checkout "${branch}"`);
|
|
336
|
-
} else {
|
|
337
|
-
await git(directory, `checkout -b "${branch}"`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Create a worktree for a branch (creates branch if it doesn't exist)
|
|
343
|
-
* @param {string} directory - Main repo directory
|
|
344
|
-
* @param {string} branch - Branch name
|
|
345
|
-
* @param {string} worktreePath - Path for the new worktree
|
|
346
|
-
* @param {Object} options
|
|
347
|
-
* @param {boolean} options.skipFetch - Skip fetching from origin (default: false)
|
|
348
|
-
* @returns {Promise<{path: string, branch: string}>}
|
|
349
|
-
*/
|
|
350
|
-
export async function createWorktreeForBranch(directory, branch, worktreePath, options = {}) {
|
|
351
|
-
const { skipFetch = false } = options;
|
|
352
|
-
|
|
353
|
-
// Fetch latest from origin to ensure we have up-to-date default branch
|
|
354
|
-
if (!skipFetch) {
|
|
355
|
-
await safeFetchOrigin(directory);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const exists = await branchExists(directory, branch);
|
|
359
|
-
if (exists) {
|
|
360
|
-
await git(directory, `worktree add "${worktreePath}" "${branch}"`);
|
|
361
|
-
} else {
|
|
362
|
-
// Get the default branch from origin (main or master)
|
|
363
|
-
const defaultBranch = await getOriginDefaultBranch(directory);
|
|
364
|
-
// Base new branch on origin's default branch to avoid including unrelated commits from HEAD
|
|
365
|
-
// Use --no-track to prevent the new branch from tracking the start-point (main/master)
|
|
366
|
-
await git(directory, `worktree add --no-track -b "${branch}" "${worktreePath}" ${defaultBranch}`);
|
|
367
|
-
}
|
|
368
|
-
return { path: worktreePath, branch };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Get list of untracked files
|
|
373
|
-
* @param {string} directory
|
|
374
|
-
* @returns {Promise<string[]>}
|
|
375
|
-
*/
|
|
376
|
-
export async function getUntrackedFiles(directory) {
|
|
377
|
-
try {
|
|
378
|
-
const output = await git(directory, 'ls-files --others --exclude-standard');
|
|
379
|
-
if (!output) return [];
|
|
380
|
-
return output.split('\n').filter((line) => line.trim());
|
|
381
|
-
} catch {
|
|
382
|
-
return [];
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Get diff for a directory compared to a specific branch
|
|
388
|
-
* @param {string} directory
|
|
389
|
-
* @param {string} branch - Branch to compare against (e.g., 'origin/main')
|
|
390
|
-
* @returns {Promise<string>}
|
|
391
|
-
*/
|
|
392
|
-
export async function getDiffAgainstBranch(directory, branch) {
|
|
393
|
-
try {
|
|
394
|
-
return await git(directory, `diff ${branch}`);
|
|
395
|
-
} catch {
|
|
396
|
-
return '';
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Get staged diff for a directory compared to a specific branch
|
|
402
|
-
* @param {string} directory
|
|
403
|
-
* @param {string} branch - Branch to compare against (e.g., 'origin/main')
|
|
404
|
-
* @returns {Promise<string>}
|
|
405
|
-
*/
|
|
406
|
-
export async function getStagedDiffAgainstBranch(directory, branch) {
|
|
407
|
-
try {
|
|
408
|
-
return await git(directory, `diff --cached ${branch}`);
|
|
409
|
-
} catch {
|
|
410
|
-
return '';
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Get diff between two git refs (e.g., comparing HEAD to origin/main)
|
|
416
|
-
* This shows the committed changes between two refs, ignoring working tree state
|
|
417
|
-
* @param {string} directory
|
|
418
|
-
* @param {string} fromRef - Base ref (e.g., 'origin/main')
|
|
419
|
-
* @param {string} toRef - Target ref (e.g., 'HEAD')
|
|
420
|
-
* @returns {Promise<string>}
|
|
421
|
-
*/
|
|
422
|
-
export async function getDiffBetweenRefs(directory, fromRef, toRef) {
|
|
423
|
-
try {
|
|
424
|
-
return await git(directory, `diff ${fromRef} ${toRef}`);
|
|
425
|
-
} catch {
|
|
426
|
-
return '';
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Get count of files modified/added compared to a branch
|
|
432
|
-
* Includes committed changes + staged + unstaged + untracked files
|
|
433
|
-
* @param {string} directory - The git repository directory
|
|
434
|
-
* @param {string} branch - Branch to compare against (e.g., 'origin/main')
|
|
435
|
-
* @returns {Promise<number>} - Total count of unique files modified/added
|
|
436
|
-
*/
|
|
437
|
-
export async function getModifiedFilesCount(directory, branch) {
|
|
438
|
-
try {
|
|
439
|
-
// Get all modified files in one command using --name-only
|
|
440
|
-
// This includes: committed changes vs branch + staged
|
|
441
|
-
const committedAndStaged = await git(
|
|
442
|
-
directory,
|
|
443
|
-
`diff --name-only ${branch}...HEAD`
|
|
444
|
-
);
|
|
445
|
-
|
|
446
|
-
// Get unstaged changes (working tree vs index)
|
|
447
|
-
const unstaged = await git(directory, 'diff --name-only');
|
|
448
|
-
|
|
449
|
-
// Get untracked files
|
|
450
|
-
const untracked = await getUntrackedFiles(directory);
|
|
451
|
-
|
|
452
|
-
// Combine all files into a Set to get unique count
|
|
453
|
-
const allFiles = new Set();
|
|
454
|
-
|
|
455
|
-
// Parse committed+staged files
|
|
456
|
-
if (committedAndStaged) {
|
|
457
|
-
committedAndStaged.split('\n').forEach(f => {
|
|
458
|
-
if (f.trim()) allFiles.add(f.trim());
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Parse unstaged files
|
|
463
|
-
if (unstaged) {
|
|
464
|
-
unstaged.split('\n').forEach(f => {
|
|
465
|
-
if (f.trim()) allFiles.add(f.trim());
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Add untracked files
|
|
470
|
-
untracked.forEach(f => allFiles.add(f));
|
|
471
|
-
|
|
472
|
-
return allFiles.size;
|
|
473
|
-
} catch (error) {
|
|
474
|
-
logger.warn(`Failed to get modified files count for ${directory}:`, error.message);
|
|
475
|
-
return 0;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
253
|
/**
|
|
480
254
|
* Get the git author info from the global config (~/.gitconfig).
|
|
481
|
-
*
|
|
482
|
-
* Uses `--global` so that a contaminated local config (e.g. one that
|
|
483
|
-
* already has Claude Code's identity) is bypassed.
|
|
484
|
-
*
|
|
255
|
+
* Uses `--global` so that a contaminated local config is bypassed.
|
|
485
256
|
* @param {string} directory
|
|
486
257
|
* @param {Object} [options]
|
|
487
258
|
* @param {Object} [options.env] - Custom environment variables (useful for tests)
|
|
@@ -502,17 +273,12 @@ export async function getGitAuthor(directory, { env } = {}) {
|
|
|
502
273
|
|
|
503
274
|
/**
|
|
504
275
|
* Pin the human developer's git identity in a worktree's config.
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* them into the worktree-specific config (--worktree). This ensures the
|
|
508
|
-
* human is always the commit Author, even if the session's environment
|
|
509
|
-
* tries to override it. Claude Code already adds its own Co-Authored-By
|
|
510
|
-
* trailer via its system prompt, so no hook is needed.
|
|
511
|
-
*
|
|
512
|
-
* Only call this for worktree directories, not the main repo.
|
|
513
|
-
*
|
|
276
|
+
* Reads user.name/user.email from the main project directory and writes them
|
|
277
|
+
* into the worktree-specific config (--worktree). Only call for worktree dirs.
|
|
514
278
|
* @param {string} worktreePath - The worktree directory
|
|
515
279
|
* @param {string} projectDir - The main project directory (to read author from)
|
|
280
|
+
* @param {Object} [options]
|
|
281
|
+
* @param {Object} [options.env] - Custom environment variables (useful for tests)
|
|
516
282
|
* @returns {Promise<boolean>} - True if author was pinned
|
|
517
283
|
*/
|
|
518
284
|
export async function pinAuthorInWorktree(worktreePath, projectDir, { env } = {}) {
|
|
@@ -529,37 +295,3 @@ export async function pinAuthorInWorktree(worktreePath, projectDir, { env } = {}
|
|
|
529
295
|
|
|
530
296
|
return true;
|
|
531
297
|
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Detect the worktree path for a directory by inspecting existing worktrees.
|
|
535
|
-
* If external worktrees exist, uses the parent directory of the first one.
|
|
536
|
-
* Otherwise, falls back to {directory}/.worktrees.
|
|
537
|
-
* @param {string} directory - The git repository directory
|
|
538
|
-
* @returns {Promise<{worktreePath: string, source: 'detected' | 'default'}>}
|
|
539
|
-
*/
|
|
540
|
-
export async function detectWorktreePath(directory) {
|
|
541
|
-
const isRepo = await isGitRepo(directory);
|
|
542
|
-
if (!isRepo) {
|
|
543
|
-
return { worktreePath: path.join(directory, '.worktrees'), source: 'default' };
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Resolve symlinks for consistent path comparison (e.g., /var -> /private/var on macOS)
|
|
547
|
-
let resolvedDir;
|
|
548
|
-
try {
|
|
549
|
-
resolvedDir = await realpath(directory);
|
|
550
|
-
} catch {
|
|
551
|
-
resolvedDir = path.resolve(directory);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const worktrees = await getWorktrees(directory);
|
|
555
|
-
// Filter out the main worktree (its path === directory or resolves to it)
|
|
556
|
-
const externalWorktrees = worktrees.filter(wt => path.resolve(wt.path) !== resolvedDir);
|
|
557
|
-
|
|
558
|
-
if (externalWorktrees.length > 0) {
|
|
559
|
-
// Use the parent directory of the first external worktree
|
|
560
|
-
const parentDir = path.dirname(path.resolve(externalWorktrees[0].path));
|
|
561
|
-
return { worktreePath: parentDir, source: 'detected' };
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
return { worktreePath: path.join(resolvedDir, '.worktrees'), source: 'default' };
|
|
565
|
-
}
|