@codemcp/ade-harnesses 0.0.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/.prettierignore +1 -0
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-format.log +6 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +20 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +39 -0
- package/dist/skills-installer.d.ts +13 -0
- package/dist/skills-installer.js +46 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +53 -0
- package/dist/util.js +130 -0
- package/dist/writers/claude-code.d.ts +2 -0
- package/dist/writers/claude-code.js +45 -0
- package/dist/writers/cline.d.ts +2 -0
- package/dist/writers/cline.js +15 -0
- package/dist/writers/copilot.d.ts +2 -0
- package/dist/writers/copilot.js +28 -0
- package/dist/writers/cursor.d.ts +2 -0
- package/dist/writers/cursor.js +27 -0
- package/dist/writers/kiro.d.ts +2 -0
- package/dist/writers/kiro.js +46 -0
- package/dist/writers/opencode.d.ts +2 -0
- package/dist/writers/opencode.js +45 -0
- package/dist/writers/roo-code.d.ts +2 -0
- package/dist/writers/roo-code.js +15 -0
- package/dist/writers/universal.d.ts +2 -0
- package/dist/writers/universal.js +22 -0
- package/dist/writers/windsurf.d.ts +2 -0
- package/dist/writers/windsurf.js +15 -0
- package/eslint.config.mjs +40 -0
- package/package.json +35 -0
- package/src/index.spec.ts +45 -0
- package/src/index.ts +46 -0
- package/src/skills-installer.ts +54 -0
- package/src/types.ts +12 -0
- package/src/util.ts +208 -0
- package/src/writers/claude-code.spec.ts +162 -0
- package/src/writers/claude-code.ts +64 -0
- package/src/writers/cline.spec.ts +69 -0
- package/src/writers/cline.ts +24 -0
- package/src/writers/copilot.spec.ts +102 -0
- package/src/writers/copilot.ts +38 -0
- package/src/writers/cursor.spec.ts +116 -0
- package/src/writers/cursor.ts +33 -0
- package/src/writers/kiro.ts +52 -0
- package/src/writers/opencode.ts +54 -0
- package/src/writers/roo-code.spec.ts +69 -0
- package/src/writers/roo-code.ts +24 -0
- package/src/writers/universal.ts +31 -0
- package/src/writers/windsurf.spec.ts +70 -0
- package/src/writers/windsurf.ts +27 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +7 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.vitest.json +7 -0
- package/vitest.config.ts +5 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { writeMcpServers, writeGitHooks } from "../util.js";
|
|
4
|
+
export const universalWriter = {
|
|
5
|
+
id: "universal",
|
|
6
|
+
label: "Universal (AGENTS.md + .mcp.json)",
|
|
7
|
+
description: "Cross-tool standard — AGENTS.md + .mcp.json (works with any agent)",
|
|
8
|
+
async install(config, projectRoot) {
|
|
9
|
+
if (config.instructions.length > 0) {
|
|
10
|
+
const lines = [
|
|
11
|
+
"# AGENTS",
|
|
12
|
+
"",
|
|
13
|
+
...config.instructions.flatMap((i) => [i, ""])
|
|
14
|
+
];
|
|
15
|
+
await writeFile(join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8");
|
|
16
|
+
}
|
|
17
|
+
await writeMcpServers(config.mcp_servers, {
|
|
18
|
+
path: join(projectRoot, ".mcp.json")
|
|
19
|
+
});
|
|
20
|
+
await writeGitHooks(config.git_hooks, projectRoot);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { writeMcpServers, alwaysAllowEntry, writeRulesFile, writeGitHooks } from "../util.js";
|
|
3
|
+
export const windsurfWriter = {
|
|
4
|
+
id: "windsurf",
|
|
5
|
+
label: "Windsurf",
|
|
6
|
+
description: "Codeium's AI IDE — .windsurf/mcp.json + .windsurfrules",
|
|
7
|
+
async install(config, projectRoot) {
|
|
8
|
+
await writeMcpServers(config.mcp_servers, {
|
|
9
|
+
path: join(projectRoot, ".windsurf", "mcp.json"),
|
|
10
|
+
transform: alwaysAllowEntry
|
|
11
|
+
});
|
|
12
|
+
await writeRulesFile(config.instructions, join(projectRoot, ".windsurfrules"));
|
|
13
|
+
await writeGitHooks(config.git_hooks, projectRoot);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import { parser, configs } from "typescript-eslint";
|
|
3
|
+
import prettier from "eslint-config-prettier";
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
js.configs.recommended,
|
|
7
|
+
...configs.recommended,
|
|
8
|
+
prettier,
|
|
9
|
+
{
|
|
10
|
+
// Config for TypeScript files
|
|
11
|
+
files: ["**/*.{ts,tsx}"],
|
|
12
|
+
languageOptions: {
|
|
13
|
+
parser,
|
|
14
|
+
parserOptions: {
|
|
15
|
+
project: ["./tsconfig.json", "./tsconfig.vitest.json"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
// Config for JavaScript files - no TypeScript parsing
|
|
21
|
+
files: ["**/*.{js,jsx}"],
|
|
22
|
+
...js.configs.recommended
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
// Relaxed rules for test files
|
|
26
|
+
files: ["**/*.test.ts", "**/*.spec.ts"],
|
|
27
|
+
rules: {
|
|
28
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
29
|
+
"@typescript-eslint/no-unused-vars": "off"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
ignores: [
|
|
34
|
+
"**/node_modules/**",
|
|
35
|
+
"**/dist/**",
|
|
36
|
+
".pnpm-store/**",
|
|
37
|
+
"pnpm-lock.yaml"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codemcp/ade-harnesses",
|
|
3
|
+
"main": "dist/index.js",
|
|
4
|
+
"types": "dist/index.d.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@codemcp/skills": "^2.3.0",
|
|
11
|
+
"@codemcp/ade-core": "0.0.2"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
|
15
|
+
"@typescript-eslint/parser": "^8.21.0",
|
|
16
|
+
"eslint": "^9.18.0",
|
|
17
|
+
"eslint-config-prettier": "^10.0.1",
|
|
18
|
+
"prettier": "^3.4.2",
|
|
19
|
+
"rimraf": "^6.0.1",
|
|
20
|
+
"typescript": "^5.7.3"
|
|
21
|
+
},
|
|
22
|
+
"version": "0.0.2",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.build.json",
|
|
25
|
+
"clean:build": "rimraf ./dist",
|
|
26
|
+
"dev": "nodemon",
|
|
27
|
+
"lint": "eslint .",
|
|
28
|
+
"lint:fix": "eslint --fix .",
|
|
29
|
+
"format": "prettier --check .",
|
|
30
|
+
"format:fix": "prettier --write .",
|
|
31
|
+
"test": "vitest --run",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"typecheck": "tsc"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { allHarnessWriters, getHarnessWriter, getHarnessIds } from "./index.js";
|
|
3
|
+
|
|
4
|
+
describe("harness registry", () => {
|
|
5
|
+
it("exports all harness writers", () => {
|
|
6
|
+
expect(allHarnessWriters).toHaveLength(9);
|
|
7
|
+
const ids = allHarnessWriters.map((w) => w.id);
|
|
8
|
+
expect(ids).toContain("universal");
|
|
9
|
+
expect(ids).toContain("claude-code");
|
|
10
|
+
expect(ids).toContain("cursor");
|
|
11
|
+
expect(ids).toContain("copilot");
|
|
12
|
+
expect(ids).toContain("windsurf");
|
|
13
|
+
expect(ids).toContain("cline");
|
|
14
|
+
expect(ids).toContain("roo-code");
|
|
15
|
+
expect(ids).toContain("kiro");
|
|
16
|
+
expect(ids).toContain("opencode");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("looks up harness by id", () => {
|
|
20
|
+
expect(getHarnessWriter("cursor")?.label).toBe("Cursor");
|
|
21
|
+
expect(getHarnessWriter("nonexistent")).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns all harness ids", () => {
|
|
25
|
+
const ids = getHarnessIds();
|
|
26
|
+
expect(ids).toEqual([
|
|
27
|
+
"universal",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"cursor",
|
|
30
|
+
"copilot",
|
|
31
|
+
"windsurf",
|
|
32
|
+
"cline",
|
|
33
|
+
"roo-code",
|
|
34
|
+
"kiro",
|
|
35
|
+
"opencode"
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("all writers have label and description", () => {
|
|
40
|
+
for (const w of allHarnessWriters) {
|
|
41
|
+
expect(w.label).toBeTruthy();
|
|
42
|
+
expect(w.description).toBeTruthy();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type { HarnessWriter } from "./types.js";
|
|
2
|
+
export { installSkills } from "./skills-installer.js";
|
|
3
|
+
|
|
4
|
+
export { universalWriter } from "./writers/universal.js";
|
|
5
|
+
export { claudeCodeWriter } from "./writers/claude-code.js";
|
|
6
|
+
export { cursorWriter } from "./writers/cursor.js";
|
|
7
|
+
export { copilotWriter } from "./writers/copilot.js";
|
|
8
|
+
export { windsurfWriter } from "./writers/windsurf.js";
|
|
9
|
+
export { clineWriter } from "./writers/cline.js";
|
|
10
|
+
export { rooCodeWriter } from "./writers/roo-code.js";
|
|
11
|
+
export { kiroWriter } from "./writers/kiro.js";
|
|
12
|
+
export { opencodeWriter } from "./writers/opencode.js";
|
|
13
|
+
|
|
14
|
+
import type { HarnessWriter } from "./types.js";
|
|
15
|
+
import { universalWriter } from "./writers/universal.js";
|
|
16
|
+
import { claudeCodeWriter } from "./writers/claude-code.js";
|
|
17
|
+
import { cursorWriter } from "./writers/cursor.js";
|
|
18
|
+
import { copilotWriter } from "./writers/copilot.js";
|
|
19
|
+
import { windsurfWriter } from "./writers/windsurf.js";
|
|
20
|
+
import { clineWriter } from "./writers/cline.js";
|
|
21
|
+
import { rooCodeWriter } from "./writers/roo-code.js";
|
|
22
|
+
import { kiroWriter } from "./writers/kiro.js";
|
|
23
|
+
import { opencodeWriter } from "./writers/opencode.js";
|
|
24
|
+
|
|
25
|
+
/** All built-in harness writers, ordered for wizard display. */
|
|
26
|
+
export const allHarnessWriters: HarnessWriter[] = [
|
|
27
|
+
universalWriter,
|
|
28
|
+
claudeCodeWriter,
|
|
29
|
+
cursorWriter,
|
|
30
|
+
copilotWriter,
|
|
31
|
+
windsurfWriter,
|
|
32
|
+
clineWriter,
|
|
33
|
+
rooCodeWriter,
|
|
34
|
+
kiroWriter,
|
|
35
|
+
opencodeWriter
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** Look up a harness writer by id. */
|
|
39
|
+
export function getHarnessWriter(id: string): HarnessWriter | undefined {
|
|
40
|
+
return allHarnessWriters.find((w) => w.id === id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** All valid harness IDs. */
|
|
44
|
+
export function getHarnessIds(): string[] {
|
|
45
|
+
return allHarnessWriters.map((w) => w.id);
|
|
46
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { SkillDefinition, InlineSkill } from "@codemcp/ade-core";
|
|
3
|
+
import { runAdd } from "@codemcp/skills/api";
|
|
4
|
+
|
|
5
|
+
function isInlineSkill(skill: SkillDefinition): skill is InlineSkill {
|
|
6
|
+
return "body" in skill;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Install skills using the @codemcp/skills programmatic API.
|
|
11
|
+
*
|
|
12
|
+
* Inline skills are expected to already exist as SKILL.md files under
|
|
13
|
+
* `<projectRoot>/.ade/skills/<name>/` (written by the agent writer).
|
|
14
|
+
* This function calls `runAdd` with the local path for inline skills
|
|
15
|
+
* and the remote source for external skills.
|
|
16
|
+
*
|
|
17
|
+
* Note: `runAdd` uses `process.cwd()` to determine the install destination.
|
|
18
|
+
* This function changes cwd to `projectRoot` before calling `runAdd`.
|
|
19
|
+
*/
|
|
20
|
+
export async function installSkills(
|
|
21
|
+
skills: SkillDefinition[],
|
|
22
|
+
projectRoot: string
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
if (skills.length === 0) return;
|
|
25
|
+
|
|
26
|
+
const originalCwd = process.cwd();
|
|
27
|
+
process.chdir(projectRoot);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
for (const skill of skills) {
|
|
31
|
+
const source = isInlineSkill(skill)
|
|
32
|
+
? join(projectRoot, ".ade", "skills", skill.name)
|
|
33
|
+
: skill.source;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await runAdd([source], { yes: true, all: true });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// runAdd may throw on network errors for external skills.
|
|
39
|
+
// Log and continue — inline skills should always succeed.
|
|
40
|
+
console.warn(
|
|
41
|
+
`Warning: failed to install skill "${skill.name}" from ${source}:`,
|
|
42
|
+
err instanceof Error ? err.message : err
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
// Restore cwd only if the original directory still exists
|
|
48
|
+
try {
|
|
49
|
+
process.chdir(originalCwd);
|
|
50
|
+
} catch {
|
|
51
|
+
// Original cwd may have been removed (e.g. in tests)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentWriterDef } from "@codemcp/ade-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A harness writer extends AgentWriterDef with metadata for display in the
|
|
5
|
+
* setup wizard and CLI help.
|
|
6
|
+
*/
|
|
7
|
+
export interface HarnessWriter extends AgentWriterDef {
|
|
8
|
+
/** Human-readable label for the wizard (e.g. "Claude Code") */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Short description shown as hint in the wizard */
|
|
11
|
+
description: string;
|
|
12
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import type { GitHook, LogicalConfig, McpServerEntry } from "@codemcp/ade-core";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// JSON helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/** Read a JSON file, returning `{}` if missing or unparseable. */
|
|
10
|
+
export async function readJsonOrEmpty(
|
|
11
|
+
path: string
|
|
12
|
+
): Promise<Record<string, unknown>> {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(await readFile(path, "utf-8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Write a JSON object with trailing newline. Creates parent dirs. */
|
|
21
|
+
export async function writeJson(path: string, data: unknown): Promise<void> {
|
|
22
|
+
await mkdir(dirname(path), { recursive: true });
|
|
23
|
+
await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Server entry transform — each harness overrides only what differs
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** Minimal MCP entry: command + args + optional env. */
|
|
31
|
+
function baseEntry(server: McpServerEntry) {
|
|
32
|
+
return {
|
|
33
|
+
command: server.command,
|
|
34
|
+
args: server.args,
|
|
35
|
+
...(Object.keys(server.env).length > 0 ? { env: server.env } : {})
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ServerTransform = (
|
|
40
|
+
server: McpServerEntry
|
|
41
|
+
) => Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
/** Standard mcpServers entry (cursor, universal, claude-code). */
|
|
44
|
+
export const standardEntry: ServerTransform = baseEntry;
|
|
45
|
+
|
|
46
|
+
/** Adds `type: "stdio"` (copilot). */
|
|
47
|
+
export const stdioEntry: ServerTransform = (s) => ({
|
|
48
|
+
type: "stdio",
|
|
49
|
+
...baseEntry(s)
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** Adds `alwaysAllow` (cline, roo-code, windsurf). */
|
|
53
|
+
export const alwaysAllowEntry: ServerTransform = (s) => ({
|
|
54
|
+
...baseEntry(s),
|
|
55
|
+
alwaysAllow: s.allowedTools ?? ["*"]
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// MCP JSON writer — covers 7 of 9 harnesses
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
interface WriteMcpServersOpts {
|
|
63
|
+
/** Full path to the JSON file. */
|
|
64
|
+
path: string;
|
|
65
|
+
/** Key in the JSON that holds the server map. Default: `"mcpServers"`. */
|
|
66
|
+
key?: string;
|
|
67
|
+
/** Transform each McpServerEntry into the harness-specific shape. */
|
|
68
|
+
transform?: ServerTransform;
|
|
69
|
+
/** Extra top-level fields to merge (e.g. `$schema`). */
|
|
70
|
+
defaults?: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Merge MCP server entries into an existing JSON config file.
|
|
75
|
+
* Creates the file (and parent dirs) if missing.
|
|
76
|
+
*/
|
|
77
|
+
export async function writeMcpServers(
|
|
78
|
+
servers: McpServerEntry[],
|
|
79
|
+
opts: WriteMcpServersOpts
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
if (servers.length === 0) return;
|
|
82
|
+
|
|
83
|
+
const key = opts.key ?? "mcpServers";
|
|
84
|
+
const transform = opts.transform ?? standardEntry;
|
|
85
|
+
|
|
86
|
+
const existing = await readJsonOrEmpty(opts.path);
|
|
87
|
+
const map = (existing[key] as Record<string, unknown>) ?? {};
|
|
88
|
+
|
|
89
|
+
for (const server of servers) {
|
|
90
|
+
map[server.ref] = transform(server);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = { ...(opts.defaults ?? {}), ...existing, [key]: map };
|
|
94
|
+
await writeJson(opts.path, result);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Instructions → flat rules file (windsurf, cline, roo-code)
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Write instructions as a plain text rules file.
|
|
103
|
+
* Skips if no instructions.
|
|
104
|
+
*/
|
|
105
|
+
export async function writeRulesFile(
|
|
106
|
+
instructions: string[],
|
|
107
|
+
path: string
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
if (instructions.length === 0) return;
|
|
110
|
+
const lines = instructions.flatMap((i) => [i, ""]);
|
|
111
|
+
await mkdir(dirname(path), { recursive: true });
|
|
112
|
+
await writeFile(path, lines.join("\n"), "utf-8");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Instructions → agent markdown with YAML frontmatter
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
interface AgentMdOpts {
|
|
120
|
+
/** Full path to the .md file. */
|
|
121
|
+
path: string;
|
|
122
|
+
/** Extra YAML frontmatter lines (after name/description, before `---`). */
|
|
123
|
+
extraFrontmatter?: string[];
|
|
124
|
+
/** Fallback body when instructions are empty. */
|
|
125
|
+
fallbackBody?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Write an agent markdown file with YAML frontmatter.
|
|
130
|
+
* Shared by claude-code, copilot, and opencode.
|
|
131
|
+
*/
|
|
132
|
+
export async function writeAgentMd(
|
|
133
|
+
config: LogicalConfig,
|
|
134
|
+
opts: AgentMdOpts
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
if (config.instructions.length === 0 && config.mcp_servers.length === 0)
|
|
137
|
+
return;
|
|
138
|
+
|
|
139
|
+
const fm: string[] = [
|
|
140
|
+
"---",
|
|
141
|
+
"name: ade",
|
|
142
|
+
"description: ADE — Agentic Development Environment agent with project conventions and tools"
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
if (opts.extraFrontmatter) {
|
|
146
|
+
fm.push(...opts.extraFrontmatter);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fm.push("---");
|
|
150
|
+
|
|
151
|
+
const body =
|
|
152
|
+
config.instructions.length > 0
|
|
153
|
+
? config.instructions.join("\n\n")
|
|
154
|
+
: (opts.fallbackBody ?? "");
|
|
155
|
+
|
|
156
|
+
const content = fm.join("\n") + "\n\n" + body + "\n";
|
|
157
|
+
await mkdir(dirname(opts.path), { recursive: true });
|
|
158
|
+
await writeFile(opts.path, content, "utf-8");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Inline skill SKILL.md writer (used by claude-code)
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Git hook installer
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Write git hook scripts to `.git/hooks/<phase>`.
|
|
171
|
+
* Files are created with executable permissions (0o755).
|
|
172
|
+
* No-op when the hooks array is empty.
|
|
173
|
+
*/
|
|
174
|
+
export async function writeGitHooks(
|
|
175
|
+
hooks: GitHook[] | undefined,
|
|
176
|
+
projectRoot: string
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
if (!hooks) return;
|
|
179
|
+
for (const hook of hooks) {
|
|
180
|
+
const hookPath = join(projectRoot, ".git", "hooks", hook.phase);
|
|
181
|
+
await writeFile(hookPath, hook.script, { mode: 0o755 });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function writeInlineSkills(
|
|
186
|
+
config: LogicalConfig,
|
|
187
|
+
projectRoot: string
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
for (const skill of config.skills) {
|
|
190
|
+
if (!("body" in skill)) continue;
|
|
191
|
+
|
|
192
|
+
const skillDir = join(projectRoot, ".ade", "skills", skill.name);
|
|
193
|
+
await mkdir(skillDir, { recursive: true });
|
|
194
|
+
|
|
195
|
+
const frontmatter = [
|
|
196
|
+
"---",
|
|
197
|
+
`name: ${skill.name}`,
|
|
198
|
+
`description: ${skill.description}`,
|
|
199
|
+
"---"
|
|
200
|
+
].join("\n");
|
|
201
|
+
|
|
202
|
+
await writeFile(
|
|
203
|
+
join(skillDir, "SKILL.md"),
|
|
204
|
+
`${frontmatter}\n\n${skill.body}\n`,
|
|
205
|
+
"utf-8"
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { LogicalConfig } from "@codemcp/ade-core";
|
|
6
|
+
import { claudeCodeWriter } from "./claude-code.js";
|
|
7
|
+
|
|
8
|
+
describe("claudeCodeWriter", () => {
|
|
9
|
+
let dir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
dir = await mkdtemp(join(tmpdir(), "ade-harness-cc-"));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await rm(dir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("has correct metadata", () => {
|
|
20
|
+
expect(claudeCodeWriter.id).toBe("claude-code");
|
|
21
|
+
expect(claudeCodeWriter.label).toBe("Claude Code");
|
|
22
|
+
expect(claudeCodeWriter.description).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("writes .claude/agents/ade.md custom agent", async () => {
|
|
26
|
+
const config: LogicalConfig = {
|
|
27
|
+
mcp_servers: [
|
|
28
|
+
{
|
|
29
|
+
ref: "workflows",
|
|
30
|
+
command: "npx",
|
|
31
|
+
args: ["-y", "@codemcp/workflows"],
|
|
32
|
+
env: {}
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
instructions: ["Use workflow files.", "Follow conventions."],
|
|
36
|
+
cli_actions: [],
|
|
37
|
+
knowledge_sources: [],
|
|
38
|
+
skills: [],
|
|
39
|
+
git_hooks: [],
|
|
40
|
+
setup_notes: []
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
await claudeCodeWriter.install(config, dir);
|
|
44
|
+
|
|
45
|
+
const content = await readFile(
|
|
46
|
+
join(dir, ".claude", "agents", "ade.md"),
|
|
47
|
+
"utf-8"
|
|
48
|
+
);
|
|
49
|
+
expect(content).toContain("name: ade");
|
|
50
|
+
expect(content).toContain("description:");
|
|
51
|
+
expect(content).toContain("Use workflow files.");
|
|
52
|
+
expect(content).toContain("Follow conventions.");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("writes .mcp.json with MCP servers", async () => {
|
|
56
|
+
const config: LogicalConfig = {
|
|
57
|
+
mcp_servers: [
|
|
58
|
+
{
|
|
59
|
+
ref: "@codemcp/workflows",
|
|
60
|
+
command: "npx",
|
|
61
|
+
args: ["-y", "@codemcp/workflows"],
|
|
62
|
+
env: {}
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
instructions: [],
|
|
66
|
+
cli_actions: [],
|
|
67
|
+
knowledge_sources: [],
|
|
68
|
+
skills: [],
|
|
69
|
+
git_hooks: [],
|
|
70
|
+
setup_notes: []
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await claudeCodeWriter.install(config, dir);
|
|
74
|
+
|
|
75
|
+
const raw = await readFile(join(dir, ".mcp.json"), "utf-8");
|
|
76
|
+
const parsed = JSON.parse(raw);
|
|
77
|
+
expect(parsed.mcpServers["@codemcp/workflows"]).toEqual({
|
|
78
|
+
command: "npx",
|
|
79
|
+
args: ["-y", "@codemcp/workflows"]
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("writes .claude/settings.json with MCP tool permissions", async () => {
|
|
84
|
+
const config: LogicalConfig = {
|
|
85
|
+
mcp_servers: [
|
|
86
|
+
{
|
|
87
|
+
ref: "workflows",
|
|
88
|
+
command: "npx",
|
|
89
|
+
args: ["-y", "@codemcp/workflows"],
|
|
90
|
+
env: {}
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
instructions: [],
|
|
94
|
+
cli_actions: [],
|
|
95
|
+
knowledge_sources: [],
|
|
96
|
+
skills: [],
|
|
97
|
+
git_hooks: [],
|
|
98
|
+
setup_notes: []
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
await claudeCodeWriter.install(config, dir);
|
|
102
|
+
|
|
103
|
+
const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
|
|
104
|
+
const settings = JSON.parse(raw);
|
|
105
|
+
expect(settings.permissions.allow).toContain("MCP(workflows:*)");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("includes agentskills server from mcp_servers", async () => {
|
|
109
|
+
const config: LogicalConfig = {
|
|
110
|
+
mcp_servers: [
|
|
111
|
+
{
|
|
112
|
+
ref: "agentskills",
|
|
113
|
+
command: "npx",
|
|
114
|
+
args: ["-y", "@codemcp/skills-server"],
|
|
115
|
+
env: {}
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
instructions: [],
|
|
119
|
+
cli_actions: [],
|
|
120
|
+
knowledge_sources: [],
|
|
121
|
+
skills: [{ name: "my-skill", description: "A skill", body: "Do stuff." }],
|
|
122
|
+
git_hooks: [],
|
|
123
|
+
setup_notes: []
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
await claudeCodeWriter.install(config, dir);
|
|
127
|
+
|
|
128
|
+
const raw = await readFile(join(dir, ".mcp.json"), "utf-8");
|
|
129
|
+
const parsed = JSON.parse(raw);
|
|
130
|
+
expect(parsed.mcpServers["agentskills"]).toEqual({
|
|
131
|
+
command: "npx",
|
|
132
|
+
args: ["-y", "@codemcp/skills-server"]
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("writes inline SKILL.md files", async () => {
|
|
137
|
+
const config: LogicalConfig = {
|
|
138
|
+
mcp_servers: [],
|
|
139
|
+
instructions: [],
|
|
140
|
+
cli_actions: [],
|
|
141
|
+
knowledge_sources: [],
|
|
142
|
+
skills: [
|
|
143
|
+
{
|
|
144
|
+
name: "tanstack-architecture",
|
|
145
|
+
description: "TanStack architecture conventions",
|
|
146
|
+
body: "# Architecture\n\nUse file-based routing."
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
git_hooks: [],
|
|
150
|
+
setup_notes: []
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
await claudeCodeWriter.install(config, dir);
|
|
154
|
+
|
|
155
|
+
const skillMd = await readFile(
|
|
156
|
+
join(dir, ".ade", "skills", "tanstack-architecture", "SKILL.md"),
|
|
157
|
+
"utf-8"
|
|
158
|
+
);
|
|
159
|
+
expect(skillMd).toContain("name: tanstack-architecture");
|
|
160
|
+
expect(skillMd).toContain("# Architecture");
|
|
161
|
+
});
|
|
162
|
+
});
|