claude-remote-cli 3.0.9 → 3.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/frontend/assets/index-BEffbpai.js +47 -0
- package/dist/frontend/assets/index-w5wJhB5f.css +32 -0
- package/dist/frontend/index.html +2 -2
- package/dist/server/sessions.js +8 -14
- package/dist/server/workspaces.js +151 -0
- package/dist/server/ws.js +44 -40
- package/dist/test/fs-browse.test.js +202 -0
- package/package.json +1 -1
- package/dist/frontend/assets/index-De_IzAmR.js +0 -47
- package/dist/frontend/assets/index-yTmvRrnt.css +0 -32
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, test, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { createWorkspaceRouter } from '../server/workspaces.js';
|
|
8
|
+
import { saveConfig, DEFAULTS } from '../server/config.js';
|
|
9
|
+
let tmpDir;
|
|
10
|
+
let configPath;
|
|
11
|
+
let server;
|
|
12
|
+
let baseUrl;
|
|
13
|
+
before(async () => {
|
|
14
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fs-browse-test-'));
|
|
15
|
+
configPath = path.join(tmpDir, 'config.json');
|
|
16
|
+
// Create a directory tree for testing
|
|
17
|
+
// tmpDir/
|
|
18
|
+
// browsable/
|
|
19
|
+
// visible-dir/
|
|
20
|
+
// nested/
|
|
21
|
+
// .hidden-dir/
|
|
22
|
+
// git-repo/
|
|
23
|
+
// .git/
|
|
24
|
+
// empty-dir/
|
|
25
|
+
// node_modules/
|
|
26
|
+
// file.txt
|
|
27
|
+
const browsable = path.join(tmpDir, 'browsable');
|
|
28
|
+
fs.mkdirSync(path.join(browsable, 'visible-dir', 'nested'), { recursive: true });
|
|
29
|
+
fs.mkdirSync(path.join(browsable, '.hidden-dir'), { recursive: true });
|
|
30
|
+
fs.mkdirSync(path.join(browsable, 'git-repo', '.git'), { recursive: true });
|
|
31
|
+
fs.mkdirSync(path.join(browsable, 'empty-dir'), { recursive: true });
|
|
32
|
+
fs.mkdirSync(path.join(browsable, 'node_modules'), { recursive: true });
|
|
33
|
+
fs.writeFileSync(path.join(browsable, 'file.txt'), 'not a directory');
|
|
34
|
+
// Create 110 dirs to test truncation
|
|
35
|
+
const manyDir = path.join(tmpDir, 'many');
|
|
36
|
+
fs.mkdirSync(manyDir);
|
|
37
|
+
for (let i = 0; i < 110; i++) {
|
|
38
|
+
fs.mkdirSync(path.join(manyDir, `dir-${String(i).padStart(3, '0')}`));
|
|
39
|
+
}
|
|
40
|
+
// Save a config so the router can load it
|
|
41
|
+
saveConfig(configPath, { ...DEFAULTS, workspaces: [] });
|
|
42
|
+
// Start a test server
|
|
43
|
+
const app = express();
|
|
44
|
+
app.use(express.json());
|
|
45
|
+
app.use('/workspaces', createWorkspaceRouter({ configPath }));
|
|
46
|
+
await new Promise((resolve) => {
|
|
47
|
+
server = app.listen(0, '127.0.0.1', () => resolve());
|
|
48
|
+
});
|
|
49
|
+
const addr = server.address();
|
|
50
|
+
if (typeof addr === 'object' && addr) {
|
|
51
|
+
baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
after(() => {
|
|
55
|
+
server?.close();
|
|
56
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
57
|
+
});
|
|
58
|
+
async function browse(query = {}) {
|
|
59
|
+
const params = new URLSearchParams(query);
|
|
60
|
+
const res = await fetch(`${baseUrl}/workspaces/browse?${params}`);
|
|
61
|
+
assert.equal(res.status, 200, `Expected 200 but got ${res.status}`);
|
|
62
|
+
return res.json();
|
|
63
|
+
}
|
|
64
|
+
describe('GET /workspaces/browse', () => {
|
|
65
|
+
test('lists directories in a given path', async () => {
|
|
66
|
+
const browsable = path.join(tmpDir, 'browsable');
|
|
67
|
+
const data = await browse({ path: browsable });
|
|
68
|
+
assert.equal(data.resolved, browsable);
|
|
69
|
+
const names = data.entries.map((e) => e.name);
|
|
70
|
+
// Should include visible directories but not files or denylisted dirs
|
|
71
|
+
assert.ok(names.includes('visible-dir'), 'should include visible-dir');
|
|
72
|
+
assert.ok(names.includes('git-repo'), 'should include git-repo');
|
|
73
|
+
assert.ok(names.includes('empty-dir'), 'should include empty-dir');
|
|
74
|
+
assert.ok(!names.includes('file.txt'), 'should exclude files');
|
|
75
|
+
assert.ok(!names.includes('node_modules'), 'should exclude node_modules');
|
|
76
|
+
});
|
|
77
|
+
test('hides dotfiles by default', async () => {
|
|
78
|
+
const browsable = path.join(tmpDir, 'browsable');
|
|
79
|
+
const data = await browse({ path: browsable });
|
|
80
|
+
const names = data.entries.map((e) => e.name);
|
|
81
|
+
assert.ok(!names.includes('.hidden-dir'), 'should exclude hidden dirs by default');
|
|
82
|
+
});
|
|
83
|
+
test('shows dotfiles when showHidden=true', async () => {
|
|
84
|
+
const browsable = path.join(tmpDir, 'browsable');
|
|
85
|
+
const data = await browse({ path: browsable, showHidden: 'true' });
|
|
86
|
+
const names = data.entries.map((e) => e.name);
|
|
87
|
+
assert.ok(names.includes('.hidden-dir'), 'should include hidden dirs when showHidden');
|
|
88
|
+
// .git should still be excluded (in denylist)
|
|
89
|
+
assert.ok(!names.includes('.git'), 'should still exclude .git');
|
|
90
|
+
});
|
|
91
|
+
test('filters by prefix', async () => {
|
|
92
|
+
const browsable = path.join(tmpDir, 'browsable');
|
|
93
|
+
const data = await browse({ path: browsable, prefix: 'vis' });
|
|
94
|
+
assert.equal(data.entries.length, 1);
|
|
95
|
+
assert.equal(data.entries[0].name, 'visible-dir');
|
|
96
|
+
});
|
|
97
|
+
test('prefix filter is case-insensitive', async () => {
|
|
98
|
+
const browsable = path.join(tmpDir, 'browsable');
|
|
99
|
+
const data = await browse({ path: browsable, prefix: 'VIS' });
|
|
100
|
+
assert.equal(data.entries.length, 1);
|
|
101
|
+
assert.equal(data.entries[0].name, 'visible-dir');
|
|
102
|
+
});
|
|
103
|
+
test('detects isGitRepo correctly', async () => {
|
|
104
|
+
const browsable = path.join(tmpDir, 'browsable');
|
|
105
|
+
const data = await browse({ path: browsable });
|
|
106
|
+
const gitRepo = data.entries.find((e) => e.name === 'git-repo');
|
|
107
|
+
const visibleDir = data.entries.find((e) => e.name === 'visible-dir');
|
|
108
|
+
assert.ok(gitRepo, 'git-repo entry should exist');
|
|
109
|
+
assert.equal(gitRepo.isGitRepo, true, 'git-repo should have isGitRepo=true');
|
|
110
|
+
assert.ok(visibleDir, 'visible-dir entry should exist');
|
|
111
|
+
assert.equal(visibleDir.isGitRepo, false, 'visible-dir should have isGitRepo=false');
|
|
112
|
+
});
|
|
113
|
+
test('detects hasChildren correctly', async () => {
|
|
114
|
+
const browsable = path.join(tmpDir, 'browsable');
|
|
115
|
+
const data = await browse({ path: browsable });
|
|
116
|
+
const visibleDir = data.entries.find((e) => e.name === 'visible-dir');
|
|
117
|
+
const emptyDir = data.entries.find((e) => e.name === 'empty-dir');
|
|
118
|
+
assert.ok(visibleDir, 'visible-dir entry should exist');
|
|
119
|
+
assert.equal(visibleDir.hasChildren, true, 'visible-dir should have children');
|
|
120
|
+
assert.ok(emptyDir, 'empty-dir entry should exist');
|
|
121
|
+
assert.equal(emptyDir.hasChildren, false, 'empty-dir should not have children');
|
|
122
|
+
});
|
|
123
|
+
test('truncates at 100 entries', async () => {
|
|
124
|
+
const manyDir = path.join(tmpDir, 'many');
|
|
125
|
+
const data = await browse({ path: manyDir });
|
|
126
|
+
assert.equal(data.entries.length, 100);
|
|
127
|
+
assert.equal(data.truncated, true);
|
|
128
|
+
assert.equal(data.total, 110);
|
|
129
|
+
});
|
|
130
|
+
test('sorts alphabetically case-insensitive', async () => {
|
|
131
|
+
const browsable = path.join(tmpDir, 'browsable');
|
|
132
|
+
const data = await browse({ path: browsable });
|
|
133
|
+
const names = data.entries.map((e) => e.name);
|
|
134
|
+
const sorted = [...names].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
135
|
+
assert.deepEqual(names, sorted, 'entries should be sorted alphabetically');
|
|
136
|
+
});
|
|
137
|
+
test('returns 400 for non-existent path', async () => {
|
|
138
|
+
const params = new URLSearchParams({ path: path.join(tmpDir, 'nonexistent') });
|
|
139
|
+
const res = await fetch(`${baseUrl}/workspaces/browse?${params}`);
|
|
140
|
+
assert.equal(res.status, 400);
|
|
141
|
+
});
|
|
142
|
+
test('returns 400 for file path', async () => {
|
|
143
|
+
const params = new URLSearchParams({ path: path.join(tmpDir, 'browsable', 'file.txt') });
|
|
144
|
+
const res = await fetch(`${baseUrl}/workspaces/browse?${params}`);
|
|
145
|
+
assert.equal(res.status, 400);
|
|
146
|
+
});
|
|
147
|
+
test('defaults to home directory when no path given', async () => {
|
|
148
|
+
const data = await browse();
|
|
149
|
+
assert.equal(data.resolved, os.homedir());
|
|
150
|
+
// Should have at least some entries (home dir is not empty)
|
|
151
|
+
assert.ok(data.entries.length > 0, 'home directory should have entries');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('POST /workspaces/bulk', () => {
|
|
155
|
+
test('adds multiple workspaces', async () => {
|
|
156
|
+
const dir1 = path.join(tmpDir, 'browsable', 'visible-dir');
|
|
157
|
+
const dir2 = path.join(tmpDir, 'browsable', 'empty-dir');
|
|
158
|
+
const res = await fetch(`${baseUrl}/workspaces/bulk`, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
161
|
+
body: JSON.stringify({ paths: [dir1, dir2] }),
|
|
162
|
+
});
|
|
163
|
+
assert.equal(res.status, 201);
|
|
164
|
+
const data = await res.json();
|
|
165
|
+
assert.equal(data.added.length, 2);
|
|
166
|
+
assert.equal(data.errors.length, 0);
|
|
167
|
+
});
|
|
168
|
+
test('rejects duplicate workspaces', async () => {
|
|
169
|
+
const dir1 = path.join(tmpDir, 'browsable', 'visible-dir');
|
|
170
|
+
const res = await fetch(`${baseUrl}/workspaces/bulk`, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
body: JSON.stringify({ paths: [dir1] }),
|
|
174
|
+
});
|
|
175
|
+
assert.equal(res.status, 201);
|
|
176
|
+
const data = await res.json();
|
|
177
|
+
assert.equal(data.added.length, 0);
|
|
178
|
+
assert.equal(data.errors.length, 1);
|
|
179
|
+
assert.ok(data.errors[0].error.includes('Already exists'));
|
|
180
|
+
});
|
|
181
|
+
test('returns 400 for empty paths array', async () => {
|
|
182
|
+
const res = await fetch(`${baseUrl}/workspaces/bulk`, {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
headers: { 'Content-Type': 'application/json' },
|
|
185
|
+
body: JSON.stringify({ paths: [] }),
|
|
186
|
+
});
|
|
187
|
+
assert.equal(res.status, 400);
|
|
188
|
+
});
|
|
189
|
+
test('handles mixed valid/invalid paths', async () => {
|
|
190
|
+
const validDir = path.join(tmpDir, 'browsable', 'git-repo');
|
|
191
|
+
const invalidDir = path.join(tmpDir, 'nonexistent');
|
|
192
|
+
const res = await fetch(`${baseUrl}/workspaces/bulk`, {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: { 'Content-Type': 'application/json' },
|
|
195
|
+
body: JSON.stringify({ paths: [validDir, invalidDir] }),
|
|
196
|
+
});
|
|
197
|
+
assert.equal(res.status, 201);
|
|
198
|
+
const data = await res.json();
|
|
199
|
+
assert.equal(data.added.length, 1);
|
|
200
|
+
assert.equal(data.errors.length, 1);
|
|
201
|
+
});
|
|
202
|
+
});
|