@moskala/oneagent-core 0.1.0 → 0.2.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/config.ts +11 -1
- package/src/copilot.ts +22 -1
- package/src/detect.ts +26 -0
- package/src/generate.ts +75 -10
- package/src/index.ts +1 -0
- package/src/skills.ts +42 -0
- package/src/status.ts +26 -7
- package/src/symlinks.ts +88 -1
- package/src/types.ts +9 -1
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { parse, stringify } from "yaml";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import type { Config } from "./types.ts";
|
|
3
|
+
import type { AgentTarget, Config } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
const CONFIG_REL = ".oneagent/config.yml";
|
|
6
6
|
|
|
7
|
+
export const ALL_AGENT_TARGETS: AgentTarget[] = ["claude", "cursor", "windsurf", "opencode", "copilot"];
|
|
8
|
+
|
|
9
|
+
export function activeTargets(config: Config): AgentTarget[] {
|
|
10
|
+
return ALL_AGENT_TARGETS.filter((t) => config.targets[t]);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function makeTargets(...enabled: AgentTarget[]): Record<AgentTarget, boolean> {
|
|
14
|
+
return Object.fromEntries(ALL_AGENT_TARGETS.map((t) => [t, enabled.includes(t)])) as Record<AgentTarget, boolean>;
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
export async function configExists(root: string): Promise<boolean> {
|
|
8
18
|
return Bun.file(path.join(root, CONFIG_REL)).exists();
|
|
9
19
|
}
|
package/src/copilot.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
|
-
import type { RuleFile } from "./types.ts";
|
|
3
|
+
import type { RuleFile, SkillFile } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
export function buildCopilotContent(rule: RuleFile): string {
|
|
6
6
|
return `---\napplyTo: "${rule.applyTo}"\n---\n${rule.content}`;
|
|
@@ -19,3 +19,24 @@ export async function generateCopilotRule(root: string, rule: RuleFile): Promise
|
|
|
19
19
|
export async function generateCopilotRules(root: string, rules: RuleFile[]): Promise<void> {
|
|
20
20
|
await Promise.all(rules.map((rule) => generateCopilotRule(root, rule)));
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
export function buildCopilotPromptContent(skill: SkillFile): string {
|
|
24
|
+
const lines = ["---", `mode: "${skill.mode}"`];
|
|
25
|
+
if (skill.description) lines.push(`description: "${skill.description}"`);
|
|
26
|
+
lines.push("---", skill.content);
|
|
27
|
+
return lines.join("\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function copilotPromptFilePath(root: string, skillName: string): string {
|
|
31
|
+
return path.join(root, ".github/prompts", `${skillName}.prompt.md`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function generateCopilotSkill(root: string, skill: SkillFile): Promise<void> {
|
|
35
|
+
const filePath = copilotPromptFilePath(root, skill.name);
|
|
36
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
37
|
+
await Bun.write(filePath, buildCopilotPromptContent(skill));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function generateCopilotSkills(root: string, skills: SkillFile[]): Promise<void> {
|
|
41
|
+
await Promise.all(skills.map((skill) => generateCopilotSkill(root, skill)));
|
|
42
|
+
}
|
package/src/detect.ts
CHANGED
|
@@ -45,3 +45,29 @@ export function filesHaveSameContent(files: DetectedFile[]): boolean {
|
|
|
45
45
|
const first = files[0]!.content;
|
|
46
46
|
return files.every((f) => f.content === first);
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
const DEPRECATED_FILES = [".cursorrules"];
|
|
50
|
+
|
|
51
|
+
export async function removeDeprecatedFiles(root: string): Promise<void> {
|
|
52
|
+
for (const rel of DEPRECATED_FILES) {
|
|
53
|
+
const absPath = path.join(root, rel);
|
|
54
|
+
try {
|
|
55
|
+
const stat = await fs.lstat(absPath);
|
|
56
|
+
if (!stat.isSymbolicLink()) await fs.unlink(absPath);
|
|
57
|
+
} catch {
|
|
58
|
+
// doesn't exist — no-op
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function detectDeprecatedCommandFiles(root: string): Promise<string[]> {
|
|
64
|
+
const commandsDir = path.join(root, ".claude/commands");
|
|
65
|
+
try {
|
|
66
|
+
const entries = await fs.readdir(commandsDir, { withFileTypes: true });
|
|
67
|
+
return entries
|
|
68
|
+
.filter((e) => e.isFile())
|
|
69
|
+
.map((e) => path.join(".claude/commands", e.name));
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/generate.ts
CHANGED
|
@@ -1,21 +1,86 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import type { Config, DetectedFile } from "./types.ts";
|
|
4
|
+
import { activeTargets } from "./config.ts";
|
|
2
5
|
import { readRules } from "./rules.ts";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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";
|
|
5
9
|
import { writeOpencode } from "./opencode.ts";
|
|
10
|
+
import { readDetectedFile } from "./detect.ts";
|
|
11
|
+
|
|
12
|
+
export interface GenerateCollisions {
|
|
13
|
+
mainFiles: DetectedFile[];
|
|
14
|
+
ruleSkillFiles: DetectedFile[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function detectGenerateCollisions(root: string, config: Config): Promise<GenerateCollisions> {
|
|
18
|
+
const [rules, skills] = await Promise.all([readRules(root), readSkills(root)]);
|
|
19
|
+
const targets = activeTargets(config);
|
|
20
|
+
|
|
21
|
+
// 1. Main instruction file symlinks (CLAUDE.md, AGENTS.md, .windsurfrules, etc.)
|
|
22
|
+
const mainEntries = buildMainSymlinks(root, targets);
|
|
23
|
+
// 2. Rule/skill symlink paths
|
|
24
|
+
const ruleSkillEntries = [
|
|
25
|
+
...buildRulesSymlinks(root, targets, rules),
|
|
26
|
+
...buildSkillSymlinks(root, targets, skills),
|
|
27
|
+
// .agents/skills skipped — handled by migrateAgentsSkillsDir
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const [mainCollisions, ruleSkillSymlinkCollisions] = await Promise.all([
|
|
31
|
+
Promise.all(mainEntries.map((entry) => readDetectedFile(root, path.relative(root, entry.symlinkPath))))
|
|
32
|
+
.then((files) => files.filter((f): f is DetectedFile => f !== null)),
|
|
33
|
+
Promise.all(ruleSkillEntries.map((entry) => readDetectedFile(root, path.relative(root, entry.symlinkPath))))
|
|
34
|
+
.then((files) => files.filter((f): f is DetectedFile => f !== null)),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// 3. Copilot generated files — collision only if content differs (idempotent-safe)
|
|
38
|
+
const copilotCollisions: DetectedFile[] = [];
|
|
39
|
+
if (targets.includes("copilot")) {
|
|
40
|
+
const checks = await Promise.all([
|
|
41
|
+
...rules.map(async (rule): Promise<DetectedFile | null> => {
|
|
42
|
+
const filePath = copilotFilePath(root, rule.name);
|
|
43
|
+
try {
|
|
44
|
+
const content = await Bun.file(filePath).text();
|
|
45
|
+
if (content === buildCopilotContent(rule)) return null;
|
|
46
|
+
const stat = await fs.lstat(filePath);
|
|
47
|
+
return { relativePath: path.relative(root, filePath), absolutePath: filePath, sizeBytes: stat.size, modifiedAt: stat.mtime, content };
|
|
48
|
+
} catch { return null; }
|
|
49
|
+
}),
|
|
50
|
+
...skills.map(async (skill): Promise<DetectedFile | null> => {
|
|
51
|
+
const filePath = copilotPromptFilePath(root, skill.name);
|
|
52
|
+
try {
|
|
53
|
+
const content = await Bun.file(filePath).text();
|
|
54
|
+
if (content === buildCopilotPromptContent(skill)) return null;
|
|
55
|
+
const stat = await fs.lstat(filePath);
|
|
56
|
+
return { relativePath: path.relative(root, filePath), absolutePath: filePath, sizeBytes: stat.size, modifiedAt: stat.mtime, content };
|
|
57
|
+
} catch { return null; }
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
60
|
+
copilotCollisions.push(...checks.filter((c): c is DetectedFile => c !== null));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
mainFiles: mainCollisions,
|
|
65
|
+
ruleSkillFiles: [...ruleSkillSymlinkCollisions, ...copilotCollisions],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
6
68
|
|
|
7
69
|
export async function generate(root: string, config: Config): Promise<void> {
|
|
8
|
-
const rules = await readRules(root);
|
|
70
|
+
const [rules, skills] = await Promise.all([readRules(root), readSkills(root)]);
|
|
71
|
+
const targets = activeTargets(config);
|
|
9
72
|
|
|
10
|
-
const mainSymlinks = buildMainSymlinks(root,
|
|
11
|
-
const rulesSymlinks = buildRulesSymlinks(root,
|
|
12
|
-
|
|
73
|
+
const mainSymlinks = buildMainSymlinks(root, targets);
|
|
74
|
+
const rulesSymlinks = buildRulesSymlinks(root, targets, rules);
|
|
75
|
+
const skillSymlinks = buildSkillSymlinks(root, targets, skills);
|
|
76
|
+
await migrateRuleAndSkillFiles(root);
|
|
77
|
+
await createAllSymlinks([...mainSymlinks, ...rulesSymlinks, ...skillSymlinks, ...buildAgentsDirSymlinks(root)]);
|
|
13
78
|
|
|
14
|
-
if (
|
|
15
|
-
await generateCopilotRules(root, rules);
|
|
79
|
+
if (targets.includes("copilot")) {
|
|
80
|
+
await Promise.all([generateCopilotRules(root, rules), generateCopilotSkills(root, skills)]);
|
|
16
81
|
}
|
|
17
82
|
|
|
18
|
-
if (
|
|
83
|
+
if (targets.includes("opencode")) {
|
|
19
84
|
await writeOpencode(root, rules);
|
|
20
85
|
}
|
|
21
86
|
}
|
package/src/index.ts
CHANGED
package/src/skills.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import type { SkillFile } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
const VALID_MODES = ["ask", "edit", "agent"] as const;
|
|
6
|
+
type SkillMode = (typeof VALID_MODES)[number];
|
|
7
|
+
|
|
8
|
+
export function parseSkillFrontmatter(raw: string): { description: string; mode: SkillMode; content: string } {
|
|
9
|
+
const match = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
10
|
+
if (!match) return { description: "", mode: "agent", content: raw };
|
|
11
|
+
|
|
12
|
+
const frontmatter = match[1] ?? "";
|
|
13
|
+
const content = match[2] ?? "";
|
|
14
|
+
|
|
15
|
+
const descMatch = frontmatter.match(/description:\s*["']?([^"'\n]+)["']?/);
|
|
16
|
+
const description = descMatch?.[1]?.trim() ?? "";
|
|
17
|
+
|
|
18
|
+
const modeMatch = frontmatter.match(/mode:\s*["']?([^"'\n]+)["']?/);
|
|
19
|
+
const modeRaw = modeMatch?.[1]?.trim() ?? "agent";
|
|
20
|
+
const mode: SkillMode = (VALID_MODES as readonly string[]).includes(modeRaw) ? (modeRaw as SkillMode) : "agent";
|
|
21
|
+
|
|
22
|
+
return { description, mode, content };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function readSkillFile(filePath: string): Promise<SkillFile> {
|
|
26
|
+
const raw = await Bun.file(filePath).text();
|
|
27
|
+
const { description, mode, content } = parseSkillFrontmatter(raw);
|
|
28
|
+
return { name: path.basename(filePath, ".md"), path: filePath, description, mode, content };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function readSkills(root: string): Promise<SkillFile[]> {
|
|
32
|
+
const skillsDir = path.join(root, ".oneagent/skills");
|
|
33
|
+
try {
|
|
34
|
+
const files = await fs.readdir(skillsDir);
|
|
35
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
36
|
+
const skills = await Promise.all(mdFiles.map((f) => readSkillFile(path.join(skillsDir, f))));
|
|
37
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
package/src/status.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Config, GeneratedFileCheck, OpenCodeCheck, RuleFile, StatusResult } from "./types.ts";
|
|
2
|
+
import { activeTargets } from "./config.ts";
|
|
2
3
|
import { readRules } from "./rules.ts";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { readSkills } from "./skills.ts";
|
|
5
|
+
import { buildMainSymlinks, buildRulesSymlinks, buildSkillSymlinks, buildAgentsDirSymlinks, checkSymlink } from "./symlinks.ts";
|
|
6
|
+
import { buildCopilotContent, buildCopilotPromptContent, copilotFilePath, copilotPromptFilePath } from "./copilot.ts";
|
|
5
7
|
import { readOpencode } from "./opencode.ts";
|
|
6
8
|
|
|
7
9
|
export async function checkGeneratedFile(root: string, rule: RuleFile): Promise<GeneratedFileCheck> {
|
|
@@ -24,18 +26,35 @@ export async function checkOpencodeStatus(
|
|
|
24
26
|
return { exists: true, valid: existing["instructions"] === ".oneagent/instructions.md" };
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
export async function checkCopilotPrompt(root: string, skill: import("./types.ts").SkillFile): Promise<GeneratedFileCheck> {
|
|
30
|
+
const filePath = copilotPromptFilePath(root, skill.name);
|
|
31
|
+
const expected = buildCopilotPromptContent(skill);
|
|
32
|
+
try {
|
|
33
|
+
const content = await Bun.file(filePath).text();
|
|
34
|
+
return { path: filePath, exists: true, upToDate: content === expected };
|
|
35
|
+
} catch {
|
|
36
|
+
return { path: filePath, exists: false, upToDate: false };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
export async function checkStatus(root: string, config: Config): Promise<StatusResult> {
|
|
28
|
-
const rules = await readRules(root);
|
|
41
|
+
const [rules, skills] = await Promise.all([readRules(root), readSkills(root)]);
|
|
42
|
+
const targets = activeTargets(config);
|
|
29
43
|
|
|
30
44
|
const allEntries = [
|
|
31
|
-
...buildMainSymlinks(root,
|
|
32
|
-
...buildRulesSymlinks(root,
|
|
45
|
+
...buildMainSymlinks(root, targets),
|
|
46
|
+
...buildRulesSymlinks(root, targets, rules),
|
|
47
|
+
...buildSkillSymlinks(root, targets, skills),
|
|
48
|
+
...buildAgentsDirSymlinks(root),
|
|
33
49
|
];
|
|
34
50
|
|
|
35
51
|
const symlinks = await Promise.all(allEntries.map(checkSymlink));
|
|
36
52
|
|
|
37
|
-
const generatedFiles =
|
|
38
|
-
? await Promise.all(
|
|
53
|
+
const generatedFiles = targets.includes("copilot")
|
|
54
|
+
? await Promise.all([
|
|
55
|
+
...rules.map((rule) => checkGeneratedFile(root, rule)),
|
|
56
|
+
...skills.map((skill) => checkCopilotPrompt(root, skill)),
|
|
57
|
+
])
|
|
39
58
|
: [];
|
|
40
59
|
|
|
41
60
|
const opencode = await checkOpencodeStatus(root, rules);
|
package/src/symlinks.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
|
-
import type { AgentTarget, RuleFile, SymlinkCheck, SymlinkEntry } from "./types.ts";
|
|
3
|
+
import type { AgentTarget, RuleFile, SkillFile, SymlinkCheck, SymlinkEntry } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
export async function ensureDir(dirPath: string): Promise<void> {
|
|
6
6
|
await fs.mkdir(dirPath, { recursive: true });
|
|
@@ -98,6 +98,93 @@ export function buildRulesSymlinks(
|
|
|
98
98
|
return entries;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
export function buildSkillSymlinks(root: string, targets: AgentTarget[], skills: SkillFile[]): SymlinkEntry[] {
|
|
102
|
+
if (!targets.includes("claude")) return [];
|
|
103
|
+
|
|
104
|
+
const commandsDir = path.join(root, ".claude/commands");
|
|
105
|
+
return skills.map((skill) => {
|
|
106
|
+
const symlinkPath = path.join(commandsDir, `${skill.name}.md`);
|
|
107
|
+
return {
|
|
108
|
+
symlinkPath,
|
|
109
|
+
target: relativeTarget(symlinkPath, skill.path),
|
|
110
|
+
label: path.relative(root, symlinkPath),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function buildAgentsDirSymlinks(root: string): SymlinkEntry[] {
|
|
116
|
+
const symlinkPath = path.join(root, ".agents/skills");
|
|
117
|
+
const targetAbs = path.join(root, ".oneagent/skills");
|
|
118
|
+
return [{ symlinkPath, target: relativeTarget(symlinkPath, targetAbs), label: ".agents/skills" }];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function migrateFilesFromDir(srcDir: string, destDir: string, root: string): Promise<void> {
|
|
122
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
123
|
+
let entries: import("fs").Dirent[];
|
|
124
|
+
try {
|
|
125
|
+
entries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
126
|
+
} catch {
|
|
127
|
+
return; // srcDir doesn't exist — no-op
|
|
128
|
+
}
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (!entry.isFile()) continue; // skip symlinks and subdirs
|
|
131
|
+
const srcFile = path.join(srcDir, entry.name);
|
|
132
|
+
const destFile = path.join(destDir, entry.name);
|
|
133
|
+
|
|
134
|
+
let destExists = false;
|
|
135
|
+
try {
|
|
136
|
+
await fs.access(destFile);
|
|
137
|
+
destExists = true;
|
|
138
|
+
} catch {
|
|
139
|
+
// dest doesn't exist
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (destExists) {
|
|
143
|
+
// dest exists — compare content before deleting source
|
|
144
|
+
const [srcContent, destContent] = await Promise.all([
|
|
145
|
+
Bun.file(srcFile).text(),
|
|
146
|
+
Bun.file(destFile).text(),
|
|
147
|
+
]);
|
|
148
|
+
if (srcContent !== destContent) {
|
|
149
|
+
// Different content — backup source before deleting
|
|
150
|
+
const backupDir = path.join(root, ".oneagent/backup");
|
|
151
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
152
|
+
const safeName = path.relative(root, srcFile).replace(/\//g, "_");
|
|
153
|
+
await Bun.write(path.join(backupDir, safeName), srcContent);
|
|
154
|
+
}
|
|
155
|
+
await fs.unlink(srcFile); // safe to delete — dest has the content (or backup was created)
|
|
156
|
+
} else {
|
|
157
|
+
await fs.rename(srcFile, destFile);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function migrateAndRemoveDir(src: string, dest: string, root: string): Promise<void> {
|
|
163
|
+
let stat;
|
|
164
|
+
try {
|
|
165
|
+
stat = await fs.lstat(src);
|
|
166
|
+
} catch {
|
|
167
|
+
return; // doesn't exist — no-op
|
|
168
|
+
}
|
|
169
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return;
|
|
170
|
+
await migrateFilesFromDir(src, dest, root);
|
|
171
|
+
await fs.rm(src, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function migrateRuleAndSkillFiles(root: string): Promise<void> {
|
|
175
|
+
const destRules = path.join(root, ".oneagent/rules");
|
|
176
|
+
const destSkills = path.join(root, ".oneagent/skills");
|
|
177
|
+
// Rules dirs: only individual files become symlinks, so we only move the files.
|
|
178
|
+
// The directories themselves stay — generate() recreates per-file symlinks inside them.
|
|
179
|
+
// Sequential to avoid same-name conflicts across dirs.
|
|
180
|
+
await migrateFilesFromDir(path.join(root, ".cursor/rules"), destRules, root);
|
|
181
|
+
await migrateFilesFromDir(path.join(root, ".claude/rules"), destRules, root);
|
|
182
|
+
await migrateFilesFromDir(path.join(root, ".windsurf/rules"), destRules, root);
|
|
183
|
+
// .agents/skills is different: the entire directory becomes a symlink to .oneagent/skills,
|
|
184
|
+
// so the real directory must be removed first to make room for the symlink.
|
|
185
|
+
await migrateAndRemoveDir(path.join(root, ".agents/skills"), destSkills, root);
|
|
186
|
+
}
|
|
187
|
+
|
|
101
188
|
export async function createAllSymlinks(entries: SymlinkEntry[]): Promise<void> {
|
|
102
189
|
await Promise.all(entries.map((e) => createSymlink(e.symlinkPath, e.target)));
|
|
103
190
|
}
|
package/src/types.ts
CHANGED
|
@@ -2,7 +2,7 @@ export type AgentTarget = "claude" | "cursor" | "windsurf" | "opencode" | "copil
|
|
|
2
2
|
|
|
3
3
|
export interface Config {
|
|
4
4
|
version: 1;
|
|
5
|
-
targets: AgentTarget
|
|
5
|
+
targets: Record<AgentTarget, boolean>;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export interface DetectedFile {
|
|
@@ -20,6 +20,14 @@ export interface RuleFile {
|
|
|
20
20
|
content: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface SkillFile {
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
description: string;
|
|
27
|
+
mode: "ask" | "edit" | "agent";
|
|
28
|
+
content: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export interface SymlinkEntry {
|
|
24
32
|
symlinkPath: string;
|
|
25
33
|
target: string;
|