opencode-cockpit 0.1.4 → 0.2.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 +141 -32
- package/dist/compose.js +38 -0
- package/dist/features.js +23 -0
- package/dist/server.js +17 -0
- package/dist/tui.js +14 -0
- package/package.json +13 -6
- package/types/compose.d.ts +7 -0
- package/types/features.d.ts +17 -0
- package/types/server.d.ts +5 -0
- package/types/tui.d.ts +5 -0
- package/src/compose.ts +0 -43
- package/src/features.ts +0 -34
- package/src/server.ts +0 -16
- package/src/tui.ts +0 -13
package/README.md
CHANGED
|
@@ -1,15 +1,106 @@
|
|
|
1
1
|
# opencode-cockpit
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Codestz/opencode-cockpit/actions/workflows/ci.yml)
|
|
4
|
-
[](https://www.npmjs.com/package/opencode-cockpit)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-cockpit)
|
|
5
|
+
[](https://www.npmjs.com/package/@opencode-cockpit/shell)
|
|
5
6
|
[](https://github.com/Codestz/opencode-cockpit/blob/main/LICENSE)
|
|
6
7
|
|
|
7
|
-
Superpowers for [OpenCode](https://opencode.ai)
|
|
8
|
+
**Superpowers for [OpenCode](https://opencode.ai) — take all of them, or just the one you need.**
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Coding agents are stuck in a one-command-at-a-time world: they run something, wait for it to
|
|
11
|
+
finish, and paste the whole log back into their context. Cockpit gives your agent the things a
|
|
12
|
+
developer actually has — long-running terminals, a way to wait for "ready", and output it can read
|
|
13
|
+
without drowning in it — and gives *you* a live view of all of it, inside OpenCode.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
────────────────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
Shells ⠹ RUN dev server FAIL unit tests ▸ 3 more ctrl+x i console · ctrl+x o hide
|
|
18
|
+
VITE v7.3.1 ready in 431 ms
|
|
19
|
+
➜ Local: http://localhost:5173/
|
|
20
|
+
✓ 142 modules transformed
|
|
21
|
+
2m14s sh_k4tq8b2p · $ npm run dev
|
|
22
|
+
────────────────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
| Feature | What your agent gains | Package |
|
|
10
28
|
|---|---|---|
|
|
11
|
-
| **Shell** | Background terminals
|
|
12
|
-
| **Agents** *(
|
|
29
|
+
| **Shell** | Background terminals it starts, waits on, reads and types into — dev servers, watchers, test suites, REPLs | [`@opencode-cockpit/shell`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/shell) |
|
|
30
|
+
| **Agents** *(next)* | A live, keyboard-first view of every subagent, without leaving your chat | `@opencode-cockpit/agents` |
|
|
31
|
+
|
|
32
|
+
## Shell, by example
|
|
33
|
+
|
|
34
|
+
**"Start the dev server and wait until it's actually ready."**
|
|
35
|
+
The agent starts it in a real terminal and blocks on the port opening — not a guess, not a sleep:
|
|
36
|
+
```
|
|
37
|
+
shell_start command="npm run dev" waitFor={ port: 5173 }
|
|
38
|
+
→ condition met: port is accepting connections
|
|
39
|
+
1| VITE v7.3.1 ready in 431 ms
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**"Run the tests, keep working, tell me if they fail."**
|
|
43
|
+
The suite runs in the background. When it exits, the agent is messaged once, with the error line
|
|
44
|
+
already picked out:
|
|
45
|
+
```
|
|
46
|
+
<shell_exited id="sh_9wq2f1ab" title="unit tests">
|
|
47
|
+
exited with code 1 after 48s
|
|
48
|
+
last output: 37| FAIL src/auth.test.ts > refresh token expiry
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**"How is DB Monitoring doing?"**
|
|
52
|
+
Shells are shared across sessions and can be addressed by name:
|
|
53
|
+
```
|
|
54
|
+
shell_read name="DB Monitoring"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Logs that don't eat your context.** Colour codes stripped, progress-bar redraws collapsed to
|
|
58
|
+
their final frame, repeated lines folded to `(×12)`, and every read returns a cursor so the next
|
|
59
|
+
one only brings what's new. For full-screen programs (`vitest --ui`, `htop`, prompts) the agent can
|
|
60
|
+
ask for the *screen* instead of the log.
|
|
61
|
+
|
|
62
|
+
**It notices breakage on its own.** Watch a process that never exits and the agent hears only about
|
|
63
|
+
changes, never about a thousand identical recompiles:
|
|
64
|
+
```
|
|
65
|
+
shell_start command="tsc --watch --noEmit" description="type checker" watch=true
|
|
66
|
+
→ tsc: ok → fail · src/auth.ts(42,3): error TS2339: Property 'id' does not exist
|
|
67
|
+
```
|
|
68
|
+
Presets cover about 35 tools (tsc, vitest, jest, eslint, cargo, go, gradle, pytest, vite, next,
|
|
69
|
+
docker compose…), and anything else takes three regexes of its own. A watched process that dies
|
|
70
|
+
counts as a failure, so a crashed dev server is reported too.
|
|
71
|
+
|
|
72
|
+
**You can find things in a huge log.** `/` in the console filters the scrollback to matching lines,
|
|
73
|
+
keeping line numbers and highlighting matches — and output keeps the colours the program printed.
|
|
74
|
+
|
|
75
|
+
**It can type.** Prompts, REPLs, migration wizards: `shell_send` sends text or named keys
|
|
76
|
+
(`ctrl+c`, `up`, `enter`) and returns whatever the program printed back.
|
|
77
|
+
|
|
78
|
+
### Why not just `bash`?
|
|
79
|
+
|
|
80
|
+
| | Built-in `bash` tool | Cockpit Shell |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| Long-running processes | Blocks until exit | Runs in the background, survives the turn |
|
|
83
|
+
| Knowing something is ready | Guess, or sleep and poll | Blocks on a port, a pattern, silence or exit |
|
|
84
|
+
| Interactive programs | Not possible (no TTY) | Real PTY: prompts, REPLs, ctrl+c |
|
|
85
|
+
| Reading output | Whole log, every time | Clean lines from a cursor, with grep |
|
|
86
|
+
| Noticing a break later | Never | Watchers report health changes |
|
|
87
|
+
| Your visibility | None until it finishes | Live panel, console and sidebar |
|
|
88
|
+
| After OpenCode restarts | Gone | Still running |
|
|
89
|
+
|
|
90
|
+
## For you, not just the agent
|
|
91
|
+
|
|
92
|
+
| Key / command | Does |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `ctrl+x o` · `/shells` | Toggle the shells panel under the chat |
|
|
95
|
+
| `ctrl+x i` · `/shell` | Open the shell console |
|
|
96
|
+
| `/shell-new` | Start a shell yourself |
|
|
97
|
+
| `/shells-clear` | Remove finished shells |
|
|
98
|
+
| `/cockpit-update` | Update the plugin when a newer release exists |
|
|
99
|
+
|
|
100
|
+
Status reads the same everywhere — `RUN` (with a spinner), `FAIL`, `STOP`, `DONE` — running shells
|
|
101
|
+
and recent failures stay in view, the rest folds behind `▸ N more`. In the console: `i` types
|
|
102
|
+
straight into the program (`ctrl+]` to stop), `c` sends ctrl+c, `r` restarts, `x` stops, `tab`
|
|
103
|
+
switches between the live screen and the scrollback, `?` shows details.
|
|
13
104
|
|
|
14
105
|
## Install
|
|
15
106
|
|
|
@@ -25,34 +116,36 @@ opencode plugin opencode-cockpit --global
|
|
|
25
116
|
opencode plugin @opencode-cockpit/shell --global
|
|
26
117
|
```
|
|
27
118
|
|
|
28
|
-
|
|
29
|
-
and
|
|
119
|
+
Restart OpenCode. Requires OpenCode 1.18+ on macOS or Linux. Install a feature either through
|
|
120
|
+
`opencode-cockpit` or on its own — if both are configured, the first one loaded is used and
|
|
121
|
+
OpenCode warns you which entry to remove.
|
|
122
|
+
|
|
123
|
+
**Turn features off** (in both `opencode.json` and `tui.json`):
|
|
30
124
|
|
|
31
125
|
```json
|
|
32
|
-
{
|
|
126
|
+
{
|
|
127
|
+
"plugin": [["opencode-cockpit", { "features": { "shell": true } }]]
|
|
128
|
+
}
|
|
33
129
|
```
|
|
34
130
|
|
|
35
|
-
|
|
131
|
+
**Configure them** in one file, read by both halves of the plugin and by every project:
|
|
36
132
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
## Configuring features
|
|
42
|
-
|
|
43
|
-
Each feature's README lists its options. With `opencode-cockpit`, nest them under the feature's
|
|
44
|
-
name:
|
|
133
|
+
```
|
|
134
|
+
~/.config/opencode-cockpit/config.json → <project>/.cockpit.json → plugin-entry options
|
|
135
|
+
```
|
|
45
136
|
|
|
46
137
|
```json
|
|
47
138
|
{
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
139
|
+
"kinds": { "e2e": "playwright|cypress" },
|
|
140
|
+
"defaults": { "logFile": true, "timeoutSeconds": 900 },
|
|
141
|
+
"ui": { "dockHeight": 16, "historyMinutes": 60 }
|
|
51
142
|
}
|
|
52
143
|
```
|
|
53
144
|
|
|
54
|
-
|
|
55
|
-
|
|
145
|
+
Later sources win key by key, and an invalid file is ignored rather than fatal. You can categorize
|
|
146
|
+
your own commands, define watch rules, cap how long shells live, choose what may interrupt the
|
|
147
|
+
agent, and trade context tokens for accuracy. Each feature's README documents its own settings:
|
|
148
|
+
[Shell](https://github.com/Codestz/opencode-cockpit/tree/main/packages/shell#configuration).
|
|
56
149
|
|
|
57
150
|
## How it works
|
|
58
151
|
|
|
@@ -62,25 +155,41 @@ OpenCode TUI thread ── feature plugins (tui) ──┐
|
|
|
62
155
|
OpenCode server worker ─ feature plugins (server) ┘
|
|
63
156
|
```
|
|
64
157
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
158
|
+
OpenCode runs its interface and its server in separate threads, so a plugin's two halves can't
|
|
159
|
+
share memory. Both talk to **`cockpitd`**, a small daemon that owns every long-lived process: it
|
|
160
|
+
starts on demand, is shared by every OpenCode window, upgrades itself when a newer plugin connects,
|
|
161
|
+
cleans up processes left by a crash, and exits when idle. That's why shells outlive OpenCode
|
|
162
|
+
restarts, and why one session can look at a shell another session started.
|
|
163
|
+
|
|
164
|
+
Each shell's output feeds three views at once: a normalized **log** for the agent, an emulated
|
|
165
|
+
**screen** for you, and a raw ring buffer so a panel opened late can catch up.
|
|
68
166
|
|
|
69
167
|
| Package | Role |
|
|
70
168
|
|---|---|
|
|
71
|
-
| [`opencode-cockpit`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/opencode) | All features in one plugin
|
|
169
|
+
| [`opencode-cockpit`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/opencode) | All features in one plugin |
|
|
72
170
|
| [`@opencode-cockpit/shell`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/shell) | Shell feature |
|
|
73
171
|
| [`@opencode-cockpit/daemon`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/daemon) | `cockpitd`, the shared process host |
|
|
74
|
-
| [`@opencode-cockpit/client`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/client) | Typed, auto-spawning client and
|
|
172
|
+
| [`@opencode-cockpit/client`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/client) | Typed, auto-spawning client and plugin helpers |
|
|
75
173
|
| [`@opencode-cockpit/protocol`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/protocol) | Wire contracts |
|
|
76
174
|
|
|
77
175
|
## Roadmap
|
|
78
176
|
|
|
79
|
-
- **
|
|
80
|
-
- **
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
|
|
177
|
+
- **Agents** — live subagent tree with a peek overlay.
|
|
178
|
+
- **Doctor** — one command that checks your setup and tells you how to fix it.
|
|
179
|
+
- Shell groups in the sidebar, if the five-row cap ever stops being enough.
|
|
180
|
+
|
|
181
|
+
## Contributing
|
|
182
|
+
|
|
183
|
+
Issues and pull requests are welcome. [CONTRIBUTING.md](https://github.com/Codestz/opencode-cockpit/blob/main/CONTRIBUTING.md) covers the architecture,
|
|
184
|
+
the invariants worth knowing before changing anything, and how to run your working copy inside
|
|
185
|
+
OpenCode.
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
bun install
|
|
189
|
+
bun run check # lint, typecheck, tests (real PTYs, real daemon)
|
|
190
|
+
bun run pack:check # pack, install the tarballs, run a shell through them
|
|
191
|
+
bun run smoke:tui # drive a real OpenCode against the packed plugin
|
|
192
|
+
```
|
|
84
193
|
|
|
85
194
|
## License
|
|
86
195
|
|
package/dist/compose.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merges the server hooks of several features into one plugin. Tool maps are unioned (a name
|
|
3
|
+
* clash is a bug, so it throws); function hooks run in feature order, each awaited, which is how
|
|
4
|
+
* OpenCode itself runs hooks from separate plugins.
|
|
5
|
+
*/
|
|
6
|
+
export function composeHooks(parts) {
|
|
7
|
+
const tools = {};
|
|
8
|
+
const functions = new Map();
|
|
9
|
+
const singles = {};
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
for (const [key, value] of Object.entries(part)) {
|
|
12
|
+
if (value === undefined) continue;
|
|
13
|
+
if (key === "tool") {
|
|
14
|
+
for (const [id, def] of Object.entries(value)) {
|
|
15
|
+
if (id in tools) throw new Error(`tool "${id}" is registered by more than one cockpit feature`);
|
|
16
|
+
tools[id] = def;
|
|
17
|
+
}
|
|
18
|
+
} else if (typeof value === "function") {
|
|
19
|
+
const list = functions.get(key) ?? [];
|
|
20
|
+
list.push(value);
|
|
21
|
+
functions.set(key, list);
|
|
22
|
+
} else {
|
|
23
|
+
if (key in singles) throw new Error(`hook "${key}" is provided by more than one cockpit feature`);
|
|
24
|
+
singles[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const hooks = {
|
|
29
|
+
...singles
|
|
30
|
+
};
|
|
31
|
+
for (const [key, list] of functions) {
|
|
32
|
+
hooks[key] = list.length === 1 ? list[0] : async (...args) => {
|
|
33
|
+
for (const fn of list) await fn(...args);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (Object.keys(tools).length > 0) hooks.tool = tools;
|
|
37
|
+
return hooks;
|
|
38
|
+
}
|
package/dist/features.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Every feature the bundle can load. Adding one: list it here and wire it in server.ts / tui.ts. */
|
|
2
|
+
export const FEATURES = ["shell"];
|
|
3
|
+
export const BUNDLE = "opencode-cockpit";
|
|
4
|
+
export function isEnabled(options, feature) {
|
|
5
|
+
return options?.features?.[feature] !== false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for one feature. Shell also accepts options at the top level, which is where 0.1.x
|
|
10
|
+
* (when `opencode-cockpit` was Shell) read them from.
|
|
11
|
+
*/
|
|
12
|
+
export function featureOptions(options, feature) {
|
|
13
|
+
const own = options?.[feature] ?? {};
|
|
14
|
+
if (feature !== "shell" || !options) return own;
|
|
15
|
+
const legacy = {};
|
|
16
|
+
for (const [key, value] of Object.entries(options)) {
|
|
17
|
+
if (key !== "features" && !FEATURES.includes(key)) legacy[key] = value;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
...legacy,
|
|
21
|
+
...own
|
|
22
|
+
};
|
|
23
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createShellServer } from "@opencode-cockpit/shell/server";
|
|
2
|
+
import { composeHooks } from "./compose.js";
|
|
3
|
+
import { BUNDLE, featureOptions, isEnabled } from "./features.js";
|
|
4
|
+
const shell = createShellServer({
|
|
5
|
+
source: BUNDLE
|
|
6
|
+
});
|
|
7
|
+
const server = async (input, rawOptions) => {
|
|
8
|
+
const options = rawOptions;
|
|
9
|
+
const parts = [];
|
|
10
|
+
if (isEnabled(options, "shell")) parts.push(await shell(input, featureOptions(options, "shell")));
|
|
11
|
+
return composeHooks(parts);
|
|
12
|
+
};
|
|
13
|
+
const plugin = {
|
|
14
|
+
id: BUNDLE,
|
|
15
|
+
server
|
|
16
|
+
};
|
|
17
|
+
export default plugin;
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createShellTui } from "@opencode-cockpit/shell/tui";
|
|
2
|
+
import { BUNDLE, featureOptions, isEnabled } from "./features.js";
|
|
3
|
+
const shell = createShellTui({
|
|
4
|
+
source: BUNDLE
|
|
5
|
+
});
|
|
6
|
+
const tui = async (api, rawOptions, meta) => {
|
|
7
|
+
const options = rawOptions;
|
|
8
|
+
if (isEnabled(options, "shell")) await shell(api, featureOptions(options, "shell"), meta);
|
|
9
|
+
};
|
|
10
|
+
const plugin = {
|
|
11
|
+
id: BUNDLE,
|
|
12
|
+
tui
|
|
13
|
+
};
|
|
14
|
+
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-cockpit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "OpenCode superpowers, all in one: installs every opencode-cockpit feature (Shell today), each can be switched off",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,15 +25,22 @@
|
|
|
25
25
|
"agents"
|
|
26
26
|
],
|
|
27
27
|
"exports": {
|
|
28
|
-
"./server":
|
|
29
|
-
|
|
28
|
+
"./server": {
|
|
29
|
+
"types": "./types/server.d.ts",
|
|
30
|
+
"default": "./dist/server.js"
|
|
31
|
+
},
|
|
32
|
+
"./tui": {
|
|
33
|
+
"types": "./types/tui.d.ts",
|
|
34
|
+
"default": "./dist/tui.js"
|
|
35
|
+
}
|
|
30
36
|
},
|
|
31
37
|
"engines": {
|
|
32
38
|
"opencode": ">=1.18.0",
|
|
33
39
|
"bun": ">=1.3.5"
|
|
34
40
|
},
|
|
35
41
|
"files": [
|
|
36
|
-
"
|
|
42
|
+
"dist",
|
|
43
|
+
"types",
|
|
37
44
|
"README.md",
|
|
38
45
|
"LICENSE"
|
|
39
46
|
],
|
|
@@ -41,10 +48,10 @@
|
|
|
41
48
|
"access": "public"
|
|
42
49
|
},
|
|
43
50
|
"dependencies": {
|
|
44
|
-
"@opencode-cockpit/shell": "0.
|
|
51
|
+
"@opencode-cockpit/shell": "0.2.0",
|
|
45
52
|
"@opencode-ai/plugin": "1.18.31"
|
|
46
53
|
},
|
|
47
54
|
"devDependencies": {
|
|
48
|
-
"@opencode-cockpit/client": "0.
|
|
55
|
+
"@opencode-cockpit/client": "0.2.0"
|
|
49
56
|
}
|
|
50
57
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Hooks } from "@opencode-ai/plugin";
|
|
2
|
+
/**
|
|
3
|
+
* Merges the server hooks of several features into one plugin. Tool maps are unioned (a name
|
|
4
|
+
* clash is a bug, so it throws); function hooks run in feature order, each awaited, which is how
|
|
5
|
+
* OpenCode itself runs hooks from separate plugins.
|
|
6
|
+
*/
|
|
7
|
+
export declare function composeHooks(parts: Hooks[]): Hooks;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Every feature the bundle can load. Adding one: list it here and wire it in server.ts / tui.ts. */
|
|
2
|
+
export declare const FEATURES: readonly ["shell"];
|
|
3
|
+
export type Feature = (typeof FEATURES)[number];
|
|
4
|
+
export interface CockpitOptions {
|
|
5
|
+
/** Switch features off, e.g. `{ "shell": false }`. Everything is on by default. */
|
|
6
|
+
features?: Partial<Record<Feature, boolean>>;
|
|
7
|
+
/** Options passed to a single feature, keyed by feature name. */
|
|
8
|
+
shell?: Record<string, unknown>;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export declare const BUNDLE = "opencode-cockpit";
|
|
12
|
+
export declare function isEnabled(options: CockpitOptions | undefined, feature: Feature): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Options for one feature. Shell also accepts options at the top level, which is where 0.1.x
|
|
15
|
+
* (when `opencode-cockpit` was Shell) read them from.
|
|
16
|
+
*/
|
|
17
|
+
export declare function featureOptions(options: CockpitOptions | undefined, feature: Feature): Record<string, unknown>;
|
package/types/tui.d.ts
ADDED
package/src/compose.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { Hooks } from "@opencode-ai/plugin"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Merges the server hooks of several features into one plugin. Tool maps are unioned (a name
|
|
5
|
-
* clash is a bug, so it throws); function hooks run in feature order, each awaited, which is how
|
|
6
|
-
* OpenCode itself runs hooks from separate plugins.
|
|
7
|
-
*/
|
|
8
|
-
export function composeHooks(parts: Hooks[]): Hooks {
|
|
9
|
-
const tools: NonNullable<Hooks["tool"]> = {}
|
|
10
|
-
const functions = new Map<string, ((...args: unknown[]) => unknown)[]>()
|
|
11
|
-
const singles: Record<string, unknown> = {}
|
|
12
|
-
|
|
13
|
-
for (const part of parts) {
|
|
14
|
-
for (const [key, value] of Object.entries(part)) {
|
|
15
|
-
if (value === undefined) continue
|
|
16
|
-
if (key === "tool") {
|
|
17
|
-
for (const [id, def] of Object.entries(value as NonNullable<Hooks["tool"]>)) {
|
|
18
|
-
if (id in tools) throw new Error(`tool "${id}" is registered by more than one cockpit feature`)
|
|
19
|
-
tools[id] = def
|
|
20
|
-
}
|
|
21
|
-
} else if (typeof value === "function") {
|
|
22
|
-
const list = functions.get(key) ?? []
|
|
23
|
-
list.push(value as (...args: unknown[]) => unknown)
|
|
24
|
-
functions.set(key, list)
|
|
25
|
-
} else {
|
|
26
|
-
if (key in singles) throw new Error(`hook "${key}" is provided by more than one cockpit feature`)
|
|
27
|
-
singles[key] = value
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const hooks: Record<string, unknown> = { ...singles }
|
|
33
|
-
for (const [key, list] of functions) {
|
|
34
|
-
hooks[key] =
|
|
35
|
-
list.length === 1
|
|
36
|
-
? list[0]
|
|
37
|
-
: async (...args: unknown[]) => {
|
|
38
|
-
for (const fn of list) await fn(...args)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (Object.keys(tools).length > 0) hooks.tool = tools
|
|
42
|
-
return hooks as Hooks
|
|
43
|
-
}
|
package/src/features.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/** Every feature the bundle can load. Adding one: list it here and wire it in server.ts / tui.ts. */
|
|
2
|
-
export const FEATURES = ["shell"] as const
|
|
3
|
-
export type Feature = (typeof FEATURES)[number]
|
|
4
|
-
|
|
5
|
-
export interface CockpitOptions {
|
|
6
|
-
/** Switch features off, e.g. `{ "shell": false }`. Everything is on by default. */
|
|
7
|
-
features?: Partial<Record<Feature, boolean>>
|
|
8
|
-
/** Options passed to a single feature, keyed by feature name. */
|
|
9
|
-
shell?: Record<string, unknown>
|
|
10
|
-
[key: string]: unknown
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const BUNDLE = "opencode-cockpit"
|
|
14
|
-
|
|
15
|
-
export function isEnabled(options: CockpitOptions | undefined, feature: Feature): boolean {
|
|
16
|
-
return options?.features?.[feature] !== false
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Options for one feature. Shell also accepts options at the top level, which is where 0.1.x
|
|
21
|
-
* (when `opencode-cockpit` was Shell) read them from.
|
|
22
|
-
*/
|
|
23
|
-
export function featureOptions(
|
|
24
|
-
options: CockpitOptions | undefined,
|
|
25
|
-
feature: Feature,
|
|
26
|
-
): Record<string, unknown> {
|
|
27
|
-
const own = (options?.[feature] as Record<string, unknown> | undefined) ?? {}
|
|
28
|
-
if (feature !== "shell" || !options) return own
|
|
29
|
-
const legacy: Record<string, unknown> = {}
|
|
30
|
-
for (const [key, value] of Object.entries(options)) {
|
|
31
|
-
if (key !== "features" && !(FEATURES as readonly string[]).includes(key)) legacy[key] = value
|
|
32
|
-
}
|
|
33
|
-
return { ...legacy, ...own }
|
|
34
|
-
}
|
package/src/server.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Hooks, Plugin, PluginModule } from "@opencode-ai/plugin"
|
|
2
|
-
import { createShellServer } from "@opencode-cockpit/shell/server"
|
|
3
|
-
import { composeHooks } from "./compose.ts"
|
|
4
|
-
import { BUNDLE, type CockpitOptions, featureOptions, isEnabled } from "./features.ts"
|
|
5
|
-
|
|
6
|
-
const shell = createShellServer({ source: BUNDLE })
|
|
7
|
-
|
|
8
|
-
const server: Plugin = async (input, rawOptions) => {
|
|
9
|
-
const options = rawOptions as CockpitOptions | undefined
|
|
10
|
-
const parts: Hooks[] = []
|
|
11
|
-
if (isEnabled(options, "shell")) parts.push(await shell(input, featureOptions(options, "shell")))
|
|
12
|
-
return composeHooks(parts)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const plugin: PluginModule & { id: string } = { id: BUNDLE, server }
|
|
16
|
-
export default plugin
|
package/src/tui.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
2
|
-
import { createShellTui } from "@opencode-cockpit/shell/tui"
|
|
3
|
-
import { BUNDLE, type CockpitOptions, featureOptions, isEnabled } from "./features.ts"
|
|
4
|
-
|
|
5
|
-
const shell = createShellTui({ source: BUNDLE })
|
|
6
|
-
|
|
7
|
-
const tui: TuiPlugin = async (api, rawOptions, meta) => {
|
|
8
|
-
const options = rawOptions as CockpitOptions | undefined
|
|
9
|
-
if (isEnabled(options, "shell")) await shell(api, featureOptions(options, "shell"), meta)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const plugin: TuiPluginModule & { id: string } = { id: BUNDLE, tui }
|
|
13
|
-
export default plugin
|