claude-remote-cli 2.0.0 → 2.1.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/bin/claude-remote-cli.js +80 -1
- package/dist/server/index.js +32 -31
- package/dist/server/watcher.js +16 -5
- package/package.json +1 -1
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
|
-
import { execFile } from 'node:child_process';
|
|
4
|
+
import { execFile, spawn } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import * as service from '../server/service.js';
|
|
8
8
|
import { DEFAULTS } from '../server/config.js';
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
function execErrorMessage(err, fallback) {
|
|
12
|
+
const e = err;
|
|
13
|
+
return (e.stderr || e.message || fallback).trimEnd();
|
|
14
|
+
}
|
|
11
15
|
// Parse CLI flags
|
|
12
16
|
const args = process.argv.slice(2);
|
|
13
17
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -19,12 +23,17 @@ Commands:
|
|
|
19
23
|
install Install as a background service (survives reboot)
|
|
20
24
|
uninstall Stop and remove the background service
|
|
21
25
|
status Show whether the service is running
|
|
26
|
+
worktree Manage git worktrees (wraps git worktree)
|
|
27
|
+
add [path] [-b branch] [--yolo] Create worktree and launch Claude
|
|
28
|
+
remove <path> Forward to git worktree remove
|
|
29
|
+
list Forward to git worktree list
|
|
22
30
|
|
|
23
31
|
Options:
|
|
24
32
|
--bg Shortcut: install and start as background service
|
|
25
33
|
--port <port> Override server port (default: 3456)
|
|
26
34
|
--host <host> Override bind address (default: 0.0.0.0)
|
|
27
35
|
--config <path> Path to config.json (default: ~/.config/claude-remote-cli/config.json)
|
|
36
|
+
--yolo With 'worktree add': pass --dangerously-skip-permissions to Claude
|
|
28
37
|
--version, -v Show version
|
|
29
38
|
--help, -h Show this help`);
|
|
30
39
|
process.exit(0);
|
|
@@ -87,6 +96,76 @@ if (command === 'update') {
|
|
|
87
96
|
}
|
|
88
97
|
process.exit(0);
|
|
89
98
|
}
|
|
99
|
+
if (command === 'worktree') {
|
|
100
|
+
const wtArgs = args.slice(1);
|
|
101
|
+
const subCommand = wtArgs[0];
|
|
102
|
+
if (!subCommand) {
|
|
103
|
+
console.error('Usage: claude-remote-cli worktree <add|remove|list> [options]');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
if (subCommand !== 'add') {
|
|
107
|
+
try {
|
|
108
|
+
const result = await execFileAsync('git', ['worktree', ...wtArgs]);
|
|
109
|
+
if (result.stdout)
|
|
110
|
+
console.log(result.stdout.trimEnd());
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
console.error(execErrorMessage(err, 'git worktree failed'));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
// Handle 'add' -- strip --yolo, determine path, forward to git, then launch claude
|
|
119
|
+
const hasYolo = wtArgs.includes('--yolo');
|
|
120
|
+
const gitWtArgs = wtArgs.filter(function (a) { return a !== '--yolo'; });
|
|
121
|
+
const addSubArgs = gitWtArgs.slice(1);
|
|
122
|
+
let targetDir;
|
|
123
|
+
const bIdx = gitWtArgs.indexOf('-b');
|
|
124
|
+
const branchForDefault = bIdx !== -1 && bIdx + 1 < gitWtArgs.length ? gitWtArgs[bIdx + 1] : undefined;
|
|
125
|
+
if (addSubArgs.length === 0 || addSubArgs[0].startsWith('-')) {
|
|
126
|
+
let repoRoot;
|
|
127
|
+
try {
|
|
128
|
+
const result = await execFileAsync('git', ['rev-parse', '--show-toplevel']);
|
|
129
|
+
repoRoot = result.stdout.trim();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
console.error('Not inside a git repository.');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
const dirName = branchForDefault
|
|
136
|
+
? branchForDefault.replace(/\//g, '-')
|
|
137
|
+
: 'worktree-' + Date.now().toString(36);
|
|
138
|
+
targetDir = path.join(repoRoot, '.worktrees', dirName);
|
|
139
|
+
gitWtArgs.splice(1, 0, targetDir);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
targetDir = path.resolve(addSubArgs[0]);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const result = await execFileAsync('git', ['worktree', ...gitWtArgs]);
|
|
146
|
+
if (result.stdout)
|
|
147
|
+
console.log(result.stdout.trimEnd());
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
console.error(execErrorMessage(err, 'git worktree add failed'));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
console.log(`Worktree created at ${targetDir}`);
|
|
154
|
+
const claudeArgs = [];
|
|
155
|
+
if (hasYolo)
|
|
156
|
+
claudeArgs.push('--dangerously-skip-permissions');
|
|
157
|
+
console.log(`Launching claude${hasYolo ? ' (yolo mode)' : ''} in ${targetDir}...`);
|
|
158
|
+
const child = spawn('claude', claudeArgs, {
|
|
159
|
+
cwd: targetDir,
|
|
160
|
+
stdio: 'inherit',
|
|
161
|
+
env: { ...process.env, CLAUDECODE: undefined },
|
|
162
|
+
});
|
|
163
|
+
child.on('exit', (code) => {
|
|
164
|
+
process.exit(code ?? 0);
|
|
165
|
+
});
|
|
166
|
+
// Block until child exits via the handler above
|
|
167
|
+
await new Promise(() => { });
|
|
168
|
+
}
|
|
90
169
|
if (command === 'install' || command === 'uninstall' || command === 'status' || args.includes('--bg')) {
|
|
91
170
|
if (command === 'uninstall') {
|
|
92
171
|
runServiceCommand(() => { service.uninstall(); });
|
package/dist/server/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { loadConfig, saveConfig, DEFAULTS, readMeta, writeMeta, ensureMetaDir }
|
|
|
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 } from './watcher.js';
|
|
15
|
+
import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath } 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);
|
|
@@ -59,6 +59,10 @@ async function getLatestVersion() {
|
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
function execErrorMessage(err, fallback) {
|
|
63
|
+
const e = err;
|
|
64
|
+
return (e.stderr || e.message || fallback).trim();
|
|
65
|
+
}
|
|
62
66
|
function parseTTL(ttl) {
|
|
63
67
|
if (typeof ttl !== 'string')
|
|
64
68
|
return 24 * 60 * 60 * 1000;
|
|
@@ -246,28 +250,30 @@ async function main() {
|
|
|
246
250
|
reposToScan = scanAllRepos(roots);
|
|
247
251
|
}
|
|
248
252
|
for (const repo of reposToScan) {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
for (const entry of entries) {
|
|
258
|
-
if (!entry.isDirectory())
|
|
253
|
+
for (const dir of WORKTREE_DIRS) {
|
|
254
|
+
const worktreeDir = path.join(repo.path, dir);
|
|
255
|
+
let entries;
|
|
256
|
+
try {
|
|
257
|
+
entries = fs.readdirSync(worktreeDir, { withFileTypes: true });
|
|
258
|
+
}
|
|
259
|
+
catch (_) {
|
|
259
260
|
continue;
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
261
|
+
}
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
if (!entry.isDirectory())
|
|
264
|
+
continue;
|
|
265
|
+
const wtPath = path.join(worktreeDir, entry.name);
|
|
266
|
+
const meta = readMeta(CONFIG_PATH, wtPath);
|
|
267
|
+
worktrees.push({
|
|
268
|
+
name: entry.name,
|
|
269
|
+
path: wtPath,
|
|
270
|
+
repoName: repo.name,
|
|
271
|
+
repoPath: repo.path,
|
|
272
|
+
root: repo.root,
|
|
273
|
+
displayName: meta ? meta.displayName : '',
|
|
274
|
+
lastActivity: meta ? meta.lastActivity : '',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
271
277
|
}
|
|
272
278
|
}
|
|
273
279
|
res.json(worktrees);
|
|
@@ -315,9 +321,8 @@ async function main() {
|
|
|
315
321
|
res.status(400).json({ error: 'worktreePath and repoPath are required' });
|
|
316
322
|
return;
|
|
317
323
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
res.status(400).json({ error: 'Path is not inside a .worktrees/ directory' });
|
|
324
|
+
if (!isValidWorktreePath(worktreePath)) {
|
|
325
|
+
res.status(400).json({ error: 'Path is not inside a worktree directory' });
|
|
321
326
|
return;
|
|
322
327
|
}
|
|
323
328
|
// Check no active session is using this worktree
|
|
@@ -335,9 +340,7 @@ async function main() {
|
|
|
335
340
|
await execFileAsync('git', ['worktree', 'remove', worktreePath], { cwd: repoPath });
|
|
336
341
|
}
|
|
337
342
|
catch (err) {
|
|
338
|
-
|
|
339
|
-
const message = (execErr.stderr || execErr.message || 'Failed to remove worktree').trim();
|
|
340
|
-
res.status(500).json({ error: message });
|
|
343
|
+
res.status(500).json({ error: execErrorMessage(err, 'Failed to remove worktree') });
|
|
341
344
|
return;
|
|
342
345
|
}
|
|
343
346
|
try {
|
|
@@ -427,9 +430,7 @@ async function main() {
|
|
|
427
430
|
}
|
|
428
431
|
}
|
|
429
432
|
catch (err) {
|
|
430
|
-
|
|
431
|
-
const message = (execErr.stderr || execErr.message || 'Failed to create worktree').trim();
|
|
432
|
-
res.status(500).json({ error: message });
|
|
433
|
+
res.status(500).json({ error: execErrorMessage(err, 'Failed to create worktree') });
|
|
433
434
|
return;
|
|
434
435
|
}
|
|
435
436
|
worktreeName = dirName;
|
package/dist/server/watcher.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
|
+
export const WORKTREE_DIRS = ['.worktrees', '.claude/worktrees'];
|
|
5
|
+
export function isValidWorktreePath(worktreePath) {
|
|
6
|
+
const resolved = path.resolve(worktreePath);
|
|
7
|
+
return WORKTREE_DIRS.some(function (dir) {
|
|
8
|
+
return resolved.includes(path.sep + dir + path.sep);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
4
11
|
export class WorktreeWatcher extends EventEmitter {
|
|
5
12
|
_watchers;
|
|
6
13
|
_debounceTimer;
|
|
@@ -30,12 +37,16 @@ export class WorktreeWatcher extends EventEmitter {
|
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
_watchRepo(repoPath) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
let anyWatched = false;
|
|
41
|
+
for (const dir of WORKTREE_DIRS) {
|
|
42
|
+
const worktreeDir = path.join(repoPath, dir);
|
|
43
|
+
if (fs.existsSync(worktreeDir)) {
|
|
44
|
+
this._addWatch(worktreeDir);
|
|
45
|
+
anyWatched = true;
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
|
-
|
|
38
|
-
// Watch
|
|
48
|
+
if (!anyWatched) {
|
|
49
|
+
// Watch repo root so we detect when either dir is first created
|
|
39
50
|
this._addWatch(repoPath);
|
|
40
51
|
}
|
|
41
52
|
}
|