lunel-cli 0.1.31 → 0.1.33
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 +157 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ const ignore = Ignore.default;
|
|
|
8
8
|
import * as fs from "fs/promises";
|
|
9
9
|
import * as path from "path";
|
|
10
10
|
import * as os from "os";
|
|
11
|
-
import { spawn, execSync } from "child_process";
|
|
11
|
+
import { spawn, execSync, execFileSync } from "child_process";
|
|
12
12
|
import { createServer, createConnection } from "net";
|
|
13
13
|
import { createInterface } from "readline";
|
|
14
14
|
const DEFAULT_PROXY_URL = process.env.LUNEL_PROXY_URL || "https://gateway.lunel.dev";
|
|
@@ -17,6 +17,8 @@ const CLI_ARGS = process.argv.slice(2);
|
|
|
17
17
|
import { createRequire } from "module";
|
|
18
18
|
const __require = createRequire(import.meta.url);
|
|
19
19
|
const VERSION = __require("../package.json").version;
|
|
20
|
+
const PTY_RELEASE_INFO_URL = process.env.LUNEL_PTY_INFO_URL ||
|
|
21
|
+
"https://raw.githubusercontent.com/ssbharambe-m/pty-releases/refs/heads/main/info.json";
|
|
20
22
|
// Root directory - sandbox all file operations to this
|
|
21
23
|
const ROOT_DIR = process.cwd();
|
|
22
24
|
// Terminal sessions (managed by Rust PTY binary)
|
|
@@ -396,7 +398,9 @@ async function handleGitStatus() {
|
|
|
396
398
|
const branch = branchResult.stdout.trim();
|
|
397
399
|
// Get status
|
|
398
400
|
const statusResult = await runGit(["status", "--porcelain", "-uall"]);
|
|
399
|
-
|
|
401
|
+
// Preserve leading whitespace in each porcelain line.
|
|
402
|
+
// Example: " M file" (unstaged-only) starts with a space that is semantically important.
|
|
403
|
+
const lines = statusResult.stdout.split(/\r?\n/).filter((line) => line.length > 0);
|
|
400
404
|
const staged = [];
|
|
401
405
|
const unstaged = [];
|
|
402
406
|
const untracked = [];
|
|
@@ -593,14 +597,157 @@ async function handleGitDiscard(payload) {
|
|
|
593
597
|
// Terminal Handlers (delegates to Rust PTY binary)
|
|
594
598
|
// ============================================================================
|
|
595
599
|
let dataChannel = null;
|
|
600
|
+
let ensurePtyBinaryPromise = null;
|
|
601
|
+
function normalizeJsonWithTrailingCommas(text) {
|
|
602
|
+
return text.replace(/,\s*([}\]])/g, "$1");
|
|
603
|
+
}
|
|
604
|
+
function compareSemver(a, b) {
|
|
605
|
+
const aParts = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
606
|
+
const bParts = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
607
|
+
const max = Math.max(aParts.length, bParts.length);
|
|
608
|
+
for (let i = 0; i < max; i++) {
|
|
609
|
+
const left = aParts[i] ?? 0;
|
|
610
|
+
const right = bParts[i] ?? 0;
|
|
611
|
+
if (left !== right)
|
|
612
|
+
return left > right ? 1 : -1;
|
|
613
|
+
}
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
function getLunelConfigDir() {
|
|
617
|
+
const platform = os.platform();
|
|
618
|
+
if (platform === "win32") {
|
|
619
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
620
|
+
return path.join(appData, "lunel");
|
|
621
|
+
}
|
|
622
|
+
if (platform === "darwin") {
|
|
623
|
+
return path.join(os.homedir(), "Library", "Application Support", "lunel");
|
|
624
|
+
}
|
|
625
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
626
|
+
return path.join(xdg, "lunel");
|
|
627
|
+
}
|
|
596
628
|
function getPtyBinaryPath() {
|
|
597
|
-
|
|
598
|
-
return path.join(
|
|
629
|
+
const binName = os.platform() === "win32" ? "lunel-pty.exe" : "lunel-pty";
|
|
630
|
+
return path.join(getLunelConfigDir(), "pty-releases", binName);
|
|
631
|
+
}
|
|
632
|
+
function getPtyPlatformKey() {
|
|
633
|
+
const platform = os.platform();
|
|
634
|
+
if (platform === "win32")
|
|
635
|
+
return "windows";
|
|
636
|
+
if (platform === "linux")
|
|
637
|
+
return "linux";
|
|
638
|
+
if (platform === "darwin")
|
|
639
|
+
return "macos";
|
|
640
|
+
throw new Error(`Unsupported platform for PTY: ${platform}`);
|
|
641
|
+
}
|
|
642
|
+
function getPtyArchKey() {
|
|
643
|
+
const arch = os.arch();
|
|
644
|
+
if (arch === "arm64" || arch === "arm")
|
|
645
|
+
return "arm";
|
|
646
|
+
if (arch === "x64" || arch === "ia32")
|
|
647
|
+
return "x86";
|
|
648
|
+
throw new Error(`Unsupported architecture for PTY: ${arch}`);
|
|
649
|
+
}
|
|
650
|
+
async function fetchPtyReleaseInfo() {
|
|
651
|
+
const response = await fetch(PTY_RELEASE_INFO_URL);
|
|
652
|
+
if (!response.ok) {
|
|
653
|
+
throw new Error(`Failed to fetch PTY release info (${response.status})`);
|
|
654
|
+
}
|
|
655
|
+
const raw = await response.text();
|
|
656
|
+
const parsed = JSON.parse(raw);
|
|
657
|
+
if (!parsed?.version) {
|
|
658
|
+
throw new Error("Invalid PTY release info: missing version");
|
|
659
|
+
}
|
|
660
|
+
return parsed;
|
|
661
|
+
}
|
|
662
|
+
function readInstalledPtyVersion(binPath) {
|
|
663
|
+
try {
|
|
664
|
+
const output = execFileSync(binPath, ["--version"], {
|
|
665
|
+
encoding: "utf8",
|
|
666
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
667
|
+
});
|
|
668
|
+
const version = output.trim();
|
|
669
|
+
return version || null;
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async function downloadPtyBinary(url, destination) {
|
|
676
|
+
const tempPath = `${destination}.download`;
|
|
677
|
+
console.log("[pty] Downloading PTY [downloading...]");
|
|
678
|
+
const response = await fetch(url);
|
|
679
|
+
if (!response.ok) {
|
|
680
|
+
throw new Error(`Failed to download PTY binary (${response.status})`);
|
|
681
|
+
}
|
|
682
|
+
if (!response.body) {
|
|
683
|
+
throw new Error("PTY download response had no body");
|
|
684
|
+
}
|
|
685
|
+
const reader = response.body.getReader();
|
|
686
|
+
const chunks = [];
|
|
687
|
+
let totalBytes = 0;
|
|
688
|
+
while (true) {
|
|
689
|
+
const { done, value } = await reader.read();
|
|
690
|
+
if (done)
|
|
691
|
+
break;
|
|
692
|
+
if (value) {
|
|
693
|
+
chunks.push(value);
|
|
694
|
+
totalBytes += value.byteLength;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const binary = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
|
698
|
+
await fs.writeFile(tempPath, binary);
|
|
699
|
+
if (os.platform() !== "win32") {
|
|
700
|
+
await fs.chmod(tempPath, 0o755);
|
|
701
|
+
}
|
|
702
|
+
await fs.rename(tempPath, destination);
|
|
703
|
+
console.log(`[pty] Downloaded PTY (${Math.max(1, Math.round(totalBytes / 1024))} KB)`);
|
|
704
|
+
}
|
|
705
|
+
async function ensurePtyBinaryReady() {
|
|
706
|
+
if (ensurePtyBinaryPromise)
|
|
707
|
+
return ensurePtyBinaryPromise;
|
|
708
|
+
ensurePtyBinaryPromise = (async () => {
|
|
709
|
+
const binPath = getPtyBinaryPath();
|
|
710
|
+
await fs.mkdir(path.dirname(binPath), { recursive: true });
|
|
711
|
+
const releaseInfo = await fetchPtyReleaseInfo();
|
|
712
|
+
const platformKey = getPtyPlatformKey();
|
|
713
|
+
const archKey = getPtyArchKey();
|
|
714
|
+
const downloadUrl = releaseInfo[platformKey]?.[archKey] || null;
|
|
715
|
+
if (!downloadUrl) {
|
|
716
|
+
throw new Error(`PTY binary is not available for ${platformKey}/${archKey} in release ${releaseInfo.version}`);
|
|
717
|
+
}
|
|
718
|
+
let hasBinary = true;
|
|
719
|
+
try {
|
|
720
|
+
await fs.access(binPath);
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
hasBinary = false;
|
|
724
|
+
}
|
|
725
|
+
const installedVersion = hasBinary ? readInstalledPtyVersion(binPath) : null;
|
|
726
|
+
const shouldDownload = !hasBinary ||
|
|
727
|
+
!installedVersion ||
|
|
728
|
+
compareSemver(installedVersion, releaseInfo.version) < 0;
|
|
729
|
+
if (!shouldDownload)
|
|
730
|
+
return binPath;
|
|
731
|
+
if (!hasBinary) {
|
|
732
|
+
console.log(`[pty] PTY missing. Installing ${releaseInfo.version}...`);
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
console.log(`[pty] PTY outdated (${installedVersion} -> ${releaseInfo.version}). Updating...`);
|
|
736
|
+
}
|
|
737
|
+
await downloadPtyBinary(downloadUrl, binPath);
|
|
738
|
+
return binPath;
|
|
739
|
+
})();
|
|
740
|
+
try {
|
|
741
|
+
return await ensurePtyBinaryPromise;
|
|
742
|
+
}
|
|
743
|
+
finally {
|
|
744
|
+
ensurePtyBinaryPromise = null;
|
|
745
|
+
}
|
|
599
746
|
}
|
|
600
|
-
function ensurePtyProcess() {
|
|
747
|
+
async function ensurePtyProcess() {
|
|
601
748
|
if (ptyProcess && ptyProcess.exitCode === null)
|
|
602
749
|
return;
|
|
603
|
-
const binPath =
|
|
750
|
+
const binPath = await ensurePtyBinaryReady();
|
|
604
751
|
ptyProcess = spawn(binPath, [], {
|
|
605
752
|
cwd: ROOT_DIR,
|
|
606
753
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -688,7 +835,7 @@ function sendToPty(cmd) {
|
|
|
688
835
|
ptyProcess.stdin.write(JSON.stringify(cmd) + "\n");
|
|
689
836
|
}
|
|
690
837
|
async function handleTerminalSpawn(payload) {
|
|
691
|
-
ensurePtyProcess();
|
|
838
|
+
await ensurePtyProcess();
|
|
692
839
|
const shell = payload.shell || process.env.SHELL || "/bin/sh";
|
|
693
840
|
const cols = payload.cols || 80;
|
|
694
841
|
const rows = payload.rows || 24;
|
|
@@ -2150,6 +2297,9 @@ async function main() {
|
|
|
2150
2297
|
console.log(`Extra ports enabled: ${EXTRA_PORTS.join(", ")}`);
|
|
2151
2298
|
}
|
|
2152
2299
|
try {
|
|
2300
|
+
console.log("Checking PTY runtime...");
|
|
2301
|
+
await ensurePtyBinaryReady();
|
|
2302
|
+
console.log("PTY runtime ready.\n");
|
|
2153
2303
|
// Generate auth credentials (like CodeNomad does)
|
|
2154
2304
|
const opencodeUsername = "lunel";
|
|
2155
2305
|
const opencodePassword = crypto.randomBytes(32).toString("base64url");
|