lunel-cli 0.1.36 → 0.1.38

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 +103 -2
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import * as os from "os";
11
11
  import { spawn, execSync, execFileSync } from "child_process";
12
12
  import { createServer, createConnection } from "net";
13
13
  import { createInterface } from "readline";
14
+ import { simpleGit } from "simple-git";
14
15
  const DEFAULT_PROXY_URL = process.env.LUNEL_PROXY_URL || "https://gateway.lunel.dev";
15
16
  const MANAGER_URL = process.env.LUNEL_MANAGER_URL || "https://manager.lunel.dev";
16
17
  const CLI_ARGS = process.argv.slice(2);
@@ -22,6 +23,7 @@ const PTY_RELEASE_INFO_URL = process.env.LUNEL_PTY_INFO_URL ||
22
23
  const VERBOSE_AI_LOGS = process.env.LUNEL_DEBUG_AI === "1";
23
24
  // Root directory - sandbox all file operations to this
24
25
  const ROOT_DIR = process.cwd();
26
+ const gitClient = simpleGit(ROOT_DIR);
25
27
  // Terminal sessions (managed by Rust PTY binary)
26
28
  const terminals = new Set();
27
29
  // PTY binary process
@@ -220,6 +222,21 @@ function assertSafePath(requestedPath) {
220
222
  // ============================================================================
221
223
  // File System Handlers
222
224
  // ============================================================================
225
+ function isLikelyBinaryBuffer(buffer) {
226
+ if (buffer.length === 0)
227
+ return false;
228
+ if (buffer.includes(0x00))
229
+ return true;
230
+ let suspicious = 0;
231
+ for (let i = 0; i < buffer.length; i++) {
232
+ const byte = buffer[i];
233
+ const isPrintableAscii = byte >= 0x20 && byte <= 0x7e;
234
+ const isCommonControl = byte === 0x09 || byte === 0x0a || byte === 0x0d;
235
+ if (!isPrintableAscii && !isCommonControl)
236
+ suspicious++;
237
+ }
238
+ return suspicious / buffer.length > 0.3;
239
+ }
223
240
  async function handleFsLs(payload) {
224
241
  const reqPath = payload.path || ".";
225
242
  const safePath = assertSafePath(reqPath);
@@ -258,6 +275,25 @@ async function handleFsStat(payload) {
258
275
  mtime: stat.mtimeMs,
259
276
  mode: stat.mode,
260
277
  };
278
+ if (stat.isFile()) {
279
+ try {
280
+ const fd = await fs.open(safePath, "r");
281
+ try {
282
+ const sampleSize = Math.min(stat.size, 8192);
283
+ const sample = Buffer.alloc(sampleSize);
284
+ if (sampleSize > 0) {
285
+ await fd.read(sample, 0, sampleSize, 0);
286
+ }
287
+ result.isBinary = isLikelyBinaryBuffer(sample);
288
+ }
289
+ finally {
290
+ await fd.close();
291
+ }
292
+ }
293
+ catch {
294
+ // Keep stat resilient even if sampling fails
295
+ }
296
+ }
261
297
  return result;
262
298
  }
263
299
  async function handleFsRead(payload) {
@@ -268,8 +304,8 @@ async function handleFsRead(payload) {
268
304
  // Check if binary
269
305
  const stat = await fs.stat(safePath);
270
306
  const content = await fs.readFile(safePath);
271
- // Try to detect if binary
272
- const isBinary = content.includes(0x00);
307
+ // Detect if binary
308
+ const isBinary = isLikelyBinaryBuffer(content.subarray(0, 8192));
273
309
  if (isBinary) {
274
310
  return {
275
311
  path: reqPath,
@@ -570,6 +606,68 @@ async function handleGitLog(payload) {
570
606
  });
571
607
  return { commits };
572
608
  }
609
+ async function handleGitCommitDetails(payload) {
610
+ const hash = payload.hash;
611
+ if (!hash)
612
+ throw Object.assign(new Error("hash is required"), { code: "EINVAL" });
613
+ try {
614
+ const logResult = await gitClient.log({
615
+ from: hash,
616
+ to: hash,
617
+ maxCount: 1,
618
+ format: {
619
+ fullHash: "%H",
620
+ message: "%s",
621
+ author: "%an",
622
+ timestamp: "%at",
623
+ },
624
+ });
625
+ const latest = logResult.latest;
626
+ if (!latest) {
627
+ throw Object.assign(new Error("Commit not found"), { code: "EGIT" });
628
+ }
629
+ const filesRaw = await gitClient.show(["--name-status", "--format=", hash]);
630
+ const files = filesRaw
631
+ .split(/\r?\n/)
632
+ .map((line) => line.trim())
633
+ .filter(Boolean)
634
+ .map((line) => {
635
+ const parts = line.split("\t");
636
+ const status = parts[0] || "?";
637
+ // Handles regular + rename/copy name-status output.
638
+ const path = parts[2] || parts[1] || "";
639
+ return { status, path };
640
+ })
641
+ .filter((entry) => !!entry.path);
642
+ const diff = await gitClient.show(["--patch", "--format=", hash]);
643
+ const fileDiffs = {};
644
+ const fileChunks = diff.split(/^diff --git /m).filter(Boolean);
645
+ for (const chunk of fileChunks) {
646
+ const patch = `diff --git ${chunk}`;
647
+ const firstLine = chunk.split(/\r?\n/, 1)[0] || "";
648
+ const match = firstLine.match(/^a\/(.+?) b\/(.+)$/);
649
+ if (match?.[2]) {
650
+ fileDiffs[match[2]] = patch;
651
+ }
652
+ }
653
+ return {
654
+ commit: {
655
+ hash: latest.fullHash.substring(0, 7),
656
+ fullHash: latest.fullHash,
657
+ message: latest.message,
658
+ author: latest.author,
659
+ date: parseInt(latest.timestamp, 10) * 1000,
660
+ },
661
+ files,
662
+ diff,
663
+ fileDiffs,
664
+ };
665
+ }
666
+ catch (err) {
667
+ const message = err instanceof Error ? err.message : "git show failed";
668
+ throw Object.assign(new Error(message), { code: "EGIT" });
669
+ }
670
+ }
573
671
  async function handleGitDiff(payload) {
574
672
  const filepath = payload.path;
575
673
  const staged = payload.staged === true;
@@ -2110,6 +2208,9 @@ async function processMessage(message) {
2110
2208
  case "log":
2111
2209
  result = await handleGitLog(payload);
2112
2210
  break;
2211
+ case "commitDetails":
2212
+ result = await handleGitCommitDetails(payload);
2213
+ break;
2113
2214
  case "diff":
2114
2215
  result = await handleGitDiff(payload);
2115
2216
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",
@@ -14,7 +14,7 @@
14
14
  "license": "Functional Source License, Version 1.1, Apache 2.0 Future License",
15
15
  "type": "module",
16
16
  "bin": {
17
- "lunel-cli": "./dist/index.js"
17
+ "lunel-cli": "dist/index.js"
18
18
  },
19
19
  "files": [
20
20
  "dist",
@@ -29,6 +29,7 @@
29
29
  "@opencode-ai/sdk": "^1.1.56",
30
30
  "ignore": "^6.0.2",
31
31
  "qrcode-terminal": "^0.12.0",
32
+ "simple-git": "^3.32.3",
32
33
  "ws": "^8.18.0"
33
34
  },
34
35
  "devDependencies": {