@moskala/oneagent-core 0.2.5 → 0.2.6
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/package.json +1 -1
- package/src/agents.ts +3 -3
- package/src/apply-template.ts +90 -1
- package/src/opencode.ts +14 -0
- package/src/symlinks.ts +7 -9
package/package.json
CHANGED
package/src/agents.ts
CHANGED
|
@@ -49,11 +49,11 @@ export const AGENT_DEFINITIONS: AgentDefinition[] = [
|
|
|
49
49
|
{
|
|
50
50
|
target: "opencode",
|
|
51
51
|
displayName: "OpenCode",
|
|
52
|
-
hint: "AGENTS.md + opencode
|
|
52
|
+
hint: "AGENTS.md + .opencode/",
|
|
53
53
|
detectIndicators: ["opencode.json", ".opencode"],
|
|
54
54
|
mainFile: "AGENTS.md",
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
rulesDir: ".opencode/rules",
|
|
56
|
+
skillsDir: ".opencode/skills",
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
target: "copilot",
|
package/src/apply-template.ts
CHANGED
|
@@ -2,17 +2,49 @@ import path from "path";
|
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import { execFile } from "child_process";
|
|
4
4
|
import { promisify } from "util";
|
|
5
|
+
import type { AgentTarget } from "./types.ts";
|
|
6
|
+
import { addOpenCodePlugin } from "./opencode.ts";
|
|
5
7
|
|
|
6
8
|
const execFileAsync = promisify(execFile);
|
|
7
9
|
|
|
10
|
+
export interface TemplatePlugin {
|
|
11
|
+
target: AgentTarget;
|
|
12
|
+
id: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
export interface TemplateDefinition {
|
|
9
16
|
name: string;
|
|
10
17
|
description: string;
|
|
11
18
|
skills: string[];
|
|
19
|
+
plugins: TemplatePlugin[];
|
|
12
20
|
instructions: string;
|
|
13
21
|
rules: Array<{ name: string; content: string }>;
|
|
14
22
|
}
|
|
15
23
|
|
|
24
|
+
// Parses the `plugins:` block from a template.yml string.
|
|
25
|
+
// Expects entries in the format:
|
|
26
|
+
// plugins:
|
|
27
|
+
// - target: claude
|
|
28
|
+
// id: typescript-lsp@claude-plugins-official
|
|
29
|
+
export function parsePluginsFromYaml(yamlText: string): TemplatePlugin[] {
|
|
30
|
+
const plugins: TemplatePlugin[] = [];
|
|
31
|
+
const section = yamlText.match(/^plugins:\s*\n((?:(?: -.+|\s{4}.+)\n?)*)/m);
|
|
32
|
+
if (!section) return plugins;
|
|
33
|
+
const block = section[1]!;
|
|
34
|
+
const entries = block.split(/\n(?= -)/);
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const targetMatch = entry.match(/target:\s*(\S+)/);
|
|
37
|
+
const idMatch = entry.match(/id:\s*(.+)/);
|
|
38
|
+
if (targetMatch && idMatch) {
|
|
39
|
+
plugins.push({
|
|
40
|
+
target: targetMatch[1]!.trim() as AgentTarget,
|
|
41
|
+
id: idMatch[1]!.trim(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return plugins;
|
|
46
|
+
}
|
|
47
|
+
|
|
16
48
|
// Phase 1: writes instructions.md and rules/*.md.
|
|
17
49
|
// Call this BEFORE generate() so symlinks to rules are created.
|
|
18
50
|
export async function applyTemplateFiles(root: string, template: TemplateDefinition): Promise<void> {
|
|
@@ -46,6 +78,61 @@ export async function installTemplateSkills(
|
|
|
46
78
|
}
|
|
47
79
|
}
|
|
48
80
|
|
|
81
|
+
export interface PluginInstallResult {
|
|
82
|
+
installed: TemplatePlugin[];
|
|
83
|
+
manual: TemplatePlugin[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Phase 3: installs plugins for active targets. Call AFTER generate().
|
|
87
|
+
// - claude → `claude plugin install <id>`
|
|
88
|
+
// - copilot → `copilot plugin install <id>`
|
|
89
|
+
// - opencode → adds id to plugin[] in opencode.json
|
|
90
|
+
// - cursor → added to manual list (no CLI yet — user runs /add-plugin in chat)
|
|
91
|
+
// - windsurf → skipped (no marketplace)
|
|
92
|
+
export async function installTemplatePlugins(
|
|
93
|
+
root: string,
|
|
94
|
+
template: TemplateDefinition,
|
|
95
|
+
activeTargets: AgentTarget[],
|
|
96
|
+
onPluginInstalled?: (plugin: TemplatePlugin) => void,
|
|
97
|
+
): Promise<PluginInstallResult> {
|
|
98
|
+
const installed: TemplatePlugin[] = [];
|
|
99
|
+
const manual: TemplatePlugin[] = [];
|
|
100
|
+
|
|
101
|
+
for (const plugin of template.plugins) {
|
|
102
|
+
if (!activeTargets.includes(plugin.target)) continue;
|
|
103
|
+
|
|
104
|
+
switch (plugin.target) {
|
|
105
|
+
case "claude":
|
|
106
|
+
await execFileAsync("claude", ["plugin", "install", plugin.id], { cwd: root });
|
|
107
|
+
installed.push(plugin);
|
|
108
|
+
onPluginInstalled?.(plugin);
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case "copilot":
|
|
112
|
+
await execFileAsync("copilot", ["plugin", "install", plugin.id], { cwd: root });
|
|
113
|
+
installed.push(plugin);
|
|
114
|
+
onPluginInstalled?.(plugin);
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case "opencode":
|
|
118
|
+
await addOpenCodePlugin(root, plugin.id);
|
|
119
|
+
installed.push(plugin);
|
|
120
|
+
onPluginInstalled?.(plugin);
|
|
121
|
+
break;
|
|
122
|
+
|
|
123
|
+
case "cursor":
|
|
124
|
+
manual.push(plugin);
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
case "windsurf":
|
|
128
|
+
// No marketplace yet — skip silently
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { installed, manual };
|
|
134
|
+
}
|
|
135
|
+
|
|
49
136
|
// Fetches a template from a GitHub URL.
|
|
50
137
|
// Expects the repository to contain: template.yml, instructions.md, and optionally rules/*.md
|
|
51
138
|
export async function fetchTemplateFromGitHub(url: string): Promise<TemplateDefinition> {
|
|
@@ -74,10 +161,12 @@ export async function fetchTemplateFromGitHub(url: string): Promise<TemplateDefi
|
|
|
74
161
|
}
|
|
75
162
|
}
|
|
76
163
|
|
|
164
|
+
const plugins = parsePluginsFromYaml(yamlText);
|
|
165
|
+
|
|
77
166
|
// Try to list rules via GitHub API
|
|
78
167
|
const rules = await fetchGitHubRules(url);
|
|
79
168
|
|
|
80
|
-
return { name, description, skills, instructions, rules };
|
|
169
|
+
return { name, description, skills, plugins, instructions, rules };
|
|
81
170
|
}
|
|
82
171
|
|
|
83
172
|
async function fetchText(url: string): Promise<string> {
|
package/src/opencode.ts
CHANGED
|
@@ -18,6 +18,20 @@ export function buildOpencodeConfig(existing: Record<string, unknown> | null): o
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export async function addOpenCodePlugin(root: string, id: string): Promise<void> {
|
|
22
|
+
const filePath = path.join(root, "opencode.json");
|
|
23
|
+
let existing: Record<string, unknown>;
|
|
24
|
+
try {
|
|
25
|
+
existing = JSON.parse(await fs.readFile(filePath, "utf-8")) as Record<string, unknown>;
|
|
26
|
+
} catch {
|
|
27
|
+
return; // no opencode.json — no-op
|
|
28
|
+
}
|
|
29
|
+
const current = Array.isArray(existing.plugin) ? (existing.plugin as string[]) : [];
|
|
30
|
+
if (current.includes(id)) return;
|
|
31
|
+
existing.plugin = [...current, id];
|
|
32
|
+
await fs.writeFile(filePath, JSON.stringify(existing, null, 2) + "\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
export async function writeOpencode(root: string, _rules: RuleFile[]): Promise<void> {
|
|
22
36
|
const existing = await readOpencode(root);
|
|
23
37
|
const config = buildOpencodeConfig(existing);
|
package/src/symlinks.ts
CHANGED
|
@@ -10,9 +10,9 @@ export async function ensureDir(dirPath: string): Promise<void> {
|
|
|
10
10
|
export async function createSymlink(symlinkPath: string, target: string): Promise<void> {
|
|
11
11
|
await ensureDir(path.dirname(symlinkPath));
|
|
12
12
|
try {
|
|
13
|
-
await fs.
|
|
13
|
+
await fs.rm(symlinkPath, { recursive: true });
|
|
14
14
|
} catch {
|
|
15
|
-
//
|
|
15
|
+
// doesn't exist — that's fine
|
|
16
16
|
}
|
|
17
17
|
await fs.symlink(target, symlinkPath);
|
|
18
18
|
}
|
|
@@ -122,14 +122,12 @@ async function migrateAndRemoveDir(src: string, dest: string, root: string): Pro
|
|
|
122
122
|
export async function migrateRuleAndSkillFiles(root: string): Promise<void> {
|
|
123
123
|
const destRules = path.join(root, ".oneagent/rules");
|
|
124
124
|
const destSkills = path.join(root, ".oneagent/skills");
|
|
125
|
-
// Rules dirs:
|
|
126
|
-
// The directories themselves stay — generate() recreates per-file symlinks inside them.
|
|
125
|
+
// Rules dirs: the entire directory becomes a symlink, so move files out and remove the dir.
|
|
127
126
|
// Sequential to avoid same-name conflicts across dirs.
|
|
128
|
-
await
|
|
129
|
-
await
|
|
130
|
-
await
|
|
131
|
-
|
|
132
|
-
// so the real directory must be removed first to make room for the symlink.
|
|
127
|
+
await migrateAndRemoveDir(path.join(root, ".cursor/rules"), destRules, root);
|
|
128
|
+
await migrateAndRemoveDir(path.join(root, ".claude/rules"), destRules, root);
|
|
129
|
+
await migrateAndRemoveDir(path.join(root, ".windsurf/rules"), destRules, root);
|
|
130
|
+
await migrateAndRemoveDir(path.join(root, ".opencode/rules"), destRules, root);
|
|
133
131
|
await migrateAndRemoveDir(path.join(root, ".agents/skills"), destSkills, root);
|
|
134
132
|
}
|
|
135
133
|
|