Skip to content

Commit 2c7643e

Browse files
committed
refactor: delegate skill installation to agent-install
Replace the in-house symlink/copy logic in init.ts with installSkillsFromSource from agent-install, and re-export its agent registry instead of maintaining a local one. Drops the now-dead create-symlink-safe helper, swaps amp for the broader set agent-install supports (goose, roo, cline, kilo, universal), and makes init() / detectInstalledAgents() async to match the upstream API.
1 parent e66a2fc commit 2c7643e

10 files changed

Lines changed: 214 additions & 354 deletions

File tree

packages/debug-agent/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"typecheck": "tsc --noEmit"
4747
},
4848
"dependencies": {
49+
"agent-install": "^0.0.2",
4950
"commander": "^14.0.3",
5051
"ora": "^9.3.0",
5152
"picocolors": "^1.1.1",

packages/debug-agent/src/agents.ts

Lines changed: 13 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,16 @@
1-
import { existsSync } from "node:fs";
2-
import { homedir } from "node:os";
3-
import { join } from "node:path";
1+
import {
2+
CANONICAL_SKILLS_DIR,
3+
detectInstalledSkillAgents,
4+
isUniversalSkillAgent,
5+
skillAgents,
6+
} from "agent-install";
7+
import type { SkillAgentConfig, SkillAgentType } from "agent-install";
48

5-
const homeDirectory = homedir();
9+
export type AgentName = SkillAgentType;
10+
export type AgentDef = SkillAgentConfig;
611

7-
export interface AgentDef {
8-
displayName: string;
9-
skillsDir: string;
10-
globalSkillsDir: string;
11-
detect: () => boolean;
12-
}
12+
export const agents = skillAgents;
13+
export const isUniversalAgent = isUniversalSkillAgent;
14+
export const detectInstalledAgents = detectInstalledSkillAgents;
1315

14-
export const CANONICAL_SKILLS_DIR = ".agents/skills";
15-
16-
export const agents: Record<string, AgentDef> = {
17-
cursor: {
18-
displayName: "Cursor",
19-
skillsDir: ".agents/skills",
20-
globalSkillsDir: join(homeDirectory, ".cursor/skills"),
21-
detect: () => existsSync(join(homeDirectory, ".cursor")),
22-
},
23-
"claude-code": {
24-
displayName: "Claude Code",
25-
skillsDir: ".claude/skills",
26-
globalSkillsDir: join(homeDirectory, ".claude/skills"),
27-
detect: () => existsSync(join(homeDirectory, ".claude")),
28-
},
29-
codex: {
30-
displayName: "Codex",
31-
skillsDir: ".agents/skills",
32-
globalSkillsDir: join(homeDirectory, ".codex/skills"),
33-
detect: () => existsSync(join(homeDirectory, ".codex")),
34-
},
35-
"github-copilot": {
36-
displayName: "GitHub Copilot",
37-
skillsDir: ".agents/skills",
38-
globalSkillsDir: join(homeDirectory, ".copilot/skills"),
39-
detect: () => existsSync(join(homeDirectory, ".copilot")),
40-
},
41-
"gemini-cli": {
42-
displayName: "Gemini CLI",
43-
skillsDir: ".agents/skills",
44-
globalSkillsDir: join(homeDirectory, ".gemini/skills"),
45-
detect: () => existsSync(join(homeDirectory, ".gemini")),
46-
},
47-
windsurf: {
48-
displayName: "Windsurf",
49-
skillsDir: ".windsurf/skills",
50-
globalSkillsDir: join(homeDirectory, ".codeium/windsurf/skills"),
51-
detect: () => existsSync(join(homeDirectory, ".codeium/windsurf")),
52-
},
53-
amp: {
54-
displayName: "Amp",
55-
skillsDir: ".agents/skills",
56-
globalSkillsDir: join(homeDirectory, ".config/agents/skills"),
57-
detect: () => existsSync(join(homeDirectory, ".config/amp")),
58-
},
59-
opencode: {
60-
displayName: "OpenCode",
61-
skillsDir: ".agents/skills",
62-
globalSkillsDir: join(homeDirectory, ".config/opencode/skills"),
63-
detect: () => existsSync(join(homeDirectory, ".config/opencode")),
64-
},
65-
};
66-
67-
export const isUniversalAgent = (agentDefinition: AgentDef): boolean =>
68-
agentDefinition.skillsDir === CANONICAL_SKILLS_DIR;
69-
70-
export const detectInstalledAgents = (): Array<[string, AgentDef]> =>
71-
Object.entries(agents).filter(([_agentKey, agentDefinition]) => agentDefinition.detect());
16+
export { CANONICAL_SKILLS_DIR };

packages/debug-agent/src/commands/init.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ export const initCommand = new Command("init")
1515
.action(async (options) => {
1616
if (options.list) {
1717
logger.break();
18-
for (const [agentKey, agentDefinition] of Object.entries(agents)) {
19-
const detectionIndicator = agentDefinition.detect()
20-
? highlighter.success("✓")
21-
: highlighter.dim("·");
18+
const detectionStatuses = await Promise.all(
19+
Object.entries(agents).map(async ([agentKey, agentDefinition]) => ({
20+
agentKey,
21+
agentDefinition,
22+
isInstalled: await agentDefinition.detectInstalled(),
23+
})),
24+
);
25+
for (const { agentKey, agentDefinition, isInstalled } of detectionStatuses) {
26+
const detectionIndicator = isInstalled ? highlighter.success("✓") : highlighter.dim("·");
2227
logger.log(
2328
` ${detectionIndicator} ${highlighter.bold(agentDefinition.displayName)} ${highlighter.dim(agentKey)}`,
2429
);
@@ -30,7 +35,7 @@ export const initCommand = new Command("init")
3035
let selectedAgents: string[] | undefined = options.agent;
3136

3237
if (!selectedAgents) {
33-
const installedAgents = detectInstalledAgents();
38+
const installedAgentSet = new Set<string>(await detectInstalledAgents());
3439

3540
logger.break();
3641
const response = await prompts({
@@ -40,7 +45,7 @@ export const initCommand = new Command("init")
4045
choices: Object.entries(agents).map(([agentKey, agentDefinition]) => ({
4146
title: agentDefinition.displayName,
4247
value: agentKey,
43-
selected: installedAgents.some(([installedKey]) => installedKey === agentKey),
48+
selected: installedAgentSet.has(agentKey),
4449
})),
4550
hint: "Space to select, Enter to confirm",
4651
});
@@ -55,7 +60,7 @@ export const initCommand = new Command("init")
5560

5661
const installSpinner = spinner("Installing debug-agent skill...").start();
5762

58-
const initResult = init({
63+
const initResult = await init({
5964
global: options.global,
6065
agent: selectedAgents,
6166
copy: options.copy,

packages/debug-agent/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export { createServer } from "./server.js";
22
export { init } from "./init.js";
3-
export { agents, detectInstalledAgents, isUniversalAgent } from "./agents.js";
3+
export { agents, detectInstalledAgents, isUniversalAgent, CANONICAL_SKILLS_DIR } from "./agents.js";
44
export type { ServerOptions, ServerInfo, ServerResult } from "./server.js";
5-
export type { AgentDef } from "./agents.js";
5+
export type { AgentDef, AgentName } from "./agents.js";
66
export type { InitOptions, InitResult } from "./init.js";

packages/debug-agent/src/init.ts

Lines changed: 74 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import fs from "node:fs";
21
import { homedir } from "node:os";
32
import path from "node:path";
4-
import { agents, isUniversalAgent, detectInstalledAgents, CANONICAL_SKILLS_DIR } from "./agents.js";
5-
import { createSymlinkSafe } from "./utils/create-symlink-safe.js";
3+
import {
4+
CANONICAL_SKILLS_DIR,
5+
detectInstalledSkillAgents,
6+
installSkillsFromSource,
7+
isSkillAgentType,
8+
} from "agent-install";
9+
import type { InstallMode, SkillAgentType } from "agent-install";
610

711
const SKILL_NAME = "debug-agent";
812

9-
const getSkillContent = (): string => {
10-
const skillPath = path.resolve(import.meta.dirname, "..", "skill", "SKILL.md");
11-
return fs.readFileSync(skillPath, "utf-8");
12-
};
13+
const getSkillSourceDirectory = (): string => path.resolve(import.meta.dirname, "..", "skill");
1314

1415
export interface InitOptions {
1516
global?: boolean;
@@ -24,73 +25,79 @@ export interface InitResult {
2425
errors: string[];
2526
}
2627

27-
const ensureRealDirectory = (directory: string) => {
28-
const segments = directory.split(path.sep);
29-
for (let index = 1; index <= segments.length; index++) {
30-
const partial = segments.slice(0, index).join(path.sep) || path.sep;
31-
try {
32-
const stat = fs.lstatSync(partial);
33-
if (stat.isSymbolicLink()) {
34-
fs.unlinkSync(partial);
35-
fs.mkdirSync(partial, { recursive: true });
36-
}
37-
} catch {
38-
break;
28+
interface ResolvedAgents {
29+
agents: SkillAgentType[];
30+
userIncludedUniversal: boolean;
31+
}
32+
33+
const resolveRequestedAgents = async (
34+
requestedAgents: string[] | undefined,
35+
errors: string[],
36+
): Promise<ResolvedAgents> => {
37+
if (requestedAgents === undefined) {
38+
return {
39+
agents: await detectInstalledSkillAgents(),
40+
userIncludedUniversal: false,
41+
};
42+
}
43+
44+
const validAgents: SkillAgentType[] = [];
45+
let userIncludedUniversal = false;
46+
47+
for (const agentName of requestedAgents) {
48+
if (isSkillAgentType(agentName)) {
49+
if (agentName === "universal") userIncludedUniversal = true;
50+
validAgents.push(agentName);
51+
} else {
52+
errors.push(`Unknown agent: ${agentName}`);
3953
}
4054
}
41-
fs.mkdirSync(directory, { recursive: true });
42-
};
4355

44-
const writeSkillToDirectory = (directory: string, content: string) => {
45-
ensureRealDirectory(directory);
46-
fs.writeFileSync(path.join(directory, "SKILL.md"), content);
56+
return { agents: validAgents, userIncludedUniversal };
4757
};
4858

49-
export const init = (options: InitOptions = {}): InitResult => {
50-
const workingDirectory = options.cwd || process.cwd();
59+
export const init = async (options: InitOptions = {}): Promise<InitResult> => {
5160
const isGlobal = options.global ?? false;
52-
const useSymlinks = !(options.copy ?? false);
53-
54-
const baseDirectory = isGlobal ? homedir() : workingDirectory;
55-
const canonicalDirectory = path.join(baseDirectory, CANONICAL_SKILLS_DIR, SKILL_NAME);
56-
57-
const result: InitResult = {
58-
canonicalPath: canonicalDirectory,
59-
linkedAgents: [],
60-
errors: [],
61-
};
62-
63-
const skillContent = getSkillContent();
64-
65-
writeSkillToDirectory(canonicalDirectory, skillContent);
66-
67-
const agentsToInstall = options.agent
68-
? options.agent
69-
.map((agentName) => [agentName, agents[agentName]] as const)
70-
.filter(([agentName, agentDefinition]) => {
71-
if (!agentDefinition) {
72-
result.errors.push(`Unknown agent: ${agentName}`);
73-
return false;
74-
}
75-
return true;
76-
})
77-
: detectInstalledAgents();
78-
79-
for (const [agentName, agentDefinition] of agentsToInstall) {
80-
if (!isUniversalAgent(agentDefinition)) {
81-
const agentSkillsDirectory = isGlobal
82-
? agentDefinition.globalSkillsDir
83-
: path.join(workingDirectory, agentDefinition.skillsDir);
84-
const agentSkillDirectory = path.join(agentSkillsDirectory, SKILL_NAME);
85-
86-
const didLink = useSymlinks && createSymlinkSafe(canonicalDirectory, agentSkillDirectory);
87-
if (!didLink) {
88-
writeSkillToDirectory(agentSkillDirectory, skillContent);
89-
}
90-
}
61+
const workingDirectory = options.cwd || process.cwd();
62+
const installMode: InstallMode = options.copy ? "copy" : "symlink";
63+
64+
const canonicalPath = path.join(
65+
isGlobal ? homedir() : workingDirectory,
66+
CANONICAL_SKILLS_DIR,
67+
SKILL_NAME,
68+
);
69+
70+
const errors: string[] = [];
71+
const { agents: requestedAgents, userIncludedUniversal } = await resolveRequestedAgents(
72+
options.agent,
73+
errors,
74+
);
75+
76+
const agentsToInstall: SkillAgentType[] = requestedAgents.includes("universal")
77+
? requestedAgents
78+
: [...requestedAgents, "universal"];
79+
80+
const installResult = await installSkillsFromSource({
81+
source: getSkillSourceDirectory(),
82+
agents: agentsToInstall,
83+
cwd: workingDirectory,
84+
global: isGlobal,
85+
mode: installMode,
86+
});
87+
88+
for (const failure of installResult.failed) {
89+
errors.push(`${failure.agent}: ${failure.error}`);
90+
}
9191

92-
result.linkedAgents.push(agentName);
92+
const linkedAgentSet = new Set<string>();
93+
for (const installed of installResult.installed) {
94+
if (!userIncludedUniversal && installed.agent === "universal") continue;
95+
linkedAgentSet.add(installed.agent);
9396
}
9497

95-
return result;
98+
return {
99+
canonicalPath,
100+
linkedAgents: [...linkedAgentSet],
101+
errors,
102+
};
96103
};

packages/debug-agent/src/utils/create-symlink-safe.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)