@babylen/legion 0.1.4 → 0.1.5
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 +449 -49
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var import_child_process = require("child_process");
|
|
28
28
|
|
|
29
29
|
// package.json
|
|
30
|
-
var version = "0.1.
|
|
30
|
+
var version = "0.1.5";
|
|
31
31
|
|
|
32
32
|
// src/index.ts
|
|
33
33
|
var import_socket = require("socket.io-client");
|
|
@@ -43,7 +43,9 @@ var ConfigSchema = import_zod.z.object({
|
|
|
43
43
|
// Token ID from database (for faster lookup)
|
|
44
44
|
token: import_zod.z.string(),
|
|
45
45
|
// Secret token (lg_xxx format)
|
|
46
|
-
serverUrl: import_zod.z.string().url()
|
|
46
|
+
serverUrl: import_zod.z.string().url(),
|
|
47
|
+
allowedPaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
48
|
+
// Whitelist paths for file system access
|
|
47
49
|
});
|
|
48
50
|
var HOME_DIR = import_os.default.homedir();
|
|
49
51
|
var CONFIG_DIR = import_path.default.join(HOME_DIR, ".tanuki");
|
|
@@ -96,8 +98,17 @@ async function getConfig() {
|
|
|
96
98
|
if (id) {
|
|
97
99
|
config2.id = id;
|
|
98
100
|
}
|
|
101
|
+
if (fileConfig?.allowedPaths) {
|
|
102
|
+
config2.allowedPaths = fileConfig.allowedPaths;
|
|
103
|
+
}
|
|
99
104
|
return config2;
|
|
100
105
|
}
|
|
106
|
+
function getAllowedPaths(config2) {
|
|
107
|
+
if (config2.allowedPaths && config2.allowedPaths.length > 0) {
|
|
108
|
+
return config2.allowedPaths.map((p) => import_path.default.resolve(p));
|
|
109
|
+
}
|
|
110
|
+
return [HOME_DIR];
|
|
111
|
+
}
|
|
101
112
|
async function updateConfig(updates) {
|
|
102
113
|
const currentConfig = await loadConfig();
|
|
103
114
|
if (!currentConfig) {
|
|
@@ -326,6 +337,389 @@ async function getSystemFingerprint() {
|
|
|
326
337
|
return cachedFingerprint;
|
|
327
338
|
}
|
|
328
339
|
|
|
340
|
+
// src/file/legion-bridge.ts
|
|
341
|
+
var import_promises4 = __toESM(require("fs/promises"));
|
|
342
|
+
var import_path4 = __toESM(require("path"));
|
|
343
|
+
var log = Log.create({ service: "file.legion-bridge" });
|
|
344
|
+
async function listFiles(targetPath, depth = 1) {
|
|
345
|
+
const resolved = import_path4.default.resolve(targetPath);
|
|
346
|
+
const exclude = [".git", ".DS_Store", ".tanuki"];
|
|
347
|
+
const nodes = [];
|
|
348
|
+
try {
|
|
349
|
+
const entries = await import_promises4.default.readdir(resolved, { withFileTypes: true });
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
if (exclude.includes(entry.name)) continue;
|
|
352
|
+
const fullPath = import_path4.default.join(resolved, entry.name);
|
|
353
|
+
const isDirectory = entry.isDirectory();
|
|
354
|
+
const node = {
|
|
355
|
+
name: entry.name,
|
|
356
|
+
path: fullPath,
|
|
357
|
+
type: isDirectory ? "directory" : "file"
|
|
358
|
+
};
|
|
359
|
+
if (!isDirectory) {
|
|
360
|
+
try {
|
|
361
|
+
const stats = await import_promises4.default.stat(fullPath);
|
|
362
|
+
node.size = stats.size;
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
nodes.push(node);
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
log.error("Failed to list directory", { error, targetPath });
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
return nodes.sort((a, b) => {
|
|
373
|
+
if (a.type !== b.type) {
|
|
374
|
+
return a.type === "directory" ? -1 : 1;
|
|
375
|
+
}
|
|
376
|
+
return a.name.localeCompare(b.name);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
async function shouldEncode(file) {
|
|
380
|
+
const type = file.type?.toLowerCase();
|
|
381
|
+
if (!type) return false;
|
|
382
|
+
if (type.startsWith("text/")) return false;
|
|
383
|
+
if (type.includes("charset=")) return false;
|
|
384
|
+
const parts = type.split("/", 2);
|
|
385
|
+
const top = parts[0];
|
|
386
|
+
const rest = parts[1] ?? "";
|
|
387
|
+
const sub = rest.split(";", 1)[0];
|
|
388
|
+
const tops = ["image", "audio", "video", "font", "model", "multipart"];
|
|
389
|
+
if (tops.includes(top)) return true;
|
|
390
|
+
const bins = [
|
|
391
|
+
"zip",
|
|
392
|
+
"gzip",
|
|
393
|
+
"bzip",
|
|
394
|
+
"compressed",
|
|
395
|
+
"binary",
|
|
396
|
+
"pdf",
|
|
397
|
+
"msword",
|
|
398
|
+
"vnd.ms",
|
|
399
|
+
"octet-stream"
|
|
400
|
+
];
|
|
401
|
+
if (bins.includes(sub)) return true;
|
|
402
|
+
try {
|
|
403
|
+
const buffer = await file.arrayBuffer();
|
|
404
|
+
if (buffer.byteLength === 0) return false;
|
|
405
|
+
const view = new Uint8Array(buffer.slice(0, 512));
|
|
406
|
+
return view.some((byte) => byte === 0);
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function readFile(filePath, maxSize = 1024 * 1024) {
|
|
412
|
+
const resolved = import_path4.default.resolve(filePath);
|
|
413
|
+
const bunFile = Bun.file(resolved);
|
|
414
|
+
if (!await bunFile.exists()) {
|
|
415
|
+
throw new Error(`File not found: ${filePath}`);
|
|
416
|
+
}
|
|
417
|
+
let stats;
|
|
418
|
+
try {
|
|
419
|
+
stats = await import_promises4.default.stat(resolved);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
throw new Error(`Failed to stat file: ${filePath}`);
|
|
422
|
+
}
|
|
423
|
+
if (stats.size > maxSize) {
|
|
424
|
+
return {
|
|
425
|
+
type: "blob",
|
|
426
|
+
size: stats.size,
|
|
427
|
+
error: "too_large"
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const encode = await shouldEncode(bunFile);
|
|
431
|
+
if (encode) {
|
|
432
|
+
const buffer = await bunFile.arrayBuffer();
|
|
433
|
+
const content2 = Buffer.from(buffer).toString("base64");
|
|
434
|
+
const mimeType = bunFile.type || "application/octet-stream";
|
|
435
|
+
return {
|
|
436
|
+
type: "blob",
|
|
437
|
+
content: content2,
|
|
438
|
+
encoding: "base64",
|
|
439
|
+
mimeType,
|
|
440
|
+
size: stats.size
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const content = await bunFile.text().catch(() => "");
|
|
444
|
+
return {
|
|
445
|
+
type: "text",
|
|
446
|
+
content,
|
|
447
|
+
encoding: "utf-8",
|
|
448
|
+
size: stats.size
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/core/path-validator.ts
|
|
453
|
+
var import_path6 = __toESM(require("path"));
|
|
454
|
+
var import_fs3 = require("fs");
|
|
455
|
+
|
|
456
|
+
// src/util/filesystem.ts
|
|
457
|
+
var import_fs2 = require("fs");
|
|
458
|
+
var import_path5 = require("path");
|
|
459
|
+
var Filesystem;
|
|
460
|
+
((Filesystem2) => {
|
|
461
|
+
Filesystem2.exists = (p) => Bun.file(p).stat().then(() => true).catch(() => false);
|
|
462
|
+
Filesystem2.isDir = (p) => Bun.file(p).stat().then((s) => s.isDirectory()).catch(() => false);
|
|
463
|
+
function normalizePath(p) {
|
|
464
|
+
if (process.platform !== "win32") return p;
|
|
465
|
+
try {
|
|
466
|
+
return import_fs2.realpathSync.native(p);
|
|
467
|
+
} catch {
|
|
468
|
+
return p;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
Filesystem2.normalizePath = normalizePath;
|
|
472
|
+
function overlaps(a, b) {
|
|
473
|
+
const relA = (0, import_path5.relative)(a, b);
|
|
474
|
+
const relB = (0, import_path5.relative)(b, a);
|
|
475
|
+
return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..");
|
|
476
|
+
}
|
|
477
|
+
Filesystem2.overlaps = overlaps;
|
|
478
|
+
function contains(parent, child) {
|
|
479
|
+
return !(0, import_path5.relative)(parent, child).startsWith("..");
|
|
480
|
+
}
|
|
481
|
+
Filesystem2.contains = contains;
|
|
482
|
+
async function findUp(target, start, stop) {
|
|
483
|
+
let current = start;
|
|
484
|
+
const result = [];
|
|
485
|
+
while (true) {
|
|
486
|
+
const search = (0, import_path5.join)(current, target);
|
|
487
|
+
if (await (0, Filesystem2.exists)(search)) result.push(search);
|
|
488
|
+
if (stop === current) break;
|
|
489
|
+
const parent = (0, import_path5.dirname)(current);
|
|
490
|
+
if (parent === current) break;
|
|
491
|
+
current = parent;
|
|
492
|
+
}
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
Filesystem2.findUp = findUp;
|
|
496
|
+
async function* up(options) {
|
|
497
|
+
const { targets, start, stop } = options;
|
|
498
|
+
let current = start;
|
|
499
|
+
while (true) {
|
|
500
|
+
for (const target of targets) {
|
|
501
|
+
const search = (0, import_path5.join)(current, target);
|
|
502
|
+
if (await (0, Filesystem2.exists)(search)) yield search;
|
|
503
|
+
}
|
|
504
|
+
if (stop === current) break;
|
|
505
|
+
const parent = (0, import_path5.dirname)(current);
|
|
506
|
+
if (parent === current) break;
|
|
507
|
+
current = parent;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
Filesystem2.up = up;
|
|
511
|
+
async function globUp(pattern, start, stop) {
|
|
512
|
+
let current = start;
|
|
513
|
+
const result = [];
|
|
514
|
+
while (true) {
|
|
515
|
+
try {
|
|
516
|
+
const glob = new Bun.Glob(pattern);
|
|
517
|
+
for await (const match of glob.scan({
|
|
518
|
+
cwd: current,
|
|
519
|
+
absolute: true,
|
|
520
|
+
onlyFiles: true,
|
|
521
|
+
followSymlinks: true,
|
|
522
|
+
dot: true
|
|
523
|
+
})) {
|
|
524
|
+
result.push(match);
|
|
525
|
+
}
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
if (stop === current) break;
|
|
529
|
+
const parent = (0, import_path5.dirname)(current);
|
|
530
|
+
if (parent === current) break;
|
|
531
|
+
current = parent;
|
|
532
|
+
}
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
Filesystem2.globUp = globUp;
|
|
536
|
+
})(Filesystem || (Filesystem = {}));
|
|
537
|
+
|
|
538
|
+
// src/core/path-validator.ts
|
|
539
|
+
var log2 = Log.create({ service: "path-validator" });
|
|
540
|
+
function isPathAllowed(requestedPath, allowedPaths) {
|
|
541
|
+
try {
|
|
542
|
+
const normalized = import_path6.default.resolve(requestedPath);
|
|
543
|
+
let realPath;
|
|
544
|
+
try {
|
|
545
|
+
realPath = (0, import_fs3.realpathSync)(normalized);
|
|
546
|
+
} catch {
|
|
547
|
+
realPath = normalized;
|
|
548
|
+
}
|
|
549
|
+
for (const allowed of allowedPaths) {
|
|
550
|
+
const allowedResolved = import_path6.default.resolve(allowed);
|
|
551
|
+
if (Filesystem.contains(allowedResolved, realPath)) {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
log2.warn("Path access denied", { requestedPath, realPath, allowedPaths });
|
|
556
|
+
return false;
|
|
557
|
+
} catch (error) {
|
|
558
|
+
log2.error("Path validation error", { error, requestedPath });
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/socket/handlers/fs-list.ts
|
|
564
|
+
async function handleFsList(req, socket) {
|
|
565
|
+
const { path: requestedPath, depth = 1 } = req;
|
|
566
|
+
const targetPath = requestedPath || process.cwd();
|
|
567
|
+
const config2 = socket.legionConfig;
|
|
568
|
+
const allowedPaths = getAllowedPaths(config2);
|
|
569
|
+
if (!isPathAllowed(targetPath, allowedPaths)) {
|
|
570
|
+
return {
|
|
571
|
+
id: req.id,
|
|
572
|
+
status: "error",
|
|
573
|
+
error: "Access denied: path not in whitelist"
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const files = await listFiles(targetPath, depth);
|
|
578
|
+
return {
|
|
579
|
+
id: req.id,
|
|
580
|
+
status: "ok",
|
|
581
|
+
data: files
|
|
582
|
+
};
|
|
583
|
+
} catch (error) {
|
|
584
|
+
return {
|
|
585
|
+
id: req.id,
|
|
586
|
+
status: "error",
|
|
587
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/socket/handlers/fs-read.ts
|
|
593
|
+
async function handleFsRead(req, socket) {
|
|
594
|
+
const { path: requestedPath, maxSize } = req;
|
|
595
|
+
if (!requestedPath) {
|
|
596
|
+
return {
|
|
597
|
+
id: req.id,
|
|
598
|
+
status: "error",
|
|
599
|
+
error: "Path is required"
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const config2 = socket.legionConfig;
|
|
603
|
+
const allowedPaths = getAllowedPaths(config2);
|
|
604
|
+
if (!isPathAllowed(requestedPath, allowedPaths)) {
|
|
605
|
+
return {
|
|
606
|
+
id: req.id,
|
|
607
|
+
status: "error",
|
|
608
|
+
error: "Access denied: path not in whitelist"
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
const content = await readFile(requestedPath, maxSize || 1024 * 1024);
|
|
613
|
+
return {
|
|
614
|
+
id: req.id,
|
|
615
|
+
status: "ok",
|
|
616
|
+
data: content
|
|
617
|
+
};
|
|
618
|
+
} catch (error) {
|
|
619
|
+
return {
|
|
620
|
+
id: req.id,
|
|
621
|
+
status: "error",
|
|
622
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/project/binding.ts
|
|
628
|
+
var import_promises5 = __toESM(require("fs/promises"));
|
|
629
|
+
var import_path7 = __toESM(require("path"));
|
|
630
|
+
var log3 = Log.create({ service: "project.binding" });
|
|
631
|
+
async function bindProject(targetPath, projectId, projectName) {
|
|
632
|
+
const resolved = import_path7.default.resolve(targetPath);
|
|
633
|
+
const tanukiDir = import_path7.default.join(resolved, ".tanuki");
|
|
634
|
+
const projectFile = import_path7.default.join(tanukiDir, "project.json");
|
|
635
|
+
await import_promises5.default.mkdir(tanukiDir, { recursive: true });
|
|
636
|
+
const config2 = {
|
|
637
|
+
id: projectId,
|
|
638
|
+
name: projectName || import_path7.default.basename(resolved)
|
|
639
|
+
};
|
|
640
|
+
await import_promises5.default.writeFile(projectFile, JSON.stringify(config2, null, 2), "utf-8");
|
|
641
|
+
const gitignorePath = import_path7.default.join(resolved, ".gitignore");
|
|
642
|
+
try {
|
|
643
|
+
let gitignoreContent = await import_promises5.default.readFile(gitignorePath, "utf-8");
|
|
644
|
+
if (!gitignoreContent.includes(".tanuki")) {
|
|
645
|
+
gitignoreContent += "\n.tanuki\n";
|
|
646
|
+
await import_promises5.default.writeFile(gitignorePath, gitignoreContent, "utf-8");
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
await import_promises5.default.writeFile(gitignorePath, ".tanuki\n", "utf-8");
|
|
650
|
+
}
|
|
651
|
+
log3.info("Project bound", { path: resolved, projectId });
|
|
652
|
+
return projectFile;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/socket/handlers/project-bind.ts
|
|
656
|
+
async function handleProjectBind(req, socket) {
|
|
657
|
+
const { path: requestedPath, projectId, projectName } = req;
|
|
658
|
+
if (!requestedPath) {
|
|
659
|
+
return {
|
|
660
|
+
id: req.id,
|
|
661
|
+
status: "error",
|
|
662
|
+
error: "Path is required"
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
if (!projectId) {
|
|
666
|
+
return {
|
|
667
|
+
id: req.id,
|
|
668
|
+
status: "error",
|
|
669
|
+
error: "projectId is required"
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
const config2 = socket.legionConfig;
|
|
673
|
+
const allowedPaths = getAllowedPaths(config2);
|
|
674
|
+
if (!isPathAllowed(requestedPath, allowedPaths)) {
|
|
675
|
+
return {
|
|
676
|
+
id: req.id,
|
|
677
|
+
status: "error",
|
|
678
|
+
error: "Access denied: path not in whitelist"
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
const configPath = await bindProject(requestedPath, projectId, projectName);
|
|
683
|
+
return {
|
|
684
|
+
id: req.id,
|
|
685
|
+
status: "ok",
|
|
686
|
+
data: { configPath }
|
|
687
|
+
};
|
|
688
|
+
} catch (error) {
|
|
689
|
+
return {
|
|
690
|
+
id: req.id,
|
|
691
|
+
status: "error",
|
|
692
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/socket/dispatcher.ts
|
|
698
|
+
var handlers = {
|
|
699
|
+
"legion:fs:list": handleFsList,
|
|
700
|
+
"legion:fs:read": handleFsRead,
|
|
701
|
+
"legion:project:bind": handleProjectBind
|
|
702
|
+
};
|
|
703
|
+
function setupDispatcher(socket, log4, config2) {
|
|
704
|
+
socket.legionConfig = config2;
|
|
705
|
+
for (const [event, handler] of Object.entries(handlers)) {
|
|
706
|
+
socket.on(event, async (request) => {
|
|
707
|
+
try {
|
|
708
|
+
const response = await handler(request, socket);
|
|
709
|
+
socket.emit(`${event}:response`, response);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
log4.error(`Handler error for ${event}`, { error, request });
|
|
712
|
+
socket.emit(`${event}:response`, {
|
|
713
|
+
id: request.id,
|
|
714
|
+
status: "error",
|
|
715
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
log4.info("Socket dispatcher initialized", { events: Object.keys(handlers) });
|
|
721
|
+
}
|
|
722
|
+
|
|
329
723
|
// src/index.ts
|
|
330
724
|
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
331
725
|
console.log(version);
|
|
@@ -350,7 +744,7 @@ function parseArgs() {
|
|
|
350
744
|
}
|
|
351
745
|
return parsed;
|
|
352
746
|
}
|
|
353
|
-
async function handleDeviceLogin(
|
|
747
|
+
async function handleDeviceLogin(log4, serverUrl = "https://tanuki.sabw.ru") {
|
|
354
748
|
const codeEndpoint = `${serverUrl}/api/v1/legion/device/code`;
|
|
355
749
|
const tokenEndpoint = `${serverUrl}/api/v1/legion/device/token`;
|
|
356
750
|
const codeResponse = await fetch(codeEndpoint, {
|
|
@@ -380,7 +774,7 @@ async function handleDeviceLogin(log, serverUrl = "https://tanuki.sabw.ru") {
|
|
|
380
774
|
id: token_id,
|
|
381
775
|
serverUrl: existingConfig?.serverUrl || "wss://tanuki.sabw.ru"
|
|
382
776
|
});
|
|
383
|
-
|
|
777
|
+
log4.info("\u2705 Device activated successfully");
|
|
384
778
|
return;
|
|
385
779
|
} else if (tokenResponse.status === 202) {
|
|
386
780
|
continue;
|
|
@@ -411,20 +805,20 @@ function checkForUpdates() {
|
|
|
411
805
|
}
|
|
412
806
|
}, 100);
|
|
413
807
|
}
|
|
414
|
-
async function enterHibernationMode(
|
|
415
|
-
|
|
416
|
-
const
|
|
808
|
+
async function enterHibernationMode(log4, configFile, onConfigChanged) {
|
|
809
|
+
log4.warn("\u26D4 Auth failed. Entering hibernation mode. Waiting for config update...");
|
|
810
|
+
const fs6 = await import("fs");
|
|
417
811
|
return new Promise((resolve) => {
|
|
418
|
-
const watchHandle =
|
|
812
|
+
const watchHandle = fs6.watch(configFile, async (eventType) => {
|
|
419
813
|
if (eventType === "change") {
|
|
420
|
-
|
|
814
|
+
log4.info("\u{1F4DD} Config file changed. Attempting reconnection...");
|
|
421
815
|
try {
|
|
422
816
|
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
423
817
|
await onConfigChanged();
|
|
424
818
|
watchHandle.close();
|
|
425
819
|
resolve();
|
|
426
820
|
} catch (error) {
|
|
427
|
-
|
|
821
|
+
log4.error("\u274C Reconnection failed", { error });
|
|
428
822
|
}
|
|
429
823
|
}
|
|
430
824
|
});
|
|
@@ -446,9 +840,9 @@ function createSocket(config2) {
|
|
|
446
840
|
reconnectionAttempts: Infinity
|
|
447
841
|
});
|
|
448
842
|
}
|
|
449
|
-
function setupSocketHandlers(socket,
|
|
843
|
+
function setupSocketHandlers(socket, log4, fingerprint, version2, onTokenRotation, onReconnect) {
|
|
450
844
|
socket.on("connect", () => {
|
|
451
|
-
|
|
845
|
+
log4.info("\u2705 Connected to server", { socketId: socket.id });
|
|
452
846
|
const handshakeData = {
|
|
453
847
|
fingerprint,
|
|
454
848
|
version: version2,
|
|
@@ -458,98 +852,102 @@ function setupSocketHandlers(socket, log, fingerprint, version2, onTokenRotation
|
|
|
458
852
|
cwd: process.cwd()
|
|
459
853
|
};
|
|
460
854
|
socket.emit("legion:handshake", handshakeData);
|
|
461
|
-
|
|
855
|
+
log4.debug("\u{1F4E4} Sent handshake", handshakeData);
|
|
856
|
+
const socketConfig = socket.legionConfig;
|
|
857
|
+
if (socketConfig) {
|
|
858
|
+
setupDispatcher(socket, log4, socketConfig);
|
|
859
|
+
}
|
|
462
860
|
});
|
|
463
861
|
socket.on("connect_error", (err) => {
|
|
464
|
-
|
|
862
|
+
log4.error("\u274C Connection error", {
|
|
465
863
|
message: err.message,
|
|
466
864
|
type: err.type
|
|
467
865
|
});
|
|
468
866
|
if (err.message.includes("auth") || err.message.includes("token")) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
enterHibernationMode(
|
|
472
|
-
|
|
867
|
+
log4.error("\u{1F510} Authentication failed. Please check your token.");
|
|
868
|
+
log4.error("\u{1F4A1} Re-authenticate by updating ~/.tanuki/config.json or LEGION_TOKEN env var");
|
|
869
|
+
enterHibernationMode(log4, CONFIG_FILE, onReconnect).catch((error) => {
|
|
870
|
+
log4.error("\u274C Hibernation mode failed", { error });
|
|
473
871
|
process.exit(1);
|
|
474
872
|
});
|
|
475
873
|
}
|
|
476
874
|
});
|
|
477
875
|
socket.on("disconnect", (reason) => {
|
|
478
|
-
|
|
876
|
+
log4.warn("\u26A0\uFE0F Disconnected from server", { reason });
|
|
479
877
|
if (reason === "io server disconnect") {
|
|
480
|
-
|
|
481
|
-
enterHibernationMode(
|
|
482
|
-
|
|
878
|
+
log4.error("\u{1F6AB} Server disconnected this client. Please check your token.");
|
|
879
|
+
enterHibernationMode(log4, CONFIG_FILE, onReconnect).catch((error) => {
|
|
880
|
+
log4.error("\u274C Hibernation mode failed", { error });
|
|
483
881
|
process.exit(1);
|
|
484
882
|
});
|
|
485
883
|
}
|
|
486
884
|
});
|
|
487
885
|
socket.on("legion:update_token", async (data2) => {
|
|
488
886
|
try {
|
|
489
|
-
|
|
887
|
+
log4.info("\u{1F504} Received permanent credentials. Persisting...");
|
|
490
888
|
await updateConfig({
|
|
491
889
|
token: data2.secret,
|
|
492
890
|
// secret -> token
|
|
493
891
|
id: data2.token_id
|
|
494
892
|
// token_id -> id
|
|
495
893
|
});
|
|
496
|
-
|
|
894
|
+
log4.info("\u2705 Config updated successfully. Reconnecting with new token...");
|
|
497
895
|
await onTokenRotation();
|
|
498
896
|
} catch (error) {
|
|
499
|
-
|
|
897
|
+
log4.error("\u274C Failed to update config", { error });
|
|
500
898
|
}
|
|
501
899
|
});
|
|
502
900
|
socket.on("server:ping", (data2) => {
|
|
503
|
-
|
|
901
|
+
log4.debug("\u{1F4E9} Ping from server", data2);
|
|
504
902
|
socket.emit("legion:pong", { ts: Date.now() });
|
|
505
903
|
});
|
|
506
904
|
socket.on("reconnect", (attemptNumber) => {
|
|
507
|
-
|
|
905
|
+
log4.info("\u{1F504} Reconnected to server", { attempt: attemptNumber });
|
|
508
906
|
});
|
|
509
907
|
socket.on("reconnect_attempt", (attemptNumber) => {
|
|
510
|
-
|
|
908
|
+
log4.debug("\u{1F504} Reconnection attempt", { attempt: attemptNumber });
|
|
511
909
|
});
|
|
512
910
|
socket.on("reconnect_error", (error) => {
|
|
513
|
-
|
|
911
|
+
log4.error("\u{1F504} Reconnection error", { message: error.message });
|
|
514
912
|
});
|
|
515
913
|
socket.on("reconnect_failed", () => {
|
|
516
|
-
|
|
914
|
+
log4.error("\u{1F504} Reconnection failed after all attempts");
|
|
517
915
|
process.exit(1);
|
|
518
916
|
});
|
|
519
917
|
}
|
|
520
918
|
async function main() {
|
|
521
|
-
let
|
|
919
|
+
let log4 = null;
|
|
522
920
|
const safeLog = {
|
|
523
921
|
info: (message, extra) => {
|
|
524
|
-
if (
|
|
922
|
+
if (log4) log4.info(message, extra);
|
|
525
923
|
else console.log(message, extra);
|
|
526
924
|
},
|
|
527
925
|
error: (message, extra) => {
|
|
528
|
-
if (
|
|
926
|
+
if (log4) log4.error(message, extra);
|
|
529
927
|
else console.error(message, extra);
|
|
530
928
|
},
|
|
531
929
|
warn: (message, extra) => {
|
|
532
|
-
if (
|
|
930
|
+
if (log4) log4.warn(message, extra);
|
|
533
931
|
else console.warn(message, extra);
|
|
534
932
|
},
|
|
535
933
|
debug: (message, extra) => {
|
|
536
|
-
if (
|
|
934
|
+
if (log4) log4.debug(message, extra);
|
|
537
935
|
}
|
|
538
936
|
};
|
|
539
937
|
try {
|
|
540
|
-
|
|
938
|
+
log4 = Log.create({ service: "legion" });
|
|
541
939
|
const args = parseArgs();
|
|
542
940
|
if (args.command === "login") {
|
|
543
941
|
try {
|
|
544
|
-
await handleDeviceLogin(
|
|
942
|
+
await handleDeviceLogin(log4);
|
|
545
943
|
process.exit(0);
|
|
546
944
|
} catch (error) {
|
|
547
|
-
|
|
945
|
+
log4.error("\u274C Device login failed", { error });
|
|
548
946
|
process.exit(1);
|
|
549
947
|
}
|
|
550
948
|
}
|
|
551
949
|
if (args.token) {
|
|
552
|
-
|
|
950
|
+
log4.info("\u{1F511} Saving token from command line...");
|
|
553
951
|
try {
|
|
554
952
|
const defaultServerUrl = process.env.TANUKI_SERVER_URL || "wss://tanuki.sabw.ru";
|
|
555
953
|
const existingConfig = await loadConfig();
|
|
@@ -558,18 +956,18 @@ async function main() {
|
|
|
558
956
|
serverUrl: existingConfig?.serverUrl || defaultServerUrl,
|
|
559
957
|
...existingConfig?.id ? { id: existingConfig.id } : {}
|
|
560
958
|
});
|
|
561
|
-
|
|
959
|
+
log4.info("\u2705 Token saved successfully");
|
|
562
960
|
} catch (error) {
|
|
563
|
-
|
|
961
|
+
log4.error("\u274C Failed to save token", { error });
|
|
564
962
|
process.exit(1);
|
|
565
963
|
}
|
|
566
964
|
}
|
|
567
965
|
checkForUpdates();
|
|
568
966
|
await Global.init();
|
|
569
967
|
const fingerprint = await getSystemFingerprint();
|
|
570
|
-
|
|
968
|
+
log4.info(`\u{1F6E1}\uFE0F Legion v${version} starting...`);
|
|
571
969
|
let config2 = await getConfig();
|
|
572
|
-
|
|
970
|
+
log4.info("\u{1F517} Connecting to server", { serverUrl: config2.serverUrl });
|
|
573
971
|
let currentSocket;
|
|
574
972
|
let isTokenRotation = false;
|
|
575
973
|
let handleTokenRotation;
|
|
@@ -582,9 +980,10 @@ async function main() {
|
|
|
582
980
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
583
981
|
}
|
|
584
982
|
const newConfig = await getConfig();
|
|
585
|
-
|
|
983
|
+
log4.info("\u{1F504} Reconnecting...");
|
|
586
984
|
currentSocket = createSocket(newConfig);
|
|
587
|
-
|
|
985
|
+
currentSocket.legionConfig = newConfig;
|
|
986
|
+
setupSocketHandlers(currentSocket, log4, fingerprint, version, handleTokenRotation, handleReconnect);
|
|
588
987
|
config2 = newConfig;
|
|
589
988
|
};
|
|
590
989
|
handleTokenRotation = async () => {
|
|
@@ -593,20 +992,21 @@ async function main() {
|
|
|
593
992
|
isTokenRotation = false;
|
|
594
993
|
};
|
|
595
994
|
currentSocket = createSocket(config2);
|
|
596
|
-
|
|
995
|
+
currentSocket.legionConfig = config2;
|
|
996
|
+
setupSocketHandlers(currentSocket, log4, fingerprint, version, handleTokenRotation, handleReconnect);
|
|
597
997
|
process.stdin.resume();
|
|
598
998
|
const shutdown = () => {
|
|
599
|
-
|
|
999
|
+
log4.info("\u{1F6D1} Shutting down...");
|
|
600
1000
|
currentSocket.disconnect();
|
|
601
1001
|
process.exit(0);
|
|
602
1002
|
};
|
|
603
1003
|
process.on("SIGINT", shutdown);
|
|
604
1004
|
process.on("SIGTERM", shutdown);
|
|
605
1005
|
process.on("unhandledRejection", (reason) => {
|
|
606
|
-
|
|
1006
|
+
log4.error("Unhandled rejection", { reason });
|
|
607
1007
|
});
|
|
608
1008
|
process.on("uncaughtException", (error) => {
|
|
609
|
-
|
|
1009
|
+
log4.error("Uncaught exception", { error: error.message, stack: error.stack });
|
|
610
1010
|
shutdown();
|
|
611
1011
|
});
|
|
612
1012
|
} catch (error) {
|