@moskala/oneagent-core 0.2.5 → 0.3.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/package.json +1 -1
- package/src/agents.ts +9 -4
- package/src/apply-template.ts +180 -37
- package/src/commands.ts +16 -0
- package/src/copilot.ts +3 -7
- package/src/detect.ts +0 -11
- package/src/generate.ts +11 -6
- package/src/index.ts +1 -0
- package/src/opencode.ts +14 -0
- package/src/rules.ts +4 -22
- package/src/status.ts +8 -5
- package/src/symlinks.ts +24 -11
- package/src/types.ts +5 -2
package/package.json
CHANGED
package/src/agents.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface AgentDefinition {
|
|
|
12
12
|
rulesDir?: string;
|
|
13
13
|
/** Whole-dir symlink for skills (relative to root). Omit if not applicable. */
|
|
14
14
|
skillsDir?: string;
|
|
15
|
+
/** Whole-dir symlink for commands (relative to root). Omit if agent does not support custom commands. */
|
|
16
|
+
commandsDir?: string;
|
|
15
17
|
/** Legacy files to remove during init (superseded by current format). */
|
|
16
18
|
deprecatedFiles?: string[];
|
|
17
19
|
}
|
|
@@ -25,6 +27,7 @@ export const AGENT_DEFINITIONS: AgentDefinition[] = [
|
|
|
25
27
|
mainFile: "CLAUDE.md",
|
|
26
28
|
rulesDir: ".claude/rules",
|
|
27
29
|
skillsDir: ".claude/skills",
|
|
30
|
+
commandsDir: ".claude/commands",
|
|
28
31
|
},
|
|
29
32
|
{
|
|
30
33
|
target: "cursor",
|
|
@@ -35,6 +38,7 @@ export const AGENT_DEFINITIONS: AgentDefinition[] = [
|
|
|
35
38
|
rulesDir: ".cursor/rules",
|
|
36
39
|
skillsDir: ".cursor/skills",
|
|
37
40
|
deprecatedFiles: [".cursorrules"],
|
|
41
|
+
commandsDir: ".cursor/commands",
|
|
38
42
|
},
|
|
39
43
|
{
|
|
40
44
|
target: "windsurf",
|
|
@@ -49,11 +53,12 @@ export const AGENT_DEFINITIONS: AgentDefinition[] = [
|
|
|
49
53
|
{
|
|
50
54
|
target: "opencode",
|
|
51
55
|
displayName: "OpenCode",
|
|
52
|
-
hint: "AGENTS.md + opencode
|
|
56
|
+
hint: "AGENTS.md + .opencode/",
|
|
53
57
|
detectIndicators: ["opencode.json", ".opencode"],
|
|
54
58
|
mainFile: "AGENTS.md",
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
rulesDir: ".opencode/rules",
|
|
60
|
+
skillsDir: ".opencode/skills",
|
|
61
|
+
commandsDir: ".opencode/commands",
|
|
57
62
|
},
|
|
58
63
|
{
|
|
59
64
|
target: "copilot",
|
|
@@ -62,7 +67,7 @@ export const AGENT_DEFINITIONS: AgentDefinition[] = [
|
|
|
62
67
|
detectIndicators: [".github/copilot-instructions.md", ".github"],
|
|
63
68
|
mainFile: ".github/copilot-instructions.md",
|
|
64
69
|
skillsDir: ".github/skills",
|
|
65
|
-
// rules: generated as
|
|
70
|
+
// rules: generated as <name>.instructions.md files, not symlinks
|
|
66
71
|
},
|
|
67
72
|
];
|
|
68
73
|
|
package/src/apply-template.ts
CHANGED
|
@@ -2,17 +2,90 @@ 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
|
+
|
|
15
|
+
export interface SkillEntry {
|
|
16
|
+
repo: string;
|
|
17
|
+
skill: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
export interface TemplateDefinition {
|
|
9
21
|
name: string;
|
|
10
22
|
description: string;
|
|
11
|
-
skills:
|
|
23
|
+
skills: SkillEntry[];
|
|
24
|
+
plugins: TemplatePlugin[];
|
|
12
25
|
instructions: string;
|
|
13
26
|
rules: Array<{ name: string; content: string }>;
|
|
14
27
|
}
|
|
15
28
|
|
|
29
|
+
// Parses name, description, skills and plugins from a template.yml string.
|
|
30
|
+
// This is the single source of truth for the template.yml format — used by
|
|
31
|
+
// both builtin template loading and GitHub URL template fetching.
|
|
32
|
+
export function parseTemplateYaml(yamlText: string, fallbackName = "custom"): Pick<TemplateDefinition, "name" | "description" | "skills" | "plugins"> {
|
|
33
|
+
const nameMatch = yamlText.match(/^name:\s*(.+)$/m);
|
|
34
|
+
const name = nameMatch?.[1]?.trim() ?? fallbackName;
|
|
35
|
+
|
|
36
|
+
const descMatch = yamlText.match(/^description:\s*(.+)$/m);
|
|
37
|
+
const description = descMatch?.[1]?.trim() ?? "";
|
|
38
|
+
|
|
39
|
+
const skills = parseSkillsFromYaml(yamlText);
|
|
40
|
+
const plugins = parsePluginsFromYaml(yamlText);
|
|
41
|
+
return { name, description, skills, plugins };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Parses the `skills:` block from a template.yml string.
|
|
45
|
+
// Expects entries in the format:
|
|
46
|
+
// skills:
|
|
47
|
+
// - repo: https://github.com/owner/skills
|
|
48
|
+
// skill: skill-name
|
|
49
|
+
export function parseSkillsFromYaml(yamlText: string): SkillEntry[] {
|
|
50
|
+
const skills: SkillEntry[] = [];
|
|
51
|
+
const section = yamlText.match(/^skills:\s*\n((?:(?: -.+|\s{4}.+)\n?)*)/m);
|
|
52
|
+
if (!section) return skills;
|
|
53
|
+
const block = section[1]!;
|
|
54
|
+
const entries = block.split(/\n(?= -)/);
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const repoMatch = entry.match(/repo:\s*(\S+)/);
|
|
57
|
+
const skillMatch = entry.match(/skill:\s*(\S+)/);
|
|
58
|
+
if (repoMatch && skillMatch) {
|
|
59
|
+
skills.push({ repo: repoMatch[1]!.trim(), skill: skillMatch[1]!.trim() });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return skills;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Parses the `plugins:` block from a template.yml string.
|
|
66
|
+
// Expects entries in the format:
|
|
67
|
+
// plugins:
|
|
68
|
+
// - target: claude
|
|
69
|
+
// id: typescript-lsp@claude-plugins-official
|
|
70
|
+
export function parsePluginsFromYaml(yamlText: string): TemplatePlugin[] {
|
|
71
|
+
const plugins: TemplatePlugin[] = [];
|
|
72
|
+
const section = yamlText.match(/^plugins:\s*\n((?:(?: -.+|\s{4}.+)\n?)*)/m);
|
|
73
|
+
if (!section) return plugins;
|
|
74
|
+
const block = section[1]!;
|
|
75
|
+
const entries = block.split(/\n(?= -)/);
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const targetMatch = entry.match(/target:\s*(\S+)/);
|
|
78
|
+
const idMatch = entry.match(/id:\s*(.+)/);
|
|
79
|
+
if (targetMatch && idMatch) {
|
|
80
|
+
plugins.push({
|
|
81
|
+
target: targetMatch[1]!.trim() as AgentTarget,
|
|
82
|
+
id: idMatch[1]!.trim(),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return plugins;
|
|
87
|
+
}
|
|
88
|
+
|
|
16
89
|
// Phase 1: writes instructions.md and rules/*.md.
|
|
17
90
|
// Call this BEFORE generate() so symlinks to rules are created.
|
|
18
91
|
export async function applyTemplateFiles(root: string, template: TemplateDefinition): Promise<void> {
|
|
@@ -28,22 +101,93 @@ export async function applyTemplateFiles(root: string, template: TemplateDefinit
|
|
|
28
101
|
}
|
|
29
102
|
}
|
|
30
103
|
|
|
31
|
-
|
|
32
|
-
|
|
104
|
+
export interface SkillInstallResult {
|
|
105
|
+
installed: SkillEntry[];
|
|
106
|
+
failed: Array<{ entry: SkillEntry; reason: string }>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Phase 2: installs skills via `npx skills add <repo> --skill <name>`.
|
|
110
|
+
// All skills are installed in parallel. Call this AFTER generate() so agent directories exist.
|
|
111
|
+
// Never throws — failed skills are collected and returned in the result.
|
|
33
112
|
export async function installTemplateSkills(
|
|
34
113
|
root: string,
|
|
35
114
|
template: TemplateDefinition,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
115
|
+
): Promise<SkillInstallResult> {
|
|
116
|
+
const results = await Promise.all(
|
|
117
|
+
template.skills.map(async (entry) => {
|
|
118
|
+
try {
|
|
119
|
+
await execFileAsync("npx", ["skills", "add", entry.repo, "--skill", entry.skill, "--agent", "universal", "--yes"], { cwd: root });
|
|
120
|
+
return { entry, ok: true as const };
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
123
|
+
return { entry, ok: false as const, reason };
|
|
124
|
+
}
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
installed: results.filter((r) => r.ok).map((r) => r.entry),
|
|
130
|
+
failed: results.filter((r) => !r.ok).map((r) => ({ entry: r.entry, reason: (r as { reason: string }).reason })),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface PluginInstallResult {
|
|
135
|
+
installed: TemplatePlugin[];
|
|
136
|
+
manual: TemplatePlugin[];
|
|
137
|
+
failed: Array<{ plugin: TemplatePlugin; reason: string }>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Phase 3: installs plugins for active targets. Call AFTER generate().
|
|
141
|
+
// - claude → `claude plugin install <id>`
|
|
142
|
+
// - copilot → `copilot plugin install <id>`
|
|
143
|
+
// - opencode → adds id to plugin[] in opencode.json
|
|
144
|
+
// - cursor → added to manual list (no CLI yet — user runs /add-plugin in chat)
|
|
145
|
+
// - windsurf → skipped (no marketplace)
|
|
146
|
+
// Never throws — failed plugins are collected and returned in the result.
|
|
147
|
+
export async function installTemplatePlugins(
|
|
148
|
+
root: string,
|
|
149
|
+
template: TemplateDefinition,
|
|
150
|
+
activeTargets: AgentTarget[],
|
|
151
|
+
): Promise<PluginInstallResult> {
|
|
152
|
+
const installed: TemplatePlugin[] = [];
|
|
153
|
+
const manual: TemplatePlugin[] = [];
|
|
154
|
+
const failed: Array<{ plugin: TemplatePlugin; reason: string }> = [];
|
|
155
|
+
|
|
156
|
+
for (const plugin of template.plugins) {
|
|
157
|
+
if (!activeTargets.includes(plugin.target)) continue;
|
|
158
|
+
|
|
39
159
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
160
|
+
switch (plugin.target) {
|
|
161
|
+
case "claude":
|
|
162
|
+
await execFileAsync("claude", ["plugin", "install", plugin.id], { cwd: root });
|
|
163
|
+
installed.push(plugin);
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case "copilot":
|
|
167
|
+
await execFileAsync("copilot", ["plugin", "install", plugin.id], { cwd: root });
|
|
168
|
+
installed.push(plugin);
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case "opencode":
|
|
172
|
+
await addOpenCodePlugin(root, plugin.id);
|
|
173
|
+
installed.push(plugin);
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case "cursor":
|
|
177
|
+
manual.push(plugin);
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case "windsurf":
|
|
181
|
+
// No marketplace yet — skip silently
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
42
184
|
} catch (err) {
|
|
43
|
-
const
|
|
44
|
-
|
|
185
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
186
|
+
failed.push({ plugin, reason });
|
|
45
187
|
}
|
|
46
188
|
}
|
|
189
|
+
|
|
190
|
+
return { installed, manual, failed };
|
|
47
191
|
}
|
|
48
192
|
|
|
49
193
|
// Fetches a template from a GitHub URL.
|
|
@@ -58,26 +202,12 @@ export async function fetchTemplateFromGitHub(url: string): Promise<TemplateDefi
|
|
|
58
202
|
fetchText(`${rawBase}/instructions.md`),
|
|
59
203
|
]);
|
|
60
204
|
|
|
61
|
-
const
|
|
62
|
-
const description = descMatch?.[1]?.trim() ?? "";
|
|
63
|
-
|
|
64
|
-
const nameMatch = yamlText.match(/^name:\s*(.+)$/m);
|
|
65
|
-
const name = nameMatch?.[1]?.trim() ?? "custom";
|
|
66
|
-
|
|
67
|
-
const skills: string[] = [];
|
|
68
|
-
const skillsBlockMatch = yamlText.match(/^skills:\s*\n((?: - .+\n?)*)/m);
|
|
69
|
-
if (skillsBlockMatch) {
|
|
70
|
-
const lines = skillsBlockMatch[1]!.split("\n").filter(Boolean);
|
|
71
|
-
for (const line of lines) {
|
|
72
|
-
const skill = line.replace(/^\s*-\s*/, "").trim();
|
|
73
|
-
if (skill) skills.push(skill);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
205
|
+
const { name, description, skills, plugins } = parseTemplateYaml(yamlText);
|
|
76
206
|
|
|
77
207
|
// Try to list rules via GitHub API
|
|
78
208
|
const rules = await fetchGitHubRules(url);
|
|
79
209
|
|
|
80
|
-
return { name, description, skills, instructions, rules };
|
|
210
|
+
return { name, description, skills, plugins, instructions, rules };
|
|
81
211
|
}
|
|
82
212
|
|
|
83
213
|
async function fetchText(url: string): Promise<string> {
|
|
@@ -88,23 +218,36 @@ async function fetchText(url: string): Promise<string> {
|
|
|
88
218
|
return response.text();
|
|
89
219
|
}
|
|
90
220
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
221
|
+
interface GitHubUrlParts {
|
|
222
|
+
owner: string;
|
|
223
|
+
repo: string;
|
|
224
|
+
branch: string;
|
|
225
|
+
subdir: string; // "" for root, "path/to/dir" for subdirectories
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function parseGitHubUrl(url: string): GitHubUrlParts {
|
|
229
|
+
// Supports:
|
|
230
|
+
// https://github.com/owner/repo
|
|
231
|
+
// https://github.com/owner/repo/tree/branch
|
|
232
|
+
// https://github.com/owner/repo/tree/branch/path/to/subdir
|
|
233
|
+
const match = url.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\/tree\/([^/]+?)(?:\/(.+))?)?(?:\/)?$/);
|
|
94
234
|
if (!match) {
|
|
95
235
|
throw new Error(`Invalid GitHub URL: "${url}". Expected format: https://github.com/owner/repo`);
|
|
96
236
|
}
|
|
97
|
-
const [, owner, repo, branch = "main"] = match;
|
|
98
|
-
return
|
|
237
|
+
const [, owner, repo, branch = "main", subdir = ""] = match;
|
|
238
|
+
return { owner: owner!, repo: repo!, branch, subdir };
|
|
99
239
|
}
|
|
100
240
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
241
|
+
function githubUrlToRawBase(url: string): string {
|
|
242
|
+
const { owner, repo, branch, subdir } = parseGitHubUrl(url);
|
|
243
|
+
const base = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`;
|
|
244
|
+
return subdir ? `${base}/${subdir}` : base;
|
|
245
|
+
}
|
|
106
246
|
|
|
107
|
-
|
|
247
|
+
async function fetchGitHubRules(repoUrl: string): Promise<Array<{ name: string; content: string }>> {
|
|
248
|
+
const { owner, repo, branch, subdir } = parseGitHubUrl(repoUrl);
|
|
249
|
+
const rulesPath = subdir ? `${subdir}/rules` : "rules";
|
|
250
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${rulesPath}?ref=${branch}`;
|
|
108
251
|
try {
|
|
109
252
|
const response = await fetch(apiUrl, {
|
|
110
253
|
headers: { Accept: "application/vnd.github.v3+json" },
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import type { CommandFile } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export async function readCommands(root: string): Promise<CommandFile[]> {
|
|
6
|
+
const commandsDir = path.join(root, ".oneagent/commands");
|
|
7
|
+
try {
|
|
8
|
+
const files = await fs.readdir(commandsDir);
|
|
9
|
+
return files
|
|
10
|
+
.filter((f) => f.endsWith(".md"))
|
|
11
|
+
.map((f) => ({ name: path.basename(f, ".md"), path: path.join(commandsDir, f) }))
|
|
12
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/copilot.ts
CHANGED
|
@@ -2,18 +2,14 @@ import path from "path";
|
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import type { RuleFile, SkillFile } from "./types.ts";
|
|
4
4
|
|
|
5
|
-
export function buildCopilotContent(rule: RuleFile): string {
|
|
6
|
-
return `---\napplyTo: "${rule.applyTo}"\n---\n${rule.content}`;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
export function copilotFilePath(root: string, ruleName: string): string {
|
|
10
6
|
return path.join(root, ".github/instructions", `${ruleName}.instructions.md`);
|
|
11
7
|
}
|
|
12
8
|
|
|
13
9
|
export async function generateCopilotRule(root: string, rule: RuleFile): Promise<void> {
|
|
14
|
-
const
|
|
15
|
-
await fs.mkdir(path.dirname(
|
|
16
|
-
await fs.
|
|
10
|
+
const destPath = copilotFilePath(root, rule.name);
|
|
11
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
12
|
+
await fs.copyFile(rule.path, destPath);
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
export async function generateCopilotRules(root: string, rules: RuleFile[]): Promise<void> {
|
package/src/detect.ts
CHANGED
|
@@ -61,14 +61,3 @@ export async function removeDeprecatedFiles(root: string): Promise<void> {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export async function detectDeprecatedCommandFiles(root: string): Promise<string[]> {
|
|
65
|
-
const commandsDir = path.join(root, ".claude/commands");
|
|
66
|
-
try {
|
|
67
|
-
const entries = await fs.readdir(commandsDir, { withFileTypes: true });
|
|
68
|
-
return entries
|
|
69
|
-
.filter((e) => e.isFile())
|
|
70
|
-
.map((e) => path.join(".claude/commands", e.name));
|
|
71
|
-
} catch {
|
|
72
|
-
return [];
|
|
73
|
-
}
|
|
74
|
-
}
|
package/src/generate.ts
CHANGED
|
@@ -4,8 +4,8 @@ import type { Config, DetectedFile } from "./types.ts";
|
|
|
4
4
|
import { activeTargets } from "./config.ts";
|
|
5
5
|
import { readRules } from "./rules.ts";
|
|
6
6
|
import { readSkills } from "./skills.ts";
|
|
7
|
-
import { buildMainSymlinks, buildRulesSymlinks, buildSkillSymlinks, buildAgentsDirSymlinks, createAllSymlinks, migrateRuleAndSkillFiles } from "./symlinks.ts";
|
|
8
|
-
import {
|
|
7
|
+
import { buildMainSymlinks, buildRulesSymlinks, buildSkillSymlinks, buildCommandSymlinks, buildAgentsDirSymlinks, createAllSymlinks, migrateRuleAndSkillFiles } from "./symlinks.ts";
|
|
8
|
+
import { copilotFilePath, buildCopilotPromptContent, copilotPromptFilePath, generateCopilotRules, generateCopilotSkills } from "./copilot.ts";
|
|
9
9
|
import { writeOpencode } from "./opencode.ts";
|
|
10
10
|
import { readDetectedFile } from "./detect.ts";
|
|
11
11
|
|
|
@@ -24,6 +24,7 @@ export async function detectGenerateCollisions(root: string, config: Config): Pr
|
|
|
24
24
|
const ruleSkillEntries = [
|
|
25
25
|
...buildRulesSymlinks(root, targets),
|
|
26
26
|
...buildSkillSymlinks(root, targets),
|
|
27
|
+
...buildCommandSymlinks(root, targets),
|
|
27
28
|
// .agents/skills skipped — handled by migrateAgentsSkillsDir
|
|
28
29
|
];
|
|
29
30
|
|
|
@@ -41,10 +42,13 @@ export async function detectGenerateCollisions(root: string, config: Config): Pr
|
|
|
41
42
|
...rules.map(async (rule): Promise<DetectedFile | null> => {
|
|
42
43
|
const filePath = copilotFilePath(root, rule.name);
|
|
43
44
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
45
|
+
const [source, dest] = await Promise.all([
|
|
46
|
+
fs.readFile(rule.path, "utf-8"),
|
|
47
|
+
fs.readFile(filePath, "utf-8"),
|
|
48
|
+
]);
|
|
49
|
+
if (source === dest) return null;
|
|
46
50
|
const stat = await fs.lstat(filePath);
|
|
47
|
-
return { relativePath: path.relative(root, filePath), absolutePath: filePath, sizeBytes: stat.size, modifiedAt: stat.mtime, content };
|
|
51
|
+
return { relativePath: path.relative(root, filePath), absolutePath: filePath, sizeBytes: stat.size, modifiedAt: stat.mtime, content: dest };
|
|
48
52
|
} catch { return null; }
|
|
49
53
|
}),
|
|
50
54
|
...skills.map(async (skill): Promise<DetectedFile | null> => {
|
|
@@ -75,7 +79,8 @@ export async function generate(root: string, config: Config): Promise<void> {
|
|
|
75
79
|
const mainSymlinks = buildMainSymlinks(root, targets);
|
|
76
80
|
const rulesSymlinks = buildRulesSymlinks(root, targets);
|
|
77
81
|
const skillSymlinks = await buildSkillSymlinks(root, targets);
|
|
78
|
-
|
|
82
|
+
const commandSymlinks = buildCommandSymlinks(root, targets);
|
|
83
|
+
await createAllSymlinks([...mainSymlinks, ...rulesSymlinks, ...skillSymlinks, ...commandSymlinks, ...buildAgentsDirSymlinks(root)]);
|
|
79
84
|
|
|
80
85
|
if (targets.includes("copilot")) {
|
|
81
86
|
await Promise.all([generateCopilotRules(root, rules), generateCopilotSkills(root, skills)]);
|
package/src/index.ts
CHANGED
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/rules.ts
CHANGED
|
@@ -2,32 +2,14 @@ import path from "path";
|
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import type { RuleFile } from "./types.ts";
|
|
4
4
|
|
|
5
|
-
export function parseFrontmatter(raw: string): { applyTo: string; content: string } {
|
|
6
|
-
const match = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
7
|
-
if (!match) return { applyTo: "**", content: raw };
|
|
8
|
-
|
|
9
|
-
const frontmatter = match[1] ?? "";
|
|
10
|
-
const content = match[2] ?? "";
|
|
11
|
-
|
|
12
|
-
const applyToMatch = frontmatter.match(/applyTo:\s*["']?([^"'\n]+)["']?/);
|
|
13
|
-
const applyTo = applyToMatch?.[1]?.trim() ?? "**";
|
|
14
|
-
|
|
15
|
-
return { applyTo, content };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function readRuleFile(filePath: string): Promise<RuleFile> {
|
|
19
|
-
const raw = await fs.readFile(filePath, "utf-8");
|
|
20
|
-
const { applyTo, content } = parseFrontmatter(raw);
|
|
21
|
-
return { name: path.basename(filePath, ".md"), path: filePath, applyTo, content };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
5
|
export async function readRules(root: string): Promise<RuleFile[]> {
|
|
25
6
|
const rulesDir = path.join(root, ".oneagent/rules");
|
|
26
7
|
try {
|
|
27
8
|
const files = await fs.readdir(rulesDir);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
9
|
+
return files
|
|
10
|
+
.filter((f) => f.endsWith(".md"))
|
|
11
|
+
.map((f) => ({ name: path.basename(f, ".md"), path: path.join(rulesDir, f) }))
|
|
12
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
31
13
|
} catch {
|
|
32
14
|
return [];
|
|
33
15
|
}
|
package/src/status.ts
CHANGED
|
@@ -3,16 +3,18 @@ import type { Config, GeneratedFileCheck, OpenCodeCheck, RuleFile, StatusResult
|
|
|
3
3
|
import { activeTargets } from "./config.ts";
|
|
4
4
|
import { readRules } from "./rules.ts";
|
|
5
5
|
import { readSkills } from "./skills.ts";
|
|
6
|
-
import { buildMainSymlinks, buildRulesSymlinks, buildSkillSymlinks, buildAgentsDirSymlinks, checkSymlink } from "./symlinks.ts";
|
|
7
|
-
import {
|
|
6
|
+
import { buildMainSymlinks, buildRulesSymlinks, buildSkillSymlinks, buildCommandSymlinks, buildAgentsDirSymlinks, checkSymlink } from "./symlinks.ts";
|
|
7
|
+
import { buildCopilotPromptContent, copilotFilePath, copilotPromptFilePath } from "./copilot.ts";
|
|
8
8
|
import { readOpencode } from "./opencode.ts";
|
|
9
9
|
|
|
10
10
|
export async function checkGeneratedFile(root: string, rule: RuleFile): Promise<GeneratedFileCheck> {
|
|
11
11
|
const filePath = copilotFilePath(root, rule.name);
|
|
12
|
-
const expected = buildCopilotContent(rule);
|
|
13
12
|
try {
|
|
14
|
-
const
|
|
15
|
-
|
|
13
|
+
const [source, dest] = await Promise.all([
|
|
14
|
+
fs.readFile(rule.path, "utf-8"),
|
|
15
|
+
fs.readFile(filePath, "utf-8"),
|
|
16
|
+
]);
|
|
17
|
+
return { path: filePath, exists: true, upToDate: source === dest };
|
|
16
18
|
} catch {
|
|
17
19
|
return { path: filePath, exists: false, upToDate: false };
|
|
18
20
|
}
|
|
@@ -46,6 +48,7 @@ export async function checkStatus(root: string, config: Config): Promise<StatusR
|
|
|
46
48
|
...buildMainSymlinks(root, targets),
|
|
47
49
|
...buildRulesSymlinks(root, targets),
|
|
48
50
|
...buildSkillSymlinks(root, targets),
|
|
51
|
+
...buildCommandSymlinks(root, targets),
|
|
49
52
|
...buildAgentsDirSymlinks(root),
|
|
50
53
|
];
|
|
51
54
|
|
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
|
}
|
|
@@ -60,6 +60,16 @@ export function buildSkillSymlinks(root: string, targets: AgentTarget[]): Symlin
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export function buildCommandSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
64
|
+
const targetAbs = path.join(root, ".oneagent/commands");
|
|
65
|
+
return AGENT_DEFINITIONS
|
|
66
|
+
.filter((d) => targets.includes(d.target) && d.commandsDir)
|
|
67
|
+
.map((d) => {
|
|
68
|
+
const symlinkPath = path.join(root, d.commandsDir!);
|
|
69
|
+
return { symlinkPath, target: relativeTarget(symlinkPath, targetAbs), label: d.commandsDir! };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
export function buildAgentsDirSymlinks(root: string): SymlinkEntry[] {
|
|
64
74
|
const symlinkPath = path.join(root, ".agents/skills");
|
|
65
75
|
const targetAbs = path.join(root, ".oneagent/skills");
|
|
@@ -122,19 +132,22 @@ async function migrateAndRemoveDir(src: string, dest: string, root: string): Pro
|
|
|
122
132
|
export async function migrateRuleAndSkillFiles(root: string): Promise<void> {
|
|
123
133
|
const destRules = path.join(root, ".oneagent/rules");
|
|
124
134
|
const destSkills = path.join(root, ".oneagent/skills");
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// .agents/skills is different: the entire directory becomes a symlink to .oneagent/skills,
|
|
132
|
-
// so the real directory must be removed first to make room for the symlink.
|
|
135
|
+
const destCommands = path.join(root, ".oneagent/commands");
|
|
136
|
+
// Derive migration sources from agent definitions — sequential to avoid same-name conflicts.
|
|
137
|
+
for (const def of AGENT_DEFINITIONS) {
|
|
138
|
+
if (def.rulesDir) await migrateAndRemoveDir(path.join(root, def.rulesDir), destRules, root);
|
|
139
|
+
}
|
|
140
|
+
// .agents/skills — standard skills.sh path, not represented in agent definitions
|
|
133
141
|
await migrateAndRemoveDir(path.join(root, ".agents/skills"), destSkills, root);
|
|
142
|
+
for (const def of AGENT_DEFINITIONS) {
|
|
143
|
+
if (def.commandsDir) await migrateAndRemoveDir(path.join(root, def.commandsDir), destCommands, root);
|
|
144
|
+
}
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
export async function createAllSymlinks(entries: SymlinkEntry[]): Promise<void> {
|
|
137
|
-
|
|
148
|
+
for (const e of entries) {
|
|
149
|
+
await createSymlink(e.symlinkPath, e.target);
|
|
150
|
+
}
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
export async function checkSymlink(entry: SymlinkEntry): Promise<SymlinkCheck> {
|
package/src/types.ts
CHANGED
|
@@ -16,8 +16,11 @@ export interface DetectedFile {
|
|
|
16
16
|
export interface RuleFile {
|
|
17
17
|
name: string;
|
|
18
18
|
path: string;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CommandFile {
|
|
22
|
+
name: string;
|
|
23
|
+
path: string;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export interface SkillFile {
|