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.
@@ -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(); });
@@ -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 worktreeDir = path.join(repo.path, '.worktrees');
250
- let entries;
251
- try {
252
- entries = fs.readdirSync(worktreeDir, { withFileTypes: true });
253
- }
254
- catch (_) {
255
- continue;
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
- const wtPath = path.join(worktreeDir, entry.name);
261
- const meta = readMeta(CONFIG_PATH, wtPath);
262
- worktrees.push({
263
- name: entry.name,
264
- path: wtPath,
265
- repoName: repo.name,
266
- repoPath: repo.path,
267
- root: repo.root,
268
- displayName: meta ? meta.displayName : '',
269
- lastActivity: meta ? meta.lastActivity : '',
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
- // Validate the path is inside a .worktrees/ directory
319
- if (!worktreePath.includes(path.sep + '.worktrees' + path.sep)) {
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
- const execErr = err;
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
- const execErr = err;
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;
@@ -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
- const worktreeDir = path.join(repoPath, '.worktrees');
34
- if (fs.existsSync(worktreeDir)) {
35
- this._addWatch(worktreeDir);
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
- else {
38
- // Watch the repo root so we detect when .worktrees/ is first created
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-cli",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Remote web interface for Claude Code CLI sessions",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",