@rexeus/agentic 0.3.1
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 +202 -0
- package/README.md +201 -0
- package/assets/opencode/agents/analyst.md +358 -0
- package/assets/opencode/agents/architect.md +308 -0
- package/assets/opencode/agents/developer.md +311 -0
- package/assets/opencode/agents/lead.md +368 -0
- package/assets/opencode/agents/refiner.md +418 -0
- package/assets/opencode/agents/reviewer.md +285 -0
- package/assets/opencode/agents/scout.md +241 -0
- package/assets/opencode/agents/tester.md +323 -0
- package/assets/opencode/commands/agentic-commit.md +128 -0
- package/assets/opencode/commands/agentic-develop.md +170 -0
- package/assets/opencode/commands/agentic-plan.md +165 -0
- package/assets/opencode/commands/agentic-polish.md +190 -0
- package/assets/opencode/commands/agentic-pr.md +226 -0
- package/assets/opencode/commands/agentic-review.md +119 -0
- package/assets/opencode/commands/agentic-simplify.md +123 -0
- package/assets/opencode/commands/agentic-verify.md +193 -0
- package/bin/agentic.js +139 -0
- package/opencode/config.mjs +453 -0
- package/opencode/doctor.mjs +9 -0
- package/opencode/guardrails.mjs +172 -0
- package/opencode/install.mjs +48 -0
- package/opencode/manifest.mjs +34 -0
- package/opencode/plugin.mjs +53 -0
- package/opencode/uninstall.mjs +64 -0
- package/package.json +69 -0
- package/skills/conventions/SKILL.md +83 -0
- package/skills/git-conventions/SKILL.md +141 -0
- package/skills/quality-patterns/SKILL.md +73 -0
- package/skills/security/SKILL.md +77 -0
- package/skills/setup/SKILL.md +105 -0
- package/skills/testing/SKILL.md +113 -0
package/bin/agentic.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { doctorOpenCode } from "../opencode/doctor.mjs";
|
|
4
|
+
import { installOpenCode } from "../opencode/install.mjs";
|
|
5
|
+
import { uninstallOpenCode } from "../opencode/uninstall.mjs";
|
|
6
|
+
|
|
7
|
+
const printUsage = () => {
|
|
8
|
+
console.log(`Usage:
|
|
9
|
+
agentic install opencode
|
|
10
|
+
agentic uninstall opencode
|
|
11
|
+
agentic doctor`);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const fail = (message) => {
|
|
15
|
+
console.error(`agentic: ${message}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const printAssetDoctorStatus = (label, installedLabel, missingLabel, conflictLabel, status) => {
|
|
20
|
+
console.log(`${label} dir: ${status.dir}`);
|
|
21
|
+
console.log(`${label} present: ${status.presentFiles.length > 0 ? "yes" : "no"}`);
|
|
22
|
+
|
|
23
|
+
if (status.missingFiles.length === 0 && status.conflictingFiles.length === 0) {
|
|
24
|
+
console.log(`${installedLabel}: yes`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(
|
|
29
|
+
`${installedLabel}: no (${status.conflictingFiles.length} conflicting, ${status.missingFiles.length} missing)`,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
for (const fileName of status.conflictingFiles) {
|
|
33
|
+
console.log(`- ${conflictLabel}: ${fileName}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const fileName of status.missingFiles) {
|
|
37
|
+
console.log(`- ${missingLabel}: ${fileName}`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const run = async () => {
|
|
42
|
+
const command = process.argv[2];
|
|
43
|
+
const target = process.argv[3];
|
|
44
|
+
|
|
45
|
+
if (command === "install") {
|
|
46
|
+
if (target !== "opencode") {
|
|
47
|
+
fail("expected target 'opencode'");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = await installOpenCode();
|
|
51
|
+
console.log(`Installed OpenCode integration.`);
|
|
52
|
+
console.log(`Config: ${result.configPath}`);
|
|
53
|
+
if (result.backupPath) {
|
|
54
|
+
console.log(`Backup: ${result.backupPath}`);
|
|
55
|
+
}
|
|
56
|
+
console.log(`Commands: ${result.commandsDir}`);
|
|
57
|
+
console.log(`Agents: ${result.agentsDir}`);
|
|
58
|
+
console.log(`Skills: ${result.skillsDir}`);
|
|
59
|
+
if (
|
|
60
|
+
result.conflicts.commands.length > 0 ||
|
|
61
|
+
result.conflicts.agents.length > 0 ||
|
|
62
|
+
result.conflicts.skills.length > 0
|
|
63
|
+
) {
|
|
64
|
+
console.log("Conflicts detected with existing unowned OpenCode assets:");
|
|
65
|
+
for (const commandName of result.conflicts.commands) {
|
|
66
|
+
console.log(`- command conflict: ${commandName}`);
|
|
67
|
+
}
|
|
68
|
+
for (const agentName of result.conflicts.agents) {
|
|
69
|
+
console.log(`- agent conflict: ${agentName}`);
|
|
70
|
+
}
|
|
71
|
+
for (const skillName of result.conflicts.skills) {
|
|
72
|
+
console.log(`- skill conflict: ${skillName}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (command === "uninstall") {
|
|
79
|
+
if (target !== "opencode") {
|
|
80
|
+
fail("expected target 'opencode'");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = await uninstallOpenCode();
|
|
84
|
+
console.log(`Removed OpenCode integration.`);
|
|
85
|
+
console.log(`Config: ${result.configPath}`);
|
|
86
|
+
if (result.backupPath) {
|
|
87
|
+
console.log(`Backup: ${result.backupPath}`);
|
|
88
|
+
}
|
|
89
|
+
console.log(`Removed commands: ${result.removedCommands}`);
|
|
90
|
+
console.log(`Removed agents: ${result.removedAgents}`);
|
|
91
|
+
console.log(`Removed skills: ${result.removedSkills}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (command === "doctor") {
|
|
96
|
+
const result = await doctorOpenCode();
|
|
97
|
+
console.log("Agentic OpenCode Doctor");
|
|
98
|
+
console.log(`Config: ${result.configPath}`);
|
|
99
|
+
if (result.configError) {
|
|
100
|
+
console.log(`Config valid: no`);
|
|
101
|
+
console.log(`Config error: ${result.configError}`);
|
|
102
|
+
} else {
|
|
103
|
+
console.log(`Config valid: yes`);
|
|
104
|
+
}
|
|
105
|
+
console.log(`Plugin installed: ${result.pluginInstalled ? "yes" : "no"}`);
|
|
106
|
+
console.log(`Plugin via config: ${result.pluginSources.config ? "yes" : "no"}`);
|
|
107
|
+
console.log(`Plugin local files: ${result.pluginSources.localFiles.length}`);
|
|
108
|
+
for (const pluginFile of result.pluginSources.localFiles) {
|
|
109
|
+
console.log(`- plugin file: ${pluginFile}`);
|
|
110
|
+
}
|
|
111
|
+
printAssetDoctorStatus("Commands", "Commands installed", "missing", "command conflict", {
|
|
112
|
+
dir: result.commandsDir,
|
|
113
|
+
presentFiles: result.commands.presentFiles,
|
|
114
|
+
conflictingFiles: result.commands.conflictingFiles,
|
|
115
|
+
missingFiles: result.commands.missingFiles,
|
|
116
|
+
});
|
|
117
|
+
printAssetDoctorStatus("Agents", "Agents installed", "missing agent", "agent conflict", {
|
|
118
|
+
dir: result.agentsDir,
|
|
119
|
+
presentFiles: result.agents.presentFiles,
|
|
120
|
+
conflictingFiles: result.agents.conflictingFiles,
|
|
121
|
+
missingFiles: result.agents.missingFiles,
|
|
122
|
+
});
|
|
123
|
+
printAssetDoctorStatus("Skills", "Skills installed", "missing skill", "skill conflict", {
|
|
124
|
+
dir: result.skillsDir,
|
|
125
|
+
presentFiles: result.skills.presentFiles,
|
|
126
|
+
conflictingFiles: result.skills.conflictingFiles,
|
|
127
|
+
missingFiles: result.skills.missingFiles,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
printUsage();
|
|
134
|
+
process.exit(1);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
run().catch((error) => {
|
|
138
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
139
|
+
});
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { cp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
AGENT_FILES,
|
|
10
|
+
COMMAND_FILES,
|
|
11
|
+
LOCAL_PLUGIN_PREFIX,
|
|
12
|
+
PLUGIN_NAME,
|
|
13
|
+
SKILL_NAMES,
|
|
14
|
+
} from "./manifest.mjs";
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
const projectRoot = resolve(__dirname, "..");
|
|
19
|
+
|
|
20
|
+
const AGENT_MARKER = "Generated by pnpm run sync:opencode-agents";
|
|
21
|
+
const COMMAND_MARKER = "Installed by @rexeus/agentic command";
|
|
22
|
+
const SKILL_MARKER = "Installed by @rexeus/agentic";
|
|
23
|
+
|
|
24
|
+
export const getOpenCodeConfigDir = () => {
|
|
25
|
+
if (
|
|
26
|
+
typeof process.env.OPENCODE_CONFIG_DIR === "string" &&
|
|
27
|
+
process.env.OPENCODE_CONFIG_DIR.trim()
|
|
28
|
+
) {
|
|
29
|
+
return process.env.OPENCODE_CONFIG_DIR;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof process.env.HOME !== "string" || process.env.HOME.length === 0) {
|
|
33
|
+
throw new Error("Unable to resolve home directory");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return join(process.env.HOME, ".config", "opencode");
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const ensureOpenCodeConfigDir = () => {
|
|
40
|
+
const configDir = getOpenCodeConfigDir();
|
|
41
|
+
mkdirSync(configDir, { recursive: true });
|
|
42
|
+
return configDir;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const formatConfigError = (configPath, message) =>
|
|
46
|
+
`Invalid OpenCode config at ${configPath}: ${message}`;
|
|
47
|
+
|
|
48
|
+
const getPluginConfigError = (configPath, config) => {
|
|
49
|
+
if (Object.hasOwn(config, "plugin") && !Array.isArray(config.plugin)) {
|
|
50
|
+
return formatConfigError(
|
|
51
|
+
configPath,
|
|
52
|
+
"expected 'plugin' to be an array when present. No changes were made.",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getParsedConfig = (raw, configPath) => {
|
|
60
|
+
const errors = [];
|
|
61
|
+
const parsed = parse(raw, errors);
|
|
62
|
+
|
|
63
|
+
if (errors.length > 0) {
|
|
64
|
+
const [firstError] = errors;
|
|
65
|
+
return {
|
|
66
|
+
config: null,
|
|
67
|
+
error: formatConfigError(
|
|
68
|
+
configPath,
|
|
69
|
+
`${printParseErrorCode(firstError.error)} at offset ${firstError.offset}. No changes were made.`,
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
75
|
+
return {
|
|
76
|
+
config: null,
|
|
77
|
+
error: formatConfigError(configPath, "expected a JSON object. No changes were made."),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { config: parsed, error: null };
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const getConfigPath = (configDir) => {
|
|
85
|
+
const jsoncPath = join(configDir, "opencode.jsonc");
|
|
86
|
+
if (existsSync(jsoncPath)) {
|
|
87
|
+
return jsoncPath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const jsonPath = join(configDir, "opencode.json");
|
|
91
|
+
if (existsSync(jsonPath)) {
|
|
92
|
+
return jsonPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return jsonPath;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const getProjectConfigDir = () => join(process.cwd(), ".opencode");
|
|
99
|
+
|
|
100
|
+
const insertMarkerAfterFrontmatter = (content, marker) => {
|
|
101
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n\n?/);
|
|
102
|
+
if (!match) {
|
|
103
|
+
return `<!-- ${marker} -->\n\n${content}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const body = content.slice(match[0].length);
|
|
107
|
+
return `${match[0]}<!-- ${marker} -->\n\n${body}`;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const isMarkedFile = async (filePath, marker) => {
|
|
111
|
+
try {
|
|
112
|
+
const content = await readFile(filePath, "utf8");
|
|
113
|
+
return content.includes(marker);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const hasOwnPluginKey = (config) => Object.hasOwn(config, "plugin");
|
|
120
|
+
|
|
121
|
+
const getPluginEntryName = (entry) => {
|
|
122
|
+
if (typeof entry === "string") {
|
|
123
|
+
return entry;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(entry) && typeof entry[0] === "string") {
|
|
127
|
+
return entry[0];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const isAgenticPluginName = (name) =>
|
|
134
|
+
typeof name === "string" && (name === PLUGIN_NAME || name.startsWith(`${PLUGIN_NAME}@`));
|
|
135
|
+
|
|
136
|
+
const isAgenticPluginEntry = (entry) => isAgenticPluginName(getPluginEntryName(entry));
|
|
137
|
+
|
|
138
|
+
const getMarkedAssetStatus = async ({ files, marker, resolveTargetPath }) => {
|
|
139
|
+
const installedFiles = [];
|
|
140
|
+
const conflictingFiles = [];
|
|
141
|
+
const missingFiles = [];
|
|
142
|
+
|
|
143
|
+
for (const fileName of files) {
|
|
144
|
+
const targetPath = resolveTargetPath(fileName);
|
|
145
|
+
if (!existsSync(targetPath)) {
|
|
146
|
+
missingFiles.push(fileName);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (marker && (await isMarkedFile(targetPath, marker))) {
|
|
151
|
+
installedFiles.push(fileName);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
conflictingFiles.push(fileName);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
installedFiles,
|
|
160
|
+
conflictingFiles,
|
|
161
|
+
missingFiles,
|
|
162
|
+
presentFiles: [...installedFiles, ...conflictingFiles],
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const installAssetFiles = async ({ files, sourceDir, targetDir, marker }) => {
|
|
167
|
+
mkdirSync(targetDir, { recursive: true });
|
|
168
|
+
|
|
169
|
+
for (const fileName of files) {
|
|
170
|
+
const sourcePath = join(sourceDir, fileName);
|
|
171
|
+
const targetPath = join(targetDir, fileName);
|
|
172
|
+
if (existsSync(targetPath) && marker && !(await isMarkedFile(targetPath, marker))) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const sourceContent = readFileSync(sourcePath, "utf8");
|
|
177
|
+
const output = marker ? insertMarkerAfterFrontmatter(sourceContent, marker) : sourceContent;
|
|
178
|
+
await writeFile(targetPath, output, "utf8");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return targetDir;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const removeMarkedFiles = async ({ files, targetDir, marker }) => {
|
|
185
|
+
let removed = 0;
|
|
186
|
+
|
|
187
|
+
for (const fileName of files) {
|
|
188
|
+
const targetPath = join(targetDir, fileName);
|
|
189
|
+
if (!existsSync(targetPath)) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (marker && !(await isMarkedFile(targetPath, marker))) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await rm(targetPath, { force: true });
|
|
198
|
+
removed += 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return removed;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const detectLocalPluginFiles = async (pluginsDir) => {
|
|
205
|
+
if (!existsSync(pluginsDir)) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const entries = await readdir(pluginsDir, { withFileTypes: true });
|
|
210
|
+
const matches = [];
|
|
211
|
+
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if (!entry.isFile()) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
!entry.name.endsWith(".js") &&
|
|
219
|
+
!entry.name.endsWith(".mjs") &&
|
|
220
|
+
!entry.name.endsWith(".ts")
|
|
221
|
+
) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const filePath = join(pluginsDir, entry.name);
|
|
226
|
+
try {
|
|
227
|
+
const content = await readFile(filePath, "utf8");
|
|
228
|
+
if (
|
|
229
|
+
entry.name.startsWith(LOCAL_PLUGIN_PREFIX) ||
|
|
230
|
+
content.includes("AgenticPlugin") ||
|
|
231
|
+
content.includes(PLUGIN_NAME) ||
|
|
232
|
+
content.includes("/opencode/plugin.mjs")
|
|
233
|
+
) {
|
|
234
|
+
matches.push(filePath);
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return matches;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export const readOpenCodeConfig = (configDir) => {
|
|
245
|
+
const configPath = getConfigPath(configDir);
|
|
246
|
+
if (!existsSync(configPath)) {
|
|
247
|
+
return { configPath, config: {}, error: null };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const raw = readFileSync(configPath, "utf8");
|
|
251
|
+
const { config, error } = getParsedConfig(raw, configPath);
|
|
252
|
+
|
|
253
|
+
return { configPath, config, error };
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const validateOpenCodeConfigForMutation = (configPath, config) =>
|
|
257
|
+
getPluginConfigError(configPath, config);
|
|
258
|
+
|
|
259
|
+
export const backupConfigFile = async (configPath) => {
|
|
260
|
+
if (!existsSync(configPath)) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
265
|
+
await cp(configPath, backupPath);
|
|
266
|
+
return backupPath;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const writeOpenCodeConfig = async (configPath, config) => {
|
|
270
|
+
if (configPath.endsWith(".jsonc") && existsSync(configPath)) {
|
|
271
|
+
const raw = readFileSync(configPath, "utf8");
|
|
272
|
+
const { config: parsed, error } = getParsedConfig(raw, configPath);
|
|
273
|
+
|
|
274
|
+
if (error) {
|
|
275
|
+
throw new Error(error);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
279
|
+
const edits = modify(raw, ["plugin"], hasOwnPluginKey(config) ? config.plugin : undefined, {
|
|
280
|
+
formattingOptions: {
|
|
281
|
+
insertSpaces: true,
|
|
282
|
+
tabSize: 2,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
const normalized = `${applyEdits(raw, edits).trimEnd()}\n`;
|
|
286
|
+
await writeFile(configPath, normalized, "utf8");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const normalized = `${JSON.stringify(config, null, 2)}\n`;
|
|
292
|
+
await writeFile(configPath, normalized, "utf8");
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export const ensurePluginEntry = (config) => {
|
|
296
|
+
const plugins = Array.isArray(config.plugin) ? [...config.plugin] : [];
|
|
297
|
+
const existingAgenticEntry = plugins.find(isAgenticPluginEntry);
|
|
298
|
+
const withoutAgentic = plugins.filter((entry) => !isAgenticPluginEntry(entry));
|
|
299
|
+
withoutAgentic.push(Array.isArray(existingAgenticEntry) ? existingAgenticEntry : PLUGIN_NAME);
|
|
300
|
+
return { ...config, plugin: withoutAgentic };
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export const removePluginEntry = (config) => {
|
|
304
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
305
|
+
const filtered = plugins.filter((entry) => !isAgenticPluginEntry(entry));
|
|
306
|
+
|
|
307
|
+
if (filtered.length > 0) {
|
|
308
|
+
return { ...config, plugin: filtered };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const { plugin: _plugin, ...rest } = config;
|
|
312
|
+
return rest;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export const installGlobalCommands = async (configDir) =>
|
|
316
|
+
installAssetFiles({
|
|
317
|
+
files: COMMAND_FILES,
|
|
318
|
+
sourceDir: join(projectRoot, "assets", "opencode", "commands"),
|
|
319
|
+
targetDir: join(configDir, "commands"),
|
|
320
|
+
marker: COMMAND_MARKER,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
export const installGlobalAgents = async (configDir) =>
|
|
324
|
+
installAssetFiles({
|
|
325
|
+
files: AGENT_FILES,
|
|
326
|
+
sourceDir: join(projectRoot, "assets", "opencode", "agents"),
|
|
327
|
+
targetDir: join(configDir, "agents"),
|
|
328
|
+
marker: AGENT_MARKER,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
export const installGlobalSkills = async (configDir) => {
|
|
332
|
+
const skillsDir = join(configDir, "skills");
|
|
333
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
334
|
+
|
|
335
|
+
for (const skillName of SKILL_NAMES) {
|
|
336
|
+
const sourcePath = join(projectRoot, "skills", skillName, "SKILL.md");
|
|
337
|
+
const targetDir = join(skillsDir, skillName);
|
|
338
|
+
const targetPath = join(targetDir, "SKILL.md");
|
|
339
|
+
|
|
340
|
+
if (existsSync(targetPath) && !(await isMarkedFile(targetPath, SKILL_MARKER))) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
mkdirSync(targetDir, { recursive: true });
|
|
345
|
+
const sourceContent = readFileSync(sourcePath, "utf8");
|
|
346
|
+
await writeFile(targetPath, insertMarkerAfterFrontmatter(sourceContent, SKILL_MARKER), "utf8");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return skillsDir;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
export const uninstallGlobalCommands = async (configDir) => {
|
|
353
|
+
const commandsDir = join(configDir, "commands");
|
|
354
|
+
if (!existsSync(commandsDir)) {
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return removeMarkedFiles({
|
|
359
|
+
files: COMMAND_FILES,
|
|
360
|
+
targetDir: commandsDir,
|
|
361
|
+
marker: COMMAND_MARKER,
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export const uninstallGlobalAgents = async (configDir) => {
|
|
366
|
+
const agentsDir = join(configDir, "agents");
|
|
367
|
+
if (!existsSync(agentsDir)) {
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return removeMarkedFiles({ files: AGENT_FILES, targetDir: agentsDir, marker: AGENT_MARKER });
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
export const uninstallGlobalSkills = async (configDir) => {
|
|
375
|
+
const skillsDir = join(configDir, "skills");
|
|
376
|
+
if (!existsSync(skillsDir)) {
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let removed = 0;
|
|
381
|
+
|
|
382
|
+
for (const skillName of SKILL_NAMES) {
|
|
383
|
+
const targetDir = join(skillsDir, skillName);
|
|
384
|
+
const targetPath = join(targetDir, "SKILL.md");
|
|
385
|
+
if (!existsSync(targetPath)) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!(await isMarkedFile(targetPath, SKILL_MARKER))) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
await rm(targetPath, { force: true });
|
|
394
|
+
|
|
395
|
+
const remainingEntries = await readdir(targetDir);
|
|
396
|
+
if (remainingEntries.length === 0) {
|
|
397
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
removed += 1;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return removed;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
export const getDoctorStatus = async (configDir, config) => {
|
|
407
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
408
|
+
const pluginFromConfig = plugins.some((entry) => isAgenticPluginEntry(entry));
|
|
409
|
+
|
|
410
|
+
const globalPluginFiles = await detectLocalPluginFiles(join(configDir, "plugins"));
|
|
411
|
+
const projectPluginFiles = await detectLocalPluginFiles(join(getProjectConfigDir(), "plugins"));
|
|
412
|
+
const localPluginFiles = [...globalPluginFiles, ...projectPluginFiles];
|
|
413
|
+
|
|
414
|
+
const commandsDir = join(configDir, "commands");
|
|
415
|
+
const agentsDir = join(configDir, "agents");
|
|
416
|
+
const skillsDir = join(configDir, "skills");
|
|
417
|
+
|
|
418
|
+
const commandStatus = await getMarkedAssetStatus({
|
|
419
|
+
files: COMMAND_FILES,
|
|
420
|
+
marker: COMMAND_MARKER,
|
|
421
|
+
resolveTargetPath: (commandFile) => join(commandsDir, commandFile),
|
|
422
|
+
});
|
|
423
|
+
const agentStatus = await getMarkedAssetStatus({
|
|
424
|
+
files: AGENT_FILES,
|
|
425
|
+
marker: AGENT_MARKER,
|
|
426
|
+
resolveTargetPath: (agentFile) => join(agentsDir, agentFile),
|
|
427
|
+
});
|
|
428
|
+
const skillStatus = await getMarkedAssetStatus({
|
|
429
|
+
files: SKILL_NAMES,
|
|
430
|
+
marker: SKILL_MARKER,
|
|
431
|
+
resolveTargetPath: (skillName) => join(skillsDir, skillName, "SKILL.md"),
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
pluginInstalled: pluginFromConfig || localPluginFiles.length > 0,
|
|
436
|
+
pluginSources: {
|
|
437
|
+
config: pluginFromConfig,
|
|
438
|
+
localFiles: localPluginFiles,
|
|
439
|
+
},
|
|
440
|
+
commandsDir,
|
|
441
|
+
agentsDir,
|
|
442
|
+
skillsDir,
|
|
443
|
+
commands: commandStatus,
|
|
444
|
+
agents: agentStatus,
|
|
445
|
+
skills: skillStatus,
|
|
446
|
+
missingCommands: commandStatus.missingFiles,
|
|
447
|
+
missingAgents: agentStatus.missingFiles,
|
|
448
|
+
missingSkills: skillStatus.missingFiles,
|
|
449
|
+
conflictingCommands: commandStatus.conflictingFiles,
|
|
450
|
+
conflictingAgents: agentStatus.conflictingFiles,
|
|
451
|
+
conflictingSkills: skillStatus.conflictingFiles,
|
|
452
|
+
};
|
|
453
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getDoctorStatus, getOpenCodeConfigDir, readOpenCodeConfig } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
export const doctorOpenCode = async () => {
|
|
4
|
+
const configDir = getOpenCodeConfigDir();
|
|
5
|
+
const { configPath, config, error } = readOpenCodeConfig(configDir);
|
|
6
|
+
const status = await getDoctorStatus(configDir, config ?? {});
|
|
7
|
+
|
|
8
|
+
return { configPath, configError: error, ...status };
|
|
9
|
+
};
|