@moskala/oneagent-core 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agents.ts +1 -1
- package/src/apply-template.ts +89 -24
- package/src/commands.ts +2 -1
- package/src/config.ts +2 -1
- package/src/constants.ts +1 -0
- package/src/detect.ts +2 -1
- package/src/index.ts +1 -0
- package/src/opencode.ts +2 -1
- package/src/rules.ts +2 -1
- package/src/skills.ts +2 -1
- package/src/status.ts +2 -1
- package/src/symlinks.ts +95 -14
package/package.json
CHANGED
package/src/agents.ts
CHANGED
|
@@ -64,7 +64,7 @@ export const AGENT_DEFINITIONS: AgentDefinition[] = [
|
|
|
64
64
|
target: "copilot",
|
|
65
65
|
displayName: "GitHub Copilot",
|
|
66
66
|
hint: ".github/instructions/*.instructions.md",
|
|
67
|
-
detectIndicators: [".github/copilot-instructions.md", ".github"],
|
|
67
|
+
detectIndicators: [".github/copilot-instructions.md", ".github/instructions"],
|
|
68
68
|
mainFile: ".github/copilot-instructions.md",
|
|
69
69
|
skillsDir: ".github/skills",
|
|
70
70
|
// rules: generated as <name>.instructions.md files, not symlinks
|
package/src/apply-template.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { execFile } from "child_process";
|
|
|
4
4
|
import { promisify } from "util";
|
|
5
5
|
import type { AgentTarget } from "./types.ts";
|
|
6
6
|
import { addOpenCodePlugin } from "./opencode.ts";
|
|
7
|
+
import { ONEAGENT_DIR } from "./constants.ts";
|
|
7
8
|
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
9
10
|
|
|
@@ -29,16 +30,19 @@ export interface TemplateDefinition {
|
|
|
29
30
|
// Parses name, description, skills and plugins from a template.yml string.
|
|
30
31
|
// This is the single source of truth for the template.yml format — used by
|
|
31
32
|
// both builtin template loading and GitHub URL template fetching.
|
|
32
|
-
export function parseTemplateYaml(yamlText: string, fallbackName = "custom"): Pick<TemplateDefinition, "name" | "description" | "skills" | "plugins"> {
|
|
33
|
+
export function parseTemplateYaml(yamlText: string, fallbackName = "custom"): Pick<TemplateDefinition, "name" | "description" | "skills" | "plugins"> & { extends?: string } {
|
|
33
34
|
const nameMatch = yamlText.match(/^name:\s*(.+)$/m);
|
|
34
35
|
const name = nameMatch?.[1]?.trim() ?? fallbackName;
|
|
35
36
|
|
|
36
37
|
const descMatch = yamlText.match(/^description:\s*(.+)$/m);
|
|
37
38
|
const description = descMatch?.[1]?.trim() ?? "";
|
|
38
39
|
|
|
40
|
+
const extendsMatch = yamlText.match(/^extends:\s*(.+)$/m);
|
|
41
|
+
const extendsValue = extendsMatch?.[1]?.trim();
|
|
42
|
+
|
|
39
43
|
const skills = parseSkillsFromYaml(yamlText);
|
|
40
44
|
const plugins = parsePluginsFromYaml(yamlText);
|
|
41
|
-
return { name, description, skills, plugins };
|
|
45
|
+
return { name, description, skills, plugins, ...(extendsValue ? { extends: extendsValue } : {}) };
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
// Parses the `skills:` block from a template.yml string.
|
|
@@ -86,10 +90,40 @@ export function parsePluginsFromYaml(yamlText: string): TemplatePlugin[] {
|
|
|
86
90
|
return plugins;
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
// Resolves the `extends` field — loads parent template from builtin name or GitHub URL.
|
|
94
|
+
// Merges skills, plugins, rules (parent first, then child).
|
|
95
|
+
// Instructions are NOT inherited — each template has its own.
|
|
96
|
+
export async function resolveExtends(
|
|
97
|
+
child: TemplateDefinition & { extends?: string },
|
|
98
|
+
resolveBuiltin?: (name: string) => Promise<TemplateDefinition | null>,
|
|
99
|
+
): Promise<TemplateDefinition> {
|
|
100
|
+
if (!child.extends) return child;
|
|
101
|
+
|
|
102
|
+
let parent: TemplateDefinition;
|
|
103
|
+
if (child.extends.startsWith("https://")) {
|
|
104
|
+
parent = await fetchTemplateFromGitHub(child.extends);
|
|
105
|
+
} else if (resolveBuiltin) {
|
|
106
|
+
const resolved = await resolveBuiltin(child.extends);
|
|
107
|
+
if (!resolved) throw new Error(`Unknown builtin template: "${child.extends}"`);
|
|
108
|
+
parent = resolved;
|
|
109
|
+
} else {
|
|
110
|
+
throw new Error(`Cannot resolve extends: "${child.extends}"`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
name: child.name,
|
|
115
|
+
description: child.description,
|
|
116
|
+
instructions: child.instructions,
|
|
117
|
+
skills: [...parent.skills, ...child.skills],
|
|
118
|
+
plugins: [...parent.plugins, ...child.plugins],
|
|
119
|
+
rules: [...parent.rules, ...child.rules],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
89
123
|
// Phase 1: writes instructions.md and rules/*.md.
|
|
90
124
|
// Call this BEFORE generate() so symlinks to rules are created.
|
|
91
125
|
export async function applyTemplateFiles(root: string, template: TemplateDefinition): Promise<void> {
|
|
92
|
-
const oneagentDir = path.join(root,
|
|
126
|
+
const oneagentDir = path.join(root, ONEAGENT_DIR);
|
|
93
127
|
|
|
94
128
|
await fs.mkdir(path.join(oneagentDir, "rules"), { recursive: true });
|
|
95
129
|
await fs.mkdir(path.join(oneagentDir, "skills"), { recursive: true });
|
|
@@ -107,28 +141,42 @@ export interface SkillInstallResult {
|
|
|
107
141
|
}
|
|
108
142
|
|
|
109
143
|
// Phase 2: installs skills via `npx skills add <repo> --skill <name>`.
|
|
110
|
-
//
|
|
144
|
+
// Skills are installed sequentially to avoid race conditions — each `npx skills add` call
|
|
145
|
+
// sets up agent directories and running them in parallel causes "skills 2" naming conflicts.
|
|
146
|
+
// Call this AFTER generate() so agent directories exist.
|
|
111
147
|
// Never throws — failed skills are collected and returned in the result.
|
|
112
148
|
export async function installTemplateSkills(
|
|
113
149
|
root: string,
|
|
114
150
|
template: TemplateDefinition,
|
|
115
151
|
): Promise<SkillInstallResult> {
|
|
116
|
-
const
|
|
117
|
-
|
|
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
|
-
);
|
|
152
|
+
const installed: SkillEntry[] = [];
|
|
153
|
+
const failed: Array<{ entry: SkillEntry; reason: string }> = [];
|
|
127
154
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
for (const entry of template.skills) {
|
|
156
|
+
try {
|
|
157
|
+
await execFileAsync("npx", ["skills", "add", entry.repo, "--skill", entry.skill, "--agent", "universal", "--yes"], { cwd: root });
|
|
158
|
+
installed.push(entry);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
161
|
+
failed.push({ entry, reason });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { installed, failed };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const BUILTIN_SKILL_REPO = "https://github.com/moskalakamil/oneagent";
|
|
169
|
+
const BUILTIN_SKILL_NAME = "oneagent";
|
|
170
|
+
|
|
171
|
+
// Installs the built-in oneagent skill that teaches agents how to work with the .oneagent/ directory.
|
|
172
|
+
// Never throws — returns true if installed, false if failed.
|
|
173
|
+
export async function installBuiltinSkill(root: string): Promise<boolean> {
|
|
174
|
+
try {
|
|
175
|
+
await execFileAsync("npx", ["skills", "add", BUILTIN_SKILL_REPO, "--skill", BUILTIN_SKILL_NAME, "--agent", "universal", "--yes"], { cwd: root });
|
|
176
|
+
return true;
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
132
180
|
}
|
|
133
181
|
|
|
134
182
|
export interface PluginInstallResult {
|
|
@@ -192,22 +240,39 @@ export async function installTemplatePlugins(
|
|
|
192
240
|
|
|
193
241
|
// Fetches a template from a GitHub URL.
|
|
194
242
|
// Expects the repository to contain: template.yml, instructions.md, and optionally rules/*.md
|
|
243
|
+
// When no branch is specified in the URL, queries the GitHub API for the default branch.
|
|
195
244
|
export async function fetchTemplateFromGitHub(url: string): Promise<TemplateDefinition> {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
245
|
+
const { owner, repo, branch, subdir } = parseGitHubUrl(url);
|
|
246
|
+
const branchExplicit = url.includes("/tree/");
|
|
247
|
+
|
|
248
|
+
const resolvedBranch = branchExplicit ? branch : await fetchDefaultBranch(owner, repo);
|
|
249
|
+
const rawBase = `https://raw.githubusercontent.com/${owner}/${repo}/${resolvedBranch}${subdir ? `/${subdir}` : ""}`;
|
|
199
250
|
|
|
200
251
|
const [yamlText, instructions] = await Promise.all([
|
|
201
252
|
fetchText(`${rawBase}/template.yml`),
|
|
202
253
|
fetchText(`${rawBase}/instructions.md`),
|
|
203
254
|
]);
|
|
204
255
|
|
|
205
|
-
const
|
|
256
|
+
const parsed = parseTemplateYaml(yamlText);
|
|
206
257
|
|
|
207
258
|
// Try to list rules via GitHub API
|
|
208
259
|
const rules = await fetchGitHubRules(url);
|
|
209
260
|
|
|
210
|
-
|
|
261
|
+
const base = { ...parsed, instructions, rules };
|
|
262
|
+
return resolveExtends(base);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function fetchDefaultBranch(owner: string, repo: string): Promise<string> {
|
|
266
|
+
try {
|
|
267
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
268
|
+
headers: { Accept: "application/vnd.github.v3+json" },
|
|
269
|
+
});
|
|
270
|
+
if (!response.ok) return "main";
|
|
271
|
+
const data = (await response.json()) as { default_branch?: string };
|
|
272
|
+
return data.default_branch ?? "main";
|
|
273
|
+
} catch {
|
|
274
|
+
return "main";
|
|
275
|
+
}
|
|
211
276
|
}
|
|
212
277
|
|
|
213
278
|
async function fetchText(url: string): Promise<string> {
|
package/src/commands.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import type { CommandFile } from "./types.ts";
|
|
4
|
+
import { ONEAGENT_DIR } from "./constants.ts";
|
|
4
5
|
|
|
5
6
|
export async function readCommands(root: string): Promise<CommandFile[]> {
|
|
6
|
-
const commandsDir = path.join(root, "
|
|
7
|
+
const commandsDir = path.join(root, ONEAGENT_DIR, "commands");
|
|
7
8
|
try {
|
|
8
9
|
const files = await fs.readdir(commandsDir);
|
|
9
10
|
return files
|
package/src/config.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { parse, stringify } from "yaml";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import fs from "fs/promises";
|
|
4
4
|
import type { AgentTarget, Config } from "./types.ts";
|
|
5
|
+
import { ONEAGENT_DIR } from "./constants.ts";
|
|
5
6
|
|
|
6
|
-
const CONFIG_REL =
|
|
7
|
+
const CONFIG_REL = `${ONEAGENT_DIR}/config.yml`;
|
|
7
8
|
|
|
8
9
|
export const ALL_AGENT_TARGETS: AgentTarget[] = ["claude", "cursor", "windsurf", "opencode", "copilot"];
|
|
9
10
|
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ONEAGENT_DIR = ".oneagent";
|
package/src/detect.ts
CHANGED
|
@@ -2,6 +2,7 @@ import path from "path";
|
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import type { DetectedFile } from "./types.ts";
|
|
4
4
|
import { AGENT_DEFINITIONS } from "./agents.ts";
|
|
5
|
+
import { ONEAGENT_DIR } from "./constants.ts";
|
|
5
6
|
|
|
6
7
|
export const AGENT_FILES = [
|
|
7
8
|
"CLAUDE.md",
|
|
@@ -20,7 +21,7 @@ export async function readDetectedFile(root: string, rel: string): Promise<Detec
|
|
|
20
21
|
if (stat.isSymbolicLink()) {
|
|
21
22
|
const linkTarget = await fs.readlink(absolutePath);
|
|
22
23
|
const resolved = path.resolve(path.dirname(absolutePath), linkTarget);
|
|
23
|
-
if (resolved.startsWith(path.join(root,
|
|
24
|
+
if (resolved.startsWith(path.join(root, ONEAGENT_DIR))) return null;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const content = await fs.readFile(absolutePath, "utf-8");
|
package/src/index.ts
CHANGED
package/src/opencode.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import type { RuleFile } from "./types.ts";
|
|
4
|
+
import { ONEAGENT_DIR } from "./constants.ts";
|
|
4
5
|
|
|
5
6
|
export async function readOpencode(root: string): Promise<Record<string, unknown> | null> {
|
|
6
7
|
try {
|
|
@@ -14,7 +15,7 @@ export async function readOpencode(root: string): Promise<Record<string, unknown
|
|
|
14
15
|
export function buildOpencodeConfig(existing: Record<string, unknown> | null): object {
|
|
15
16
|
return {
|
|
16
17
|
...existing,
|
|
17
|
-
instructions:
|
|
18
|
+
instructions: `${ONEAGENT_DIR}/instructions.md`,
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
|
package/src/rules.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import type { RuleFile } from "./types.ts";
|
|
4
|
+
import { ONEAGENT_DIR } from "./constants.ts";
|
|
4
5
|
|
|
5
6
|
export async function readRules(root: string): Promise<RuleFile[]> {
|
|
6
|
-
const rulesDir = path.join(root, "
|
|
7
|
+
const rulesDir = path.join(root, ONEAGENT_DIR, "rules");
|
|
7
8
|
try {
|
|
8
9
|
const files = await fs.readdir(rulesDir);
|
|
9
10
|
return files
|
package/src/skills.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import type { SkillFile } from "./types.ts";
|
|
4
|
+
import { ONEAGENT_DIR } from "./constants.ts";
|
|
4
5
|
|
|
5
6
|
const VALID_MODES = ["ask", "edit", "agent"] as const;
|
|
6
7
|
type SkillMode = (typeof VALID_MODES)[number];
|
|
@@ -29,7 +30,7 @@ export async function readSkillFile(filePath: string): Promise<SkillFile> {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export async function readSkills(root: string): Promise<SkillFile[]> {
|
|
32
|
-
const skillsDir = path.join(root, "
|
|
33
|
+
const skillsDir = path.join(root, ONEAGENT_DIR, "skills");
|
|
33
34
|
try {
|
|
34
35
|
const files = await fs.readdir(skillsDir);
|
|
35
36
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
package/src/status.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import type { Config, GeneratedFileCheck, OpenCodeCheck, RuleFile, StatusResult } from "./types.ts";
|
|
3
3
|
import { activeTargets } from "./config.ts";
|
|
4
|
+
import { ONEAGENT_DIR } from "./constants.ts";
|
|
4
5
|
import { readRules } from "./rules.ts";
|
|
5
6
|
import { readSkills } from "./skills.ts";
|
|
6
7
|
import { buildMainSymlinks, buildRulesSymlinks, buildSkillSymlinks, buildCommandSymlinks, buildAgentsDirSymlinks, checkSymlink } from "./symlinks.ts";
|
|
@@ -26,7 +27,7 @@ export async function checkOpencodeStatus(
|
|
|
26
27
|
): Promise<OpenCodeCheck> {
|
|
27
28
|
const existing = await readOpencode(root);
|
|
28
29
|
if (!existing) return { exists: false, valid: false };
|
|
29
|
-
return { exists: true, valid: existing["instructions"] ===
|
|
30
|
+
return { exists: true, valid: existing["instructions"] === `${ONEAGENT_DIR}/instructions.md` };
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export async function checkCopilotPrompt(root: string, skill: import("./types.ts").SkillFile): Promise<GeneratedFileCheck> {
|
package/src/symlinks.ts
CHANGED
|
@@ -2,6 +2,7 @@ import path from "path";
|
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import type { AgentTarget, SymlinkCheck, SymlinkEntry } from "./types.ts";
|
|
4
4
|
import { AGENT_DEFINITIONS } from "./agents.ts";
|
|
5
|
+
import { ONEAGENT_DIR } from "./constants.ts";
|
|
5
6
|
|
|
6
7
|
export async function ensureDir(dirPath: string): Promise<void> {
|
|
7
8
|
await fs.mkdir(dirPath, { recursive: true });
|
|
@@ -9,12 +10,27 @@ export async function ensureDir(dirPath: string): Promise<void> {
|
|
|
9
10
|
|
|
10
11
|
export async function createSymlink(symlinkPath: string, target: string): Promise<void> {
|
|
11
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)
|
|
12
16
|
try {
|
|
13
|
-
await fs.
|
|
17
|
+
const stat = await fs.lstat(symlinkPath);
|
|
18
|
+
if (stat.isSymbolicLink() && (await fs.readlink(symlinkPath)) === target) return;
|
|
14
19
|
} catch {
|
|
15
|
-
// doesn't exist —
|
|
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
|
+
}
|
|
16
33
|
}
|
|
17
|
-
await fs.symlink(target, symlinkPath);
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
function relativeTarget(symlinkPath: string, targetAbsPath: string): string {
|
|
@@ -22,7 +38,7 @@ function relativeTarget(symlinkPath: string, targetAbsPath: string): string {
|
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
export function buildMainSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
25
|
-
const instructionsAbs = path.join(root, "
|
|
41
|
+
const instructionsAbs = path.join(root, ONEAGENT_DIR, "instructions.md");
|
|
26
42
|
const seen = new Map<string, SymlinkEntry>();
|
|
27
43
|
|
|
28
44
|
for (const target of targets) {
|
|
@@ -41,7 +57,7 @@ export function buildMainSymlinks(root: string, targets: AgentTarget[]): Symlink
|
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
export function buildRulesSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
44
|
-
const targetAbs = path.join(root, "
|
|
60
|
+
const targetAbs = path.join(root, ONEAGENT_DIR, "rules");
|
|
45
61
|
return AGENT_DEFINITIONS
|
|
46
62
|
.filter((d) => targets.includes(d.target) && d.rulesDir)
|
|
47
63
|
.map((d) => {
|
|
@@ -51,7 +67,7 @@ export function buildRulesSymlinks(root: string, targets: AgentTarget[]): Symlin
|
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
export function buildSkillSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
54
|
-
const targetAbs = path.join(root, "
|
|
70
|
+
const targetAbs = path.join(root, ONEAGENT_DIR, "skills");
|
|
55
71
|
return AGENT_DEFINITIONS
|
|
56
72
|
.filter((d) => targets.includes(d.target) && d.skillsDir)
|
|
57
73
|
.map((d) => {
|
|
@@ -61,7 +77,7 @@ export function buildSkillSymlinks(root: string, targets: AgentTarget[]): Symlin
|
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
export function buildCommandSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
64
|
-
const targetAbs = path.join(root, "
|
|
80
|
+
const targetAbs = path.join(root, ONEAGENT_DIR, "commands");
|
|
65
81
|
return AGENT_DEFINITIONS
|
|
66
82
|
.filter((d) => targets.includes(d.target) && d.commandsDir)
|
|
67
83
|
.map((d) => {
|
|
@@ -72,7 +88,7 @@ export function buildCommandSymlinks(root: string, targets: AgentTarget[]): Syml
|
|
|
72
88
|
|
|
73
89
|
export function buildAgentsDirSymlinks(root: string): SymlinkEntry[] {
|
|
74
90
|
const symlinkPath = path.join(root, ".agents/skills");
|
|
75
|
-
const targetAbs = path.join(root, "
|
|
91
|
+
const targetAbs = path.join(root, ONEAGENT_DIR, "skills");
|
|
76
92
|
return [{ symlinkPath, target: relativeTarget(symlinkPath, targetAbs), label: ".agents/skills" }];
|
|
77
93
|
}
|
|
78
94
|
|
|
@@ -85,8 +101,21 @@ async function migrateFilesFromDir(srcDir: string, destDir: string, root: string
|
|
|
85
101
|
return; // srcDir doesn't exist — no-op
|
|
86
102
|
}
|
|
87
103
|
for (const entry of entries) {
|
|
88
|
-
if (!entry.isFile()) continue; // skip symlinks and subdirs
|
|
89
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;
|
|
90
119
|
const destFile = path.join(destDir, entry.name);
|
|
91
120
|
|
|
92
121
|
let destExists = false;
|
|
@@ -105,7 +134,7 @@ async function migrateFilesFromDir(srcDir: string, destDir: string, root: string
|
|
|
105
134
|
]);
|
|
106
135
|
if (srcContent !== destContent) {
|
|
107
136
|
// Different content — backup source before deleting
|
|
108
|
-
const backupDir = path.join(root, "
|
|
137
|
+
const backupDir = path.join(root, ONEAGENT_DIR, "backup");
|
|
109
138
|
await fs.mkdir(backupDir, { recursive: true });
|
|
110
139
|
const safeName = path.relative(root, srcFile).replace(/\//g, "_");
|
|
111
140
|
await fs.writeFile(path.join(backupDir, safeName), srcContent);
|
|
@@ -130,13 +159,16 @@ async function migrateAndRemoveDir(src: string, dest: string, root: string): Pro
|
|
|
130
159
|
}
|
|
131
160
|
|
|
132
161
|
export async function migrateRuleAndSkillFiles(root: string): Promise<void> {
|
|
133
|
-
const destRules = path.join(root, "
|
|
134
|
-
const destSkills = path.join(root, "
|
|
135
|
-
const destCommands = path.join(root, "
|
|
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");
|
|
136
165
|
// Derive migration sources from agent definitions — sequential to avoid same-name conflicts.
|
|
137
166
|
for (const def of AGENT_DEFINITIONS) {
|
|
138
167
|
if (def.rulesDir) await migrateAndRemoveDir(path.join(root, def.rulesDir), destRules, root);
|
|
139
168
|
}
|
|
169
|
+
for (const def of AGENT_DEFINITIONS) {
|
|
170
|
+
if (def.skillsDir) await migrateAndRemoveDir(path.join(root, def.skillsDir), destSkills, root);
|
|
171
|
+
}
|
|
140
172
|
// .agents/skills — standard skills.sh path, not represented in agent definitions
|
|
141
173
|
await migrateAndRemoveDir(path.join(root, ".agents/skills"), destSkills, root);
|
|
142
174
|
for (const def of AGENT_DEFINITIONS) {
|
|
@@ -144,8 +176,57 @@ export async function migrateRuleAndSkillFiles(root: string): Promise<void> {
|
|
|
144
176
|
}
|
|
145
177
|
}
|
|
146
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
|
+
|
|
147
225
|
export async function createAllSymlinks(entries: SymlinkEntry[]): Promise<void> {
|
|
148
|
-
|
|
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()) {
|
|
149
230
|
await createSymlink(e.symlinkPath, e.target);
|
|
150
231
|
}
|
|
151
232
|
}
|