claude-remote-cli 2.7.0 → 2.9.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/README.md +5 -2
- package/dist/frontend/assets/index-B3mDW63X.css +32 -0
- package/dist/frontend/assets/index-Dkj00kFR.js +47 -0
- package/dist/frontend/index.html +2 -2
- package/dist/server/config.js +1 -0
- package/dist/server/index.js +97 -10
- package/dist/server/sessions.js +25 -9
- package/dist/server/watcher.js +26 -0
- package/dist/test/config.test.js +2 -0
- package/dist/test/sessions.test.js +35 -0
- package/dist/test/worktrees.test.js +86 -1
- package/package.json +1 -1
- package/dist/frontend/assets/index-C2nVSRxb.js +0 -47
- package/dist/frontend/assets/index-Not5cXLa.css +0 -32
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-Dkj00kFR.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B3mDW63X.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="app"></div>
|
package/dist/server/config.js
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -11,8 +11,9 @@ import cookieParser from 'cookie-parser';
|
|
|
11
11
|
import { loadConfig, saveConfig, DEFAULTS, readMeta, writeMeta, deleteMeta, ensureMetaDir } from './config.js';
|
|
12
12
|
import * as auth from './auth.js';
|
|
13
13
|
import * as sessions from './sessions.js';
|
|
14
|
+
import { AGENT_CONTINUE_ARGS } from './sessions.js';
|
|
14
15
|
import { setupWebSocket } from './ws.js';
|
|
15
|
-
import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain } from './watcher.js';
|
|
16
|
+
import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain, parseAllWorktrees } from './watcher.js';
|
|
16
17
|
import { isInstalled as serviceIsInstalled } from './service.js';
|
|
17
18
|
import { extensionForMime, setClipboardImage } from './clipboard.js';
|
|
18
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -500,6 +501,21 @@ async function main() {
|
|
|
500
501
|
broadcastEvent('worktrees-changed');
|
|
501
502
|
res.json(config.rootDirs);
|
|
502
503
|
});
|
|
504
|
+
// GET /config/defaultAgent — get default coding agent
|
|
505
|
+
app.get('/config/defaultAgent', requireAuth, (_req, res) => {
|
|
506
|
+
res.json({ defaultAgent: config.defaultAgent || 'claude' });
|
|
507
|
+
});
|
|
508
|
+
// PATCH /config/defaultAgent — set default coding agent
|
|
509
|
+
app.patch('/config/defaultAgent', requireAuth, (req, res) => {
|
|
510
|
+
const { defaultAgent } = req.body;
|
|
511
|
+
if (!defaultAgent || (defaultAgent !== 'claude' && defaultAgent !== 'codex')) {
|
|
512
|
+
res.status(400).json({ error: 'defaultAgent must be "claude" or "codex"' });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
config.defaultAgent = defaultAgent;
|
|
516
|
+
saveConfig(CONFIG_PATH, config);
|
|
517
|
+
res.json({ defaultAgent: config.defaultAgent });
|
|
518
|
+
});
|
|
503
519
|
// DELETE /worktrees — remove a worktree, prune, and delete its branch
|
|
504
520
|
app.delete('/worktrees', requireAuth, async (req, res) => {
|
|
505
521
|
const { worktreePath, repoPath } = req.body;
|
|
@@ -507,9 +523,22 @@ async function main() {
|
|
|
507
523
|
res.status(400).json({ error: 'worktreePath and repoPath are required' });
|
|
508
524
|
return;
|
|
509
525
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
526
|
+
// Validate the path is a real git worktree (not the main worktree)
|
|
527
|
+
try {
|
|
528
|
+
const { stdout: wtListOut } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { cwd: repoPath });
|
|
529
|
+
const allWorktrees = parseAllWorktrees(wtListOut, repoPath);
|
|
530
|
+
const isKnownWorktree = allWorktrees.some(wt => wt.path === path.resolve(worktreePath) && !wt.isMain);
|
|
531
|
+
if (!isKnownWorktree) {
|
|
532
|
+
res.status(400).json({ error: 'Path is not a recognized git worktree' });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// If git worktree list fails, fall back to the directory-name check
|
|
538
|
+
if (!isValidWorktreePath(worktreePath)) {
|
|
539
|
+
res.status(400).json({ error: 'Path is not inside a worktree directory' });
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
513
542
|
}
|
|
514
543
|
// Check no active session is using this worktree
|
|
515
544
|
const activeSessions = sessions.list();
|
|
@@ -561,11 +590,12 @@ async function main() {
|
|
|
561
590
|
});
|
|
562
591
|
// POST /sessions
|
|
563
592
|
app.post('/sessions', requireAuth, async (req, res) => {
|
|
564
|
-
const { repoPath, repoName, worktreePath, branchName, claudeArgs } = req.body;
|
|
593
|
+
const { repoPath, repoName, worktreePath, branchName, claudeArgs, agent } = req.body;
|
|
565
594
|
if (!repoPath) {
|
|
566
595
|
res.status(400).json({ error: 'repoPath is required' });
|
|
567
596
|
return;
|
|
568
597
|
}
|
|
598
|
+
const resolvedAgent = agent || config.defaultAgent || 'claude';
|
|
569
599
|
const name = repoName || repoPath.split('/').filter(Boolean).pop() || 'session';
|
|
570
600
|
const baseArgs = [...(config.claudeArgs || []), ...(claudeArgs || [])];
|
|
571
601
|
// Compute root by matching repoPath against configured rootDirs
|
|
@@ -578,7 +608,7 @@ async function main() {
|
|
|
578
608
|
let resolvedBranch = '';
|
|
579
609
|
if (worktreePath) {
|
|
580
610
|
// Resume existing worktree
|
|
581
|
-
args = [
|
|
611
|
+
args = [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs];
|
|
582
612
|
cwd = worktreePath;
|
|
583
613
|
sessionRepoPath = worktreePath;
|
|
584
614
|
worktreeName = worktreePath.split('/').pop() || '';
|
|
@@ -620,6 +650,62 @@ async function main() {
|
|
|
620
650
|
}
|
|
621
651
|
}
|
|
622
652
|
if (branchName && branchExists) {
|
|
653
|
+
// Check if branch is already checked out in an existing worktree
|
|
654
|
+
const { stdout: wtListOut } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { cwd: repoPath });
|
|
655
|
+
const allWorktrees = parseAllWorktrees(wtListOut, repoPath);
|
|
656
|
+
const existingWt = allWorktrees.find(wt => wt.branch === branchName);
|
|
657
|
+
if (existingWt) {
|
|
658
|
+
// Branch already checked out — redirect to the existing worktree
|
|
659
|
+
if (existingWt.isMain) {
|
|
660
|
+
// Main worktree → create a repo session
|
|
661
|
+
const existingRepoSession = sessions.findRepoSession(repoPath);
|
|
662
|
+
if (existingRepoSession) {
|
|
663
|
+
res.status(409).json({ error: 'A session already exists for this repo', sessionId: existingRepoSession.id });
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const repoSession = sessions.create({
|
|
667
|
+
type: 'repo',
|
|
668
|
+
agent: resolvedAgent,
|
|
669
|
+
repoName: name,
|
|
670
|
+
repoPath,
|
|
671
|
+
cwd: repoPath,
|
|
672
|
+
root,
|
|
673
|
+
displayName: name,
|
|
674
|
+
args: baseArgs,
|
|
675
|
+
});
|
|
676
|
+
res.status(201).json(repoSession);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
// Another worktree → create a worktree session with --continue
|
|
681
|
+
cwd = existingWt.path;
|
|
682
|
+
sessionRepoPath = existingWt.path;
|
|
683
|
+
worktreeName = existingWt.path.split('/').pop() || '';
|
|
684
|
+
args = [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs];
|
|
685
|
+
const displayNameVal = branchName || worktreeName;
|
|
686
|
+
const session = sessions.create({
|
|
687
|
+
type: 'worktree',
|
|
688
|
+
agent: resolvedAgent,
|
|
689
|
+
repoName: name,
|
|
690
|
+
repoPath: sessionRepoPath,
|
|
691
|
+
cwd,
|
|
692
|
+
root,
|
|
693
|
+
worktreeName,
|
|
694
|
+
branchName: branchName || worktreeName,
|
|
695
|
+
displayName: displayNameVal,
|
|
696
|
+
args,
|
|
697
|
+
configPath: CONFIG_PATH,
|
|
698
|
+
});
|
|
699
|
+
writeMeta(CONFIG_PATH, {
|
|
700
|
+
worktreePath: sessionRepoPath,
|
|
701
|
+
displayName: displayNameVal,
|
|
702
|
+
lastActivity: new Date().toISOString(),
|
|
703
|
+
branchName: branchName || worktreeName,
|
|
704
|
+
});
|
|
705
|
+
res.status(201).json(session);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
623
709
|
await execFileAsync('git', ['worktree', 'add', targetDir, resolvedBranch], { cwd: repoPath });
|
|
624
710
|
}
|
|
625
711
|
else if (branchName) {
|
|
@@ -641,6 +727,7 @@ async function main() {
|
|
|
641
727
|
const displayName = branchName || worktreeName;
|
|
642
728
|
const session = sessions.create({
|
|
643
729
|
type: 'worktree',
|
|
730
|
+
agent: resolvedAgent,
|
|
644
731
|
repoName: name,
|
|
645
732
|
repoPath: sessionRepoPath,
|
|
646
733
|
cwd,
|
|
@@ -648,7 +735,6 @@ async function main() {
|
|
|
648
735
|
worktreeName,
|
|
649
736
|
branchName: branchName || worktreeName,
|
|
650
737
|
displayName,
|
|
651
|
-
command: config.claudeCommand,
|
|
652
738
|
args,
|
|
653
739
|
configPath: CONFIG_PATH,
|
|
654
740
|
});
|
|
@@ -664,11 +750,12 @@ async function main() {
|
|
|
664
750
|
});
|
|
665
751
|
// POST /sessions/repo — start a session in the repo root (no worktree)
|
|
666
752
|
app.post('/sessions/repo', requireAuth, (req, res) => {
|
|
667
|
-
const { repoPath, repoName, continue: continueSession, claudeArgs } = req.body;
|
|
753
|
+
const { repoPath, repoName, continue: continueSession, claudeArgs, agent } = req.body;
|
|
668
754
|
if (!repoPath) {
|
|
669
755
|
res.status(400).json({ error: 'repoPath is required' });
|
|
670
756
|
return;
|
|
671
757
|
}
|
|
758
|
+
const resolvedAgent = agent || config.defaultAgent || 'claude';
|
|
672
759
|
// One repo session at a time
|
|
673
760
|
const existing = sessions.findRepoSession(repoPath);
|
|
674
761
|
if (existing) {
|
|
@@ -677,17 +764,17 @@ async function main() {
|
|
|
677
764
|
}
|
|
678
765
|
const name = repoName || repoPath.split('/').filter(Boolean).pop() || 'session';
|
|
679
766
|
const baseArgs = [...(config.claudeArgs || []), ...(claudeArgs || [])];
|
|
680
|
-
const args = continueSession ? [
|
|
767
|
+
const args = continueSession ? [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs] : [...baseArgs];
|
|
681
768
|
const roots = config.rootDirs || [];
|
|
682
769
|
const root = roots.find(function (r) { return repoPath.startsWith(r); }) || '';
|
|
683
770
|
const session = sessions.create({
|
|
684
771
|
type: 'repo',
|
|
772
|
+
agent: resolvedAgent,
|
|
685
773
|
repoName: name,
|
|
686
774
|
repoPath,
|
|
687
775
|
cwd: repoPath,
|
|
688
776
|
root,
|
|
689
777
|
displayName: name,
|
|
690
|
-
command: config.claudeCommand,
|
|
691
778
|
args,
|
|
692
779
|
});
|
|
693
780
|
res.status(201).json(session);
|
package/dist/server/sessions.js
CHANGED
|
@@ -4,6 +4,18 @@ import fs from 'node:fs';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { readMeta, writeMeta } from './config.js';
|
|
7
|
+
const AGENT_COMMANDS = {
|
|
8
|
+
claude: 'claude',
|
|
9
|
+
codex: 'codex',
|
|
10
|
+
};
|
|
11
|
+
const AGENT_YOLO_ARGS = {
|
|
12
|
+
claude: ['--dangerously-skip-permissions'],
|
|
13
|
+
codex: ['--full-auto'],
|
|
14
|
+
};
|
|
15
|
+
const AGENT_CONTINUE_ARGS = {
|
|
16
|
+
claude: ['--continue'],
|
|
17
|
+
codex: ['resume', '--last'],
|
|
18
|
+
};
|
|
7
19
|
// In-memory registry: id -> Session
|
|
8
20
|
const sessions = new Map();
|
|
9
21
|
const IDLE_TIMEOUT_MS = 5000;
|
|
@@ -11,13 +23,14 @@ let idleChangeCallback = null;
|
|
|
11
23
|
function onIdleChange(cb) {
|
|
12
24
|
idleChangeCallback = cb;
|
|
13
25
|
}
|
|
14
|
-
function create({ type, repoName, repoPath, cwd, root, worktreeName, branchName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
|
|
26
|
+
function create({ type, agent = 'claude', repoName, repoPath, cwd, root, worktreeName, branchName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
|
|
15
27
|
const id = crypto.randomBytes(8).toString('hex');
|
|
16
28
|
const createdAt = new Date().toISOString();
|
|
29
|
+
const resolvedCommand = command || AGENT_COMMANDS[agent];
|
|
17
30
|
// Strip CLAUDECODE env var to allow spawning claude inside a claude-managed server
|
|
18
31
|
const env = Object.assign({}, process.env);
|
|
19
32
|
delete env.CLAUDECODE;
|
|
20
|
-
const ptyProcess = pty.spawn(
|
|
33
|
+
const ptyProcess = pty.spawn(resolvedCommand, args, {
|
|
21
34
|
name: 'xterm-256color',
|
|
22
35
|
cols,
|
|
23
36
|
rows,
|
|
@@ -31,6 +44,7 @@ function create({ type, repoName, repoPath, cwd, root, worktreeName, branchName,
|
|
|
31
44
|
const session = {
|
|
32
45
|
id,
|
|
33
46
|
type: type || 'worktree',
|
|
47
|
+
agent,
|
|
34
48
|
root: root || '',
|
|
35
49
|
repoName: repoName || '',
|
|
36
50
|
repoPath,
|
|
@@ -70,6 +84,7 @@ function create({ type, repoName, repoPath, cwd, root, worktreeName, branchName,
|
|
|
70
84
|
}
|
|
71
85
|
}, IDLE_TIMEOUT_MS);
|
|
72
86
|
}
|
|
87
|
+
const continueArgs = AGENT_CONTINUE_ARGS[agent];
|
|
73
88
|
function attachHandlers(proc, canRetry) {
|
|
74
89
|
const spawnTime = Date.now();
|
|
75
90
|
proc.onData((data) => {
|
|
@@ -89,12 +104,12 @@ function create({ type, repoName, repoPath, cwd, root, worktreeName, branchName,
|
|
|
89
104
|
}
|
|
90
105
|
});
|
|
91
106
|
proc.onExit(({ exitCode }) => {
|
|
92
|
-
// If
|
|
107
|
+
// If continue args failed quickly, retry without them
|
|
93
108
|
if (canRetry && (Date.now() - spawnTime) < 3000 && exitCode !== 0) {
|
|
94
|
-
const retryArgs = args.filter(a => a
|
|
109
|
+
const retryArgs = args.filter(a => !continueArgs.includes(a));
|
|
95
110
|
scrollback.length = 0;
|
|
96
111
|
scrollbackBytes = 0;
|
|
97
|
-
const retryPty = pty.spawn(
|
|
112
|
+
const retryPty = pty.spawn(resolvedCommand, retryArgs, {
|
|
98
113
|
name: 'xterm-256color',
|
|
99
114
|
cols,
|
|
100
115
|
rows,
|
|
@@ -117,17 +132,18 @@ function create({ type, repoName, repoPath, cwd, root, worktreeName, branchName,
|
|
|
117
132
|
fs.rm(tmpDir, { recursive: true, force: true }, () => { });
|
|
118
133
|
});
|
|
119
134
|
}
|
|
120
|
-
attachHandlers(ptyProcess, args.includes(
|
|
121
|
-
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 };
|
|
135
|
+
attachHandlers(ptyProcess, continueArgs.some(a => args.includes(a)));
|
|
136
|
+
return { id, type: session.type, agent: session.agent, root: session.root, repoName: session.repoName, repoPath, worktreeName: session.worktreeName, branchName: session.branchName, displayName: session.displayName, pid: ptyProcess.pid, createdAt, lastActivity: createdAt, idle: false };
|
|
122
137
|
}
|
|
123
138
|
function get(id) {
|
|
124
139
|
return sessions.get(id);
|
|
125
140
|
}
|
|
126
141
|
function list() {
|
|
127
142
|
return Array.from(sessions.values())
|
|
128
|
-
.map(({ id, type, root, repoName, repoPath, worktreeName, branchName, displayName, createdAt, lastActivity, idle }) => ({
|
|
143
|
+
.map(({ id, type, agent, root, repoName, repoPath, worktreeName, branchName, displayName, createdAt, lastActivity, idle }) => ({
|
|
129
144
|
id,
|
|
130
145
|
type,
|
|
146
|
+
agent,
|
|
131
147
|
root,
|
|
132
148
|
repoName,
|
|
133
149
|
repoPath,
|
|
@@ -172,4 +188,4 @@ function write(id, data) {
|
|
|
172
188
|
function findRepoSession(repoPath) {
|
|
173
189
|
return list().find((s) => s.type === 'repo' && s.repoPath === repoPath);
|
|
174
190
|
}
|
|
175
|
-
export { create, get, list, kill, resize, updateDisplayName, write, onIdleChange, findRepoSession };
|
|
191
|
+
export { create, get, list, kill, resize, updateDisplayName, write, onIdleChange, findRepoSession, AGENT_COMMANDS, AGENT_YOLO_ARGS, AGENT_CONTINUE_ARGS };
|
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.
|
package/dist/test/config.test.js
CHANGED
|
@@ -40,6 +40,7 @@ test('loadConfig merges with defaults for missing fields', () => {
|
|
|
40
40
|
assert.deepEqual(config.repos, DEFAULTS.repos);
|
|
41
41
|
assert.equal(config.claudeCommand, DEFAULTS.claudeCommand);
|
|
42
42
|
assert.deepEqual(config.claudeArgs, DEFAULTS.claudeArgs);
|
|
43
|
+
assert.equal(config.defaultAgent, DEFAULTS.defaultAgent);
|
|
43
44
|
});
|
|
44
45
|
test('loadConfig throws if config file not found', () => {
|
|
45
46
|
const configPath = path.join(tmpDir, 'nonexistent.json');
|
|
@@ -59,6 +60,7 @@ test('DEFAULTS has expected keys and values', () => {
|
|
|
59
60
|
assert.deepEqual(DEFAULTS.repos, []);
|
|
60
61
|
assert.equal(DEFAULTS.claudeCommand, 'claude');
|
|
61
62
|
assert.deepEqual(DEFAULTS.claudeArgs, []);
|
|
63
|
+
assert.equal(DEFAULTS.defaultAgent, 'claude');
|
|
62
64
|
});
|
|
63
65
|
test('ensureMetaDir creates worktree-meta directory', () => {
|
|
64
66
|
const configPath = path.join(tmpDir, 'config.json');
|
|
@@ -264,4 +264,39 @@ describe('sessions', () => {
|
|
|
264
264
|
createdIds.push(result.id);
|
|
265
265
|
assert.strictEqual(result.branchName, '');
|
|
266
266
|
});
|
|
267
|
+
it('agent defaults to claude when not specified', () => {
|
|
268
|
+
const result = sessions.create({
|
|
269
|
+
repoName: 'test-repo',
|
|
270
|
+
repoPath: '/tmp',
|
|
271
|
+
command: '/bin/echo',
|
|
272
|
+
args: ['hello'],
|
|
273
|
+
});
|
|
274
|
+
createdIds.push(result.id);
|
|
275
|
+
assert.strictEqual(result.agent, 'claude');
|
|
276
|
+
});
|
|
277
|
+
it('agent is set when specified', () => {
|
|
278
|
+
const result = sessions.create({
|
|
279
|
+
repoName: 'test-repo',
|
|
280
|
+
repoPath: '/tmp',
|
|
281
|
+
agent: 'codex',
|
|
282
|
+
command: '/bin/echo',
|
|
283
|
+
args: ['hello'],
|
|
284
|
+
});
|
|
285
|
+
createdIds.push(result.id);
|
|
286
|
+
assert.strictEqual(result.agent, 'codex');
|
|
287
|
+
});
|
|
288
|
+
it('list includes agent field', () => {
|
|
289
|
+
const result = sessions.create({
|
|
290
|
+
repoName: 'test-repo',
|
|
291
|
+
repoPath: '/tmp',
|
|
292
|
+
agent: 'codex',
|
|
293
|
+
command: '/bin/echo',
|
|
294
|
+
args: ['hello'],
|
|
295
|
+
});
|
|
296
|
+
createdIds.push(result.id);
|
|
297
|
+
const list = sessions.list();
|
|
298
|
+
const session = list.find(s => s.id === result.id);
|
|
299
|
+
assert.ok(session);
|
|
300
|
+
assert.strictEqual(session.agent, 'codex');
|
|
301
|
+
});
|
|
267
302
|
});
|
|
@@ -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'];
|