opencastle 0.27.0 → 0.27.2
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/bin/cli.mjs +6 -0
- package/dist/cli/agents.d.ts +3 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +161 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/baselines.d.ts +3 -0
- package/dist/cli/baselines.d.ts.map +1 -0
- package/dist/cli/baselines.js +128 -0
- package/dist/cli/baselines.js.map +1 -0
- package/dist/cli/convoy/dashboard-types.d.ts +146 -0
- package/dist/cli/convoy/dashboard-types.d.ts.map +1 -0
- package/dist/cli/convoy/dashboard-types.js +2 -0
- package/dist/cli/convoy/dashboard-types.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts +67 -2
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +2036 -28
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +1659 -70
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/event-schemas.d.ts +9 -0
- package/dist/cli/convoy/event-schemas.d.ts.map +1 -0
- package/dist/cli/convoy/event-schemas.js +185 -0
- package/dist/cli/convoy/event-schemas.js.map +1 -0
- package/dist/cli/convoy/events.d.ts +12 -1
- package/dist/cli/convoy/events.d.ts.map +1 -1
- package/dist/cli/convoy/events.js +186 -13
- package/dist/cli/convoy/events.js.map +1 -1
- package/dist/cli/convoy/events.test.js +325 -28
- package/dist/cli/convoy/events.test.js.map +1 -1
- package/dist/cli/convoy/expertise.d.ts +16 -0
- package/dist/cli/convoy/expertise.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.js +121 -0
- package/dist/cli/convoy/expertise.js.map +1 -0
- package/dist/cli/convoy/expertise.test.d.ts +2 -0
- package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.test.js +96 -0
- package/dist/cli/convoy/expertise.test.js.map +1 -0
- package/dist/cli/convoy/export.test.js +1 -0
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/formula.d.ts +19 -0
- package/dist/cli/convoy/formula.d.ts.map +1 -0
- package/dist/cli/convoy/formula.js +142 -0
- package/dist/cli/convoy/formula.js.map +1 -0
- package/dist/cli/convoy/formula.test.d.ts +2 -0
- package/dist/cli/convoy/formula.test.d.ts.map +1 -0
- package/dist/cli/convoy/formula.test.js +342 -0
- package/dist/cli/convoy/formula.test.js.map +1 -0
- package/dist/cli/convoy/gates.d.ts +128 -0
- package/dist/cli/convoy/gates.d.ts.map +1 -0
- package/dist/cli/convoy/gates.js +606 -0
- package/dist/cli/convoy/gates.js.map +1 -0
- package/dist/cli/convoy/gates.test.d.ts +2 -0
- package/dist/cli/convoy/gates.test.d.ts.map +1 -0
- package/dist/cli/convoy/gates.test.js +976 -0
- package/dist/cli/convoy/gates.test.js.map +1 -0
- package/dist/cli/convoy/health.d.ts +11 -0
- package/dist/cli/convoy/health.d.ts.map +1 -1
- package/dist/cli/convoy/health.js +54 -0
- package/dist/cli/convoy/health.js.map +1 -1
- package/dist/cli/convoy/health.test.js +56 -1
- package/dist/cli/convoy/health.test.js.map +1 -1
- package/dist/cli/convoy/issues.d.ts +8 -0
- package/dist/cli/convoy/issues.d.ts.map +1 -0
- package/dist/cli/convoy/issues.js +98 -0
- package/dist/cli/convoy/issues.js.map +1 -0
- package/dist/cli/convoy/issues.test.d.ts +2 -0
- package/dist/cli/convoy/issues.test.d.ts.map +1 -0
- package/dist/cli/convoy/issues.test.js +107 -0
- package/dist/cli/convoy/issues.test.js.map +1 -0
- package/dist/cli/convoy/knowledge.d.ts +5 -0
- package/dist/cli/convoy/knowledge.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.js +116 -0
- package/dist/cli/convoy/knowledge.js.map +1 -0
- package/dist/cli/convoy/knowledge.test.d.ts +2 -0
- package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.test.js +87 -0
- package/dist/cli/convoy/knowledge.test.js.map +1 -0
- package/dist/cli/convoy/lessons.d.ts +17 -0
- package/dist/cli/convoy/lessons.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.js +149 -0
- package/dist/cli/convoy/lessons.js.map +1 -0
- package/dist/cli/convoy/lessons.test.d.ts +2 -0
- package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.test.js +135 -0
- package/dist/cli/convoy/lessons.test.js.map +1 -0
- package/dist/cli/convoy/lock.d.ts +13 -0
- package/dist/cli/convoy/lock.d.ts.map +1 -0
- package/dist/cli/convoy/lock.js +88 -0
- package/dist/cli/convoy/lock.js.map +1 -0
- package/dist/cli/convoy/lock.test.d.ts +2 -0
- package/dist/cli/convoy/lock.test.d.ts.map +1 -0
- package/dist/cli/convoy/lock.test.js +136 -0
- package/dist/cli/convoy/lock.test.js.map +1 -0
- package/dist/cli/convoy/log-merge.test.d.ts +2 -0
- package/dist/cli/convoy/log-merge.test.d.ts.map +1 -0
- package/dist/cli/convoy/log-merge.test.js +147 -0
- package/dist/cli/convoy/log-merge.test.js.map +1 -0
- package/dist/cli/convoy/merge.d.ts +4 -0
- package/dist/cli/convoy/merge.d.ts.map +1 -1
- package/dist/cli/convoy/merge.js +18 -1
- package/dist/cli/convoy/merge.js.map +1 -1
- package/dist/cli/convoy/merge.test.js +6 -7
- package/dist/cli/convoy/merge.test.js.map +1 -1
- package/dist/cli/convoy/partition.d.ts +51 -0
- package/dist/cli/convoy/partition.d.ts.map +1 -0
- package/dist/cli/convoy/partition.js +186 -0
- package/dist/cli/convoy/partition.js.map +1 -0
- package/dist/cli/convoy/partition.test.d.ts +2 -0
- package/dist/cli/convoy/partition.test.d.ts.map +1 -0
- package/dist/cli/convoy/partition.test.js +315 -0
- package/dist/cli/convoy/partition.test.js.map +1 -0
- package/dist/cli/convoy/pipeline.test.js +6 -0
- package/dist/cli/convoy/pipeline.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +99 -7
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +764 -31
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +1810 -18
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +427 -5
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/convoy/types.js +42 -1
- package/dist/cli/convoy/types.js.map +1 -1
- package/dist/cli/log.d.ts +11 -0
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +114 -2
- package/dist/cli/log.js.map +1 -1
- package/dist/cli/run/adapters/claude.d.ts +2 -0
- package/dist/cli/run/adapters/claude.d.ts.map +1 -1
- package/dist/cli/run/adapters/claude.js +89 -49
- package/dist/cli/run/adapters/claude.js.map +1 -1
- package/dist/cli/run/adapters/claude.test.d.ts +2 -0
- package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/claude.test.js +205 -0
- package/dist/cli/run/adapters/claude.test.js.map +1 -0
- package/dist/cli/run/adapters/copilot.d.ts +1 -0
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +84 -46
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
- package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/copilot.test.js +195 -0
- package/dist/cli/run/adapters/copilot.test.js.map +1 -0
- package/dist/cli/run/adapters/cursor.d.ts +1 -0
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/run/adapters/cursor.js +83 -47
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
- package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/cursor.test.js +129 -0
- package/dist/cli/run/adapters/cursor.test.js.map +1 -0
- package/dist/cli/run/adapters/opencode.d.ts +1 -0
- package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/run/adapters/opencode.js +81 -47
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
- package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/opencode.test.js +119 -0
- package/dist/cli/run/adapters/opencode.test.js.map +1 -0
- package/dist/cli/run/executor.js +1 -1
- package/dist/cli/run/executor.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +245 -4
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +669 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +362 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +85 -2
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js.map +1 -1
- package/dist/cli/watch.d.ts +15 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +279 -0
- package/dist/cli/watch.js.map +1 -0
- package/package.json +5 -1
- package/src/cli/agents.ts +177 -0
- package/src/cli/baselines.ts +143 -0
- package/src/cli/convoy/TELEMETRY.md +203 -0
- package/src/cli/convoy/dashboard-types.ts +141 -0
- package/src/cli/convoy/engine.test.ts +1937 -70
- package/src/cli/convoy/engine.ts +2350 -40
- package/src/cli/convoy/event-schemas.ts +195 -0
- package/src/cli/convoy/events.test.ts +384 -39
- package/src/cli/convoy/events.ts +202 -16
- package/src/cli/convoy/expertise.test.ts +128 -0
- package/src/cli/convoy/expertise.ts +163 -0
- package/src/cli/convoy/export.test.ts +1 -0
- package/src/cli/convoy/formula.test.ts +405 -0
- package/src/cli/convoy/formula.ts +174 -0
- package/src/cli/convoy/gates.test.ts +1169 -0
- package/src/cli/convoy/gates.ts +774 -0
- package/src/cli/convoy/health.test.ts +64 -2
- package/src/cli/convoy/health.ts +80 -2
- package/src/cli/convoy/issues.test.ts +143 -0
- package/src/cli/convoy/issues.ts +136 -0
- package/src/cli/convoy/knowledge.test.ts +101 -0
- package/src/cli/convoy/knowledge.ts +132 -0
- package/src/cli/convoy/lessons.test.ts +188 -0
- package/src/cli/convoy/lessons.ts +164 -0
- package/src/cli/convoy/lock.test.ts +181 -0
- package/src/cli/convoy/lock.ts +103 -0
- package/src/cli/convoy/log-merge.test.ts +179 -0
- package/src/cli/convoy/merge.test.ts +6 -7
- package/src/cli/convoy/merge.ts +19 -1
- package/src/cli/convoy/partition.test.ts +423 -0
- package/src/cli/convoy/partition.ts +232 -0
- package/src/cli/convoy/pipeline.test.ts +6 -0
- package/src/cli/convoy/store.test.ts +2041 -20
- package/src/cli/convoy/store.ts +945 -46
- package/src/cli/convoy/types.ts +278 -4
- package/src/cli/log.ts +120 -2
- package/src/cli/run/adapters/claude.test.ts +234 -0
- package/src/cli/run/adapters/claude.ts +45 -5
- package/src/cli/run/adapters/copilot.test.ts +224 -0
- package/src/cli/run/adapters/copilot.ts +34 -4
- package/src/cli/run/adapters/cursor.test.ts +144 -0
- package/src/cli/run/adapters/cursor.ts +33 -2
- package/src/cli/run/adapters/opencode.test.ts +135 -0
- package/src/cli/run/adapters/opencode.ts +30 -2
- package/src/cli/run/executor.ts +1 -1
- package/src/cli/run/schema.test.ts +758 -0
- package/src/cli/run/schema.ts +300 -25
- package/src/cli/run.ts +341 -21
- package/src/cli/types.ts +86 -1
- package/src/cli/watch.ts +298 -0
- package/src/dashboard/dist/_astro/{index.DtnyD8a5.css → index.6L3_HsPT.css} +1 -1
- package/src/dashboard/dist/data/.gitkeep +0 -0
- package/src/dashboard/dist/data/convoy-list.json +1 -0
- package/src/dashboard/dist/data/overall-stats.json +24 -0
- package/src/dashboard/dist/index.html +701 -3
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/public/data/.gitkeep +0 -0
- package/src/dashboard/public/data/convoy-list.json +1 -0
- package/src/dashboard/public/data/overall-stats.json +24 -0
- package/src/dashboard/scripts/etl.test.ts +210 -0
- package/src/dashboard/scripts/etl.ts +108 -0
- package/src/dashboard/scripts/integration-test.ts +504 -0
- package/src/dashboard/src/pages/index.astro +854 -15
- package/src/dashboard/src/styles/dashboard.css +557 -1
- package/src/orchestrator/prompts/generate-convoy.prompt.md +212 -13
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { EventEmitter } from 'node:events'
|
|
6
|
+
import type { Task } from '../../types.js'
|
|
7
|
+
|
|
8
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeTask(): Task {
|
|
11
|
+
return {
|
|
12
|
+
id: 'test-task',
|
|
13
|
+
agent: 'developer',
|
|
14
|
+
prompt: 'Do something',
|
|
15
|
+
files: [],
|
|
16
|
+
timeout: '5m',
|
|
17
|
+
depends_on: [],
|
|
18
|
+
description: 'test task',
|
|
19
|
+
max_retries: 0,
|
|
20
|
+
} as unknown as Task
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeMockProc(exitCode = 0, stdoutData = '{"result":"ok"}') {
|
|
24
|
+
const proc = new EventEmitter() as EventEmitter & {
|
|
25
|
+
stdout: EventEmitter
|
|
26
|
+
stderr: EventEmitter
|
|
27
|
+
killed: boolean
|
|
28
|
+
kill: ReturnType<typeof vi.fn>
|
|
29
|
+
}
|
|
30
|
+
proc.stdout = new EventEmitter()
|
|
31
|
+
proc.stderr = new EventEmitter()
|
|
32
|
+
proc.killed = false
|
|
33
|
+
proc.kill = vi.fn()
|
|
34
|
+
process.nextTick(() => {
|
|
35
|
+
if (stdoutData) proc.stdout.emit('data', Buffer.from(stdoutData))
|
|
36
|
+
proc.emit('close', exitCode)
|
|
37
|
+
})
|
|
38
|
+
return proc
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── CLI mode ──────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
describe('cursor adapter — MCP support', () => {
|
|
44
|
+
let tmpDir: string
|
|
45
|
+
let mockSpawn: ReturnType<typeof vi.fn>
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.resetModules()
|
|
49
|
+
tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'cursor-test-')))
|
|
50
|
+
|
|
51
|
+
mockSpawn = vi.fn().mockImplementation((cmd: string) => {
|
|
52
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
53
|
+
return makeMockProc(0, '{"result":"ok"}')
|
|
54
|
+
})
|
|
55
|
+
vi.doMock('node:child_process', () => ({ spawn: mockSpawn }))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
60
|
+
vi.restoreAllMocks()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('writes mcp.json to cwd with correct format when mcpServers provided', async () => {
|
|
64
|
+
let capturedContent: string | null = null
|
|
65
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
66
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
67
|
+
const mcpPath = join(tmpDir, 'mcp.json')
|
|
68
|
+
if (existsSync(mcpPath)) {
|
|
69
|
+
capturedContent = readFileSync(mcpPath, 'utf8')
|
|
70
|
+
}
|
|
71
|
+
return makeMockProc(0, '{}')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const { execute } = await import('./cursor.js')
|
|
75
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
76
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
77
|
+
|
|
78
|
+
expect(capturedContent).not.toBeNull()
|
|
79
|
+
expect(JSON.parse(capturedContent!)).toEqual({
|
|
80
|
+
mcpServers: { 'my-mcp': { command: 'node', args: ['server.js'] } },
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('passes --approve-mcps when mcp_approve_all is true', async () => {
|
|
85
|
+
const capturedArgs: string[] = []
|
|
86
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
87
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
88
|
+
capturedArgs.push(...args)
|
|
89
|
+
return makeMockProc(0, '{}')
|
|
90
|
+
})
|
|
91
|
+
const { execute } = await import('./cursor.js')
|
|
92
|
+
await execute(makeTask(), { mcp_approve_all: true, cwd: tmpDir })
|
|
93
|
+
expect(capturedArgs).toContain('--approve-mcps')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('cleans up mcp.json after successful execution', async () => {
|
|
97
|
+
const { execute } = await import('./cursor.js')
|
|
98
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
99
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
100
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('cleans up mcp.json after failed execution (non-zero exit)', async () => {
|
|
104
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
105
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
106
|
+
return makeMockProc(1, '')
|
|
107
|
+
})
|
|
108
|
+
const { execute } = await import('./cursor.js')
|
|
109
|
+
const mcpServers = [{ name: 'err-mcp', type: 'local', command: 'node', args: [] }]
|
|
110
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
111
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('does NOT write mcp.json when mcpServers not configured', async () => {
|
|
115
|
+
const { execute } = await import('./cursor.js')
|
|
116
|
+
await execute(makeTask(), { cwd: tmpDir })
|
|
117
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('does NOT add --approve-mcps when mcp_approve_all is not set', async () => {
|
|
121
|
+
const capturedArgs: string[] = []
|
|
122
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
123
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
124
|
+
capturedArgs.push(...args)
|
|
125
|
+
return makeMockProc(0, '{}')
|
|
126
|
+
})
|
|
127
|
+
const { execute } = await import('./cursor.js')
|
|
128
|
+
await execute(makeTask(), { cwd: tmpDir })
|
|
129
|
+
expect(capturedArgs).not.toContain('--approve-mcps')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('includes --approve-mcps when mcp_approve_all is true (no mcpServers)', async () => {
|
|
133
|
+
const capturedArgs: string[] = []
|
|
134
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
135
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
136
|
+
capturedArgs.push(...args)
|
|
137
|
+
return makeMockProc(0, '{}')
|
|
138
|
+
})
|
|
139
|
+
const { execute } = await import('./cursor.js')
|
|
140
|
+
await execute(makeTask(), { mcp_approve_all: true, cwd: tmpDir })
|
|
141
|
+
expect(capturedArgs).toContain('--approve-mcps')
|
|
142
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
+
import { writeFileSync, unlinkSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
2
4
|
import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
|
|
3
5
|
|
|
4
6
|
/** Adapter name */
|
|
5
7
|
export const name = 'cursor'
|
|
6
8
|
|
|
9
|
+
export function supportsSessionContinuity(): boolean { return false }
|
|
7
10
|
/**
|
|
8
11
|
* Check if the Cursor CLI (`agent`) is available on the system PATH.
|
|
9
12
|
*/
|
|
@@ -33,11 +36,34 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
33
36
|
'json',
|
|
34
37
|
]
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
const cwd = options?.cwd ?? process.cwd()
|
|
40
|
+
const mcpJsonPath = join(cwd, 'mcp.json')
|
|
41
|
+
let wroteJson = false
|
|
42
|
+
|
|
43
|
+
if (options.mcpServers?.length) {
|
|
44
|
+
const mcpJson: Record<string, Record<string, unknown>> = {}
|
|
45
|
+
for (const server of options.mcpServers) {
|
|
46
|
+
const entry: Record<string, unknown> = {}
|
|
47
|
+
if (server.command) entry.command = server.command
|
|
48
|
+
if (server.args) entry.args = server.args
|
|
49
|
+
if (server.url) entry.url = server.url
|
|
50
|
+
if (server.config) Object.assign(entry, server.config)
|
|
51
|
+
mcpJson[server.name] = entry
|
|
52
|
+
}
|
|
53
|
+
writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: mcpJson }, null, 2), 'utf8')
|
|
54
|
+
wroteJson = true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (options.mcp_approve_all) {
|
|
58
|
+
args.push('--approve-mcps')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return await new Promise<ExecuteResult>((resolve) => {
|
|
37
63
|
const proc = spawn('agent', args, {
|
|
38
64
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
65
|
env: { ...process.env },
|
|
40
|
-
cwd
|
|
66
|
+
cwd,
|
|
41
67
|
})
|
|
42
68
|
|
|
43
69
|
let stdout = ''
|
|
@@ -89,6 +115,11 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
89
115
|
// Store process ref for potential timeout kill
|
|
90
116
|
task._process = proc
|
|
91
117
|
})
|
|
118
|
+
} finally {
|
|
119
|
+
if (wroteJson) {
|
|
120
|
+
try { unlinkSync(mcpJsonPath) } catch { /* ignore */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
92
123
|
}
|
|
93
124
|
|
|
94
125
|
/**
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { EventEmitter } from 'node:events'
|
|
6
|
+
import type { Task } from '../../types.js'
|
|
7
|
+
|
|
8
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeTask(): Task {
|
|
11
|
+
return {
|
|
12
|
+
id: 'test-task',
|
|
13
|
+
agent: 'developer',
|
|
14
|
+
prompt: 'Do something',
|
|
15
|
+
files: [],
|
|
16
|
+
timeout: '5m',
|
|
17
|
+
depends_on: [],
|
|
18
|
+
description: 'test task',
|
|
19
|
+
max_retries: 0,
|
|
20
|
+
} as unknown as Task
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeMockProc(exitCode = 0, stdoutData = '{"result":"ok"}') {
|
|
24
|
+
const proc = new EventEmitter() as EventEmitter & {
|
|
25
|
+
stdout: EventEmitter
|
|
26
|
+
stderr: EventEmitter
|
|
27
|
+
killed: boolean
|
|
28
|
+
kill: ReturnType<typeof vi.fn>
|
|
29
|
+
}
|
|
30
|
+
proc.stdout = new EventEmitter()
|
|
31
|
+
proc.stderr = new EventEmitter()
|
|
32
|
+
proc.killed = false
|
|
33
|
+
proc.kill = vi.fn()
|
|
34
|
+
process.nextTick(() => {
|
|
35
|
+
if (stdoutData) proc.stdout.emit('data', Buffer.from(stdoutData))
|
|
36
|
+
proc.emit('close', exitCode)
|
|
37
|
+
})
|
|
38
|
+
return proc
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── CLI mode ──────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
describe('opencode adapter — MCP support', () => {
|
|
44
|
+
let tmpDir: string
|
|
45
|
+
let mockSpawn: ReturnType<typeof vi.fn>
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.resetModules()
|
|
49
|
+
tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'opencode-test-')))
|
|
50
|
+
|
|
51
|
+
mockSpawn = vi.fn().mockImplementation((cmd: string) => {
|
|
52
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
53
|
+
return makeMockProc(0, '{"result":"ok"}')
|
|
54
|
+
})
|
|
55
|
+
vi.doMock('node:child_process', () => ({ spawn: mockSpawn }))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
60
|
+
vi.restoreAllMocks()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('writes mcp.json to cwd with correct format when mcpServers provided', async () => {
|
|
64
|
+
let capturedContent: string | null = null
|
|
65
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
66
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
67
|
+
const mcpPath = join(tmpDir, 'mcp.json')
|
|
68
|
+
if (existsSync(mcpPath)) {
|
|
69
|
+
capturedContent = readFileSync(mcpPath, 'utf8')
|
|
70
|
+
}
|
|
71
|
+
return makeMockProc(0, '{}')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const { execute } = await import('./opencode.js')
|
|
75
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
76
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
77
|
+
|
|
78
|
+
expect(capturedContent).not.toBeNull()
|
|
79
|
+
expect(JSON.parse(capturedContent!)).toEqual({
|
|
80
|
+
mcpServers: { 'my-mcp': { command: 'node', args: ['server.js'] } },
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('passes --mcp-config flag pointing to mcp.json path', async () => {
|
|
85
|
+
const capturedArgs: string[] = []
|
|
86
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
87
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
88
|
+
capturedArgs.push(...args)
|
|
89
|
+
return makeMockProc(0, '{}')
|
|
90
|
+
})
|
|
91
|
+
const { execute } = await import('./opencode.js')
|
|
92
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
93
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
94
|
+
|
|
95
|
+
const idx = capturedArgs.indexOf('--mcp-config')
|
|
96
|
+
expect(idx).toBeGreaterThanOrEqual(0)
|
|
97
|
+
expect(capturedArgs[idx + 1]).toBe(join(tmpDir, 'mcp.json'))
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('cleans up mcp.json after successful execution', async () => {
|
|
101
|
+
const { execute } = await import('./opencode.js')
|
|
102
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
103
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
104
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('cleans up mcp.json after failed execution (non-zero exit)', async () => {
|
|
108
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
109
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
110
|
+
return makeMockProc(1, '')
|
|
111
|
+
})
|
|
112
|
+
const { execute } = await import('./opencode.js')
|
|
113
|
+
const mcpServers = [{ name: 'err-mcp', type: 'local', command: 'node', args: [] }]
|
|
114
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
115
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('does NOT write mcp.json when mcpServers not configured', async () => {
|
|
119
|
+
const { execute } = await import('./opencode.js')
|
|
120
|
+
await execute(makeTask(), { cwd: tmpDir })
|
|
121
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('does NOT add --mcp-config when mcpServers not provided', async () => {
|
|
125
|
+
const capturedArgs: string[] = []
|
|
126
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
127
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
128
|
+
capturedArgs.push(...args)
|
|
129
|
+
return makeMockProc(0, '{}')
|
|
130
|
+
})
|
|
131
|
+
const { execute } = await import('./opencode.js')
|
|
132
|
+
await execute(makeTask(), { cwd: tmpDir })
|
|
133
|
+
expect(capturedArgs).not.toContain('--mcp-config')
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
+
import { writeFileSync, unlinkSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
2
4
|
import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
|
|
3
5
|
|
|
4
6
|
/** Adapter name */
|
|
5
7
|
export const name = 'opencode'
|
|
6
8
|
|
|
9
|
+
export function supportsSessionContinuity(): boolean { return false }
|
|
7
10
|
/**
|
|
8
11
|
* Check if the `opencode` CLI is available on the system PATH.
|
|
9
12
|
*/
|
|
@@ -27,11 +30,31 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
27
30
|
|
|
28
31
|
const args = ['--headless', '-p', prompt]
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
const cwd = options?.cwd ?? process.cwd()
|
|
34
|
+
const mcpJsonPath = join(cwd, 'mcp.json')
|
|
35
|
+
let wroteJson = false
|
|
36
|
+
|
|
37
|
+
if (options.mcpServers?.length) {
|
|
38
|
+
const mcpJson: Record<string, Record<string, unknown>> = {}
|
|
39
|
+
for (const server of options.mcpServers) {
|
|
40
|
+
const entry: Record<string, unknown> = {}
|
|
41
|
+
if (server.command) entry.command = server.command
|
|
42
|
+
if (server.args) entry.args = server.args
|
|
43
|
+
if (server.url) entry.url = server.url
|
|
44
|
+
if (server.config) Object.assign(entry, server.config)
|
|
45
|
+
mcpJson[server.name] = entry
|
|
46
|
+
}
|
|
47
|
+
writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: mcpJson }, null, 2), 'utf8')
|
|
48
|
+
args.push('--mcp-config', mcpJsonPath)
|
|
49
|
+
wroteJson = true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
return await new Promise<ExecuteResult>((resolve) => {
|
|
31
54
|
const proc = spawn('opencode', args, {
|
|
32
55
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
33
56
|
env: { ...process.env },
|
|
34
|
-
cwd
|
|
57
|
+
cwd,
|
|
35
58
|
})
|
|
36
59
|
|
|
37
60
|
let stdout = ''
|
|
@@ -83,6 +106,11 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
83
106
|
// Store process ref for potential timeout kill
|
|
84
107
|
task._process = proc
|
|
85
108
|
})
|
|
109
|
+
} finally {
|
|
110
|
+
if (wroteJson) {
|
|
111
|
+
try { unlinkSync(mcpJsonPath) } catch { /* ignore */ }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
86
114
|
}
|
|
87
115
|
|
|
88
116
|
/**
|
package/src/cli/run/executor.ts
CHANGED
|
@@ -190,7 +190,7 @@ export function createExecutor(
|
|
|
190
190
|
reporter.onPhaseStart(phaseIdx + 1, eligible)
|
|
191
191
|
|
|
192
192
|
// Process eligible tasks in batches limited by concurrency
|
|
193
|
-
const concurrency = spec.concurrency
|
|
193
|
+
const concurrency = spec.concurrency === 'auto' ? eligible.length : spec.concurrency
|
|
194
194
|
for (let i = 0; i < eligible.length; i += concurrency) {
|
|
195
195
|
if (halted) break
|
|
196
196
|
const batch = eligible.slice(i, i + concurrency)
|