claude-remote-cli 1.1.1 → 1.2.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/server/index.js +53 -2
- package/package.json +1 -1
- package/public/app.js +107 -0
- package/public/index.html +16 -0
- package/public/style.css +81 -0
package/dist/server/index.js
CHANGED
|
@@ -3,6 +3,8 @@ import http from 'node:http';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import readline from 'node:readline';
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
6
8
|
import express from 'express';
|
|
7
9
|
import cookieParser from 'cookie-parser';
|
|
8
10
|
import { loadConfig, saveConfig, DEFAULTS } from './config.js';
|
|
@@ -12,9 +14,10 @@ import { setupWebSocket } from './ws.js';
|
|
|
12
14
|
import { WorktreeWatcher } from './watcher.js';
|
|
13
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
16
|
const __dirname = path.dirname(__filename);
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
15
18
|
// When run via CLI bin, config lives in ~/.config/claude-remote-cli/
|
|
16
19
|
// When run directly (development), fall back to local config.json
|
|
17
|
-
const CONFIG_PATH = process.env.CLAUDE_REMOTE_CONFIG || path.join(__dirname, '..', 'config.json');
|
|
20
|
+
const CONFIG_PATH = process.env.CLAUDE_REMOTE_CONFIG || path.join(__dirname, '..', '..', 'config.json');
|
|
18
21
|
function parseTTL(ttl) {
|
|
19
22
|
if (typeof ttl !== 'string')
|
|
20
23
|
return 24 * 60 * 60 * 1000;
|
|
@@ -89,7 +92,7 @@ async function main() {
|
|
|
89
92
|
const app = express();
|
|
90
93
|
app.use(express.json());
|
|
91
94
|
app.use(cookieParser());
|
|
92
|
-
app.use(express.static(path.join(__dirname, '..', 'public')));
|
|
95
|
+
app.use(express.static(path.join(__dirname, '..', '..', 'public')));
|
|
93
96
|
const requireAuth = (req, res, next) => {
|
|
94
97
|
const token = req.cookies && req.cookies.token;
|
|
95
98
|
if (!token || !authenticatedTokens.has(token)) {
|
|
@@ -221,6 +224,54 @@ async function main() {
|
|
|
221
224
|
broadcastEvent('worktrees-changed');
|
|
222
225
|
res.json(config.rootDirs);
|
|
223
226
|
});
|
|
227
|
+
// DELETE /worktrees — remove a worktree, prune, and delete its branch
|
|
228
|
+
app.delete('/worktrees', requireAuth, async (req, res) => {
|
|
229
|
+
const { worktreePath, repoPath } = req.body;
|
|
230
|
+
if (!worktreePath || !repoPath) {
|
|
231
|
+
res.status(400).json({ error: 'worktreePath and repoPath are required' });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Validate the path is inside a .claude/worktrees/ directory
|
|
235
|
+
if (!worktreePath.includes(path.sep + '.claude' + path.sep + 'worktrees' + path.sep)) {
|
|
236
|
+
res.status(400).json({ error: 'Path is not inside a .claude/worktrees/ directory' });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Check no active session is using this worktree
|
|
240
|
+
const activeSessions = sessions.list();
|
|
241
|
+
const conflict = activeSessions.find(function (s) { return s.repoPath === worktreePath; });
|
|
242
|
+
if (conflict) {
|
|
243
|
+
res.status(409).json({ error: 'Close the active session first' });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// Derive branch name from worktree directory name
|
|
247
|
+
const branchName = worktreePath.split('/').pop() || '';
|
|
248
|
+
try {
|
|
249
|
+
// Remove the worktree (will fail if uncommitted changes — no --force)
|
|
250
|
+
await execFileAsync('git', ['worktree', 'remove', worktreePath], { cwd: repoPath });
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
const message = err instanceof Error ? err.message : 'Failed to remove worktree';
|
|
254
|
+
res.status(500).json({ error: message });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
// Prune stale worktree refs
|
|
259
|
+
await execFileAsync('git', ['worktree', 'prune'], { cwd: repoPath });
|
|
260
|
+
}
|
|
261
|
+
catch (_) {
|
|
262
|
+
// Non-fatal: prune failure doesn't block success
|
|
263
|
+
}
|
|
264
|
+
if (branchName) {
|
|
265
|
+
try {
|
|
266
|
+
// Delete the branch
|
|
267
|
+
await execFileAsync('git', ['branch', '-D', branchName], { cwd: repoPath });
|
|
268
|
+
}
|
|
269
|
+
catch (_) {
|
|
270
|
+
// Non-fatal: branch may not exist or may be checked out elsewhere
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
res.json({ ok: true });
|
|
274
|
+
});
|
|
224
275
|
// POST /sessions
|
|
225
276
|
app.post('/sessions', requireAuth, (req, res) => {
|
|
226
277
|
const { repoPath, repoName, worktreePath, claudeArgs } = req.body;
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -34,6 +34,37 @@
|
|
|
34
34
|
var sidebarRepoFilter = document.getElementById('sidebar-repo-filter');
|
|
35
35
|
var dialogRootSelect = document.getElementById('dialog-root-select');
|
|
36
36
|
var dialogRepoSelect = document.getElementById('dialog-repo-select');
|
|
37
|
+
var contextMenu = document.getElementById('context-menu');
|
|
38
|
+
var ctxDeleteWorktree = document.getElementById('ctx-delete-worktree');
|
|
39
|
+
var deleteWtDialog = document.getElementById('delete-worktree-dialog');
|
|
40
|
+
var deleteWtName = document.getElementById('delete-wt-name');
|
|
41
|
+
var deleteWtCancel = document.getElementById('delete-wt-cancel');
|
|
42
|
+
var deleteWtConfirm = document.getElementById('delete-wt-confirm');
|
|
43
|
+
|
|
44
|
+
// Context menu state
|
|
45
|
+
var contextMenuTarget = null; // stores { worktreePath, repoPath, name }
|
|
46
|
+
var longPressTimer = null;
|
|
47
|
+
var longPressFired = false;
|
|
48
|
+
|
|
49
|
+
function showContextMenu(x, y, wt) {
|
|
50
|
+
contextMenuTarget = { worktreePath: wt.path, repoPath: wt.repoPath, name: wt.name };
|
|
51
|
+
contextMenu.style.left = Math.min(x, window.innerWidth - 180) + 'px';
|
|
52
|
+
contextMenu.style.top = Math.min(y, window.innerHeight - 60) + 'px';
|
|
53
|
+
contextMenu.hidden = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hideContextMenu() {
|
|
57
|
+
contextMenu.hidden = true;
|
|
58
|
+
contextMenuTarget = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
document.addEventListener('click', function () {
|
|
62
|
+
hideContextMenu();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
document.addEventListener('keydown', function (e) {
|
|
66
|
+
if (e.key === 'Escape') hideContextMenu();
|
|
67
|
+
});
|
|
37
68
|
|
|
38
69
|
// Session / worktree / repo state
|
|
39
70
|
var cachedSessions = [];
|
|
@@ -409,10 +440,44 @@
|
|
|
409
440
|
|
|
410
441
|
li.appendChild(infoDiv);
|
|
411
442
|
|
|
443
|
+
// Click to resume (but not if context menu just opened or long-press fired)
|
|
412
444
|
li.addEventListener('click', function () {
|
|
445
|
+
if (longPressFired || !contextMenu.hidden) return;
|
|
413
446
|
startSession(wt.repoPath, wt.path);
|
|
414
447
|
});
|
|
415
448
|
|
|
449
|
+
// Right-click context menu (desktop)
|
|
450
|
+
li.addEventListener('contextmenu', function (e) {
|
|
451
|
+
e.preventDefault();
|
|
452
|
+
e.stopPropagation();
|
|
453
|
+
showContextMenu(e.clientX, e.clientY, wt);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Long-press context menu (mobile)
|
|
457
|
+
li.addEventListener('touchstart', function (e) {
|
|
458
|
+
longPressFired = false;
|
|
459
|
+
longPressTimer = setTimeout(function () {
|
|
460
|
+
longPressTimer = null;
|
|
461
|
+
longPressFired = true;
|
|
462
|
+
var touch = e.touches[0];
|
|
463
|
+
showContextMenu(touch.clientX, touch.clientY, wt);
|
|
464
|
+
}, 500);
|
|
465
|
+
}, { passive: true });
|
|
466
|
+
|
|
467
|
+
li.addEventListener('touchend', function () {
|
|
468
|
+
if (longPressTimer) {
|
|
469
|
+
clearTimeout(longPressTimer);
|
|
470
|
+
longPressTimer = null;
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
li.addEventListener('touchmove', function () {
|
|
475
|
+
if (longPressTimer) {
|
|
476
|
+
clearTimeout(longPressTimer);
|
|
477
|
+
longPressTimer = null;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
416
481
|
return li;
|
|
417
482
|
}
|
|
418
483
|
|
|
@@ -480,6 +545,48 @@
|
|
|
480
545
|
.catch(function () {});
|
|
481
546
|
}
|
|
482
547
|
|
|
548
|
+
// ── Delete Worktree ────────────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
ctxDeleteWorktree.addEventListener('click', function (e) {
|
|
551
|
+
e.stopPropagation();
|
|
552
|
+
hideContextMenu();
|
|
553
|
+
if (!contextMenuTarget) return;
|
|
554
|
+
deleteWtName.textContent = contextMenuTarget.name;
|
|
555
|
+
deleteWtDialog.showModal();
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
deleteWtCancel.addEventListener('click', function () {
|
|
559
|
+
deleteWtDialog.close();
|
|
560
|
+
contextMenuTarget = null;
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
deleteWtConfirm.addEventListener('click', function () {
|
|
564
|
+
if (!contextMenuTarget) return;
|
|
565
|
+
var target = contextMenuTarget;
|
|
566
|
+
deleteWtDialog.close();
|
|
567
|
+
contextMenuTarget = null;
|
|
568
|
+
|
|
569
|
+
fetch('/worktrees', {
|
|
570
|
+
method: 'DELETE',
|
|
571
|
+
headers: { 'Content-Type': 'application/json' },
|
|
572
|
+
body: JSON.stringify({
|
|
573
|
+
worktreePath: target.worktreePath,
|
|
574
|
+
repoPath: target.repoPath,
|
|
575
|
+
}),
|
|
576
|
+
})
|
|
577
|
+
.then(function (res) {
|
|
578
|
+
if (!res.ok) {
|
|
579
|
+
return res.json().then(function (data) {
|
|
580
|
+
alert(data.error || 'Failed to delete worktree');
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
// UI will auto-update via worktrees-changed WebSocket event
|
|
584
|
+
})
|
|
585
|
+
.catch(function () {
|
|
586
|
+
alert('Failed to delete worktree');
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
483
590
|
function highlightActiveSession() {
|
|
484
591
|
var items = sessionList.querySelectorAll('li');
|
|
485
592
|
items.forEach(function (li) {
|
package/public/index.html
CHANGED
|
@@ -122,6 +122,22 @@
|
|
|
122
122
|
</div>
|
|
123
123
|
</dialog>
|
|
124
124
|
|
|
125
|
+
<!-- Context Menu -->
|
|
126
|
+
<div id="context-menu" class="context-menu" hidden>
|
|
127
|
+
<button id="ctx-delete-worktree" class="context-menu-item">Delete worktree</button>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<!-- Delete Worktree Confirmation Dialog -->
|
|
131
|
+
<dialog id="delete-worktree-dialog">
|
|
132
|
+
<h2>Delete worktree?</h2>
|
|
133
|
+
<p class="delete-wt-warning">This will remove the worktree directory and delete its branch. This cannot be undone.</p>
|
|
134
|
+
<p class="delete-wt-name" id="delete-wt-name"></p>
|
|
135
|
+
<div class="dialog-actions">
|
|
136
|
+
<button id="delete-wt-cancel">Cancel</button>
|
|
137
|
+
<button id="delete-wt-confirm" class="btn-danger">Delete</button>
|
|
138
|
+
</div>
|
|
139
|
+
</dialog>
|
|
140
|
+
|
|
125
141
|
<script src="/vendor/xterm.js"></script>
|
|
126
142
|
<script src="/vendor/addon-fit.js"></script>
|
|
127
143
|
<script src="/app.js"></script>
|
package/public/style.css
CHANGED
|
@@ -737,6 +737,87 @@ dialog#settings-dialog h2 {
|
|
|
737
737
|
opacity: 0.85;
|
|
738
738
|
}
|
|
739
739
|
|
|
740
|
+
/* ===== Context Menu ===== */
|
|
741
|
+
.context-menu {
|
|
742
|
+
position: fixed;
|
|
743
|
+
z-index: 200;
|
|
744
|
+
background: var(--surface);
|
|
745
|
+
border: 1px solid var(--border);
|
|
746
|
+
border-radius: 8px;
|
|
747
|
+
padding: 4px;
|
|
748
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
749
|
+
min-width: 160px;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.context-menu-item {
|
|
753
|
+
display: block;
|
|
754
|
+
width: 100%;
|
|
755
|
+
padding: 10px 14px;
|
|
756
|
+
background: none;
|
|
757
|
+
border: none;
|
|
758
|
+
border-radius: 6px;
|
|
759
|
+
color: var(--text);
|
|
760
|
+
font-size: 0.85rem;
|
|
761
|
+
text-align: left;
|
|
762
|
+
cursor: pointer;
|
|
763
|
+
touch-action: manipulation;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.context-menu-item:hover,
|
|
767
|
+
.context-menu-item:active {
|
|
768
|
+
background: var(--bg);
|
|
769
|
+
color: var(--accent);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/* ===== Delete Worktree Dialog ===== */
|
|
773
|
+
dialog#delete-worktree-dialog {
|
|
774
|
+
background: var(--surface);
|
|
775
|
+
border: 1px solid var(--border);
|
|
776
|
+
border-radius: 12px;
|
|
777
|
+
color: var(--text);
|
|
778
|
+
padding: 1.5rem;
|
|
779
|
+
width: 90%;
|
|
780
|
+
max-width: 400px;
|
|
781
|
+
margin: auto;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
dialog#delete-worktree-dialog::backdrop {
|
|
785
|
+
background: rgba(0, 0, 0, 0.7);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
dialog#delete-worktree-dialog h2 {
|
|
789
|
+
font-size: 1.1rem;
|
|
790
|
+
margin-bottom: 0.75rem;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.delete-wt-warning {
|
|
794
|
+
font-size: 0.85rem;
|
|
795
|
+
color: var(--text-muted);
|
|
796
|
+
margin-bottom: 0.75rem;
|
|
797
|
+
line-height: 1.4;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.delete-wt-name {
|
|
801
|
+
font-size: 0.85rem;
|
|
802
|
+
font-family: monospace;
|
|
803
|
+
color: var(--accent);
|
|
804
|
+
padding: 8px 10px;
|
|
805
|
+
background: var(--bg);
|
|
806
|
+
border-radius: 6px;
|
|
807
|
+
margin-bottom: 1rem;
|
|
808
|
+
word-break: break-all;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.btn-danger {
|
|
812
|
+
background: #c0392b !important;
|
|
813
|
+
border-color: #c0392b !important;
|
|
814
|
+
color: #fff !important;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.btn-danger:active {
|
|
818
|
+
opacity: 0.85;
|
|
819
|
+
}
|
|
820
|
+
|
|
740
821
|
/* ===== Mobile Responsive ===== */
|
|
741
822
|
@media (max-width: 600px) {
|
|
742
823
|
#mobile-header {
|