arisa 2.2.11 → 2.3.0
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/bin/arisa.js +23 -313
- package/package.json +1 -1
- package/src/daemon/setup.ts +6 -3
- package/src/shared/ai-cli.ts +30 -0
package/bin/arisa.js
CHANGED
|
@@ -403,7 +403,9 @@ function printForegroundNotice() {
|
|
|
403
403
|
process.stdout.write("Use `arisa start` to run it as a background service.\n");
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
// ── Root
|
|
406
|
+
// ── Root: create arisa user for claude/codex execution ──────────────
|
|
407
|
+
// Daemon runs as root. Only claude/codex CLI calls run as user arisa
|
|
408
|
+
// (Claude CLI refuses to run as root). This avoids two heavy bun processes.
|
|
407
409
|
|
|
408
410
|
function isRoot() {
|
|
409
411
|
return process.getuid?.() === 0;
|
|
@@ -413,24 +415,8 @@ function arisaUserExists() {
|
|
|
413
415
|
return spawnSync("id", ["arisa"], { stdio: "ignore" }).status === 0;
|
|
414
416
|
}
|
|
415
417
|
|
|
416
|
-
function
|
|
417
|
-
return arisaUserExists() && existsSync("/home/arisa/.bun/bin/bun")
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function isArisaConfigured() {
|
|
421
|
-
const envPath = "/home/arisa/.arisa/.env";
|
|
422
|
-
if (!existsSync(envPath)) return false;
|
|
423
|
-
const content = readFileSync(envPath, "utf8");
|
|
424
|
-
return content.includes("TELEGRAM_BOT_TOKEN=");
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function detectSudoGroup() {
|
|
428
|
-
// Debian/Ubuntu use 'sudo', RHEL/Fedora use 'wheel'
|
|
429
|
-
const sudoGroup = spawnSync("getent", ["group", "sudo"], { stdio: "ignore" });
|
|
430
|
-
if (sudoGroup.status === 0) return "sudo";
|
|
431
|
-
const wheelGroup = spawnSync("getent", ["group", "wheel"], { stdio: "ignore" });
|
|
432
|
-
if (wheelGroup.status === 0) return "wheel";
|
|
433
|
-
return null;
|
|
418
|
+
function isArisaUserProvisioned() {
|
|
419
|
+
return arisaUserExists() && existsSync("/home/arisa/.bun/bin/bun");
|
|
434
420
|
}
|
|
435
421
|
|
|
436
422
|
function step(ok, msg) {
|
|
@@ -438,320 +424,44 @@ function step(ok, msg) {
|
|
|
438
424
|
}
|
|
439
425
|
|
|
440
426
|
const ARISA_BUN_ENV = 'export BUN_INSTALL=/home/arisa/.bun && export PATH=/home/arisa/.bun/bin:$PATH';
|
|
441
|
-
const SHARED_ARISA_ROOT = "/opt/arisa/node_modules/arisa";
|
|
442
|
-
const sharedDaemonEntry = join(SHARED_ARISA_ROOT, "src", "daemon", "index.ts");
|
|
443
|
-
|
|
444
|
-
function runAsInherit(cmd) {
|
|
445
|
-
return spawnSync("su", ["-", "arisa", "-c", `${ARISA_BUN_ENV} && ${cmd}`], {
|
|
446
|
-
stdio: "inherit",
|
|
447
|
-
timeout: 180_000,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function ensureSwap() {
|
|
452
|
-
// Check if swap already exists
|
|
453
|
-
const swapon = spawnSync("swapon", ["--show"], { stdio: "pipe" });
|
|
454
|
-
const swapOutput = (swapon.stdout || "").toString().trim();
|
|
455
|
-
if (swapOutput.includes("/")) return; // swap already active
|
|
456
|
-
|
|
457
|
-
// Create 512MB swap file (essential on 1GB VPS for bun add)
|
|
458
|
-
process.stdout.write(" Creating swap file (512MB)...\n");
|
|
459
|
-
const cmds = [
|
|
460
|
-
["fallocate", ["-l", "512M", "/swapfile"]],
|
|
461
|
-
["chmod", ["600", "/swapfile"]],
|
|
462
|
-
["mkswap", ["/swapfile"]],
|
|
463
|
-
["swapon", ["/swapfile"]],
|
|
464
|
-
];
|
|
465
|
-
for (const [cmd, cmdArgs] of cmds) {
|
|
466
|
-
const result = spawnSync(cmd, cmdArgs, { stdio: "pipe" });
|
|
467
|
-
if (result.status !== 0) {
|
|
468
|
-
step(false, "Failed to create swap file");
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Make persistent across reboots
|
|
474
|
-
const fstabPath = "/etc/fstab";
|
|
475
|
-
if (existsSync(fstabPath)) {
|
|
476
|
-
const fstab = readFileSync(fstabPath, "utf8");
|
|
477
|
-
if (!fstab.includes("/swapfile")) {
|
|
478
|
-
writeFileSync(fstabPath, fstab.trimEnd() + "\n/swapfile none swap sw 0 0\n");
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
step(true, "Swap file created (512MB)");
|
|
483
|
-
}
|
|
484
427
|
|
|
485
428
|
function provisionArisaUser() {
|
|
486
|
-
process.stdout.write("
|
|
487
|
-
|
|
488
|
-
// 0. Ensure swap exists (prevents OOM on 1GB VPS during CLI installs)
|
|
489
|
-
ensureSwap();
|
|
429
|
+
process.stdout.write("Creating user 'arisa' for Claude/Codex CLI execution...\n");
|
|
490
430
|
|
|
491
431
|
// 1. Create user
|
|
492
|
-
const useradd = spawnSync("useradd", ["-m", "-s", "/bin/bash", "arisa"], {
|
|
493
|
-
stdio: "pipe",
|
|
494
|
-
});
|
|
432
|
+
const useradd = spawnSync("useradd", ["-m", "-s", "/bin/bash", "arisa"], { stdio: "pipe" });
|
|
495
433
|
if (useradd.status !== 0) {
|
|
496
434
|
step(false, `Failed to create user: ${(useradd.stderr || "").toString().trim()}`);
|
|
497
435
|
process.exit(1);
|
|
498
436
|
}
|
|
499
437
|
step(true, "User arisa created");
|
|
500
438
|
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// 2. Install bun (curl, not bun — low memory footprint)
|
|
508
|
-
process.stdout.write(" Installing bun (this may take a minute)...\n");
|
|
509
|
-
const bunInstall = runAsInherit("curl -fsSL https://bun.sh/install | bash");
|
|
439
|
+
// 2. Install bun for arisa (curl — lightweight, no bun child process)
|
|
440
|
+
process.stdout.write(" Installing bun for arisa (this may take a minute)...\n");
|
|
441
|
+
const bunInstall = spawnSync("su", ["-", "arisa", "-c", "curl -fsSL https://bun.sh/install | bash"], {
|
|
442
|
+
stdio: "inherit",
|
|
443
|
+
timeout: 180_000,
|
|
444
|
+
});
|
|
510
445
|
if (bunInstall.status !== 0) {
|
|
511
446
|
step(false, "Failed to install bun");
|
|
512
447
|
process.exit(1);
|
|
513
448
|
}
|
|
514
449
|
step(true, "Bun installed for arisa");
|
|
515
450
|
|
|
516
|
-
//
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
writeFileSync(profilePath, profileContent + bunPath, "utf8");
|
|
522
|
-
spawnSync("chown", ["arisa:arisa", profilePath], { stdio: "ignore" });
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// 3. Copy global node_modules to shared location (lightweight cp, no bun)
|
|
526
|
-
const sharedDir = "/opt/arisa";
|
|
527
|
-
const globalModules = resolve(pkgRoot, "..");
|
|
528
|
-
mkdirSync(sharedDir, { recursive: true });
|
|
529
|
-
spawnSync("cp", ["-r", globalModules, join(sharedDir, "node_modules")], { stdio: "pipe" });
|
|
530
|
-
spawnSync("chown", ["-R", "arisa:arisa", sharedDir], { stdio: "pipe" });
|
|
531
|
-
step(true, "Arisa copied to /opt/arisa");
|
|
532
|
-
|
|
533
|
-
// 4. Migrate data
|
|
534
|
-
const rootArisa = "/root/.arisa";
|
|
535
|
-
if (existsSync(rootArisa)) {
|
|
536
|
-
const destArisa = "/home/arisa/.arisa";
|
|
537
|
-
spawnSync("cp", ["-r", rootArisa, destArisa], { stdio: "pipe" });
|
|
538
|
-
spawnSync("chown", ["-R", "arisa:arisa", destArisa], { stdio: "pipe" });
|
|
539
|
-
step(true, "Data migrated to /home/arisa/.arisa/");
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// ── System-level systemd (for root-provisioned installs) ────────────
|
|
544
|
-
|
|
545
|
-
const systemdSystemUnitPath = "/etc/systemd/system/arisa.service";
|
|
546
|
-
|
|
547
|
-
function writeSystemdSystemUnit() {
|
|
548
|
-
const unit = `[Unit]
|
|
549
|
-
Description=Arisa Agent Runtime
|
|
550
|
-
After=network-online.target
|
|
551
|
-
Wants=network-online.target
|
|
552
|
-
|
|
553
|
-
[Service]
|
|
554
|
-
Type=simple
|
|
555
|
-
User=arisa
|
|
556
|
-
WorkingDirectory=${SHARED_ARISA_ROOT}
|
|
557
|
-
ExecStart=/home/arisa/.bun/bin/bun ${sharedDaemonEntry}
|
|
558
|
-
Restart=always
|
|
559
|
-
RestartSec=5
|
|
560
|
-
Environment=ARISA_PROJECT_DIR=${SHARED_ARISA_ROOT}
|
|
561
|
-
Environment=BUN_INSTALL=/home/arisa/.bun
|
|
562
|
-
Environment=PATH=/home/arisa/.bun/bin:/usr/local/bin:/usr/bin:/bin
|
|
563
|
-
|
|
564
|
-
[Install]
|
|
565
|
-
WantedBy=multi-user.target
|
|
566
|
-
`;
|
|
567
|
-
writeFileSync(systemdSystemUnitPath, unit, "utf8");
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function runSystemdSystem(commandArgs) {
|
|
571
|
-
const child = runCommand("systemctl", commandArgs, { stdio: "pipe" });
|
|
572
|
-
if (child.status !== 0) {
|
|
573
|
-
const stderr = child.stderr || "Unknown systemd error";
|
|
574
|
-
process.stderr.write(stderr.endsWith("\n") ? stderr : `${stderr}\n`);
|
|
575
|
-
return { ok: false, status: child.status ?? 1 };
|
|
576
|
-
}
|
|
577
|
-
return { ok: true, status: 0, stdout: child.stdout || "" };
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function startSystemdSystem() {
|
|
581
|
-
const start = runSystemdSystem(["start", "arisa"]);
|
|
582
|
-
if (!start.ok) return start.status;
|
|
583
|
-
process.stdout.write("Arisa service started.\n");
|
|
584
|
-
return 0;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function stopSystemdSystem() {
|
|
588
|
-
const stop = runSystemdSystem(["stop", "arisa"]);
|
|
589
|
-
if (!stop.ok) return stop.status;
|
|
590
|
-
process.stdout.write("Arisa service stopped.\n");
|
|
591
|
-
return 0;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function restartSystemdSystem() {
|
|
595
|
-
const restart = runSystemdSystem(["restart", "arisa"]);
|
|
596
|
-
if (!restart.ok) return restart.status;
|
|
597
|
-
process.stdout.write("Arisa service restarted.\n");
|
|
598
|
-
return 0;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function statusSystemdSystem() {
|
|
602
|
-
const result = runCommand("systemctl", ["status", "arisa"], { stdio: "inherit" });
|
|
603
|
-
return result.status ?? 1;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function isSystemdActive() {
|
|
607
|
-
const result = runCommand("systemctl", ["is-active", "arisa"], { stdio: "pipe" });
|
|
608
|
-
return result.status === 0;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function canUseSystemdSystem() {
|
|
612
|
-
if (platform() !== "linux") return false;
|
|
613
|
-
if (!commandExists("systemctl")) return false;
|
|
614
|
-
const probe = runCommand("systemctl", ["is-system-running"], { stdio: "pipe" });
|
|
615
|
-
const state = (probe.stdout || "").trim();
|
|
616
|
-
return probe.status === 0 || state === "degraded" || state === "running";
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
function runArisaForeground() {
|
|
620
|
-
const result = spawnSync("su", ["-", "arisa", "-c", `${ARISA_BUN_ENV} && export ARISA_PROJECT_DIR=${SHARED_ARISA_ROOT} && exec /home/arisa/.bun/bin/bun ${sharedDaemonEntry}`], {
|
|
621
|
-
stdio: "inherit",
|
|
622
|
-
});
|
|
623
|
-
return result.status ?? 1;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// ── Minimal setup (runs as root, no second bun process) ─────────────
|
|
451
|
+
// 3. Write ink-shim for non-TTY execution (prevents Ink setRawMode crash)
|
|
452
|
+
const shimPath = "/home/arisa/.arisa-ink-shim.js";
|
|
453
|
+
writeFileSync(shimPath, 'if(process.stdin&&!process.stdin.setRawMode)process.stdin.setRawMode=()=>process.stdin;\n');
|
|
454
|
+
spawnSync("chown", ["arisa:arisa", shimPath], { stdio: "ignore" });
|
|
455
|
+
step(true, "Ink shim installed");
|
|
627
456
|
|
|
628
|
-
|
|
629
|
-
process.stdout.write(promptText);
|
|
630
|
-
const result = spawnSync("bash", ["-c", "read -r line; echo \"$line\""], {
|
|
631
|
-
stdio: ["inherit", "pipe", "inherit"],
|
|
632
|
-
});
|
|
633
|
-
return (result.stdout || "").toString().trim();
|
|
457
|
+
process.stdout.write(" Done. Claude/Codex will run as user arisa.\n\n");
|
|
634
458
|
}
|
|
635
459
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
// Load existing .env if any
|
|
641
|
-
const vars = {};
|
|
642
|
-
if (existsSync(envPath)) {
|
|
643
|
-
for (const line of readFileSync(envPath, "utf8").split("\n")) {
|
|
644
|
-
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/);
|
|
645
|
-
if (match) vars[match[1]] = match[2].trim();
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (!vars.TELEGRAM_BOT_TOKEN) {
|
|
650
|
-
process.stdout.write("\nArisa Setup\n\n");
|
|
651
|
-
const token = askLine("Telegram Bot Token (from https://t.me/BotFather): ");
|
|
652
|
-
if (!token) {
|
|
653
|
-
process.stdout.write("No token provided. Cannot start without Telegram Bot Token.\n");
|
|
654
|
-
process.exit(1);
|
|
655
|
-
}
|
|
656
|
-
vars.TELEGRAM_BOT_TOKEN = token;
|
|
657
|
-
process.stdout.write(" Token saved.\n");
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
if (!vars.OPENAI_API_KEY) {
|
|
661
|
-
const key = askLine("OpenAI API Key (optional, enter to skip): ");
|
|
662
|
-
if (key) {
|
|
663
|
-
vars.OPENAI_API_KEY = key;
|
|
664
|
-
process.stdout.write(" Key saved.\n");
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
vars.ARISA_SETUP_COMPLETE = "1";
|
|
669
|
-
|
|
670
|
-
// Write .env
|
|
671
|
-
mkdirSync(arisaDataDir, { recursive: true });
|
|
672
|
-
const content = Object.entries(vars).map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
|
|
673
|
-
writeFileSync(envPath, content, "utf8");
|
|
674
|
-
spawnSync("chown", ["-R", "arisa:arisa", arisaDataDir], { stdio: "ignore" });
|
|
675
|
-
|
|
676
|
-
process.stdout.write(`\nConfig saved to ${envPath}\n`);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// ── Root guard ──────────────────────────────────────────────────────
|
|
680
|
-
|
|
681
|
-
if (isRoot()) {
|
|
682
|
-
if (!isProvisioned()) {
|
|
683
|
-
provisionArisaUser();
|
|
684
|
-
if (canUseSystemdSystem()) {
|
|
685
|
-
writeSystemdSystemUnit();
|
|
686
|
-
spawnSync("systemctl", ["daemon-reload"], { stdio: "inherit" });
|
|
687
|
-
spawnSync("systemctl", ["enable", "arisa"], { stdio: "inherit" });
|
|
688
|
-
step(true, "Systemd service enabled (auto-starts on reboot)");
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Minimal setup: collect tokens here (no second bun process)
|
|
693
|
-
if (!isArisaConfigured()) {
|
|
694
|
-
runMinimalSetup();
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Already provisioned + configured — route commands
|
|
698
|
-
if (command === "help" || command === "--help" || command === "-h") {
|
|
699
|
-
printHelp();
|
|
700
|
-
process.exit(0);
|
|
701
|
-
}
|
|
702
|
-
if (command === "version" || command === "--version" || command === "-v") {
|
|
703
|
-
printVersion();
|
|
704
|
-
process.exit(0);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const hasSystemd = canUseSystemdSystem();
|
|
708
|
-
|
|
709
|
-
if (isDefaultInvocation) {
|
|
710
|
-
if (hasSystemd) {
|
|
711
|
-
if (!isSystemdActive()) {
|
|
712
|
-
const start = startSystemdSystem();
|
|
713
|
-
if (start !== 0) process.exit(start);
|
|
714
|
-
}
|
|
715
|
-
process.stdout.write("\nArisa is running. Management commands:\n");
|
|
716
|
-
process.stdout.write(" Status: systemctl status arisa\n");
|
|
717
|
-
process.stdout.write(" Logs: journalctl -u arisa -f\n");
|
|
718
|
-
process.stdout.write(" Restart: systemctl restart arisa\n");
|
|
719
|
-
process.stdout.write(" Stop: systemctl stop arisa\n\n");
|
|
720
|
-
process.exit(0);
|
|
721
|
-
}
|
|
722
|
-
// No systemd → foreground (two bun processes, but no other option)
|
|
723
|
-
process.exit(runArisaForeground());
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
switch (command) {
|
|
727
|
-
case "start":
|
|
728
|
-
if (hasSystemd) process.exit(startSystemdSystem());
|
|
729
|
-
process.exit(runArisaForeground());
|
|
730
|
-
break;
|
|
731
|
-
case "stop":
|
|
732
|
-
if (hasSystemd) process.exit(stopSystemdSystem());
|
|
733
|
-
process.stderr.write("No systemd available. Stop the foreground process with Ctrl+C.\n");
|
|
734
|
-
process.exit(1);
|
|
735
|
-
break;
|
|
736
|
-
case "restart":
|
|
737
|
-
if (hasSystemd) process.exit(restartSystemdSystem());
|
|
738
|
-
process.stderr.write("No systemd available. Restart the foreground process manually.\n");
|
|
739
|
-
process.exit(1);
|
|
740
|
-
break;
|
|
741
|
-
case "status":
|
|
742
|
-
if (hasSystemd) process.exit(statusSystemdSystem());
|
|
743
|
-
process.stderr.write("No systemd available.\n");
|
|
744
|
-
process.exit(1);
|
|
745
|
-
break;
|
|
746
|
-
case "daemon":
|
|
747
|
-
case "run":
|
|
748
|
-
process.exit(runArisaForeground());
|
|
749
|
-
default:
|
|
750
|
-
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
751
|
-
printHelp();
|
|
752
|
-
process.exit(1);
|
|
753
|
-
}
|
|
460
|
+
// Provision arisa user if running as root and not yet done
|
|
461
|
+
if (isRoot() && !isArisaUserProvisioned()) {
|
|
462
|
+
provisionArisaUser();
|
|
754
463
|
}
|
|
464
|
+
// Then fall through to normal daemon startup (as root)
|
|
755
465
|
|
|
756
466
|
// ── Non-root flow (unchanged) ───────────────────────────────────────
|
|
757
467
|
|
package/package.json
CHANGED
package/src/daemon/setup.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
|
15
15
|
import { dirname, join } from "path";
|
|
16
16
|
import { dataDir } from "../shared/paths";
|
|
17
17
|
import { secrets, setSecret } from "../shared/secrets";
|
|
18
|
-
import { isAgentCliInstalled, buildBunWrappedAgentCliCommand, type AgentCliName } from "../shared/ai-cli";
|
|
18
|
+
import { isAgentCliInstalled, buildBunWrappedAgentCliCommand, isRunningAsRoot, type AgentCliName } from "../shared/ai-cli";
|
|
19
19
|
|
|
20
20
|
const ENV_PATH = join(dataDir, ".env");
|
|
21
21
|
const SETUP_DONE_KEY = "ARISA_SETUP_COMPLETE";
|
|
@@ -230,11 +230,14 @@ async function setupClis(inq: typeof import("@inquirer/prompts") | null, vars: R
|
|
|
230
230
|
|
|
231
231
|
async function installCli(cli: AgentCliName): Promise<boolean> {
|
|
232
232
|
try {
|
|
233
|
-
const
|
|
233
|
+
const cmd = isRunningAsRoot()
|
|
234
|
+
? ["su", "-", "arisa", "-c", `export BUN_INSTALL=/home/arisa/.bun && export PATH=/home/arisa/.bun/bin:$PATH && bun add -g ${CLI_PACKAGES[cli]}`]
|
|
235
|
+
: ["bun", "add", "-g", CLI_PACKAGES[cli]];
|
|
236
|
+
const proc = Bun.spawn(cmd, {
|
|
234
237
|
stdout: "inherit",
|
|
235
238
|
stderr: "inherit",
|
|
236
239
|
});
|
|
237
|
-
const timeout = setTimeout(() => proc.kill(),
|
|
240
|
+
const timeout = setTimeout(() => proc.kill(), 180_000);
|
|
238
241
|
const exitCode = await proc.exited;
|
|
239
242
|
clearTimeout(timeout);
|
|
240
243
|
return exitCode === 0;
|
package/src/shared/ai-cli.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module shared/ai-cli
|
|
3
3
|
* @role Resolve agent CLI binaries and execute them via Bun runtime.
|
|
4
|
+
* When running as root, wraps calls with su - arisa to satisfy
|
|
5
|
+
* Claude CLI's non-root requirement.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import { existsSync } from "fs";
|
|
@@ -8,6 +10,18 @@ import { delimiter, dirname, join } from "path";
|
|
|
8
10
|
|
|
9
11
|
export type AgentCliName = "claude" | "codex";
|
|
10
12
|
|
|
13
|
+
const ARISA_USER_BUN = "/home/arisa/.bun/bin";
|
|
14
|
+
const ARISA_INK_SHIM = "/home/arisa/.arisa-ink-shim.js";
|
|
15
|
+
const ARISA_BUN_ENV = `export BUN_INSTALL=/home/arisa/.bun && export PATH=${ARISA_USER_BUN}:$PATH`;
|
|
16
|
+
|
|
17
|
+
export function isRunningAsRoot(): boolean {
|
|
18
|
+
return process.getuid?.() === 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function shellEscape(arg: string): string {
|
|
22
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
function unique(paths: Array<string | null | undefined>): string[] {
|
|
12
26
|
const seen = new Set<string>();
|
|
13
27
|
const out: string[] = [];
|
|
@@ -25,6 +39,14 @@ function cliOverrideEnvVar(cli: AgentCliName): string | undefined {
|
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
function candidatePaths(cli: AgentCliName): string[] {
|
|
42
|
+
if (isRunningAsRoot()) {
|
|
43
|
+
// When root, CLIs are installed under arisa user's bun
|
|
44
|
+
return unique([
|
|
45
|
+
cliOverrideEnvVar(cli),
|
|
46
|
+
join(ARISA_USER_BUN, cli),
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
const bunInstall = process.env.BUN_INSTALL?.trim();
|
|
29
51
|
const bunDir = dirname(process.execPath);
|
|
30
52
|
const fromPath = Bun.which(cli);
|
|
@@ -57,6 +79,14 @@ export function isAgentCliInstalled(cli: AgentCliName): boolean {
|
|
|
57
79
|
const INK_SHIM = join(dirname(new URL(import.meta.url).pathname), "ink-shim.js");
|
|
58
80
|
|
|
59
81
|
export function buildBunWrappedAgentCliCommand(cli: AgentCliName, args: string[]): string[] {
|
|
82
|
+
if (isRunningAsRoot()) {
|
|
83
|
+
// Run as arisa user — Claude CLI refuses to run as root
|
|
84
|
+
const cliPath = resolveAgentCliPath(cli) || join(ARISA_USER_BUN, cli);
|
|
85
|
+
const shimPath = existsSync(ARISA_INK_SHIM) ? ARISA_INK_SHIM : INK_SHIM;
|
|
86
|
+
const inner = ["bun", "--preload", shimPath, cliPath, ...args].map(shellEscape).join(" ");
|
|
87
|
+
return ["su", "-", "arisa", "-c", `${ARISA_BUN_ENV} && ${inner}`];
|
|
88
|
+
}
|
|
89
|
+
|
|
60
90
|
const cliPath = resolveAgentCliPath(cli);
|
|
61
91
|
if (!cliPath) {
|
|
62
92
|
throw new Error(`${cli} CLI not found`);
|