@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moskala/oneagent-core",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Core library for oneagent — one source of truth for AI agent rules",
6
6
  "license": "MIT",
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 .instructions.md files, not symlinks
70
+ // rules: generated as <name>.instructions.md files, not symlinks
66
71
  },
67
72
  ];
68
73
 
@@ -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: string[];
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
- // Phase 2: installs skills via `bunx skills add <identifier> --yes`.
64
- // Call this AFTER generate() so agent directories (symlinks) already exist.
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
- onSkillInstalled?: (identifier: string) => void,
69
- ): Promise<void> {
70
- for (const identifier of template.skills) {
71
- try {
72
- await execFileAsync("npx", ["skills", "add", identifier, "--agent", "universal", "--yes"], { cwd: root });
73
- onSkillInstalled?.(identifier);
74
- } catch (err) {
75
- const message = err instanceof Error ? err.message : String(err);
76
- throw new Error(`Failed to install skill "${identifier}": ${message}`);
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
- 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;
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 descMatch = yamlText.match(/^description:\s*(.+)$/m);
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
- function githubUrlToRawBase(url: string): string {
181
- // Handle https://github.com/owner/repo/tree/branch or https://github.com/owner/repo
182
- const match = url.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\/tree\/([^/]+))?(?:\/.*)?$/);
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 `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`;
237
+ const [, owner, repo, branch = "main", subdir = ""] = match;
238
+ return { owner: owner!, repo: repo!, branch, subdir };
188
239
  }
189
240
 
190
- async function fetchGitHubRules(repoUrl: string): Promise<Array<{ name: string; content: string }>> {
191
- // Parse owner/repo from URL
192
- const match = repoUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\/tree\/([^/]+))?(?:\/.*)?$/);
193
- if (!match) return [];
194
- const [, owner, repo, branch = "main"] = match;
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
- const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/rules?ref=${branch}`;
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" },
@@ -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 filePath = copilotFilePath(root, rule.name);
15
- await fs.mkdir(path.dirname(filePath), { recursive: true });
16
- await fs.writeFile(filePath, buildCopilotContent(rule));
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 { buildCopilotContent, copilotFilePath, buildCopilotPromptContent, copilotPromptFilePath, generateCopilotRules, generateCopilotSkills } from "./copilot.ts";
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 content = await fs.readFile(filePath, "utf-8");
45
- if (content === buildCopilotContent(rule)) return null;
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
- await createAllSymlinks([...mainSymlinks, ...rulesSymlinks, ...skillSymlinks, ...buildAgentsDirSymlinks(root)]);
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
@@ -3,6 +3,7 @@ export * from "./agents.ts";
3
3
  export * from "./config.ts";
4
4
  export * from "./detect.ts";
5
5
  export * from "./rules.ts";
6
+ export * from "./commands.ts";
6
7
  export * from "./skills.ts";
7
8
  export * from "./symlinks.ts";
8
9
  export * from "./copilot.ts";
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
- const mdFiles = files.filter((f) => f.endsWith(".md"));
29
- const rules = await Promise.all(mdFiles.map((f) => readRuleFile(path.join(rulesDir, f))));
30
- return rules.sort((a, b) => a.name.localeCompare(b.name));
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 { buildCopilotContent, buildCopilotPromptContent, copilotFilePath, copilotPromptFilePath } from "./copilot.ts";
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 content = await fs.readFile(filePath, "utf-8");
15
- return { path: filePath, exists: true, upToDate: content === expected };
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
- // Rules dirs: the entire directory becomes a symlink, so move files out and remove the dir.
126
- // Sequential to avoid same-name conflicts across dirs.
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);
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
- await Promise.all(entries.map((e) => createSymlink(e.symlinkPath, e.target)));
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
- applyTo: string;
20
- content: string;
19
+ }
20
+
21
+ export interface CommandFile {
22
+ name: string;
23
+ path: string;
21
24
  }
22
25
 
23
26
  export interface SkillFile {