opencode-agent-tmux 1.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 +162 -0
- package/bin/opencode-tmux.js +127 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +508 -0
- package/package.json +39 -0
- package/scripts/install.js +142 -0
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# opencode-agent-tmux
|
|
2
|
+
|
|
3
|
+
OpenCode plugin that provides tmux integration for viewing agent execution in real-time. Automatically spawns panes, streams output, and manages your terminal workspace.
|
|
4
|
+
|
|
5
|
+
## 🤖 For Humans (Quick Start)
|
|
6
|
+
|
|
7
|
+
Want to get started immediately? Just paste this prompt into your OpenCode agent (or any other agentic tool like Claude Code) and let it handle the setup for you:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
Please install the opencode-agent-tmux plugin for me.
|
|
11
|
+
|
|
12
|
+
1. Clone the repository to ~/Code/opencode-agent-tmux
|
|
13
|
+
2. Run 'bun install' and 'bun run build' inside the directory
|
|
14
|
+
3. Add the plugin path to my ~/.config/opencode/opencode.json file
|
|
15
|
+
4. Verify the installation by running 'opencode --version'
|
|
16
|
+
|
|
17
|
+
The plugin repo is: https://github.com/AnganSamadder/opencode-agent-tmux.git
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## ✨ Features
|
|
21
|
+
|
|
22
|
+
- **Automatic Tmux Pane Spawning**: When any agent starts, automatically spawns a tmux pane
|
|
23
|
+
- **Live Streaming**: Each pane runs `opencode attach` to show real-time agent output
|
|
24
|
+
- **Auto-Cleanup**: Panes automatically close when agents complete
|
|
25
|
+
- **Configurable Layout**: Support multiple tmux layouts (main-vertical, tiled, etc.)
|
|
26
|
+
- **Multi-Port Support**: Automatically finds available ports when running multiple instances
|
|
27
|
+
- **Agent-Agnostic**: Works with oh-my-opencode, omoc-slim, or vanilla OpenCode
|
|
28
|
+
|
|
29
|
+
## 📋 Requirements
|
|
30
|
+
|
|
31
|
+
- **OpenCode**
|
|
32
|
+
- **tmux**
|
|
33
|
+
- **Bun** (for building)
|
|
34
|
+
|
|
35
|
+
## 📦 Installation (Official)
|
|
36
|
+
|
|
37
|
+
1. **Install via NPM:**
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g opencode-agent-tmux
|
|
40
|
+
```
|
|
41
|
+
*Note: The installation automatically configures a shell alias to enable the smart tmux wrapper.*
|
|
42
|
+
|
|
43
|
+
2. **Configure OpenCode:**
|
|
44
|
+
Add the plugin name to your `~/.config/opencode/opencode.json`:
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"plugins": [
|
|
48
|
+
"opencode-agent-tmux"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 🛠 Manual Installation (Development)
|
|
54
|
+
|
|
55
|
+
If you prefer to install it yourself:
|
|
56
|
+
|
|
57
|
+
1. **Clone the repository:**
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/AnganSamadder/opencode-agent-tmux.git ~/Code/opencode-agent-tmux
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
2. **Build the plugin:**
|
|
63
|
+
```bash
|
|
64
|
+
cd ~/Code/opencode-agent-tmux
|
|
65
|
+
bun install
|
|
66
|
+
bun run build
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
3. **Configure OpenCode:**
|
|
70
|
+
Add the plugin to your `~/.config/opencode/opencode.json`:
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"plugins": [
|
|
74
|
+
"~/Code/opencode-agent-tmux"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 🚀 Usage
|
|
80
|
+
|
|
81
|
+
### Easy Mode (Recommended)
|
|
82
|
+
|
|
83
|
+
After installation, just type:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
opencode
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The wrapper automatically:
|
|
90
|
+
- Launches tmux if you're not already in it
|
|
91
|
+
- Finds an available port (4096-4106) if default is in use
|
|
92
|
+
- Starts OpenCode with the available port
|
|
93
|
+
- Enables the plugin to spawn panes for agents
|
|
94
|
+
|
|
95
|
+
### Running Multiple Instances
|
|
96
|
+
|
|
97
|
+
Want to run multiple OpenCode sessions? No problem:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Terminal 1
|
|
101
|
+
opencode
|
|
102
|
+
# → Starts on port 4096
|
|
103
|
+
|
|
104
|
+
# Terminal 2
|
|
105
|
+
opencode
|
|
106
|
+
# → Detects 4096 in use, automatically uses port 4097
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Each instance works independently with its own tmux panes!
|
|
110
|
+
|
|
111
|
+
### Manual Mode
|
|
112
|
+
|
|
113
|
+
Or start OpenCode inside tmux manually:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
tmux
|
|
117
|
+
opencode --port 4096
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## ⚙️ Configuration
|
|
121
|
+
|
|
122
|
+
You can customize behavior by creating `~/.config/opencode/opencode-agent-tmux.json`:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"enabled": true,
|
|
127
|
+
"port": 4096,
|
|
128
|
+
"layout": "main-vertical",
|
|
129
|
+
"main_pane_size": 60,
|
|
130
|
+
"auto_close": true
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
| Option | Type | Default | Description |
|
|
135
|
+
|--------|------|---------|-------------|
|
|
136
|
+
| `enabled` | boolean | `true` | Enable/disable the plugin |
|
|
137
|
+
| `port` | number | `4096` | OpenCode server port |
|
|
138
|
+
| `layout` | string | `"main-vertical"` | Tmux layout: `main-horizontal`, `main-vertical`, `tiled`, etc. |
|
|
139
|
+
| `main_pane_size` | number | `60` | Size of main pane (20-80%) |
|
|
140
|
+
| `auto_close` | boolean | `true` | Auto-close panes when sessions complete |
|
|
141
|
+
|
|
142
|
+
## ❓ Troubleshooting
|
|
143
|
+
|
|
144
|
+
### Panes Not Spawning
|
|
145
|
+
1. Verify you're inside tmux: `echo $TMUX`
|
|
146
|
+
2. Check tmux is installed: `which tmux`
|
|
147
|
+
3. Check OpenCode server is running with port: `opencode --port 4096`
|
|
148
|
+
4. Check logs: `cat /tmp/opencode-agent-tmux.log`
|
|
149
|
+
|
|
150
|
+
### Server Not Found
|
|
151
|
+
Make sure OpenCode is started with the `--port` flag matching your config:
|
|
152
|
+
```bash
|
|
153
|
+
opencode --port 4096
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 📄 License
|
|
157
|
+
|
|
158
|
+
MIT
|
|
159
|
+
|
|
160
|
+
## 🙏 Acknowledgements
|
|
161
|
+
|
|
162
|
+
This project extracts and improves upon the tmux session management from [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) by alvinunreal.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn, execSync } from 'child_process';
|
|
4
|
+
import { createServer } from 'net';
|
|
5
|
+
import { env, platform, exit, argv } from 'process';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
|
|
10
|
+
const OPENCODE_PORT_START = parseInt(env.OPENCODE_PORT || '4096', 10);
|
|
11
|
+
const OPENCODE_PORT_MAX = OPENCODE_PORT_START + 10;
|
|
12
|
+
|
|
13
|
+
function findOpencodeBin() {
|
|
14
|
+
try {
|
|
15
|
+
const cmd = platform === 'win32' ? 'where opencode' : 'which -a opencode';
|
|
16
|
+
const output = execSync(cmd, { encoding: 'utf-8' }).trim().split('\n');
|
|
17
|
+
const currentScript = process.argv[1];
|
|
18
|
+
|
|
19
|
+
for (const bin of output) {
|
|
20
|
+
const normalizedBin = bin.trim();
|
|
21
|
+
if (normalizedBin.includes('opencode-tmux') || normalizedBin === currentScript) continue;
|
|
22
|
+
if (normalizedBin) return normalizedBin;
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const commonPaths = [
|
|
28
|
+
join(homedir(), '.opencode', 'bin', platform === 'win32' ? 'opencode.exe' : 'opencode'),
|
|
29
|
+
join(homedir(), 'AppData', 'Local', 'opencode', 'bin', 'opencode.exe'),
|
|
30
|
+
'/usr/local/bin/opencode',
|
|
31
|
+
'/usr/bin/opencode'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const p of commonPaths) {
|
|
35
|
+
if (existsSync(p)) return p;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function checkPort(port) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const server = createServer();
|
|
44
|
+
server.listen(port, '127.0.0.1');
|
|
45
|
+
server.on('listening', () => {
|
|
46
|
+
server.close();
|
|
47
|
+
resolve(true);
|
|
48
|
+
});
|
|
49
|
+
server.on('error', () => {
|
|
50
|
+
resolve(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function findAvailablePort() {
|
|
56
|
+
for (let port = OPENCODE_PORT_START; port <= OPENCODE_PORT_MAX; port++) {
|
|
57
|
+
if (await checkPort(port)) return port;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasTmux() {
|
|
63
|
+
try {
|
|
64
|
+
execSync('tmux -V', { stdio: 'ignore' });
|
|
65
|
+
return true;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function main() {
|
|
72
|
+
const opencodeBin = findOpencodeBin();
|
|
73
|
+
if (!opencodeBin) {
|
|
74
|
+
console.error("❌ Error: Could not find 'opencode' binary.");
|
|
75
|
+
console.error(" Please ensure OpenCode is installed and in your PATH.");
|
|
76
|
+
exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const port = await findAvailablePort();
|
|
80
|
+
if (!port) {
|
|
81
|
+
console.error("❌ No ports available in range " + OPENCODE_PORT_START + "-" + OPENCODE_PORT_MAX);
|
|
82
|
+
exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (port !== OPENCODE_PORT_START) {
|
|
86
|
+
console.warn(`⚠️ Port ${OPENCODE_PORT_START} is in use, using port ${port} instead`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
env.OPENCODE_PORT = port.toString();
|
|
90
|
+
const args = argv.slice(2);
|
|
91
|
+
const childArgs = ['--port', port.toString(), ...args];
|
|
92
|
+
|
|
93
|
+
const inTmux = !!env.TMUX;
|
|
94
|
+
const tmuxAvailable = hasTmux();
|
|
95
|
+
|
|
96
|
+
if (inTmux || !tmuxAvailable) {
|
|
97
|
+
const child = spawn(opencodeBin, childArgs, { stdio: 'inherit' });
|
|
98
|
+
child.on('close', (code) => exit(code));
|
|
99
|
+
|
|
100
|
+
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
101
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
102
|
+
|
|
103
|
+
} else {
|
|
104
|
+
console.log("🚀 Launching tmux session...");
|
|
105
|
+
|
|
106
|
+
const safeCommand = [
|
|
107
|
+
`"${opencodeBin}"`,
|
|
108
|
+
`--port ${port}`,
|
|
109
|
+
...args.map(a => `"${a}"`)
|
|
110
|
+
].join(' ');
|
|
111
|
+
|
|
112
|
+
const shellCommand = `${safeCommand} || { echo "Exit code: $?"; echo "Press Enter to close..."; read; }`;
|
|
113
|
+
|
|
114
|
+
const tmuxArgs = [
|
|
115
|
+
'new-session',
|
|
116
|
+
shellCommand
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const child = spawn('tmux', tmuxArgs, { stdio: 'inherit' });
|
|
120
|
+
child.on('close', (code) => exit(code));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main().catch(err => {
|
|
125
|
+
console.error(err);
|
|
126
|
+
exit(1);
|
|
127
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
interface PluginInput {
|
|
4
|
+
directory: string;
|
|
5
|
+
serverUrl?: URL | string;
|
|
6
|
+
client: {
|
|
7
|
+
session: {
|
|
8
|
+
status(): Promise<{
|
|
9
|
+
data?: Record<string, {
|
|
10
|
+
type: string;
|
|
11
|
+
}>;
|
|
12
|
+
}>;
|
|
13
|
+
subscribe(callback: (event: {
|
|
14
|
+
type: string;
|
|
15
|
+
properties?: unknown;
|
|
16
|
+
}) => void): () => void;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
interface PluginOutput {
|
|
21
|
+
name: string;
|
|
22
|
+
event?: (input: {
|
|
23
|
+
event: {
|
|
24
|
+
type: string;
|
|
25
|
+
properties?: unknown;
|
|
26
|
+
};
|
|
27
|
+
}) => Promise<void>;
|
|
28
|
+
tool?: Record<string, unknown>;
|
|
29
|
+
config?: unknown;
|
|
30
|
+
}
|
|
31
|
+
type Plugin = (ctx: PluginInput) => Promise<PluginOutput>;
|
|
32
|
+
|
|
33
|
+
declare const TmuxLayoutSchema: z.ZodEnum<["main-horizontal", "main-vertical", "tiled", "even-horizontal", "even-vertical"]>;
|
|
34
|
+
type TmuxLayout = z.infer<typeof TmuxLayoutSchema>;
|
|
35
|
+
declare const TmuxConfigSchema: z.ZodObject<{
|
|
36
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
37
|
+
layout: z.ZodDefault<z.ZodEnum<["main-horizontal", "main-vertical", "tiled", "even-horizontal", "even-vertical"]>>;
|
|
38
|
+
main_pane_size: z.ZodDefault<z.ZodNumber>;
|
|
39
|
+
}, "strip", z.ZodTypeAny, {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
layout: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical";
|
|
42
|
+
main_pane_size: number;
|
|
43
|
+
}, {
|
|
44
|
+
enabled?: boolean | undefined;
|
|
45
|
+
layout?: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical" | undefined;
|
|
46
|
+
main_pane_size?: number | undefined;
|
|
47
|
+
}>;
|
|
48
|
+
type TmuxConfig = z.infer<typeof TmuxConfigSchema>;
|
|
49
|
+
declare const PluginConfigSchema: z.ZodObject<{
|
|
50
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
51
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
52
|
+
layout: z.ZodDefault<z.ZodEnum<["main-horizontal", "main-vertical", "tiled", "even-horizontal", "even-vertical"]>>;
|
|
53
|
+
main_pane_size: z.ZodDefault<z.ZodNumber>;
|
|
54
|
+
auto_close: z.ZodDefault<z.ZodBoolean>;
|
|
55
|
+
}, "strip", z.ZodTypeAny, {
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
port: number;
|
|
58
|
+
layout: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical";
|
|
59
|
+
main_pane_size: number;
|
|
60
|
+
auto_close: boolean;
|
|
61
|
+
}, {
|
|
62
|
+
enabled?: boolean | undefined;
|
|
63
|
+
port?: number | undefined;
|
|
64
|
+
layout?: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical" | undefined;
|
|
65
|
+
main_pane_size?: number | undefined;
|
|
66
|
+
auto_close?: boolean | undefined;
|
|
67
|
+
}>;
|
|
68
|
+
type PluginConfig = z.infer<typeof PluginConfigSchema>;
|
|
69
|
+
|
|
70
|
+
declare const OpencodeAgentTmux: Plugin;
|
|
71
|
+
|
|
72
|
+
export { type PluginConfig, type TmuxConfig, type TmuxLayout, OpencodeAgentTmux as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as fs2 from "fs";
|
|
3
|
+
import * as path2 from "path";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
var TmuxLayoutSchema = z.enum([
|
|
8
|
+
"main-horizontal",
|
|
9
|
+
"main-vertical",
|
|
10
|
+
"tiled",
|
|
11
|
+
"even-horizontal",
|
|
12
|
+
"even-vertical"
|
|
13
|
+
]);
|
|
14
|
+
var TmuxConfigSchema = z.object({
|
|
15
|
+
enabled: z.boolean().default(true),
|
|
16
|
+
layout: TmuxLayoutSchema.default("main-vertical"),
|
|
17
|
+
main_pane_size: z.number().min(20).max(80).default(60)
|
|
18
|
+
});
|
|
19
|
+
var PluginConfigSchema = z.object({
|
|
20
|
+
enabled: z.boolean().default(true),
|
|
21
|
+
port: z.number().default(4096),
|
|
22
|
+
layout: TmuxLayoutSchema.default("main-vertical"),
|
|
23
|
+
main_pane_size: z.number().min(20).max(80).default(60),
|
|
24
|
+
auto_close: z.boolean().default(true)
|
|
25
|
+
});
|
|
26
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
27
|
+
var SESSION_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
28
|
+
var SESSION_MISSING_GRACE_MS = POLL_INTERVAL_MS * 3;
|
|
29
|
+
|
|
30
|
+
// src/utils/logger.ts
|
|
31
|
+
import * as fs from "fs";
|
|
32
|
+
import * as os from "os";
|
|
33
|
+
import * as path from "path";
|
|
34
|
+
var logFile = path.join(os.tmpdir(), "opencode-agent-tmux.log");
|
|
35
|
+
function log(message, data) {
|
|
36
|
+
try {
|
|
37
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
38
|
+
const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
|
|
39
|
+
`;
|
|
40
|
+
fs.appendFileSync(logFile, logEntry);
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/utils/tmux.ts
|
|
46
|
+
import { spawn } from "child_process";
|
|
47
|
+
var tmuxPath = null;
|
|
48
|
+
var tmuxChecked = false;
|
|
49
|
+
var storedConfig = null;
|
|
50
|
+
var serverAvailable = null;
|
|
51
|
+
var serverCheckUrl = null;
|
|
52
|
+
async function spawnAsync(command, options) {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const [cmd, ...args] = command;
|
|
55
|
+
const proc = spawn(cmd, args, { stdio: "pipe" });
|
|
56
|
+
let stdout = "";
|
|
57
|
+
let stderr = "";
|
|
58
|
+
if (!options?.ignoreOutput) {
|
|
59
|
+
proc.stdout?.on("data", (data) => {
|
|
60
|
+
stdout += data.toString();
|
|
61
|
+
});
|
|
62
|
+
proc.stderr?.on("data", (data) => {
|
|
63
|
+
stderr += data.toString();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
proc.on("close", (code) => {
|
|
67
|
+
resolve({
|
|
68
|
+
exitCode: code ?? 1,
|
|
69
|
+
stdout,
|
|
70
|
+
stderr
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
proc.on("error", () => {
|
|
74
|
+
resolve({
|
|
75
|
+
exitCode: 1,
|
|
76
|
+
stdout,
|
|
77
|
+
stderr
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function isServerRunning(serverUrl) {
|
|
83
|
+
if (serverCheckUrl === serverUrl && serverAvailable === true) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const healthUrl = new URL("/health", serverUrl).toString();
|
|
87
|
+
const timeoutMs = 3e3;
|
|
88
|
+
const maxAttempts = 2;
|
|
89
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
92
|
+
let response = null;
|
|
93
|
+
try {
|
|
94
|
+
response = await fetch(healthUrl, { signal: controller.signal }).catch(
|
|
95
|
+
() => null
|
|
96
|
+
);
|
|
97
|
+
} finally {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
}
|
|
100
|
+
const available = response?.ok ?? false;
|
|
101
|
+
if (available) {
|
|
102
|
+
serverCheckUrl = serverUrl;
|
|
103
|
+
serverAvailable = true;
|
|
104
|
+
log("[tmux] isServerRunning: checked", { serverUrl, available, attempt });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (attempt < maxAttempts) {
|
|
108
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
log("[tmux] isServerRunning: checked", { serverUrl, available: false });
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
async function findTmuxPath() {
|
|
115
|
+
const isWindows = process.platform === "win32";
|
|
116
|
+
const cmd = isWindows ? "where" : "which";
|
|
117
|
+
try {
|
|
118
|
+
const result = await spawnAsync([cmd, "tmux"]);
|
|
119
|
+
if (result.exitCode !== 0) {
|
|
120
|
+
log("[tmux] findTmuxPath: 'which tmux' failed", {
|
|
121
|
+
exitCode: result.exitCode
|
|
122
|
+
});
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const path3 = result.stdout.trim().split("\n")[0];
|
|
126
|
+
if (!path3) {
|
|
127
|
+
log("[tmux] findTmuxPath: no path in output");
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const verifyResult = await spawnAsync([path3, "-V"]);
|
|
131
|
+
if (verifyResult.exitCode !== 0) {
|
|
132
|
+
log("[tmux] findTmuxPath: tmux -V failed", {
|
|
133
|
+
path: path3,
|
|
134
|
+
verifyExit: verifyResult.exitCode
|
|
135
|
+
});
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
log("[tmux] findTmuxPath: found tmux", { path: path3 });
|
|
139
|
+
return path3;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
log("[tmux] findTmuxPath: exception", { error: String(err) });
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function getTmuxPath() {
|
|
146
|
+
if (tmuxChecked) {
|
|
147
|
+
return tmuxPath;
|
|
148
|
+
}
|
|
149
|
+
tmuxPath = await findTmuxPath();
|
|
150
|
+
tmuxChecked = true;
|
|
151
|
+
log("[tmux] getTmuxPath: initialized", { tmuxPath });
|
|
152
|
+
return tmuxPath;
|
|
153
|
+
}
|
|
154
|
+
function isInsideTmux() {
|
|
155
|
+
return !!process.env.TMUX;
|
|
156
|
+
}
|
|
157
|
+
async function applyLayout(tmux, layout, mainPaneSize) {
|
|
158
|
+
try {
|
|
159
|
+
await spawnAsync([tmux, "select-layout", layout]);
|
|
160
|
+
if (layout === "main-horizontal" || layout === "main-vertical") {
|
|
161
|
+
const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
|
|
162
|
+
await spawnAsync([
|
|
163
|
+
tmux,
|
|
164
|
+
"set-window-option",
|
|
165
|
+
sizeOption,
|
|
166
|
+
`${mainPaneSize}%`
|
|
167
|
+
]);
|
|
168
|
+
await spawnAsync([tmux, "select-layout", layout]);
|
|
169
|
+
}
|
|
170
|
+
log("[tmux] applyLayout: applied", { layout, mainPaneSize });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
log("[tmux] applyLayout: exception", { error: String(err) });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function spawnTmuxPane(sessionId, description, config, serverUrl) {
|
|
176
|
+
log("[tmux] spawnTmuxPane called", {
|
|
177
|
+
sessionId,
|
|
178
|
+
description,
|
|
179
|
+
config,
|
|
180
|
+
serverUrl
|
|
181
|
+
});
|
|
182
|
+
if (!config.enabled) {
|
|
183
|
+
log("[tmux] spawnTmuxPane: config.enabled is false, skipping");
|
|
184
|
+
return { success: false };
|
|
185
|
+
}
|
|
186
|
+
if (!isInsideTmux()) {
|
|
187
|
+
log("[tmux] spawnTmuxPane: not inside tmux, skipping");
|
|
188
|
+
return { success: false };
|
|
189
|
+
}
|
|
190
|
+
const serverRunning = await isServerRunning(serverUrl);
|
|
191
|
+
if (!serverRunning) {
|
|
192
|
+
const defaultPort = process.env.OPENCODE_PORT ?? "4096";
|
|
193
|
+
log("[tmux] spawnTmuxPane: OpenCode server not running, skipping", {
|
|
194
|
+
serverUrl,
|
|
195
|
+
hint: `Start opencode with --port ${defaultPort}`
|
|
196
|
+
});
|
|
197
|
+
return { success: false };
|
|
198
|
+
}
|
|
199
|
+
const tmux = await getTmuxPath();
|
|
200
|
+
if (!tmux) {
|
|
201
|
+
log("[tmux] spawnTmuxPane: tmux binary not found, skipping");
|
|
202
|
+
return { success: false };
|
|
203
|
+
}
|
|
204
|
+
storedConfig = config;
|
|
205
|
+
try {
|
|
206
|
+
const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`;
|
|
207
|
+
const args = [
|
|
208
|
+
"split-window",
|
|
209
|
+
"-h",
|
|
210
|
+
"-d",
|
|
211
|
+
"-P",
|
|
212
|
+
"-F",
|
|
213
|
+
"#{pane_id}",
|
|
214
|
+
opencodeCmd
|
|
215
|
+
];
|
|
216
|
+
log("[tmux] spawnTmuxPane: executing", { tmux, args, opencodeCmd });
|
|
217
|
+
const result = await spawnAsync([tmux, ...args]);
|
|
218
|
+
const paneId = result.stdout.trim();
|
|
219
|
+
log("[tmux] spawnTmuxPane: split result", {
|
|
220
|
+
exitCode: result.exitCode,
|
|
221
|
+
paneId,
|
|
222
|
+
stderr: result.stderr.trim()
|
|
223
|
+
});
|
|
224
|
+
if (result.exitCode === 0 && paneId) {
|
|
225
|
+
await spawnAsync(
|
|
226
|
+
[tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)],
|
|
227
|
+
{ ignoreOutput: true }
|
|
228
|
+
);
|
|
229
|
+
const layout = config.layout ?? "main-vertical";
|
|
230
|
+
const mainPaneSize = config.main_pane_size ?? 60;
|
|
231
|
+
await applyLayout(tmux, layout, mainPaneSize);
|
|
232
|
+
log("[tmux] spawnTmuxPane: SUCCESS, pane created and layout applied", {
|
|
233
|
+
paneId,
|
|
234
|
+
layout
|
|
235
|
+
});
|
|
236
|
+
return { success: true, paneId };
|
|
237
|
+
}
|
|
238
|
+
return { success: false };
|
|
239
|
+
} catch (err) {
|
|
240
|
+
log("[tmux] spawnTmuxPane: exception", { error: String(err) });
|
|
241
|
+
return { success: false };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function closeTmuxPane(paneId) {
|
|
245
|
+
log("[tmux] closeTmuxPane called", { paneId });
|
|
246
|
+
if (!paneId) {
|
|
247
|
+
log("[tmux] closeTmuxPane: no paneId provided");
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
const tmux = await getTmuxPath();
|
|
251
|
+
if (!tmux) {
|
|
252
|
+
log("[tmux] closeTmuxPane: tmux binary not found");
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const result = await spawnAsync([tmux, "kill-pane", "-t", paneId]);
|
|
257
|
+
log("[tmux] closeTmuxPane: result", {
|
|
258
|
+
exitCode: result.exitCode,
|
|
259
|
+
stderr: result.stderr.trim()
|
|
260
|
+
});
|
|
261
|
+
if (result.exitCode === 0) {
|
|
262
|
+
log("[tmux] closeTmuxPane: SUCCESS, pane closed", { paneId });
|
|
263
|
+
if (storedConfig) {
|
|
264
|
+
const layout = storedConfig.layout ?? "main-vertical";
|
|
265
|
+
const mainPaneSize = storedConfig.main_pane_size ?? 60;
|
|
266
|
+
await applyLayout(tmux, layout, mainPaneSize);
|
|
267
|
+
log("[tmux] closeTmuxPane: layout reapplied", { layout });
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
log("[tmux] closeTmuxPane: failed (pane may already be closed)", {
|
|
272
|
+
paneId
|
|
273
|
+
});
|
|
274
|
+
return false;
|
|
275
|
+
} catch (err) {
|
|
276
|
+
log("[tmux] closeTmuxPane: exception", { error: String(err) });
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function startTmuxCheck() {
|
|
281
|
+
if (!tmuxChecked) {
|
|
282
|
+
getTmuxPath().catch(() => {
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/tmux-session-manager.ts
|
|
288
|
+
var TmuxSessionManager = class {
|
|
289
|
+
client;
|
|
290
|
+
tmuxConfig;
|
|
291
|
+
serverUrl;
|
|
292
|
+
sessions = /* @__PURE__ */ new Map();
|
|
293
|
+
pollInterval;
|
|
294
|
+
enabled = false;
|
|
295
|
+
constructor(ctx, tmuxConfig, serverUrl) {
|
|
296
|
+
this.client = ctx.client;
|
|
297
|
+
this.tmuxConfig = tmuxConfig;
|
|
298
|
+
this.serverUrl = serverUrl;
|
|
299
|
+
this.enabled = tmuxConfig.enabled && isInsideTmux();
|
|
300
|
+
log("[tmux-session-manager] initialized", {
|
|
301
|
+
enabled: this.enabled,
|
|
302
|
+
tmuxConfig: this.tmuxConfig,
|
|
303
|
+
serverUrl: this.serverUrl
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
async onSessionCreated(event) {
|
|
307
|
+
if (!this.enabled) return;
|
|
308
|
+
if (event.type !== "session.created") return;
|
|
309
|
+
const info = event.properties?.info;
|
|
310
|
+
if (!info?.id || !info?.parentID) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const sessionId = info.id;
|
|
314
|
+
const parentId = info.parentID;
|
|
315
|
+
const title = info.title ?? "Subagent";
|
|
316
|
+
if (this.sessions.has(sessionId)) {
|
|
317
|
+
log("[tmux-session-manager] session already tracked", { sessionId });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
log("[tmux-session-manager] child session created, spawning pane", {
|
|
321
|
+
sessionId,
|
|
322
|
+
parentId,
|
|
323
|
+
title
|
|
324
|
+
});
|
|
325
|
+
const paneResult = await spawnTmuxPane(
|
|
326
|
+
sessionId,
|
|
327
|
+
title,
|
|
328
|
+
this.tmuxConfig,
|
|
329
|
+
this.serverUrl
|
|
330
|
+
).catch((err) => {
|
|
331
|
+
log("[tmux-session-manager] failed to spawn pane", {
|
|
332
|
+
error: String(err)
|
|
333
|
+
});
|
|
334
|
+
return { success: false, paneId: void 0 };
|
|
335
|
+
});
|
|
336
|
+
if (paneResult.success && paneResult.paneId) {
|
|
337
|
+
const now = Date.now();
|
|
338
|
+
this.sessions.set(sessionId, {
|
|
339
|
+
sessionId,
|
|
340
|
+
paneId: paneResult.paneId,
|
|
341
|
+
parentId,
|
|
342
|
+
title,
|
|
343
|
+
createdAt: now,
|
|
344
|
+
lastSeenAt: now
|
|
345
|
+
});
|
|
346
|
+
log("[tmux-session-manager] pane spawned", {
|
|
347
|
+
sessionId,
|
|
348
|
+
paneId: paneResult.paneId
|
|
349
|
+
});
|
|
350
|
+
this.startPolling();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
startPolling() {
|
|
354
|
+
if (this.pollInterval) return;
|
|
355
|
+
this.pollInterval = setInterval(
|
|
356
|
+
() => this.pollSessions(),
|
|
357
|
+
POLL_INTERVAL_MS
|
|
358
|
+
);
|
|
359
|
+
log("[tmux-session-manager] polling started");
|
|
360
|
+
}
|
|
361
|
+
stopPolling() {
|
|
362
|
+
if (this.pollInterval) {
|
|
363
|
+
clearInterval(this.pollInterval);
|
|
364
|
+
this.pollInterval = void 0;
|
|
365
|
+
log("[tmux-session-manager] polling stopped");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async pollSessions() {
|
|
369
|
+
if (this.sessions.size === 0) {
|
|
370
|
+
this.stopPolling();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const statusResult = await this.client.session.status();
|
|
375
|
+
const allStatuses = statusResult.data ?? {};
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
const sessionsToClose = [];
|
|
378
|
+
for (const [sessionId, tracked] of this.sessions.entries()) {
|
|
379
|
+
const status = allStatuses[sessionId];
|
|
380
|
+
const isIdle = status?.type === "idle";
|
|
381
|
+
if (status) {
|
|
382
|
+
tracked.lastSeenAt = now;
|
|
383
|
+
tracked.missingSince = void 0;
|
|
384
|
+
} else if (!tracked.missingSince) {
|
|
385
|
+
tracked.missingSince = now;
|
|
386
|
+
}
|
|
387
|
+
const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
|
|
388
|
+
const isTimedOut = now - tracked.createdAt > SESSION_TIMEOUT_MS;
|
|
389
|
+
if (isIdle || missingTooLong || isTimedOut) {
|
|
390
|
+
sessionsToClose.push(sessionId);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (const sessionId of sessionsToClose) {
|
|
394
|
+
await this.closeSession(sessionId);
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
log("[tmux-session-manager] poll error", { error: String(err) });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async closeSession(sessionId) {
|
|
401
|
+
const tracked = this.sessions.get(sessionId);
|
|
402
|
+
if (!tracked) return;
|
|
403
|
+
log("[tmux-session-manager] closing session pane", {
|
|
404
|
+
sessionId,
|
|
405
|
+
paneId: tracked.paneId
|
|
406
|
+
});
|
|
407
|
+
await closeTmuxPane(tracked.paneId);
|
|
408
|
+
this.sessions.delete(sessionId);
|
|
409
|
+
if (this.sessions.size === 0) {
|
|
410
|
+
this.stopPolling();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
createEventHandler() {
|
|
414
|
+
return async (input) => {
|
|
415
|
+
await this.onSessionCreated(input.event);
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async cleanup() {
|
|
419
|
+
this.stopPolling();
|
|
420
|
+
if (this.sessions.size > 0) {
|
|
421
|
+
log("[tmux-session-manager] closing all panes", {
|
|
422
|
+
count: this.sessions.size
|
|
423
|
+
});
|
|
424
|
+
const closePromises = Array.from(this.sessions.values()).map(
|
|
425
|
+
(s) => closeTmuxPane(s.paneId).catch(
|
|
426
|
+
(err) => log("[tmux-session-manager] cleanup error for pane", {
|
|
427
|
+
paneId: s.paneId,
|
|
428
|
+
error: String(err)
|
|
429
|
+
})
|
|
430
|
+
)
|
|
431
|
+
);
|
|
432
|
+
await Promise.all(closePromises);
|
|
433
|
+
this.sessions.clear();
|
|
434
|
+
}
|
|
435
|
+
log("[tmux-session-manager] cleanup complete");
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// src/index.ts
|
|
440
|
+
function detectServerUrl() {
|
|
441
|
+
if (process.env.OPENCODE_PORT) {
|
|
442
|
+
return `http://localhost:${process.env.OPENCODE_PORT}`;
|
|
443
|
+
}
|
|
444
|
+
return "http://localhost:4096";
|
|
445
|
+
}
|
|
446
|
+
function loadConfig(directory) {
|
|
447
|
+
const configPaths = [
|
|
448
|
+
path2.join(directory, "opencode-agent-tmux.json"),
|
|
449
|
+
path2.join(
|
|
450
|
+
process.env.HOME ?? "",
|
|
451
|
+
".config",
|
|
452
|
+
"opencode",
|
|
453
|
+
"opencode-agent-tmux.json"
|
|
454
|
+
)
|
|
455
|
+
];
|
|
456
|
+
for (const configPath of configPaths) {
|
|
457
|
+
try {
|
|
458
|
+
if (fs2.existsSync(configPath)) {
|
|
459
|
+
const content = fs2.readFileSync(configPath, "utf-8");
|
|
460
|
+
const parsed = JSON.parse(content);
|
|
461
|
+
const result = PluginConfigSchema.safeParse(parsed);
|
|
462
|
+
if (result.success) {
|
|
463
|
+
log("[plugin] loaded config", { configPath, config: result.data });
|
|
464
|
+
return result.data;
|
|
465
|
+
}
|
|
466
|
+
log("[plugin] config parse error", {
|
|
467
|
+
configPath,
|
|
468
|
+
error: result.error.message
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
} catch (err) {
|
|
472
|
+
log("[plugin] config load error", { configPath, error: String(err) });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const defaultConfig = PluginConfigSchema.parse({});
|
|
476
|
+
log("[plugin] using default config", { config: defaultConfig });
|
|
477
|
+
return defaultConfig;
|
|
478
|
+
}
|
|
479
|
+
var OpencodeAgentTmux = async (ctx) => {
|
|
480
|
+
const config = loadConfig(ctx.directory);
|
|
481
|
+
const tmuxConfig = {
|
|
482
|
+
enabled: config.enabled,
|
|
483
|
+
layout: config.layout,
|
|
484
|
+
main_pane_size: config.main_pane_size
|
|
485
|
+
};
|
|
486
|
+
const serverUrl = ctx.serverUrl?.toString() || detectServerUrl();
|
|
487
|
+
log("[plugin] initialized", {
|
|
488
|
+
tmuxConfig,
|
|
489
|
+
directory: ctx.directory,
|
|
490
|
+
serverUrl
|
|
491
|
+
});
|
|
492
|
+
if (tmuxConfig.enabled) {
|
|
493
|
+
startTmuxCheck();
|
|
494
|
+
}
|
|
495
|
+
const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig, serverUrl);
|
|
496
|
+
return {
|
|
497
|
+
name: "opencode-agent-tmux",
|
|
498
|
+
event: async (input) => {
|
|
499
|
+
await tmuxSessionManager.onSessionCreated(
|
|
500
|
+
input.event
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
var index_default = OpencodeAgentTmux;
|
|
506
|
+
export {
|
|
507
|
+
index_default as default
|
|
508
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-agent-tmux",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenCode plugin that provides tmux integration for viewing agent execution in real-time",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"opencode-tmux": "./bin/opencode-tmux.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"bin",
|
|
14
|
+
"scripts"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
18
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"prepublishOnly": "bun run build",
|
|
21
|
+
"postinstall": "node scripts/install.js"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"opencode",
|
|
25
|
+
"opencode-plugin",
|
|
26
|
+
"tmux",
|
|
27
|
+
"agent"
|
|
28
|
+
],
|
|
29
|
+
"author": "",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"zod": "^3.24.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/bun": "^1.2.4",
|
|
36
|
+
"tsup": "^8.3.6",
|
|
37
|
+
"typescript": "^5.7.3"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const HOME = os.homedir();
|
|
8
|
+
|
|
9
|
+
function detectShell() {
|
|
10
|
+
const shell = process.env.SHELL || '';
|
|
11
|
+
const platform = process.platform;
|
|
12
|
+
|
|
13
|
+
if (platform === 'win32') {
|
|
14
|
+
const documents = path.join(HOME, 'Documents');
|
|
15
|
+
const psDir = path.join(documents, 'PowerShell');
|
|
16
|
+
const psProfile = path.join(psDir, 'Microsoft.PowerShell_profile.ps1');
|
|
17
|
+
return {
|
|
18
|
+
name: 'powershell',
|
|
19
|
+
rcFile: psProfile,
|
|
20
|
+
dir: psDir
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (shell.includes('zsh')) {
|
|
25
|
+
return { name: 'zsh', rcFile: path.join(HOME, '.zshrc') };
|
|
26
|
+
} else if (shell.includes('bash')) {
|
|
27
|
+
const bashProfile = path.join(HOME, '.bash_profile');
|
|
28
|
+
const bashrc = path.join(HOME, '.bashrc');
|
|
29
|
+
return {
|
|
30
|
+
name: 'bash',
|
|
31
|
+
rcFile: fs.existsSync(bashProfile) ? bashProfile : bashrc
|
|
32
|
+
};
|
|
33
|
+
} else if (shell.includes('fish')) {
|
|
34
|
+
return {
|
|
35
|
+
name: 'fish',
|
|
36
|
+
rcFile: path.join(HOME, '.config', 'fish', 'config.fish')
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { name: 'unknown', rcFile: path.join(HOME, '.profile') };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getAliasContent(shellName) {
|
|
44
|
+
if (shellName === 'powershell') {
|
|
45
|
+
return `
|
|
46
|
+
function opencode {
|
|
47
|
+
opencode-tmux $args
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return `alias opencode='opencode-tmux'`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getExportLine() {
|
|
56
|
+
return `export OPENCODE_PORT=4096`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function setupAlias() {
|
|
60
|
+
const shell = detectShell();
|
|
61
|
+
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log('🔧 Setting up opencode-agent-tmux auto-launcher...');
|
|
64
|
+
console.log(` Detected shell: ${shell.name}`);
|
|
65
|
+
console.log(` Config file: ${shell.rcFile}`);
|
|
66
|
+
|
|
67
|
+
if (shell.name === 'powershell') {
|
|
68
|
+
if (!fs.existsSync(shell.dir)) {
|
|
69
|
+
fs.mkdirSync(shell.dir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(shell.rcFile)) {
|
|
74
|
+
console.log(` Creating ${shell.rcFile}...`);
|
|
75
|
+
fs.writeFileSync(shell.rcFile, '', 'utf-8');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let rcContent = fs.readFileSync(shell.rcFile, 'utf-8');
|
|
79
|
+
const aliasContent = getAliasContent(shell.name);
|
|
80
|
+
|
|
81
|
+
const MARKER_START = '# >>> opencode-agent-tmux >>>';
|
|
82
|
+
const MARKER_END = '# <<< opencode-agent-tmux <<<';
|
|
83
|
+
|
|
84
|
+
const OLD_MARKER_START = '# >>> opencode-subagent-tmux >>>';
|
|
85
|
+
const OLD_MARKER_END = '# <<< opencode-subagent-tmux <<<';
|
|
86
|
+
|
|
87
|
+
if (rcContent.includes(OLD_MARKER_START)) {
|
|
88
|
+
console.log(' Removing old opencode-subagent-tmux alias...');
|
|
89
|
+
const regex = new RegExp(`${OLD_MARKER_START}[\\s\\S]*?${OLD_MARKER_END}\\n?`, 'g');
|
|
90
|
+
rcContent = rcContent.replace(regex, '');
|
|
91
|
+
fs.writeFileSync(shell.rcFile, rcContent, 'utf-8');
|
|
92
|
+
console.log(' ✓ Removed old alias');
|
|
93
|
+
rcContent = fs.readFileSync(shell.rcFile, 'utf-8');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (rcContent.includes(MARKER_START)) {
|
|
97
|
+
console.log(' ✓ Auto-launcher already configured');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let configBlock = '';
|
|
102
|
+
if (shell.name === 'powershell') {
|
|
103
|
+
configBlock = `
|
|
104
|
+
${MARKER_START}
|
|
105
|
+
$env:OPENCODE_PORT="4096"
|
|
106
|
+
${aliasContent}
|
|
107
|
+
${MARKER_END}
|
|
108
|
+
`;
|
|
109
|
+
} else {
|
|
110
|
+
configBlock = `
|
|
111
|
+
${MARKER_START}
|
|
112
|
+
${getExportLine()}
|
|
113
|
+
${aliasContent}
|
|
114
|
+
${MARKER_END}
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fs.appendFileSync(shell.rcFile, configBlock);
|
|
119
|
+
|
|
120
|
+
console.log(' ✓ Auto-launcher configured successfully!');
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(' To activate now:');
|
|
123
|
+
if (shell.name === 'powershell') {
|
|
124
|
+
console.log(` . ${shell.rcFile}`);
|
|
125
|
+
} else {
|
|
126
|
+
console.log(` source ${shell.rcFile}`);
|
|
127
|
+
}
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(' Or restart your terminal.');
|
|
130
|
+
console.log('');
|
|
131
|
+
console.log(' Usage: Just type "opencode" and tmux + port 4096 will be auto-configured!');
|
|
132
|
+
console.log('');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
setupAlias();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('');
|
|
139
|
+
console.error('⚠️ Failed to auto-configure shell alias:', error.message);
|
|
140
|
+
console.error('');
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|