opencode-architect 0.2.4 → 0.4.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 +78 -18
- package/agent-loader.ts +98 -0
- package/assets/agents/opencode-agent-designer.md +13 -47
- package/assets/agents/opencode-architect.md +60 -244
- package/assets/agents/opencode-command-crafter.md +9 -27
- package/assets/agents/opencode-extension-auditor.md +25 -36
- package/assets/agents/opencode-mcp-integrator.md +13 -17
- package/assets/agents/opencode-packager.md +31 -203
- package/assets/agents/opencode-plugin-engineer.md +16 -29
- package/assets/agents/opencode-publisher.md +27 -206
- package/assets/agents/opencode-skill-creator.md +15 -35
- package/assets/agents/opencode-tool-builder.md +13 -17
- package/assets/references/agents.md +68 -0
- package/assets/references/commands.md +49 -0
- package/assets/references/config.md +50 -0
- package/assets/references/mcp-servers.md +55 -0
- package/assets/references/opencode-architect-oneshots.md +19 -58
- package/assets/references/plugins.md +76 -0
- package/assets/references/prompt-engineering.md +46 -0
- package/assets/references/skills.md +55 -0
- package/assets/references/tools.md +49 -0
- package/assets/templates/skill-structure.template.md +4 -4
- package/cli.ts +120 -0
- package/index.ts +4 -103
- package/installer.ts +319 -0
- package/package.json +30 -8
- package/assets/templates/package-analysis.template.md +0 -60
- package/commands/sync-docs.ts +0 -9
- package/scripts/fetch-opencode-docs.ts +0 -193
- package/scripts/logger.ts +0 -20
- package/tools/sync-docs.ts +0 -24
|
@@ -24,7 +24,7 @@ compatibility: opencode
|
|
|
24
24
|
metadata:
|
|
25
25
|
version: 1.0.0
|
|
26
26
|
audience: agents
|
|
27
|
-
topic:
|
|
27
|
+
topic: "topic1, topic2"
|
|
28
28
|
---
|
|
29
29
|
|
|
30
30
|
## Activation Triggers
|
|
@@ -32,9 +32,9 @@ metadata:
|
|
|
32
32
|
**USE this skill when user asks about:**
|
|
33
33
|
- Category: "Example query"
|
|
34
34
|
|
|
35
|
-
**
|
|
36
|
-
-
|
|
37
|
-
-
|
|
35
|
+
**Route elsewhere when:**
|
|
36
|
+
- The topic is non-technical
|
|
37
|
+
- The repo is well-known and already documented
|
|
38
38
|
|
|
39
39
|
## Workflow Summary
|
|
40
40
|
|
package/cli.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { Installer, type Scope } from "./installer";
|
|
4
|
+
|
|
5
|
+
const VERSION = (JSON.parse(await Bun.file(`${import.meta.dirname}/package.json`).text()) as { version: string }).version;
|
|
6
|
+
|
|
7
|
+
async function main(): Promise<void> {
|
|
8
|
+
const { positionals, values } = parseArgs({
|
|
9
|
+
options: {
|
|
10
|
+
scope: { type: "string", short: "s" },
|
|
11
|
+
force: { type: "boolean", short: "f", default: false },
|
|
12
|
+
help: { type: "boolean", short: "h", default: false },
|
|
13
|
+
version: { type: "boolean", short: "v", default: false },
|
|
14
|
+
},
|
|
15
|
+
allowPositionals: true,
|
|
16
|
+
strict: true,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (values.version) {
|
|
20
|
+
console.log(`opencode-architect v${VERSION}`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (values.help || positionals.length === 0) {
|
|
24
|
+
printHelp();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const command = positionals[0];
|
|
29
|
+
const scopeInput = values.scope;
|
|
30
|
+
if (scopeInput !== undefined && scopeInput !== "local" && scopeInput !== "global") {
|
|
31
|
+
console.error(`Invalid scope: ${scopeInput}. Must be "local" or "global".`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const scope: Scope = scopeInput === "global" ? "global" : "local";
|
|
35
|
+
const installer = new Installer();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
switch (command) {
|
|
39
|
+
case "install": {
|
|
40
|
+
const outcome = await installer.install(scope, { force: values.force, projectDir: process.cwd() });
|
|
41
|
+
if (outcome.action === "noop") {
|
|
42
|
+
console.log(`Already installed at version ${VERSION}; nothing to do.`);
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`${outcome.action === "installed" ? "Installed" : "Upgraded"} (${scope} scope):`);
|
|
45
|
+
console.log(` Agents: ${outcome.agentsDir}`);
|
|
46
|
+
console.log(` References: ${outcome.referencesDir}`);
|
|
47
|
+
console.log(` Templates: ${outcome.templatesDir}`);
|
|
48
|
+
}
|
|
49
|
+
if (outcome.pluginRemoved) {
|
|
50
|
+
console.log(" Removed the plugin entry from opencode.json (switched to copy install).");
|
|
51
|
+
}
|
|
52
|
+
if (outcome.skipped.length > 0) {
|
|
53
|
+
console.log(" Skipped locally modified files (re-run with --force to overwrite):");
|
|
54
|
+
for (const file of outcome.skipped) console.log(` ${file}`);
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "uninstall": {
|
|
59
|
+
const outcome = await installer.uninstall(scope, process.cwd());
|
|
60
|
+
if (outcome.mode === "none") {
|
|
61
|
+
console.log(`Nothing to uninstall for the ${scope} scope.`);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (outcome.mode === "copy") {
|
|
65
|
+
console.log(`Uninstalled (${scope} scope):`);
|
|
66
|
+
for (const file of outcome.removed) console.log(` Removed: ${file}`);
|
|
67
|
+
}
|
|
68
|
+
if (outcome.pluginRemoved) {
|
|
69
|
+
console.log(" Removed the plugin entry from opencode.json.");
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "status": {
|
|
74
|
+
const outcome = await installer.status(scope, process.cwd());
|
|
75
|
+
const version = outcome.version ?? "-";
|
|
76
|
+
console.log(`${scope} scope: mode=${outcome.mode} version=${version}`);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
default:
|
|
80
|
+
console.error(`Unknown command: ${command}`);
|
|
81
|
+
printHelp();
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
console.error(`Error: ${message}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function printHelp(): void {
|
|
92
|
+
console.log(`
|
|
93
|
+
opencode-architect v${VERSION}
|
|
94
|
+
|
|
95
|
+
Installs the opencode-architect agent suite by copying agents into the scope
|
|
96
|
+
base's agents/, references into opencode-architect/references/, and templates
|
|
97
|
+
into opencode-architect/templates/, rewriting relative reference paths to
|
|
98
|
+
absolute paths at install time. Copy install and plugin install are mutually
|
|
99
|
+
exclusive per scope.
|
|
100
|
+
|
|
101
|
+
Commands:
|
|
102
|
+
install Copy agents, references, and templates into the scope base
|
|
103
|
+
uninstall Remove exactly the files a copy install wrote (or the plugin entry)
|
|
104
|
+
status Show install mode and version for a scope
|
|
105
|
+
|
|
106
|
+
Options:
|
|
107
|
+
-s, --scope <scope> "local" (project .opencode/) or "global" (~/.config/opencode/); default local
|
|
108
|
+
-f, --force install: remove an existing plugin entry and overwrite locally modified files
|
|
109
|
+
-h, --help Show this help message
|
|
110
|
+
-v, --version Show version
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
opencode-architect install
|
|
114
|
+
opencode-architect install --scope global
|
|
115
|
+
opencode-architect uninstall
|
|
116
|
+
opencode-architect status
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
main();
|
package/index.ts
CHANGED
|
@@ -1,120 +1,21 @@
|
|
|
1
|
-
import type { Plugin
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
6
|
-
import { command as syncDocsCommand } from "./commands/sync-docs";
|
|
7
|
-
import { OpenCodeDocsFetcher } from "./scripts/fetch-opencode-docs";
|
|
8
|
-
import { SilentLogger } from "./scripts/logger";
|
|
9
|
-
import { createSyncDocsTool } from "./tools/sync-docs";
|
|
3
|
+
import { AgentLoader } from "./agent-loader";
|
|
10
4
|
|
|
11
5
|
const AGENTS_DIR = path.join(import.meta.dirname, "assets", "agents");
|
|
12
6
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
interface AgentFrontmatter {
|
|
16
|
-
description: string;
|
|
17
|
-
mode: "primary" | "subagent" | "all";
|
|
18
|
-
tools?: Record<string, boolean>;
|
|
19
|
-
permission?: {
|
|
20
|
-
edit?: "ask" | "allow" | "deny";
|
|
21
|
-
bash?: ("ask" | "allow" | "deny") | Record<string, "ask" | "allow" | "deny">;
|
|
22
|
-
webfetch?: "ask" | "allow" | "deny";
|
|
23
|
-
doom_loop?: "ask" | "allow" | "deny";
|
|
24
|
-
external_directory?: "ask" | "allow" | "deny";
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function parseAgentMarkdown(content: string, agentName: string): Promise<AgentConfig> {
|
|
29
|
-
const match = content.match(FRONTMATTER_REGEX);
|
|
30
|
-
|
|
31
|
-
if (!match || match.length < 3) {
|
|
32
|
-
throw new Error(`Agent ${agentName} must have YAML frontmatter`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const frontmatterYaml = match[1] as string;
|
|
36
|
-
const rawPrompt = match[2] as string;
|
|
37
|
-
const frontmatter = parseYaml(frontmatterYaml) as AgentFrontmatter;
|
|
38
|
-
const prompt = rawPrompt.replace(/^\r?\n/, "");
|
|
39
|
-
|
|
40
|
-
const config: AgentConfig = {
|
|
41
|
-
description: frontmatter.description,
|
|
42
|
-
mode: frontmatter.mode,
|
|
43
|
-
prompt,
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
if (frontmatter.tools) {
|
|
47
|
-
config.tools = frontmatter.tools;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (frontmatter.permission) {
|
|
51
|
-
config.permission = frontmatter.permission;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return config;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const OpencodeArchitect: Plugin = async (input) => {
|
|
58
|
-
const syncDocsTool = createSyncDocsTool();
|
|
59
|
-
|
|
60
|
-
syncDocsOnStartup(input.client);
|
|
61
|
-
|
|
62
|
-
const agents = await loadAgents();
|
|
7
|
+
const OpencodeArchitect: Plugin = async () => {
|
|
8
|
+
const agents = await new AgentLoader(AGENTS_DIR).loadAgents();
|
|
63
9
|
|
|
64
10
|
return {
|
|
65
11
|
config: async (config) => {
|
|
66
12
|
config.agent = config.agent || {};
|
|
67
|
-
config.command = config.command || {};
|
|
68
13
|
|
|
69
14
|
for (const [name, agentConfig] of Object.entries(agents)) {
|
|
70
15
|
config.agent[name] = agentConfig;
|
|
71
16
|
}
|
|
72
|
-
|
|
73
|
-
config.command["sync-docs"] = syncDocsCommand;
|
|
74
|
-
},
|
|
75
|
-
tool: {
|
|
76
|
-
"sync-docs": syncDocsTool,
|
|
77
17
|
},
|
|
78
18
|
};
|
|
79
19
|
};
|
|
80
20
|
|
|
81
21
|
export default OpencodeArchitect;
|
|
82
|
-
|
|
83
|
-
async function loadAgents(): Promise<Record<string, AgentConfig>> {
|
|
84
|
-
const agentFiles = [
|
|
85
|
-
"opencode-agent-designer.md",
|
|
86
|
-
"opencode-architect.md",
|
|
87
|
-
"opencode-command-crafter.md",
|
|
88
|
-
"opencode-extension-auditor.md",
|
|
89
|
-
"opencode-packager.md",
|
|
90
|
-
"opencode-publisher.md",
|
|
91
|
-
"opencode-mcp-integrator.md",
|
|
92
|
-
"opencode-plugin-engineer.md",
|
|
93
|
-
"opencode-skill-creator.md",
|
|
94
|
-
"opencode-tool-builder.md",
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
const agents: Record<string, AgentConfig> = {};
|
|
98
|
-
|
|
99
|
-
for (const filename of agentFiles) {
|
|
100
|
-
const agentPath = path.join(AGENTS_DIR, filename);
|
|
101
|
-
const agentContent = await readFile(agentPath, "utf-8");
|
|
102
|
-
const agentName = path.basename(filename, ".md");
|
|
103
|
-
agents[agentName] = await parseAgentMarkdown(agentContent, agentName);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return agents;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function syncDocsOnStartup(client: PluginInput["client"]): void {
|
|
110
|
-
const fetcher = new OpenCodeDocsFetcher(new SilentLogger());
|
|
111
|
-
fetcher.run().catch((error: unknown) => {
|
|
112
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
-
client.tui.showToast({
|
|
114
|
-
body: {
|
|
115
|
-
message: `Failed to sync OpenCode docs: ${message}`,
|
|
116
|
-
variant: "error",
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
}
|
package/installer.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { copyFile, exists, mkdir, readdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { AGENT_FILENAMES, RELATIVE_REFERENCE_REGEX } from "./agent-loader";
|
|
6
|
+
|
|
7
|
+
export type Scope = "local" | "global";
|
|
8
|
+
export type InstallMode = "none" | "copy" | "plugin";
|
|
9
|
+
export type InstallAction = "installed" | "upgraded" | "noop";
|
|
10
|
+
|
|
11
|
+
export interface ManifestHashEntry {
|
|
12
|
+
path: string;
|
|
13
|
+
hash: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Manifest {
|
|
17
|
+
version: string;
|
|
18
|
+
agentFiles: string[];
|
|
19
|
+
referencesDir: string;
|
|
20
|
+
templatesDir: string;
|
|
21
|
+
hashes: ManifestHashEntry[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface InstallOptions {
|
|
25
|
+
force: boolean;
|
|
26
|
+
projectDir: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface InstallOutcome {
|
|
30
|
+
action: InstallAction;
|
|
31
|
+
scope: Scope;
|
|
32
|
+
agentsDir: string;
|
|
33
|
+
referencesDir: string;
|
|
34
|
+
templatesDir: string;
|
|
35
|
+
manifestPath: string;
|
|
36
|
+
copied: string[];
|
|
37
|
+
skipped: string[];
|
|
38
|
+
overwritten: string[];
|
|
39
|
+
pluginRemoved: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UninstallOutcome {
|
|
43
|
+
scope: Scope;
|
|
44
|
+
mode: InstallMode;
|
|
45
|
+
removed: string[];
|
|
46
|
+
pluginRemoved: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface StatusOutcome {
|
|
50
|
+
scope: Scope;
|
|
51
|
+
mode: InstallMode;
|
|
52
|
+
version: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const PACKAGE_NAME = "opencode-architect";
|
|
56
|
+
const MANIFEST_NAME = "opencode-architect.json";
|
|
57
|
+
const ASSETS_AGENTS_DIR = path.join(import.meta.dirname, "assets", "agents");
|
|
58
|
+
const ASSETS_REFERENCES_DIR = path.join(import.meta.dirname, "assets", "references");
|
|
59
|
+
const ASSETS_TEMPLATES_DIR = path.join(import.meta.dirname, "assets", "templates");
|
|
60
|
+
|
|
61
|
+
export class Installer {
|
|
62
|
+
public async install(scope: Scope, options: InstallOptions): Promise<InstallOutcome> {
|
|
63
|
+
const base = this.scopeBase(scope, options.projectDir);
|
|
64
|
+
const configPath = path.join(base, "opencode.json");
|
|
65
|
+
const manifestPath = path.join(base, MANIFEST_NAME);
|
|
66
|
+
const agentsDir = path.join(base, "agents");
|
|
67
|
+
const referencesDir = path.join(base, "opencode-architect", "references");
|
|
68
|
+
const templatesDir = path.join(base, "opencode-architect", "templates");
|
|
69
|
+
const version = await this.getPackageVersion();
|
|
70
|
+
|
|
71
|
+
let pluginRemoved = false;
|
|
72
|
+
if (await this.hasPluginEntry(configPath)) {
|
|
73
|
+
if (!options.force) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`The "${PACKAGE_NAME}" plugin entry in ${configPath} must be removed before a copy install ` +
|
|
76
|
+
`(copied agents would silently shadow the plugin's agents). Re-run with --force to remove ` +
|
|
77
|
+
`the entry and switch this scope to copy install.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
await this.removePluginEntry(configPath);
|
|
81
|
+
pluginRemoved = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const existingManifest = await this.readManifest(manifestPath);
|
|
85
|
+
if (existingManifest !== null && existingManifest.version === version) {
|
|
86
|
+
return {
|
|
87
|
+
action: "noop",
|
|
88
|
+
scope,
|
|
89
|
+
agentsDir,
|
|
90
|
+
referencesDir,
|
|
91
|
+
templatesDir,
|
|
92
|
+
manifestPath,
|
|
93
|
+
copied: [],
|
|
94
|
+
skipped: [],
|
|
95
|
+
overwritten: [],
|
|
96
|
+
pluginRemoved,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const action: InstallAction = existingManifest === null ? "installed" : "upgraded";
|
|
101
|
+
const copied: string[] = [];
|
|
102
|
+
const skipped: string[] = [];
|
|
103
|
+
const overwritten: string[] = [];
|
|
104
|
+
const hashes: ManifestHashEntry[] = [];
|
|
105
|
+
|
|
106
|
+
await mkdir(agentsDir, { recursive: true });
|
|
107
|
+
await mkdir(referencesDir, { recursive: true });
|
|
108
|
+
await mkdir(templatesDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
for (const filename of AGENT_FILENAMES) {
|
|
111
|
+
const relativePath = path.join("agents", filename);
|
|
112
|
+
const { action: fileAction, priorHash } = await this.disposition(
|
|
113
|
+
existingManifest,
|
|
114
|
+
base,
|
|
115
|
+
relativePath,
|
|
116
|
+
options.force,
|
|
117
|
+
);
|
|
118
|
+
if (fileAction === "skip") {
|
|
119
|
+
skipped.push(relativePath);
|
|
120
|
+
hashes.push({ path: relativePath, hash: priorHash as string });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const source = await readFile(path.join(ASSETS_AGENTS_DIR, filename), "utf-8");
|
|
124
|
+
await writeFile(path.join(base, relativePath), rewriteReferencePaths(source, referencesDir, templatesDir));
|
|
125
|
+
hashes.push({ path: relativePath, hash: await this.sha256File(path.join(base, relativePath)) });
|
|
126
|
+
if (fileAction === "overwrite") overwritten.push(relativePath);
|
|
127
|
+
else copied.push(relativePath);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const entry of await readdir(ASSETS_REFERENCES_DIR)) {
|
|
131
|
+
const relativePath = path.join("opencode-architect", "references", entry);
|
|
132
|
+
const { action: fileAction, priorHash } = await this.disposition(
|
|
133
|
+
existingManifest,
|
|
134
|
+
base,
|
|
135
|
+
relativePath,
|
|
136
|
+
options.force,
|
|
137
|
+
);
|
|
138
|
+
if (fileAction === "skip") {
|
|
139
|
+
skipped.push(relativePath);
|
|
140
|
+
hashes.push({ path: relativePath, hash: priorHash as string });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
await copyFile(path.join(ASSETS_REFERENCES_DIR, entry), path.join(base, relativePath));
|
|
144
|
+
hashes.push({ path: relativePath, hash: await this.sha256File(path.join(base, relativePath)) });
|
|
145
|
+
if (fileAction === "overwrite") overwritten.push(relativePath);
|
|
146
|
+
else copied.push(relativePath);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const entry of await readdir(ASSETS_TEMPLATES_DIR)) {
|
|
150
|
+
const relativePath = path.join("opencode-architect", "templates", entry);
|
|
151
|
+
const { action: fileAction, priorHash } = await this.disposition(
|
|
152
|
+
existingManifest,
|
|
153
|
+
base,
|
|
154
|
+
relativePath,
|
|
155
|
+
options.force,
|
|
156
|
+
);
|
|
157
|
+
if (fileAction === "skip") {
|
|
158
|
+
skipped.push(relativePath);
|
|
159
|
+
hashes.push({ path: relativePath, hash: priorHash as string });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
await copyFile(path.join(ASSETS_TEMPLATES_DIR, entry), path.join(base, relativePath));
|
|
163
|
+
hashes.push({ path: relativePath, hash: await this.sha256File(path.join(base, relativePath)) });
|
|
164
|
+
if (fileAction === "overwrite") overwritten.push(relativePath);
|
|
165
|
+
else copied.push(relativePath);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const manifest: Manifest = { version, agentFiles: [...AGENT_FILENAMES], referencesDir, templatesDir, hashes };
|
|
169
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
170
|
+
|
|
171
|
+
return { action, scope, agentsDir, referencesDir, templatesDir, manifestPath, copied, skipped, overwritten, pluginRemoved };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public async uninstall(scope: Scope, projectDir: string): Promise<UninstallOutcome> {
|
|
175
|
+
const base = this.scopeBase(scope, projectDir);
|
|
176
|
+
const configPath = path.join(base, "opencode.json");
|
|
177
|
+
const manifestPath = path.join(base, MANIFEST_NAME);
|
|
178
|
+
const manifest = await this.readManifest(manifestPath);
|
|
179
|
+
const hadPluginEntry = await this.hasPluginEntry(configPath);
|
|
180
|
+
const removed: string[] = [];
|
|
181
|
+
|
|
182
|
+
if (manifest !== null) {
|
|
183
|
+
for (const entry of manifest.hashes) {
|
|
184
|
+
const target = path.join(base, entry.path);
|
|
185
|
+
if (await exists(target)) {
|
|
186
|
+
await rm(target);
|
|
187
|
+
removed.push(target);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await rm(manifestPath);
|
|
191
|
+
removed.push(manifestPath);
|
|
192
|
+
await this.removeIfEmpty(path.join(base, "opencode-architect", "templates"));
|
|
193
|
+
await this.removeIfEmpty(path.join(base, "opencode-architect", "references"));
|
|
194
|
+
await this.removeIfEmpty(path.join(base, "opencode-architect"));
|
|
195
|
+
await this.removeIfEmpty(path.join(base, "agents"));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let pluginRemoved = false;
|
|
199
|
+
if (hadPluginEntry) {
|
|
200
|
+
await this.removePluginEntry(configPath);
|
|
201
|
+
pluginRemoved = true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const mode: InstallMode = manifest !== null ? "copy" : hadPluginEntry ? "plugin" : "none";
|
|
205
|
+
return { scope, mode, removed, pluginRemoved };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public async status(scope: Scope, projectDir: string): Promise<StatusOutcome> {
|
|
209
|
+
const base = this.scopeBase(scope, projectDir);
|
|
210
|
+
const manifest = await this.readManifest(path.join(base, MANIFEST_NAME));
|
|
211
|
+
|
|
212
|
+
if (manifest !== null) {
|
|
213
|
+
return { scope, mode: "copy", version: manifest.version };
|
|
214
|
+
}
|
|
215
|
+
if (await this.hasPluginEntry(path.join(base, "opencode.json"))) {
|
|
216
|
+
return { scope, mode: "plugin", version: null };
|
|
217
|
+
}
|
|
218
|
+
return { scope, mode: "none", version: null };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async disposition(
|
|
222
|
+
manifest: Manifest | null,
|
|
223
|
+
base: string,
|
|
224
|
+
relativePath: string,
|
|
225
|
+
force: boolean,
|
|
226
|
+
): Promise<{ action: "copy" | "overwrite" | "skip"; priorHash: string | null }> {
|
|
227
|
+
if (manifest === null) return { action: "copy", priorHash: null };
|
|
228
|
+
const priorHash = manifest.hashes.find((entry) => entry.path === relativePath)?.hash ?? null;
|
|
229
|
+
if (priorHash === null) return { action: "copy", priorHash };
|
|
230
|
+
const installedPath = path.join(base, relativePath);
|
|
231
|
+
if (!(await exists(installedPath))) return { action: "copy", priorHash };
|
|
232
|
+
if ((await this.sha256File(installedPath)) === priorHash) return { action: "copy", priorHash };
|
|
233
|
+
return { action: force ? "overwrite" : "skip", priorHash };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async sha256File(filePath: string): Promise<string> {
|
|
237
|
+
return createHash("sha256").update(await readFile(filePath)).digest("hex");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private scopeBase(scope: Scope, projectDir: string): string {
|
|
241
|
+
if (scope === "local") return path.join(projectDir, ".opencode");
|
|
242
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
243
|
+
if (xdgConfigHome) return path.join(xdgConfigHome, "opencode");
|
|
244
|
+
return path.join(homedir(), ".config", "opencode");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async readManifest(manifestPath: string): Promise<Manifest | null> {
|
|
248
|
+
try {
|
|
249
|
+
return JSON.parse(await readFile(manifestPath, "utf-8")) as Manifest;
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async hasPluginEntry(configPath: string): Promise<boolean> {
|
|
256
|
+
const plugins = await this.readPluginArray(configPath);
|
|
257
|
+
return plugins.some((name) => this.isPluginEntry(name));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async removePluginEntry(configPath: string): Promise<void> {
|
|
261
|
+
const plugins = await this.readPluginArray(configPath);
|
|
262
|
+
const remaining = plugins.filter((name) => !this.isPluginEntry(name));
|
|
263
|
+
const config = await this.readConfig(configPath);
|
|
264
|
+
if (remaining.length === 0) delete config.plugin;
|
|
265
|
+
else config.plugin = remaining;
|
|
266
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
267
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private isPluginEntry(name: string): boolean {
|
|
271
|
+
return name === PACKAGE_NAME || name.startsWith(`${PACKAGE_NAME}@`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private async readPluginArray(configPath: string): Promise<string[]> {
|
|
275
|
+
const config = await this.readConfig(configPath);
|
|
276
|
+
const plugins = config.plugin;
|
|
277
|
+
return Array.isArray(plugins) ? plugins.filter((name): name is string => typeof name === "string") : [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private async readConfig(configPath: string): Promise<Record<string, unknown>> {
|
|
281
|
+
try {
|
|
282
|
+
return JSON.parse(await readFile(configPath, "utf-8")) as Record<string, unknown>;
|
|
283
|
+
} catch {
|
|
284
|
+
return {};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private async getPackageVersion(): Promise<string> {
|
|
289
|
+
const content = await readFile(path.join(import.meta.dirname, "package.json"), "utf-8");
|
|
290
|
+
return (JSON.parse(content) as { version: string }).version;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async removeIfEmpty(directory: string): Promise<void> {
|
|
294
|
+
if (!(await exists(directory))) return;
|
|
295
|
+
const contents = await readdir(directory);
|
|
296
|
+
if (contents.length === 0) await rmdir(directory);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function rewriteReferencePaths(content: string, referencesDir: string, templatesDir: string): string {
|
|
301
|
+
return content.replace(
|
|
302
|
+
RELATIVE_REFERENCE_REGEX,
|
|
303
|
+
(token: string, relativePath: string): string => {
|
|
304
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
305
|
+
const packagedPath = path.resolve(ASSETS_AGENTS_DIR, normalized);
|
|
306
|
+
const withinReferences = path.relative(ASSETS_REFERENCES_DIR, packagedPath);
|
|
307
|
+
if (!withinReferences.startsWith("..")) {
|
|
308
|
+
const installedPath = path.resolve(referencesDir, withinReferences).replaceAll("\\", "/");
|
|
309
|
+
return `\`${installedPath}\``;
|
|
310
|
+
}
|
|
311
|
+
const withinTemplates = path.relative(ASSETS_TEMPLATES_DIR, packagedPath);
|
|
312
|
+
if (!withinTemplates.startsWith("..")) {
|
|
313
|
+
const installedPath = path.resolve(templatesDir, withinTemplates).replaceAll("\\", "/");
|
|
314
|
+
return `\`${installedPath}\``;
|
|
315
|
+
}
|
|
316
|
+
return token;
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-architect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "OpenCode plugin and CLI with ten specialist agents for agent skills, slash commands, custom tools, plugins, and MCP server integration",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"opencode",
|
|
7
|
+
"opencode-plugin",
|
|
8
|
+
"opencode-agents",
|
|
9
|
+
"agent-skills",
|
|
10
|
+
"ai-agents",
|
|
11
|
+
"slash-commands",
|
|
12
|
+
"mcp",
|
|
13
|
+
"mcp-server",
|
|
14
|
+
"coding-agent",
|
|
15
|
+
"bun"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
4
18
|
"private": false,
|
|
5
19
|
"type": "module",
|
|
6
20
|
"module": "index.ts",
|
|
21
|
+
"bin": {
|
|
22
|
+
"opencode-architect": "cli.ts"
|
|
23
|
+
},
|
|
7
24
|
"repository": {
|
|
8
25
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/
|
|
26
|
+
"url": "git+https://github.com/Expert-Vision-Software/opencode-architect.git"
|
|
10
27
|
},
|
|
28
|
+
"homepage": "https://github.com/Expert-Vision-Software/opencode-architect#readme",
|
|
11
29
|
"publisher": "Expert Vision Software",
|
|
12
30
|
"author": "Expert Vision Support <support@expertvision.software>",
|
|
13
31
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
32
|
+
"url": "https://github.com/Expert-Vision-Software/opencode-architect/issues",
|
|
15
33
|
"email": "support@expertvision.software"
|
|
16
34
|
},
|
|
17
35
|
"scripts": {
|
|
18
36
|
"check": "tsc --noEmit",
|
|
37
|
+
"test": "bun test",
|
|
19
38
|
"release": "npm version patch && npm publish && git push --follow-tags"
|
|
20
39
|
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
21
43
|
"files": [
|
|
22
44
|
"index.ts",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
45
|
+
"agent-loader.ts",
|
|
46
|
+
"cli.ts",
|
|
47
|
+
"installer.ts",
|
|
26
48
|
"assets"
|
|
27
49
|
],
|
|
28
50
|
"dependencies": {
|
|
@@ -30,9 +52,9 @@
|
|
|
30
52
|
"@opencode-ai/plugin": "*"
|
|
31
53
|
},
|
|
32
54
|
"devDependencies": {
|
|
33
|
-
"@opencode-ai/plugin": "latest",
|
|
34
55
|
"@opencode-ai/sdk": "latest",
|
|
35
56
|
"@types/bun": "latest",
|
|
36
|
-
"@types/node": "latest"
|
|
57
|
+
"@types/node": "latest",
|
|
58
|
+
"typescript": "latest"
|
|
37
59
|
}
|
|
38
60
|
}
|