opencode-pilot 0.24.12 → 0.25.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.
@@ -1,8 +1,8 @@
1
1
  class OpencodePilot < Formula
2
2
  desc "Automation daemon for OpenCode - polls GitHub/Linear issues and spawns sessions"
3
3
  homepage "https://github.com/athal7/opencode-pilot"
4
- url "https://github.com/athal7/opencode-pilot/archive/refs/tags/v0.24.11.tar.gz"
5
- sha256 "c0c9e3cdb3b34275d7a1d63b128430dd7e6fdd00dc8fe9e3baa5f3124b955422"
4
+ url "https://github.com/athal7/opencode-pilot/archive/refs/tags/v0.24.12.tar.gz"
5
+ sha256 "ea6f7ba7814225f9e312143f0222777bf4094da87cdbadad1e6589208c5323ab"
6
6
  license "MIT"
7
7
 
8
8
  depends_on "node"
@@ -14,7 +14,7 @@
14
14
  import { fileURLToPath } from "url";
15
15
  import { dirname, join } from "path";
16
16
  import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs";
17
- import { execSync } from "child_process";
17
+ import { execSync, spawn } from "child_process";
18
18
  import os from "os";
19
19
  import YAML from "yaml";
20
20
 
@@ -179,6 +179,7 @@ Commands:
179
179
  status Show service status and version
180
180
  config Validate and show configuration
181
181
  clear Clear processed state entries
182
+ logs Show debug log output
182
183
  test-source NAME Test a source by fetching items and showing mappings
183
184
  test-mapping MCP Test field mappings with sample JSON input
184
185
  help Show this help message
@@ -189,6 +190,11 @@ Clear options:
189
190
  --item ID Clear a specific item
190
191
  --expired Clear only expired entries (uses configured TTL)
191
192
 
193
+ Logs options:
194
+ --path Print the log file path and exit
195
+ --lines N Print last N lines (default: 50)
196
+ --follow Follow the log file (like tail -f)
197
+
192
198
  The service handles:
193
199
  - Polling for GitHub/Linear issues to work on
194
200
  - Spawning OpenCode sessions for ready items
@@ -200,6 +206,10 @@ Examples:
200
206
  opencode-pilot config # Validate and show config
201
207
  opencode-pilot clear --all # Clear all processed state
202
208
  opencode-pilot clear --expired # Clear expired entries
209
+ opencode-pilot logs # Show last 50 log lines
210
+ opencode-pilot logs --lines 100 # Show last 100 log lines
211
+ opencode-pilot logs --follow # Follow the log in real time
212
+ opencode-pilot logs --path # Print the log file path
203
213
  opencode-pilot test-source my-issues # Test a source
204
214
  echo '{"url":"https://linear.app/team/issue/PROJ-123/title"}' | opencode-pilot test-mapping linear
205
215
  `);
@@ -812,6 +822,87 @@ async function clearCommand(flags) {
812
822
  }
813
823
  }
814
824
 
825
+ // ============================================================================
826
+ // Logs Command
827
+ // ============================================================================
828
+
829
+ // Default log path (mirrors service/logger.js)
830
+ const DEFAULT_LOG_PATH = join(os.homedir(), ".local", "share", "opencode-pilot", "debug.log");
831
+
832
+ /**
833
+ * Resolve the debug log file path.
834
+ * Can be overridden via PILOT_LOG_PATH env var (used in tests).
835
+ */
836
+ function getLogPath() {
837
+ return process.env.PILOT_LOG_PATH || DEFAULT_LOG_PATH;
838
+ }
839
+
840
+ /**
841
+ * Read the last `n` lines from a file synchronously.
842
+ * Returns an empty array if the file does not exist.
843
+ */
844
+ function tailLines(filePath, n) {
845
+ if (!existsSync(filePath)) return [];
846
+ const content = readFileSync(filePath, "utf8");
847
+ const lines = content.split("\n");
848
+ // Remove trailing empty line produced by a file ending with \n
849
+ if (lines[lines.length - 1] === "") lines.pop();
850
+ return lines.slice(-n);
851
+ }
852
+
853
+ /**
854
+ * Parse a --lines flag value, falling back to `defaultN` for missing/invalid input.
855
+ */
856
+ function parseLineCount(value, defaultN) {
857
+ if (value === undefined || value === true || value === false) return defaultN;
858
+ const n = parseInt(value, 10);
859
+ return Number.isFinite(n) && n > 0 ? n : defaultN;
860
+ }
861
+
862
+ async function logsCommand(flags) {
863
+ const logPath = getLogPath();
864
+
865
+ // --path: just print the path and exit
866
+ if (flags.path) {
867
+ console.log(logPath);
868
+ return;
869
+ }
870
+
871
+ // --follow: stream the file using tail -f
872
+ if (flags.follow) {
873
+ if (!existsSync(logPath)) {
874
+ console.log(`No log file yet. Enable debug logging with PILOT_DEBUG=true.`);
875
+ console.log(`Log path: ${logPath}`);
876
+ return;
877
+ }
878
+ const lineCount = parseLineCount(flags.lines, 50);
879
+ // Use system `tail -f` for reliable follow behaviour
880
+ const tail = spawn("tail", ["-f", "-n", String(lineCount), logPath], {
881
+ stdio: ["ignore", "inherit", "inherit"],
882
+ });
883
+ await new Promise((resolve, reject) => {
884
+ tail.on("error", reject);
885
+ tail.on("close", resolve);
886
+ });
887
+ return;
888
+ }
889
+
890
+ // Default: print last N lines
891
+ const n = parseLineCount(flags.lines, 50);
892
+ if (!existsSync(logPath)) {
893
+ console.log(`No log file found. Debug logging is enabled via PILOT_DEBUG=true.`);
894
+ console.log(`Log path: ${logPath}`);
895
+ return;
896
+ }
897
+
898
+ const lines = tailLines(logPath, n);
899
+ if (lines.length === 0) {
900
+ console.log(`Log file is empty: ${logPath}`);
901
+ return;
902
+ }
903
+ console.log(lines.join("\n"));
904
+ }
905
+
815
906
  // ============================================================================
816
907
  // Main
817
908
  // ============================================================================
@@ -846,6 +937,10 @@ async function main() {
846
937
  await clearCommand(parseArgs(args).flags);
847
938
  break;
848
939
 
940
+ case "logs":
941
+ await logsCommand(parseArgs(args).flags);
942
+ break;
943
+
849
944
  case "test-source":
850
945
  await testSourceCommand(subcommand);
851
946
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-pilot",
3
- "version": "0.24.12",
3
+ "version": "0.25.0",
4
4
  "type": "module",
5
5
  "main": "plugin/index.js",
6
6
  "description": "Automation daemon for OpenCode - polls for work and spawns sessions",
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Tests for the `logs` CLI command.
3
+ *
4
+ * The command prints the debug log path and optionally tails/follows it.
5
+ * Flags:
6
+ * --path Print only the log file path
7
+ * --lines N Print last N lines (default 50)
8
+ * --follow Follow the log file (tail -f)
9
+ */
10
+
11
+ import { test, describe } from "node:test";
12
+ import assert from "node:assert";
13
+ import { spawnSync } from "child_process";
14
+ import { join, dirname } from "path";
15
+ import { fileURLToPath } from "url";
16
+ import {
17
+ writeFileSync,
18
+ mkdirSync,
19
+ rmSync,
20
+ existsSync,
21
+ } from "fs";
22
+ import { tmpdir } from "os";
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = dirname(__filename);
26
+ const CLI = join(__dirname, "..", "..", "bin", "opencode-pilot");
27
+
28
+ // Helper: run opencode-pilot with overridden log path via env
29
+ function runLogs(args = [], env = {}) {
30
+ return spawnSync(process.execPath, [CLI, "logs", ...args], {
31
+ encoding: "utf8",
32
+ timeout: 5000,
33
+ env: { ...process.env, ...env },
34
+ });
35
+ }
36
+
37
+ describe("logs command", () => {
38
+ let tmpDir;
39
+ let logFile;
40
+
41
+ // Create a temp log file for each test group
42
+ function setupLogFile(lines = []) {
43
+ tmpDir = join(tmpdir(), `opencode-pilot-test-${Date.now()}`);
44
+ mkdirSync(tmpDir, { recursive: true });
45
+ logFile = join(tmpDir, "debug.log");
46
+ if (lines.length) {
47
+ writeFileSync(logFile, lines.join("\n") + "\n");
48
+ }
49
+ return logFile;
50
+ }
51
+
52
+ function cleanup() {
53
+ if (tmpDir && existsSync(tmpDir)) {
54
+ rmSync(tmpDir, { recursive: true, force: true });
55
+ }
56
+ }
57
+
58
+ describe("--path flag", () => {
59
+ test("prints the log file path", () => {
60
+ const path = setupLogFile();
61
+ const result = runLogs(["--path"], { PILOT_LOG_PATH: path });
62
+ cleanup();
63
+ assert.strictEqual(result.status, 0);
64
+ assert.match(result.stdout.trim(), /debug\.log/);
65
+ });
66
+
67
+ test("prints path even when log file does not exist", () => {
68
+ const nonExistentPath = join(tmpdir(), "no-such-dir", "debug.log");
69
+ const result = runLogs(["--path"], { PILOT_LOG_PATH: nonExistentPath });
70
+ assert.strictEqual(result.status, 0);
71
+ assert.match(result.stdout.trim(), /debug\.log/);
72
+ });
73
+ });
74
+
75
+ describe("default output (last N lines)", () => {
76
+ test("prints last 50 lines by default when log exists", () => {
77
+ const lines = Array.from({ length: 60 }, (_, i) => `line ${i + 1}`);
78
+ const path = setupLogFile(lines);
79
+ const result = runLogs([], { PILOT_LOG_PATH: path });
80
+ cleanup();
81
+ assert.strictEqual(result.status, 0);
82
+ // Should contain lines 11-60 (last 50)
83
+ assert.match(result.stdout, /line 60/);
84
+ // Lines 1-10 should be excluded (only last 50 of 60 are shown)
85
+ assert.doesNotMatch(result.stdout, /^line [1-9]\b/m);
86
+ });
87
+
88
+ test("--lines N prints last N lines", () => {
89
+ const lines = Array.from({ length: 20 }, (_, i) => `entry ${i + 1}`);
90
+ const path = setupLogFile(lines);
91
+ const result = runLogs(["--lines", "5"], { PILOT_LOG_PATH: path });
92
+ cleanup();
93
+ assert.strictEqual(result.status, 0);
94
+ assert.match(result.stdout, /entry 20/);
95
+ assert.doesNotMatch(result.stdout, /entry 1\n/);
96
+ });
97
+
98
+ test("exits 0 with informational message when log does not exist", () => {
99
+ const nonExistentPath = join(
100
+ tmpdir(),
101
+ `no-log-${Date.now()}`,
102
+ "debug.log"
103
+ );
104
+ const result = runLogs([], { PILOT_LOG_PATH: nonExistentPath });
105
+ assert.strictEqual(result.status, 0);
106
+ assert.match(result.stdout, /no log/i);
107
+ });
108
+ });
109
+
110
+ describe("help text", () => {
111
+ test("opencode-pilot help includes logs command", () => {
112
+ const result = spawnSync(process.execPath, [CLI, "help"], {
113
+ encoding: "utf8",
114
+ timeout: 5000,
115
+ });
116
+ assert.strictEqual(result.status, 0);
117
+ assert.match(result.stdout, /logs/);
118
+ });
119
+ });
120
+ });