lunel-cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1184 -168
- 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
|
-
|
|
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 = "0.1.3";
|
|
9
13
|
// Root directory - sandbox all file operations to this
|
|
10
14
|
const ROOT_DIR = process.cwd();
|
|
11
|
-
//
|
|
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
|
-
|
|
22
|
-
async function handleLs(requestedPath) {
|
|
31
|
+
function assertSafePath(requestedPath) {
|
|
23
32
|
const safePath = resolveSafePath(requestedPath);
|
|
24
33
|
if (!safePath) {
|
|
25
|
-
|
|
34
|
+
const error = new Error("Access denied: path outside root directory");
|
|
35
|
+
error.code = "EACCES";
|
|
36
|
+
throw error;
|
|
26
37
|
}
|
|
27
|
-
|
|
28
|
-
return entries.map((entry) => ({
|
|
29
|
-
name: entry.name,
|
|
30
|
-
type: entry.isDirectory() ? "directory" : "file",
|
|
31
|
-
}));
|
|
38
|
+
return safePath;
|
|
32
39
|
}
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
return content;
|
|
66
|
+
return { path: reqPath, entries: result };
|
|
41
67
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
167
|
+
// No .gitignore
|
|
65
168
|
}
|
|
66
169
|
return ig;
|
|
67
170
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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),
|
|
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
|
|
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,
|
|
234
|
+
await searchDir(safePath, reqPath === "." ? "" : reqPath, rootIgnore);
|
|
137
235
|
}
|
|
138
236
|
else {
|
|
139
|
-
await searchFile(safePath,
|
|
237
|
+
await searchFile(safePath, reqPath);
|
|
140
238
|
}
|
|
141
|
-
return matches;
|
|
239
|
+
return { matches };
|
|
142
240
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
if (!
|
|
147
|
-
throw new Error("
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
if (
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
|
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
|
|
1211
|
+
const err = error;
|
|
254
1212
|
return {
|
|
1213
|
+
v: 1,
|
|
255
1214
|
id,
|
|
256
|
-
|
|
1215
|
+
ns,
|
|
257
1216
|
action,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
|
311
|
-
if (message.
|
|
312
|
-
const response = await
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
326
|
-
console.log(`\
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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);
|