opencode-architect 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/LICENSE.md +21 -0
- package/README.md +53 -0
- package/assets/agents/opencode-agent-designer.md +62 -0
- package/assets/agents/opencode-architect.md +288 -0
- package/assets/agents/opencode-command-crafter.md +42 -0
- package/assets/agents/opencode-extension-auditor.md +61 -0
- package/assets/agents/opencode-mcp-integrator.md +38 -0
- package/assets/agents/opencode-packager.md +237 -0
- package/assets/agents/opencode-plugin-engineer.md +49 -0
- package/assets/agents/opencode-publisher.md +241 -0
- package/assets/agents/opencode-skill-creator.md +52 -0
- package/assets/agents/opencode-tool-builder.md +37 -0
- package/assets/references/opencode-architect-oneshots.md +329 -0
- package/assets/templates/cli.template.txt +109 -0
- package/assets/templates/index.template.txt +15 -0
- package/assets/templates/installer.template.txt +269 -0
- package/assets/templates/package-analysis.template.md +60 -0
- package/assets/templates/package-basics.template.json +28 -0
- package/assets/templates/package-full.template.json +56 -0
- package/assets/templates/plugin-local.template.txt +69 -0
- package/assets/templates/prompts.template.txt +28 -0
- package/assets/templates/skill-structure.template.md +46 -0
- package/assets/templates/tsconfig.template.json +15 -0
- package/commands/sync-docs.ts +9 -0
- package/index.ts +120 -0
- package/package.json +35 -0
- package/scripts/fetch-opencode-docs.ts +193 -0
- package/scripts/logger.ts +20 -0
- package/tools/sync-docs.ts +24 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
TEMPLATE INSTRUCTIONS
|
|
2
|
+
====================
|
|
3
|
+
Replace the following placeholders before use:
|
|
4
|
+
|
|
5
|
+
EXTENSION NAME
|
|
6
|
+
"myextension" → your extension identifier (e.g., "mytool")
|
|
7
|
+
|
|
8
|
+
SKILL NAME
|
|
9
|
+
"myextension" → must match the folder name in assets/skills/
|
|
10
|
+
|
|
11
|
+
COMMAND NAME
|
|
12
|
+
"my-command.md" → your command file name in assets/commands/
|
|
13
|
+
|
|
14
|
+
PACKAGE NAME
|
|
15
|
+
"opencode-myextension" → your npm package name
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
import { exists, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
export type Scope = "local" | "global";
|
|
23
|
+
|
|
24
|
+
export interface InstallOptions {
|
|
25
|
+
configurePermission?: boolean;
|
|
26
|
+
configureMcp?: boolean;
|
|
27
|
+
addPluginConfig?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface InstallResult {
|
|
31
|
+
scope: Scope;
|
|
32
|
+
skillPath: string;
|
|
33
|
+
commandPath: string;
|
|
34
|
+
configPath: string;
|
|
35
|
+
migrated: boolean;
|
|
36
|
+
permissionConfigured: boolean;
|
|
37
|
+
mcpConfigured: boolean;
|
|
38
|
+
pluginAdded: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface UninstallResult {
|
|
42
|
+
scope: Scope;
|
|
43
|
+
removed: string[];
|
|
44
|
+
pluginRemoved: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface StatusResult {
|
|
48
|
+
local: { installed: boolean; version: string | null; pluginInConfig: boolean } | null;
|
|
49
|
+
global: { installed: boolean; version: string | null; pluginInConfig: boolean } | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SKILL_NAME = "myextension";
|
|
53
|
+
const COMMAND_NAME = "my-command.md";
|
|
54
|
+
const PLUGIN_NAME = "opencode-myextension";
|
|
55
|
+
|
|
56
|
+
export async function getPackageVersion(): Promise<string> {
|
|
57
|
+
const content = await Bun.file(`${import.meta.dirname}/../package.json`).text();
|
|
58
|
+
return JSON.parse(content).version;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function getPackageName(): Promise<string> {
|
|
62
|
+
const content = await Bun.file(`${import.meta.dirname}/../package.json`).text();
|
|
63
|
+
return JSON.parse(content).name;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getPackageDir(): string {
|
|
67
|
+
return join(import.meta.dirname, "..");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getGlobalConfigPath(): string {
|
|
71
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
72
|
+
if (xdgConfig) {
|
|
73
|
+
return join(xdgConfig, "opencode");
|
|
74
|
+
}
|
|
75
|
+
return join(homedir(), ".config", "opencode");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getLocalConfigPath(projectDir: string): string {
|
|
79
|
+
return join(projectDir, ".opencode");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function copyDir(src: string, dest: string): Promise<void> {
|
|
83
|
+
await mkdir(dest, { recursive: true });
|
|
84
|
+
for (const entry of await readdir(src, { withFileTypes: true })) {
|
|
85
|
+
const s = join(src, entry.name);
|
|
86
|
+
const d = join(dest, entry.name);
|
|
87
|
+
if (entry.isDirectory()) {
|
|
88
|
+
await copyDir(s, d);
|
|
89
|
+
} else {
|
|
90
|
+
await Bun.write(d, Bun.file(s));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function readJsonConfig(path: string): Promise<Record<string, unknown>> {
|
|
96
|
+
try {
|
|
97
|
+
const content = await readFile(path, "utf-8");
|
|
98
|
+
return JSON.parse(content);
|
|
99
|
+
} catch {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function writeJsonConfig(path: string, config: Record<string, unknown>): Promise<void> {
|
|
105
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
106
|
+
await writeFile(path, JSON.stringify(config, null, 2));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function ensureSkillPermission(configPath: string): Promise<void> {
|
|
110
|
+
const config = await readJsonConfig(configPath);
|
|
111
|
+
if (!config.permission) config.permission = {};
|
|
112
|
+
if (!(config.permission as Record<string, unknown>).skill) (config.permission as Record<string, unknown>).skill = {};
|
|
113
|
+
const skillPerms = (config.permission as Record<string, unknown>).skill as Record<string, unknown>;
|
|
114
|
+
if (skillPerms[SKILL_NAME] !== "allow") {
|
|
115
|
+
skillPerms[SKILL_NAME] = "allow";
|
|
116
|
+
await mkdir(join(configPath, ".."), { recursive: true });
|
|
117
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function removeSkillPermission(configPath: string): Promise<void> {
|
|
122
|
+
const config = await readJsonConfig(configPath);
|
|
123
|
+
if (config.permission && (config.permission as Record<string, unknown>).skill) {
|
|
124
|
+
const skillPerms = (config.permission as Record<string, unknown>).skill as Record<string, unknown>;
|
|
125
|
+
delete skillPerms[SKILL_NAME];
|
|
126
|
+
if (Object.keys(skillPerms).length === 0) delete (config.permission as Record<string, unknown>).skill;
|
|
127
|
+
if (Object.keys(config.permission as Record<string, unknown>).length === 0) delete config.permission;
|
|
128
|
+
await mkdir(join(configPath, ".."), { recursive: true });
|
|
129
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function addPluginToConfig(configPath: string, pluginName: string): Promise<boolean> {
|
|
134
|
+
const config = await readJsonConfig(configPath);
|
|
135
|
+
if (!config.plugin) config.plugin = [];
|
|
136
|
+
const plugins = config.plugin as string[];
|
|
137
|
+
if (plugins.includes(pluginName)) return false;
|
|
138
|
+
plugins.push(pluginName);
|
|
139
|
+
config.plugin = plugins;
|
|
140
|
+
await mkdir(join(configPath, ".."), { recursive: true });
|
|
141
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function removePluginFromConfig(configPath: string, pluginName: string): Promise<boolean> {
|
|
146
|
+
const config = await readJsonConfig(configPath);
|
|
147
|
+
if (!config.plugin) return false;
|
|
148
|
+
const plugins = config.plugin as string[];
|
|
149
|
+
const index = plugins.indexOf(pluginName);
|
|
150
|
+
if (index === -1) return false;
|
|
151
|
+
plugins.splice(index, 1);
|
|
152
|
+
if (plugins.length === 0) delete config.plugin;
|
|
153
|
+
else config.plugin = plugins;
|
|
154
|
+
await mkdir(join(configPath, ".."), { recursive: true });
|
|
155
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function isPluginInConfig(configPath: string, pluginName: string): Promise<boolean> {
|
|
160
|
+
const config = await readJsonConfig(configPath);
|
|
161
|
+
if (!config.plugin) return false;
|
|
162
|
+
return (config.plugin as string[]).includes(pluginName);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function addMcpServer(configPath: string): Promise<boolean> {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function checkMigrationNeeded(projectDir: string): Promise<{
|
|
170
|
+
needed: boolean;
|
|
171
|
+
rootConfigPath: string;
|
|
172
|
+
dotOpenencodeConfigPath: string;
|
|
173
|
+
rootConfig: Record<string, unknown> | null;
|
|
174
|
+
dotOpenencodeConfig: Record<string, unknown> | null;
|
|
175
|
+
}> {
|
|
176
|
+
const rootConfigPath = join(projectDir, "opencode.json");
|
|
177
|
+
const dotOpenencodeConfigPath = join(projectDir, ".opencode", "opencode.json");
|
|
178
|
+
const rootExists = await exists(rootConfigPath);
|
|
179
|
+
const dotOpenencodeExists = await exists(dotOpenencodeConfigPath);
|
|
180
|
+
if (!rootExists) return { needed: false, rootConfigPath, dotOpenencodeConfigPath, rootConfig: null, dotOpenencodeConfig: null };
|
|
181
|
+
const rootConfig = await readJsonConfig(rootConfigPath);
|
|
182
|
+
const dotOpenencodeConfig = dotOpenencodeExists ? await readJsonConfig(dotOpenencodeConfigPath) : null;
|
|
183
|
+
return { needed: rootExists, rootConfigPath, dotOpenencodeConfigPath, rootConfig, dotOpenencodeConfig };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function migrateRootConfig(
|
|
187
|
+
projectDir: string,
|
|
188
|
+
onConflict?: (root: Record<string, unknown>, dot: Record<string, unknown>) => Promise<boolean>
|
|
189
|
+
): Promise<boolean> {
|
|
190
|
+
const { needed, rootConfigPath, dotOpenencodeConfigPath, rootConfig, dotOpenencodeConfig } = await checkMigrationNeeded(projectDir);
|
|
191
|
+
if (!needed || !rootConfig) return false;
|
|
192
|
+
if (dotOpenencodeConfig) {
|
|
193
|
+
const hasConflict = Object.keys(rootConfig).some(key => key in dotOpenencodeConfig);
|
|
194
|
+
if (hasConflict && onConflict) {
|
|
195
|
+
const shouldContinue = await onConflict(rootConfig, dotOpenencodeConfig);
|
|
196
|
+
if (!shouldContinue) return false;
|
|
197
|
+
}
|
|
198
|
+
const merged = { ...rootConfig, ...dotOpenencodeConfig };
|
|
199
|
+
await writeJsonConfig(dotOpenencodeConfigPath, merged);
|
|
200
|
+
} else {
|
|
201
|
+
await mkdir(join(projectDir, ".opencode"), { recursive: true });
|
|
202
|
+
await writeJsonConfig(dotOpenencodeConfigPath, rootConfig);
|
|
203
|
+
}
|
|
204
|
+
await rm(rootConfigPath);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function install(
|
|
209
|
+
scope: Scope,
|
|
210
|
+
projectDir: string = process.cwd(),
|
|
211
|
+
options: InstallOptions = {}
|
|
212
|
+
): Promise<InstallResult> {
|
|
213
|
+
const version = await getPackageVersion();
|
|
214
|
+
const packageName = await getPackageName();
|
|
215
|
+
const pkgDir = getPackageDir();
|
|
216
|
+
const { configurePermission = true, configureMcp = true, addPluginConfig = true } = options;
|
|
217
|
+
const configBase = scope === "global" ? getGlobalConfigPath() : getLocalConfigPath(projectDir);
|
|
218
|
+
const skillPath = join(configBase, "skills", SKILL_NAME);
|
|
219
|
+
const commandPath = join(configBase, "commands", COMMAND_NAME);
|
|
220
|
+
const configPath = join(configBase, "opencode.json");
|
|
221
|
+
const versionMarker = join(skillPath, ".version");
|
|
222
|
+
let migrated = false;
|
|
223
|
+
if (scope === "local") migrated = await migrateRootConfig(projectDir);
|
|
224
|
+
await copyDir(join(pkgDir, "assets", "skills", SKILL_NAME), skillPath);
|
|
225
|
+
await mkdir(join(configBase, "commands"), { recursive: true });
|
|
226
|
+
await Bun.write(commandPath, Bun.file(join(pkgDir, "assets", "commands", COMMAND_NAME)));
|
|
227
|
+
await Bun.write(versionMarker, version);
|
|
228
|
+
let permissionConfigured = false;
|
|
229
|
+
if (configurePermission) { await ensureSkillPermission(configPath); permissionConfigured = true; }
|
|
230
|
+
let mcpConfigured = false;
|
|
231
|
+
if (configureMcp) mcpConfigured = await addMcpServer(configPath);
|
|
232
|
+
let pluginAdded = false;
|
|
233
|
+
if (addPluginConfig) pluginAdded = await addPluginToConfig(configPath, packageName);
|
|
234
|
+
return { scope, skillPath, commandPath, configPath, migrated, permissionConfigured, mcpConfigured, pluginAdded };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function uninstall(scope: Scope, projectDir: string = process.cwd()): Promise<UninstallResult> {
|
|
238
|
+
const packageName = await getPackageName();
|
|
239
|
+
const configBase = scope === "global" ? getGlobalConfigPath() : getLocalConfigPath(projectDir);
|
|
240
|
+
const skillPath = join(configBase, "skills", SKILL_NAME);
|
|
241
|
+
const commandPath = join(configBase, "commands", COMMAND_NAME);
|
|
242
|
+
const configPath = join(configBase, "opencode.json");
|
|
243
|
+
const removed: string[] = [];
|
|
244
|
+
if (await exists(skillPath)) { await rm(skillPath, { recursive: true }); removed.push(skillPath); }
|
|
245
|
+
if ( await exists(commandPath)) { await rm(commandPath); removed.push(commandPath); }
|
|
246
|
+
let pluginRemoved = false;
|
|
247
|
+
if (await exists(configPath)) { await removeSkillPermission(configPath); pluginRemoved = await removePluginFromConfig(configPath, packageName); }
|
|
248
|
+
const skillsDir = join(configBase, "skills");
|
|
249
|
+
const commandsDir = join(configBase, "commands");
|
|
250
|
+
try { const skillsContents = await readdir(skillsDir); if (skillsContents.length === 0) await rm(skillsDir, { recursive: true }); } catch {}
|
|
251
|
+
try { const commandsContents = await readdir(commandsDir); if (commandsContents.length === 0) await rm(commandsDir, { recursive: true }); } catch {}
|
|
252
|
+
return { scope, removed, pluginRemoved };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function status(projectDir: string = process.cwd()): Promise<StatusResult> {
|
|
256
|
+
const version = await getPackageVersion();
|
|
257
|
+
const packageName = await getPackageName();
|
|
258
|
+
const localConfigPath = getLocalConfigPath(projectDir);
|
|
259
|
+
const localVersionMarker = join(localConfigPath, "skills", SKILL_NAME, ".version");
|
|
260
|
+
const localConfigFile = join(localConfigPath, "opencode.json");
|
|
261
|
+
const globalConfigPath = getGlobalConfigPath();
|
|
262
|
+
const globalVersionMarker = join(globalConfigPath, "skills", SKILL_NAME, ".version");
|
|
263
|
+
const globalConfigFile = join(globalConfigPath, "opencode.json");
|
|
264
|
+
let localStatus: { installed: boolean; version: string | null; pluginInConfig: boolean } | null = null;
|
|
265
|
+
let globalStatus: { installed: boolean; version: string | null; pluginInConfig: boolean } | null = null;
|
|
266
|
+
try { const localVersion = (await readFile(localVersionMarker, "utf-8")).trim(); const localPluginInConfig = await isPluginInConfig(localConfigFile, packageName); localStatus = { installed: true, version: localVersion, pluginInConfig: localPluginInConfig }; } catch { const skillPath = join(localConfigPath, "skills", SKILL_NAME); if (await exists(skillPath)) { const localPluginInConfig = await isPluginInConfig(localConfigFile, packageName); localStatus = { installed: true, version: null, pluginInConfig: localPluginInConfig }; } }
|
|
267
|
+
try { const globalVersion = (await readFile(globalVersionMarker, "utf-8")).trim(); const globalPluginInConfig = await isPluginInConfig(globalConfigFile, packageName); globalStatus = { installed: true, version: globalVersion, pluginInConfig: globalPluginInConfig }; } catch { const skillPath = join(globalConfigPath, "skills", SKILL_NAME); if (await exists(skillPath)) { const globalPluginInConfig = await isPluginInConfig(globalConfigFile, packageName); globalStatus = { installed: true, version: null, pluginInConfig: globalPluginInConfig }; } }
|
|
268
|
+
return { local: localStatus, global: globalStatus };
|
|
269
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Package Analysis Template
|
|
2
|
+
|
|
3
|
+
Use this template to document extension analysis before packaging.
|
|
4
|
+
|
|
5
|
+
## Source Information
|
|
6
|
+
|
|
7
|
+
- **Source Path**: [path to .opencode/ or existing package]
|
|
8
|
+
- **Source Type**: [project-local | existing-package]
|
|
9
|
+
- **Target Package Name**: opencode-[name]
|
|
10
|
+
|
|
11
|
+
## Extension Inventory
|
|
12
|
+
|
|
13
|
+
### Skills
|
|
14
|
+
| Name | Description | Dependencies | Issues |
|
|
15
|
+
| ---- | ----------- | ------------ | ------ |
|
|
16
|
+
| | | | |
|
|
17
|
+
|
|
18
|
+
### Commands
|
|
19
|
+
| Name | Description | Template Vars | Issues |
|
|
20
|
+
| ---- | ----------- | ------------- | ------ |
|
|
21
|
+
| | | | |
|
|
22
|
+
|
|
23
|
+
### Agents
|
|
24
|
+
| Name | Description | Mode | Issues |
|
|
25
|
+
| ---- | ----------- | ---- | ------ |
|
|
26
|
+
| | | | |
|
|
27
|
+
|
|
28
|
+
### Plugins
|
|
29
|
+
| File | Hooks Used | Merge Strategy | Issues |
|
|
30
|
+
| ---- | ---------- | -------------- | ------ |
|
|
31
|
+
| | | | |
|
|
32
|
+
|
|
33
|
+
### Tools
|
|
34
|
+
| File | Tool Name | Merge Strategy | Issues |
|
|
35
|
+
| ---- | --------- | -------------- | ------ |
|
|
36
|
+
| | | | |
|
|
37
|
+
|
|
38
|
+
## Dependencies
|
|
39
|
+
|
|
40
|
+
From `.opencode/package.json`:
|
|
41
|
+
- [list dependencies]
|
|
42
|
+
|
|
43
|
+
## Packaging Assessment
|
|
44
|
+
|
|
45
|
+
- **Complexity**: [simple | medium | complex]
|
|
46
|
+
- **Requires Guidance**: [yes | no]
|
|
47
|
+
- **Guidance Needed For**: [plugins | tools | both | none]
|
|
48
|
+
|
|
49
|
+
## Recommendations
|
|
50
|
+
|
|
51
|
+
1. [recommendation 1]
|
|
52
|
+
2. [recommendation 2]
|
|
53
|
+
|
|
54
|
+
## Merge Decisions (if applicable)
|
|
55
|
+
|
|
56
|
+
### Plugin Merge
|
|
57
|
+
- [decision and rationale]
|
|
58
|
+
|
|
59
|
+
### Tool Merge
|
|
60
|
+
- [decision and rationale]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
TEMPLATE INSTRUCTIONS
|
|
2
|
+
====================
|
|
3
|
+
Replace the following placeholders before use:
|
|
4
|
+
|
|
5
|
+
PACKAGE NAME
|
|
6
|
+
"opencode-myextension" → your extension name (e.g., "opencode-mytool")
|
|
7
|
+
"myextension" → skill/extension identifier (e.g., "mytool")
|
|
8
|
+
|
|
9
|
+
VERSION
|
|
10
|
+
"1.0.0" → initial version
|
|
11
|
+
|
|
12
|
+
PATHS
|
|
13
|
+
"skills/myextension" → path to your skill assets
|
|
14
|
+
"commands/my-command.md" → path to your command file
|
|
15
|
+
|
|
16
|
+
METADATA
|
|
17
|
+
"Publisher Name" → your name or organization
|
|
18
|
+
"email@example.com" → your email
|
|
19
|
+
"https://github.com/org/opencode-myextension.git" → your repo URL
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
{
|
|
23
|
+
"name": "opencode-myextension",
|
|
24
|
+
"version": "1.0.0",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"module": "index.ts",
|
|
27
|
+
"files": ["index.ts", "plugin.ts", "assets"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
TEMPLATE INSTRUCTIONS
|
|
2
|
+
====================
|
|
3
|
+
Replace the following placeholders before use:
|
|
4
|
+
|
|
5
|
+
PACKAGE NAME
|
|
6
|
+
"opencode-myextension" → your extension name (e.g., "opencode-mytool")
|
|
7
|
+
"myextension" → skill/extension identifier (e.g., "mytool")
|
|
8
|
+
|
|
9
|
+
VERSION
|
|
10
|
+
"1.0.0" → initial version
|
|
11
|
+
|
|
12
|
+
PATHS
|
|
13
|
+
"skills/myextension" → path to your skill assets
|
|
14
|
+
"commands/my-command.md" → path to your command file
|
|
15
|
+
|
|
16
|
+
METADATA
|
|
17
|
+
"Publisher Name" → your name or organization
|
|
18
|
+
"Author Name <email@example.com>" → your name and email
|
|
19
|
+
"email@example.com" → your support email
|
|
20
|
+
"https://github.com/org/opencode-myextension.git" → your repo URL
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
{
|
|
24
|
+
"name": "opencode-myextension",
|
|
25
|
+
"version": "1.0.0",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"module": "index.ts",
|
|
28
|
+
"bin": {
|
|
29
|
+
"opencode-myextension": "./src/cli.ts"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/org/opencode-myextension.git"
|
|
34
|
+
},
|
|
35
|
+
"publisher": "Publisher Name",
|
|
36
|
+
"author": "Author Name <email@example.com>",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/org/opencode-myextension/issues",
|
|
39
|
+
"email": "support@example.com"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"files": ["index.ts", "plugin.ts", "src", "assets"],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"check": "tsc --noEmit",
|
|
45
|
+
"test": "bun test"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@inquirer/prompts": "^8.3.0",
|
|
49
|
+
"@opencode-ai/plugin": "*",
|
|
50
|
+
"@opencode-ai/sdk": "^1.2.20"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/bun": "latest",
|
|
54
|
+
"typescript": "^5.7.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
TEMPLATE INSTRUCTIONS
|
|
2
|
+
====================
|
|
3
|
+
Replace the following placeholders before use:
|
|
4
|
+
|
|
5
|
+
EXTENSION NAME
|
|
6
|
+
"myextension" → your extension identifier (e.g., "mytool")
|
|
7
|
+
|
|
8
|
+
SKILL NAME
|
|
9
|
+
"myextension" → must match the folder name in assets/skills/
|
|
10
|
+
|
|
11
|
+
COMMAND NAME
|
|
12
|
+
"my-command.md" → your command file name in assets/commands/
|
|
13
|
+
|
|
14
|
+
PACKAGE NAME
|
|
15
|
+
"./src/installer.ts" → path to installer module (for published packages)
|
|
16
|
+
Or inline install functions (for local packages)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
20
|
+
import { install, getGlobalConfigPath, getPackageVersion, type Scope } from "./src/installer.ts";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
const plugin: Plugin = async ({ directory }) => ({
|
|
24
|
+
config: async () => {
|
|
25
|
+
const version = await getPackageVersion();
|
|
26
|
+
const globalConfigPath = getGlobalConfigPath();
|
|
27
|
+
const globalVersionMarker = join(globalConfigPath, "skills", "myextension", ".version");
|
|
28
|
+
|
|
29
|
+
const isGlobalInstall = directory === globalConfigPath ||
|
|
30
|
+
directory.startsWith(globalConfigPath + "/") ||
|
|
31
|
+
directory.startsWith(globalConfigPath + "\\");
|
|
32
|
+
|
|
33
|
+
let scope: Scope;
|
|
34
|
+
|
|
35
|
+
if (isGlobalInstall) {
|
|
36
|
+
scope = "global";
|
|
37
|
+
} else {
|
|
38
|
+
try {
|
|
39
|
+
const globalVersion = (await Bun.file(globalVersionMarker).text()).trim();
|
|
40
|
+
if (globalVersion === version) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
scope = "local";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const marker = scope === "global"
|
|
48
|
+
? globalVersionMarker
|
|
49
|
+
: join(directory, ".opencode", "skills", "myextension", ".version");
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const installedVersion = (await Bun.file(marker).text()).trim();
|
|
53
|
+
if (installedVersion === version) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
|
|
58
|
+
const result = await install(scope, directory);
|
|
59
|
+
|
|
60
|
+
console.log(`\nOpenCode MyExtension installed ${scope === "global" ? "globally" : "locally"}:`);
|
|
61
|
+
console.log(` Skill: ${result.skillPath}`);
|
|
62
|
+
console.log(` Command: ${result.commandPath}`);
|
|
63
|
+
if (result.migrated) {
|
|
64
|
+
console.log(` Migrated: opencode.json → .opencode/opencode.json`);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export default plugin;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
TEMPLATE INSTRUCTIONS
|
|
2
|
+
====================
|
|
3
|
+
Replace the following placeholders before use:
|
|
4
|
+
|
|
5
|
+
EXTENSION NAME
|
|
6
|
+
"myextension" → your extension identifier
|
|
7
|
+
|
|
8
|
+
SKILL NAME
|
|
9
|
+
"myextension" → must match the skill folder name
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
import { confirm } from "@inquirer/prompts";
|
|
13
|
+
|
|
14
|
+
export async function confirmOverwrite(message: string): Promise<boolean> {
|
|
15
|
+
return confirm({ message, default: false });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function confirmPermissionConfig(): Promise<boolean> {
|
|
19
|
+
return confirm({ message: "Configure skill permission (allow myextension)?", default: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function confirmMcpConfig(): Promise<boolean> {
|
|
23
|
+
return confirm({ message: "Configure MCP server?", default: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function confirmPluginConfig(): Promise<boolean> {
|
|
27
|
+
return confirm({ message: "Add plugin to opencode.json config?", default: true });
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
TEMPLATE INSTRUCTIONS
|
|
2
|
+
====================
|
|
3
|
+
Replace the following placeholders before use:
|
|
4
|
+
|
|
5
|
+
EXTENSION NAME
|
|
6
|
+
"myextension" → your extension identifier
|
|
7
|
+
|
|
8
|
+
VERSION
|
|
9
|
+
"1.0.0" → initial version
|
|
10
|
+
|
|
11
|
+
TOPICS
|
|
12
|
+
"topic1" → your skill topics
|
|
13
|
+
"topic2"
|
|
14
|
+
|
|
15
|
+
DESCRIPTION
|
|
16
|
+
"Use this skill when the user asks about..." → your skill description
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
---
|
|
20
|
+
name: myextension
|
|
21
|
+
description: Use this skill when the user asks about...
|
|
22
|
+
license: MIT
|
|
23
|
+
compatibility: opencode
|
|
24
|
+
metadata:
|
|
25
|
+
version: 1.0.0
|
|
26
|
+
audience: agents
|
|
27
|
+
topic: [topic1, topic2]
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Activation Triggers
|
|
31
|
+
|
|
32
|
+
**USE this skill when user asks about:**
|
|
33
|
+
- Category: "Example query"
|
|
34
|
+
|
|
35
|
+
**DO NOT USE for:**
|
|
36
|
+
- Non-technical topics
|
|
37
|
+
- Known specific repos
|
|
38
|
+
|
|
39
|
+
## Workflow Summary
|
|
40
|
+
|
|
41
|
+
<workflow>
|
|
42
|
+
<phase name="detect">Check available tools</phase>
|
|
43
|
+
<phase name="search">Execute search</phase>
|
|
44
|
+
<phase name="query">Query sources</phase>
|
|
45
|
+
<phase name="synthesize">Return answer</phase>
|
|
46
|
+
</workflow>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"target": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"types": ["bun-types"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["*.ts", "src/**/*.ts", "tests/**/*.ts"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Config } from "@opencode-ai/sdk";
|
|
2
|
+
|
|
3
|
+
export const command: NonNullable<Config["command"]>[string] = {
|
|
4
|
+
description: "Sync OpenCode docs into ~/.cache/opencode/opencode-architect/docs",
|
|
5
|
+
template:
|
|
6
|
+
"Use the sync-docs tool to fetch the latest OpenCode documentation into `~/.cache/opencode/opencode-architect/docs`.",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default command;
|