opencode-pilot 0.24.12 → 0.25.1
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/Formula/opencode-pilot.rb +2 -2
- package/bin/opencode-pilot +96 -1
- package/package.json +1 -1
- package/service/poller.js +13 -0
- package/test/unit/logs.test.js +120 -0
- package/test/unit/poller.test.js +19 -0
|
@@ -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.
|
|
5
|
-
sha256 "
|
|
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"
|
package/bin/opencode-pilot
CHANGED
|
@@ -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
package/service/poller.js
CHANGED
|
@@ -1215,7 +1215,20 @@ export function createPoller(options = {}) {
|
|
|
1215
1215
|
if (!meta) return false; // Not processed before
|
|
1216
1216
|
|
|
1217
1217
|
// Check if item reappeared after being missing (e.g., uncompleted reminder)
|
|
1218
|
+
// Exception: suppress reprocessing when the item cycled through an intermediate
|
|
1219
|
+
// state (e.g., Linear: In Progress -> In Review -> In Progress). If the stored
|
|
1220
|
+
// state and the current state are both "in progress", the issue just passed
|
|
1221
|
+
// through code review and back — no new work is needed.
|
|
1218
1222
|
if (meta.wasUnseen) {
|
|
1223
|
+
const storedState = meta.itemState;
|
|
1224
|
+
const currentState = item.state || item.status;
|
|
1225
|
+
if (storedState && currentState) {
|
|
1226
|
+
const stored = storedState.toLowerCase();
|
|
1227
|
+
const current = currentState.toLowerCase();
|
|
1228
|
+
if (stored === 'in progress' && current === 'in progress') {
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1219
1232
|
return true;
|
|
1220
1233
|
}
|
|
1221
1234
|
|
|
@@ -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
|
+
});
|
package/test/unit/poller.test.js
CHANGED
|
@@ -746,6 +746,25 @@ describe('poller.js', () => {
|
|
|
746
746
|
);
|
|
747
747
|
});
|
|
748
748
|
|
|
749
|
+
test('shouldReprocess returns false when Linear issue cycles in_progress -> code_review -> in_progress', async () => {
|
|
750
|
+
const { createPoller } = await import('../../service/poller.js');
|
|
751
|
+
|
|
752
|
+
const poller = createPoller({ stateFile });
|
|
753
|
+
// Issue was processed while in_progress
|
|
754
|
+
poller.markProcessed('linear:ENG-1', { source: 'linear', itemState: 'In Progress' });
|
|
755
|
+
|
|
756
|
+
// Issue moved to In Review - disappears from the "my ready issues" poll
|
|
757
|
+
poller.markUnseen('linear', []);
|
|
758
|
+
|
|
759
|
+
// Issue moved back to In Progress - reappears
|
|
760
|
+
const item = { id: 'linear:ENG-1', status: 'In Progress' };
|
|
761
|
+
assert.strictEqual(
|
|
762
|
+
poller.shouldReprocess(item, { reprocessOn: ['status'] }),
|
|
763
|
+
false,
|
|
764
|
+
'should NOT reprocess: issue cycled through code review and returned to same in_progress state'
|
|
765
|
+
);
|
|
766
|
+
});
|
|
767
|
+
|
|
749
768
|
test('shouldReprocess returns true for reappeared item (e.g., uncompleted reminder)', async () => {
|
|
750
769
|
const { createPoller } = await import('../../service/poller.js');
|
|
751
770
|
|