lunel-cli 0.1.1 → 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +1184 -168
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,55 +5,158 @@ import Ignore from "ignore";
5
5
  const ignore = Ignore.default;
6
6
  import * as fs from "fs/promises";
7
7
  import * as path from "path";
8
- const PROXY_URL = process.env.LUNEL_PROXY_URL || "https://proxy-1.lunel.dev";
8
+ import * as os from "os";
9
+ import { spawn, execSync } from "child_process";
10
+ import { createServer } from "net";
11
+ const PROXY_URL = process.env.LUNEL_PROXY_URL || "https://gateway.lunel.dev";
12
+ const VERSION = "1.0.0";
9
13
  // Root directory - sandbox all file operations to this
10
14
  const ROOT_DIR = process.cwd();
11
- // Validate and resolve path within sandbox
15
+ // Terminal sessions
16
+ const terminals = new Map();
17
+ const processes = new Map();
18
+ const processOutputBuffers = new Map();
19
+ // CPU usage tracking
20
+ let lastCpuInfo = null;
21
+ // ============================================================================
22
+ // Path Safety
23
+ // ============================================================================
12
24
  function resolveSafePath(requestedPath) {
13
- // Resolve the requested path relative to ROOT_DIR
14
25
  const resolved = path.resolve(ROOT_DIR, requestedPath);
15
- // Ensure the resolved path is within ROOT_DIR
16
26
  if (!resolved.startsWith(ROOT_DIR)) {
17
27
  return null;
18
28
  }
19
29
  return resolved;
20
30
  }
21
- // Handle ls command
22
- async function handleLs(requestedPath) {
31
+ function assertSafePath(requestedPath) {
23
32
  const safePath = resolveSafePath(requestedPath);
24
33
  if (!safePath) {
25
- throw new Error("Access denied: path outside root directory");
34
+ const error = new Error("Access denied: path outside root directory");
35
+ error.code = "EACCES";
36
+ throw error;
26
37
  }
27
- const entries = await fs.readdir(safePath, { withFileTypes: true });
28
- return entries.map((entry) => ({
29
- name: entry.name,
30
- type: entry.isDirectory() ? "directory" : "file",
31
- }));
38
+ return safePath;
32
39
  }
33
- // Handle read command
34
- async function handleRead(requestedPath) {
35
- const safePath = resolveSafePath(requestedPath);
36
- if (!safePath) {
37
- throw new Error("Access denied: path outside root directory");
40
+ // ============================================================================
41
+ // File System Handlers
42
+ // ============================================================================
43
+ async function handleFsLs(payload) {
44
+ const reqPath = payload.path || ".";
45
+ const safePath = assertSafePath(reqPath);
46
+ const entries = await fs.readdir(safePath, { withFileTypes: true });
47
+ const result = [];
48
+ for (const entry of entries) {
49
+ const item = {
50
+ name: entry.name,
51
+ type: entry.isDirectory() ? "directory" : "file",
52
+ };
53
+ // Try to get size and mtime for files
54
+ if (entry.isFile()) {
55
+ try {
56
+ const stat = await fs.stat(path.join(safePath, entry.name));
57
+ item.size = stat.size;
58
+ item.mtime = stat.mtimeMs;
59
+ }
60
+ catch {
61
+ // Ignore stat errors
62
+ }
63
+ }
64
+ result.push(item);
38
65
  }
39
- const content = await fs.readFile(safePath, "utf-8");
40
- return content;
66
+ return { path: reqPath, entries: result };
41
67
  }
42
- // Handle write command
43
- async function handleWrite(requestedPath, content) {
44
- const safePath = resolveSafePath(requestedPath);
45
- if (!safePath) {
46
- throw new Error("Access denied: path outside root directory");
68
+ async function handleFsStat(payload) {
69
+ const reqPath = payload.path;
70
+ if (!reqPath)
71
+ throw Object.assign(new Error("path is required"), { code: "EINVAL" });
72
+ const safePath = assertSafePath(reqPath);
73
+ const stat = await fs.stat(safePath);
74
+ const result = {
75
+ path: reqPath,
76
+ type: stat.isDirectory() ? "directory" : "file",
77
+ size: stat.size,
78
+ mtime: stat.mtimeMs,
79
+ mode: stat.mode,
80
+ };
81
+ return result;
82
+ }
83
+ async function handleFsRead(payload) {
84
+ const reqPath = payload.path;
85
+ if (!reqPath)
86
+ throw Object.assign(new Error("path is required"), { code: "EINVAL" });
87
+ const safePath = assertSafePath(reqPath);
88
+ // Check if binary
89
+ const stat = await fs.stat(safePath);
90
+ const content = await fs.readFile(safePath);
91
+ // Try to detect if binary
92
+ const isBinary = content.includes(0x00);
93
+ if (isBinary) {
94
+ return {
95
+ path: reqPath,
96
+ content: content.toString("base64"),
97
+ encoding: "base64",
98
+ size: stat.size,
99
+ };
47
100
  }
48
- // Ensure parent directory exists
101
+ return {
102
+ path: reqPath,
103
+ content: content.toString("utf-8"),
104
+ encoding: "utf8",
105
+ size: stat.size,
106
+ };
107
+ }
108
+ async function handleFsWrite(payload) {
109
+ const reqPath = payload.path;
110
+ const content = payload.content;
111
+ const encoding = payload.encoding || "utf8";
112
+ if (!reqPath)
113
+ throw Object.assign(new Error("path is required"), { code: "EINVAL" });
114
+ if (typeof content !== "string")
115
+ throw Object.assign(new Error("content is required"), { code: "EINVAL" });
116
+ const safePath = assertSafePath(reqPath);
49
117
  const parentDir = path.dirname(safePath);
50
118
  await fs.mkdir(parentDir, { recursive: true });
51
- await fs.writeFile(safePath, content, "utf-8");
119
+ if (encoding === "base64") {
120
+ await fs.writeFile(safePath, Buffer.from(content, "base64"));
121
+ }
122
+ else {
123
+ await fs.writeFile(safePath, content, "utf-8");
124
+ }
125
+ return { path: reqPath };
52
126
  }
53
- // Load gitignore patterns from a directory
127
+ async function handleFsMkdir(payload) {
128
+ const reqPath = payload.path;
129
+ const recursive = payload.recursive !== false;
130
+ if (!reqPath)
131
+ throw Object.assign(new Error("path is required"), { code: "EINVAL" });
132
+ const safePath = assertSafePath(reqPath);
133
+ await fs.mkdir(safePath, { recursive });
134
+ return { path: reqPath };
135
+ }
136
+ async function handleFsRm(payload) {
137
+ const reqPath = payload.path;
138
+ const recursive = payload.recursive === true;
139
+ if (!reqPath)
140
+ throw Object.assign(new Error("path is required"), { code: "EINVAL" });
141
+ const safePath = assertSafePath(reqPath);
142
+ await fs.rm(safePath, { recursive, force: false });
143
+ return { path: reqPath };
144
+ }
145
+ async function handleFsMv(payload) {
146
+ const from = payload.from;
147
+ const to = payload.to;
148
+ if (!from)
149
+ throw Object.assign(new Error("from is required"), { code: "EINVAL" });
150
+ if (!to)
151
+ throw Object.assign(new Error("to is required"), { code: "EINVAL" });
152
+ const safeFrom = assertSafePath(from);
153
+ const safeTo = assertSafePath(to);
154
+ await fs.rename(safeFrom, safeTo);
155
+ return { from, to };
156
+ }
157
+ // Load gitignore patterns
54
158
  async function loadGitignore(dirPath) {
55
159
  const ig = ignore();
56
- // Always ignore .git directory
57
160
  ig.add(".git");
58
161
  try {
59
162
  const gitignorePath = path.join(dirPath, ".gitignore");
@@ -61,20 +164,20 @@ async function loadGitignore(dirPath) {
61
164
  ig.add(content);
62
165
  }
63
166
  catch {
64
- // No .gitignore file, that's fine
167
+ // No .gitignore
65
168
  }
66
169
  return ig;
67
170
  }
68
- // Handle grep command - search for pattern in files
69
- async function handleGrep(requestedPath, pattern, options = {}) {
70
- const safePath = resolveSafePath(requestedPath);
71
- if (!safePath) {
72
- throw new Error("Access denied: path outside root directory");
73
- }
74
- const { caseSensitive = true, maxResults = 100 } = options;
171
+ async function handleFsGrep(payload) {
172
+ const reqPath = payload.path || ".";
173
+ const pattern = payload.pattern;
174
+ const caseSensitive = payload.caseSensitive !== false;
175
+ const maxResults = payload.maxResults || 100;
176
+ if (!pattern)
177
+ throw Object.assign(new Error("pattern is required"), { code: "EINVAL" });
178
+ const safePath = assertSafePath(reqPath);
75
179
  const matches = [];
76
180
  const regex = new RegExp(pattern, caseSensitive ? "g" : "gi");
77
- // Load root gitignore
78
181
  const rootIgnore = await loadGitignore(ROOT_DIR);
79
182
  async function searchFile(filePath, relativePath) {
80
183
  if (matches.length >= maxResults)
@@ -87,21 +190,19 @@ async function handleGrep(requestedPath, pattern, options = {}) {
87
190
  matches.push({
88
191
  file: relativePath,
89
192
  line: i + 1,
90
- content: lines[i].substring(0, 500), // Limit line length
193
+ content: lines[i].substring(0, 500),
91
194
  });
92
195
  }
93
- // Reset regex lastIndex for global flag
94
196
  regex.lastIndex = 0;
95
197
  }
96
198
  }
97
199
  catch {
98
- // Skip files that can't be read (binary, permissions, etc.)
200
+ // Skip unreadable files
99
201
  }
100
202
  }
101
203
  async function searchDir(dirPath, relativePath, ig) {
102
204
  if (matches.length >= maxResults)
103
205
  return;
104
- // Check for local .gitignore and merge with parent
105
206
  const localIgnore = ignore().add(ig);
106
207
  try {
107
208
  const localGitignorePath = path.join(dirPath, ".gitignore");
@@ -116,12 +217,9 @@ async function handleGrep(requestedPath, pattern, options = {}) {
116
217
  if (matches.length >= maxResults)
117
218
  break;
118
219
  const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
119
- // Check if ignored by gitignore
120
- // For directories, append / to match gitignore patterns correctly
121
220
  const checkPath = entry.isDirectory() ? `${relPath}/` : relPath;
122
- if (localIgnore.ignores(checkPath)) {
221
+ if (localIgnore.ignores(checkPath))
123
222
  continue;
124
- }
125
223
  const fullPath = path.join(dirPath, entry.name);
126
224
  if (entry.isDirectory()) {
127
225
  await searchDir(fullPath, relPath, localIgnore);
@@ -133,141 +231,1005 @@ async function handleGrep(requestedPath, pattern, options = {}) {
133
231
  }
134
232
  const stat = await fs.stat(safePath);
135
233
  if (stat.isDirectory()) {
136
- await searchDir(safePath, requestedPath === "." ? "" : requestedPath, rootIgnore);
234
+ await searchDir(safePath, reqPath === "." ? "" : reqPath, rootIgnore);
137
235
  }
138
236
  else {
139
- await searchFile(safePath, requestedPath);
237
+ await searchFile(safePath, reqPath);
140
238
  }
141
- return matches;
239
+ return { matches };
142
240
  }
143
- // Handle replace command - find and replace in a file
144
- async function handleReplace(requestedPath, search, replace, options = {}) {
145
- const safePath = resolveSafePath(requestedPath);
146
- if (!safePath) {
147
- throw new Error("Access denied: path outside root directory");
148
- }
149
- const { caseSensitive = true, replaceAll = true } = options;
150
- const content = await fs.readFile(safePath, "utf-8");
151
- const flags = replaceAll ? (caseSensitive ? "g" : "gi") : (caseSensitive ? "" : "i");
152
- const regex = new RegExp(search.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
153
- // Count replacements
154
- const matchCount = (content.match(regex) || []).length;
155
- if (matchCount === 0) {
156
- return { file: requestedPath, replacements: 0 };
157
- }
158
- const newContent = content.replace(regex, replace);
159
- await fs.writeFile(safePath, newContent, "utf-8");
160
- return { file: requestedPath, replacements: matchCount };
161
- }
162
- // Process incoming request from app
163
- async function processRequest(message) {
164
- const { id, action, payload } = message;
241
+ async function handleFsCreate(payload) {
242
+ const reqPath = payload.path;
243
+ const type = payload.type;
244
+ if (!reqPath)
245
+ throw Object.assign(new Error("path is required"), { code: "EINVAL" });
246
+ if (!type || (type !== "file" && type !== "directory")) {
247
+ throw Object.assign(new Error("type must be 'file' or 'directory'"), { code: "EINVAL" });
248
+ }
249
+ const safePath = assertSafePath(reqPath);
250
+ if (type === "directory") {
251
+ await fs.mkdir(safePath, { recursive: true });
252
+ }
253
+ else {
254
+ // Create parent directories if needed
255
+ const parentDir = path.dirname(safePath);
256
+ await fs.mkdir(parentDir, { recursive: true });
257
+ // Create empty file
258
+ await fs.writeFile(safePath, "");
259
+ }
260
+ return { path: reqPath };
261
+ }
262
+ // ============================================================================
263
+ // Git Handlers
264
+ // ============================================================================
265
+ async function runGit(args) {
266
+ return new Promise((resolve) => {
267
+ const proc = spawn("git", args, { cwd: ROOT_DIR });
268
+ let stdout = "";
269
+ let stderr = "";
270
+ proc.stdout.on("data", (data) => (stdout += data.toString()));
271
+ proc.stderr.on("data", (data) => (stderr += data.toString()));
272
+ proc.on("close", (code) => {
273
+ resolve({ stdout, stderr, code: code || 0 });
274
+ });
275
+ proc.on("error", (err) => {
276
+ resolve({ stdout: "", stderr: err.message, code: 1 });
277
+ });
278
+ });
279
+ }
280
+ async function handleGitStatus() {
281
+ // Get branch
282
+ const branchResult = await runGit(["branch", "--show-current"]);
283
+ const branch = branchResult.stdout.trim();
284
+ // Get status
285
+ const statusResult = await runGit(["status", "--porcelain", "-uall"]);
286
+ const lines = statusResult.stdout.trim().split("\n").filter(Boolean);
287
+ const staged = [];
288
+ const unstaged = [];
289
+ const untracked = [];
290
+ for (const line of lines) {
291
+ const index = line[0];
292
+ const worktree = line[1];
293
+ const filepath = line.substring(3);
294
+ if (index === "?" && worktree === "?") {
295
+ untracked.push(filepath);
296
+ }
297
+ else {
298
+ if (index !== " " && index !== "?") {
299
+ staged.push({ path: filepath, status: index });
300
+ }
301
+ if (worktree !== " " && worktree !== "?") {
302
+ unstaged.push({ path: filepath, status: worktree });
303
+ }
304
+ }
305
+ }
306
+ // Get ahead/behind
307
+ const aheadBehind = await runGit(["rev-list", "--left-right", "--count", "@{u}...HEAD"]);
308
+ let ahead = 0;
309
+ let behind = 0;
310
+ if (aheadBehind.code === 0) {
311
+ const parts = aheadBehind.stdout.trim().split(/\s+/);
312
+ behind = parseInt(parts[0]) || 0;
313
+ ahead = parseInt(parts[1]) || 0;
314
+ }
315
+ return { branch, ahead, behind, staged, unstaged, untracked };
316
+ }
317
+ async function handleGitStage(payload) {
318
+ const paths = payload.paths;
319
+ if (!paths || !paths.length)
320
+ throw Object.assign(new Error("paths is required"), { code: "EINVAL" });
321
+ const result = await runGit(["add", ...paths]);
322
+ if (result.code !== 0) {
323
+ throw Object.assign(new Error(result.stderr || "git add failed"), { code: "EGIT" });
324
+ }
325
+ return {};
326
+ }
327
+ async function handleGitUnstage(payload) {
328
+ const paths = payload.paths;
329
+ if (!paths || !paths.length)
330
+ throw Object.assign(new Error("paths is required"), { code: "EINVAL" });
331
+ const result = await runGit(["reset", "HEAD", ...paths]);
332
+ if (result.code !== 0) {
333
+ throw Object.assign(new Error(result.stderr || "git reset failed"), { code: "EGIT" });
334
+ }
335
+ return {};
336
+ }
337
+ async function handleGitCommit(payload) {
338
+ const message = payload.message;
339
+ if (!message)
340
+ throw Object.assign(new Error("message is required"), { code: "EINVAL" });
341
+ const result = await runGit(["commit", "-m", message]);
342
+ if (result.code !== 0) {
343
+ throw Object.assign(new Error(result.stderr || "git commit failed"), { code: "EGIT" });
344
+ }
345
+ // Get the commit hash
346
+ const hashResult = await runGit(["rev-parse", "HEAD"]);
347
+ const hash = hashResult.stdout.trim().substring(0, 7);
348
+ return { hash, message };
349
+ }
350
+ async function handleGitLog(payload) {
351
+ const limit = payload.limit || 20;
352
+ const result = await runGit([
353
+ "log",
354
+ `-${limit}`,
355
+ "--pretty=format:%H|%s|%an|%at",
356
+ ]);
357
+ if (result.code !== 0) {
358
+ throw Object.assign(new Error(result.stderr || "git log failed"), { code: "EGIT" });
359
+ }
360
+ const commits = result.stdout
361
+ .trim()
362
+ .split("\n")
363
+ .filter(Boolean)
364
+ .map((line) => {
365
+ const [hash, message, author, timestamp] = line.split("|");
366
+ return {
367
+ hash: hash.substring(0, 7),
368
+ message,
369
+ author,
370
+ date: parseInt(timestamp) * 1000,
371
+ };
372
+ });
373
+ return { commits };
374
+ }
375
+ async function handleGitDiff(payload) {
376
+ const filepath = payload.path;
377
+ const staged = payload.staged === true;
378
+ const args = ["diff"];
379
+ if (staged)
380
+ args.push("--staged");
381
+ if (filepath)
382
+ args.push(filepath);
383
+ const result = await runGit(args);
384
+ return { diff: result.stdout };
385
+ }
386
+ async function handleGitBranches() {
387
+ const result = await runGit(["branch", "-a"]);
388
+ if (result.code !== 0) {
389
+ throw Object.assign(new Error(result.stderr || "git branch failed"), { code: "EGIT" });
390
+ }
391
+ const lines = result.stdout.trim().split("\n");
392
+ let current = "";
393
+ const branches = [];
394
+ for (const line of lines) {
395
+ const trimmed = line.trim();
396
+ if (trimmed.startsWith("* ")) {
397
+ current = trimmed.substring(2);
398
+ branches.push(current);
399
+ }
400
+ else if (!trimmed.startsWith("remotes/")) {
401
+ branches.push(trimmed);
402
+ }
403
+ }
404
+ return { current, branches };
405
+ }
406
+ async function handleGitCheckout(payload) {
407
+ const branch = payload.branch;
408
+ const create = payload.create === true;
409
+ if (!branch)
410
+ throw Object.assign(new Error("branch is required"), { code: "EINVAL" });
411
+ const args = create ? ["checkout", "-b", branch] : ["checkout", branch];
412
+ const result = await runGit(args);
413
+ if (result.code !== 0) {
414
+ throw Object.assign(new Error(result.stderr || "git checkout failed"), { code: "EGIT" });
415
+ }
416
+ return { branch };
417
+ }
418
+ async function handleGitPull() {
419
+ const result = await runGit(["pull"]);
420
+ if (result.code !== 0) {
421
+ throw Object.assign(new Error(result.stderr || "git pull failed"), { code: "EGIT" });
422
+ }
423
+ return { success: true, summary: result.stdout.trim() || result.stderr.trim() };
424
+ }
425
+ async function handleGitPush(payload) {
426
+ const setUpstream = payload.setUpstream === true;
427
+ const args = ["push"];
428
+ if (setUpstream) {
429
+ // Get current branch name
430
+ const branchResult = await runGit(["branch", "--show-current"]);
431
+ const branch = branchResult.stdout.trim();
432
+ args.push("-u", "origin", branch);
433
+ }
434
+ const result = await runGit(args);
435
+ if (result.code !== 0) {
436
+ throw Object.assign(new Error(result.stderr || "git push failed"), { code: "EGIT" });
437
+ }
438
+ return { success: true };
439
+ }
440
+ async function handleGitDiscard(payload) {
441
+ const paths = payload.paths;
442
+ const all = payload.all === true;
443
+ if (!paths && !all) {
444
+ throw Object.assign(new Error("paths or all is required"), { code: "EINVAL" });
445
+ }
446
+ if (all) {
447
+ // Discard all changes
448
+ const result = await runGit(["checkout", "--", "."]);
449
+ if (result.code !== 0) {
450
+ throw Object.assign(new Error(result.stderr || "git checkout failed"), { code: "EGIT" });
451
+ }
452
+ // Also clean untracked files
453
+ await runGit(["clean", "-fd"]);
454
+ }
455
+ else if (paths && paths.length > 0) {
456
+ const result = await runGit(["checkout", "--", ...paths]);
457
+ if (result.code !== 0) {
458
+ throw Object.assign(new Error(result.stderr || "git checkout failed"), { code: "EGIT" });
459
+ }
460
+ }
461
+ return {};
462
+ }
463
+ // ============================================================================
464
+ // Terminal Handlers
465
+ // ============================================================================
466
+ let dataChannel = null;
467
+ function handleTerminalSpawn(payload) {
468
+ const shell = payload.shell || process.env.SHELL || "/bin/sh";
469
+ const cols = payload.cols || 80;
470
+ const rows = payload.rows || 24;
471
+ const terminalId = `term-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
472
+ const proc = spawn(shell, [], {
473
+ cwd: ROOT_DIR,
474
+ env: {
475
+ ...process.env,
476
+ TERM: "xterm-256color",
477
+ COLUMNS: cols.toString(),
478
+ LINES: rows.toString(),
479
+ },
480
+ stdio: ["pipe", "pipe", "pipe"],
481
+ });
482
+ terminals.set(terminalId, proc);
483
+ // Stream output to app via data channel
484
+ const sendOutput = (data) => {
485
+ if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
486
+ const msg = {
487
+ v: 1,
488
+ id: `evt-${Date.now()}`,
489
+ ns: "terminal",
490
+ action: "output",
491
+ payload: { terminalId, data: data.toString() },
492
+ };
493
+ dataChannel.send(JSON.stringify(msg));
494
+ }
495
+ };
496
+ proc.stdout?.on("data", sendOutput);
497
+ proc.stderr?.on("data", sendOutput);
498
+ proc.on("close", (code) => {
499
+ terminals.delete(terminalId);
500
+ if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
501
+ const msg = {
502
+ v: 1,
503
+ id: `evt-${Date.now()}`,
504
+ ns: "terminal",
505
+ action: "exit",
506
+ payload: { terminalId, code },
507
+ };
508
+ dataChannel.send(JSON.stringify(msg));
509
+ }
510
+ });
511
+ return { terminalId };
512
+ }
513
+ function handleTerminalWrite(payload) {
514
+ const terminalId = payload.terminalId;
515
+ const data = payload.data;
516
+ if (!terminalId)
517
+ throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
518
+ if (typeof data !== "string")
519
+ throw Object.assign(new Error("data is required"), { code: "EINVAL" });
520
+ const proc = terminals.get(terminalId);
521
+ if (!proc)
522
+ throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
523
+ proc.stdin?.write(data);
524
+ return {};
525
+ }
526
+ function handleTerminalResize(payload) {
527
+ const terminalId = payload.terminalId;
528
+ const cols = payload.cols;
529
+ const rows = payload.rows;
530
+ if (!terminalId)
531
+ throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
532
+ const proc = terminals.get(terminalId);
533
+ if (!proc)
534
+ throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
535
+ // Note: For proper PTY resize, you'd need node-pty
536
+ // This is a simplified version
537
+ return {};
538
+ }
539
+ function handleTerminalKill(payload) {
540
+ const terminalId = payload.terminalId;
541
+ if (!terminalId)
542
+ throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
543
+ const proc = terminals.get(terminalId);
544
+ if (!proc)
545
+ throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
546
+ proc.kill();
547
+ terminals.delete(terminalId);
548
+ return {};
549
+ }
550
+ // ============================================================================
551
+ // System Handlers
552
+ // ============================================================================
553
+ function handleSystemCapabilities() {
554
+ return {
555
+ version: VERSION,
556
+ namespaces: ["fs", "git", "terminal", "processes", "ports", "monitor", "http"],
557
+ platform: os.platform(),
558
+ rootDir: ROOT_DIR,
559
+ hostname: os.hostname(),
560
+ };
561
+ }
562
+ function handleSystemPing() {
563
+ return { pong: true, timestamp: Date.now() };
564
+ }
565
+ // ============================================================================
566
+ // Processes Handlers
567
+ // ============================================================================
568
+ function handleProcessesList() {
569
+ const result = [];
570
+ for (const [pid, proc] of processes) {
571
+ result.push({
572
+ pid,
573
+ command: `${proc.command} ${proc.args.join(" ")}`.trim(),
574
+ startTime: proc.startTime,
575
+ status: proc.proc.killed ? "stopped" : "running",
576
+ channel: proc.channel,
577
+ });
578
+ }
579
+ return { processes: result };
580
+ }
581
+ function handleProcessesSpawn(payload) {
582
+ const command = payload.command;
583
+ const args = payload.args || [];
584
+ const cwd = payload.cwd;
585
+ const extraEnv = payload.env || {};
586
+ if (!command)
587
+ throw Object.assign(new Error("command is required"), { code: "EINVAL" });
588
+ const workDir = cwd ? assertSafePath(cwd) : ROOT_DIR;
589
+ const proc = spawn(command, args, {
590
+ cwd: workDir,
591
+ env: { ...process.env, ...extraEnv },
592
+ stdio: ["pipe", "pipe", "pipe"],
593
+ shell: true,
594
+ });
595
+ const pid = proc.pid;
596
+ const channel = `proc-${pid}`;
597
+ const managedProc = {
598
+ pid,
599
+ proc,
600
+ command,
601
+ args,
602
+ startTime: Date.now(),
603
+ output: [],
604
+ channel,
605
+ };
606
+ processes.set(pid, managedProc);
607
+ processOutputBuffers.set(channel, "");
608
+ // Stream output
609
+ const sendOutput = (stream) => (data) => {
610
+ const text = data.toString();
611
+ managedProc.output.push(text);
612
+ processOutputBuffers.set(channel, (processOutputBuffers.get(channel) || "") + text);
613
+ if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
614
+ const msg = {
615
+ v: 1,
616
+ id: `evt-${Date.now()}`,
617
+ ns: "processes",
618
+ action: "output",
619
+ payload: { pid, channel, stream, data: text },
620
+ };
621
+ dataChannel.send(JSON.stringify(msg));
622
+ }
623
+ };
624
+ proc.stdout?.on("data", sendOutput("stdout"));
625
+ proc.stderr?.on("data", sendOutput("stderr"));
626
+ proc.on("close", (code, signal) => {
627
+ if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
628
+ const msg = {
629
+ v: 1,
630
+ id: `evt-${Date.now()}`,
631
+ ns: "processes",
632
+ action: "exit",
633
+ payload: { pid, channel, code, signal },
634
+ };
635
+ dataChannel.send(JSON.stringify(msg));
636
+ }
637
+ });
638
+ return { pid, channel };
639
+ }
640
+ function handleProcessesKill(payload) {
641
+ const pid = payload.pid;
642
+ if (!pid)
643
+ throw Object.assign(new Error("pid is required"), { code: "EINVAL" });
644
+ const proc = processes.get(pid);
645
+ if (!proc)
646
+ throw Object.assign(new Error("Process not found"), { code: "ENOPROC" });
647
+ proc.proc.kill();
648
+ processes.delete(pid);
649
+ return {};
650
+ }
651
+ function handleProcessesGetOutput(payload) {
652
+ const channel = payload.channel;
653
+ if (!channel)
654
+ throw Object.assign(new Error("channel is required"), { code: "EINVAL" });
655
+ const output = processOutputBuffers.get(channel) || "";
656
+ return { channel, output };
657
+ }
658
+ function handleProcessesClearOutput(payload) {
659
+ const channel = payload.channel;
660
+ if (channel) {
661
+ processOutputBuffers.set(channel, "");
662
+ }
663
+ else {
664
+ processOutputBuffers.clear();
665
+ }
666
+ return {};
667
+ }
668
+ // ============================================================================
669
+ // Ports Handlers
670
+ // ============================================================================
671
+ function handlePortsList() {
672
+ const platform = os.platform();
673
+ const ports = [];
165
674
  try {
166
- switch (action) {
167
- case "ls": {
168
- const reqPath = payload.path || ".";
169
- const entries = await handleLs(reqPath);
170
- return {
171
- id,
172
- type: "response",
173
- action: "ls",
174
- payload: { path: reqPath, entries },
175
- };
675
+ let output;
676
+ if (platform === "darwin" || platform === "linux") {
677
+ // Use lsof on macOS/Linux
678
+ try {
679
+ output = execSync("lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null || true", {
680
+ encoding: "utf-8",
681
+ timeout: 5000,
682
+ });
683
+ const lines = output.trim().split("\n").slice(1); // Skip header
684
+ for (const line of lines) {
685
+ const parts = line.split(/\s+/);
686
+ if (parts.length >= 9) {
687
+ const processName = parts[0];
688
+ const pid = parseInt(parts[1]);
689
+ const nameField = parts[8];
690
+ // Parse address:port format
691
+ const match = nameField.match(/:(\d+)$/);
692
+ if (match) {
693
+ const port = parseInt(match[1]);
694
+ const address = nameField.replace(`:${port}`, "") || "0.0.0.0";
695
+ ports.push({ port, pid, process: processName, address });
696
+ }
697
+ }
698
+ }
699
+ }
700
+ catch {
701
+ // lsof might fail, try netstat
702
+ output = execSync("netstat -tlnp 2>/dev/null || netstat -an 2>/dev/null || true", {
703
+ encoding: "utf-8",
704
+ timeout: 5000,
705
+ });
176
706
  }
177
- case "read": {
178
- const reqPath = payload.path;
179
- if (!reqPath) {
180
- throw new Error("path is required");
707
+ }
708
+ else if (platform === "win32") {
709
+ output = execSync("netstat -ano | findstr LISTENING", {
710
+ encoding: "utf-8",
711
+ timeout: 5000,
712
+ });
713
+ const lines = output.trim().split("\n");
714
+ for (const line of lines) {
715
+ const parts = line.trim().split(/\s+/);
716
+ if (parts.length >= 5) {
717
+ const localAddr = parts[1];
718
+ const pid = parseInt(parts[4]);
719
+ const match = localAddr.match(/:(\d+)$/);
720
+ if (match) {
721
+ const port = parseInt(match[1]);
722
+ const address = localAddr.replace(`:${port}`, "");
723
+ ports.push({ port, pid, process: "unknown", address });
724
+ }
181
725
  }
182
- const content = await handleRead(reqPath);
183
- return {
184
- id,
185
- type: "response",
186
- action: "read",
187
- payload: { path: reqPath, content },
188
- };
189
726
  }
190
- case "write": {
191
- const reqPath = payload.path;
192
- const content = payload.content;
193
- if (!reqPath) {
194
- throw new Error("path is required");
727
+ }
728
+ }
729
+ catch {
730
+ // Return empty list on error
731
+ }
732
+ return { ports };
733
+ }
734
+ function handlePortsIsAvailable(payload) {
735
+ const port = payload.port;
736
+ if (!port)
737
+ throw Object.assign(new Error("port is required"), { code: "EINVAL" });
738
+ return new Promise((resolve) => {
739
+ const server = createServer();
740
+ server.once("error", (err) => {
741
+ if (err.code === "EADDRINUSE") {
742
+ resolve({ port, available: false });
743
+ }
744
+ else {
745
+ resolve({ port, available: false, error: err.message });
746
+ }
747
+ });
748
+ server.once("listening", () => {
749
+ server.close(() => {
750
+ resolve({ port, available: true });
751
+ });
752
+ });
753
+ server.listen(port, "127.0.0.1");
754
+ });
755
+ }
756
+ function handlePortsKill(payload) {
757
+ const port = payload.port;
758
+ if (!port)
759
+ throw Object.assign(new Error("port is required"), { code: "EINVAL" });
760
+ const platform = os.platform();
761
+ try {
762
+ let pid = null;
763
+ if (platform === "darwin" || platform === "linux") {
764
+ const output = execSync(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf-8" });
765
+ const pids = output.trim().split("\n").filter(Boolean);
766
+ if (pids.length > 0) {
767
+ pid = parseInt(pids[0]);
768
+ execSync(`kill -9 ${pids.join(" ")}`, { encoding: "utf-8" });
769
+ }
770
+ }
771
+ else if (platform === "win32") {
772
+ const output = execSync(`netstat -ano | findstr :${port}`, { encoding: "utf-8" });
773
+ const lines = output.trim().split("\n");
774
+ for (const line of lines) {
775
+ const parts = line.trim().split(/\s+/);
776
+ if (parts.length >= 5) {
777
+ pid = parseInt(parts[4]);
778
+ execSync(`taskkill /F /PID ${pid}`, { encoding: "utf-8" });
779
+ break;
195
780
  }
196
- if (typeof content !== "string") {
197
- throw new Error("content is required");
781
+ }
782
+ }
783
+ return { port, pid };
784
+ }
785
+ catch (err) {
786
+ throw Object.assign(new Error(`Failed to kill process on port ${port}`), { code: "EPERM" });
787
+ }
788
+ }
789
+ // ============================================================================
790
+ // Monitor Handlers
791
+ // ============================================================================
792
+ function getCpuUsage() {
793
+ const cpus = os.cpus();
794
+ const coreUsages = [];
795
+ let totalIdle = 0;
796
+ let totalTick = 0;
797
+ for (let i = 0; i < cpus.length; i++) {
798
+ const cpu = cpus[i];
799
+ const total = Object.values(cpu.times).reduce((a, b) => a + b, 0);
800
+ const idle = cpu.times.idle;
801
+ if (lastCpuInfo && lastCpuInfo[i]) {
802
+ const deltaTotal = total - lastCpuInfo[i].total;
803
+ const deltaIdle = idle - lastCpuInfo[i].idle;
804
+ const usage = deltaTotal > 0 ? ((deltaTotal - deltaIdle) / deltaTotal) * 100 : 0;
805
+ coreUsages.push(Math.round(usage * 10) / 10);
806
+ }
807
+ else {
808
+ coreUsages.push(0);
809
+ }
810
+ totalIdle += idle;
811
+ totalTick += total;
812
+ }
813
+ // Update last CPU info for next calculation
814
+ lastCpuInfo = cpus.map((cpu) => ({
815
+ idle: cpu.times.idle,
816
+ total: Object.values(cpu.times).reduce((a, b) => a + b, 0),
817
+ }));
818
+ const avgUsage = coreUsages.length > 0
819
+ ? coreUsages.reduce((a, b) => a + b, 0) / coreUsages.length
820
+ : 0;
821
+ return { usage: Math.round(avgUsage * 10) / 10, cores: coreUsages };
822
+ }
823
+ function getMemoryInfo() {
824
+ const total = os.totalmem();
825
+ const free = os.freemem();
826
+ const used = total - free;
827
+ const usedPercent = Math.round((used / total) * 1000) / 10;
828
+ return { total, used, free, usedPercent };
829
+ }
830
+ function getDiskInfo() {
831
+ const platform = os.platform();
832
+ const disks = [];
833
+ try {
834
+ if (platform === "darwin" || platform === "linux") {
835
+ const output = execSync("df -k 2>/dev/null || true", { encoding: "utf-8" });
836
+ const lines = output.trim().split("\n").slice(1);
837
+ for (const line of lines) {
838
+ const parts = line.split(/\s+/);
839
+ if (parts.length >= 6) {
840
+ const filesystem = parts[0];
841
+ const size = parseInt(parts[1]) * 1024;
842
+ const used = parseInt(parts[2]) * 1024;
843
+ const free = parseInt(parts[3]) * 1024;
844
+ const mount = parts[5];
845
+ // Skip special filesystems
846
+ if (mount.startsWith("/") && !filesystem.startsWith("devfs") && !filesystem.startsWith("map ")) {
847
+ disks.push({
848
+ mount,
849
+ filesystem,
850
+ size,
851
+ used,
852
+ free,
853
+ usedPercent: size > 0 ? Math.round((used / size) * 1000) / 10 : 0,
854
+ });
855
+ }
198
856
  }
199
- await handleWrite(reqPath, content);
200
- return {
201
- id,
202
- type: "response",
203
- action: "write",
204
- payload: { path: reqPath, success: true },
205
- };
206
857
  }
207
- case "grep": {
208
- const reqPath = payload.path || ".";
209
- const pattern = payload.pattern;
210
- if (!pattern) {
211
- throw new Error("pattern is required");
858
+ }
859
+ else if (platform === "win32") {
860
+ const output = execSync("wmic logicaldisk get size,freespace,caption", { encoding: "utf-8" });
861
+ const lines = output.trim().split("\n").slice(1);
862
+ for (const line of lines) {
863
+ const parts = line.trim().split(/\s+/);
864
+ if (parts.length >= 3) {
865
+ const mount = parts[0];
866
+ const free = parseInt(parts[1]) || 0;
867
+ const size = parseInt(parts[2]) || 0;
868
+ const used = size - free;
869
+ if (size > 0) {
870
+ disks.push({
871
+ mount,
872
+ filesystem: "NTFS",
873
+ size,
874
+ used,
875
+ free,
876
+ usedPercent: Math.round((used / size) * 1000) / 10,
877
+ });
878
+ }
212
879
  }
213
- const matches = await handleGrep(reqPath, pattern, {
214
- caseSensitive: payload.caseSensitive,
215
- maxResults: payload.maxResults,
216
- });
880
+ }
881
+ }
882
+ }
883
+ catch {
884
+ // Return empty on error
885
+ }
886
+ return disks;
887
+ }
888
+ function getBatteryInfo() {
889
+ const platform = os.platform();
890
+ try {
891
+ if (platform === "darwin") {
892
+ const output = execSync("pmset -g batt 2>/dev/null || true", { encoding: "utf-8" });
893
+ const percentMatch = output.match(/(\d+)%/);
894
+ const chargingMatch = output.match(/AC Power|charging|charged/i);
895
+ const timeMatch = output.match(/(\d+):(\d+) remaining/);
896
+ if (percentMatch) {
217
897
  return {
218
- id,
219
- type: "response",
220
- action: "grep",
221
- payload: { path: reqPath, pattern, matches },
898
+ hasBattery: true,
899
+ percent: parseInt(percentMatch[1]),
900
+ charging: !!chargingMatch,
901
+ timeRemaining: timeMatch ? parseInt(timeMatch[1]) * 60 + parseInt(timeMatch[2]) : null,
222
902
  };
223
903
  }
224
- case "replace": {
225
- const reqPath = payload.path;
226
- const search = payload.search;
227
- const replaceWith = payload.replace;
228
- if (!reqPath) {
229
- throw new Error("path is required");
230
- }
231
- if (!search) {
232
- throw new Error("search is required");
904
+ }
905
+ else if (platform === "linux") {
906
+ try {
907
+ const capacityPath = "/sys/class/power_supply/BAT0/capacity";
908
+ const statusPath = "/sys/class/power_supply/BAT0/status";
909
+ const capacity = parseInt(execSync(`cat ${capacityPath} 2>/dev/null || echo 0`, { encoding: "utf-8" }).trim());
910
+ const status = execSync(`cat ${statusPath} 2>/dev/null || echo Unknown`, { encoding: "utf-8" }).trim();
911
+ if (capacity > 0) {
912
+ return {
913
+ hasBattery: true,
914
+ percent: capacity,
915
+ charging: status === "Charging" || status === "Full",
916
+ timeRemaining: null,
917
+ };
233
918
  }
234
- if (typeof replaceWith !== "string") {
235
- throw new Error("replace is required");
919
+ }
920
+ catch {
921
+ // No battery
922
+ }
923
+ }
924
+ else if (platform === "win32") {
925
+ const output = execSync("WMIC Path Win32_Battery Get EstimatedChargeRemaining,BatteryStatus 2>nul || echo", { encoding: "utf-8" });
926
+ const lines = output.trim().split("\n").slice(1);
927
+ if (lines.length > 0) {
928
+ const parts = lines[0].trim().split(/\s+/);
929
+ if (parts.length >= 2) {
930
+ const status = parseInt(parts[0]);
931
+ const percent = parseInt(parts[1]);
932
+ return {
933
+ hasBattery: true,
934
+ percent: percent || 0,
935
+ charging: status === 2 || status === 6, // Charging or Charging High
936
+ timeRemaining: null,
937
+ };
236
938
  }
237
- const result = await handleReplace(reqPath, search, replaceWith, {
238
- caseSensitive: payload.caseSensitive,
239
- replaceAll: payload.replaceAll,
240
- });
241
- return {
242
- id,
243
- type: "response",
244
- action: "replace",
245
- payload: { ...result },
246
- };
247
939
  }
940
+ }
941
+ }
942
+ catch {
943
+ // No battery or error
944
+ }
945
+ return { hasBattery: false, percent: 0, charging: false, timeRemaining: null };
946
+ }
947
+ function handleMonitorSystem() {
948
+ return {
949
+ cpu: getCpuUsage(),
950
+ memory: getMemoryInfo(),
951
+ disk: getDiskInfo(),
952
+ battery: getBatteryInfo(),
953
+ };
954
+ }
955
+ function handleMonitorCpu() {
956
+ const cpuInfo = getCpuUsage();
957
+ const cpus = os.cpus();
958
+ return {
959
+ ...cpuInfo,
960
+ model: cpus[0]?.model || "Unknown",
961
+ speed: cpus[0]?.speed || 0,
962
+ };
963
+ }
964
+ function handleMonitorMemory() {
965
+ return getMemoryInfo();
966
+ }
967
+ function handleMonitorDisk() {
968
+ return { disks: getDiskInfo() };
969
+ }
970
+ function handleMonitorBattery() {
971
+ return getBatteryInfo();
972
+ }
973
+ // ============================================================================
974
+ // HTTP Handlers
975
+ // ============================================================================
976
+ async function handleHttpRequest(payload) {
977
+ const method = payload.method || "GET";
978
+ const url = payload.url;
979
+ const headers = payload.headers || {};
980
+ const body = payload.body;
981
+ const timeout = payload.timeout || 30000;
982
+ if (!url)
983
+ throw Object.assign(new Error("url is required"), { code: "EINVAL" });
984
+ const startTime = Date.now();
985
+ try {
986
+ const controller = new AbortController();
987
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
988
+ const response = await fetch(url, {
989
+ method,
990
+ headers,
991
+ body: body || undefined,
992
+ signal: controller.signal,
993
+ });
994
+ clearTimeout(timeoutId);
995
+ const responseHeaders = {};
996
+ response.headers.forEach((value, key) => {
997
+ responseHeaders[key] = value;
998
+ });
999
+ const responseBody = await response.text();
1000
+ const timing = Date.now() - startTime;
1001
+ return {
1002
+ status: response.status,
1003
+ statusText: response.statusText,
1004
+ headers: responseHeaders,
1005
+ body: responseBody,
1006
+ timing,
1007
+ };
1008
+ }
1009
+ catch (err) {
1010
+ const error = err;
1011
+ if (error.name === "AbortError") {
1012
+ throw Object.assign(new Error("Request timed out"), { code: "ETIMEOUT" });
1013
+ }
1014
+ throw Object.assign(new Error(error.message || "Network error"), { code: "ENETWORK" });
1015
+ }
1016
+ }
1017
+ // ============================================================================
1018
+ // Message Router
1019
+ // ============================================================================
1020
+ async function processMessage(message) {
1021
+ const { v, id, ns, action, payload } = message;
1022
+ // Validate protocol version
1023
+ if (v !== 1) {
1024
+ return {
1025
+ v: 1,
1026
+ id,
1027
+ ns,
1028
+ action,
1029
+ ok: false,
1030
+ payload: {},
1031
+ error: { code: "EPROTO", message: `Unsupported protocol version: ${v}` },
1032
+ };
1033
+ }
1034
+ try {
1035
+ let result;
1036
+ switch (ns) {
1037
+ case "system":
1038
+ switch (action) {
1039
+ case "capabilities":
1040
+ result = handleSystemCapabilities();
1041
+ break;
1042
+ case "ping":
1043
+ result = handleSystemPing();
1044
+ break;
1045
+ default:
1046
+ throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
1047
+ }
1048
+ break;
1049
+ case "fs":
1050
+ switch (action) {
1051
+ case "ls":
1052
+ result = await handleFsLs(payload);
1053
+ break;
1054
+ case "stat":
1055
+ result = await handleFsStat(payload);
1056
+ break;
1057
+ case "read":
1058
+ result = await handleFsRead(payload);
1059
+ break;
1060
+ case "write":
1061
+ result = await handleFsWrite(payload);
1062
+ break;
1063
+ case "mkdir":
1064
+ result = await handleFsMkdir(payload);
1065
+ break;
1066
+ case "rm":
1067
+ result = await handleFsRm(payload);
1068
+ break;
1069
+ case "mv":
1070
+ result = await handleFsMv(payload);
1071
+ break;
1072
+ case "grep":
1073
+ result = await handleFsGrep(payload);
1074
+ break;
1075
+ case "create":
1076
+ result = await handleFsCreate(payload);
1077
+ break;
1078
+ default:
1079
+ throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
1080
+ }
1081
+ break;
1082
+ case "git":
1083
+ switch (action) {
1084
+ case "status":
1085
+ result = await handleGitStatus();
1086
+ break;
1087
+ case "stage":
1088
+ result = await handleGitStage(payload);
1089
+ break;
1090
+ case "unstage":
1091
+ result = await handleGitUnstage(payload);
1092
+ break;
1093
+ case "commit":
1094
+ result = await handleGitCommit(payload);
1095
+ break;
1096
+ case "log":
1097
+ result = await handleGitLog(payload);
1098
+ break;
1099
+ case "diff":
1100
+ result = await handleGitDiff(payload);
1101
+ break;
1102
+ case "branches":
1103
+ result = await handleGitBranches();
1104
+ break;
1105
+ case "checkout":
1106
+ result = await handleGitCheckout(payload);
1107
+ break;
1108
+ case "pull":
1109
+ result = await handleGitPull();
1110
+ break;
1111
+ case "push":
1112
+ result = await handleGitPush(payload);
1113
+ break;
1114
+ case "discard":
1115
+ result = await handleGitDiscard(payload);
1116
+ break;
1117
+ default:
1118
+ throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
1119
+ }
1120
+ break;
1121
+ case "terminal":
1122
+ switch (action) {
1123
+ case "spawn":
1124
+ result = handleTerminalSpawn(payload);
1125
+ break;
1126
+ case "write":
1127
+ result = handleTerminalWrite(payload);
1128
+ break;
1129
+ case "resize":
1130
+ result = handleTerminalResize(payload);
1131
+ break;
1132
+ case "kill":
1133
+ result = handleTerminalKill(payload);
1134
+ break;
1135
+ default:
1136
+ throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
1137
+ }
1138
+ break;
1139
+ case "processes":
1140
+ switch (action) {
1141
+ case "list":
1142
+ result = handleProcessesList();
1143
+ break;
1144
+ case "spawn":
1145
+ result = handleProcessesSpawn(payload);
1146
+ break;
1147
+ case "kill":
1148
+ result = handleProcessesKill(payload);
1149
+ break;
1150
+ case "getOutput":
1151
+ result = handleProcessesGetOutput(payload);
1152
+ break;
1153
+ case "clearOutput":
1154
+ result = handleProcessesClearOutput(payload);
1155
+ break;
1156
+ default:
1157
+ throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
1158
+ }
1159
+ break;
1160
+ case "ports":
1161
+ switch (action) {
1162
+ case "list":
1163
+ result = handlePortsList();
1164
+ break;
1165
+ case "isAvailable":
1166
+ result = await handlePortsIsAvailable(payload);
1167
+ break;
1168
+ case "kill":
1169
+ result = handlePortsKill(payload);
1170
+ break;
1171
+ default:
1172
+ throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
1173
+ }
1174
+ break;
1175
+ case "monitor":
1176
+ switch (action) {
1177
+ case "system":
1178
+ result = handleMonitorSystem();
1179
+ break;
1180
+ case "cpu":
1181
+ result = handleMonitorCpu();
1182
+ break;
1183
+ case "memory":
1184
+ result = handleMonitorMemory();
1185
+ break;
1186
+ case "disk":
1187
+ result = handleMonitorDisk();
1188
+ break;
1189
+ case "battery":
1190
+ result = handleMonitorBattery();
1191
+ break;
1192
+ default:
1193
+ throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
1194
+ }
1195
+ break;
1196
+ case "http":
1197
+ switch (action) {
1198
+ case "request":
1199
+ result = await handleHttpRequest(payload);
1200
+ break;
1201
+ default:
1202
+ throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
1203
+ }
1204
+ break;
248
1205
  default:
249
- throw new Error(`Unknown action: ${action}`);
1206
+ throw Object.assign(new Error(`Unknown namespace: ${ns}`), { code: "EINVAL" });
250
1207
  }
1208
+ return { v: 1, id, ns, action, ok: true, payload: result };
251
1209
  }
252
1210
  catch (error) {
253
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1211
+ const err = error;
254
1212
  return {
1213
+ v: 1,
255
1214
  id,
256
- type: "error",
1215
+ ns,
257
1216
  action,
258
- payload: {
259
- code: error.code || "ERROR",
260
- message: errorMessage,
1217
+ ok: false,
1218
+ payload: {},
1219
+ error: {
1220
+ code: err.code || "ERROR",
1221
+ message: err.message || "Unknown error",
261
1222
  },
262
1223
  };
263
1224
  }
264
1225
  }
1226
+ // ============================================================================
1227
+ // WebSocket Connection
1228
+ // ============================================================================
265
1229
  async function createSession() {
266
1230
  const response = await fetch(`${PROXY_URL}/v1/session`, {
267
1231
  method: "POST",
268
- headers: {
269
- "Content-Type": "application/json",
270
- },
1232
+ headers: { "Content-Type": "application/json" },
271
1233
  });
272
1234
  if (!response.ok) {
273
1235
  throw new Error(`Failed to create session: ${response.status}`);
@@ -286,13 +1248,29 @@ function displayQR(code) {
286
1248
  });
287
1249
  }
288
1250
  function connectWebSocket(code) {
289
- const wsUrl = PROXY_URL.replace(/^http/, "ws") + `/v1/ws/cli?code=${code}`;
1251
+ const wsBase = PROXY_URL.replace(/^http/, "ws");
1252
+ const controlUrl = `${wsBase}/v1/ws/cli/control?code=${code}`;
1253
+ const dataUrl = `${wsBase}/v1/ws/cli/data?code=${code}`;
290
1254
  console.log("Connecting to proxy...");
291
- const ws = new WebSocket(wsUrl);
292
- ws.on("open", () => {
293
- console.log("Connected to proxy. Waiting for app...\n");
1255
+ // Control channel
1256
+ const controlWs = new WebSocket(controlUrl);
1257
+ let controlConnected = false;
1258
+ // Data channel
1259
+ const dataWs = new WebSocket(dataUrl);
1260
+ let dataConnected = false;
1261
+ // Store data channel reference for terminal output
1262
+ dataChannel = dataWs;
1263
+ function checkFullyConnected() {
1264
+ if (controlConnected && dataConnected) {
1265
+ console.log("Connected to proxy (control + data channels). Waiting for app...\n");
1266
+ }
1267
+ }
1268
+ // Control channel handlers
1269
+ controlWs.on("open", () => {
1270
+ controlConnected = true;
1271
+ checkFullyConnected();
294
1272
  });
295
- ws.on("message", async (data) => {
1273
+ controlWs.on("message", async (data) => {
296
1274
  try {
297
1275
  const message = JSON.parse(data.toString());
298
1276
  // Handle system messages
@@ -307,38 +1285,76 @@ function connectWebSocket(code) {
307
1285
  console.log("App disconnected.\n");
308
1286
  return;
309
1287
  }
310
- // Handle file operation requests
311
- if (message.type === "request") {
312
- const response = await processRequest(message);
313
- ws.send(JSON.stringify(response));
1288
+ // Handle v1 protocol messages
1289
+ if (message.v === 1) {
1290
+ const response = await processMessage(message);
1291
+ controlWs.send(JSON.stringify(response));
314
1292
  }
315
- // Handle proxy errors
316
- if (message.type === "error" && !("action" in message)) {
317
- console.error("Proxy error:", message.payload?.message);
1293
+ }
1294
+ catch (error) {
1295
+ console.error("Error processing control message:", error);
1296
+ }
1297
+ });
1298
+ controlWs.on("close", (code, reason) => {
1299
+ console.log(`\nControl channel disconnected (${code}: ${reason.toString()})`);
1300
+ dataWs.close();
1301
+ process.exit(0);
1302
+ });
1303
+ controlWs.on("error", (error) => {
1304
+ console.error("Control WebSocket error:", error.message);
1305
+ });
1306
+ // Data channel handlers
1307
+ dataWs.on("open", () => {
1308
+ dataConnected = true;
1309
+ checkFullyConnected();
1310
+ });
1311
+ dataWs.on("message", async (data) => {
1312
+ try {
1313
+ const message = JSON.parse(data.toString());
1314
+ // Handle system messages
1315
+ if (message.type === "connected") {
318
1316
  return;
319
1317
  }
1318
+ // Handle v1 protocol messages
1319
+ if (message.v === 1) {
1320
+ const response = await processMessage(message);
1321
+ dataWs.send(JSON.stringify(response));
1322
+ }
320
1323
  }
321
1324
  catch (error) {
322
- console.error("Error processing message:", error);
1325
+ console.error("Error processing data message:", error);
323
1326
  }
324
1327
  });
325
- ws.on("close", (code, reason) => {
326
- console.log(`\nDisconnected from proxy (${code}: ${reason.toString()})`);
1328
+ dataWs.on("close", (code, reason) => {
1329
+ console.log(`\nData channel disconnected (${code}: ${reason.toString()})`);
1330
+ controlWs.close();
327
1331
  process.exit(0);
328
1332
  });
329
- ws.on("error", (error) => {
330
- console.error("WebSocket error:", error.message);
1333
+ dataWs.on("error", (error) => {
1334
+ console.error("Data WebSocket error:", error.message);
331
1335
  });
332
1336
  // Handle graceful shutdown
333
1337
  process.on("SIGINT", () => {
334
1338
  console.log("\nShutting down...");
335
- ws.close();
1339
+ // Kill all terminals
1340
+ for (const [id, proc] of terminals) {
1341
+ proc.kill();
1342
+ }
1343
+ terminals.clear();
1344
+ // Kill all managed processes
1345
+ for (const [pid, managedProc] of processes) {
1346
+ managedProc.proc.kill();
1347
+ }
1348
+ processes.clear();
1349
+ processOutputBuffers.clear();
1350
+ controlWs.close();
1351
+ dataWs.close();
336
1352
  process.exit(0);
337
1353
  });
338
1354
  }
339
1355
  async function main() {
340
- console.log("Lunel CLI");
341
- console.log("=========\n");
1356
+ console.log("Lunel CLI v" + VERSION);
1357
+ console.log("=".repeat(20) + "\n");
342
1358
  try {
343
1359
  const code = await createSession();
344
1360
  displayQR(code);