opencode-pilot 0.1.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.
- package/.devcontainer/devcontainer.json +16 -0
- package/.github/workflows/ci.yml +67 -0
- package/.releaserc.cjs +28 -0
- package/AGENTS.md +71 -0
- package/CONTRIBUTING.md +102 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/bin/opencode-pilot +809 -0
- package/dist/opencode-ntfy.tar.gz +0 -0
- package/examples/config.yaml +73 -0
- package/examples/templates/default.md +7 -0
- package/examples/templates/devcontainer.md +7 -0
- package/examples/templates/review-feedback.md +7 -0
- package/examples/templates/review.md +15 -0
- package/install.sh +246 -0
- package/package.json +40 -0
- package/plugin/config.js +76 -0
- package/plugin/index.js +260 -0
- package/plugin/logger.js +125 -0
- package/plugin/notifier.js +110 -0
- package/service/actions.js +334 -0
- package/service/io.opencode.ntfy.plist +29 -0
- package/service/logger.js +82 -0
- package/service/poll-service.js +246 -0
- package/service/poller.js +339 -0
- package/service/readiness.js +234 -0
- package/service/repo-config.js +222 -0
- package/service/server.js +1523 -0
- package/service/utils.js +21 -0
- package/test/run_tests.bash +34 -0
- package/test/test_actions.bash +263 -0
- package/test/test_cli.bash +161 -0
- package/test/test_config.bash +438 -0
- package/test/test_helper.bash +140 -0
- package/test/test_logger.bash +401 -0
- package/test/test_notifier.bash +310 -0
- package/test/test_plist.bash +125 -0
- package/test/test_plugin.bash +952 -0
- package/test/test_poll_service.bash +179 -0
- package/test/test_poller.bash +120 -0
- package/test/test_readiness.bash +313 -0
- package/test/test_repo_config.bash +406 -0
- package/test/test_service.bash +1342 -0
- package/test/unit/actions.test.js +235 -0
- package/test/unit/config.test.js +86 -0
- package/test/unit/paths.test.js +77 -0
- package/test/unit/poll-service.test.js +142 -0
- package/test/unit/poller.test.js +347 -0
- package/test/unit/repo-config.test.js +441 -0
- package/test/unit/utils.test.js +53 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* opencode-pilot - CLI for opencode-pilot automation
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* setup Copy plugin files and configure OpenCode
|
|
7
|
+
* status Show installation and service status
|
|
8
|
+
* help Show help
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { dirname, join } from "path";
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync } from "fs";
|
|
14
|
+
import { execSync } from "child_process";
|
|
15
|
+
import os from "os";
|
|
16
|
+
|
|
17
|
+
// Get script directory for relative imports
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
|
|
21
|
+
// Find service directory - check multiple locations for homebrew compatibility
|
|
22
|
+
function findServiceDir() {
|
|
23
|
+
const candidates = [
|
|
24
|
+
join(__dirname, "..", "service"), // Development: bin/../service
|
|
25
|
+
join(__dirname, "..", "libexec"), // Homebrew: bin/../libexec
|
|
26
|
+
];
|
|
27
|
+
for (const dir of candidates) {
|
|
28
|
+
if (existsSync(join(dir, "server.js"))) {
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return candidates[0];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Find plugin directory
|
|
36
|
+
function findPluginDir() {
|
|
37
|
+
const candidates = [
|
|
38
|
+
join(__dirname, "..", "plugin"), // Development
|
|
39
|
+
join(__dirname, "..", "lib", "opencode-pilot", "plugin"), // Homebrew
|
|
40
|
+
];
|
|
41
|
+
for (const dir of candidates) {
|
|
42
|
+
if (existsSync(join(dir, "index.js"))) {
|
|
43
|
+
return dir;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const serviceDir = findServiceDir();
|
|
50
|
+
|
|
51
|
+
// Paths
|
|
52
|
+
const OPENCODE_CONFIG_DIR = join(os.homedir(), ".config/opencode");
|
|
53
|
+
const PLUGIN_NAME = "opencode-pilot";
|
|
54
|
+
const PLUGIN_DEST = join(OPENCODE_CONFIG_DIR, "plugins", PLUGIN_NAME);
|
|
55
|
+
const OPENCODE_CONFIG_FILE = join(OPENCODE_CONFIG_DIR, "opencode.json");
|
|
56
|
+
const PILOT_CONFIG_FILE = join(os.homedir(), ".config/opencode-pilot/config.yaml");
|
|
57
|
+
const PILOT_TEMPLATES_DIR = join(os.homedir(), ".config/opencode-pilot/templates");
|
|
58
|
+
|
|
59
|
+
// Parse command line arguments
|
|
60
|
+
function parseArgs(args) {
|
|
61
|
+
const result = {
|
|
62
|
+
command: null,
|
|
63
|
+
subcommand: null,
|
|
64
|
+
flags: {},
|
|
65
|
+
positional: [],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let i = 0;
|
|
69
|
+
while (i < args.length) {
|
|
70
|
+
const arg = args[i];
|
|
71
|
+
|
|
72
|
+
if (arg.startsWith("--")) {
|
|
73
|
+
const key = arg.slice(2);
|
|
74
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
75
|
+
result.flags[key] = args[i + 1];
|
|
76
|
+
i += 2;
|
|
77
|
+
} else {
|
|
78
|
+
result.flags[key] = true;
|
|
79
|
+
i += 1;
|
|
80
|
+
}
|
|
81
|
+
} else if (arg.startsWith("-")) {
|
|
82
|
+
const key = arg.slice(1);
|
|
83
|
+
result.flags[key] = true;
|
|
84
|
+
i += 1;
|
|
85
|
+
} else if (!result.command) {
|
|
86
|
+
result.command = arg;
|
|
87
|
+
i += 1;
|
|
88
|
+
} else if (!result.subcommand) {
|
|
89
|
+
result.subcommand = arg;
|
|
90
|
+
i += 1;
|
|
91
|
+
} else {
|
|
92
|
+
result.positional.push(arg);
|
|
93
|
+
i += 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Help text
|
|
101
|
+
function showHelp() {
|
|
102
|
+
console.log(`opencode-pilot - Automation layer for OpenCode
|
|
103
|
+
|
|
104
|
+
Usage:
|
|
105
|
+
opencode-pilot <command> [options]
|
|
106
|
+
|
|
107
|
+
Commands:
|
|
108
|
+
start Start the callback service (foreground)
|
|
109
|
+
setup Copy plugin files and configure OpenCode
|
|
110
|
+
status Show installation and service status
|
|
111
|
+
config Validate and show configuration
|
|
112
|
+
test-source NAME Test a source by fetching items and showing mappings
|
|
113
|
+
test-mapping MCP Test field mappings with sample JSON input
|
|
114
|
+
help Show this help message
|
|
115
|
+
|
|
116
|
+
The service handles:
|
|
117
|
+
- Notification callbacks from ntfy
|
|
118
|
+
- Polling for GitHub/Linear issues to work on
|
|
119
|
+
|
|
120
|
+
Examples:
|
|
121
|
+
opencode-pilot start # Start service (foreground)
|
|
122
|
+
opencode-pilot setup # Initial setup
|
|
123
|
+
opencode-pilot status # Check status
|
|
124
|
+
opencode-pilot config # Validate and show config
|
|
125
|
+
opencode-pilot test-source my-issues # Test a source
|
|
126
|
+
echo '{"url":"https://linear.app/team/issue/PROJ-123/title"}' | opencode-pilot test-mapping linear
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Start Command
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
async function startCommand() {
|
|
135
|
+
// Import and run the service directly
|
|
136
|
+
const serverPath = join(serviceDir, "server.js");
|
|
137
|
+
if (!existsSync(serverPath)) {
|
|
138
|
+
console.error("ERROR: Could not find server.js");
|
|
139
|
+
console.error(`Expected at: ${serverPath}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log("[opencode-pilot] Starting callback service...");
|
|
144
|
+
|
|
145
|
+
// Dynamic import of the service module
|
|
146
|
+
const { startService, stopService } = await import(serverPath);
|
|
147
|
+
|
|
148
|
+
const config = {
|
|
149
|
+
httpPort: parseInt(process.env.NTFY_CALLBACK_PORT || "4097", 10),
|
|
150
|
+
socketPath: process.env.NTFY_SOCKET_PATH || "/tmp/opencode-pilot.sock",
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const service = await startService(config);
|
|
154
|
+
|
|
155
|
+
// Handle graceful shutdown
|
|
156
|
+
process.on("SIGTERM", async () => {
|
|
157
|
+
console.log("[opencode-pilot] Received SIGTERM, shutting down...");
|
|
158
|
+
await stopService(service);
|
|
159
|
+
process.exit(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
process.on("SIGINT", async () => {
|
|
163
|
+
console.log("[opencode-pilot] Received SIGINT, shutting down...");
|
|
164
|
+
await stopService(service);
|
|
165
|
+
process.exit(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Keep running until signal received
|
|
169
|
+
await new Promise(() => {});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Setup Command
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
async function setupCommand() {
|
|
177
|
+
console.log("Setting up opencode-pilot...");
|
|
178
|
+
console.log("");
|
|
179
|
+
|
|
180
|
+
const pluginSource = findPluginDir();
|
|
181
|
+
if (!pluginSource) {
|
|
182
|
+
console.error("ERROR: Could not find plugin source files");
|
|
183
|
+
console.error("Install via: brew install athal7/tap/opencode-pilot");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log(`Source: ${pluginSource}`);
|
|
188
|
+
console.log(`Destination: ${PLUGIN_DEST}`);
|
|
189
|
+
console.log("");
|
|
190
|
+
|
|
191
|
+
// Backup existing config
|
|
192
|
+
if (existsSync(OPENCODE_CONFIG_FILE)) {
|
|
193
|
+
const backupFile = `${OPENCODE_CONFIG_FILE}.backup.${Date.now()}`;
|
|
194
|
+
try {
|
|
195
|
+
cpSync(OPENCODE_CONFIG_FILE, backupFile);
|
|
196
|
+
console.log(`Backup created: ${backupFile}`);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error("ERROR: Failed to create backup of opencode.json");
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Copy plugin files
|
|
204
|
+
mkdirSync(join(OPENCODE_CONFIG_DIR, "plugins"), { recursive: true });
|
|
205
|
+
if (existsSync(PLUGIN_DEST)) {
|
|
206
|
+
rmSync(PLUGIN_DEST, { recursive: true });
|
|
207
|
+
}
|
|
208
|
+
cpSync(pluginSource, PLUGIN_DEST, { recursive: true });
|
|
209
|
+
console.log("Plugin files copied.");
|
|
210
|
+
|
|
211
|
+
// Update opencode.json
|
|
212
|
+
let config = {};
|
|
213
|
+
if (existsSync(OPENCODE_CONFIG_FILE)) {
|
|
214
|
+
try {
|
|
215
|
+
config = JSON.parse(readFileSync(OPENCODE_CONFIG_FILE, "utf8"));
|
|
216
|
+
} catch {
|
|
217
|
+
config = {};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
config.plugin = config.plugin || [];
|
|
222
|
+
const alreadyRegistered = config.plugin.some(p => p.includes(`plugins/${PLUGIN_NAME}`));
|
|
223
|
+
|
|
224
|
+
if (alreadyRegistered) {
|
|
225
|
+
console.log("Plugin already in opencode.json");
|
|
226
|
+
} else {
|
|
227
|
+
config.plugin.push(PLUGIN_DEST);
|
|
228
|
+
const tempFile = `${OPENCODE_CONFIG_FILE}.tmp`;
|
|
229
|
+
writeFileSync(tempFile, JSON.stringify(config, null, 2) + "\n");
|
|
230
|
+
const { renameSync } = await import("fs");
|
|
231
|
+
renameSync(tempFile, OPENCODE_CONFIG_FILE);
|
|
232
|
+
console.log("Added plugin to opencode.json");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Try to restart service
|
|
236
|
+
try {
|
|
237
|
+
execSync("brew services restart opencode-pilot", { stdio: "pipe" });
|
|
238
|
+
console.log("");
|
|
239
|
+
console.log("Service restarted.");
|
|
240
|
+
} catch {
|
|
241
|
+
// Not installed via brew or service not available
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log("");
|
|
245
|
+
console.log("Setup complete!");
|
|
246
|
+
console.log("");
|
|
247
|
+
console.log("Next steps:");
|
|
248
|
+
console.log(" 1. Edit ~/.config/opencode-pilot/config.yaml");
|
|
249
|
+
console.log(" 2. Run: opencode-pilot start");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// Status Command
|
|
254
|
+
// ============================================================================
|
|
255
|
+
|
|
256
|
+
function getConfigValue(envVar, configKey, defaultValue = "") {
|
|
257
|
+
// Check env var first
|
|
258
|
+
if (process.env[envVar]) {
|
|
259
|
+
return process.env[envVar];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check config file
|
|
263
|
+
if (existsSync(PILOT_CONFIG_FILE)) {
|
|
264
|
+
try {
|
|
265
|
+
const config = JSON.parse(readFileSync(PILOT_CONFIG_FILE, "utf8"));
|
|
266
|
+
if (config[configKey] !== undefined && config[configKey] !== "") {
|
|
267
|
+
return config[configKey];
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// Ignore
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return defaultValue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function statusCommand() {
|
|
278
|
+
console.log("opencode-pilot status");
|
|
279
|
+
console.log("=====================");
|
|
280
|
+
console.log("");
|
|
281
|
+
|
|
282
|
+
// Plugin installed?
|
|
283
|
+
if (existsSync(PLUGIN_DEST)) {
|
|
284
|
+
console.log(`Plugin: installed at ${PLUGIN_DEST}`);
|
|
285
|
+
} else {
|
|
286
|
+
console.log("Plugin: NOT INSTALLED (run: opencode-pilot setup)");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Config?
|
|
290
|
+
if (existsSync(OPENCODE_CONFIG_FILE)) {
|
|
291
|
+
try {
|
|
292
|
+
const config = JSON.parse(readFileSync(OPENCODE_CONFIG_FILE, "utf8"));
|
|
293
|
+
const registered = (config.plugin || []).some(p => p.includes(`plugins/${PLUGIN_NAME}`));
|
|
294
|
+
if (registered) {
|
|
295
|
+
console.log("Config: plugin registered in opencode.json");
|
|
296
|
+
} else {
|
|
297
|
+
console.log("Config: plugin NOT in opencode.json");
|
|
298
|
+
}
|
|
299
|
+
} catch {
|
|
300
|
+
console.log("Config: could not parse opencode.json");
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
console.log("Config: opencode.json not found");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Service running? Check if socket exists and HTTP responds
|
|
307
|
+
const socketPath = "/tmp/opencode-pilot.sock";
|
|
308
|
+
const servicePort = getConfigValue("NTFY_CALLBACK_PORT", "callbackPort", "4097");
|
|
309
|
+
|
|
310
|
+
if (existsSync(socketPath)) {
|
|
311
|
+
// Socket exists, try health check
|
|
312
|
+
try {
|
|
313
|
+
const res = execSync(`curl -s -o /dev/null -w "%{http_code}" http://localhost:${servicePort}/health`, { encoding: "utf8", timeout: 2000 });
|
|
314
|
+
if (res.trim() === "200") {
|
|
315
|
+
console.log("Service: running");
|
|
316
|
+
} else {
|
|
317
|
+
console.log("Service: socket exists but not responding (run: opencode-pilot start)");
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
console.log("Service: socket exists but health check failed");
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
console.log("Service: not running (run: opencode-pilot start)");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Notification configuration
|
|
327
|
+
console.log("");
|
|
328
|
+
console.log("Notification Configuration:");
|
|
329
|
+
|
|
330
|
+
const topic = getConfigValue("NTFY_TOPIC", "topic", "");
|
|
331
|
+
const server = getConfigValue("NTFY_SERVER", "server", "https://ntfy.sh");
|
|
332
|
+
const callbackHost = getConfigValue("NTFY_CALLBACK_HOST", "callbackHost", "");
|
|
333
|
+
const callbackPort = getConfigValue("NTFY_CALLBACK_PORT", "callbackPort", "4097");
|
|
334
|
+
|
|
335
|
+
console.log(` topic: ${topic || "<not set>"}`);
|
|
336
|
+
console.log(` server: ${server}`);
|
|
337
|
+
console.log(` callbackHost: ${callbackHost || "<not set>"}`);
|
|
338
|
+
console.log(` callbackPort: ${callbackPort}`);
|
|
339
|
+
|
|
340
|
+
// Polling configuration
|
|
341
|
+
console.log("");
|
|
342
|
+
console.log("Polling Configuration:");
|
|
343
|
+
if (existsSync(PILOT_CONFIG_FILE)) {
|
|
344
|
+
console.log(` config.yaml: ${PILOT_CONFIG_FILE}`);
|
|
345
|
+
console.log(" polling: enabled (handled by service)");
|
|
346
|
+
} else {
|
|
347
|
+
console.log(` config.yaml: not found at ${PILOT_CONFIG_FILE}`);
|
|
348
|
+
console.log(" polling: disabled");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ============================================================================
|
|
353
|
+
// Config Command
|
|
354
|
+
// ============================================================================
|
|
355
|
+
|
|
356
|
+
async function configCommand() {
|
|
357
|
+
console.log("opencode-pilot configuration");
|
|
358
|
+
console.log("============================");
|
|
359
|
+
console.log("");
|
|
360
|
+
|
|
361
|
+
// Check config file exists
|
|
362
|
+
if (!existsSync(PILOT_CONFIG_FILE)) {
|
|
363
|
+
console.log(`Config file not found: ${PILOT_CONFIG_FILE}`);
|
|
364
|
+
console.log("");
|
|
365
|
+
console.log("Create one by copying the example:");
|
|
366
|
+
console.log(" cp node_modules/opencode-pilot/examples/config.yaml ~/.config/opencode-pilot/config.yaml");
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Load and parse config
|
|
371
|
+
let config;
|
|
372
|
+
try {
|
|
373
|
+
const { default: YAML } = await import("yaml");
|
|
374
|
+
const content = readFileSync(PILOT_CONFIG_FILE, "utf8");
|
|
375
|
+
config = YAML.parse(content);
|
|
376
|
+
console.log("✓ Config file parsed successfully");
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.log(`✗ Failed to parse config: ${err.message}`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Validate notifications section
|
|
383
|
+
console.log("");
|
|
384
|
+
console.log("Notifications:");
|
|
385
|
+
const notifications = config.notifications || {};
|
|
386
|
+
if (notifications.topic) {
|
|
387
|
+
console.log(` ✓ topic: ${notifications.topic.substring(0, 8)}...`);
|
|
388
|
+
} else {
|
|
389
|
+
console.log(" ✗ topic: not set (required for notifications)");
|
|
390
|
+
}
|
|
391
|
+
console.log(` server: ${notifications.server || "https://ntfy.sh"}`);
|
|
392
|
+
if (notifications.callback_host) {
|
|
393
|
+
console.log(` ✓ callback_host: ${notifications.callback_host}`);
|
|
394
|
+
} else {
|
|
395
|
+
console.log(" ⚠ callback_host: not set (interactive notifications disabled)");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Validate defaults section
|
|
399
|
+
console.log("");
|
|
400
|
+
console.log("Defaults:");
|
|
401
|
+
const defaults = config.defaults || {};
|
|
402
|
+
console.log(` working_dir: ${defaults.working_dir || "~"}`);
|
|
403
|
+
console.log(` prompt: ${defaults.prompt || "default"}`);
|
|
404
|
+
if (defaults.agent) console.log(` agent: ${defaults.agent}`);
|
|
405
|
+
if (defaults.model) console.log(` model: ${defaults.model}`);
|
|
406
|
+
|
|
407
|
+
// Validate repos section
|
|
408
|
+
console.log("");
|
|
409
|
+
console.log("Repos:");
|
|
410
|
+
const repos = config.repos || {};
|
|
411
|
+
const repoKeys = Object.keys(repos);
|
|
412
|
+
if (repoKeys.length === 0) {
|
|
413
|
+
console.log(" (none configured)");
|
|
414
|
+
} else {
|
|
415
|
+
for (const key of repoKeys) {
|
|
416
|
+
const repo = repos[key];
|
|
417
|
+
const isPrefix = key.endsWith("/");
|
|
418
|
+
const path = repo.path || repo.repo_path;
|
|
419
|
+
const expandedPath = path ? path.replace("~", os.homedir()).replace("{repo}", "<repo>") : "<not set>";
|
|
420
|
+
const pathExists = path && !path.includes("{repo}") ? existsSync(path.replace("~", os.homedir())) : null;
|
|
421
|
+
|
|
422
|
+
if (pathExists === false) {
|
|
423
|
+
console.log(` ✗ ${key}: ${expandedPath} (path not found)`);
|
|
424
|
+
} else if (isPrefix) {
|
|
425
|
+
console.log(` ${key}: ${expandedPath} (prefix)`);
|
|
426
|
+
} else {
|
|
427
|
+
console.log(` ✓ ${key}: ${expandedPath}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Validate sources section
|
|
433
|
+
console.log("");
|
|
434
|
+
console.log("Sources:");
|
|
435
|
+
const sources = config.sources || [];
|
|
436
|
+
if (sources.length === 0) {
|
|
437
|
+
console.log(" (none configured)");
|
|
438
|
+
} else {
|
|
439
|
+
for (const source of sources) {
|
|
440
|
+
const name = source.name || "<unnamed>";
|
|
441
|
+
const tool = source.tool;
|
|
442
|
+
|
|
443
|
+
if (!tool || !tool.mcp || !tool.name) {
|
|
444
|
+
console.log(` ✗ ${name}: missing tool.mcp or tool.name`);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const itemId = source.item?.id;
|
|
449
|
+
if (!itemId) {
|
|
450
|
+
console.log(` ⚠ ${name}: ${tool.mcp}/${tool.name} (no item.id template)`);
|
|
451
|
+
} else {
|
|
452
|
+
console.log(` ✓ ${name}: ${tool.mcp}/${tool.name}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Show repo resolution
|
|
456
|
+
if (source.repo) {
|
|
457
|
+
console.log(` repo: ${source.repo}`);
|
|
458
|
+
} else if (source.repos) {
|
|
459
|
+
console.log(` repos: ${Array.isArray(source.repos) ? source.repos.join(", ") : JSON.stringify(source.repos)}`);
|
|
460
|
+
} else if (source.working_dir) {
|
|
461
|
+
console.log(` working_dir: ${source.working_dir}`);
|
|
462
|
+
} else {
|
|
463
|
+
console.log(` (uses defaults.working_dir)`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Validate templates
|
|
469
|
+
console.log("");
|
|
470
|
+
console.log("Templates:");
|
|
471
|
+
if (!existsSync(PILOT_TEMPLATES_DIR)) {
|
|
472
|
+
console.log(` ⚠ Templates directory not found: ${PILOT_TEMPLATES_DIR}`);
|
|
473
|
+
} else {
|
|
474
|
+
const { readdirSync } = await import("fs");
|
|
475
|
+
const templates = readdirSync(PILOT_TEMPLATES_DIR).filter(f => f.endsWith(".md"));
|
|
476
|
+
if (templates.length === 0) {
|
|
477
|
+
console.log(" (no templates found)");
|
|
478
|
+
} else {
|
|
479
|
+
for (const t of templates) {
|
|
480
|
+
console.log(` ✓ ${t.replace(".md", "")}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Check if referenced templates exist
|
|
485
|
+
const referencedTemplates = new Set();
|
|
486
|
+
if (defaults.prompt) referencedTemplates.add(defaults.prompt);
|
|
487
|
+
for (const repo of Object.values(repos)) {
|
|
488
|
+
if (repo.prompt) referencedTemplates.add(repo.prompt);
|
|
489
|
+
}
|
|
490
|
+
for (const source of sources) {
|
|
491
|
+
if (source.prompt) referencedTemplates.add(source.prompt);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const existingTemplates = new Set(templates.map(t => t.replace(".md", "")));
|
|
495
|
+
for (const ref of referencedTemplates) {
|
|
496
|
+
if (!existingTemplates.has(ref)) {
|
|
497
|
+
console.log(` ✗ ${ref} (referenced but not found)`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
console.log("");
|
|
503
|
+
console.log("Configuration valid!");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ============================================================================
|
|
507
|
+
// Test Source Command
|
|
508
|
+
// ============================================================================
|
|
509
|
+
|
|
510
|
+
async function testSourceCommand(sourceName) {
|
|
511
|
+
if (!sourceName) {
|
|
512
|
+
console.error("Usage: opencode-pilot test-source <source-name>");
|
|
513
|
+
console.error("");
|
|
514
|
+
console.error("Available sources:");
|
|
515
|
+
|
|
516
|
+
// Load and show available sources
|
|
517
|
+
if (existsSync(PILOT_CONFIG_FILE)) {
|
|
518
|
+
try {
|
|
519
|
+
const { default: YAML } = await import("yaml");
|
|
520
|
+
const content = readFileSync(PILOT_CONFIG_FILE, "utf8");
|
|
521
|
+
const config = YAML.parse(content);
|
|
522
|
+
const sources = config.sources || [];
|
|
523
|
+
if (sources.length === 0) {
|
|
524
|
+
console.error(" (no sources configured)");
|
|
525
|
+
} else {
|
|
526
|
+
for (const s of sources) {
|
|
527
|
+
console.error(` ${s.name}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
} catch (err) {
|
|
531
|
+
console.error(` Error loading config: ${err.message}`);
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
console.error(" (config file not found)");
|
|
535
|
+
}
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
console.log(`Testing source: ${sourceName}`);
|
|
540
|
+
console.log("=".repeat(40));
|
|
541
|
+
console.log("");
|
|
542
|
+
|
|
543
|
+
// Load config
|
|
544
|
+
if (!existsSync(PILOT_CONFIG_FILE)) {
|
|
545
|
+
console.error(`Config file not found: ${PILOT_CONFIG_FILE}`);
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const { default: YAML } = await import("yaml");
|
|
550
|
+
const content = readFileSync(PILOT_CONFIG_FILE, "utf8");
|
|
551
|
+
const config = YAML.parse(content);
|
|
552
|
+
|
|
553
|
+
// Find source
|
|
554
|
+
const sources = config.sources || [];
|
|
555
|
+
const source = sources.find(s => s.name === sourceName);
|
|
556
|
+
|
|
557
|
+
if (!source) {
|
|
558
|
+
console.error(`Source not found: ${sourceName}`);
|
|
559
|
+
console.error("");
|
|
560
|
+
console.error("Available sources:");
|
|
561
|
+
for (const s of sources) {
|
|
562
|
+
console.error(` ${s.name}`);
|
|
563
|
+
}
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Show source config
|
|
568
|
+
console.log("Source Configuration:");
|
|
569
|
+
console.log(` name: ${source.name}`);
|
|
570
|
+
console.log(` tool.mcp: ${source.tool?.mcp || "(not set)"}`);
|
|
571
|
+
console.log(` tool.name: ${source.tool?.name || "(not set)"}`);
|
|
572
|
+
console.log(` args: ${JSON.stringify(source.args || {})}`);
|
|
573
|
+
console.log(` item.id: ${source.item?.id || "(not set)"}`);
|
|
574
|
+
if (source.repo) console.log(` repo: ${source.repo}`);
|
|
575
|
+
if (source.working_dir) console.log(` working_dir: ${source.working_dir}`);
|
|
576
|
+
if (source.prompt) console.log(` prompt: ${source.prompt}`);
|
|
577
|
+
if (source.agent) console.log(` agent: ${source.agent}`);
|
|
578
|
+
|
|
579
|
+
// Show mappings
|
|
580
|
+
const tools = config.tools || {};
|
|
581
|
+
const provider = source.tool?.mcp;
|
|
582
|
+
const mappings = tools[provider]?.mappings;
|
|
583
|
+
|
|
584
|
+
console.log("");
|
|
585
|
+
console.log("Field Mappings:");
|
|
586
|
+
if (mappings) {
|
|
587
|
+
for (const [target, sourcePath] of Object.entries(mappings)) {
|
|
588
|
+
console.log(` ${target} ← ${sourcePath}`);
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
console.log(` (no mappings configured for provider '${provider}')`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Fetch items
|
|
595
|
+
console.log("");
|
|
596
|
+
console.log("Fetching items...");
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
// Import poller
|
|
600
|
+
const pollerPath = join(serviceDir, "poller.js");
|
|
601
|
+
const { pollGenericSource, applyMappings } = await import(pollerPath);
|
|
602
|
+
const { getToolMappings } = await import(join(serviceDir, "repo-config.js"));
|
|
603
|
+
|
|
604
|
+
// Fetch items with mappings applied
|
|
605
|
+
const items = await pollGenericSource(source, { mappings });
|
|
606
|
+
|
|
607
|
+
console.log(`Found ${items.length} item(s)`);
|
|
608
|
+
console.log("");
|
|
609
|
+
|
|
610
|
+
// Show first few items
|
|
611
|
+
const maxItems = 3;
|
|
612
|
+
for (let i = 0; i < Math.min(items.length, maxItems); i++) {
|
|
613
|
+
const item = items[i];
|
|
614
|
+
console.log(`Item ${i + 1}:`);
|
|
615
|
+
console.log(" Raw fields:");
|
|
616
|
+
for (const [key, value] of Object.entries(item)) {
|
|
617
|
+
const displayValue = typeof value === "object"
|
|
618
|
+
? JSON.stringify(value)
|
|
619
|
+
: String(value);
|
|
620
|
+
const truncated = displayValue.length > 60
|
|
621
|
+
? displayValue.substring(0, 57) + "..."
|
|
622
|
+
: displayValue;
|
|
623
|
+
console.log(` ${key}: ${truncated}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Apply mappings
|
|
627
|
+
if (mappings) {
|
|
628
|
+
const mapped = applyMappings(item, mappings);
|
|
629
|
+
console.log(" After mappings:");
|
|
630
|
+
// Show mapped fields that are different
|
|
631
|
+
for (const [target] of Object.entries(mappings)) {
|
|
632
|
+
if (mapped[target] !== undefined) {
|
|
633
|
+
const displayValue = typeof mapped[target] === "object"
|
|
634
|
+
? JSON.stringify(mapped[target])
|
|
635
|
+
: String(mapped[target]);
|
|
636
|
+
const truncated = displayValue.length > 60
|
|
637
|
+
? displayValue.substring(0, 57) + "..."
|
|
638
|
+
: displayValue;
|
|
639
|
+
console.log(` ${target}: ${truncated}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
console.log("");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (items.length > maxItems) {
|
|
647
|
+
console.log(`... and ${items.length - maxItems} more item(s)`);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
} catch (err) {
|
|
651
|
+
console.error(`Error fetching items: ${err.message}`);
|
|
652
|
+
if (err.stack) {
|
|
653
|
+
console.error("");
|
|
654
|
+
console.error("Stack trace:");
|
|
655
|
+
console.error(err.stack);
|
|
656
|
+
}
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ============================================================================
|
|
662
|
+
// Test Mapping Command
|
|
663
|
+
// ============================================================================
|
|
664
|
+
|
|
665
|
+
async function testMappingCommand(mcpName) {
|
|
666
|
+
if (!mcpName) {
|
|
667
|
+
console.error("Usage: echo '<json>' | opencode-pilot test-mapping <mcp-name>");
|
|
668
|
+
console.error("");
|
|
669
|
+
console.error("Test field mappings by piping JSON and specifying the MCP server name.");
|
|
670
|
+
console.error("");
|
|
671
|
+
console.error("Example:");
|
|
672
|
+
console.error(' echo \'{"url":"https://linear.app/team/issue/PROJ-123/title","title":"Fix bug"}\' | opencode-pilot test-mapping linear');
|
|
673
|
+
console.error("");
|
|
674
|
+
|
|
675
|
+
// Show available tools with mappings
|
|
676
|
+
const { loadRepoConfig, getToolMappings } = await import(join(serviceDir, "repo-config.js"));
|
|
677
|
+
loadRepoConfig(PILOT_CONFIG_FILE);
|
|
678
|
+
|
|
679
|
+
console.error("Configured MCP servers with mappings:");
|
|
680
|
+
for (const name of ["github", "linear"]) {
|
|
681
|
+
const mappings = getToolMappings(name);
|
|
682
|
+
if (mappings && Object.keys(mappings).length > 0) {
|
|
683
|
+
console.error(` ${name}:`);
|
|
684
|
+
for (const [target, source] of Object.entries(mappings)) {
|
|
685
|
+
console.error(` ${target} ← ${source}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Read JSON from stdin
|
|
693
|
+
const chunks = [];
|
|
694
|
+
for await (const chunk of process.stdin) {
|
|
695
|
+
chunks.push(chunk);
|
|
696
|
+
}
|
|
697
|
+
const input = Buffer.concat(chunks).toString().trim();
|
|
698
|
+
|
|
699
|
+
if (!input) {
|
|
700
|
+
console.error("Error: No JSON input provided. Pipe JSON to stdin.");
|
|
701
|
+
console.error("");
|
|
702
|
+
console.error("Example:");
|
|
703
|
+
console.error(' echo \'{"url":"https://linear.app/team/issue/PROJ-123/title"}\' | opencode-pilot test-mapping linear');
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
let item;
|
|
708
|
+
try {
|
|
709
|
+
item = JSON.parse(input);
|
|
710
|
+
} catch (err) {
|
|
711
|
+
console.error(`Error parsing JSON: ${err.message}`);
|
|
712
|
+
process.exit(1);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Load mappings
|
|
716
|
+
const { loadRepoConfig, getToolMappings } = await import(join(serviceDir, "repo-config.js"));
|
|
717
|
+
const { applyMappings } = await import(join(serviceDir, "poller.js"));
|
|
718
|
+
|
|
719
|
+
loadRepoConfig(PILOT_CONFIG_FILE);
|
|
720
|
+
const mappings = getToolMappings(mcpName);
|
|
721
|
+
|
|
722
|
+
if (!mappings || Object.keys(mappings).length === 0) {
|
|
723
|
+
console.error(`No mappings configured for MCP server: ${mcpName}`);
|
|
724
|
+
console.error("");
|
|
725
|
+
console.error("Add mappings to your config.yaml:");
|
|
726
|
+
console.error(" tools:");
|
|
727
|
+
console.error(` ${mcpName}:`);
|
|
728
|
+
console.error(" mappings:");
|
|
729
|
+
console.error(' number: "url:/([A-Z0-9]+-[0-9]+)/"');
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
console.log("Input:");
|
|
734
|
+
for (const [key, value] of Object.entries(item)) {
|
|
735
|
+
const displayValue = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
736
|
+
const truncated = displayValue.length > 60 ? displayValue.substring(0, 57) + "..." : displayValue;
|
|
737
|
+
console.log(` ${key}: ${truncated}`);
|
|
738
|
+
}
|
|
739
|
+
console.log("");
|
|
740
|
+
|
|
741
|
+
console.log(`Mappings (${mcpName}):`);
|
|
742
|
+
for (const [target, source] of Object.entries(mappings)) {
|
|
743
|
+
console.log(` ${target} ← ${source}`);
|
|
744
|
+
}
|
|
745
|
+
console.log("");
|
|
746
|
+
|
|
747
|
+
const mapped = applyMappings(item, mappings);
|
|
748
|
+
|
|
749
|
+
console.log("Result:");
|
|
750
|
+
for (const [target] of Object.entries(mappings)) {
|
|
751
|
+
const value = mapped[target];
|
|
752
|
+
if (value !== undefined) {
|
|
753
|
+
console.log(` ${target}: ${value}`);
|
|
754
|
+
} else {
|
|
755
|
+
console.log(` ${target}: (undefined - no match)`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ============================================================================
|
|
761
|
+
// Main
|
|
762
|
+
// ============================================================================
|
|
763
|
+
|
|
764
|
+
async function main() {
|
|
765
|
+
const args = process.argv.slice(2);
|
|
766
|
+
const { command, subcommand, flags } = parseArgs(args);
|
|
767
|
+
|
|
768
|
+
if (!command || command === "help") {
|
|
769
|
+
showHelp();
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
switch (command) {
|
|
774
|
+
case "start":
|
|
775
|
+
await startCommand();
|
|
776
|
+
break;
|
|
777
|
+
|
|
778
|
+
case "setup":
|
|
779
|
+
await setupCommand();
|
|
780
|
+
break;
|
|
781
|
+
|
|
782
|
+
case "status":
|
|
783
|
+
statusCommand();
|
|
784
|
+
break;
|
|
785
|
+
|
|
786
|
+
case "config":
|
|
787
|
+
await configCommand();
|
|
788
|
+
break;
|
|
789
|
+
|
|
790
|
+
case "test-source":
|
|
791
|
+
await testSourceCommand(subcommand);
|
|
792
|
+
break;
|
|
793
|
+
|
|
794
|
+
case "test-mapping":
|
|
795
|
+
await testMappingCommand(subcommand);
|
|
796
|
+
break;
|
|
797
|
+
|
|
798
|
+
default:
|
|
799
|
+
console.error(`Unknown command: ${command}`);
|
|
800
|
+
console.error("");
|
|
801
|
+
showHelp();
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
main().catch((err) => {
|
|
807
|
+
console.error("Error:", err.message);
|
|
808
|
+
process.exit(1);
|
|
809
|
+
});
|