claude-remote-cli 2.7.0 → 2.8.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/dist/frontend/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
12
12
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
13
13
|
<meta name="theme-color" content="#1a1a1a" />
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-CUkDx_1l.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-Not5cXLa.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
package/dist/server/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { loadConfig, saveConfig, DEFAULTS, readMeta, writeMeta, deleteMeta, ensu
|
|
|
12
12
|
import * as auth from './auth.js';
|
|
13
13
|
import * as sessions from './sessions.js';
|
|
14
14
|
import { setupWebSocket } from './ws.js';
|
|
15
|
-
import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain } from './watcher.js';
|
|
15
|
+
import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain, parseAllWorktrees } from './watcher.js';
|
|
16
16
|
import { isInstalled as serviceIsInstalled } from './service.js';
|
|
17
17
|
import { extensionForMime, setClipboardImage } from './clipboard.js';
|
|
18
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -507,9 +507,22 @@ async function main() {
|
|
|
507
507
|
res.status(400).json({ error: 'worktreePath and repoPath are required' });
|
|
508
508
|
return;
|
|
509
509
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
510
|
+
// Validate the path is a real git worktree (not the main worktree)
|
|
511
|
+
try {
|
|
512
|
+
const { stdout: wtListOut } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { cwd: repoPath });
|
|
513
|
+
const allWorktrees = parseAllWorktrees(wtListOut, repoPath);
|
|
514
|
+
const isKnownWorktree = allWorktrees.some(wt => wt.path === path.resolve(worktreePath) && !wt.isMain);
|
|
515
|
+
if (!isKnownWorktree) {
|
|
516
|
+
res.status(400).json({ error: 'Path is not a recognized git worktree' });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
// If git worktree list fails, fall back to the directory-name check
|
|
522
|
+
if (!isValidWorktreePath(worktreePath)) {
|
|
523
|
+
res.status(400).json({ error: 'Path is not inside a worktree directory' });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
513
526
|
}
|
|
514
527
|
// Check no active session is using this worktree
|
|
515
528
|
const activeSessions = sessions.list();
|
|
@@ -620,6 +633,62 @@ async function main() {
|
|
|
620
633
|
}
|
|
621
634
|
}
|
|
622
635
|
if (branchName && branchExists) {
|
|
636
|
+
// Check if branch is already checked out in an existing worktree
|
|
637
|
+
const { stdout: wtListOut } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { cwd: repoPath });
|
|
638
|
+
const allWorktrees = parseAllWorktrees(wtListOut, repoPath);
|
|
639
|
+
const existingWt = allWorktrees.find(wt => wt.branch === branchName);
|
|
640
|
+
if (existingWt) {
|
|
641
|
+
// Branch already checked out — redirect to the existing worktree
|
|
642
|
+
if (existingWt.isMain) {
|
|
643
|
+
// Main worktree → create a repo session
|
|
644
|
+
const existingRepoSession = sessions.findRepoSession(repoPath);
|
|
645
|
+
if (existingRepoSession) {
|
|
646
|
+
res.status(409).json({ error: 'A session already exists for this repo', sessionId: existingRepoSession.id });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const repoSession = sessions.create({
|
|
650
|
+
type: 'repo',
|
|
651
|
+
repoName: name,
|
|
652
|
+
repoPath,
|
|
653
|
+
cwd: repoPath,
|
|
654
|
+
root,
|
|
655
|
+
displayName: name,
|
|
656
|
+
command: config.claudeCommand,
|
|
657
|
+
args: baseArgs,
|
|
658
|
+
});
|
|
659
|
+
res.status(201).json(repoSession);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
// Another worktree → create a worktree session with --continue
|
|
664
|
+
cwd = existingWt.path;
|
|
665
|
+
sessionRepoPath = existingWt.path;
|
|
666
|
+
worktreeName = existingWt.path.split('/').pop() || '';
|
|
667
|
+
args = ['--continue', ...baseArgs];
|
|
668
|
+
const displayNameVal = branchName || worktreeName;
|
|
669
|
+
const session = sessions.create({
|
|
670
|
+
type: 'worktree',
|
|
671
|
+
repoName: name,
|
|
672
|
+
repoPath: sessionRepoPath,
|
|
673
|
+
cwd,
|
|
674
|
+
root,
|
|
675
|
+
worktreeName,
|
|
676
|
+
branchName: branchName || worktreeName,
|
|
677
|
+
displayName: displayNameVal,
|
|
678
|
+
command: config.claudeCommand,
|
|
679
|
+
args,
|
|
680
|
+
configPath: CONFIG_PATH,
|
|
681
|
+
});
|
|
682
|
+
writeMeta(CONFIG_PATH, {
|
|
683
|
+
worktreePath: sessionRepoPath,
|
|
684
|
+
displayName: displayNameVal,
|
|
685
|
+
lastActivity: new Date().toISOString(),
|
|
686
|
+
branchName: branchName || worktreeName,
|
|
687
|
+
});
|
|
688
|
+
res.status(201).json(session);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
623
692
|
await execFileAsync('git', ['worktree', 'add', targetDir, resolvedBranch], { cwd: repoPath });
|
|
624
693
|
}
|
|
625
694
|
else if (branchName) {
|
package/dist/server/watcher.js
CHANGED
|
@@ -8,6 +8,32 @@ export function isValidWorktreePath(worktreePath) {
|
|
|
8
8
|
return resolved.includes(path.sep + dir + path.sep);
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse `git worktree list --porcelain` output into ALL entries (including main worktree).
|
|
13
|
+
* Skips bare entries. Detached HEAD entries get empty branch string.
|
|
14
|
+
*/
|
|
15
|
+
export function parseAllWorktrees(stdout, repoPath) {
|
|
16
|
+
const results = [];
|
|
17
|
+
const blocks = stdout.split('\n\n').filter(Boolean);
|
|
18
|
+
for (const block of blocks) {
|
|
19
|
+
const lines = block.split('\n');
|
|
20
|
+
let wtPath = '';
|
|
21
|
+
let branch = '';
|
|
22
|
+
let bare = false;
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
if (line.startsWith('worktree '))
|
|
25
|
+
wtPath = line.slice(9);
|
|
26
|
+
if (line.startsWith('branch refs/heads/'))
|
|
27
|
+
branch = line.slice(18);
|
|
28
|
+
if (line === 'bare')
|
|
29
|
+
bare = true;
|
|
30
|
+
}
|
|
31
|
+
if (!wtPath || bare)
|
|
32
|
+
continue;
|
|
33
|
+
results.push({ path: wtPath, branch, isMain: wtPath === repoPath });
|
|
34
|
+
}
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
11
37
|
/**
|
|
12
38
|
* Parse `git worktree list --porcelain` output into structured entries.
|
|
13
39
|
* Skips the main worktree (matching repoPath) and bare/detached entries.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain } from '../server/watcher.js';
|
|
3
|
+
import { WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain, parseAllWorktrees } from '../server/watcher.js';
|
|
4
4
|
describe('worktree directories constant', () => {
|
|
5
5
|
it('should include both .worktrees and .claude/worktrees', () => {
|
|
6
6
|
assert.deepEqual(WORKTREE_DIRS, ['.worktrees', '.claude/worktrees']);
|
|
@@ -146,6 +146,91 @@ describe('parseWorktreeListPorcelain', () => {
|
|
|
146
146
|
assert.equal(result[0].branch, 'dy/feat/deep/nesting/here');
|
|
147
147
|
});
|
|
148
148
|
});
|
|
149
|
+
describe('parseAllWorktrees', () => {
|
|
150
|
+
const repoPath = '/Users/me/code/my-repo';
|
|
151
|
+
it('should include the main worktree with isMain=true', () => {
|
|
152
|
+
const stdout = [
|
|
153
|
+
`worktree ${repoPath}`,
|
|
154
|
+
'HEAD abc123',
|
|
155
|
+
'branch refs/heads/main',
|
|
156
|
+
'',
|
|
157
|
+
].join('\n');
|
|
158
|
+
const result = parseAllWorktrees(stdout, repoPath);
|
|
159
|
+
assert.equal(result.length, 1);
|
|
160
|
+
assert.equal(result[0].path, repoPath);
|
|
161
|
+
assert.equal(result[0].branch, 'main');
|
|
162
|
+
assert.equal(result[0].isMain, true);
|
|
163
|
+
});
|
|
164
|
+
it('should mark non-main worktrees with isMain=false', () => {
|
|
165
|
+
const stdout = [
|
|
166
|
+
`worktree ${repoPath}`,
|
|
167
|
+
'HEAD abc123',
|
|
168
|
+
'branch refs/heads/main',
|
|
169
|
+
'',
|
|
170
|
+
'worktree /Users/me/code/my-repo/.worktrees/feat-branch',
|
|
171
|
+
'HEAD def456',
|
|
172
|
+
'branch refs/heads/feat/branch',
|
|
173
|
+
'',
|
|
174
|
+
].join('\n');
|
|
175
|
+
const result = parseAllWorktrees(stdout, repoPath);
|
|
176
|
+
assert.equal(result.length, 2);
|
|
177
|
+
assert.equal(result[0].isMain, true);
|
|
178
|
+
assert.equal(result[1].isMain, false);
|
|
179
|
+
assert.equal(result[1].path, '/Users/me/code/my-repo/.worktrees/feat-branch');
|
|
180
|
+
assert.equal(result[1].branch, 'feat/branch');
|
|
181
|
+
});
|
|
182
|
+
it('should still skip bare entries', () => {
|
|
183
|
+
const stdout = [
|
|
184
|
+
`worktree ${repoPath}`,
|
|
185
|
+
'HEAD abc123',
|
|
186
|
+
'branch refs/heads/main',
|
|
187
|
+
'',
|
|
188
|
+
'worktree /some/bare/repo',
|
|
189
|
+
'HEAD def456',
|
|
190
|
+
'bare',
|
|
191
|
+
'',
|
|
192
|
+
].join('\n');
|
|
193
|
+
const result = parseAllWorktrees(stdout, repoPath);
|
|
194
|
+
assert.equal(result.length, 1);
|
|
195
|
+
assert.equal(result[0].isMain, true);
|
|
196
|
+
});
|
|
197
|
+
it('should include detached HEAD entries with empty branch', () => {
|
|
198
|
+
const stdout = [
|
|
199
|
+
`worktree ${repoPath}`,
|
|
200
|
+
'HEAD abc123',
|
|
201
|
+
'branch refs/heads/main',
|
|
202
|
+
'',
|
|
203
|
+
'worktree /Users/me/code/my-repo/.worktrees/detached',
|
|
204
|
+
'HEAD def456',
|
|
205
|
+
'detached',
|
|
206
|
+
'',
|
|
207
|
+
].join('\n');
|
|
208
|
+
const result = parseAllWorktrees(stdout, repoPath);
|
|
209
|
+
assert.equal(result.length, 2);
|
|
210
|
+
assert.equal(result[1].branch, '');
|
|
211
|
+
});
|
|
212
|
+
it('should handle empty output', () => {
|
|
213
|
+
const result = parseAllWorktrees('', repoPath);
|
|
214
|
+
assert.equal(result.length, 0);
|
|
215
|
+
});
|
|
216
|
+
it('should find worktree by branch name', () => {
|
|
217
|
+
const stdout = [
|
|
218
|
+
`worktree ${repoPath}`,
|
|
219
|
+
'HEAD abc123',
|
|
220
|
+
'branch refs/heads/dy/feat/worktree-isolation',
|
|
221
|
+
'',
|
|
222
|
+
'worktree /Users/me/code/my-repo/.worktrees/feat-a',
|
|
223
|
+
'HEAD def456',
|
|
224
|
+
'branch refs/heads/feat/a',
|
|
225
|
+
'',
|
|
226
|
+
].join('\n');
|
|
227
|
+
const result = parseAllWorktrees(stdout, repoPath);
|
|
228
|
+
const match = result.find(wt => wt.branch === 'dy/feat/worktree-isolation');
|
|
229
|
+
assert.ok(match);
|
|
230
|
+
assert.equal(match.path, repoPath);
|
|
231
|
+
assert.equal(match.isMain, true);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
149
234
|
describe('CLI worktree arg parsing', () => {
|
|
150
235
|
it('should extract --yolo and leave other args intact', () => {
|
|
151
236
|
const args = ['add', './.worktrees/my-feature', '-b', 'my-feature', '--yolo'];
|