ac-framework 1.9.7 → 1.9.9
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/README.md +37 -11
- package/package.json +1 -1
- package/src/agents/collab-summary.js +120 -0
- package/src/agents/config-store.js +3 -0
- package/src/agents/constants.js +3 -1
- package/src/agents/opencode-client.js +101 -5
- package/src/agents/orchestrator.js +225 -6
- package/src/agents/role-prompts.js +34 -1
- package/src/agents/run-state.js +113 -0
- package/src/agents/runtime.js +75 -3
- package/src/agents/state-store.js +73 -0
- package/src/commands/agents.js +616 -60
- package/src/commands/init.js +9 -5
- package/src/mcp/collab-server.js +378 -24
- package/src/mcp/test-harness.mjs +410 -0
- package/src/services/dependency-installer.js +72 -4
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Test Harness - Minimal server for async demo and testing
|
|
5
|
+
*
|
|
6
|
+
* SDK: @modelcontextprotocol/sdk@^1.27.1
|
|
7
|
+
* Transport: StdioServerTransport (stdio only)
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Real SDK behavior (not mocks)
|
|
11
|
+
* - In-memory storage for memory_* tools
|
|
12
|
+
* - Mocked collab_* tool responses
|
|
13
|
+
* - Intentional error injection for Demo 3
|
|
14
|
+
* - Lifecycle support for Demo 5
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
18
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
|
|
21
|
+
const MEMORY_DB = new Map();
|
|
22
|
+
const COLLAB_SESSIONS = new Map();
|
|
23
|
+
|
|
24
|
+
let requestCount = 0;
|
|
25
|
+
let errorInjectionMode = null;
|
|
26
|
+
|
|
27
|
+
function setErrorInjection(mode) {
|
|
28
|
+
errorInjectionMode = mode;
|
|
29
|
+
console.error(`[harness] Error injection mode: ${mode || 'none'}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleError(errorType, defaultMessage) {
|
|
33
|
+
if (!errorInjectionMode || errorInjectionMode !== errorType) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: JSON.stringify({ error: defaultMessage }) }],
|
|
38
|
+
isError: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createSuccessResponse(data) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: JSON.stringify(data) }]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createErrorResponse(message) {
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
|
|
51
|
+
isError: true
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class MCPTestHarness {
|
|
56
|
+
constructor() {
|
|
57
|
+
this.server = new McpServer({
|
|
58
|
+
name: 'acfm-test-harness',
|
|
59
|
+
version: '1.0.0',
|
|
60
|
+
});
|
|
61
|
+
this.setupTools();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setupTools() {
|
|
65
|
+
// ── memory_save ─────────────────────────────────────────────────────────
|
|
66
|
+
this.server.tool(
|
|
67
|
+
'memory_save',
|
|
68
|
+
'Save a memory observation (test harness)',
|
|
69
|
+
{
|
|
70
|
+
content: z.string().describe('Content to save'),
|
|
71
|
+
type: z.enum([
|
|
72
|
+
'architectural_decision', 'bugfix_pattern', 'api_pattern',
|
|
73
|
+
'performance_insight', 'security_fix', 'refactor_technique',
|
|
74
|
+
'dependency_note', 'workaround', 'convention', 'context_boundary',
|
|
75
|
+
'general_insight', 'session_summary'
|
|
76
|
+
]).default('general_insight'),
|
|
77
|
+
importance: z.enum(['critical', 'high', 'medium', 'low']).default('medium'),
|
|
78
|
+
tags: z.array(z.string()).optional(),
|
|
79
|
+
projectPath: z.string().optional(),
|
|
80
|
+
changeName: z.string().optional(),
|
|
81
|
+
confidence: z.number().min(0).max(1).default(0.8)
|
|
82
|
+
},
|
|
83
|
+
async ({ content, type, importance, tags, projectPath, changeName, confidence }) => {
|
|
84
|
+
requestCount++;
|
|
85
|
+
const id = Date.now();
|
|
86
|
+
|
|
87
|
+
const validationError = handleError('validation', 'Missing required param: content');
|
|
88
|
+
if (validationError) return validationError;
|
|
89
|
+
|
|
90
|
+
const dbError = handleError('db_failure', 'Database write failed: disk full');
|
|
91
|
+
if (dbError) return dbError;
|
|
92
|
+
|
|
93
|
+
const memory = {
|
|
94
|
+
id,
|
|
95
|
+
content,
|
|
96
|
+
type,
|
|
97
|
+
importance,
|
|
98
|
+
tags: tags || [],
|
|
99
|
+
projectPath,
|
|
100
|
+
changeName,
|
|
101
|
+
confidence,
|
|
102
|
+
createdAt: new Date().toISOString()
|
|
103
|
+
};
|
|
104
|
+
MEMORY_DB.set(id.toString(), memory);
|
|
105
|
+
|
|
106
|
+
return createSuccessResponse({ success: true, id, operation: 'insert', revisionCount: 1 });
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// ── memory_search ───────────────────────────────────────────────────────
|
|
111
|
+
this.server.tool(
|
|
112
|
+
'memory_search',
|
|
113
|
+
'Search memories using text query (test harness)',
|
|
114
|
+
{
|
|
115
|
+
query: z.string().describe('Search query'),
|
|
116
|
+
limit: z.number().int().positive().default(10),
|
|
117
|
+
type: z.enum([
|
|
118
|
+
'architectural_decision', 'bugfix_pattern', 'api_pattern',
|
|
119
|
+
'performance_insight', 'security_fix', 'refactor_technique',
|
|
120
|
+
'dependency_note', 'workaround', 'convention', 'context_boundary',
|
|
121
|
+
'general_insight', 'session_summary'
|
|
122
|
+
]).optional(),
|
|
123
|
+
importance: z.enum(['critical', 'high', 'medium', 'low']).optional(),
|
|
124
|
+
projectPath: z.string().optional(),
|
|
125
|
+
minConfidence: z.number().min(0).max(1).default(0)
|
|
126
|
+
},
|
|
127
|
+
async ({ query, limit, type, importance, projectPath, minConfidence }) => {
|
|
128
|
+
requestCount++;
|
|
129
|
+
|
|
130
|
+
const validationError = handleError('validation', 'Invalid query parameter');
|
|
131
|
+
if (validationError) return validationError;
|
|
132
|
+
|
|
133
|
+
const results = [];
|
|
134
|
+
const queryLower = query.toLowerCase();
|
|
135
|
+
for (const [id, memory] of MEMORY_DB) {
|
|
136
|
+
if (memory.content.toLowerCase().includes(queryLower)) {
|
|
137
|
+
if (type && memory.type !== type) continue;
|
|
138
|
+
if (importance && memory.importance !== importance) continue;
|
|
139
|
+
if (projectPath && memory.projectPath !== projectPath) continue;
|
|
140
|
+
if (memory.confidence < minConfidence) continue;
|
|
141
|
+
results.push(memory);
|
|
142
|
+
if (results.length >= limit) break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return createSuccessResponse({ query, count: results.length, results });
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// ── memory_recall ───────────────────────────────────────────────────────
|
|
151
|
+
this.server.tool(
|
|
152
|
+
'memory_recall',
|
|
153
|
+
'Recall relevant context for a task (test harness)',
|
|
154
|
+
{
|
|
155
|
+
task: z.string().optional(),
|
|
156
|
+
projectPath: z.string().optional(),
|
|
157
|
+
changeName: z.string().optional(),
|
|
158
|
+
limit: z.number().int().positive().default(5),
|
|
159
|
+
days: z.number().int().positive().default(30)
|
|
160
|
+
},
|
|
161
|
+
async ({ task, projectPath, changeName, limit, days }) => {
|
|
162
|
+
requestCount++;
|
|
163
|
+
|
|
164
|
+
const dbError = handleError('db_failure', 'Database read failed: connection lost');
|
|
165
|
+
if (dbError) return dbError;
|
|
166
|
+
|
|
167
|
+
const memories = Array.from(MEMORY_DB.values())
|
|
168
|
+
.filter(m => {
|
|
169
|
+
if (task && !m.content.toLowerCase().includes(task.toLowerCase())) return false;
|
|
170
|
+
if (projectPath && m.projectPath !== projectPath) return false;
|
|
171
|
+
if (changeName && m.changeName !== changeName) return false;
|
|
172
|
+
return true;
|
|
173
|
+
})
|
|
174
|
+
.slice(0, limit);
|
|
175
|
+
|
|
176
|
+
return createSuccessResponse({ task: task || null, projectPath, count: memories.length, memories });
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// ── memory_stats ────────────────────────────────────────────────────────
|
|
181
|
+
this.server.tool(
|
|
182
|
+
'memory_stats',
|
|
183
|
+
'Get memory system statistics (test harness)',
|
|
184
|
+
{
|
|
185
|
+
projectPath: z.string().optional(),
|
|
186
|
+
since: z.string().optional()
|
|
187
|
+
},
|
|
188
|
+
async ({ projectPath, since }) => {
|
|
189
|
+
requestCount++;
|
|
190
|
+
|
|
191
|
+
let memories = Array.from(MEMORY_DB.values());
|
|
192
|
+
if (projectPath) {
|
|
193
|
+
memories = memories.filter(m => m.projectPath === projectPath);
|
|
194
|
+
}
|
|
195
|
+
if (since) {
|
|
196
|
+
const sinceDate = new Date(since);
|
|
197
|
+
memories = memories.filter(m => new Date(m.createdAt) >= sinceDate);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const byType = {};
|
|
201
|
+
const byImportance = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
202
|
+
|
|
203
|
+
for (const m of memories) {
|
|
204
|
+
byType[m.type] = (byType[m.type] || 0) + 1;
|
|
205
|
+
byImportance[m.importance]++;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return createSuccessResponse({
|
|
209
|
+
totalMemories: memories.length,
|
|
210
|
+
byType,
|
|
211
|
+
byImportance,
|
|
212
|
+
projectPath: projectPath || null,
|
|
213
|
+
since: since || null
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// ── collab_start_session (MOCKED) ───────────────────────────────────────
|
|
219
|
+
this.server.tool(
|
|
220
|
+
'collab_start_session',
|
|
221
|
+
'Start a collaboration session (mocked in test harness)',
|
|
222
|
+
{
|
|
223
|
+
task: z.string().describe('Initial task'),
|
|
224
|
+
maxRounds: z.number().int().positive().default(3),
|
|
225
|
+
model: z.string().optional()
|
|
226
|
+
},
|
|
227
|
+
async ({ task, maxRounds, model }) => {
|
|
228
|
+
requestCount++;
|
|
229
|
+
|
|
230
|
+
const serverError = handleError('server_offline', 'Collab server not running');
|
|
231
|
+
if (serverError) return serverError;
|
|
232
|
+
|
|
233
|
+
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
234
|
+
const state = {
|
|
235
|
+
sessionId,
|
|
236
|
+
task,
|
|
237
|
+
maxRounds,
|
|
238
|
+
model: model || 'opencode/default',
|
|
239
|
+
status: 'initialized',
|
|
240
|
+
round: 0,
|
|
241
|
+
events: []
|
|
242
|
+
};
|
|
243
|
+
COLLAB_SESSIONS.set(sessionId, state);
|
|
244
|
+
|
|
245
|
+
return createSuccessResponse({
|
|
246
|
+
success: true,
|
|
247
|
+
sessionId,
|
|
248
|
+
status: state.status,
|
|
249
|
+
model: state.model,
|
|
250
|
+
tmuxSessionName: null,
|
|
251
|
+
attachCommand: null
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// ── collab_step (MOCKED) ────────────────────────────────────────────────
|
|
257
|
+
this.server.tool(
|
|
258
|
+
'collab_step',
|
|
259
|
+
'Execute a single collaboration step (mocked)',
|
|
260
|
+
{
|
|
261
|
+
sessionId: z.string().describe('Session ID'),
|
|
262
|
+
role: z.enum(['planner', 'critic', 'coder', 'reviewer']).describe('Role to execute')
|
|
263
|
+
},
|
|
264
|
+
async ({ sessionId, role }) => {
|
|
265
|
+
requestCount++;
|
|
266
|
+
|
|
267
|
+
const session = COLLAB_SESSIONS.get(sessionId);
|
|
268
|
+
if (!session) {
|
|
269
|
+
return createErrorResponse(`Session not found: ${sessionId}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
session.round++;
|
|
273
|
+
const event = {
|
|
274
|
+
role,
|
|
275
|
+
timestamp: new Date().toISOString(),
|
|
276
|
+
status: 'completed',
|
|
277
|
+
output: `[MOCK] ${role} completed round ${session.round}`
|
|
278
|
+
};
|
|
279
|
+
session.events.push(event);
|
|
280
|
+
|
|
281
|
+
return createSuccessResponse({
|
|
282
|
+
sessionId,
|
|
283
|
+
round: session.round,
|
|
284
|
+
event,
|
|
285
|
+
status: session.round >= session.maxRounds ? 'complete' : 'in_progress'
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// ── collab_get_result (MOCKED) ──────────────────────────────────────────
|
|
291
|
+
this.server.tool(
|
|
292
|
+
'collab_get_result',
|
|
293
|
+
'Get collaboration session result (mocked)',
|
|
294
|
+
{
|
|
295
|
+
sessionId: z.string().describe('Session ID')
|
|
296
|
+
},
|
|
297
|
+
async ({ sessionId }) => {
|
|
298
|
+
requestCount++;
|
|
299
|
+
|
|
300
|
+
const session = COLLAB_SESSIONS.get(sessionId);
|
|
301
|
+
if (!session) {
|
|
302
|
+
return createErrorResponse(`Session not found: ${sessionId}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return createSuccessResponse({
|
|
306
|
+
sessionId,
|
|
307
|
+
status: session.status,
|
|
308
|
+
rounds: session.round,
|
|
309
|
+
events: session.events,
|
|
310
|
+
summary: `[MOCK] Completed ${session.round} rounds for: ${session.task}`
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// ── collab_status (MOCKED) ─────────────────────────────────────────────
|
|
316
|
+
this.server.tool(
|
|
317
|
+
'collab_status',
|
|
318
|
+
'Get collaboration system status (mocked)',
|
|
319
|
+
{
|
|
320
|
+
sessionId: z.string().optional()
|
|
321
|
+
},
|
|
322
|
+
async ({ sessionId }) => {
|
|
323
|
+
requestCount++;
|
|
324
|
+
|
|
325
|
+
if (sessionId) {
|
|
326
|
+
const session = COLLAB_SESSIONS.get(sessionId);
|
|
327
|
+
if (!session) {
|
|
328
|
+
return createErrorResponse(`Session not found: ${sessionId}`);
|
|
329
|
+
}
|
|
330
|
+
return createSuccessResponse({
|
|
331
|
+
sessionId,
|
|
332
|
+
status: session.status,
|
|
333
|
+
round: session.round,
|
|
334
|
+
maxRounds: session.maxRounds
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return createSuccessResponse({
|
|
339
|
+
activeSessions: COLLAB_SESSIONS.size,
|
|
340
|
+
totalRequests: requestCount,
|
|
341
|
+
uptime: process.uptime()
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// ── harness_control (DEBUG) ─────────────────────────────────────────────
|
|
347
|
+
this.server.tool(
|
|
348
|
+
'harness_control',
|
|
349
|
+
'Control test harness behavior (debug only)',
|
|
350
|
+
{
|
|
351
|
+
action: z.enum(['reset', 'inject_error', 'clear_errors', 'status']).describe('Action to perform'),
|
|
352
|
+
errorType: z.string().optional().describe('Error type: validation, db_failure, server_offline')
|
|
353
|
+
},
|
|
354
|
+
async ({ action, errorType }) => {
|
|
355
|
+
switch (action) {
|
|
356
|
+
case 'reset':
|
|
357
|
+
MEMORY_DB.clear();
|
|
358
|
+
COLLAB_SESSIONS.clear();
|
|
359
|
+
requestCount = 0;
|
|
360
|
+
errorInjectionMode = null;
|
|
361
|
+
return createSuccessResponse({ message: 'Harness reset complete' });
|
|
362
|
+
|
|
363
|
+
case 'inject_error':
|
|
364
|
+
setErrorInjection(errorType);
|
|
365
|
+
return createSuccessResponse({ message: `Error injection set: ${errorType}` });
|
|
366
|
+
|
|
367
|
+
case 'clear_errors':
|
|
368
|
+
setErrorInjection(null);
|
|
369
|
+
return createSuccessResponse({ message: 'Error injection cleared' });
|
|
370
|
+
|
|
371
|
+
case 'status':
|
|
372
|
+
return createSuccessResponse({
|
|
373
|
+
memories: MEMORY_DB.size,
|
|
374
|
+
sessions: COLLAB_SESSIONS.size,
|
|
375
|
+
requestCount,
|
|
376
|
+
errorInjectionMode: errorInjectionMode || 'none'
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
default:
|
|
380
|
+
return createErrorResponse(`Unknown action: ${action}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async run() {
|
|
387
|
+
const transport = new StdioServerTransport();
|
|
388
|
+
await this.server.connect(transport);
|
|
389
|
+
console.error('[harness] Test harness ready on stdio');
|
|
390
|
+
console.error('[harness] Available tools: memory_save, memory_search, memory_recall, memory_stats');
|
|
391
|
+
console.error('[harness] Collab tools (mocked): collab_start_session, collab_step, collab_get_result, collab_status');
|
|
392
|
+
console.error('[harness] Debug tool: harness_control (reset, inject_error, clear_errors, status)');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const harness = new MCPTestHarness();
|
|
397
|
+
harness.run().catch(err => {
|
|
398
|
+
console.error('[harness] Fatal error:', err.message);
|
|
399
|
+
process.exit(1);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
process.on('SIGTERM', () => {
|
|
403
|
+
console.error('[harness] Received SIGTERM, shutting down gracefully');
|
|
404
|
+
process.exit(0);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
process.on('SIGINT', () => {
|
|
408
|
+
console.error('[harness] Received SIGINT, shutting down');
|
|
409
|
+
process.exit(0);
|
|
410
|
+
});
|
|
@@ -17,6 +17,13 @@ function run(command, args, options = {}) {
|
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function runInstallCommand(command) {
|
|
21
|
+
if (platform() === 'win32') {
|
|
22
|
+
return run('cmd.exe', ['/c', command], { stdio: 'inherit' });
|
|
23
|
+
}
|
|
24
|
+
return run('bash', ['-lc', command], { stdio: 'inherit' });
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
export function hasCommand(command) {
|
|
21
28
|
return Boolean(resolveCommandPath(command));
|
|
22
29
|
}
|
|
@@ -78,6 +85,29 @@ function resolveTmuxInstallCommand() {
|
|
|
78
85
|
return null;
|
|
79
86
|
}
|
|
80
87
|
|
|
88
|
+
function resolveZellijInstallCommand() {
|
|
89
|
+
if (platform() === 'darwin') {
|
|
90
|
+
if (hasCommand('brew')) return 'brew install zellij';
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (platform() === 'linux') {
|
|
95
|
+
if (hasCommand('apt-get')) return 'sudo apt-get update && sudo apt-get install -y zellij';
|
|
96
|
+
if (hasCommand('dnf')) return 'sudo dnf install -y zellij';
|
|
97
|
+
if (hasCommand('yum')) return 'sudo yum install -y zellij';
|
|
98
|
+
if (hasCommand('pacman')) return 'sudo pacman -S --noconfirm zellij';
|
|
99
|
+
if (hasCommand('zypper')) return 'sudo zypper --non-interactive install zellij';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (platform() === 'win32') {
|
|
103
|
+
if (hasCommand('winget')) return 'winget install --id zellij-org.zellij -e';
|
|
104
|
+
if (hasCommand('choco')) return 'choco install zellij -y';
|
|
105
|
+
if (hasCommand('scoop')) return 'scoop install zellij';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
81
111
|
export function installTmux() {
|
|
82
112
|
if (hasCommand('tmux')) {
|
|
83
113
|
return { success: true, installed: false, message: 'tmux already installed' };
|
|
@@ -92,7 +122,7 @@ export function installTmux() {
|
|
|
92
122
|
};
|
|
93
123
|
}
|
|
94
124
|
|
|
95
|
-
const result =
|
|
125
|
+
const result = runInstallCommand(installCommand);
|
|
96
126
|
if (result.status !== 0) {
|
|
97
127
|
return { success: false, installed: false, message: 'tmux installation command failed' };
|
|
98
128
|
}
|
|
@@ -106,12 +136,50 @@ export function installTmux() {
|
|
|
106
136
|
};
|
|
107
137
|
}
|
|
108
138
|
|
|
109
|
-
export function
|
|
139
|
+
export function installZellij() {
|
|
140
|
+
if (hasCommand('zellij')) {
|
|
141
|
+
return { success: true, installed: false, message: 'zellij already installed' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const installCommand = resolveZellijInstallCommand();
|
|
145
|
+
if (!installCommand) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
installed: false,
|
|
149
|
+
message: 'No supported package manager detected for automatic zellij installation',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = runInstallCommand(installCommand);
|
|
154
|
+
if (result.status !== 0) {
|
|
155
|
+
return { success: false, installed: false, message: 'zellij installation command failed' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
success: hasCommand('zellij'),
|
|
160
|
+
installed: true,
|
|
161
|
+
message: hasCommand('zellij')
|
|
162
|
+
? 'zellij installed successfully'
|
|
163
|
+
: 'zellij installer finished but binary is not available in PATH yet',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function ensureCollabDependencies(options = {}) {
|
|
168
|
+
const installTmuxEnabled = options.installTmux ?? true;
|
|
169
|
+
const installZellijEnabled = options.installZellij ?? true;
|
|
110
170
|
const opencode = installOpenCode();
|
|
111
|
-
const tmux =
|
|
171
|
+
const tmux = installTmuxEnabled
|
|
172
|
+
? installTmux()
|
|
173
|
+
: { success: hasCommand('tmux'), installed: false, message: hasCommand('tmux') ? 'tmux already installed' : 'tmux installation skipped' };
|
|
174
|
+
const zellij = installZellijEnabled
|
|
175
|
+
? installZellij()
|
|
176
|
+
: { success: hasCommand('zellij'), installed: false, message: hasCommand('zellij') ? 'zellij already installed' : 'zellij installation skipped' };
|
|
177
|
+
|
|
178
|
+
const hasMultiplexer = tmux.success || zellij.success;
|
|
112
179
|
return {
|
|
113
180
|
opencode,
|
|
114
181
|
tmux,
|
|
115
|
-
|
|
182
|
+
zellij,
|
|
183
|
+
success: opencode.success && hasMultiplexer,
|
|
116
184
|
};
|
|
117
185
|
}
|