@moskala/oneagent-core 0.4.0 → 0.4.2
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/dist/agents.d.ts +20 -0
- package/dist/apply-template.d.ts +48 -0
- package/dist/commands.d.ts +2 -0
- package/dist/config.d.ts +7 -0
- package/dist/constants.d.ts +1 -0
- package/dist/copilot.d.ts +8 -0
- package/dist/detect.d.ts +6 -0
- package/dist/generate.d.ts +7 -0
- package/dist/index.js +857 -0
- package/dist/opencode.d.ts +5 -0
- package/dist/rules.d.ts +2 -0
- package/dist/skills.d.ts +11 -0
- package/dist/status.d.ts +5 -0
- package/dist/symlinks.d.ts +12 -0
- package/dist/types.d.ts +50 -0
- package/package.json +10 -6
- package/src/agents.ts +0 -76
- package/src/apply-template.ts +0 -335
- package/src/commands.ts +0 -17
- package/src/config.ts +0 -32
- package/src/constants.ts +0 -1
- package/src/copilot.ts +0 -38
- package/src/detect.ts +0 -64
- package/src/generate.ts +0 -92
- package/src/opencode.ts +0 -40
- package/src/rules.ts +0 -17
- package/src/skills.ts +0 -43
- package/src/status.ts +0 -68
- package/src/symlinks.ts +0 -245
- package/src/types.ts +0 -60
- /package/{src/index.ts → dist/index.d.ts} +0 -0
package/src/detect.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs/promises";
|
|
3
|
-
import type { DetectedFile } from "./types.ts";
|
|
4
|
-
import { AGENT_DEFINITIONS } from "./agents.ts";
|
|
5
|
-
import { ONEAGENT_DIR } from "./constants.ts";
|
|
6
|
-
|
|
7
|
-
export const AGENT_FILES = [
|
|
8
|
-
"CLAUDE.md",
|
|
9
|
-
"AGENTS.md",
|
|
10
|
-
".cursorrules",
|
|
11
|
-
".windsurfrules",
|
|
12
|
-
".github/copilot-instructions.md",
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
export async function readDetectedFile(root: string, rel: string): Promise<DetectedFile | null> {
|
|
16
|
-
const absolutePath = path.join(root, rel);
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const stat = await fs.lstat(absolutePath);
|
|
20
|
-
|
|
21
|
-
if (stat.isSymbolicLink()) {
|
|
22
|
-
const linkTarget = await fs.readlink(absolutePath);
|
|
23
|
-
const resolved = path.resolve(path.dirname(absolutePath), linkTarget);
|
|
24
|
-
if (resolved.startsWith(path.join(root, ONEAGENT_DIR))) return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const content = await fs.readFile(absolutePath, "utf-8");
|
|
28
|
-
return {
|
|
29
|
-
relativePath: rel,
|
|
30
|
-
absolutePath,
|
|
31
|
-
sizeBytes: stat.size,
|
|
32
|
-
modifiedAt: stat.mtime,
|
|
33
|
-
content,
|
|
34
|
-
};
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function detectExistingFiles(root: string): Promise<DetectedFile[]> {
|
|
41
|
-
const results = await Promise.all(AGENT_FILES.map((rel) => readDetectedFile(root, rel)));
|
|
42
|
-
return results.filter((f): f is DetectedFile => f !== null);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function filesHaveSameContent(files: DetectedFile[]): boolean {
|
|
46
|
-
if (files.length <= 1) return true;
|
|
47
|
-
const first = files[0]!.content;
|
|
48
|
-
return files.every((f) => f.content === first);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const DEPRECATED_FILES = AGENT_DEFINITIONS.flatMap((d) => d.deprecatedFiles ?? []);
|
|
52
|
-
|
|
53
|
-
export async function removeDeprecatedFiles(root: string): Promise<void> {
|
|
54
|
-
for (const rel of DEPRECATED_FILES) {
|
|
55
|
-
const absPath = path.join(root, rel);
|
|
56
|
-
try {
|
|
57
|
-
const stat = await fs.lstat(absPath);
|
|
58
|
-
if (!stat.isSymbolicLink()) await fs.unlink(absPath);
|
|
59
|
-
} catch {
|
|
60
|
-
// doesn't exist — no-op
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
package/src/generate.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
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";
|
|
5
|
-
import { readRules } from "./rules.ts";
|
|
6
|
-
import { readSkills } from "./skills.ts";
|
|
7
|
-
import { buildMainSymlinks, buildRulesSymlinks, buildSkillSymlinks, buildCommandSymlinks, buildAgentsDirSymlinks, createAllSymlinks, migrateRuleAndSkillFiles } from "./symlinks.ts";
|
|
8
|
-
import { copilotFilePath, buildCopilotPromptContent, copilotPromptFilePath, generateCopilotRules, generateCopilotSkills } from "./copilot.ts";
|
|
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),
|
|
26
|
-
...buildSkillSymlinks(root, targets),
|
|
27
|
-
...buildCommandSymlinks(root, targets),
|
|
28
|
-
// .agents/skills skipped — handled by migrateAgentsSkillsDir
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
const [mainCollisions, ruleSkillSymlinkCollisions] = await Promise.all([
|
|
32
|
-
Promise.all(mainEntries.map((entry) => readDetectedFile(root, path.relative(root, entry.symlinkPath))))
|
|
33
|
-
.then((files) => files.filter((f): f is DetectedFile => f !== null)),
|
|
34
|
-
Promise.all(ruleSkillEntries.map((entry) => readDetectedFile(root, path.relative(root, entry.symlinkPath))))
|
|
35
|
-
.then((files) => files.filter((f): f is DetectedFile => f !== null)),
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
// 3. Copilot generated files — collision only if content differs (idempotent-safe)
|
|
39
|
-
const copilotCollisions: DetectedFile[] = [];
|
|
40
|
-
if (targets.includes("copilot")) {
|
|
41
|
-
const checks = await Promise.all([
|
|
42
|
-
...rules.map(async (rule): Promise<DetectedFile | null> => {
|
|
43
|
-
const filePath = copilotFilePath(root, rule.name);
|
|
44
|
-
try {
|
|
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;
|
|
50
|
-
const stat = await fs.lstat(filePath);
|
|
51
|
-
return { relativePath: path.relative(root, filePath), absolutePath: filePath, sizeBytes: stat.size, modifiedAt: stat.mtime, content: dest };
|
|
52
|
-
} catch { return null; }
|
|
53
|
-
}),
|
|
54
|
-
...skills.map(async (skill): Promise<DetectedFile | null> => {
|
|
55
|
-
const filePath = copilotPromptFilePath(root, skill.name);
|
|
56
|
-
try {
|
|
57
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
58
|
-
if (content === buildCopilotPromptContent(skill)) return null;
|
|
59
|
-
const stat = await fs.lstat(filePath);
|
|
60
|
-
return { relativePath: path.relative(root, filePath), absolutePath: filePath, sizeBytes: stat.size, modifiedAt: stat.mtime, content };
|
|
61
|
-
} catch { return null; }
|
|
62
|
-
}),
|
|
63
|
-
]);
|
|
64
|
-
copilotCollisions.push(...checks.filter((c): c is DetectedFile => c !== null));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
mainFiles: mainCollisions,
|
|
69
|
-
ruleSkillFiles: [...ruleSkillSymlinkCollisions, ...copilotCollisions],
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export async function generate(root: string, config: Config): Promise<void> {
|
|
74
|
-
const [rules, skills] = await Promise.all([readRules(root), readSkills(root)]);
|
|
75
|
-
const targets = activeTargets(config);
|
|
76
|
-
|
|
77
|
-
await migrateRuleAndSkillFiles(root);
|
|
78
|
-
|
|
79
|
-
const mainSymlinks = buildMainSymlinks(root, targets);
|
|
80
|
-
const rulesSymlinks = buildRulesSymlinks(root, targets);
|
|
81
|
-
const skillSymlinks = await buildSkillSymlinks(root, targets);
|
|
82
|
-
const commandSymlinks = buildCommandSymlinks(root, targets);
|
|
83
|
-
await createAllSymlinks([...mainSymlinks, ...rulesSymlinks, ...skillSymlinks, ...commandSymlinks, ...buildAgentsDirSymlinks(root)]);
|
|
84
|
-
|
|
85
|
-
if (targets.includes("copilot")) {
|
|
86
|
-
await Promise.all([generateCopilotRules(root, rules), generateCopilotSkills(root, skills)]);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (targets.includes("opencode")) {
|
|
90
|
-
await writeOpencode(root, rules);
|
|
91
|
-
}
|
|
92
|
-
}
|
package/src/opencode.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs/promises";
|
|
3
|
-
import type { RuleFile } from "./types.ts";
|
|
4
|
-
import { ONEAGENT_DIR } from "./constants.ts";
|
|
5
|
-
|
|
6
|
-
export async function readOpencode(root: string): Promise<Record<string, unknown> | null> {
|
|
7
|
-
try {
|
|
8
|
-
const content = await fs.readFile(path.join(root, "opencode.json"), "utf-8");
|
|
9
|
-
return JSON.parse(content) as Record<string, unknown>;
|
|
10
|
-
} catch {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function buildOpencodeConfig(existing: Record<string, unknown> | null): object {
|
|
16
|
-
return {
|
|
17
|
-
...existing,
|
|
18
|
-
instructions: `${ONEAGENT_DIR}/instructions.md`,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function addOpenCodePlugin(root: string, id: string): Promise<void> {
|
|
23
|
-
const filePath = path.join(root, "opencode.json");
|
|
24
|
-
let existing: Record<string, unknown>;
|
|
25
|
-
try {
|
|
26
|
-
existing = JSON.parse(await fs.readFile(filePath, "utf-8")) as Record<string, unknown>;
|
|
27
|
-
} catch {
|
|
28
|
-
return; // no opencode.json — no-op
|
|
29
|
-
}
|
|
30
|
-
const current = Array.isArray(existing.plugin) ? (existing.plugin as string[]) : [];
|
|
31
|
-
if (current.includes(id)) return;
|
|
32
|
-
existing.plugin = [...current, id];
|
|
33
|
-
await fs.writeFile(filePath, JSON.stringify(existing, null, 2) + "\n");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function writeOpencode(root: string, _rules: RuleFile[]): Promise<void> {
|
|
37
|
-
const existing = await readOpencode(root);
|
|
38
|
-
const config = buildOpencodeConfig(existing);
|
|
39
|
-
await fs.writeFile(path.join(root, "opencode.json"), JSON.stringify(config, null, 2) + "\n");
|
|
40
|
-
}
|
package/src/rules.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs/promises";
|
|
3
|
-
import type { RuleFile } from "./types.ts";
|
|
4
|
-
import { ONEAGENT_DIR } from "./constants.ts";
|
|
5
|
-
|
|
6
|
-
export async function readRules(root: string): Promise<RuleFile[]> {
|
|
7
|
-
const rulesDir = path.join(root, ONEAGENT_DIR, "rules");
|
|
8
|
-
try {
|
|
9
|
-
const files = await fs.readdir(rulesDir);
|
|
10
|
-
return files
|
|
11
|
-
.filter((f) => f.endsWith(".md"))
|
|
12
|
-
.map((f) => ({ name: path.basename(f, ".md"), path: path.join(rulesDir, f) }))
|
|
13
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
14
|
-
} catch {
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
}
|
package/src/skills.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs/promises";
|
|
3
|
-
import type { SkillFile } from "./types.ts";
|
|
4
|
-
import { ONEAGENT_DIR } from "./constants.ts";
|
|
5
|
-
|
|
6
|
-
const VALID_MODES = ["ask", "edit", "agent"] as const;
|
|
7
|
-
type SkillMode = (typeof VALID_MODES)[number];
|
|
8
|
-
|
|
9
|
-
export function parseSkillFrontmatter(raw: string): { description: string; mode: SkillMode; content: string } {
|
|
10
|
-
const match = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
11
|
-
if (!match) return { description: "", mode: "agent", content: raw };
|
|
12
|
-
|
|
13
|
-
const frontmatter = match[1] ?? "";
|
|
14
|
-
const content = match[2] ?? "";
|
|
15
|
-
|
|
16
|
-
const descMatch = frontmatter.match(/description:\s*["']?([^"'\n]+)["']?/);
|
|
17
|
-
const description = descMatch?.[1]?.trim() ?? "";
|
|
18
|
-
|
|
19
|
-
const modeMatch = frontmatter.match(/mode:\s*["']?([^"'\n]+)["']?/);
|
|
20
|
-
const modeRaw = modeMatch?.[1]?.trim() ?? "agent";
|
|
21
|
-
const mode: SkillMode = (VALID_MODES as readonly string[]).includes(modeRaw) ? (modeRaw as SkillMode) : "agent";
|
|
22
|
-
|
|
23
|
-
return { description, mode, content };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function readSkillFile(filePath: string): Promise<SkillFile> {
|
|
27
|
-
const raw = await fs.readFile(filePath, "utf-8");
|
|
28
|
-
const { description, mode, content } = parseSkillFrontmatter(raw);
|
|
29
|
-
return { name: path.basename(filePath, ".md"), path: filePath, description, mode, content };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function readSkills(root: string): Promise<SkillFile[]> {
|
|
33
|
-
const skillsDir = path.join(root, ONEAGENT_DIR, "skills");
|
|
34
|
-
try {
|
|
35
|
-
const files = await fs.readdir(skillsDir);
|
|
36
|
-
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
37
|
-
const skills = await Promise.all(mdFiles.map((f) => readSkillFile(path.join(skillsDir, f))));
|
|
38
|
-
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
39
|
-
} catch {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
package/src/status.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import fs from "fs/promises";
|
|
2
|
-
import type { Config, GeneratedFileCheck, OpenCodeCheck, RuleFile, StatusResult } from "./types.ts";
|
|
3
|
-
import { activeTargets } from "./config.ts";
|
|
4
|
-
import { ONEAGENT_DIR } from "./constants.ts";
|
|
5
|
-
import { readRules } from "./rules.ts";
|
|
6
|
-
import { readSkills } from "./skills.ts";
|
|
7
|
-
import { buildMainSymlinks, buildRulesSymlinks, buildSkillSymlinks, buildCommandSymlinks, buildAgentsDirSymlinks, checkSymlink } from "./symlinks.ts";
|
|
8
|
-
import { buildCopilotPromptContent, copilotFilePath, copilotPromptFilePath } from "./copilot.ts";
|
|
9
|
-
import { readOpencode } from "./opencode.ts";
|
|
10
|
-
|
|
11
|
-
export async function checkGeneratedFile(root: string, rule: RuleFile): Promise<GeneratedFileCheck> {
|
|
12
|
-
const filePath = copilotFilePath(root, rule.name);
|
|
13
|
-
try {
|
|
14
|
-
const [source, dest] = await Promise.all([
|
|
15
|
-
fs.readFile(rule.path, "utf-8"),
|
|
16
|
-
fs.readFile(filePath, "utf-8"),
|
|
17
|
-
]);
|
|
18
|
-
return { path: filePath, exists: true, upToDate: source === dest };
|
|
19
|
-
} catch {
|
|
20
|
-
return { path: filePath, exists: false, upToDate: false };
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function checkOpencodeStatus(
|
|
25
|
-
root: string,
|
|
26
|
-
_rules: RuleFile[],
|
|
27
|
-
): Promise<OpenCodeCheck> {
|
|
28
|
-
const existing = await readOpencode(root);
|
|
29
|
-
if (!existing) return { exists: false, valid: false };
|
|
30
|
-
return { exists: true, valid: existing["instructions"] === `${ONEAGENT_DIR}/instructions.md` };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function checkCopilotPrompt(root: string, skill: import("./types.ts").SkillFile): Promise<GeneratedFileCheck> {
|
|
34
|
-
const filePath = copilotPromptFilePath(root, skill.name);
|
|
35
|
-
const expected = buildCopilotPromptContent(skill);
|
|
36
|
-
try {
|
|
37
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
38
|
-
return { path: filePath, exists: true, upToDate: content === expected };
|
|
39
|
-
} catch {
|
|
40
|
-
return { path: filePath, exists: false, upToDate: false };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function checkStatus(root: string, config: Config): Promise<StatusResult> {
|
|
45
|
-
const [rules, skills] = await Promise.all([readRules(root), readSkills(root)]);
|
|
46
|
-
const targets = activeTargets(config);
|
|
47
|
-
|
|
48
|
-
const allEntries = [
|
|
49
|
-
...buildMainSymlinks(root, targets),
|
|
50
|
-
...buildRulesSymlinks(root, targets),
|
|
51
|
-
...buildSkillSymlinks(root, targets),
|
|
52
|
-
...buildCommandSymlinks(root, targets),
|
|
53
|
-
...buildAgentsDirSymlinks(root),
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
const symlinks = await Promise.all(allEntries.map(checkSymlink));
|
|
57
|
-
|
|
58
|
-
const generatedFiles = targets.includes("copilot")
|
|
59
|
-
? await Promise.all([
|
|
60
|
-
...rules.map((rule) => checkGeneratedFile(root, rule)),
|
|
61
|
-
...skills.map((skill) => checkCopilotPrompt(root, skill)),
|
|
62
|
-
])
|
|
63
|
-
: [];
|
|
64
|
-
|
|
65
|
-
const opencode = await checkOpencodeStatus(root, rules);
|
|
66
|
-
|
|
67
|
-
return { symlinks, generatedFiles, opencode };
|
|
68
|
-
}
|
package/src/symlinks.ts
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs/promises";
|
|
3
|
-
import type { AgentTarget, SymlinkCheck, SymlinkEntry } from "./types.ts";
|
|
4
|
-
import { AGENT_DEFINITIONS } from "./agents.ts";
|
|
5
|
-
import { ONEAGENT_DIR } from "./constants.ts";
|
|
6
|
-
|
|
7
|
-
export async function ensureDir(dirPath: string): Promise<void> {
|
|
8
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function createSymlink(symlinkPath: string, target: string): Promise<void> {
|
|
12
|
-
await ensureDir(path.dirname(symlinkPath));
|
|
13
|
-
|
|
14
|
-
// Skip if already a correct symlink — avoids unnecessary rm/create that can
|
|
15
|
-
// race with external filesystem watchers (e.g. Cursor IDE recreating .cursor/rules)
|
|
16
|
-
try {
|
|
17
|
-
const stat = await fs.lstat(symlinkPath);
|
|
18
|
-
if (stat.isSymbolicLink() && (await fs.readlink(symlinkPath)) === target) return;
|
|
19
|
-
} catch {
|
|
20
|
-
// path doesn't exist — proceed
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Retry up to 3 times: external tools can recreate the path between our rm
|
|
24
|
-
// and symlink calls, causing EEXIST and macOS conflict copies (e.g. "rules 2")
|
|
25
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
26
|
-
await fs.rm(symlinkPath, { recursive: true, force: true });
|
|
27
|
-
try {
|
|
28
|
-
await fs.symlink(target, symlinkPath);
|
|
29
|
-
return;
|
|
30
|
-
} catch (err) {
|
|
31
|
-
if ((err as NodeJS.ErrnoException).code !== "EEXIST" || attempt === 2) throw err;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function relativeTarget(symlinkPath: string, targetAbsPath: string): string {
|
|
37
|
-
return path.relative(path.dirname(symlinkPath), targetAbsPath);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function buildMainSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
41
|
-
const instructionsAbs = path.join(root, ONEAGENT_DIR, "instructions.md");
|
|
42
|
-
const seen = new Map<string, SymlinkEntry>();
|
|
43
|
-
|
|
44
|
-
for (const target of targets) {
|
|
45
|
-
const def = AGENT_DEFINITIONS.find((d) => d.target === target)!;
|
|
46
|
-
const symlinkPath = path.join(root, def.mainFile);
|
|
47
|
-
if (!seen.has(symlinkPath)) {
|
|
48
|
-
seen.set(symlinkPath, {
|
|
49
|
-
symlinkPath,
|
|
50
|
-
target: relativeTarget(symlinkPath, instructionsAbs),
|
|
51
|
-
label: path.relative(root, symlinkPath),
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return Array.from(seen.values());
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function buildRulesSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
60
|
-
const targetAbs = path.join(root, ONEAGENT_DIR, "rules");
|
|
61
|
-
return AGENT_DEFINITIONS
|
|
62
|
-
.filter((d) => targets.includes(d.target) && d.rulesDir)
|
|
63
|
-
.map((d) => {
|
|
64
|
-
const symlinkPath = path.join(root, d.rulesDir!);
|
|
65
|
-
return { symlinkPath, target: relativeTarget(symlinkPath, targetAbs), label: d.rulesDir! };
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function buildSkillSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
70
|
-
const targetAbs = path.join(root, ONEAGENT_DIR, "skills");
|
|
71
|
-
return AGENT_DEFINITIONS
|
|
72
|
-
.filter((d) => targets.includes(d.target) && d.skillsDir)
|
|
73
|
-
.map((d) => {
|
|
74
|
-
const symlinkPath = path.join(root, d.skillsDir!);
|
|
75
|
-
return { symlinkPath, target: relativeTarget(symlinkPath, targetAbs), label: d.skillsDir! };
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function buildCommandSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
80
|
-
const targetAbs = path.join(root, ONEAGENT_DIR, "commands");
|
|
81
|
-
return AGENT_DEFINITIONS
|
|
82
|
-
.filter((d) => targets.includes(d.target) && d.commandsDir)
|
|
83
|
-
.map((d) => {
|
|
84
|
-
const symlinkPath = path.join(root, d.commandsDir!);
|
|
85
|
-
return { symlinkPath, target: relativeTarget(symlinkPath, targetAbs), label: d.commandsDir! };
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function buildAgentsDirSymlinks(root: string): SymlinkEntry[] {
|
|
90
|
-
const symlinkPath = path.join(root, ".agents/skills");
|
|
91
|
-
const targetAbs = path.join(root, ONEAGENT_DIR, "skills");
|
|
92
|
-
return [{ symlinkPath, target: relativeTarget(symlinkPath, targetAbs), label: ".agents/skills" }];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function migrateFilesFromDir(srcDir: string, destDir: string, root: string): Promise<void> {
|
|
96
|
-
await fs.mkdir(destDir, { recursive: true });
|
|
97
|
-
let entries: import("fs").Dirent[];
|
|
98
|
-
try {
|
|
99
|
-
entries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
100
|
-
} catch {
|
|
101
|
-
return; // srcDir doesn't exist — no-op
|
|
102
|
-
}
|
|
103
|
-
for (const entry of entries) {
|
|
104
|
-
const srcFile = path.join(srcDir, entry.name);
|
|
105
|
-
const fileStat = await fs.lstat(srcFile);
|
|
106
|
-
if (fileStat.isSymbolicLink()) continue;
|
|
107
|
-
|
|
108
|
-
// Move subdirectories as whole units (e.g. skills.sh skill packages)
|
|
109
|
-
if (fileStat.isDirectory()) {
|
|
110
|
-
const destSub = path.join(destDir, entry.name);
|
|
111
|
-
try { await fs.access(destSub); } catch {
|
|
112
|
-
await fs.mkdir(destDir, { recursive: true });
|
|
113
|
-
await fs.rename(srcFile, destSub);
|
|
114
|
-
}
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (!fileStat.isFile()) continue;
|
|
119
|
-
const destFile = path.join(destDir, entry.name);
|
|
120
|
-
|
|
121
|
-
let destExists = false;
|
|
122
|
-
try {
|
|
123
|
-
await fs.access(destFile);
|
|
124
|
-
destExists = true;
|
|
125
|
-
} catch {
|
|
126
|
-
// dest doesn't exist
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (destExists) {
|
|
130
|
-
// dest exists — compare content before deleting source
|
|
131
|
-
const [srcContent, destContent] = await Promise.all([
|
|
132
|
-
fs.readFile(srcFile, "utf-8"),
|
|
133
|
-
fs.readFile(destFile, "utf-8"),
|
|
134
|
-
]);
|
|
135
|
-
if (srcContent !== destContent) {
|
|
136
|
-
// Different content — backup source before deleting
|
|
137
|
-
const backupDir = path.join(root, ONEAGENT_DIR, "backup");
|
|
138
|
-
await fs.mkdir(backupDir, { recursive: true });
|
|
139
|
-
const safeName = path.relative(root, srcFile).replace(/\//g, "_");
|
|
140
|
-
await fs.writeFile(path.join(backupDir, safeName), srcContent);
|
|
141
|
-
}
|
|
142
|
-
await fs.unlink(srcFile); // safe to delete — dest has the content (or backup was created)
|
|
143
|
-
} else {
|
|
144
|
-
await fs.rename(srcFile, destFile);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function migrateAndRemoveDir(src: string, dest: string, root: string): Promise<void> {
|
|
150
|
-
let stat;
|
|
151
|
-
try {
|
|
152
|
-
stat = await fs.lstat(src);
|
|
153
|
-
} catch {
|
|
154
|
-
return; // doesn't exist — no-op
|
|
155
|
-
}
|
|
156
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) return;
|
|
157
|
-
await migrateFilesFromDir(src, dest, root);
|
|
158
|
-
await fs.rm(src, { recursive: true, force: true });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export async function migrateRuleAndSkillFiles(root: string): Promise<void> {
|
|
162
|
-
const destRules = path.join(root, ONEAGENT_DIR, "rules");
|
|
163
|
-
const destSkills = path.join(root, ONEAGENT_DIR, "skills");
|
|
164
|
-
const destCommands = path.join(root, ONEAGENT_DIR, "commands");
|
|
165
|
-
// Derive migration sources from agent definitions — sequential to avoid same-name conflicts.
|
|
166
|
-
for (const def of AGENT_DEFINITIONS) {
|
|
167
|
-
if (def.rulesDir) await migrateAndRemoveDir(path.join(root, def.rulesDir), destRules, root);
|
|
168
|
-
}
|
|
169
|
-
for (const def of AGENT_DEFINITIONS) {
|
|
170
|
-
if (def.skillsDir) await migrateAndRemoveDir(path.join(root, def.skillsDir), destSkills, root);
|
|
171
|
-
}
|
|
172
|
-
// .agents/skills — standard skills.sh path, not represented in agent definitions
|
|
173
|
-
await migrateAndRemoveDir(path.join(root, ".agents/skills"), destSkills, root);
|
|
174
|
-
for (const def of AGENT_DEFINITIONS) {
|
|
175
|
-
if (def.commandsDir) await migrateAndRemoveDir(path.join(root, def.commandsDir), destCommands, root);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function backupDirRecursive(srcDir: string, backupDir: string, prefix: string): Promise<void> {
|
|
180
|
-
let entries: import("fs").Dirent[];
|
|
181
|
-
try { entries = await fs.readdir(srcDir, { withFileTypes: true }); } catch { return; }
|
|
182
|
-
for (const entry of entries) {
|
|
183
|
-
const srcPath = path.join(srcDir, entry.name);
|
|
184
|
-
const lstat = await fs.lstat(srcPath);
|
|
185
|
-
if (lstat.isSymbolicLink()) continue;
|
|
186
|
-
if (lstat.isDirectory()) {
|
|
187
|
-
await backupDirRecursive(srcPath, backupDir, `${prefix}_${entry.name}`);
|
|
188
|
-
} else if (lstat.isFile()) {
|
|
189
|
-
await fs.mkdir(backupDir, { recursive: true });
|
|
190
|
-
await fs.copyFile(srcPath, path.join(backupDir, `${prefix}_${entry.name}`));
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export async function cleanupAgentDir(root: string, target: import("./types.ts").AgentTarget): Promise<void> {
|
|
196
|
-
const def = AGENT_DEFINITIONS.find((d) => d.target === target)!;
|
|
197
|
-
const backupDir = path.join(root, ONEAGENT_DIR, "backup");
|
|
198
|
-
|
|
199
|
-
const agentDir = [def.rulesDir, def.skillsDir, def.commandsDir]
|
|
200
|
-
.filter(Boolean)
|
|
201
|
-
.map((d) => d!.split("/")[0]!)
|
|
202
|
-
.find((d) => d !== ".github");
|
|
203
|
-
|
|
204
|
-
if (agentDir) {
|
|
205
|
-
const agentDirAbs = path.join(root, agentDir);
|
|
206
|
-
let stat;
|
|
207
|
-
try { stat = await fs.lstat(agentDirAbs); } catch {}
|
|
208
|
-
if (stat && stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
209
|
-
await backupDirRecursive(agentDirAbs, backupDir, agentDir);
|
|
210
|
-
await fs.rm(agentDirAbs, { recursive: true, force: true });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (target === "opencode") {
|
|
215
|
-
const opPath = path.join(root, "opencode.json");
|
|
216
|
-
try {
|
|
217
|
-
const content = await fs.readFile(opPath, "utf-8");
|
|
218
|
-
await fs.mkdir(backupDir, { recursive: true });
|
|
219
|
-
await fs.writeFile(path.join(backupDir, "opencode.json"), content);
|
|
220
|
-
} catch {}
|
|
221
|
-
try { await fs.unlink(opPath); } catch {}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export async function createAllSymlinks(entries: SymlinkEntry[]): Promise<void> {
|
|
226
|
-
// Deduplicate by symlink path — last entry wins
|
|
227
|
-
const deduped = new Map<string, SymlinkEntry>();
|
|
228
|
-
for (const e of entries) deduped.set(e.symlinkPath, e);
|
|
229
|
-
for (const e of deduped.values()) {
|
|
230
|
-
await createSymlink(e.symlinkPath, e.target);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export async function checkSymlink(entry: SymlinkEntry): Promise<SymlinkCheck> {
|
|
235
|
-
try {
|
|
236
|
-
const stat = await fs.lstat(entry.symlinkPath);
|
|
237
|
-
if (!stat.isSymbolicLink()) {
|
|
238
|
-
return { ...entry, exists: true, valid: false };
|
|
239
|
-
}
|
|
240
|
-
const linkTarget = await fs.readlink(entry.symlinkPath);
|
|
241
|
-
return { ...entry, exists: true, valid: linkTarget === entry.target };
|
|
242
|
-
} catch {
|
|
243
|
-
return { ...entry, exists: false, valid: false };
|
|
244
|
-
}
|
|
245
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
export type AgentTarget = "claude" | "cursor" | "windsurf" | "opencode" | "copilot";
|
|
2
|
-
|
|
3
|
-
export interface Config {
|
|
4
|
-
version: 1;
|
|
5
|
-
targets: Record<AgentTarget, boolean>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface DetectedFile {
|
|
9
|
-
relativePath: string;
|
|
10
|
-
absolutePath: string;
|
|
11
|
-
sizeBytes: number;
|
|
12
|
-
modifiedAt: Date;
|
|
13
|
-
content: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface RuleFile {
|
|
17
|
-
name: string;
|
|
18
|
-
path: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface CommandFile {
|
|
22
|
-
name: string;
|
|
23
|
-
path: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface SkillFile {
|
|
27
|
-
name: string;
|
|
28
|
-
path: string;
|
|
29
|
-
description: string;
|
|
30
|
-
mode: "ask" | "edit" | "agent";
|
|
31
|
-
content: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface SymlinkEntry {
|
|
35
|
-
symlinkPath: string;
|
|
36
|
-
target: string;
|
|
37
|
-
label: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface SymlinkCheck extends SymlinkEntry {
|
|
41
|
-
exists: boolean;
|
|
42
|
-
valid: boolean;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface StatusResult {
|
|
46
|
-
symlinks: SymlinkCheck[];
|
|
47
|
-
generatedFiles: GeneratedFileCheck[];
|
|
48
|
-
opencode: OpenCodeCheck;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface GeneratedFileCheck {
|
|
52
|
-
path: string;
|
|
53
|
-
exists: boolean;
|
|
54
|
-
upToDate: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface OpenCodeCheck {
|
|
58
|
-
exists: boolean;
|
|
59
|
-
valid: boolean;
|
|
60
|
-
}
|
|
File without changes
|