opencode-plugin-selector 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/LICENSE +21 -0
- package/README.md +211 -0
- package/package.json +42 -0
- package/src/config.ts +81 -0
- package/src/discovery.ts +110 -0
- package/src/index.ts +117 -0
- package/src/types.ts +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# opencode-plugin-selector
|
|
2
|
+
|
|
3
|
+
> Interactive plugin selector for OpenCode - easily manage your plugins with a beautiful TUI
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/js/opencode-plugin-selector)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🎨 **Beautiful TUI** - Interactive multiselect interface powered by @clack/prompts
|
|
11
|
+
- ⚡ **Fast** - Instant loading with known plugins list
|
|
12
|
+
- ✓ **Status indicators** - See which plugins are installed/active at a glance
|
|
13
|
+
- 💾 **Auto-sync** - Automatically updates your `opencode.json` configuration
|
|
14
|
+
- 🔄 **Preserves versions** - Maintains version specifiers for existing plugins
|
|
15
|
+
- 🌍 **Cross-platform** - Works on Windows, macOS, and Linux
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### From npm (Recommended)
|
|
20
|
+
|
|
21
|
+
Add to your OpenCode configuration:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"plugin": ["opencode-plugin-selector"]
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
OpenCode will automatically install it on startup.
|
|
30
|
+
|
|
31
|
+
### Manual Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g opencode-plugin-selector
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or add to your project:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install opencode-plugin-selector
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### In OpenCode
|
|
46
|
+
|
|
47
|
+
Just type the command:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
/plugins
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This opens an interactive menu where you can:
|
|
54
|
+
|
|
55
|
+
- **↑↓** - Navigate through plugins
|
|
56
|
+
- **Space** - Toggle plugin selection
|
|
57
|
+
- **Enter** - Confirm selection
|
|
58
|
+
- **Esc** - Cancel
|
|
59
|
+
|
|
60
|
+
### Example
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
╭─────────────────────────────────────╮
|
|
64
|
+
│ OpenCode Plugin Selector │
|
|
65
|
+
├─────────────────────────────────────┤
|
|
66
|
+
│ ◉ opencode-firecrawl ✓ │
|
|
67
|
+
│ Web scraping via Firecrawl │
|
|
68
|
+
│ │
|
|
69
|
+
│ ◯ oh-my-opencode ✓ │
|
|
70
|
+
│ Background agents, LSP tools │
|
|
71
|
+
│ │
|
|
72
|
+
│ ◯ opencode-wakatime ✗ │
|
|
73
|
+
│ Track usage with Wakatime │
|
|
74
|
+
╰─────────────────────────────────────╯
|
|
75
|
+
|
|
76
|
+
[↑↓ Navigate] [Space Toggle] [Enter Confirm]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## How It Works
|
|
80
|
+
|
|
81
|
+
1. **Plugin Discovery** - Loads known plugins from the OpenCode ecosystem
|
|
82
|
+
2. **Status Check** - Scans `~/.cache/opencode/node_modules/` for installed plugins
|
|
83
|
+
3. **Config Sync** - Reads `~/.config/opencode/opencode.json` for active plugins
|
|
84
|
+
4. **Interactive Selection** - Presents a multiselect TUI
|
|
85
|
+
5. **Config Update** - Saves your selection back to `opencode.json`
|
|
86
|
+
6. **Restart Required** - Restart OpenCode to apply changes
|
|
87
|
+
|
|
88
|
+
## Supported Plugins
|
|
89
|
+
|
|
90
|
+
The selector includes all known plugins from the OpenCode ecosystem:
|
|
91
|
+
|
|
92
|
+
- **Auth plugins**: `opencode-antigravity-auth`, `opencode-openai-codex-auth`, `opencode-gemini-auth`
|
|
93
|
+
- **Development**: `opencode-daytona`, `opencode-devcontainers`, `opencode-worktree`
|
|
94
|
+
- **Monitoring**: `opencode-helicone-session`, `opencode-wakatime`, `opencode-sentry-monitor`
|
|
95
|
+
- **Utilities**: `opencode-firecrawl`, `opencode-pty`, `opencode-scheduler`
|
|
96
|
+
- **Agents**: `oh-my-opencode`, `opencode-background-agents`, `opencode-workspace`
|
|
97
|
+
- And many more...
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
The plugin respects your existing `opencode.json` configuration:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"plugin": [
|
|
106
|
+
"opencode-plugin-selector",
|
|
107
|
+
"opencode-firecrawl@1.2.0",
|
|
108
|
+
"oh-my-opencode@latest"
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Version specifiers are preserved when you update your selection.
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
### Prerequisites
|
|
118
|
+
|
|
119
|
+
- Node.js 18+
|
|
120
|
+
- Bun (for running OpenCode)
|
|
121
|
+
|
|
122
|
+
### Setup
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git clone https://github.com/yourusername/opencode-plugin-selector.git
|
|
126
|
+
cd opencode-plugin-selector
|
|
127
|
+
npm install
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Build
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm run build
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Test Locally
|
|
137
|
+
|
|
138
|
+
Add to your `opencode.json`:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"plugin": ["./path/to/opencode-plugin-selector"]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## API Reference
|
|
147
|
+
|
|
148
|
+
### Plugin Hook
|
|
149
|
+
|
|
150
|
+
The plugin implements the `tui.command.execute` hook:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
{
|
|
154
|
+
'tui.command.execute': async (input, output) => {
|
|
155
|
+
if (input.command === 'plugins') {
|
|
156
|
+
// Show interactive selector
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Functions
|
|
163
|
+
|
|
164
|
+
- `getAllPlugins()` - Get all available plugins with status
|
|
165
|
+
- `getActivePlugins()` - Get currently enabled plugins
|
|
166
|
+
- `updatePlugins(plugins: string[])` - Update configuration
|
|
167
|
+
- `getInstalledPlugins()` - Get plugins installed in cache
|
|
168
|
+
|
|
169
|
+
## Troubleshooting
|
|
170
|
+
|
|
171
|
+
### Plugin not loading?
|
|
172
|
+
|
|
173
|
+
1. Check your `opencode.json` syntax
|
|
174
|
+
2. Ensure the plugin is in the `plugin` array
|
|
175
|
+
3. Restart OpenCode
|
|
176
|
+
|
|
177
|
+
### Config not updating?
|
|
178
|
+
|
|
179
|
+
1. Check file permissions on `~/.config/opencode/opencode.json`
|
|
180
|
+
2. Ensure the file is valid JSON
|
|
181
|
+
3. Check OpenCode logs for errors
|
|
182
|
+
|
|
183
|
+
## Contributing
|
|
184
|
+
|
|
185
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
186
|
+
|
|
187
|
+
1. Fork the repository
|
|
188
|
+
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
|
189
|
+
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
190
|
+
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
191
|
+
5. Open a Pull Request
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
196
|
+
|
|
197
|
+
## Acknowledgments
|
|
198
|
+
|
|
199
|
+
- [OpenCode](https://opencode.ai) - The AI coding assistant
|
|
200
|
+
- [@clack/prompts](https://github.com/natemoo-re/clack) - Beautiful CLI prompts
|
|
201
|
+
- [picocolors](https://github.com/alexeyraspopov/picocolors) - Terminal colors
|
|
202
|
+
|
|
203
|
+
## Related
|
|
204
|
+
|
|
205
|
+
- [OpenCode Documentation](https://opencode.ai/docs)
|
|
206
|
+
- [OpenCode Ecosystem](https://opencode.ai/docs/ecosystem)
|
|
207
|
+
- [Plugin Development Guide](https://opencode.ai/docs/plugins)
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
Made with ❤️ for the OpenCode community
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-plugin-selector",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interactive plugin selector for OpenCode - easily manage your plugins with a beautiful TUI",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "tsc --watch"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"opencode",
|
|
14
|
+
"opencode-plugin",
|
|
15
|
+
"plugin",
|
|
16
|
+
"selector",
|
|
17
|
+
"tui",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/illusionaireal/opencode-plugin-selector.git"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@clack/prompts": "^0.7.0",
|
|
31
|
+
"picocolors": "^1.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@opencode-ai/plugin": "latest",
|
|
35
|
+
"@types/bun": "^1.3.11",
|
|
36
|
+
"@types/node": "^25.5.1",
|
|
37
|
+
"typescript": "^6.0.2"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@opencode-ai/plugin": "latest"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration file management for OpenCode
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile, writeFile } from 'fs/promises'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { homedir } from 'os'
|
|
8
|
+
import type { OpenCodeConfig } from './types.js'
|
|
9
|
+
|
|
10
|
+
const CONFIG_PATH = join(homedir(), '.config', 'opencode', 'opencode.json')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read OpenCode configuration file
|
|
14
|
+
*/
|
|
15
|
+
export async function readConfig(): Promise<OpenCodeConfig> {
|
|
16
|
+
try {
|
|
17
|
+
const content = await readFile(CONFIG_PATH, 'utf-8')
|
|
18
|
+
return JSON.parse(content)
|
|
19
|
+
} catch (error) {
|
|
20
|
+
// If file doesn't exist, return default config
|
|
21
|
+
if ((error as any).code === 'ENOENT') {
|
|
22
|
+
return { plugin: [] }
|
|
23
|
+
}
|
|
24
|
+
throw error
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Write OpenCode configuration file
|
|
30
|
+
*/
|
|
31
|
+
export async function writeConfig(config: OpenCodeConfig): Promise<void> {
|
|
32
|
+
const content = JSON.stringify(config, null, 2)
|
|
33
|
+
await writeFile(CONFIG_PATH, content, 'utf-8')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get current active plugins from config
|
|
38
|
+
*/
|
|
39
|
+
export async function getActivePlugins(): Promise<string[]> {
|
|
40
|
+
const config = await readConfig()
|
|
41
|
+
return config.plugin || []
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Update plugin list in config
|
|
46
|
+
* Preserves version specifiers for existing plugins
|
|
47
|
+
*/
|
|
48
|
+
export async function updatePlugins(selectedPlugins: string[]): Promise<void> {
|
|
49
|
+
const config = await readConfig()
|
|
50
|
+
|
|
51
|
+
// Build version map from existing plugins
|
|
52
|
+
const versionMap = new Map<string, string>()
|
|
53
|
+
if (config.plugin) {
|
|
54
|
+
for (const p of config.plugin) {
|
|
55
|
+
const name = stripVersion(p)
|
|
56
|
+
versionMap.set(name, p)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Apply new selection with preserved versions
|
|
61
|
+
config.plugin = selectedPlugins.map(name => {
|
|
62
|
+
return versionMap.get(name) || name
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
await writeConfig(config)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Strip version specifier from plugin name
|
|
70
|
+
* @example 'plugin@1.0.0' -> 'plugin'
|
|
71
|
+
*/
|
|
72
|
+
export function stripVersion(name: string): string {
|
|
73
|
+
return name.replace(/@.*$/, '')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get config file path
|
|
78
|
+
*/
|
|
79
|
+
export function getConfigPath(): string {
|
|
80
|
+
return CONFIG_PATH
|
|
81
|
+
}
|
package/src/discovery.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin discovery - finds available OpenCode plugins from multiple sources
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readdir } from 'fs/promises'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { homedir } from 'os'
|
|
8
|
+
import type { PluginInfo } from './types.js'
|
|
9
|
+
import { getActivePlugins, stripVersion } from './config.js'
|
|
10
|
+
|
|
11
|
+
const CACHE_DIR = join(homedir(), '.cache', 'opencode', 'node_modules')
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Known plugins from the OpenCode ecosystem
|
|
15
|
+
* Sourced from https://opencode.ai/docs/ecosystem/
|
|
16
|
+
*/
|
|
17
|
+
const KNOWN_PLUGINS: Array<{ name: string; description: string }> = [
|
|
18
|
+
{ name: 'opencode-daytona', description: 'Run sessions in isolated Daytona sandboxes with git sync' },
|
|
19
|
+
{ name: 'opencode-helicone-session', description: 'Inject Helicone session headers for request grouping' },
|
|
20
|
+
{ name: 'opencode-type-inject', description: 'Auto-inject TypeScript/Svelte types into file reads' },
|
|
21
|
+
{ name: 'opencode-openai-codex-auth', description: 'Use ChatGPT Plus/Pro subscription instead of API credits' },
|
|
22
|
+
{ name: 'opencode-gemini-auth', description: 'Use existing Gemini plan instead of API billing' },
|
|
23
|
+
{ name: 'opencode-antigravity-auth', description: 'Use Antigravity free models instead of API billing' },
|
|
24
|
+
{ name: 'opencode-devcontainers', description: 'Multi-branch devcontainer isolation with shallow clones' },
|
|
25
|
+
{ name: 'opencode-google-antigravity-auth', description: 'Google Antigravity OAuth with Google Search support' },
|
|
26
|
+
{ name: 'opencode-dynamic-context-pruning', description: 'Optimize token usage by pruning obsolete tool outputs' },
|
|
27
|
+
{ name: 'opencode-vibeguard', description: 'Redact secrets/PII into VibeGuard placeholders before LLM calls' },
|
|
28
|
+
{ name: 'opencode-websearch-cited', description: 'Native websearch with Google grounded style citations' },
|
|
29
|
+
{ name: 'opencode-pty', description: 'Run background processes in a PTY with interactive input' },
|
|
30
|
+
{ name: 'opencode-shell-strategy', description: 'Instructions for non-interactive shell commands' },
|
|
31
|
+
{ name: 'opencode-wakatime', description: 'Track OpenCode usage with Wakatime' },
|
|
32
|
+
{ name: 'opencode-md-table-formatter', description: 'Clean up markdown tables produced by LLMs' },
|
|
33
|
+
{ name: 'opencode-morph-plugin', description: 'Fast Apply editing, WarpGrep search, context compaction' },
|
|
34
|
+
{ name: 'oh-my-opencode', description: 'Background agents, LSP/AST/MCP tools, Claude Code compatible' },
|
|
35
|
+
{ name: 'opencode-notificator', description: 'Desktop notifications and sound alerts for sessions' },
|
|
36
|
+
{ name: 'opencode-notifier', description: 'Notifications for permission, completion, error events' },
|
|
37
|
+
{ name: 'opencode-zellij-namer', description: 'AI-powered Zellij session naming from context' },
|
|
38
|
+
{ name: 'opencode-skillful', description: 'Lazy load prompts on demand with skill discovery' },
|
|
39
|
+
{ name: 'opencode-supermemory', description: 'Persistent memory across sessions via Supermemory' },
|
|
40
|
+
{ name: 'plannotator-opencode', description: 'Interactive plan review with visual annotation' },
|
|
41
|
+
{ name: 'openspoon-subtask2', description: 'Extend /commands into orchestration with flow control' },
|
|
42
|
+
{ name: 'opencode-scheduler', description: 'Schedule recurring jobs with cron syntax (launchd/systemd)' },
|
|
43
|
+
{ name: 'micode', description: 'Structured Brainstorm-Plan-Implement workflow with continuity' },
|
|
44
|
+
{ name: 'octto', description: 'Interactive browser UI for AI brainstorming' },
|
|
45
|
+
{ name: 'opencode-background-agents', description: 'Claude Code-style background agents with async delegation' },
|
|
46
|
+
{ name: 'opencode-notify', description: 'Native OS notifications for task completion' },
|
|
47
|
+
{ name: 'opencode-workspace', description: 'Multi-agent orchestration harness - 16 components' },
|
|
48
|
+
{ name: 'opencode-worktree', description: 'Zero-friction git worktrees for OpenCode' },
|
|
49
|
+
{ name: 'opencode-sentry-monitor', description: 'Trace and debug AI agents with Sentry' },
|
|
50
|
+
{ name: 'opencode-firecrawl', description: 'Web scraping, crawling, and search via Firecrawl' }
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get installed plugins from cache directory
|
|
55
|
+
*/
|
|
56
|
+
export async function getInstalledPlugins(): Promise<Set<string>> {
|
|
57
|
+
try {
|
|
58
|
+
const dirs = await readdir(CACHE_DIR)
|
|
59
|
+
return new Set(dirs)
|
|
60
|
+
} catch {
|
|
61
|
+
return new Set()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Search npm for additional OpenCode plugins
|
|
67
|
+
* Returns plugin names
|
|
68
|
+
*/
|
|
69
|
+
export async function searchNpmPlugins(): Promise<string[]> {
|
|
70
|
+
try {
|
|
71
|
+
// Use Bun's shell API or spawn process to run npm search
|
|
72
|
+
const proc = Bun.spawn(['npm', 'search', 'opencode-plugin', '--json'])
|
|
73
|
+
const text = await new Response(proc.stdout).text()
|
|
74
|
+
await proc.exited
|
|
75
|
+
|
|
76
|
+
if (text) {
|
|
77
|
+
const results = JSON.parse(text)
|
|
78
|
+
return results.map((pkg: any) => pkg.name)
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// Silently fail - npm search is optional enhancement
|
|
82
|
+
}
|
|
83
|
+
return []
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get all available plugins with their status
|
|
88
|
+
*/
|
|
89
|
+
export async function getAllPlugins(): Promise<PluginInfo[]> {
|
|
90
|
+
const [installed, activeList] = await Promise.all([
|
|
91
|
+
getInstalledPlugins(),
|
|
92
|
+
getActivePlugins()
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
const activeSet = new Set(activeList.map(stripVersion))
|
|
96
|
+
|
|
97
|
+
// Start with known plugins
|
|
98
|
+
const plugins: PluginInfo[] = KNOWN_PLUGINS.map(p => ({
|
|
99
|
+
name: p.name,
|
|
100
|
+
description: p.description,
|
|
101
|
+
installed: installed.has(p.name),
|
|
102
|
+
active: activeSet.has(p.name)
|
|
103
|
+
}))
|
|
104
|
+
|
|
105
|
+
// Add any additional plugins from npm search (in background, non-blocking)
|
|
106
|
+
// For now, we'll skip this to keep the UI fast
|
|
107
|
+
// Could be added as an async enhancement later
|
|
108
|
+
|
|
109
|
+
return plugins
|
|
110
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode-plugin-selector
|
|
3
|
+
* Interactive plugin selector for OpenCode
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as p from '@clack/prompts'
|
|
7
|
+
import pc from 'picocolors'
|
|
8
|
+
import type { PluginContext, PluginHooks, PluginInfo } from './types.js'
|
|
9
|
+
import { getAllPlugins } from './discovery.js'
|
|
10
|
+
import { updatePlugins, stripVersion, getActivePlugins } from './config.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Main plugin export
|
|
14
|
+
*/
|
|
15
|
+
export const PluginSelector = async (ctx: PluginContext): Promise<PluginHooks> => {
|
|
16
|
+
return {
|
|
17
|
+
/**
|
|
18
|
+
* Handle TUI command execution
|
|
19
|
+
* Triggers on /plugins command
|
|
20
|
+
*/
|
|
21
|
+
'tui.command.execute': async (input, output) => {
|
|
22
|
+
if (input.command === 'plugins') {
|
|
23
|
+
await handlePluginsCommand(ctx)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handle the /plugins command
|
|
31
|
+
*/
|
|
32
|
+
async function handlePluginsCommand(ctx: PluginContext): Promise<void> {
|
|
33
|
+
console.clear()
|
|
34
|
+
|
|
35
|
+
p.intro(pc.bgCyan(pc.black(' OpenCode Plugin Selector ')))
|
|
36
|
+
|
|
37
|
+
const spinner = p.spinner()
|
|
38
|
+
spinner.start('Loading plugins...')
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Get all plugins with their status
|
|
42
|
+
const plugins = await getAllPlugins()
|
|
43
|
+
|
|
44
|
+
spinner.stop(`Found ${plugins.length} plugins`)
|
|
45
|
+
|
|
46
|
+
// Build options for multiselect
|
|
47
|
+
const options = plugins.map(p => ({
|
|
48
|
+
value: p.name,
|
|
49
|
+
label: `${p.name} ${p.installed ? pc.green('✓') : pc.red('✗')}`,
|
|
50
|
+
hint: p.description
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
// Get currently active plugins
|
|
54
|
+
const activePlugins = await getActivePlugins()
|
|
55
|
+
const initialValues = activePlugins.map(stripVersion)
|
|
56
|
+
|
|
57
|
+
// Show interactive selector
|
|
58
|
+
const selected = await p.multiselect({
|
|
59
|
+
message: 'Select plugins to enable (Space to toggle, Enter to confirm):',
|
|
60
|
+
options,
|
|
61
|
+
initialValues
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (p.isCancel(selected)) {
|
|
65
|
+
p.outro(pc.yellow('Cancelled'))
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Update configuration
|
|
70
|
+
spinner.start('Updating configuration...')
|
|
71
|
+
|
|
72
|
+
await updatePlugins(selected as string[])
|
|
73
|
+
|
|
74
|
+
spinner.stop('Configuration updated')
|
|
75
|
+
|
|
76
|
+
// Show summary
|
|
77
|
+
const enabledCount = (selected as string[]).length
|
|
78
|
+
p.outro(pc.green(`✓ ${enabledCount} plugins enabled`))
|
|
79
|
+
|
|
80
|
+
// Show what changed
|
|
81
|
+
const enabled = selected as string[]
|
|
82
|
+
const previouslyActive = new Set(initialValues)
|
|
83
|
+
|
|
84
|
+
const newlyEnabled = enabled.filter(n => !previouslyActive.has(n))
|
|
85
|
+
const newlyDisabled = initialValues.filter(n => !enabled.includes(n))
|
|
86
|
+
|
|
87
|
+
if (newlyEnabled.length > 0) {
|
|
88
|
+
console.log(pc.green('\nEnabled:'))
|
|
89
|
+
newlyEnabled.forEach(n => console.log(pc.green(` + ${n}`)))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (newlyDisabled.length > 0) {
|
|
93
|
+
console.log(pc.red('\nDisabled:'))
|
|
94
|
+
newlyDisabled.forEach(n => console.log(pc.red(` - ${n}`)))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Suggest restart
|
|
98
|
+
console.log(pc.dim('\nRestart OpenCode to apply changes.'))
|
|
99
|
+
|
|
100
|
+
} catch (error) {
|
|
101
|
+
spinner.stop('Failed')
|
|
102
|
+
p.outro(pc.red(`Error: ${(error as Error).message}`))
|
|
103
|
+
|
|
104
|
+
// Log error via OpenCode client
|
|
105
|
+
await ctx.client.app.log({
|
|
106
|
+
body: {
|
|
107
|
+
service: 'plugin-selector',
|
|
108
|
+
level: 'error',
|
|
109
|
+
message: 'Failed to load plugins',
|
|
110
|
+
extra: { error: (error as Error).message }
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Default export for OpenCode plugin system
|
|
117
|
+
export default PluginSelector
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for opencode-plugin-selector
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Plugin metadata
|
|
7
|
+
*/
|
|
8
|
+
export interface PluginInfo {
|
|
9
|
+
name: string
|
|
10
|
+
description: string
|
|
11
|
+
installed: boolean
|
|
12
|
+
active: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* OpenCode configuration structure
|
|
17
|
+
*/
|
|
18
|
+
export interface OpenCodeConfig {
|
|
19
|
+
$schema?: string
|
|
20
|
+
plugin?: string[]
|
|
21
|
+
[key: string]: any
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Plugin context provided by OpenCode
|
|
26
|
+
*/
|
|
27
|
+
export interface PluginContext {
|
|
28
|
+
project: any
|
|
29
|
+
client: any
|
|
30
|
+
$: any
|
|
31
|
+
directory: string
|
|
32
|
+
worktree: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Plugin hooks
|
|
37
|
+
*/
|
|
38
|
+
export interface PluginHooks {
|
|
39
|
+
[event: string]: (input: any, output: any) => Promise<void> | void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Plugin function signature
|
|
44
|
+
*/
|
|
45
|
+
export type PluginFunction = (ctx: PluginContext) => Promise<PluginHooks>
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* TUI command input
|
|
49
|
+
*/
|
|
50
|
+
export interface TUICommandInput {
|
|
51
|
+
command: string
|
|
52
|
+
args: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* TUI command output
|
|
57
|
+
*/
|
|
58
|
+
export interface TUICommandOutput {
|
|
59
|
+
result?: any
|
|
60
|
+
error?: Error
|
|
61
|
+
}
|