ac-framework 1.9.8 → 2.0.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/README.md +18 -10
- package/package.json +1 -1
- package/src/agents/config-store.js +16 -0
- package/src/agents/orchestrator.js +74 -4
- package/src/agents/runtime.js +78 -3
- package/src/agents/state-store.js +173 -4
- package/src/commands/agents.js +547 -64
- package/src/commands/init.js +16 -5
- package/src/mcp/collab-server.js +88 -26
- package/src/services/dependency-installer.js +247 -5
package/src/commands/init.js
CHANGED
|
@@ -156,7 +156,8 @@ async function setupPersistentMemory() {
|
|
|
156
156
|
async function setupCollaborativeSystem() {
|
|
157
157
|
const hasOpenCode = hasCommand('opencode');
|
|
158
158
|
const hasTmux = hasCommand('tmux');
|
|
159
|
-
const
|
|
159
|
+
const hasZellij = hasCommand('zellij');
|
|
160
|
+
const alreadyReady = hasOpenCode && (hasZellij || hasTmux);
|
|
160
161
|
|
|
161
162
|
console.log();
|
|
162
163
|
await animatedSeparator(60);
|
|
@@ -168,10 +169,10 @@ async function setupCollaborativeSystem() {
|
|
|
168
169
|
console.log(
|
|
169
170
|
chalk.hex('#636E72')(
|
|
170
171
|
` ${COLLAB_SYSTEM_NAME} launches a real-time collaborative agent war-room with\n` +
|
|
171
|
-
' 4 coordinated roles (planner, critic, coder, reviewer) in
|
|
172
|
+
' 4 coordinated roles (planner, critic, coder, reviewer) in multiplexer panes.\n\n' +
|
|
172
173
|
' Each round is turn-based with shared incremental context, so every\n' +
|
|
173
174
|
' contribution from one agent is fed to the next, not isolated fan-out.\n\n' +
|
|
174
|
-
` Dependencies: ${chalk.hex('#DFE6E9')('OpenCode')} + ${chalk.hex('#DFE6E9')('tmux')}`
|
|
175
|
+
` Dependencies: ${chalk.hex('#DFE6E9')('OpenCode')} + ${chalk.hex('#DFE6E9')('zellij')} (${chalk.hex('#DFE6E9')('tmux')} fallback)`
|
|
175
176
|
)
|
|
176
177
|
);
|
|
177
178
|
console.log();
|
|
@@ -223,7 +224,8 @@ async function setupCollaborativeSystem() {
|
|
|
223
224
|
|
|
224
225
|
if (alreadyReady) {
|
|
225
226
|
console.log();
|
|
226
|
-
|
|
227
|
+
const mux = hasZellij ? 'zellij' : 'tmux';
|
|
228
|
+
console.log(chalk.hex('#00B894')(` ◆ OpenCode and ${mux} are already available.`));
|
|
227
229
|
await installCollabMcpConnections();
|
|
228
230
|
console.log(chalk.hex('#636E72')(' Run `acfm agents start --task "..."` to launch collaboration.'));
|
|
229
231
|
console.log();
|
|
@@ -234,11 +236,20 @@ async function setupCollaborativeSystem() {
|
|
|
234
236
|
console.log(chalk.hex('#B2BEC3')(` Installing ${COLLAB_SYSTEM_NAME} dependencies...`));
|
|
235
237
|
console.log();
|
|
236
238
|
|
|
237
|
-
const result = ensureCollabDependencies(
|
|
239
|
+
const result = await ensureCollabDependencies({
|
|
240
|
+
installZellij: true,
|
|
241
|
+
installTmux: true,
|
|
242
|
+
preferManagedZellij: true,
|
|
243
|
+
});
|
|
238
244
|
|
|
239
245
|
const oColor = result.opencode.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
246
|
+
const zColor = result.zellij.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
240
247
|
const tColor = result.tmux.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
241
248
|
console.log(oColor(` ◆ OpenCode: ${result.opencode.message}`));
|
|
249
|
+
console.log(zColor(` ◆ zellij: ${result.zellij.message}`));
|
|
250
|
+
if (result.zellij.binaryPath) {
|
|
251
|
+
console.log(chalk.hex('#636E72')(` ${result.zellij.binaryPath}`));
|
|
252
|
+
}
|
|
242
253
|
console.log(tColor(` ◆ tmux: ${result.tmux.message}`));
|
|
243
254
|
console.log();
|
|
244
255
|
|
package/src/mcp/collab-server.js
CHANGED
|
@@ -17,7 +17,13 @@ import { COLLAB_ROLES } from '../agents/constants.js';
|
|
|
17
17
|
import { buildEffectiveRoleModels, sanitizeRoleModels } from '../agents/model-selection.js';
|
|
18
18
|
import { runWorkerIteration } from '../agents/orchestrator.js';
|
|
19
19
|
import { getSessionDir } from '../agents/state-store.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
spawnTmuxSession,
|
|
22
|
+
spawnZellijSession,
|
|
23
|
+
tmuxSessionExists,
|
|
24
|
+
zellijSessionExists,
|
|
25
|
+
resolveMultiplexer,
|
|
26
|
+
} from '../agents/runtime.js';
|
|
21
27
|
import {
|
|
22
28
|
addUserMessage,
|
|
23
29
|
createSession,
|
|
@@ -28,7 +34,8 @@ import {
|
|
|
28
34
|
setCurrentSession,
|
|
29
35
|
stopSession,
|
|
30
36
|
} from '../agents/state-store.js';
|
|
31
|
-
import { hasCommand, resolveCommandPath } from '../services/dependency-installer.js';
|
|
37
|
+
import { hasCommand, resolveCommandPath, resolveManagedZellijPath } from '../services/dependency-installer.js';
|
|
38
|
+
import { loadAgentsConfig } from '../agents/config-store.js';
|
|
32
39
|
|
|
33
40
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
41
|
const runnerPath = resolve(__dirname, '../../bin/acfm.js');
|
|
@@ -67,6 +74,21 @@ function launchAutopilot(sessionId) {
|
|
|
67
74
|
child.unref();
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
function resolveConfiguredZellijPath(config) {
|
|
78
|
+
const strategy = config?.agents?.zellij?.strategy || 'auto';
|
|
79
|
+
if (strategy === 'system') {
|
|
80
|
+
return resolveCommandPath('zellij');
|
|
81
|
+
}
|
|
82
|
+
const managed = resolveManagedZellijPath(config);
|
|
83
|
+
if (managed) return managed;
|
|
84
|
+
return resolveCommandPath('zellij');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function muxExists(multiplexer, sessionName, zellijPath = null) {
|
|
88
|
+
if (multiplexer === 'zellij') return zellijSessionExists(sessionName, zellijPath);
|
|
89
|
+
return tmuxSessionExists(sessionName);
|
|
90
|
+
}
|
|
91
|
+
|
|
70
92
|
class MCPCollabServer {
|
|
71
93
|
constructor() {
|
|
72
94
|
this.server = new McpServer({
|
|
@@ -92,7 +114,7 @@ class MCPCollabServer {
|
|
|
92
114
|
reviewer: z.string().optional(),
|
|
93
115
|
}).partial().optional().describe('Optional per-role models (provider/model)'),
|
|
94
116
|
cwd: z.string().optional().describe('Working directory for agents'),
|
|
95
|
-
spawnWorkers: z.boolean().default(true).describe('Create
|
|
117
|
+
spawnWorkers: z.boolean().default(true).describe('Create multiplexer workers and panes'),
|
|
96
118
|
runPolicy: z.object({
|
|
97
119
|
timeoutPerRoleMs: z.number().int().positive().optional(),
|
|
98
120
|
retryOnTimeout: z.number().int().min(0).optional(),
|
|
@@ -107,8 +129,12 @@ class MCPCollabServer {
|
|
|
107
129
|
throw new Error('OpenCode binary not found in PATH. Run: acfm agents setup');
|
|
108
130
|
}
|
|
109
131
|
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
const config = await loadAgentsConfig();
|
|
133
|
+
const configuredMux = config.agents.multiplexer || 'auto';
|
|
134
|
+
const zellijPath = resolveConfiguredZellijPath(config);
|
|
135
|
+
const multiplexer = resolveMultiplexer(configuredMux, hasCommand('tmux'), Boolean(zellijPath));
|
|
136
|
+
if (spawnWorkers && !multiplexer) {
|
|
137
|
+
throw new Error('No multiplexer found (zellij/tmux). Run: acfm agents setup');
|
|
112
138
|
}
|
|
113
139
|
|
|
114
140
|
const state = await createSession(task, {
|
|
@@ -119,18 +145,31 @@ class MCPCollabServer {
|
|
|
119
145
|
workingDirectory,
|
|
120
146
|
opencodeBin,
|
|
121
147
|
runPolicy,
|
|
148
|
+
multiplexer: multiplexer || configuredMux,
|
|
122
149
|
});
|
|
123
150
|
let updated = state;
|
|
124
151
|
if (spawnWorkers) {
|
|
125
|
-
const
|
|
152
|
+
const sessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
126
153
|
const sessionDir = getSessionDir(state.sessionId);
|
|
127
|
-
|
|
128
|
-
|
|
154
|
+
if (multiplexer === 'zellij') {
|
|
155
|
+
await spawnZellijSession({ sessionName, sessionDir, sessionId: state.sessionId, binaryPath: zellijPath });
|
|
156
|
+
} else {
|
|
157
|
+
await spawnTmuxSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
158
|
+
}
|
|
159
|
+
updated = await saveSessionState({
|
|
160
|
+
...state,
|
|
161
|
+
multiplexer,
|
|
162
|
+
multiplexerSessionName: sessionName,
|
|
163
|
+
tmuxSessionName: multiplexer === 'tmux' ? sessionName : null,
|
|
164
|
+
});
|
|
129
165
|
}
|
|
130
166
|
await setCurrentSession(state.sessionId);
|
|
131
167
|
|
|
132
|
-
const
|
|
133
|
-
const
|
|
168
|
+
const mux = updated.multiplexer || null;
|
|
169
|
+
const muxSessionName = updated.multiplexerSessionName || updated.tmuxSessionName || null;
|
|
170
|
+
const attachCommand = muxSessionName
|
|
171
|
+
? (mux === 'zellij' ? `zellij attach ${muxSessionName}` : `tmux attach -t ${muxSessionName}`)
|
|
172
|
+
: null;
|
|
134
173
|
return {
|
|
135
174
|
content: [{
|
|
136
175
|
type: 'text',
|
|
@@ -142,7 +181,8 @@ class MCPCollabServer {
|
|
|
142
181
|
roleModels: updated.roleModels || {},
|
|
143
182
|
effectiveRoleModels: buildEffectiveRoleModels(updated, updated.model || null),
|
|
144
183
|
run: summarizeRun(updated),
|
|
145
|
-
|
|
184
|
+
multiplexer: mux,
|
|
185
|
+
multiplexerSessionName: muxSessionName,
|
|
146
186
|
attachCommand,
|
|
147
187
|
}, null, 2),
|
|
148
188
|
}],
|
|
@@ -169,7 +209,7 @@ class MCPCollabServer {
|
|
|
169
209
|
throw new Error(`Session is ${state.status}. Resume/start before invoking.`);
|
|
170
210
|
}
|
|
171
211
|
|
|
172
|
-
if (!state.tmuxSessionName) {
|
|
212
|
+
if (!state.multiplexerSessionName && !state.tmuxSessionName) {
|
|
173
213
|
launchAutopilot(state.sessionId);
|
|
174
214
|
}
|
|
175
215
|
|
|
@@ -193,8 +233,13 @@ class MCPCollabServer {
|
|
|
193
233
|
status: state.status,
|
|
194
234
|
run: summarizeRun(state),
|
|
195
235
|
latestEvent: latestRunEvent(state),
|
|
196
|
-
|
|
197
|
-
|
|
236
|
+
multiplexer: state.multiplexer || null,
|
|
237
|
+
multiplexerSessionName: state.multiplexerSessionName || state.tmuxSessionName || null,
|
|
238
|
+
attachCommand: state.multiplexerSessionName
|
|
239
|
+
? (state.multiplexer === 'zellij'
|
|
240
|
+
? `zellij attach ${state.multiplexerSessionName}`
|
|
241
|
+
: `tmux attach -t ${state.multiplexerSessionName}`)
|
|
242
|
+
: (state.tmuxSessionName ? `tmux attach -t ${state.tmuxSessionName}` : null),
|
|
198
243
|
}, null, 2),
|
|
199
244
|
}],
|
|
200
245
|
};
|
|
@@ -408,35 +453,50 @@ class MCPCollabServer {
|
|
|
408
453
|
|
|
409
454
|
this.server.tool(
|
|
410
455
|
'collab_resume_session',
|
|
411
|
-
'Resume session and recreate
|
|
456
|
+
'Resume session and recreate workers if needed',
|
|
412
457
|
{
|
|
413
458
|
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
414
|
-
recreateWorkers: z.boolean().default(true).describe('Recreate
|
|
459
|
+
recreateWorkers: z.boolean().default(true).describe('Recreate multiplexer session when missing'),
|
|
415
460
|
},
|
|
416
461
|
async ({ sessionId, recreateWorkers }) => {
|
|
417
462
|
try {
|
|
418
463
|
const id = sessionId || await loadCurrentSessionId();
|
|
419
464
|
if (!id) throw new Error('No active session found');
|
|
420
465
|
let state = await loadSessionState(id);
|
|
466
|
+
const config = await loadAgentsConfig();
|
|
467
|
+
const zellijPath = resolveConfiguredZellijPath(config);
|
|
421
468
|
|
|
422
|
-
const
|
|
423
|
-
|
|
469
|
+
const multiplexer = state.multiplexer || resolveMultiplexer('auto', hasCommand('tmux'), Boolean(zellijPath));
|
|
470
|
+
if (!multiplexer) {
|
|
471
|
+
throw new Error('No multiplexer found (zellij/tmux). Run: acfm agents setup');
|
|
472
|
+
}
|
|
473
|
+
const sessionName = state.multiplexerSessionName || state.tmuxSessionName || `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
474
|
+
const sessionExists = await muxExists(multiplexer, sessionName, zellijPath);
|
|
424
475
|
|
|
425
|
-
if (!
|
|
426
|
-
if (!hasCommand('tmux')) {
|
|
427
|
-
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
428
|
-
}
|
|
476
|
+
if (!sessionExists && recreateWorkers) {
|
|
429
477
|
const sessionDir = getSessionDir(state.sessionId);
|
|
430
|
-
|
|
478
|
+
if (multiplexer === 'zellij') {
|
|
479
|
+
if (!zellijPath) throw new Error('zellij is not installed. Run: acfm agents setup');
|
|
480
|
+
await spawnZellijSession({ sessionName, sessionDir, sessionId: state.sessionId, binaryPath: zellijPath });
|
|
481
|
+
} else {
|
|
482
|
+
if (!hasCommand('tmux')) throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
483
|
+
await spawnTmuxSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
484
|
+
}
|
|
431
485
|
}
|
|
432
486
|
|
|
433
487
|
state = await saveSessionState({
|
|
434
488
|
...state,
|
|
435
489
|
status: 'running',
|
|
436
|
-
|
|
490
|
+
multiplexer,
|
|
491
|
+
multiplexerSessionName: sessionName,
|
|
492
|
+
tmuxSessionName: multiplexer === 'tmux' ? sessionName : state.tmuxSessionName || null,
|
|
437
493
|
});
|
|
438
494
|
await setCurrentSession(state.sessionId);
|
|
439
495
|
|
|
496
|
+
const attachCommand = multiplexer === 'zellij'
|
|
497
|
+
? `zellij attach ${sessionName}`
|
|
498
|
+
: `tmux attach -t ${sessionName}`;
|
|
499
|
+
|
|
440
500
|
return {
|
|
441
501
|
content: [{
|
|
442
502
|
type: 'text',
|
|
@@ -444,8 +504,10 @@ class MCPCollabServer {
|
|
|
444
504
|
success: true,
|
|
445
505
|
sessionId: state.sessionId,
|
|
446
506
|
status: state.status,
|
|
447
|
-
|
|
448
|
-
|
|
507
|
+
multiplexer,
|
|
508
|
+
multiplexerSessionName: sessionName,
|
|
509
|
+
recreatedWorkers: !sessionExists && recreateWorkers,
|
|
510
|
+
attachCommand,
|
|
449
511
|
}, null, 2),
|
|
450
512
|
}],
|
|
451
513
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { chmod, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
|
-
import { platform } from 'node:os';
|
|
5
|
+
import { arch, homedir, platform } from 'node:os';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
5
7
|
|
|
6
8
|
function preferredOpenCodePath() {
|
|
7
9
|
const home = process.env.HOME;
|
|
@@ -17,6 +19,68 @@ function run(command, args, options = {}) {
|
|
|
17
19
|
});
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
function runInstallCommand(command) {
|
|
23
|
+
if (platform() === 'win32') {
|
|
24
|
+
return run('cmd.exe', ['/c', command], { stdio: 'inherit' });
|
|
25
|
+
}
|
|
26
|
+
return run('bash', ['-lc', command], { stdio: 'inherit' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function fetchJson(url) {
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
headers: {
|
|
32
|
+
Accept: 'application/vnd.github+json',
|
|
33
|
+
'User-Agent': 'ac-framework',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`Request failed (${response.status}) while fetching ${url}`);
|
|
38
|
+
}
|
|
39
|
+
return response.json();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sha256HexFromBuffer(buffer) {
|
|
43
|
+
const hash = createHash('sha256');
|
|
44
|
+
hash.update(buffer);
|
|
45
|
+
return hash.digest('hex');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function managedToolsRoot() {
|
|
49
|
+
return join(homedir(), '.acfm', 'tools', 'zellij');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function platformAssetPrefix() {
|
|
53
|
+
const p = platform();
|
|
54
|
+
const a = arch();
|
|
55
|
+
if (p === 'linux' && a === 'x64') return 'zellij-x86_64-unknown-linux-musl';
|
|
56
|
+
if (p === 'linux' && a === 'arm64') return 'zellij-aarch64-unknown-linux-musl';
|
|
57
|
+
if (p === 'darwin' && a === 'x64') return 'zellij-x86_64-apple-darwin';
|
|
58
|
+
if (p === 'darwin' && a === 'arm64') return 'zellij-aarch64-apple-darwin';
|
|
59
|
+
if (p === 'win32' && a === 'x64') return 'zellij-x86_64-pc-windows-msvc';
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function managedZellijBinaryPath(version) {
|
|
64
|
+
const fileName = platform() === 'win32' ? 'zellij.exe' : 'zellij';
|
|
65
|
+
return join(managedToolsRoot(), version, fileName);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractTarball(tarPath, outputDir) {
|
|
69
|
+
return run('tar', ['-xzf', tarPath, '-C', outputDir]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findReleaseAsset(release, suffix) {
|
|
73
|
+
return (release.assets || []).find((asset) => asset.name === suffix) || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveManagedZellijPath(config = null) {
|
|
77
|
+
const fromEnv = process.env.ACFM_ZELLIJ_BIN;
|
|
78
|
+
if (fromEnv && existsSync(fromEnv)) return fromEnv;
|
|
79
|
+
const configured = config?.agents?.zellij?.binaryPath;
|
|
80
|
+
if (configured && existsSync(configured)) return configured;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
20
84
|
export function hasCommand(command) {
|
|
21
85
|
return Boolean(resolveCommandPath(command));
|
|
22
86
|
}
|
|
@@ -78,6 +142,29 @@ function resolveTmuxInstallCommand() {
|
|
|
78
142
|
return null;
|
|
79
143
|
}
|
|
80
144
|
|
|
145
|
+
function resolveZellijInstallCommand() {
|
|
146
|
+
if (platform() === 'darwin') {
|
|
147
|
+
if (hasCommand('brew')) return 'brew install zellij';
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (platform() === 'linux') {
|
|
152
|
+
if (hasCommand('apt-get')) return 'sudo apt-get update && sudo apt-get install -y zellij';
|
|
153
|
+
if (hasCommand('dnf')) return 'sudo dnf install -y zellij';
|
|
154
|
+
if (hasCommand('yum')) return 'sudo yum install -y zellij';
|
|
155
|
+
if (hasCommand('pacman')) return 'sudo pacman -S --noconfirm zellij';
|
|
156
|
+
if (hasCommand('zypper')) return 'sudo zypper --non-interactive install zellij';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (platform() === 'win32') {
|
|
160
|
+
if (hasCommand('winget')) return 'winget install --id zellij-org.zellij -e';
|
|
161
|
+
if (hasCommand('choco')) return 'choco install zellij -y';
|
|
162
|
+
if (hasCommand('scoop')) return 'scoop install zellij';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
81
168
|
export function installTmux() {
|
|
82
169
|
if (hasCommand('tmux')) {
|
|
83
170
|
return { success: true, installed: false, message: 'tmux already installed' };
|
|
@@ -92,7 +179,7 @@ export function installTmux() {
|
|
|
92
179
|
};
|
|
93
180
|
}
|
|
94
181
|
|
|
95
|
-
const result =
|
|
182
|
+
const result = runInstallCommand(installCommand);
|
|
96
183
|
if (result.status !== 0) {
|
|
97
184
|
return { success: false, installed: false, message: 'tmux installation command failed' };
|
|
98
185
|
}
|
|
@@ -106,12 +193,167 @@ export function installTmux() {
|
|
|
106
193
|
};
|
|
107
194
|
}
|
|
108
195
|
|
|
109
|
-
export function
|
|
196
|
+
export function installZellij() {
|
|
197
|
+
if (hasCommand('zellij')) {
|
|
198
|
+
return { success: true, installed: false, message: 'zellij already installed' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const installCommand = resolveZellijInstallCommand();
|
|
202
|
+
if (!installCommand) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
installed: false,
|
|
206
|
+
message: 'No supported package manager detected for automatic zellij installation',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result = runInstallCommand(installCommand);
|
|
211
|
+
if (result.status !== 0) {
|
|
212
|
+
return { success: false, installed: false, message: 'zellij installation command failed' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
success: hasCommand('zellij'),
|
|
217
|
+
installed: true,
|
|
218
|
+
message: hasCommand('zellij')
|
|
219
|
+
? 'zellij installed successfully'
|
|
220
|
+
: 'zellij installer finished but binary is not available in PATH yet',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function installManagedZellijLatest() {
|
|
225
|
+
const existingSystem = resolveCommandPath('zellij');
|
|
226
|
+
if (existingSystem) {
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
installed: false,
|
|
230
|
+
version: null,
|
|
231
|
+
binaryPath: existingSystem,
|
|
232
|
+
message: 'zellij already installed in system PATH',
|
|
233
|
+
source: 'system',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const prefix = platformAssetPrefix();
|
|
238
|
+
if (!prefix) {
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
installed: false,
|
|
242
|
+
version: null,
|
|
243
|
+
binaryPath: null,
|
|
244
|
+
message: `Unsupported OS/arch for managed zellij install: ${platform()}/${arch()}`,
|
|
245
|
+
source: 'managed',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const release = await fetchJson('https://api.github.com/repos/zellij-org/zellij/releases/latest');
|
|
251
|
+
const version = String(release.tag_name || '').trim() || 'latest';
|
|
252
|
+
|
|
253
|
+
if (platform() === 'win32') {
|
|
254
|
+
const zipAsset = findReleaseAsset(release, `${prefix}.zip`);
|
|
255
|
+
if (!zipAsset) {
|
|
256
|
+
throw new Error(`No matching Windows asset found for ${prefix}`);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
success: false,
|
|
260
|
+
installed: false,
|
|
261
|
+
version,
|
|
262
|
+
binaryPath: null,
|
|
263
|
+
message: 'Managed Windows zellij install is not implemented yet; use winget/choco/scoop.',
|
|
264
|
+
source: 'managed',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const tarAsset = findReleaseAsset(release, `${prefix}.tar.gz`);
|
|
269
|
+
if (!tarAsset?.browser_download_url) {
|
|
270
|
+
throw new Error(`No matching zellij asset found for ${prefix}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const targetDir = join(managedToolsRoot(), version);
|
|
274
|
+
const binaryPath = managedZellijBinaryPath(version);
|
|
275
|
+
if (existsSync(binaryPath)) {
|
|
276
|
+
return {
|
|
277
|
+
success: true,
|
|
278
|
+
installed: false,
|
|
279
|
+
version,
|
|
280
|
+
binaryPath,
|
|
281
|
+
message: `Managed zellij already installed (${version})`,
|
|
282
|
+
source: 'managed',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await mkdir(targetDir, { recursive: true });
|
|
287
|
+
const tmpTarPath = join(targetDir, `${prefix}.tar.gz.download`);
|
|
288
|
+
const response = await fetch(tarAsset.browser_download_url, {
|
|
289
|
+
headers: { 'User-Agent': 'ac-framework' },
|
|
290
|
+
});
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
throw new Error(`Failed downloading ${tarAsset.name} (${response.status})`);
|
|
293
|
+
}
|
|
294
|
+
const raw = Buffer.from(await response.arrayBuffer());
|
|
295
|
+
|
|
296
|
+
const expectedDigest = String(tarAsset.digest || '').replace(/^sha256:/, '');
|
|
297
|
+
if (expectedDigest) {
|
|
298
|
+
const actualDigest = sha256HexFromBuffer(raw);
|
|
299
|
+
if (actualDigest !== expectedDigest) {
|
|
300
|
+
throw new Error(`Digest mismatch for ${tarAsset.name}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
await writeFile(tmpTarPath, raw);
|
|
305
|
+
const extracted = extractTarball(tmpTarPath, targetDir);
|
|
306
|
+
await rm(tmpTarPath, { force: true });
|
|
307
|
+
if (extracted.status !== 0) {
|
|
308
|
+
throw new Error('Failed extracting zellij tarball');
|
|
309
|
+
}
|
|
310
|
+
if (!existsSync(binaryPath)) {
|
|
311
|
+
throw new Error(`zellij binary not found after extraction at ${binaryPath}`);
|
|
312
|
+
}
|
|
313
|
+
await chmod(binaryPath, 0o755);
|
|
314
|
+
|
|
315
|
+
const versionProbe = run(binaryPath, ['--version']);
|
|
316
|
+
if (versionProbe.status !== 0) {
|
|
317
|
+
throw new Error('Installed zellij binary failed version check');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
installed: true,
|
|
323
|
+
version,
|
|
324
|
+
binaryPath,
|
|
325
|
+
message: `Managed zellij installed (${version})`,
|
|
326
|
+
source: 'managed',
|
|
327
|
+
};
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return {
|
|
330
|
+
success: false,
|
|
331
|
+
installed: false,
|
|
332
|
+
version: null,
|
|
333
|
+
binaryPath: null,
|
|
334
|
+
message: `Managed zellij install failed: ${error.message}`,
|
|
335
|
+
source: 'managed',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function ensureCollabDependencies(options = {}) {
|
|
341
|
+
const installTmuxEnabled = options.installTmux ?? true;
|
|
342
|
+
const installZellijEnabled = options.installZellij ?? true;
|
|
343
|
+
const preferManagedZellij = options.preferManagedZellij ?? false;
|
|
110
344
|
const opencode = installOpenCode();
|
|
111
|
-
const tmux =
|
|
345
|
+
const tmux = installTmuxEnabled
|
|
346
|
+
? installTmux()
|
|
347
|
+
: { success: hasCommand('tmux'), installed: false, message: hasCommand('tmux') ? 'tmux already installed' : 'tmux installation skipped' };
|
|
348
|
+
const zellij = installZellijEnabled
|
|
349
|
+
? (preferManagedZellij ? await installManagedZellijLatest() : installZellij())
|
|
350
|
+
: { success: hasCommand('zellij'), installed: false, message: hasCommand('zellij') ? 'zellij already installed' : 'zellij installation skipped' };
|
|
351
|
+
|
|
352
|
+
const hasMultiplexer = tmux.success || zellij.success;
|
|
112
353
|
return {
|
|
113
354
|
opencode,
|
|
114
355
|
tmux,
|
|
115
|
-
|
|
356
|
+
zellij,
|
|
357
|
+
success: opencode.success && hasMultiplexer,
|
|
116
358
|
};
|
|
117
359
|
}
|