circuschief 0.8.0 → 1.0.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.
Files changed (136) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/api/commandButtons.js +16 -15
  3. package/packages/server/src/api/projects-commandButtons.js +6 -6
  4. package/packages/server/src/api/projects-session-create.js +109 -0
  5. package/packages/server/src/api/projects-session-defaults.js +51 -0
  6. package/packages/server/src/api/projects-session-helpers.js +47 -1
  7. package/packages/server/src/api/projects-templates.js +38 -0
  8. package/packages/server/src/api/projects.js +28 -180
  9. package/packages/server/src/api/sessions-commands.js +21 -18
  10. package/packages/server/src/api/sessions-patch.js +41 -1
  11. package/packages/server/src/db/SessionRepository.js +1 -1
  12. package/packages/server/src/db/SessionTemplateRepository.js +23 -2
  13. package/packages/server/src/db/migrations/canvasItemsMigrations.js +109 -0
  14. package/packages/server/src/db/migrations/conversationsMigrations.js +187 -0
  15. package/packages/server/src/db/migrations/index.js +225 -6
  16. package/packages/server/src/db/migrations/kanbanMigrations.js +99 -0
  17. package/packages/server/src/db/migrations/miscMigrations.js +244 -0
  18. package/packages/server/src/db/migrations/projectsMigrations.js +130 -0
  19. package/packages/server/src/db/migrations/providerCommitAttributionMigrations.js +30 -0
  20. package/packages/server/src/db/migrations/providerMigrations.js +165 -0
  21. package/packages/server/src/db/migrations/sessionTableRecreate.js +136 -0
  22. package/packages/server/src/db/migrations/sessionsMigrations.js +300 -0
  23. package/packages/server/src/db/session-helpers.js +26 -1
  24. package/packages/server/src/schema.sql +4 -0
  25. package/packages/server/src/services/commandButtonPrompts.js +9 -7
  26. package/packages/server/src/services/gitCommitAttribution.js +38 -8
  27. package/packages/server/src/services/gitDiff.js +132 -0
  28. package/packages/server/src/services/gitRepoUrl.js +174 -0
  29. package/packages/server/src/services/gitService.js +37 -309
  30. package/packages/server/src/services/gitWorktree.js +127 -0
  31. package/packages/server/src/services/sessionPrompts.js +1 -1
  32. package/packages/shared/src/contracts/sessions.js +27 -1
  33. package/packages/shared/src/contracts/templates.js +10 -0
  34. package/packages/web/dist/assets/{ActiveSessionsView-B0XHqLmv.js → ActiveSessionsView-Cxh8mHmB.js} +1 -1
  35. package/packages/web/dist/assets/{AgentLogsView-DmsjUMlB.js → AgentLogsView-xdfI2bR6.js} +2 -2
  36. package/packages/web/dist/assets/ApiClient-DfbJwzpz.js +1 -0
  37. package/packages/web/dist/assets/ArchiveConfirmModal-DXZYdzHR.js +1 -0
  38. package/packages/web/dist/assets/CommandButtonDetailView-D8xfqLAp.js +1 -0
  39. package/packages/web/dist/assets/CommandButtonDetailView-D9zjx9ME.css +1 -0
  40. package/packages/web/dist/assets/EffortLevelSelector-D2Hdzc_8.js +1 -0
  41. package/packages/web/dist/assets/{GeneralSettingsView-D1nI8_zk.js → GeneralSettingsView-sPXkLlLy.js} +1 -1
  42. package/packages/web/dist/assets/{InputWithButton-CAkttyqx.js → InputWithButton-B-o0DgMH.js} +1 -1
  43. package/packages/web/dist/assets/{InterpolationHelp-BO1j9Z3_.js → InterpolationHelp-Dxn1li4l.js} +1 -1
  44. package/packages/web/dist/assets/MarkdownEditor-D4Kbb-9l.js +2 -0
  45. package/packages/web/dist/assets/ModelSelector-72C7MUH4.js +1 -0
  46. package/packages/web/dist/assets/{ModelSelector-BSxKUSus.css → ModelSelector-BNYKujL-.css} +1 -1
  47. package/packages/web/dist/assets/NewSessionView-BR_COfgW.js +3 -0
  48. package/packages/web/dist/assets/{NewSessionView-BDPb-1qr.css → NewSessionView-DBl7T2Xp.css} +1 -1
  49. package/packages/web/dist/assets/ProjectEditView-DbqTbA0q.css +1 -0
  50. package/packages/web/dist/assets/ProjectEditView-WImU7sNd.js +1 -0
  51. package/packages/web/dist/assets/{ProjectListView-DcNyuINs.js → ProjectListView-CYmmAcBD.js} +1 -1
  52. package/packages/web/dist/assets/{ProjectNewView-B5YV62hv.js → ProjectNewView-DEhqw3Jv.js} +1 -1
  53. package/packages/web/dist/assets/ProvidersView-XZh3jkmH.js +1 -0
  54. package/packages/web/dist/assets/QuickResponsesPanel-BqmnTd-D.js +1 -0
  55. package/packages/web/dist/assets/QuickResponsesPanel-dk-Rj8xx.css +1 -0
  56. package/packages/web/dist/assets/ResizableTextarea-BQNw5e0C.css +1 -0
  57. package/packages/web/dist/assets/ResizableTextarea-DpWdIAP6.js +1 -0
  58. package/packages/web/dist/assets/SessionCard-Bw77-KwD.js +1 -0
  59. package/packages/web/dist/assets/SessionDetailView-B59TEkr-.js +36 -0
  60. package/packages/web/dist/assets/SessionDetailView-CKVBnR4T.css +1 -0
  61. package/packages/web/dist/assets/{SessionFormOptions-B6AxyREh.js → SessionFormOptions-hqijxc0S.js} +1 -1
  62. package/packages/web/dist/assets/{SessionListView-B5_6gW49.css → SessionListView-3-xx6EVs.css} +1 -1
  63. package/packages/web/dist/assets/SessionListView-DYXHM9I-.js +1 -0
  64. package/packages/web/dist/assets/{SessionLogStream-LlZ3z_Xj.js → SessionLogStream-5NfVr9pF.js} +6 -6
  65. package/packages/web/dist/assets/{SettingsView-CTGiGvR2.js → SettingsView-DI8ncOAV.js} +1 -1
  66. package/packages/web/dist/assets/{SlashCommandWizard-Cy04d7-o.js → SlashCommandWizard-BQ_rMzn-.js} +1 -1
  67. package/packages/web/dist/assets/{SummarySettingsView-BR2ZjEa3.js → SummarySettingsView-C2Qs35mm.js} +1 -1
  68. package/packages/web/dist/assets/TemplateDetailView-B5NI2oTR.css +1 -0
  69. package/packages/web/dist/assets/TemplateDetailView-zVkIvgtu.js +1 -0
  70. package/packages/web/dist/assets/{commandButtons-BfqR-fqq.js → commandButtons-CoU3G4zK.js} +1 -1
  71. package/packages/web/dist/assets/index-9yF1uCCA.js +1 -0
  72. package/packages/web/dist/assets/index-BKstCaYU.js +1 -0
  73. package/packages/web/dist/assets/index-BhbH7eOk.js +1 -0
  74. package/packages/web/dist/assets/{index-DgkC10TW.js → index-BjuRttEY.js} +3 -3
  75. package/packages/web/dist/assets/index-Bo7PdwM5.js +1 -0
  76. package/packages/web/dist/assets/index-C2QFVD7d.js +83 -0
  77. package/packages/web/dist/assets/index-C7Ww2auW.js +1 -0
  78. package/packages/web/dist/assets/index-CAGdsDh7.js +1 -0
  79. package/packages/web/dist/assets/index-CLRsVASf.js +3 -0
  80. package/packages/web/dist/assets/{index-DtfUt785.js → index-CP-SxOlV.js} +1 -1
  81. package/packages/web/dist/assets/index-CslU0psO.js +1 -0
  82. package/packages/web/dist/assets/index-DI4NxaWD.js +1 -0
  83. package/packages/web/dist/assets/index-DOzONENy.js +1 -0
  84. package/packages/web/dist/assets/index-DUa7adFh.js +1 -0
  85. package/packages/web/dist/assets/index-DZBpETI5.js +1 -0
  86. package/packages/web/dist/assets/index-DsjWqc6R.js +7 -0
  87. package/packages/web/dist/assets/index-c99Bo3JV.js +1 -0
  88. package/packages/web/dist/assets/index-mT1JpxDc.js +1 -0
  89. package/packages/web/dist/assets/index-rkQx2tso.js +1 -0
  90. package/packages/web/dist/assets/{index-BY174HVJ.css → index-uySCcnA_.css} +1 -1
  91. package/packages/web/dist/assets/projectDefaults-B8esIcYq.js +1 -0
  92. package/packages/web/dist/assets/{projects-DXYQNJIi.js → projects-C-8PSxKi.js} +1 -1
  93. package/packages/web/dist/assets/{providers-1bnH-exJ.js → providers-oXifvvqN.js} +1 -1
  94. package/packages/web/dist/assets/{sessions-6zGUlFrt.js → sessions-Nq5VafSf.js} +1 -1
  95. package/packages/web/dist/assets/{settings-MbfRir0d.js → settings-DtpuiyT6.js} +1 -1
  96. package/packages/web/dist/index.html +2 -2
  97. package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +0 -1
  98. package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +0 -1
  99. package/packages/web/dist/assets/CommandButtonDetailView-CdSCPp78.js +0 -1
  100. package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +0 -1
  101. package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +0 -1
  102. package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +0 -2
  103. package/packages/web/dist/assets/ModelSelector-CwTz8ZWO.js +0 -1
  104. package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +0 -3
  105. package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +0 -1
  106. package/packages/web/dist/assets/ProjectEditView-J15mcsWz.css +0 -1
  107. package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +0 -1
  108. package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +0 -1
  109. package/packages/web/dist/assets/QuickResponseSettings-BQwQXuL7.js +0 -1
  110. package/packages/web/dist/assets/QuickResponsesPanel-BlFDvnZ2.css +0 -1
  111. package/packages/web/dist/assets/QuickResponsesPanel-BzSYcCSP.js +0 -1
  112. package/packages/web/dist/assets/ResizableTextarea-B3YIdIXv.js +0 -1
  113. package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +0 -1
  114. package/packages/web/dist/assets/SessionCard-CjE1tXiT.js +0 -1
  115. package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +0 -36
  116. package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +0 -1
  117. package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +0 -1
  118. package/packages/web/dist/assets/TemplateDetailView-DH6Oswsp.js +0 -1
  119. package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +0 -1
  120. package/packages/web/dist/assets/index-1zziPL6l.js +0 -1
  121. package/packages/web/dist/assets/index-7kzHPxSF.js +0 -1
  122. package/packages/web/dist/assets/index-B0N_obMc.js +0 -1
  123. package/packages/web/dist/assets/index-BNk_gdfI.js +0 -1
  124. package/packages/web/dist/assets/index-CSqaAH-0.js +0 -1
  125. package/packages/web/dist/assets/index-C_q4WlK8.js +0 -1
  126. package/packages/web/dist/assets/index-D1wpU4y0.js +0 -7
  127. package/packages/web/dist/assets/index-D5zCA8sD.js +0 -1
  128. package/packages/web/dist/assets/index-DGR8ELWY.js +0 -1
  129. package/packages/web/dist/assets/index-DHga8pXo.js +0 -1
  130. package/packages/web/dist/assets/index-DSby02Wl.js +0 -1
  131. package/packages/web/dist/assets/index-DqjXJTVI.js +0 -1
  132. package/packages/web/dist/assets/index-_4S2uLDI.js +0 -1
  133. package/packages/web/dist/assets/index-fK8FIZgP.js +0 -83
  134. package/packages/web/dist/assets/index-gmiZeFXN.js +0 -1
  135. package/packages/web/dist/assets/index-irD539ZM.js +0 -3
  136. package/packages/web/dist/assets/index-yq-E1Y00.js +0 -1
@@ -0,0 +1,132 @@
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
+ try {
10
+ return await git(directory, 'diff');
11
+ } catch {
12
+ return '';
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Get staged diff for a directory
18
+ * @param {string} directory
19
+ * @returns {Promise<string>}
20
+ */
21
+ export async function getStagedDiff(directory) {
22
+ try {
23
+ return await git(directory, 'diff --cached');
24
+ } catch {
25
+ return '';
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Get list of untracked files
31
+ * @param {string} directory
32
+ * @returns {Promise<string[]>}
33
+ */
34
+ export async function getUntrackedFiles(directory) {
35
+ try {
36
+ const output = await git(directory, 'ls-files --others --exclude-standard');
37
+ if (!output) return [];
38
+ return output.split('\n').filter((line) => line.trim());
39
+ } catch {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get diff for a directory compared to a specific branch
46
+ * @param {string} directory
47
+ * @param {string} branch - Branch to compare against (e.g., 'origin/main')
48
+ * @returns {Promise<string>}
49
+ */
50
+ export async function getDiffAgainstBranch(directory, branch) {
51
+ try {
52
+ return await git(directory, `diff ${branch}`);
53
+ } catch {
54
+ return '';
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get staged diff for a directory compared to a specific branch
60
+ * @param {string} directory
61
+ * @param {string} branch - Branch to compare against (e.g., 'origin/main')
62
+ * @returns {Promise<string>}
63
+ */
64
+ export async function getStagedDiffAgainstBranch(directory, branch) {
65
+ try {
66
+ return await git(directory, `diff --cached ${branch}`);
67
+ } catch {
68
+ return '';
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Get diff between two git refs (e.g., comparing HEAD to origin/main)
74
+ * This shows the committed changes between two refs, ignoring working tree state
75
+ * @param {string} directory
76
+ * @param {string} fromRef - Base ref (e.g., 'origin/main')
77
+ * @param {string} toRef - Target ref (e.g., 'HEAD')
78
+ * @returns {Promise<string>}
79
+ */
80
+ export async function getDiffBetweenRefs(directory, fromRef, toRef) {
81
+ try {
82
+ return await git(directory, `diff ${fromRef} ${toRef}`);
83
+ } catch {
84
+ return '';
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get count of files modified/added compared to a branch
90
+ * Includes committed changes + staged + unstaged + untracked files
91
+ * @param {string} directory - The git repository directory
92
+ * @param {string} branch - Branch to compare against (e.g., 'origin/main')
93
+ * @returns {Promise<number>} - Total count of unique files modified/added
94
+ */
95
+ export async function getModifiedFilesCount(directory, branch) {
96
+ try {
97
+ // Get all modified files in one command using --name-only
98
+ // This includes: committed changes vs branch + staged
99
+ const committedAndStaged = await git(
100
+ directory,
101
+ `diff --name-only ${branch}...HEAD`
102
+ );
103
+
104
+ // Get unstaged changes (working tree vs index)
105
+ const unstaged = await git(directory, 'diff --name-only');
106
+
107
+ // Get untracked files
108
+ const untracked = await getUntrackedFiles(directory);
109
+
110
+ // Combine all files into a Set to get unique count
111
+ const allFiles = new Set();
112
+
113
+ if (committedAndStaged) {
114
+ committedAndStaged.split('\n').forEach(f => {
115
+ if (f.trim()) allFiles.add(f.trim());
116
+ });
117
+ }
118
+
119
+ if (unstaged) {
120
+ unstaged.split('\n').forEach(f => {
121
+ if (f.trim()) allFiles.add(f.trim());
122
+ });
123
+ }
124
+
125
+ untracked.forEach(f => allFiles.add(f));
126
+
127
+ return allFiles.size;
128
+ } catch (error) {
129
+ console.warn(`Failed to get modified files count for ${directory}:`, error.message);
130
+ return 0;
131
+ }
132
+ }
@@ -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
+ }