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.
Files changed (2) hide show
  1. package/dist/index.js +157 -7
  2. 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
- const lines = statusResult.stdout.trim().split("\n").filter(Boolean);
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
- // TODO: Switch to GitHub releases download for production
598
- return path.join(os.homedir(), "lunel-pty");
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 = getPtyBinaryPath();
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",