lunel-cli 0.1.100 → 0.1.102

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 +107 -133
  2. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { WebSocket } from "ws";
3
3
  import qrcode from "qrcode-terminal";
4
+ import shell from "shelljs";
4
5
  import { createAiManager } from "./ai/index.js";
5
6
  import { V2SessionTransport } from "./transport/v2.js";
6
7
  import Ignore from "ignore";
@@ -10,7 +11,7 @@ import * as fssync from "fs";
10
11
  import * as path from "path";
11
12
  import * as os from "os";
12
13
  import { randomBytes } from "crypto";
13
- import { spawn, spawnSync, execSync, execFileSync } from "child_process";
14
+ import { spawn, spawnSync, execSync } from "child_process";
14
15
  import { createServer, createConnection } from "net";
15
16
  import { createInterface } from "readline";
16
17
  const DEFAULT_PROXY_URL = normalizeGatewayUrl(process.env.LUNEL_PROXY_URL || "https://gateway.lunel.dev");
@@ -28,9 +29,22 @@ if (DEBUG_MODE) {
28
29
  import { createRequire } from "module";
29
30
  const __require = createRequire(import.meta.url);
30
31
  const VERSION = __require("../package.json").version;
31
- const PTY_RELEASE_INFO_URL = process.env.LUNEL_PTY_INFO_URL ||
32
- "https://raw.githubusercontent.com/ssbharambe-m/pty-releases/refs/heads/main/info.json";
33
32
  const VERBOSE_AI_LOGS = process.env.LUNEL_DEBUG_AI === "1";
33
+ const PTY_RELEASE_BASE_URL = "https://github.com/lunel-dev/lunel/releases/download/v0";
34
+ const PTY_RELEASES = {
35
+ "linux:x64": {
36
+ fileName: "lunel-pty-linux-x8664-0",
37
+ url: `${PTY_RELEASE_BASE_URL}/lunel-pty-linux-x8664-0`,
38
+ },
39
+ "darwin:arm64": {
40
+ fileName: "lunel-pty-macos-arm64-0",
41
+ url: `${PTY_RELEASE_BASE_URL}/lunel-pty-macos-arm64-0`,
42
+ },
43
+ "win32:x64": {
44
+ fileName: "lunel-pty-windows-x8664-0.exe",
45
+ url: `${PTY_RELEASE_BASE_URL}/lunel-pty-windows-x8664-0.exe`,
46
+ },
47
+ };
34
48
  // Root directory - sandbox all file operations to this
35
49
  const ROOT_DIR = (() => {
36
50
  try {
@@ -566,64 +580,82 @@ async function handleFsGrep(payload) {
566
580
  throw Object.assign(new Error("pattern is required"), { code: "EINVAL" });
567
581
  const safePath = assertSafePath(reqPath);
568
582
  const matches = [];
569
- const regex = new RegExp(pattern, caseSensitive ? "g" : "gi");
583
+ let regex;
584
+ try {
585
+ regex = new RegExp(pattern, caseSensitive ? "g" : "gi");
586
+ }
587
+ catch {
588
+ throw Object.assign(new Error("pattern must be a valid regular expression"), { code: "EINVAL" });
589
+ }
570
590
  const rootIgnore = await loadGitignore(ROOT_DIR);
571
- async function searchFile(filePath, relativePath) {
572
- if (matches.length >= maxResults)
573
- return;
574
- try {
575
- const content = await fs.readFile(filePath, "utf-8");
576
- const lines = content.split("\n");
577
- for (let i = 0; i < lines.length && matches.length < maxResults; i++) {
578
- if (regex.test(lines[i])) {
579
- matches.push({
580
- file: relativePath,
581
- line: i + 1,
582
- content: lines[i].substring(0, 500),
583
- });
591
+ const previousSilent = shell.config.silent;
592
+ shell.config.silent = true;
593
+ try {
594
+ async function searchFile(filePath, relativePath) {
595
+ if (matches.length >= maxResults)
596
+ return;
597
+ try {
598
+ const grepResult = shell.grep(regex, filePath);
599
+ if (grepResult.code !== 0 || !grepResult.stdout.trim()) {
600
+ regex.lastIndex = 0;
601
+ return;
602
+ }
603
+ const content = await fs.readFile(filePath, "utf-8");
604
+ const lines = content.split("\n");
605
+ for (let i = 0; i < lines.length && matches.length < maxResults; i++) {
606
+ if (regex.test(lines[i])) {
607
+ matches.push({
608
+ file: relativePath,
609
+ line: i + 1,
610
+ content: lines[i].substring(0, 500),
611
+ });
612
+ }
613
+ regex.lastIndex = 0;
584
614
  }
615
+ }
616
+ catch {
585
617
  regex.lastIndex = 0;
586
618
  }
587
619
  }
588
- catch {
589
- // Skip unreadable files
590
- }
591
- }
592
- async function searchDir(dirPath, relativePath, ig) {
593
- if (matches.length >= maxResults)
594
- return;
595
- const localIgnore = ignore().add(ig);
596
- try {
597
- const localGitignorePath = path.join(dirPath, ".gitignore");
598
- const content = await fs.readFile(localGitignorePath, "utf-8");
599
- localIgnore.add(content);
600
- }
601
- catch {
602
- // No local .gitignore
603
- }
604
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
605
- for (const entry of entries) {
620
+ async function searchDir(dirPath, relativePath, ig) {
606
621
  if (matches.length >= maxResults)
607
- break;
608
- const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
609
- const checkPath = entry.isDirectory() ? `${relPath}/` : relPath;
610
- if (localIgnore.ignores(checkPath))
611
- continue;
612
- const fullPath = path.join(dirPath, entry.name);
613
- if (entry.isDirectory()) {
614
- await searchDir(fullPath, relPath, localIgnore);
622
+ return;
623
+ const localIgnore = ignore().add(ig);
624
+ try {
625
+ const localGitignorePath = path.join(dirPath, ".gitignore");
626
+ const content = await fs.readFile(localGitignorePath, "utf-8");
627
+ localIgnore.add(content);
615
628
  }
616
- else if (entry.isFile()) {
617
- await searchFile(fullPath, relPath);
629
+ catch {
630
+ // No local .gitignore
631
+ }
632
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
633
+ for (const entry of entries) {
634
+ if (matches.length >= maxResults)
635
+ break;
636
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
637
+ const checkPath = entry.isDirectory() ? `${relPath}/` : relPath;
638
+ if (localIgnore.ignores(checkPath))
639
+ continue;
640
+ const fullPath = path.join(dirPath, entry.name);
641
+ if (entry.isDirectory()) {
642
+ await searchDir(fullPath, relPath, localIgnore);
643
+ }
644
+ else if (entry.isFile()) {
645
+ await searchFile(fullPath, relPath);
646
+ }
618
647
  }
619
648
  }
649
+ const stat = await fs.stat(safePath);
650
+ if (stat.isDirectory()) {
651
+ await searchDir(safePath, reqPath === "." ? "" : reqPath, rootIgnore);
652
+ }
653
+ else {
654
+ await searchFile(safePath, reqPath);
655
+ }
620
656
  }
621
- const stat = await fs.stat(safePath);
622
- if (stat.isDirectory()) {
623
- await searchDir(safePath, reqPath === "." ? "" : reqPath, rootIgnore);
624
- }
625
- else {
626
- await searchFile(safePath, reqPath);
657
+ finally {
658
+ shell.config.silent = previousSilent;
627
659
  }
628
660
  return { matches };
629
661
  }
@@ -1200,18 +1232,6 @@ let ensurePtyBinaryPromise = null;
1200
1232
  function normalizeJsonWithTrailingCommas(text) {
1201
1233
  return text.replace(/,\s*([}\]])/g, "$1");
1202
1234
  }
1203
- function compareSemver(a, b) {
1204
- const aParts = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
1205
- const bParts = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
1206
- const max = Math.max(aParts.length, bParts.length);
1207
- for (let i = 0; i < max; i++) {
1208
- const left = aParts[i] ?? 0;
1209
- const right = bParts[i] ?? 0;
1210
- if (left !== right)
1211
- return left > right ? 1 : -1;
1212
- }
1213
- return 0;
1214
- }
1215
1235
  function getLunelConfigDir() {
1216
1236
  const platform = os.platform();
1217
1237
  if (platform === "win32") {
@@ -1224,52 +1244,14 @@ function getLunelConfigDir() {
1224
1244
  const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
1225
1245
  return path.join(xdg, "lunel");
1226
1246
  }
1227
- function getPtyBinaryPath() {
1228
- const binName = os.platform() === "win32" ? "lunel-pty.exe" : "lunel-pty";
1229
- return path.join(getLunelConfigDir(), "pty-releases", binName);
1230
- }
1231
- function getPtyPlatformKey() {
1232
- const platform = os.platform();
1233
- if (platform === "win32")
1234
- return "windows";
1235
- if (platform === "linux")
1236
- return "linux";
1237
- if (platform === "darwin")
1238
- return "macos";
1239
- throw new Error(`Unsupported platform for PTY: ${platform}`);
1240
- }
1241
- function getPtyArchKey() {
1242
- const arch = os.arch();
1243
- if (arch === "arm64" || arch === "arm")
1244
- return "arm";
1245
- if (arch === "x64" || arch === "ia32")
1246
- return "x86";
1247
- throw new Error(`Unsupported architecture for PTY: ${arch}`);
1248
- }
1249
- async function fetchPtyReleaseInfo() {
1250
- const response = await fetch(PTY_RELEASE_INFO_URL);
1251
- if (!response.ok) {
1252
- throw new Error(`Failed to fetch PTY release info (${response.status})`);
1253
- }
1254
- const raw = await response.text();
1255
- const parsed = JSON.parse(raw);
1256
- if (!parsed?.version) {
1257
- throw new Error("Invalid PTY release info: missing version");
1258
- }
1259
- return parsed;
1260
- }
1261
- function readInstalledPtyVersion(binPath) {
1262
- try {
1263
- const output = execFileSync(binPath, ["--version"], {
1264
- encoding: "utf8",
1265
- stdio: ["ignore", "pipe", "ignore"],
1266
- });
1267
- const version = output.trim();
1268
- return version || null;
1269
- }
1270
- catch {
1247
+ function getPtyReleaseTarget() {
1248
+ const release = PTY_RELEASES[`${os.platform()}:${os.arch()}`];
1249
+ if (!release)
1271
1250
  return null;
1272
- }
1251
+ return release;
1252
+ }
1253
+ function getPtyBinaryPath(fileName) {
1254
+ return path.join(getLunelConfigDir(), "pty-releases", fileName);
1273
1255
  }
1274
1256
  async function downloadPtyBinary(url, destination) {
1275
1257
  const tempPath = `${destination}.download`;
@@ -1305,36 +1287,20 @@ async function ensurePtyBinaryReady() {
1305
1287
  if (ensurePtyBinaryPromise)
1306
1288
  return ensurePtyBinaryPromise;
1307
1289
  ensurePtyBinaryPromise = (async () => {
1308
- const binPath = getPtyBinaryPath();
1290
+ const release = getPtyReleaseTarget();
1291
+ if (!release)
1292
+ return null;
1293
+ const binPath = getPtyBinaryPath(release.fileName);
1309
1294
  await fs.mkdir(path.dirname(binPath), { recursive: true });
1310
- const releaseInfo = await fetchPtyReleaseInfo();
1311
- const platformKey = getPtyPlatformKey();
1312
- const archKey = getPtyArchKey();
1313
- const downloadUrl = releaseInfo[platformKey]?.[archKey] || null;
1314
- if (!downloadUrl) {
1315
- throw new Error(`PTY binary is not available for ${platformKey}/${archKey} in release ${releaseInfo.version}`);
1316
- }
1317
- let hasBinary = true;
1318
1295
  try {
1319
1296
  await fs.access(binPath);
1297
+ return binPath;
1320
1298
  }
1321
1299
  catch {
1322
- hasBinary = false;
1323
- }
1324
- const installedVersion = hasBinary ? readInstalledPtyVersion(binPath) : null;
1325
- const shouldDownload = !hasBinary ||
1326
- !installedVersion ||
1327
- compareSemver(installedVersion, releaseInfo.version) < 0;
1328
- if (!shouldDownload)
1300
+ console.log(`[pty] PTY missing. Installing ${release.fileName}...`);
1301
+ await downloadPtyBinary(release.url, binPath);
1329
1302
  return binPath;
1330
- if (!hasBinary) {
1331
- console.log(`[pty] PTY missing. Installing ${releaseInfo.version}...`);
1332
- }
1333
- else {
1334
- console.log(`[pty] PTY outdated (${installedVersion} -> ${releaseInfo.version}). Updating...`);
1335
1303
  }
1336
- await downloadPtyBinary(downloadUrl, binPath);
1337
- return binPath;
1338
1304
  })();
1339
1305
  try {
1340
1306
  return await ensurePtyBinaryPromise;
@@ -1347,6 +1313,9 @@ async function ensurePtyProcess() {
1347
1313
  if (ptyProcess && ptyProcess.exitCode === null)
1348
1314
  return;
1349
1315
  const binPath = await ensurePtyBinaryReady();
1316
+ if (!binPath) {
1317
+ throw new Error(`PTY is not supported on ${os.platform()}/${os.arch()}`);
1318
+ }
1350
1319
  ptyProcess = spawn(binPath, [], {
1351
1320
  cwd: ROOT_DIR,
1352
1321
  stdio: ["pipe", "pipe", "pipe"],
@@ -3103,8 +3072,13 @@ async function main() {
3103
3072
  const cliConfig = await getCliConfig();
3104
3073
  const savedSession = getSavedSessionForRoot(cliConfig, ROOT_DIR);
3105
3074
  debugLog("Checking PTY runtime...");
3106
- await ensurePtyBinaryReady();
3107
- debugLog("PTY runtime ready.\n");
3075
+ const ptyBinaryPath = await ensurePtyBinaryReady();
3076
+ if (ptyBinaryPath) {
3077
+ debugLog("PTY runtime ready.\n");
3078
+ }
3079
+ else {
3080
+ debugLog(`PTY runtime unsupported on ${os.platform()}/${os.arch()}. Skipping prefetch.\n`);
3081
+ }
3108
3082
  // Start both AI backends (OpenCode + Codex). Unavailable ones are skipped.
3109
3083
  aiManager = await createAiManager();
3110
3084
  // Wire provider events → mobile app data channel, tagged with backend name.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.100",
3
+ "version": "0.1.102",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",
@@ -11,7 +11,7 @@
11
11
  "email": "rinkit@lunel.dev"
12
12
  }
13
13
  ],
14
- "license": "Functional Source License, Version 1.1, Apache 2.0 Future License",
14
+ "license": "MIT",
15
15
  "type": "module",
16
16
  "bin": {
17
17
  "lunel-cli": "dist/index.js"
@@ -30,12 +30,14 @@
30
30
  "ignore": "^6.0.2",
31
31
  "libsodium-wrappers": "^0.7.15",
32
32
  "qrcode-terminal": "^0.12.0",
33
+ "shelljs": "^0.10.0",
33
34
  "ws": "^8.18.0"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/minimatch": "^5.1.2",
37
38
  "@types/node": "^20.0.0",
38
39
  "@types/qrcode-terminal": "^0.12.2",
40
+ "@types/shelljs": "^0.10.0",
39
41
  "@types/ws": "^8.5.13",
40
42
  "typescript": "^5.0.0"
41
43
  },