claude-dev-server 1.0.2
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 +185 -0
- package/dist/assets/ttyd-bridge.js +61 -0
- package/dist/assets/ttyd-terminal.html +4 -0
- package/dist/chunk-DOLSA776.js +1166 -0
- package/dist/chunk-DOLSA776.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,1166 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { dirname, join, resolve } from 'path';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { WebSocketServer } from 'ws';
|
|
7
|
+
import { SourceMapConsumer } from 'source-map';
|
|
8
|
+
import { readFile } from 'fs/promises';
|
|
9
|
+
|
|
10
|
+
// src/universal/index.ts
|
|
11
|
+
function spawnClaudeCode(options) {
|
|
12
|
+
const port = 5e4 + Math.floor(Math.random() * 1e4);
|
|
13
|
+
const ttydProc = spawn("ttyd", [
|
|
14
|
+
"--port",
|
|
15
|
+
String(port),
|
|
16
|
+
"--interface",
|
|
17
|
+
"127.0.0.1",
|
|
18
|
+
"--writable",
|
|
19
|
+
options.claudePath,
|
|
20
|
+
...options.args
|
|
21
|
+
], {
|
|
22
|
+
cwd: options.cwd,
|
|
23
|
+
env: {
|
|
24
|
+
...process.env,
|
|
25
|
+
...options.env,
|
|
26
|
+
TERM: "xterm-256color",
|
|
27
|
+
FORCE_COLOR: "1"
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
ttydProc.on("exit", (code) => {
|
|
31
|
+
console.log(`[claude-dev-server] ttyd exited - code: ${code}`);
|
|
32
|
+
});
|
|
33
|
+
ttydProc.on("error", (err) => {
|
|
34
|
+
console.error(`[claude-dev-server] ttyd error: ${err.message}`);
|
|
35
|
+
});
|
|
36
|
+
return new Promise((resolve2, reject) => {
|
|
37
|
+
ttydProc.stdout?.on("data", (data) => {
|
|
38
|
+
const msg = data.toString();
|
|
39
|
+
console.log(`[ttyd] ${msg}`);
|
|
40
|
+
});
|
|
41
|
+
ttydProc.stderr?.on("data", (data) => {
|
|
42
|
+
const msg = data.toString();
|
|
43
|
+
console.error(`[ttyd stderr] ${msg}`);
|
|
44
|
+
});
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
resolve2({
|
|
47
|
+
wsUrl: `ws://127.0.0.1:${port}`,
|
|
48
|
+
process: ttydProc,
|
|
49
|
+
port
|
|
50
|
+
});
|
|
51
|
+
}, 500);
|
|
52
|
+
ttydProc.on("error", reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
var sourceMapCache = /* @__PURE__ */ new Map();
|
|
56
|
+
async function parseSourceMap(sourceMapUrl, content) {
|
|
57
|
+
try {
|
|
58
|
+
const consumer = await new SourceMapConsumer(content);
|
|
59
|
+
sourceMapCache.set(sourceMapUrl, consumer);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error("Failed to parse source map:", err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function findOriginalPosition(generatedFile, line, column) {
|
|
65
|
+
const consumer = sourceMapCache.get(generatedFile);
|
|
66
|
+
if (!consumer) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const position = consumer.originalPositionFor({ line, column });
|
|
71
|
+
if (position.source) {
|
|
72
|
+
return {
|
|
73
|
+
source: position.source,
|
|
74
|
+
line: position.line || 1,
|
|
75
|
+
column: position.column || 0,
|
|
76
|
+
name: position.name
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error("Failed to find original position:", err);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
function formatCodeContext(location) {
|
|
85
|
+
return `
|
|
86
|
+
\u{1F4CD} Code Location:
|
|
87
|
+
File: ${location.file}
|
|
88
|
+
Line: ${location.line}
|
|
89
|
+
Column: ${location.column}
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
function createWebSocketServer(options) {
|
|
93
|
+
let wss = null;
|
|
94
|
+
let port = 0;
|
|
95
|
+
let ttydInfo = null;
|
|
96
|
+
const start = async () => {
|
|
97
|
+
console.log("[claude-dev-server] Starting ttyd...");
|
|
98
|
+
ttydInfo = await spawnClaudeCode({
|
|
99
|
+
cwd: options.projectRoot,
|
|
100
|
+
claudePath: options.claudePath,
|
|
101
|
+
args: options.claudeArgs
|
|
102
|
+
});
|
|
103
|
+
console.log(`[claude-dev-server] ttyd running at ${ttydInfo.wsUrl}`);
|
|
104
|
+
return new Promise((resolve2, reject) => {
|
|
105
|
+
wss = new WebSocketServer({ port, host: "127.0.0.1" });
|
|
106
|
+
wss.on("listening", () => {
|
|
107
|
+
const address = wss.address();
|
|
108
|
+
if (address && typeof address === "object") {
|
|
109
|
+
port = address.port;
|
|
110
|
+
resolve2({ wsPort: port, ttydPort: ttydInfo.port });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
wss.on("error", (err) => {
|
|
114
|
+
reject(err);
|
|
115
|
+
});
|
|
116
|
+
wss.on("connection", (ws) => {
|
|
117
|
+
ws.on("message", async (message) => {
|
|
118
|
+
try {
|
|
119
|
+
const msg = JSON.parse(message.toString());
|
|
120
|
+
if (msg.type === "inspect") {
|
|
121
|
+
await handleInspect(msg, ws, options.projectRoot);
|
|
122
|
+
} else if (msg.type === "loadSourceMap") {
|
|
123
|
+
await handleLoadSourceMap(msg, ws, options.projectRoot);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
ws.send(JSON.stringify({
|
|
129
|
+
type: "ready",
|
|
130
|
+
ttydUrl: ttydInfo.wsUrl
|
|
131
|
+
}));
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
const stop = () => {
|
|
136
|
+
ttydInfo?.process.kill();
|
|
137
|
+
wss?.close();
|
|
138
|
+
};
|
|
139
|
+
return { start, stop };
|
|
140
|
+
}
|
|
141
|
+
async function handleLoadSourceMap(msg, ws, projectRoot) {
|
|
142
|
+
const { sourceMapUrl } = msg;
|
|
143
|
+
try {
|
|
144
|
+
const mapPath = resolve(projectRoot, sourceMapUrl.replace(/^\//, ""));
|
|
145
|
+
const content = await readFile(mapPath, "utf-8");
|
|
146
|
+
await parseSourceMap(sourceMapUrl, content);
|
|
147
|
+
ws.send(JSON.stringify({
|
|
148
|
+
type: "sourceMapLoaded",
|
|
149
|
+
sourceMapUrl,
|
|
150
|
+
success: true
|
|
151
|
+
}));
|
|
152
|
+
} catch (err) {
|
|
153
|
+
ws.send(JSON.stringify({
|
|
154
|
+
type: "sourceMapLoaded",
|
|
155
|
+
sourceMapUrl,
|
|
156
|
+
success: false,
|
|
157
|
+
error: err.message
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function handleInspect(msg, ws, projectRoot) {
|
|
162
|
+
const { url, line, column, sourceMapUrl } = msg;
|
|
163
|
+
let location = null;
|
|
164
|
+
if (sourceMapUrl) {
|
|
165
|
+
const original = await findOriginalPosition(sourceMapUrl, line, column);
|
|
166
|
+
if (original) {
|
|
167
|
+
location = {
|
|
168
|
+
file: resolve(projectRoot, original.source),
|
|
169
|
+
line: original.line,
|
|
170
|
+
column: original.column
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!location) {
|
|
175
|
+
const match = url.match(/\/@fs\/(.+?)(?:\?|$)|\/@vite\/(.+?)(?:\?|$)/);
|
|
176
|
+
if (match) {
|
|
177
|
+
location = {
|
|
178
|
+
file: decodeURIComponent(match[1] || match[2]),
|
|
179
|
+
line,
|
|
180
|
+
column
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
ws.send(JSON.stringify({
|
|
185
|
+
type: "inspectResult",
|
|
186
|
+
location: location ? {
|
|
187
|
+
file: location.file,
|
|
188
|
+
line: location.line,
|
|
189
|
+
column: location.column,
|
|
190
|
+
context: formatCodeContext(location)
|
|
191
|
+
} : null
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/client/injection.js
|
|
196
|
+
var CLIENT_STYLES = `
|
|
197
|
+
<style id="claude-dev-server-styles">
|
|
198
|
+
.claude-dev-server-toggle {
|
|
199
|
+
position: fixed;
|
|
200
|
+
top: 20px;
|
|
201
|
+
right: 20px;
|
|
202
|
+
width: 44px;
|
|
203
|
+
height: 44px;
|
|
204
|
+
background: #1e1e1e;
|
|
205
|
+
border: none;
|
|
206
|
+
border-radius: 50%;
|
|
207
|
+
box-shadow: 0 2px 12px rgba(0,0,0,0.3);
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
z-index: 999999;
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
justify-content: center;
|
|
213
|
+
font-size: 20px;
|
|
214
|
+
transition: transform 0.2s, background 0.2s;
|
|
215
|
+
}
|
|
216
|
+
.claude-dev-server-toggle:hover {
|
|
217
|
+
transform: scale(1.1);
|
|
218
|
+
background: #2d2d2d;
|
|
219
|
+
}
|
|
220
|
+
.claude-dev-server-toggle.hidden {
|
|
221
|
+
display: none;
|
|
222
|
+
}
|
|
223
|
+
.claude-dev-server-inspect-btn {
|
|
224
|
+
position: fixed;
|
|
225
|
+
top: 74px;
|
|
226
|
+
right: 20px;
|
|
227
|
+
width: 44px;
|
|
228
|
+
height: 44px;
|
|
229
|
+
background: #1e1e1e;
|
|
230
|
+
border: none;
|
|
231
|
+
border-radius: 50%;
|
|
232
|
+
box-shadow: 0 2px 12px rgba(0,0,0,0.3);
|
|
233
|
+
cursor: pointer;
|
|
234
|
+
z-index: 999999;
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
justify-content: center;
|
|
238
|
+
font-size: 16px;
|
|
239
|
+
transition: transform 0.2s, background 0.2s;
|
|
240
|
+
}
|
|
241
|
+
.claude-dev-server-inspect-btn:hover {
|
|
242
|
+
transform: scale(1.1);
|
|
243
|
+
background: #2d2d2d;
|
|
244
|
+
}
|
|
245
|
+
.claude-dev-server-inspect-btn.active {
|
|
246
|
+
background: #007acc;
|
|
247
|
+
}
|
|
248
|
+
.claude-dev-server-inspect-btn.hidden {
|
|
249
|
+
display: none;
|
|
250
|
+
}
|
|
251
|
+
.claude-dev-server-toggle svg,
|
|
252
|
+
.claude-dev-server-inspect-btn svg {
|
|
253
|
+
width: 20px;
|
|
254
|
+
height: 20px;
|
|
255
|
+
flex-shrink: 0;
|
|
256
|
+
}
|
|
257
|
+
.claude-dev-server-panel {
|
|
258
|
+
position: fixed;
|
|
259
|
+
top: 0;
|
|
260
|
+
right: 0;
|
|
261
|
+
width: 650px;
|
|
262
|
+
height: 100vh;
|
|
263
|
+
background: #1e1e1e;
|
|
264
|
+
color: #d4d4d4;
|
|
265
|
+
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Consolas', monospace;
|
|
266
|
+
font-size: 13px;
|
|
267
|
+
box-shadow: -4px 0 20px rgba(0,0,0,0.3);
|
|
268
|
+
transform: translateX(100%);
|
|
269
|
+
transition: transform 0.2s ease;
|
|
270
|
+
z-index: 999999;
|
|
271
|
+
display: flex;
|
|
272
|
+
flex-direction: column;
|
|
273
|
+
}
|
|
274
|
+
.claude-dev-server-panel.open {
|
|
275
|
+
transform: translateX(0);
|
|
276
|
+
}
|
|
277
|
+
.claude-dev-server-header {
|
|
278
|
+
padding: 8px 12px;
|
|
279
|
+
background: #2d2d2d;
|
|
280
|
+
border-bottom: 1px solid #3e3e3e;
|
|
281
|
+
display: flex;
|
|
282
|
+
justify-content: space-between;
|
|
283
|
+
align-items: center;
|
|
284
|
+
user-select: none;
|
|
285
|
+
}
|
|
286
|
+
.claude-dev-server-title {
|
|
287
|
+
font-weight: 600;
|
|
288
|
+
color: #fff;
|
|
289
|
+
display: flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
gap: 8px;
|
|
292
|
+
font-size: 13px;
|
|
293
|
+
}
|
|
294
|
+
.claude-dev-server-title::before {
|
|
295
|
+
content: '\u{1F916}';
|
|
296
|
+
font-size: 14px;
|
|
297
|
+
}
|
|
298
|
+
.claude-dev-server-actions {
|
|
299
|
+
display: flex;
|
|
300
|
+
gap: 6px;
|
|
301
|
+
}
|
|
302
|
+
.claude-dev-server-btn {
|
|
303
|
+
background: #3e3e3e;
|
|
304
|
+
border: none;
|
|
305
|
+
color: #d4d4d4;
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
font-family: inherit;
|
|
308
|
+
font-size: 11px;
|
|
309
|
+
padding: 4px 10px;
|
|
310
|
+
border-radius: 3px;
|
|
311
|
+
transition: background 0.15s;
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: center;
|
|
314
|
+
gap: 4px;
|
|
315
|
+
}
|
|
316
|
+
.claude-dev-server-btn:hover {
|
|
317
|
+
background: #4e4e4e;
|
|
318
|
+
}
|
|
319
|
+
.claude-dev-server-btn.active {
|
|
320
|
+
background: #007acc;
|
|
321
|
+
color: #fff;
|
|
322
|
+
}
|
|
323
|
+
.claude-dev-server-close {
|
|
324
|
+
background: none;
|
|
325
|
+
border: none;
|
|
326
|
+
color: #858585;
|
|
327
|
+
cursor: pointer;
|
|
328
|
+
font-size: 18px;
|
|
329
|
+
padding: 0;
|
|
330
|
+
width: 20px;
|
|
331
|
+
height: 20px;
|
|
332
|
+
display: flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
justify-content: center;
|
|
335
|
+
border-radius: 3px;
|
|
336
|
+
transition: background 0.15s, color 0.15s;
|
|
337
|
+
}
|
|
338
|
+
.claude-dev-server-close:hover {
|
|
339
|
+
background: #3e3e3e;
|
|
340
|
+
color: #fff;
|
|
341
|
+
}
|
|
342
|
+
.claude-dev-server-terminal {
|
|
343
|
+
flex: 1;
|
|
344
|
+
overflow: hidden;
|
|
345
|
+
position: relative;
|
|
346
|
+
background: #000;
|
|
347
|
+
}
|
|
348
|
+
.claude-dev-server-terminal iframe {
|
|
349
|
+
width: 100%;
|
|
350
|
+
height: 100%;
|
|
351
|
+
border: none;
|
|
352
|
+
}
|
|
353
|
+
.claude-dev-server-inspect-overlay {
|
|
354
|
+
position: fixed;
|
|
355
|
+
top: 0;
|
|
356
|
+
left: 0;
|
|
357
|
+
right: 0;
|
|
358
|
+
bottom: 0;
|
|
359
|
+
pointer-events: none;
|
|
360
|
+
z-index: 999998;
|
|
361
|
+
display: none;
|
|
362
|
+
}
|
|
363
|
+
.claude-dev-server-inspect-overlay.active {
|
|
364
|
+
display: block;
|
|
365
|
+
}
|
|
366
|
+
.claude-dev-server-highlight {
|
|
367
|
+
position: absolute;
|
|
368
|
+
border: 2px solid #007acc;
|
|
369
|
+
background: rgba(0, 122, 204, 0.1);
|
|
370
|
+
pointer-events: none;
|
|
371
|
+
transition: all 0.1s ease;
|
|
372
|
+
}
|
|
373
|
+
.claude-dev-server-highlight::after {
|
|
374
|
+
content: attr(data-element);
|
|
375
|
+
position: absolute;
|
|
376
|
+
top: -20px;
|
|
377
|
+
left: 0;
|
|
378
|
+
background: #007acc;
|
|
379
|
+
color: #fff;
|
|
380
|
+
font-size: 10px;
|
|
381
|
+
padding: 2px 4px;
|
|
382
|
+
border-radius: 2px 2px 0 0;
|
|
383
|
+
font-family: monospace;
|
|
384
|
+
white-space: nowrap;
|
|
385
|
+
}
|
|
386
|
+
/* Responsive layout for portrait mode */
|
|
387
|
+
@media (orientation: portrait) {
|
|
388
|
+
.claude-dev-server-toggle {
|
|
389
|
+
top: auto;
|
|
390
|
+
bottom: 20px;
|
|
391
|
+
right: 10px;
|
|
392
|
+
width: 36px;
|
|
393
|
+
height: 36px;
|
|
394
|
+
}
|
|
395
|
+
.claude-dev-server-inspect-btn {
|
|
396
|
+
top: auto;
|
|
397
|
+
bottom: 66px;
|
|
398
|
+
right: 10px;
|
|
399
|
+
width: 36px;
|
|
400
|
+
height: 36px;
|
|
401
|
+
}
|
|
402
|
+
.claude-dev-server-panel {
|
|
403
|
+
top: auto;
|
|
404
|
+
bottom: 0;
|
|
405
|
+
right: 0;
|
|
406
|
+
left: 0;
|
|
407
|
+
width: 100%;
|
|
408
|
+
height: 80vh;
|
|
409
|
+
transform: translateY(100%);
|
|
410
|
+
box-shadow: 0 -4px 20px rgba(0,0,0,0.3);
|
|
411
|
+
}
|
|
412
|
+
.claude-dev-server-panel.open {
|
|
413
|
+
transform: translateY(0);
|
|
414
|
+
}
|
|
415
|
+
.claude-dev-server-toggle svg,
|
|
416
|
+
.claude-dev-server-inspect-btn svg {
|
|
417
|
+
width: 16px;
|
|
418
|
+
height: 16px;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
</style>
|
|
422
|
+
`;
|
|
423
|
+
var CLIENT_SCRIPT = `
|
|
424
|
+
(() => {
|
|
425
|
+
let ws = null
|
|
426
|
+
let panel = null
|
|
427
|
+
let toggleBtn = null
|
|
428
|
+
let inspectBtn = null // \u60AC\u6D6E inspect \u6309\u94AE
|
|
429
|
+
let terminalContainer = null
|
|
430
|
+
let terminalIframe = null
|
|
431
|
+
let overlay = null
|
|
432
|
+
let isOpen = false
|
|
433
|
+
let isInspectMode = false
|
|
434
|
+
let ttydWsUrl = null
|
|
435
|
+
|
|
436
|
+
// Fetch the WebSocket port from the server
|
|
437
|
+
async function getWsPort() {
|
|
438
|
+
const res = await fetch('/@claude-port')
|
|
439
|
+
const data = await res.json()
|
|
440
|
+
return data.port
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function initWhenReady() {
|
|
444
|
+
try {
|
|
445
|
+
const port = await getWsPort()
|
|
446
|
+
connect(port)
|
|
447
|
+
} catch (err) {
|
|
448
|
+
console.error('[Claude Dev Server] Failed to get port:', err)
|
|
449
|
+
// Retry after 1 second
|
|
450
|
+
setTimeout(initWhenReady, 1000)
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
createToggleBtn()
|
|
454
|
+
createInspectBtn()
|
|
455
|
+
createOverlay()
|
|
456
|
+
createPanel()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function createToggleBtn() {
|
|
460
|
+
if (toggleBtn) return
|
|
461
|
+
toggleBtn = document.createElement('button')
|
|
462
|
+
toggleBtn.className = 'claude-dev-server-toggle'
|
|
463
|
+
toggleBtn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M11.376 24L10.776 23.544L10.44 22.8L10.776 21.312L11.16 19.392L11.472 17.856L11.76 15.96L11.928 15.336L11.904 15.288L11.784 15.312L10.344 17.28L8.16 20.232L6.432 22.056L6.024 22.224L5.304 21.864L5.376 21.192L5.784 20.616L8.16 17.568L9.6 15.672L10.536 14.592L10.512 14.448H10.464L4.128 18.576L3 18.72L2.496 18.264L2.568 17.52L2.808 17.28L4.704 15.96L9.432 13.32L9.504 13.08L9.432 12.96H9.192L8.4 12.912L5.712 12.84L3.384 12.744L1.104 12.624L0.528 12.504L0 11.784L0.048 11.424L0.528 11.112L1.224 11.16L2.736 11.28L5.016 11.424L6.672 11.52L9.12 11.784H9.504L9.552 11.616L9.432 11.52L9.336 11.424L6.96 9.84L4.416 8.16L3.072 7.176L2.352 6.672L1.992 6.216L1.848 5.208L2.496 4.488L3.384 4.56L3.6 4.608L4.488 5.304L6.384 6.768L8.88 8.616L9.24 8.904L9.408 8.808V8.736L9.24 8.472L7.896 6.024L6.456 3.528L5.808 2.496L5.64 1.872C5.576 1.656 5.544 1.416 5.544 1.152L6.288 0.144001L6.696 0L7.704 0.144001L8.112 0.504001L8.736 1.92L9.72 4.152L11.28 7.176L11.736 8.088L11.976 8.904L12.072 9.168H12.24V9.024L12.36 7.296L12.6 5.208L12.84 2.52L12.912 1.752L13.296 0.840001L14.04 0.360001L14.616 0.624001L15.096 1.32L15.024 1.752L14.76 3.6L14.184 6.504L13.824 8.472H14.04L14.28 8.208L15.264 6.912L16.92 4.848L17.64 4.032L18.504 3.12L19.056 2.688H20.088L20.832 3.816L20.496 4.992L19.44 6.336L18.552 7.464L17.28 9.168L16.512 10.536L16.584 10.632H16.752L19.608 10.008L21.168 9.744L22.992 9.432L23.832 9.816L23.928 10.2L23.592 11.016L21.624 11.496L19.32 11.952L15.888 12.768L15.84 12.792L15.888 12.864L17.424 13.008L18.096 13.056H19.728L22.752 13.272L23.544 13.8L24 14.424L23.928 14.928L22.704 15.528L21.072 15.144L17.232 14.232L15.936 13.92H15.744V14.016L16.848 15.096L18.84 16.896L21.36 19.224L21.48 19.8L21.168 20.28L20.832 20.232L18.624 18.552L17.76 17.808L15.84 16.2H15.72V16.368L16.152 17.016L18.504 20.544L18.624 21.624L18.456 21.96L17.832 22.176L17.184 22.056L15.792 20.136L14.376 17.952L13.224 16.008L13.104 16.104L12.408 23.352L12.096 23.712L11.376 24Z" fill="#d97757"/></svg>'
|
|
464
|
+
toggleBtn.title = 'Open Claude Code (Cmd/Ctrl + \`)'
|
|
465
|
+
toggleBtn.setAttribute('data-dev-tool', 'true')
|
|
466
|
+
toggleBtn.addEventListener('click', () => togglePanel(true))
|
|
467
|
+
document.body.appendChild(toggleBtn)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function createInspectBtn() {
|
|
471
|
+
if (inspectBtn) return
|
|
472
|
+
inspectBtn = document.createElement('button')
|
|
473
|
+
inspectBtn.className = 'claude-dev-server-inspect-btn'
|
|
474
|
+
inspectBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 11V4a2 2 0 00-2-2H4a2 2 0 00-2 2v13a2 2 0 002 2h7"/><path d="M12 12l4.166 10 1.48-4.355L22 16.166 12 12z"/><path d="M18 18l3 3"/></svg>'
|
|
475
|
+
inspectBtn.title = 'Inspect Element (Cmd/Ctrl + Shift + I)'
|
|
476
|
+
inspectBtn.setAttribute('data-dev-tool', 'true')
|
|
477
|
+
inspectBtn.addEventListener('click', () => {
|
|
478
|
+
if (isInspectMode) {
|
|
479
|
+
disableInspectMode()
|
|
480
|
+
} else {
|
|
481
|
+
// \u70B9\u51FB Inspect \u65F6\u5148\u6536\u8D77\u9762\u677F
|
|
482
|
+
if (isOpen) {
|
|
483
|
+
togglePanel(false)
|
|
484
|
+
}
|
|
485
|
+
enableInspectMode()
|
|
486
|
+
}
|
|
487
|
+
})
|
|
488
|
+
document.body.appendChild(inspectBtn)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function createOverlay() {
|
|
492
|
+
if (overlay) return
|
|
493
|
+
overlay = document.createElement('div')
|
|
494
|
+
overlay.className = 'claude-dev-server-inspect-overlay'
|
|
495
|
+
document.body.appendChild(overlay)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function highlightElement(el) {
|
|
499
|
+
if (!overlay) return
|
|
500
|
+
overlay.innerHTML = ''
|
|
501
|
+
const rect = el.getBoundingClientRect()
|
|
502
|
+
const highlight = document.createElement('div')
|
|
503
|
+
highlight.className = 'claude-dev-server-highlight'
|
|
504
|
+
highlight.style.top = rect.top + 'px'
|
|
505
|
+
highlight.style.left = rect.left + 'px'
|
|
506
|
+
highlight.style.width = rect.width + 'px'
|
|
507
|
+
highlight.style.height = rect.height + 'px'
|
|
508
|
+
const className = el.className ? String(el.className) : ''
|
|
509
|
+
highlight.dataset.element = el.tagName.toLowerCase() +
|
|
510
|
+
(el.id ? '#' + el.id : '') +
|
|
511
|
+
(className ? '.' + className.split(' ').join('.') : '')
|
|
512
|
+
overlay.appendChild(highlight)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function getSourceLocation(el) {
|
|
516
|
+
let sourceFile = null
|
|
517
|
+
let sourceLine = 1
|
|
518
|
+
let sourceColumn = 1
|
|
519
|
+
|
|
520
|
+
// Try to get original line number using source map
|
|
521
|
+
async function getOriginalLine(generatedFile, generatedLine, generatedColumn) {
|
|
522
|
+
try {
|
|
523
|
+
// Find the script tag that corresponds to this file
|
|
524
|
+
const scripts = Array.from(document.querySelectorAll('script[src]'))
|
|
525
|
+
for (const script of scripts) {
|
|
526
|
+
const src = script.getAttribute('src')
|
|
527
|
+
if (src && (src.includes('/src/') || src.includes('/app.') || src.includes('/main.'))) {
|
|
528
|
+
// Try to fetch and parse the source map
|
|
529
|
+
const response = await fetch(src + '.map')
|
|
530
|
+
if (response.ok) {
|
|
531
|
+
const map = await response.json()
|
|
532
|
+
// Use the source map to find the original position
|
|
533
|
+
const consumer = await new (window.SourceMap || window.sourceMap).SourceMapConsumer(map)
|
|
534
|
+
const original = consumer.originalPositionFor({
|
|
535
|
+
line: generatedLine,
|
|
536
|
+
column: generatedColumn
|
|
537
|
+
})
|
|
538
|
+
consumer.destroy()
|
|
539
|
+
if (original.source && original.line !== null) {
|
|
540
|
+
return { line: original.line, column: original.column || 1 }
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} catch (e) {
|
|
546
|
+
console.log('[Claude Dev Server] Source map lookup failed:', e.message)
|
|
547
|
+
}
|
|
548
|
+
return null
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Try React DevTools - handle React 18's randomized suffix
|
|
552
|
+
const elKeys = Object.keys(el)
|
|
553
|
+
let fiberKey = elKeys.find(k => k === '__reactFiber__' || k === '__reactInternalInstance' || k.indexOf('__reactFiber') === 0)
|
|
554
|
+
let propsKey = elKeys.find(k => k === '__reactProps__' || k.indexOf('__reactProps') === 0)
|
|
555
|
+
|
|
556
|
+
if (fiberKey) {
|
|
557
|
+
const fiber = el[fiberKey]
|
|
558
|
+
console.log('[Claude Dev Server] Found fiber at key:', fiberKey)
|
|
559
|
+
|
|
560
|
+
// Try to get _debugSource from different locations
|
|
561
|
+
const debugSource = fiber._debugSource || fiber.elementType?._debugSource || fiber.type?._debugSource || fiber.alternate?._debugSource
|
|
562
|
+
if (debugSource && debugSource.fileName) {
|
|
563
|
+
sourceFile = debugSource.fileName
|
|
564
|
+
sourceLine = debugSource.lineNumber || 1
|
|
565
|
+
sourceColumn = debugSource.columnNumber || 1
|
|
566
|
+
|
|
567
|
+
// Try to use source map to get the original line number
|
|
568
|
+
const original = await getOriginalLine(sourceFile, sourceLine, sourceColumn)
|
|
569
|
+
if (original) {
|
|
570
|
+
sourceLine = original.line
|
|
571
|
+
sourceColumn = original.column
|
|
572
|
+
console.log('[Claude Dev Server] Original line from source map:', sourceLine)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Convert absolute path to relative path using project root
|
|
576
|
+
if (sourceFile.startsWith('/')) {
|
|
577
|
+
const projectRoot = window.__CLAUDE_PROJECT_ROOT__
|
|
578
|
+
if (projectRoot && sourceFile.startsWith(projectRoot)) {
|
|
579
|
+
sourceFile = sourceFile.substring(projectRoot.length + 1)
|
|
580
|
+
if (sourceFile.startsWith('/')) {
|
|
581
|
+
sourceFile = sourceFile.substring(1)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
console.log('[Claude Dev Server] Found React source:', sourceFile, sourceLine)
|
|
586
|
+
} else {
|
|
587
|
+
// Try going up the fiber tree
|
|
588
|
+
let currentFiber = fiber
|
|
589
|
+
let depth = 0
|
|
590
|
+
while (currentFiber && depth < 20) {
|
|
591
|
+
const ds = currentFiber._debugSource || currentFiber.elementType?._debugSource || currentFiber.type?._debugSource
|
|
592
|
+
if (ds && ds.fileName) {
|
|
593
|
+
sourceFile = ds.fileName
|
|
594
|
+
sourceLine = ds.lineNumber || 1
|
|
595
|
+
sourceColumn = ds.columnNumber || 1
|
|
596
|
+
// Convert absolute path to relative path using project root
|
|
597
|
+
if (sourceFile.startsWith('/')) {
|
|
598
|
+
const projectRoot = window.__CLAUDE_PROJECT_ROOT__
|
|
599
|
+
if (projectRoot && sourceFile.startsWith(projectRoot)) {
|
|
600
|
+
sourceFile = sourceFile.substring(projectRoot.length + 1)
|
|
601
|
+
if (sourceFile.startsWith('/')) {
|
|
602
|
+
sourceFile = sourceFile.substring(1)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
console.log('[Claude Dev Server] Found React source at depth', depth, ':', sourceFile, sourceLine)
|
|
607
|
+
break
|
|
608
|
+
}
|
|
609
|
+
currentFiber = currentFiber.return || currentFiber.alternate
|
|
610
|
+
depth++
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Try Vue component
|
|
616
|
+
if (!sourceFile) {
|
|
617
|
+
const vueComponent = el.__vueParentComponent || el.__vnode
|
|
618
|
+
if (vueComponent) {
|
|
619
|
+
const type = vueComponent.type || vueComponent.component
|
|
620
|
+
if (type && type.__file) {
|
|
621
|
+
sourceFile = type.__file
|
|
622
|
+
console.log('[Claude Dev Server] Found Vue source:', sourceFile)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Try Vite's HMR source map
|
|
628
|
+
if (!sourceFile) {
|
|
629
|
+
// Look for data-vite-dev-id or similar attributes
|
|
630
|
+
const viteId = el.getAttribute('data-vite-dev-id')
|
|
631
|
+
if (viteId) {
|
|
632
|
+
// Extract file path from viteId (remove query params if any)
|
|
633
|
+
const queryIndex = viteId.indexOf('?')
|
|
634
|
+
sourceFile = '/' + (queryIndex >= 0 ? viteId.substring(0, queryIndex) : viteId)
|
|
635
|
+
console.log('[Claude Dev Server] Found Vite ID:', viteId)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Check for inline script
|
|
640
|
+
if (!sourceFile) {
|
|
641
|
+
const inlineScripts = document.querySelectorAll('script:not([src])')
|
|
642
|
+
if (inlineScripts.length > 0) {
|
|
643
|
+
const pathParts = window.location.pathname.split('/')
|
|
644
|
+
const fileName = pathParts[pathParts.length - 1] || 'index.html'
|
|
645
|
+
sourceFile = '/' + fileName
|
|
646
|
+
|
|
647
|
+
const html = document.documentElement.outerHTML
|
|
648
|
+
const elId = el.id ? el.id : ''
|
|
649
|
+
const elClass = el.className ? el.className : ''
|
|
650
|
+
let elPattern
|
|
651
|
+
if (elId) {
|
|
652
|
+
elPattern = 'id="' + elId + '"'
|
|
653
|
+
} else if (elClass) {
|
|
654
|
+
elPattern = 'class="' + elClass.split(' ')[0] + '"'
|
|
655
|
+
} else {
|
|
656
|
+
elPattern = el.tagName.toLowerCase()
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const matchIndex = html.indexOf(elPattern)
|
|
660
|
+
if (matchIndex !== -1) {
|
|
661
|
+
const beforeElement = html.substring(0, matchIndex)
|
|
662
|
+
sourceLine = beforeElement.split('\\n').length
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Find main script
|
|
668
|
+
if (!sourceFile) {
|
|
669
|
+
const scripts = document.querySelectorAll('script[src]')
|
|
670
|
+
for (const script of scripts) {
|
|
671
|
+
const src = script.getAttribute('src')
|
|
672
|
+
if (src && !src.includes('cdn') && !src.includes('node_modules') &&
|
|
673
|
+
(src.includes('/src/') || src.includes('/app.') || src.includes('/main.'))) {
|
|
674
|
+
sourceFile = src
|
|
675
|
+
break
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Fallback
|
|
681
|
+
if (!sourceFile) {
|
|
682
|
+
const pathParts = window.location.pathname.split('/')
|
|
683
|
+
const fileName = pathParts[pathParts.length - 1] || 'index.html'
|
|
684
|
+
sourceFile = '/' + fileName
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const elClassName = el.className ? String(el.className) : ''
|
|
688
|
+
const selector = el.tagName.toLowerCase() +
|
|
689
|
+
(el.id ? '#' + el.id : '') +
|
|
690
|
+
(elClassName ? '.' + elClassName.split(' ').filter(c => c).join('.') : '')
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
url: sourceFile,
|
|
694
|
+
line: sourceLine,
|
|
695
|
+
column: sourceColumn,
|
|
696
|
+
selector: selector,
|
|
697
|
+
hint: sourceFile ? 'File: ' + sourceFile : 'Element: ' + selector
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function enableInspectMode() {
|
|
702
|
+
isInspectMode = true
|
|
703
|
+
overlay && overlay.classList.add('active')
|
|
704
|
+
document.body.style.cursor = 'crosshair'
|
|
705
|
+
|
|
706
|
+
// \u66F4\u65B0\u60AC\u6D6E inspect \u6309\u94AE\u72B6\u6001
|
|
707
|
+
if (inspectBtn) inspectBtn.classList.add('active')
|
|
708
|
+
|
|
709
|
+
// \u68C0\u67E5\u5143\u7D20\u662F\u5426\u5C5E\u4E8E dev tools
|
|
710
|
+
function isDevToolElement(el) {
|
|
711
|
+
if (!el) return false
|
|
712
|
+
// \u68C0\u67E5\u5143\u7D20\u672C\u8EAB
|
|
713
|
+
if (el.dataset && el.dataset.devTool === 'true') return true
|
|
714
|
+
// \u68C0\u67E5\u7236\u5143\u7D20
|
|
715
|
+
let parent = el.parentElement
|
|
716
|
+
while (parent) {
|
|
717
|
+
if (parent.dataset && parent.dataset.devTool === 'true') return true
|
|
718
|
+
parent = parent.parentElement
|
|
719
|
+
}
|
|
720
|
+
return false
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const onMouseMove = async (e) => {
|
|
724
|
+
if (!isInspectMode) return
|
|
725
|
+
const el = document.elementFromPoint(e.clientX, e.clientY)
|
|
726
|
+
// \u5FFD\u7565 dev tools \u5143\u7D20\u548C overlay
|
|
727
|
+
if (el && el !== overlay && !isDevToolElement(el) && (!overlay || !overlay.contains(el))) {
|
|
728
|
+
highlightElement(el)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const onClick = async (e) => {
|
|
733
|
+
if (!isInspectMode) return
|
|
734
|
+
e.preventDefault()
|
|
735
|
+
e.stopPropagation()
|
|
736
|
+
|
|
737
|
+
const el = document.elementFromPoint(e.clientX, e.clientY)
|
|
738
|
+
// \u5FFD\u7565 dev tools \u5143\u7D20\u548C overlay
|
|
739
|
+
if (el && el !== overlay && !isDevToolElement(el) && (!overlay || !overlay.contains(el))) {
|
|
740
|
+
const location = await getSourceLocation(el)
|
|
741
|
+
if (location) {
|
|
742
|
+
await inspectElement(location, el)
|
|
743
|
+
}
|
|
744
|
+
disableInspectMode()
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
document.addEventListener('mousemove', onMouseMove, true)
|
|
749
|
+
document.addEventListener('click', onClick, true)
|
|
750
|
+
|
|
751
|
+
if (overlay) overlay._inspectHandlers = { onMouseMove, onClick }
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function disableInspectMode() {
|
|
755
|
+
isInspectMode = false
|
|
756
|
+
if (overlay) overlay.classList.remove('active')
|
|
757
|
+
document.body.style.cursor = ''
|
|
758
|
+
if (overlay) overlay.innerHTML = ''
|
|
759
|
+
|
|
760
|
+
// \u66F4\u65B0\u60AC\u6D6E inspect \u6309\u94AE\u72B6\u6001
|
|
761
|
+
if (inspectBtn) inspectBtn.classList.remove('active')
|
|
762
|
+
|
|
763
|
+
const btn = panel && panel.querySelector('.claude-dev-server-btn-inspect')
|
|
764
|
+
if (btn) btn.classList.remove('active')
|
|
765
|
+
|
|
766
|
+
const handlers = overlay && overlay._inspectHandlers
|
|
767
|
+
if (handlers) {
|
|
768
|
+
document.removeEventListener('mousemove', handlers.onMouseMove, true)
|
|
769
|
+
document.removeEventListener('click', handlers.onClick, true)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function inspectElement(location, el) {
|
|
774
|
+
// \u6784\u5EFA\u683C\u5F0F\u5316\u7684 prompt
|
|
775
|
+
const filePath = location.url || location.file || 'unknown'
|
|
776
|
+
const tagName = el.tagName ? el.tagName.toLowerCase() : ''
|
|
777
|
+
const id = el.id ? '#' + el.id : ''
|
|
778
|
+
const className = el.className ? '.' + String(el.className).split(' ').filter(c => c).join('.') : ''
|
|
779
|
+
const selector = tagName + id + className
|
|
780
|
+
|
|
781
|
+
// \u83B7\u53D6\u5143\u7D20\u7684\u6587\u672C\u5185\u5BB9\uFF08\u5982\u679C\u662F\u6587\u672C\u8282\u70B9\u6216\u77ED\u6587\u672C\u5143\u7D20\uFF09
|
|
782
|
+
let textContent = ''
|
|
783
|
+
if (el.nodeType === Node.TEXT_NODE) {
|
|
784
|
+
textContent = el.textContent ? el.textContent.trim().substring(0, 50) : ''
|
|
785
|
+
} else if (el.childNodes.length === 1 && el.childNodes[0].nodeType === Node.TEXT_NODE) {
|
|
786
|
+
textContent = el.childNodes[0].textContent ? el.childNodes[0].textContent.trim().substring(0, 50) : ''
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// \u683C\u5F0F: @filename:line <selector> "text content"
|
|
790
|
+
let prompt = \`@\${filePath}:\${location.line} <\${selector}>\`
|
|
791
|
+
if (textContent) {
|
|
792
|
+
prompt += \` "\${textContent}"\`
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// \u53D1\u9001\u6210\u529F\u540E\u5C55\u5F00\u9762\u677F
|
|
796
|
+
const sendToTerminal = () => {
|
|
797
|
+
if (!terminalIframe || !terminalIframe.contentWindow) {
|
|
798
|
+
return false
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const win = terminalIframe.contentWindow
|
|
802
|
+
|
|
803
|
+
// Check if sendToTerminal function is available
|
|
804
|
+
if (win.sendToTerminal) {
|
|
805
|
+
win.sendToTerminal(prompt)
|
|
806
|
+
console.log('[Claude Dev Server] Sent via sendToTerminal:', prompt)
|
|
807
|
+
// \u53D1\u9001\u6210\u529F\u540E\u5C55\u5F00\u9762\u677F
|
|
808
|
+
setTimeout(() => togglePanel(true), 100)
|
|
809
|
+
return true
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return false
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Try immediately
|
|
816
|
+
if (sendToTerminal()) {
|
|
817
|
+
return
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// If not ready, wait and retry
|
|
821
|
+
let attempts = 0
|
|
822
|
+
const maxAttempts = 50
|
|
823
|
+
const retryInterval = setInterval(() => {
|
|
824
|
+
attempts++
|
|
825
|
+
if (sendToTerminal() || attempts >= maxAttempts) {
|
|
826
|
+
clearInterval(retryInterval)
|
|
827
|
+
if (attempts >= maxAttempts) {
|
|
828
|
+
console.warn('[Claude Dev Server] Could not send to terminal after retries')
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}, 100)
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function loadTerminalIframe(ttydUrl) {
|
|
835
|
+
if (!terminalContainer || terminalIframe) return
|
|
836
|
+
ttydWsUrl = ttydUrl
|
|
837
|
+
|
|
838
|
+
// Create iframe pointing to ttyd/index.html with ttyd WebSocket URL
|
|
839
|
+
terminalIframe = document.createElement('iframe')
|
|
840
|
+
// Pass the ttyd WebSocket URL as query param
|
|
841
|
+
terminalIframe.src = '/ttyd/index.html?ws=' + encodeURIComponent(ttydUrl)
|
|
842
|
+
terminalIframe.allow = 'clipboard-read; clipboard-write'
|
|
843
|
+
|
|
844
|
+
// Load event - iframe is ready
|
|
845
|
+
terminalIframe.onload = () => {
|
|
846
|
+
console.log('[Claude Dev Server] Terminal iframe loaded')
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
terminalContainer.appendChild(terminalIframe)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function createPanel() {
|
|
853
|
+
if (panel) return
|
|
854
|
+
|
|
855
|
+
panel = document.createElement('div')
|
|
856
|
+
panel.className = 'claude-dev-server-panel'
|
|
857
|
+
panel.innerHTML = \`
|
|
858
|
+
<div class="claude-dev-server-header">
|
|
859
|
+
<span class="claude-dev-server-title">Claude Code</span>
|
|
860
|
+
<div class="claude-dev-server-actions">
|
|
861
|
+
<button class="claude-dev-server-btn claude-dev-server-btn-inspect" title="Inspect Element">
|
|
862
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
863
|
+
<path d="M2 12h20M12 2v20M4.93 4.93l14.14 14.14M19.07 4.93L4.93 19.07"/>
|
|
864
|
+
</svg>
|
|
865
|
+
Inspect
|
|
866
|
+
</button>
|
|
867
|
+
</div>
|
|
868
|
+
<button class="claude-dev-server-close" aria-label="Close" title="Minimize to icon">×</button>
|
|
869
|
+
</div>
|
|
870
|
+
<div class="claude-dev-server-terminal"></div>
|
|
871
|
+
\`
|
|
872
|
+
|
|
873
|
+
document.body.appendChild(panel)
|
|
874
|
+
|
|
875
|
+
terminalContainer = panel.querySelector('.claude-dev-server-terminal')
|
|
876
|
+
const closeBtn = panel.querySelector('.claude-dev-server-close')
|
|
877
|
+
const inspectBtn = panel.querySelector('.claude-dev-server-btn-inspect')
|
|
878
|
+
|
|
879
|
+
closeBtn && closeBtn.addEventListener('click', () => togglePanel(false))
|
|
880
|
+
|
|
881
|
+
inspectBtn && inspectBtn.addEventListener('click', () => {
|
|
882
|
+
if (isInspectMode) {
|
|
883
|
+
disableInspectMode()
|
|
884
|
+
} else {
|
|
885
|
+
// \u70B9\u51FB Inspect \u65F6\u5148\u6536\u8D77\u9762\u677F
|
|
886
|
+
if (isOpen) {
|
|
887
|
+
togglePanel(false)
|
|
888
|
+
}
|
|
889
|
+
enableInspectMode()
|
|
890
|
+
}
|
|
891
|
+
})
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function togglePanel(force) {
|
|
895
|
+
createPanel()
|
|
896
|
+
if (typeof force === 'boolean') {
|
|
897
|
+
isOpen = force
|
|
898
|
+
} else {
|
|
899
|
+
isOpen = !isOpen
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (panel) panel.classList.toggle('open', isOpen)
|
|
903
|
+
if (toggleBtn) toggleBtn.classList.toggle('hidden', isOpen)
|
|
904
|
+
|
|
905
|
+
if (isOpen) {
|
|
906
|
+
// Focus iframe when panel opens
|
|
907
|
+
if (terminalIframe && terminalIframe.contentWindow) {
|
|
908
|
+
setTimeout(() => {
|
|
909
|
+
if (terminalIframe.contentWindow.terminalInstance) {
|
|
910
|
+
terminalIframe.contentWindow.terminalInstance.focus()
|
|
911
|
+
}
|
|
912
|
+
}, 100)
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function connect(port) {
|
|
918
|
+
const WS_URL = 'ws://localhost:' + port
|
|
919
|
+
ws = new WebSocket(WS_URL)
|
|
920
|
+
|
|
921
|
+
ws.onopen = () => {
|
|
922
|
+
console.log('[Claude Dev Server] Connected to control server')
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
ws.onmessage = (e) => {
|
|
926
|
+
try {
|
|
927
|
+
const msg = JSON.parse(e.data)
|
|
928
|
+
console.log('[Claude Dev Server] Received message:', msg.type, msg)
|
|
929
|
+
if (msg.type === 'ready' && msg.ttydUrl) {
|
|
930
|
+
loadTerminalIframe(msg.ttydUrl)
|
|
931
|
+
} else if (msg.type === 'inspectResult') {
|
|
932
|
+
if (msg.location) {
|
|
933
|
+
showContextPanel(msg.location, null, false)
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
} catch (err) {
|
|
937
|
+
console.error('[Claude Dev Server] Message parse error:', err)
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
ws.onclose = () => {
|
|
942
|
+
console.log('[Claude Dev Server] Control WebSocket disconnected, reconnecting...')
|
|
943
|
+
setTimeout(() => connect(port), 2000)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
ws.onerror = (err) => {
|
|
947
|
+
console.error('[Claude Dev Server] Control WebSocket error:', err)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
initWhenReady()
|
|
952
|
+
|
|
953
|
+
document.addEventListener('keydown', (e) => {
|
|
954
|
+
if ((e.metaKey || e.ctrlKey) && e.code === 'Backquote') {
|
|
955
|
+
e.preventDefault()
|
|
956
|
+
togglePanel()
|
|
957
|
+
}
|
|
958
|
+
if (e.key === 'Escape' && isOpen) {
|
|
959
|
+
togglePanel(false)
|
|
960
|
+
}
|
|
961
|
+
if (e.key === 'Escape' && isInspectMode) {
|
|
962
|
+
disableInspectMode()
|
|
963
|
+
}
|
|
964
|
+
})
|
|
965
|
+
})()
|
|
966
|
+
`;
|
|
967
|
+
|
|
968
|
+
// src/universal/index.ts
|
|
969
|
+
var __filename$1 = fileURLToPath(import.meta.url);
|
|
970
|
+
dirname(__filename$1);
|
|
971
|
+
async function startUniversalServer(options = {}) {
|
|
972
|
+
const cwd = options.cwd || process.cwd();
|
|
973
|
+
const port = options.port || 3e3;
|
|
974
|
+
const projectType = options.server?.type || detectProjectType(cwd);
|
|
975
|
+
const targetCommand = options.server?.command || getDefaultCommand(projectType);
|
|
976
|
+
console.log(`[Claude Dev Server] Detected project type: ${projectType}`);
|
|
977
|
+
console.log(`[Claude Dev Server] Starting target server...`);
|
|
978
|
+
const targetServer = spawnTargetServer(targetCommand, cwd);
|
|
979
|
+
console.log(`[Claude Dev Server] Waiting for target server to start (PID: ${targetServer.pid})...`);
|
|
980
|
+
const targetPort = await detectServerPort(targetServer, 3e4);
|
|
981
|
+
if (!targetPort) {
|
|
982
|
+
throw new Error("Failed to detect target server port. Please check if the dev server started successfully.");
|
|
983
|
+
}
|
|
984
|
+
console.log(`[Claude Dev Server] Target server is running on port ${targetPort}`);
|
|
985
|
+
const wsServer = createWebSocketServer({
|
|
986
|
+
port: 0,
|
|
987
|
+
// 自动分配端口
|
|
988
|
+
projectRoot: cwd,
|
|
989
|
+
claudePath: options.claudePath || "claude",
|
|
990
|
+
claudeArgs: options.claudeArgs || []
|
|
991
|
+
});
|
|
992
|
+
const { wsPort, ttydPort } = await wsServer.start();
|
|
993
|
+
console.log(`[Claude Dev Server] Control server running on ws://localhost:${wsPort}`);
|
|
994
|
+
console.log(`[Claude Dev Server] ttyd running on ws://localhost:${ttydPort}`);
|
|
995
|
+
const proxyServer = createProxyServer(targetPort, wsPort, cwd);
|
|
996
|
+
proxyServer.listen(port);
|
|
997
|
+
console.log(`[Claude Dev Server] Proxy server running on http://localhost:${port}`);
|
|
998
|
+
console.log(`
|
|
999
|
+
\u{1F680} Ready! Open http://localhost:${port} in your browser`);
|
|
1000
|
+
const cleanup = () => {
|
|
1001
|
+
console.log("[Claude Dev Server] Shutting down...");
|
|
1002
|
+
targetServer.kill();
|
|
1003
|
+
wsServer.stop();
|
|
1004
|
+
proxyServer.close();
|
|
1005
|
+
process.exit(0);
|
|
1006
|
+
};
|
|
1007
|
+
process.on("SIGINT", cleanup);
|
|
1008
|
+
process.on("SIGTERM", cleanup);
|
|
1009
|
+
return { proxyServer, targetServer, wsServer };
|
|
1010
|
+
}
|
|
1011
|
+
function detectProjectType(cwd) {
|
|
1012
|
+
if (existsSync(join(cwd, "vite.config.ts")) || existsSync(join(cwd, "vite.config.js"))) {
|
|
1013
|
+
return "vite";
|
|
1014
|
+
}
|
|
1015
|
+
if (existsSync(join(cwd, "next.config.js")) || existsSync(join(cwd, "next.config.mjs"))) {
|
|
1016
|
+
return "next";
|
|
1017
|
+
}
|
|
1018
|
+
if (existsSync(join(cwd, "webpack.config.js")) || existsSync(join(cwd, "webpack.config.ts"))) {
|
|
1019
|
+
return "webpack";
|
|
1020
|
+
}
|
|
1021
|
+
return "custom";
|
|
1022
|
+
}
|
|
1023
|
+
function getDefaultCommand(projectType) {
|
|
1024
|
+
switch (projectType) {
|
|
1025
|
+
case "vite":
|
|
1026
|
+
return "npm run dev";
|
|
1027
|
+
case "next":
|
|
1028
|
+
return "npm run dev";
|
|
1029
|
+
case "webpack":
|
|
1030
|
+
return "npm run dev";
|
|
1031
|
+
default:
|
|
1032
|
+
return "npm run dev";
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function spawnTargetServer(command, cwd) {
|
|
1036
|
+
const [cmd, ...args] = command.split(" ");
|
|
1037
|
+
const child = spawn(cmd, args, {
|
|
1038
|
+
cwd,
|
|
1039
|
+
stdio: "pipe",
|
|
1040
|
+
env: process.env
|
|
1041
|
+
});
|
|
1042
|
+
child.stdout?.pipe(process.stdout);
|
|
1043
|
+
child.stderr?.pipe(process.stderr);
|
|
1044
|
+
return child;
|
|
1045
|
+
}
|
|
1046
|
+
async function detectServerPort(childProcess, timeout) {
|
|
1047
|
+
return new Promise((resolve2) => {
|
|
1048
|
+
const timeoutId = setTimeout(() => {
|
|
1049
|
+
cleanup();
|
|
1050
|
+
resolve2(null);
|
|
1051
|
+
}, timeout);
|
|
1052
|
+
let stdout = "";
|
|
1053
|
+
const onData = (chunk) => {
|
|
1054
|
+
stdout += chunk.toString();
|
|
1055
|
+
const patterns = [
|
|
1056
|
+
/localhost:(\d{4,5})/,
|
|
1057
|
+
/127\.0\.0\.1:(\d{4,5})/,
|
|
1058
|
+
/0\.0\.0\.0:(\d{4,5})/
|
|
1059
|
+
];
|
|
1060
|
+
for (const pattern of patterns) {
|
|
1061
|
+
const match = stdout.match(pattern);
|
|
1062
|
+
if (match) {
|
|
1063
|
+
const port = parseInt(match[1], 10);
|
|
1064
|
+
console.log(`[Claude Dev Server] Detected port from stdout: ${port}`);
|
|
1065
|
+
cleanup();
|
|
1066
|
+
resolve2(port);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
const cleanup = () => {
|
|
1072
|
+
clearTimeout(timeoutId);
|
|
1073
|
+
childProcess.stdout?.off("data", onData);
|
|
1074
|
+
};
|
|
1075
|
+
childProcess.stdout?.on("data", onData);
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
function createProxyServer(targetPort, wsPort, projectRoot) {
|
|
1079
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
1080
|
+
const assetsPath = join(moduleDir, "assets");
|
|
1081
|
+
let ttydHtml;
|
|
1082
|
+
let ttydBridgeJs;
|
|
1083
|
+
try {
|
|
1084
|
+
ttydHtml = readFileSync(join(assetsPath, "ttyd-terminal.html"), "utf-8");
|
|
1085
|
+
ttydBridgeJs = readFileSync(join(assetsPath, "ttyd-bridge.js"), "utf-8");
|
|
1086
|
+
} catch (e) {
|
|
1087
|
+
console.error("[Claude Dev Server] Failed to read ttyd assets from", assetsPath);
|
|
1088
|
+
console.error("[Claude Dev Server] moduleDir:", moduleDir);
|
|
1089
|
+
console.error("[Claude Dev Server] Error:", e.message);
|
|
1090
|
+
throw new Error("ttyd assets not found. Please run `npm run build` first.");
|
|
1091
|
+
}
|
|
1092
|
+
return http.createServer((req, res) => {
|
|
1093
|
+
if (req.url === "/@claude-port") {
|
|
1094
|
+
res.setHeader("Content-Type", "application/json");
|
|
1095
|
+
res.end(JSON.stringify({ port: wsPort }));
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (req.url?.startsWith("/ttyd/")) {
|
|
1099
|
+
const urlPath = req.url.split("?")[0];
|
|
1100
|
+
if (urlPath === "/ttyd/index.html" || urlPath === "/ttyd/") {
|
|
1101
|
+
res.setHeader("Content-Type", "text/html");
|
|
1102
|
+
res.end(ttydHtml);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (urlPath === "/ttyd/ttyd-bridge.js") {
|
|
1106
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
1107
|
+
res.end(ttydBridgeJs);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
res.statusCode = 404;
|
|
1111
|
+
res.end("Not found");
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
const options = {
|
|
1115
|
+
hostname: "localhost",
|
|
1116
|
+
port: targetPort,
|
|
1117
|
+
path: req.url,
|
|
1118
|
+
method: req.method,
|
|
1119
|
+
headers: req.headers
|
|
1120
|
+
};
|
|
1121
|
+
const proxyReq = http.request(options, (proxyRes) => {
|
|
1122
|
+
if (proxyRes.headers["content-type"]?.includes("text/html")) {
|
|
1123
|
+
const body = [];
|
|
1124
|
+
proxyRes.on("data", (chunk) => body.push(chunk));
|
|
1125
|
+
proxyRes.on("end", () => {
|
|
1126
|
+
const html = Buffer.concat(body).toString("utf8");
|
|
1127
|
+
const injected = injectScripts(html, wsPort, projectRoot);
|
|
1128
|
+
const statusCode = proxyRes.statusCode || 200;
|
|
1129
|
+
res.writeHead(statusCode, {
|
|
1130
|
+
...proxyRes.headers,
|
|
1131
|
+
"content-length": Buffer.byteLength(injected)
|
|
1132
|
+
});
|
|
1133
|
+
res.end(injected);
|
|
1134
|
+
});
|
|
1135
|
+
} else {
|
|
1136
|
+
const statusCode = proxyRes.statusCode || 200;
|
|
1137
|
+
res.writeHead(statusCode, proxyRes.headers);
|
|
1138
|
+
proxyRes.pipe(res);
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
proxyReq.on("error", (err) => {
|
|
1142
|
+
console.error("[Claude Dev Server] Proxy error:", err);
|
|
1143
|
+
res.statusCode = 502;
|
|
1144
|
+
res.end("Bad Gateway");
|
|
1145
|
+
});
|
|
1146
|
+
req.pipe(proxyReq);
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
function injectScripts(html, wsPort, projectRoot) {
|
|
1150
|
+
const projectRootScript = `<script>window.__CLAUDE_PROJECT_ROOT__ = ${JSON.stringify(projectRoot)}</script>`;
|
|
1151
|
+
return html.replace(
|
|
1152
|
+
"</head>",
|
|
1153
|
+
`<!-- Claude Dev Server -->
|
|
1154
|
+
${CLIENT_STYLES}
|
|
1155
|
+
${projectRootScript}
|
|
1156
|
+
<script type="module">${CLIENT_SCRIPT}</script>
|
|
1157
|
+
</head>`
|
|
1158
|
+
).replace(
|
|
1159
|
+
/wsPort:\s*\d+/,
|
|
1160
|
+
`wsPort: ${wsPort}`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export { startUniversalServer };
|
|
1165
|
+
//# sourceMappingURL=chunk-DOLSA776.js.map
|
|
1166
|
+
//# sourceMappingURL=chunk-DOLSA776.js.map
|