claude-remote-cli 2.4.5 → 2.4.7
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/assets/{index-D_vlhnTU.css → index-0GsaVTCt.css} +1 -1
- package/dist/frontend/assets/{index-DOfplZTg.js → index-DHmjTDUE.js} +18 -18
- package/dist/frontend/index.html +2 -2
- package/dist/server/index.js +48 -22
- package/dist/server/sessions.js +5 -3
- package/dist/server/watcher.js +27 -0
- package/dist/test/sessions.test.js +50 -0
- package/dist/test/worktrees.test.js +115 -1
- package/package.json +1 -1
package/dist/frontend/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
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-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DHmjTDUE.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-0GsaVTCt.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="app"></div>
|
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 } from './watcher.js';
|
|
15
|
+
import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain } 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);
|
|
@@ -384,7 +384,7 @@ async function main() {
|
|
|
384
384
|
res.json(response);
|
|
385
385
|
});
|
|
386
386
|
// GET /worktrees?repo=<path> — list worktrees; omit repo to scan all repos in all rootDirs
|
|
387
|
-
app.get('/worktrees', requireAuth, (req, res) => {
|
|
387
|
+
app.get('/worktrees', requireAuth, async (req, res) => {
|
|
388
388
|
const repoParam = typeof req.query.repo === 'string' ? req.query.repo : undefined;
|
|
389
389
|
const roots = config.rootDirs || [];
|
|
390
390
|
const worktrees = [];
|
|
@@ -397,31 +397,54 @@ async function main() {
|
|
|
397
397
|
reposToScan = scanAllRepos(roots);
|
|
398
398
|
}
|
|
399
399
|
for (const repo of reposToScan) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
for (const entry of entries) {
|
|
410
|
-
if (!entry.isDirectory())
|
|
411
|
-
continue;
|
|
412
|
-
const wtPath = path.join(worktreeDir, entry.name);
|
|
413
|
-
const meta = readMeta(CONFIG_PATH, wtPath);
|
|
400
|
+
// Use git worktree list to discover all worktrees (including those at arbitrary paths)
|
|
401
|
+
try {
|
|
402
|
+
const { stdout } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { cwd: repo.path });
|
|
403
|
+
const parsed = parseWorktreeListPorcelain(stdout, repo.path);
|
|
404
|
+
for (const wt of parsed) {
|
|
405
|
+
const dirName = wt.path.split('/').pop() || '';
|
|
406
|
+
const meta = readMeta(CONFIG_PATH, wt.path);
|
|
414
407
|
worktrees.push({
|
|
415
|
-
name:
|
|
416
|
-
path:
|
|
408
|
+
name: dirName,
|
|
409
|
+
path: wt.path,
|
|
417
410
|
repoName: repo.name,
|
|
418
411
|
repoPath: repo.path,
|
|
419
412
|
root: repo.root,
|
|
420
|
-
displayName: meta
|
|
421
|
-
lastActivity: meta
|
|
413
|
+
displayName: meta?.displayName || wt.branch || dirName,
|
|
414
|
+
lastActivity: meta?.lastActivity || '',
|
|
415
|
+
branchName: wt.branch || meta?.branchName || dirName,
|
|
422
416
|
});
|
|
423
417
|
}
|
|
424
418
|
}
|
|
419
|
+
catch {
|
|
420
|
+
// git worktree list failed — fall back to directory scanning
|
|
421
|
+
for (const dir of WORKTREE_DIRS) {
|
|
422
|
+
const worktreeDir = path.join(repo.path, dir);
|
|
423
|
+
let entries;
|
|
424
|
+
try {
|
|
425
|
+
entries = fs.readdirSync(worktreeDir, { withFileTypes: true });
|
|
426
|
+
}
|
|
427
|
+
catch (_) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
for (const entry of entries) {
|
|
431
|
+
if (!entry.isDirectory())
|
|
432
|
+
continue;
|
|
433
|
+
const wtPath = path.join(worktreeDir, entry.name);
|
|
434
|
+
const meta = readMeta(CONFIG_PATH, wtPath);
|
|
435
|
+
worktrees.push({
|
|
436
|
+
name: entry.name,
|
|
437
|
+
path: wtPath,
|
|
438
|
+
repoName: repo.name,
|
|
439
|
+
repoPath: repo.path,
|
|
440
|
+
root: repo.root,
|
|
441
|
+
displayName: meta?.displayName || '',
|
|
442
|
+
lastActivity: meta?.lastActivity || '',
|
|
443
|
+
branchName: meta?.branchName || entry.name,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
425
448
|
}
|
|
426
449
|
res.json(worktrees);
|
|
427
450
|
});
|
|
@@ -555,13 +578,15 @@ async function main() {
|
|
|
555
578
|
dirName = 'mobile-' + name + '-' + Date.now().toString(36);
|
|
556
579
|
resolvedBranch = dirName;
|
|
557
580
|
}
|
|
558
|
-
const worktreeDir = path.join(repoPath,
|
|
581
|
+
const worktreeDir = path.join(repoPath, WORKTREE_DIRS[0]);
|
|
559
582
|
let targetDir = path.join(worktreeDir, dirName);
|
|
560
583
|
if (fs.existsSync(targetDir)) {
|
|
561
584
|
targetDir = targetDir + '-' + Date.now().toString(36);
|
|
562
585
|
dirName = path.basename(targetDir);
|
|
563
586
|
}
|
|
564
|
-
|
|
587
|
+
for (const dir of WORKTREE_DIRS) {
|
|
588
|
+
ensureGitignore(repoPath, dir + '/');
|
|
589
|
+
}
|
|
565
590
|
try {
|
|
566
591
|
// Check if branch exists locally or on a remote
|
|
567
592
|
let branchExists = false;
|
|
@@ -605,6 +630,7 @@ async function main() {
|
|
|
605
630
|
cwd,
|
|
606
631
|
root,
|
|
607
632
|
worktreeName,
|
|
633
|
+
branchName: branchName || worktreeName,
|
|
608
634
|
displayName,
|
|
609
635
|
command: config.claudeCommand,
|
|
610
636
|
args,
|
package/dist/server/sessions.js
CHANGED
|
@@ -11,7 +11,7 @@ let idleChangeCallback = null;
|
|
|
11
11
|
function onIdleChange(cb) {
|
|
12
12
|
idleChangeCallback = cb;
|
|
13
13
|
}
|
|
14
|
-
function create({ type, repoName, repoPath, cwd, root, worktreeName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
|
|
14
|
+
function create({ type, repoName, repoPath, cwd, root, worktreeName, branchName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
|
|
15
15
|
const id = crypto.randomBytes(8).toString('hex');
|
|
16
16
|
const createdAt = new Date().toISOString();
|
|
17
17
|
// Strip CLAUDECODE env var to allow spawning claude inside a claude-managed server
|
|
@@ -35,6 +35,7 @@ function create({ type, repoName, repoPath, cwd, root, worktreeName, displayName
|
|
|
35
35
|
repoName: repoName || '',
|
|
36
36
|
repoPath,
|
|
37
37
|
worktreeName: worktreeName || '',
|
|
38
|
+
branchName: branchName || worktreeName || '',
|
|
38
39
|
displayName: displayName || worktreeName || repoName || '',
|
|
39
40
|
pty: ptyProcess,
|
|
40
41
|
createdAt,
|
|
@@ -97,20 +98,21 @@ function create({ type, repoName, repoPath, cwd, root, worktreeName, displayName
|
|
|
97
98
|
const tmpDir = path.join(os.tmpdir(), 'claude-remote-cli', id);
|
|
98
99
|
fs.rm(tmpDir, { recursive: true, force: true }, () => { });
|
|
99
100
|
});
|
|
100
|
-
return { id, type: session.type, root: session.root, repoName: session.repoName, repoPath, worktreeName: session.worktreeName, displayName: session.displayName, pid: ptyProcess.pid, createdAt, lastActivity: createdAt, idle: false };
|
|
101
|
+
return { id, type: session.type, root: session.root, repoName: session.repoName, repoPath, worktreeName: session.worktreeName, branchName: session.branchName, displayName: session.displayName, pid: ptyProcess.pid, createdAt, lastActivity: createdAt, idle: false };
|
|
101
102
|
}
|
|
102
103
|
function get(id) {
|
|
103
104
|
return sessions.get(id);
|
|
104
105
|
}
|
|
105
106
|
function list() {
|
|
106
107
|
return Array.from(sessions.values())
|
|
107
|
-
.map(({ id, type, root, repoName, repoPath, worktreeName, displayName, createdAt, lastActivity, idle }) => ({
|
|
108
|
+
.map(({ id, type, root, repoName, repoPath, worktreeName, branchName, displayName, createdAt, lastActivity, idle }) => ({
|
|
108
109
|
id,
|
|
109
110
|
type,
|
|
110
111
|
root,
|
|
111
112
|
repoName,
|
|
112
113
|
repoPath,
|
|
113
114
|
worktreeName,
|
|
115
|
+
branchName,
|
|
114
116
|
displayName,
|
|
115
117
|
createdAt,
|
|
116
118
|
lastActivity,
|
package/dist/server/watcher.js
CHANGED
|
@@ -8,6 +8,33 @@ 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 structured entries.
|
|
13
|
+
* Skips the main worktree (matching repoPath) and bare/detached entries.
|
|
14
|
+
*/
|
|
15
|
+
export function parseWorktreeListPorcelain(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
|
+
// Skip the main worktree (repo root), bare repos, and detached HEAD
|
|
32
|
+
if (!wtPath || wtPath === repoPath || bare || !branch)
|
|
33
|
+
continue;
|
|
34
|
+
results.push({ path: wtPath, branch });
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
11
38
|
export class WorktreeWatcher extends EventEmitter {
|
|
12
39
|
_watchers;
|
|
13
40
|
_debounceTimer;
|
|
@@ -214,4 +214,54 @@ describe('sessions', () => {
|
|
|
214
214
|
const found = sessions.findRepoSession('/tmp/my-repo');
|
|
215
215
|
assert.strictEqual(found, undefined, 'should not match worktree sessions');
|
|
216
216
|
});
|
|
217
|
+
it('branchName defaults to worktreeName when not specified', () => {
|
|
218
|
+
const result = sessions.create({
|
|
219
|
+
repoName: 'test-repo',
|
|
220
|
+
repoPath: '/tmp',
|
|
221
|
+
worktreeName: 'dy-feat-my-feature',
|
|
222
|
+
command: '/bin/echo',
|
|
223
|
+
args: ['hello'],
|
|
224
|
+
});
|
|
225
|
+
createdIds.push(result.id);
|
|
226
|
+
assert.strictEqual(result.branchName, 'dy-feat-my-feature');
|
|
227
|
+
});
|
|
228
|
+
it('branchName is set independently from worktreeName', () => {
|
|
229
|
+
const result = sessions.create({
|
|
230
|
+
repoName: 'test-repo',
|
|
231
|
+
repoPath: '/tmp',
|
|
232
|
+
worktreeName: 'dy-feat-my-feature',
|
|
233
|
+
branchName: 'dy/feat/my-feature',
|
|
234
|
+
command: '/bin/echo',
|
|
235
|
+
args: ['hello'],
|
|
236
|
+
});
|
|
237
|
+
createdIds.push(result.id);
|
|
238
|
+
assert.strictEqual(result.worktreeName, 'dy-feat-my-feature');
|
|
239
|
+
assert.strictEqual(result.branchName, 'dy/feat/my-feature');
|
|
240
|
+
});
|
|
241
|
+
it('list includes branchName field', () => {
|
|
242
|
+
const result = sessions.create({
|
|
243
|
+
repoName: 'test-repo',
|
|
244
|
+
repoPath: '/tmp',
|
|
245
|
+
worktreeName: 'my-wt',
|
|
246
|
+
branchName: 'feat/my-branch',
|
|
247
|
+
command: '/bin/echo',
|
|
248
|
+
args: ['hello'],
|
|
249
|
+
});
|
|
250
|
+
createdIds.push(result.id);
|
|
251
|
+
const list = sessions.list();
|
|
252
|
+
const session = list.find(s => s.id === result.id);
|
|
253
|
+
assert.ok(session);
|
|
254
|
+
assert.strictEqual(session.branchName, 'feat/my-branch');
|
|
255
|
+
});
|
|
256
|
+
it('branchName defaults to empty string when neither branchName nor worktreeName provided', () => {
|
|
257
|
+
const result = sessions.create({
|
|
258
|
+
type: 'repo',
|
|
259
|
+
repoName: 'test-repo',
|
|
260
|
+
repoPath: '/tmp',
|
|
261
|
+
command: '/bin/echo',
|
|
262
|
+
args: ['hello'],
|
|
263
|
+
});
|
|
264
|
+
createdIds.push(result.id);
|
|
265
|
+
assert.strictEqual(result.branchName, '');
|
|
266
|
+
});
|
|
217
267
|
});
|
|
@@ -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 } from '../server/watcher.js';
|
|
3
|
+
import { WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain } 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']);
|
|
@@ -32,6 +32,120 @@ describe('branch name to directory name', () => {
|
|
|
32
32
|
assert.equal(dirName, 'my-feature');
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
|
+
describe('parseWorktreeListPorcelain', () => {
|
|
36
|
+
const repoPath = '/Users/me/code/my-repo';
|
|
37
|
+
it('should parse a single worktree entry', () => {
|
|
38
|
+
const stdout = [
|
|
39
|
+
`worktree ${repoPath}`,
|
|
40
|
+
'HEAD abc123',
|
|
41
|
+
'branch refs/heads/main',
|
|
42
|
+
'',
|
|
43
|
+
'worktree /Users/me/code/my-repo/.worktrees/feat-branch',
|
|
44
|
+
'HEAD def456',
|
|
45
|
+
'branch refs/heads/feat/branch',
|
|
46
|
+
'',
|
|
47
|
+
].join('\n');
|
|
48
|
+
const result = parseWorktreeListPorcelain(stdout, repoPath);
|
|
49
|
+
assert.equal(result.length, 1);
|
|
50
|
+
assert.equal(result[0].path, '/Users/me/code/my-repo/.worktrees/feat-branch');
|
|
51
|
+
assert.equal(result[0].branch, 'feat/branch');
|
|
52
|
+
});
|
|
53
|
+
it('should parse multiple worktree entries', () => {
|
|
54
|
+
const stdout = [
|
|
55
|
+
`worktree ${repoPath}`,
|
|
56
|
+
'HEAD abc123',
|
|
57
|
+
'branch refs/heads/main',
|
|
58
|
+
'',
|
|
59
|
+
'worktree /Users/me/code/my-repo/.worktrees/feat-a',
|
|
60
|
+
'HEAD def456',
|
|
61
|
+
'branch refs/heads/feat/a',
|
|
62
|
+
'',
|
|
63
|
+
'worktree /Users/me/other-path/extend-cli',
|
|
64
|
+
'HEAD 789abc',
|
|
65
|
+
'branch refs/heads/dy/feat/worktree-isolation',
|
|
66
|
+
'',
|
|
67
|
+
].join('\n');
|
|
68
|
+
const result = parseWorktreeListPorcelain(stdout, repoPath);
|
|
69
|
+
assert.equal(result.length, 2);
|
|
70
|
+
assert.equal(result[0].path, '/Users/me/code/my-repo/.worktrees/feat-a');
|
|
71
|
+
assert.equal(result[0].branch, 'feat/a');
|
|
72
|
+
assert.equal(result[1].path, '/Users/me/other-path/extend-cli');
|
|
73
|
+
assert.equal(result[1].branch, 'dy/feat/worktree-isolation');
|
|
74
|
+
});
|
|
75
|
+
it('should skip the main worktree (repo root)', () => {
|
|
76
|
+
const stdout = [
|
|
77
|
+
`worktree ${repoPath}`,
|
|
78
|
+
'HEAD abc123',
|
|
79
|
+
'branch refs/heads/main',
|
|
80
|
+
'',
|
|
81
|
+
].join('\n');
|
|
82
|
+
const result = parseWorktreeListPorcelain(stdout, repoPath);
|
|
83
|
+
assert.equal(result.length, 0);
|
|
84
|
+
});
|
|
85
|
+
it('should skip bare entries', () => {
|
|
86
|
+
const stdout = [
|
|
87
|
+
`worktree ${repoPath}`,
|
|
88
|
+
'HEAD abc123',
|
|
89
|
+
'branch refs/heads/main',
|
|
90
|
+
'',
|
|
91
|
+
'worktree /some/bare/repo',
|
|
92
|
+
'HEAD def456',
|
|
93
|
+
'bare',
|
|
94
|
+
'',
|
|
95
|
+
].join('\n');
|
|
96
|
+
const result = parseWorktreeListPorcelain(stdout, repoPath);
|
|
97
|
+
assert.equal(result.length, 0);
|
|
98
|
+
});
|
|
99
|
+
it('should skip detached HEAD worktrees (no branch line)', () => {
|
|
100
|
+
const stdout = [
|
|
101
|
+
`worktree ${repoPath}`,
|
|
102
|
+
'HEAD abc123',
|
|
103
|
+
'branch refs/heads/main',
|
|
104
|
+
'',
|
|
105
|
+
'worktree /Users/me/code/my-repo/.worktrees/detached',
|
|
106
|
+
'HEAD def456',
|
|
107
|
+
'detached',
|
|
108
|
+
'',
|
|
109
|
+
].join('\n');
|
|
110
|
+
const result = parseWorktreeListPorcelain(stdout, repoPath);
|
|
111
|
+
assert.equal(result.length, 0);
|
|
112
|
+
});
|
|
113
|
+
it('should handle empty output', () => {
|
|
114
|
+
const result = parseWorktreeListPorcelain('', repoPath);
|
|
115
|
+
assert.equal(result.length, 0);
|
|
116
|
+
});
|
|
117
|
+
it('should discover worktrees at arbitrary paths outside .worktrees/', () => {
|
|
118
|
+
const stdout = [
|
|
119
|
+
`worktree ${repoPath}`,
|
|
120
|
+
'HEAD abc123',
|
|
121
|
+
'branch refs/heads/main',
|
|
122
|
+
'',
|
|
123
|
+
'worktree /completely/different/path/project-checkout',
|
|
124
|
+
'HEAD def456',
|
|
125
|
+
'branch refs/heads/feature/my-feature',
|
|
126
|
+
'',
|
|
127
|
+
].join('\n');
|
|
128
|
+
const result = parseWorktreeListPorcelain(stdout, repoPath);
|
|
129
|
+
assert.equal(result.length, 1);
|
|
130
|
+
assert.equal(result[0].path, '/completely/different/path/project-checkout');
|
|
131
|
+
assert.equal(result[0].branch, 'feature/my-feature');
|
|
132
|
+
});
|
|
133
|
+
it('should handle deeply nested branch names', () => {
|
|
134
|
+
const stdout = [
|
|
135
|
+
`worktree ${repoPath}`,
|
|
136
|
+
'HEAD abc123',
|
|
137
|
+
'branch refs/heads/main',
|
|
138
|
+
'',
|
|
139
|
+
'worktree /Users/me/code/my-repo/.worktrees/dy-feat-deep-nesting',
|
|
140
|
+
'HEAD def456',
|
|
141
|
+
'branch refs/heads/dy/feat/deep/nesting/here',
|
|
142
|
+
'',
|
|
143
|
+
].join('\n');
|
|
144
|
+
const result = parseWorktreeListPorcelain(stdout, repoPath);
|
|
145
|
+
assert.equal(result.length, 1);
|
|
146
|
+
assert.equal(result[0].branch, 'dy/feat/deep/nesting/here');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
35
149
|
describe('CLI worktree arg parsing', () => {
|
|
36
150
|
it('should extract --yolo and leave other args intact', () => {
|
|
37
151
|
const args = ['add', './.worktrees/my-feature', '-b', 'my-feature', '--yolo'];
|