@moskala/oneagent-core 0.2.6 → 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/package.json +1 -1
- package/src/agents.ts +6 -1
- package/src/apply-template.ts +124 -70
- 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/rules.ts +4 -22
- package/src/status.ts +8 -5
- package/src/symlinks.ts +22 -7
- 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",
|
|
@@ -54,6 +58,7 @@ export const AGENT_DEFINITIONS: AgentDefinition[] = [
|
|
|
54
58
|
mainFile: "AGENTS.md",
|
|
55
59
|
rulesDir: ".opencode/rules",
|
|
56
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
|
@@ -12,15 +12,56 @@ export interface TemplatePlugin {
|
|
|
12
12
|
id: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export interface SkillEntry {
|
|
16
|
+
repo: string;
|
|
17
|
+
skill: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
export interface TemplateDefinition {
|
|
16
21
|
name: string;
|
|
17
22
|
description: string;
|
|
18
|
-
skills:
|
|
23
|
+
skills: SkillEntry[];
|
|
19
24
|
plugins: TemplatePlugin[];
|
|
20
25
|
instructions: string;
|
|
21
26
|
rules: Array<{ name: string; content: string }>;
|
|
22
27
|
}
|
|
23
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
|
+
|
|
24
65
|
// Parses the `plugins:` block from a template.yml string.
|
|
25
66
|
// Expects entries in the format:
|
|
26
67
|
// plugins:
|
|
@@ -60,27 +101,40 @@ export async function applyTemplateFiles(root: string, template: TemplateDefinit
|
|
|
60
101
|
}
|
|
61
102
|
}
|
|
62
103
|
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
65
112
|
export async function installTemplateSkills(
|
|
66
113
|
root: string,
|
|
67
114
|
template: TemplateDefinition,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
};
|
|
79
132
|
}
|
|
80
133
|
|
|
81
134
|
export interface PluginInstallResult {
|
|
82
135
|
installed: TemplatePlugin[];
|
|
83
136
|
manual: TemplatePlugin[];
|
|
137
|
+
failed: Array<{ plugin: TemplatePlugin; reason: string }>;
|
|
84
138
|
}
|
|
85
139
|
|
|
86
140
|
// Phase 3: installs plugins for active targets. Call AFTER generate().
|
|
@@ -89,48 +143,51 @@ export interface PluginInstallResult {
|
|
|
89
143
|
// - opencode → adds id to plugin[] in opencode.json
|
|
90
144
|
// - cursor → added to manual list (no CLI yet — user runs /add-plugin in chat)
|
|
91
145
|
// - windsurf → skipped (no marketplace)
|
|
146
|
+
// Never throws — failed plugins are collected and returned in the result.
|
|
92
147
|
export async function installTemplatePlugins(
|
|
93
148
|
root: string,
|
|
94
149
|
template: TemplateDefinition,
|
|
95
150
|
activeTargets: AgentTarget[],
|
|
96
|
-
onPluginInstalled?: (plugin: TemplatePlugin) => void,
|
|
97
151
|
): Promise<PluginInstallResult> {
|
|
98
152
|
const installed: TemplatePlugin[] = [];
|
|
99
153
|
const manual: TemplatePlugin[] = [];
|
|
154
|
+
const failed: Array<{ plugin: TemplatePlugin; reason: string }> = [];
|
|
100
155
|
|
|
101
156
|
for (const plugin of template.plugins) {
|
|
102
157
|
if (!activeTargets.includes(plugin.target)) continue;
|
|
103
158
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
159
|
+
try {
|
|
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
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
186
|
+
failed.push({ plugin, reason });
|
|
130
187
|
}
|
|
131
188
|
}
|
|
132
189
|
|
|
133
|
-
return { installed, manual };
|
|
190
|
+
return { installed, manual, failed };
|
|
134
191
|
}
|
|
135
192
|
|
|
136
193
|
// Fetches a template from a GitHub URL.
|
|
@@ -145,23 +202,7 @@ export async function fetchTemplateFromGitHub(url: string): Promise<TemplateDefi
|
|
|
145
202
|
fetchText(`${rawBase}/instructions.md`),
|
|
146
203
|
]);
|
|
147
204
|
|
|
148
|
-
const
|
|
149
|
-
const description = descMatch?.[1]?.trim() ?? "";
|
|
150
|
-
|
|
151
|
-
const nameMatch = yamlText.match(/^name:\s*(.+)$/m);
|
|
152
|
-
const name = nameMatch?.[1]?.trim() ?? "custom";
|
|
153
|
-
|
|
154
|
-
const skills: string[] = [];
|
|
155
|
-
const skillsBlockMatch = yamlText.match(/^skills:\s*\n((?: - .+\n?)*)/m);
|
|
156
|
-
if (skillsBlockMatch) {
|
|
157
|
-
const lines = skillsBlockMatch[1]!.split("\n").filter(Boolean);
|
|
158
|
-
for (const line of lines) {
|
|
159
|
-
const skill = line.replace(/^\s*-\s*/, "").trim();
|
|
160
|
-
if (skill) skills.push(skill);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const plugins = parsePluginsFromYaml(yamlText);
|
|
205
|
+
const { name, description, skills, plugins } = parseTemplateYaml(yamlText);
|
|
165
206
|
|
|
166
207
|
// Try to list rules via GitHub API
|
|
167
208
|
const rules = await fetchGitHubRules(url);
|
|
@@ -177,23 +218,36 @@ async function fetchText(url: string): Promise<string> {
|
|
|
177
218
|
return response.text();
|
|
178
219
|
}
|
|
179
220
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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\/([^/]+?)(?:\/(.+))?)?(?:\/)?$/);
|
|
183
234
|
if (!match) {
|
|
184
235
|
throw new Error(`Invalid GitHub URL: "${url}". Expected format: https://github.com/owner/repo`);
|
|
185
236
|
}
|
|
186
|
-
const [, owner, repo, branch = "main"] = match;
|
|
187
|
-
return
|
|
237
|
+
const [, owner, repo, branch = "main", subdir = ""] = match;
|
|
238
|
+
return { owner: owner!, repo: repo!, branch, subdir };
|
|
188
239
|
}
|
|
189
240
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
}
|
|
195
246
|
|
|
196
|
-
|
|
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}`;
|
|
197
251
|
try {
|
|
198
252
|
const response = await fetch(apiUrl, {
|
|
199
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/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
|
@@ -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,17 +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
|
-
|
|
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
|
|
131
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
|
+
}
|
|
132
145
|
}
|
|
133
146
|
|
|
134
147
|
export async function createAllSymlinks(entries: SymlinkEntry[]): Promise<void> {
|
|
135
|
-
|
|
148
|
+
for (const e of entries) {
|
|
149
|
+
await createSymlink(e.symlinkPath, e.target);
|
|
150
|
+
}
|
|
136
151
|
}
|
|
137
152
|
|
|
138
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 {
|