lunel-cli 0.1.0 → 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.
- package/dist/index.js +1338 -6
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,1235 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { WebSocket } from "ws";
|
|
2
3
|
import qrcode from "qrcode-terminal";
|
|
3
|
-
|
|
4
|
+
import Ignore from "ignore";
|
|
5
|
+
const ignore = Ignore.default;
|
|
6
|
+
import * as fs from "fs/promises";
|
|
7
|
+
import * as path from "path";
|
|
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";
|
|
13
|
+
// Root directory - sandbox all file operations to this
|
|
14
|
+
const ROOT_DIR = process.cwd();
|
|
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
|
+
// ============================================================================
|
|
24
|
+
function resolveSafePath(requestedPath) {
|
|
25
|
+
const resolved = path.resolve(ROOT_DIR, requestedPath);
|
|
26
|
+
if (!resolved.startsWith(ROOT_DIR)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return resolved;
|
|
30
|
+
}
|
|
31
|
+
function assertSafePath(requestedPath) {
|
|
32
|
+
const safePath = resolveSafePath(requestedPath);
|
|
33
|
+
if (!safePath) {
|
|
34
|
+
const error = new Error("Access denied: path outside root directory");
|
|
35
|
+
error.code = "EACCES";
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return safePath;
|
|
39
|
+
}
|
|
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);
|
|
65
|
+
}
|
|
66
|
+
return { path: reqPath, entries: result };
|
|
67
|
+
}
|
|
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
|
+
};
|
|
100
|
+
}
|
|
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);
|
|
117
|
+
const parentDir = path.dirname(safePath);
|
|
118
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
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 };
|
|
126
|
+
}
|
|
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
|
|
158
|
+
async function loadGitignore(dirPath) {
|
|
159
|
+
const ig = ignore();
|
|
160
|
+
ig.add(".git");
|
|
161
|
+
try {
|
|
162
|
+
const gitignorePath = path.join(dirPath, ".gitignore");
|
|
163
|
+
const content = await fs.readFile(gitignorePath, "utf-8");
|
|
164
|
+
ig.add(content);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// No .gitignore
|
|
168
|
+
}
|
|
169
|
+
return ig;
|
|
170
|
+
}
|
|
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);
|
|
179
|
+
const matches = [];
|
|
180
|
+
const regex = new RegExp(pattern, caseSensitive ? "g" : "gi");
|
|
181
|
+
const rootIgnore = await loadGitignore(ROOT_DIR);
|
|
182
|
+
async function searchFile(filePath, relativePath) {
|
|
183
|
+
if (matches.length >= maxResults)
|
|
184
|
+
return;
|
|
185
|
+
try {
|
|
186
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
187
|
+
const lines = content.split("\n");
|
|
188
|
+
for (let i = 0; i < lines.length && matches.length < maxResults; i++) {
|
|
189
|
+
if (regex.test(lines[i])) {
|
|
190
|
+
matches.push({
|
|
191
|
+
file: relativePath,
|
|
192
|
+
line: i + 1,
|
|
193
|
+
content: lines[i].substring(0, 500),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
regex.lastIndex = 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Skip unreadable files
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function searchDir(dirPath, relativePath, ig) {
|
|
204
|
+
if (matches.length >= maxResults)
|
|
205
|
+
return;
|
|
206
|
+
const localIgnore = ignore().add(ig);
|
|
207
|
+
try {
|
|
208
|
+
const localGitignorePath = path.join(dirPath, ".gitignore");
|
|
209
|
+
const content = await fs.readFile(localGitignorePath, "utf-8");
|
|
210
|
+
localIgnore.add(content);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// No local .gitignore
|
|
214
|
+
}
|
|
215
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
if (matches.length >= maxResults)
|
|
218
|
+
break;
|
|
219
|
+
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
220
|
+
const checkPath = entry.isDirectory() ? `${relPath}/` : relPath;
|
|
221
|
+
if (localIgnore.ignores(checkPath))
|
|
222
|
+
continue;
|
|
223
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
await searchDir(fullPath, relPath, localIgnore);
|
|
226
|
+
}
|
|
227
|
+
else if (entry.isFile()) {
|
|
228
|
+
await searchFile(fullPath, relPath);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const stat = await fs.stat(safePath);
|
|
233
|
+
if (stat.isDirectory()) {
|
|
234
|
+
await searchDir(safePath, reqPath === "." ? "" : reqPath, rootIgnore);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
await searchFile(safePath, reqPath);
|
|
238
|
+
}
|
|
239
|
+
return { matches };
|
|
240
|
+
}
|
|
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 = [];
|
|
674
|
+
try {
|
|
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
|
+
});
|
|
706
|
+
}
|
|
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
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
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;
|
|
780
|
+
}
|
|
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
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
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
|
+
}
|
|
879
|
+
}
|
|
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) {
|
|
897
|
+
return {
|
|
898
|
+
hasBattery: true,
|
|
899
|
+
percent: parseInt(percentMatch[1]),
|
|
900
|
+
charging: !!chargingMatch,
|
|
901
|
+
timeRemaining: timeMatch ? parseInt(timeMatch[1]) * 60 + parseInt(timeMatch[2]) : null,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
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
|
+
};
|
|
918
|
+
}
|
|
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
|
+
};
|
|
938
|
+
}
|
|
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;
|
|
1205
|
+
default:
|
|
1206
|
+
throw Object.assign(new Error(`Unknown namespace: ${ns}`), { code: "EINVAL" });
|
|
1207
|
+
}
|
|
1208
|
+
return { v: 1, id, ns, action, ok: true, payload: result };
|
|
1209
|
+
}
|
|
1210
|
+
catch (error) {
|
|
1211
|
+
const err = error;
|
|
1212
|
+
return {
|
|
1213
|
+
v: 1,
|
|
1214
|
+
id,
|
|
1215
|
+
ns,
|
|
1216
|
+
action,
|
|
1217
|
+
ok: false,
|
|
1218
|
+
payload: {},
|
|
1219
|
+
error: {
|
|
1220
|
+
code: err.code || "ERROR",
|
|
1221
|
+
message: err.message || "Unknown error",
|
|
1222
|
+
},
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
// ============================================================================
|
|
1227
|
+
// WebSocket Connection
|
|
1228
|
+
// ============================================================================
|
|
4
1229
|
async function createSession() {
|
|
5
1230
|
const response = await fetch(`${PROXY_URL}/v1/session`, {
|
|
6
1231
|
method: "POST",
|
|
7
|
-
headers: {
|
|
8
|
-
"Content-Type": "application/json",
|
|
9
|
-
},
|
|
1232
|
+
headers: { "Content-Type": "application/json" },
|
|
10
1233
|
});
|
|
11
1234
|
if (!response.ok) {
|
|
12
1235
|
throw new Error(`Failed to create session: ${response.status}`);
|
|
@@ -19,14 +1242,123 @@ function displayQR(code) {
|
|
|
19
1242
|
qrcode.generate(code, { small: true }, (qr) => {
|
|
20
1243
|
console.log(qr);
|
|
21
1244
|
console.log(`\n Session code: ${code}\n`);
|
|
22
|
-
console.log(
|
|
1245
|
+
console.log(` Root directory: ${ROOT_DIR}\n`);
|
|
1246
|
+
console.log(" Scan the QR code with the Lunel app to connect.");
|
|
1247
|
+
console.log(" Press Ctrl+C to exit.\n");
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
function connectWebSocket(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}`;
|
|
1254
|
+
console.log("Connecting to proxy...");
|
|
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();
|
|
1272
|
+
});
|
|
1273
|
+
controlWs.on("message", async (data) => {
|
|
1274
|
+
try {
|
|
1275
|
+
const message = JSON.parse(data.toString());
|
|
1276
|
+
// Handle system messages
|
|
1277
|
+
if (message.type === "connected") {
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
if (message.type === "peer_connected") {
|
|
1281
|
+
console.log("App connected!\n");
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (message.type === "peer_disconnected") {
|
|
1285
|
+
console.log("App disconnected.\n");
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
// Handle v1 protocol messages
|
|
1289
|
+
if (message.v === 1) {
|
|
1290
|
+
const response = await processMessage(message);
|
|
1291
|
+
controlWs.send(JSON.stringify(response));
|
|
1292
|
+
}
|
|
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") {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
// Handle v1 protocol messages
|
|
1319
|
+
if (message.v === 1) {
|
|
1320
|
+
const response = await processMessage(message);
|
|
1321
|
+
dataWs.send(JSON.stringify(response));
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
catch (error) {
|
|
1325
|
+
console.error("Error processing data message:", error);
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
dataWs.on("close", (code, reason) => {
|
|
1329
|
+
console.log(`\nData channel disconnected (${code}: ${reason.toString()})`);
|
|
1330
|
+
controlWs.close();
|
|
1331
|
+
process.exit(0);
|
|
1332
|
+
});
|
|
1333
|
+
dataWs.on("error", (error) => {
|
|
1334
|
+
console.error("Data WebSocket error:", error.message);
|
|
1335
|
+
});
|
|
1336
|
+
// Handle graceful shutdown
|
|
1337
|
+
process.on("SIGINT", () => {
|
|
1338
|
+
console.log("\nShutting down...");
|
|
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();
|
|
1352
|
+
process.exit(0);
|
|
23
1353
|
});
|
|
24
1354
|
}
|
|
25
1355
|
async function main() {
|
|
26
|
-
console.log("
|
|
1356
|
+
console.log("Lunel CLI v" + VERSION);
|
|
1357
|
+
console.log("=".repeat(20) + "\n");
|
|
27
1358
|
try {
|
|
28
1359
|
const code = await createSession();
|
|
29
1360
|
displayQR(code);
|
|
1361
|
+
connectWebSocket(code);
|
|
30
1362
|
}
|
|
31
1363
|
catch (error) {
|
|
32
1364
|
if (error instanceof Error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lunel-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"author": [
|
|
5
5
|
{
|
|
6
6
|
"name": "Soham Bharambe",
|
|
@@ -25,11 +25,14 @@
|
|
|
25
25
|
"prepublishOnly": "npm run build"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"
|
|
28
|
+
"ignore": "^6.0.2",
|
|
29
|
+
"qrcode-terminal": "^0.12.0",
|
|
30
|
+
"ws": "^8.18.0"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
33
|
"@types/node": "^20.0.0",
|
|
32
34
|
"@types/qrcode-terminal": "^0.12.2",
|
|
35
|
+
"@types/ws": "^8.5.13",
|
|
33
36
|
"typescript": "^5.0.0"
|
|
34
37
|
},
|
|
35
38
|
"engines": {
|