generateAgents.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. "use strict";
  2. var __create = Object.create;
  3. var __defProp = Object.defineProperty;
  4. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  5. var __getOwnPropNames = Object.getOwnPropertyNames;
  6. var __getProtoOf = Object.getPrototypeOf;
  7. var __hasOwnProp = Object.prototype.hasOwnProperty;
  8. var __export = (target, all) => {
  9. for (var name in all)
  10. __defProp(target, name, { get: all[name], enumerable: true });
  11. };
  12. var __copyProps = (to, from, except, desc) => {
  13. if (from && typeof from === "object" || typeof from === "function") {
  14. for (let key of __getOwnPropNames(from))
  15. if (!__hasOwnProp.call(to, key) && key !== except)
  16. __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  17. }
  18. return to;
  19. };
  20. var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  21. // If the importer is in node compatibility mode or this is not an ESM
  22. // file that has been converted to a CommonJS file using a Babel-
  23. // compatible transform (i.e. "__esModule" has not been set), then set
  24. // "default" to the CommonJS "module.exports" for node compatibility.
  25. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  26. mod
  27. ));
  28. var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
  29. var generateAgents_exports = {};
  30. __export(generateAgents_exports, {
  31. ClaudeGenerator: () => ClaudeGenerator,
  32. CopilotGenerator: () => CopilotGenerator,
  33. OpencodeGenerator: () => OpencodeGenerator,
  34. VSCodeGenerator: () => VSCodeGenerator
  35. });
  36. module.exports = __toCommonJS(generateAgents_exports);
  37. var import_fs = __toESM(require("fs"));
  38. var import_path = __toESM(require("path"));
  39. var import_utilsBundle = require("playwright-core/lib/utilsBundle");
  40. var import_utils = require("playwright-core/lib/utils");
  41. var import_seed = require("../mcp/test/seed");
  42. class AgentParser {
  43. static async loadAgents() {
  44. const files = await import_fs.default.promises.readdir(__dirname);
  45. return Promise.all(files.filter((file) => file.endsWith(".agent.md")).map((file) => AgentParser.parseFile(import_path.default.join(__dirname, file))));
  46. }
  47. static async parseFile(filePath) {
  48. const source = await import_fs.default.promises.readFile(filePath, "utf-8");
  49. const { header, content } = this.extractYamlAndContent(source);
  50. const { instructions, examples } = this.extractInstructionsAndExamples(content);
  51. return { header, instructions, examples };
  52. }
  53. static extractYamlAndContent(markdown) {
  54. const lines = markdown.split("\n");
  55. if (lines[0] !== "---")
  56. throw new Error("Markdown file must start with YAML front matter (---)");
  57. let yamlEndIndex = -1;
  58. for (let i = 1; i < lines.length; i++) {
  59. if (lines[i] === "---") {
  60. yamlEndIndex = i;
  61. break;
  62. }
  63. }
  64. if (yamlEndIndex === -1)
  65. throw new Error("YAML front matter must be closed with ---");
  66. const yamlLines = lines.slice(1, yamlEndIndex);
  67. const yamlRaw = yamlLines.join("\n");
  68. const contentLines = lines.slice(yamlEndIndex + 1);
  69. const content = contentLines.join("\n");
  70. let header;
  71. try {
  72. header = import_utilsBundle.yaml.parse(yamlRaw);
  73. } catch (error) {
  74. throw new Error(`Failed to parse YAML header: ${error.message}`);
  75. }
  76. if (!header.name)
  77. throw new Error('YAML header must contain a "name" field');
  78. if (!header.description)
  79. throw new Error('YAML header must contain a "description" field');
  80. return { header, content };
  81. }
  82. static extractInstructionsAndExamples(content) {
  83. const examples = [];
  84. const instructions = content.split("<example>")[0].trim();
  85. const exampleRegex = /<example>([\s\S]*?)<\/example>/g;
  86. let match;
  87. while ((match = exampleRegex.exec(content)) !== null) {
  88. const example = match[1].trim();
  89. examples.push(example.replace(/[\n]/g, " ").replace(/ +/g, " "));
  90. }
  91. return { instructions, examples };
  92. }
  93. }
  94. class ClaudeGenerator {
  95. static async init(config, projectName, prompts) {
  96. await initRepo(config, projectName, {
  97. promptsFolder: prompts ? ".claude/prompts" : void 0
  98. });
  99. const agents = await AgentParser.loadAgents();
  100. await import_fs.default.promises.mkdir(".claude/agents", { recursive: true });
  101. for (const agent of agents)
  102. await writeFile(`.claude/agents/${agent.header.name}.md`, ClaudeGenerator.agentSpec(agent), "\u{1F916}", "agent definition");
  103. await writeFile(".mcp.json", JSON.stringify({
  104. mcpServers: {
  105. "playwright-test": {
  106. command: "npx",
  107. args: ["playwright", "run-test-mcp-server"]
  108. }
  109. }
  110. }, null, 2), "\u{1F527}", "mcp configuration");
  111. initRepoDone();
  112. }
  113. static agentSpec(agent) {
  114. const claudeToolMap = /* @__PURE__ */ new Map([
  115. ["search", ["Glob", "Grep", "Read", "LS"]],
  116. ["edit", ["Edit", "MultiEdit", "Write"]]
  117. ]);
  118. function asClaudeTool(tool) {
  119. const [first, second] = tool.split("/");
  120. if (!second)
  121. return (claudeToolMap.get(first) || [first]).join(", ");
  122. return `mcp__${first}__${second}`;
  123. }
  124. const examples = agent.examples.length ? ` Examples: ${agent.examples.map((example) => `<example>${example}</example>`).join("")}` : "";
  125. const lines = [];
  126. const header = {
  127. name: agent.header.name,
  128. description: agent.header.description + examples,
  129. tools: agent.header.tools.map((tool) => asClaudeTool(tool)).join(", "),
  130. model: agent.header.model,
  131. color: agent.header.color
  132. };
  133. lines.push(`---`);
  134. lines.push(import_utilsBundle.yaml.stringify(header, { lineWidth: 1e5 }) + `---`);
  135. lines.push("");
  136. lines.push(agent.instructions);
  137. return lines.join("\n");
  138. }
  139. }
  140. class OpencodeGenerator {
  141. static async init(config, projectName, prompts) {
  142. await initRepo(config, projectName, {
  143. defaultAgentName: "Build",
  144. promptsFolder: prompts ? ".opencode/prompts" : void 0
  145. });
  146. const agents = await AgentParser.loadAgents();
  147. for (const agent of agents) {
  148. const prompt = [agent.instructions];
  149. prompt.push("");
  150. prompt.push(...agent.examples.map((example) => `<example>${example}</example>`));
  151. await writeFile(`.opencode/prompts/${agent.header.name}.md`, prompt.join("\n"), "\u{1F916}", "agent definition");
  152. }
  153. await writeFile("opencode.json", OpencodeGenerator.configuration(agents), "\u{1F527}", "opencode configuration");
  154. initRepoDone();
  155. }
  156. static configuration(agents) {
  157. const opencodeToolMap = /* @__PURE__ */ new Map([
  158. ["search", ["ls", "glob", "grep", "read"]],
  159. ["edit", ["edit", "write"]]
  160. ]);
  161. const asOpencodeTool = (tools, tool) => {
  162. const [first, second] = tool.split("/");
  163. if (!second) {
  164. for (const tool2 of opencodeToolMap.get(first) || [first])
  165. tools[tool2] = true;
  166. } else {
  167. tools[`${first}*${second}`] = true;
  168. }
  169. };
  170. const result = {};
  171. result["$schema"] = "https://opencode.ai/config.json";
  172. result["mcp"] = {};
  173. result["tools"] = {
  174. "playwright*": false
  175. };
  176. result["agent"] = {};
  177. for (const agent of agents) {
  178. const tools = {};
  179. result["agent"][agent.header.name] = {
  180. description: agent.header.description,
  181. mode: "subagent",
  182. prompt: `{file:.opencode/prompts/${agent.header.name}.md}`,
  183. tools
  184. };
  185. for (const tool of agent.header.tools)
  186. asOpencodeTool(tools, tool);
  187. }
  188. result["mcp"]["playwright-test"] = {
  189. type: "local",
  190. command: ["npx", "playwright", "run-test-mcp-server"],
  191. enabled: true
  192. };
  193. return JSON.stringify(result, null, 2);
  194. }
  195. }
  196. class CopilotGenerator {
  197. static async init(config, projectName, prompts) {
  198. await initRepo(config, projectName, {
  199. defaultAgentName: "agent",
  200. promptsFolder: prompts ? ".github/prompts" : void 0,
  201. promptSuffix: "prompt"
  202. });
  203. const agents = await AgentParser.loadAgents();
  204. await import_fs.default.promises.mkdir(".github/agents", { recursive: true });
  205. for (const agent of agents)
  206. await writeFile(`.github/agents/${agent.header.name}.agent.md`, CopilotGenerator.agentSpec(agent), "\u{1F916}", "agent definition");
  207. await deleteFile(`.github/chatmodes/ \u{1F3AD} planner.chatmode.md`, "legacy planner chatmode");
  208. await deleteFile(`.github/chatmodes/\u{1F3AD} generator.chatmode.md`, "legacy generator chatmode");
  209. await deleteFile(`.github/chatmodes/\u{1F3AD} healer.chatmode.md`, "legacy healer chatmode");
  210. await deleteFile(`.github/agents/ \u{1F3AD} planner.agent.md`, "legacy planner agent");
  211. await deleteFile(`.github/agents/\u{1F3AD} generator.agent.md`, "legacy generator agent");
  212. await deleteFile(`.github/agents/\u{1F3AD} healer.agent.md`, "legacy healer agent");
  213. await VSCodeGenerator.appendToMCPJson();
  214. const mcpConfig = { mcpServers: CopilotGenerator.mcpServers };
  215. if (!import_fs.default.existsSync(".github/copilot-setup-steps.yml")) {
  216. const yaml2 = import_fs.default.readFileSync(import_path.default.join(__dirname, "copilot-setup-steps.yml"), "utf-8");
  217. await writeFile(".github/workflows/copilot-setup-steps.yml", yaml2, "\u{1F527}", "GitHub Copilot setup steps");
  218. }
  219. console.log("");
  220. console.log("");
  221. console.log(" \u{1F527} TODO: GitHub > Settings > Copilot > Coding agent > MCP configuration");
  222. console.log("------------------------------------------------------------------");
  223. console.log(JSON.stringify(mcpConfig, null, 2));
  224. console.log("------------------------------------------------------------------");
  225. initRepoDone();
  226. }
  227. static agentSpec(agent) {
  228. const examples = agent.examples.length ? ` Examples: ${agent.examples.map((example) => `<example>${example}</example>`).join("")}` : "";
  229. const lines = [];
  230. const header = {
  231. "name": agent.header.name,
  232. "description": agent.header.description + examples,
  233. "tools": agent.header.tools,
  234. "model": "Claude Sonnet 4",
  235. "mcp-servers": CopilotGenerator.mcpServers
  236. };
  237. lines.push(`---`);
  238. lines.push(import_utilsBundle.yaml.stringify(header) + `---`);
  239. lines.push("");
  240. lines.push(agent.instructions);
  241. lines.push("");
  242. return lines.join("\n");
  243. }
  244. static {
  245. this.mcpServers = {
  246. "playwright-test": {
  247. "type": "stdio",
  248. "command": "npx",
  249. "args": [
  250. "playwright",
  251. "run-test-mcp-server"
  252. ],
  253. "tools": ["*"]
  254. }
  255. };
  256. }
  257. }
  258. class VSCodeGenerator {
  259. static async init(config, projectName) {
  260. await initRepo(config, projectName, {
  261. promptsFolder: void 0
  262. });
  263. const agents = await AgentParser.loadAgents();
  264. const nameMap = /* @__PURE__ */ new Map([
  265. ["playwright-test-planner", " \u{1F3AD} planner"],
  266. ["playwright-test-generator", "\u{1F3AD} generator"],
  267. ["playwright-test-healer", "\u{1F3AD} healer"]
  268. ]);
  269. await import_fs.default.promises.mkdir(".github/chatmodes", { recursive: true });
  270. for (const agent of agents)
  271. await writeFile(`.github/chatmodes/${nameMap.get(agent.header.name)}.chatmode.md`, VSCodeGenerator.agentSpec(agent), "\u{1F916}", "chatmode definition");
  272. await VSCodeGenerator.appendToMCPJson();
  273. initRepoDone();
  274. }
  275. static async appendToMCPJson() {
  276. await import_fs.default.promises.mkdir(".vscode", { recursive: true });
  277. const mcpJsonPath = ".vscode/mcp.json";
  278. let mcpJson = {
  279. servers: {},
  280. inputs: []
  281. };
  282. try {
  283. mcpJson = JSON.parse(import_fs.default.readFileSync(mcpJsonPath, "utf8"));
  284. } catch {
  285. }
  286. if (!mcpJson.servers)
  287. mcpJson.servers = {};
  288. mcpJson.servers["playwright-test"] = {
  289. type: "stdio",
  290. command: "npx",
  291. args: ["playwright", "run-test-mcp-server"]
  292. };
  293. await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2), "\u{1F527}", "mcp configuration");
  294. }
  295. static agentSpec(agent) {
  296. const vscodeToolMap = /* @__PURE__ */ new Map([
  297. ["search", ["search/listDirectory", "search/fileSearch", "search/textSearch"]],
  298. ["read", ["search/readFile"]],
  299. ["edit", ["edit/editFiles"]],
  300. ["write", ["edit/createFile", "edit/createDirectory"]]
  301. ]);
  302. const vscodeToolsOrder = ["edit/createFile", "edit/createDirectory", "edit/editFiles", "search/fileSearch", "search/textSearch", "search/listDirectory", "search/readFile"];
  303. const vscodeMcpName = "playwright-test";
  304. function asVscodeTool(tool) {
  305. const [first, second] = tool.split("/");
  306. if (second)
  307. return `${vscodeMcpName}/${second}`;
  308. return vscodeToolMap.get(first) || first;
  309. }
  310. const tools = agent.header.tools.map(asVscodeTool).flat().sort((a, b) => {
  311. const indexA = vscodeToolsOrder.indexOf(a);
  312. const indexB = vscodeToolsOrder.indexOf(b);
  313. if (indexA === -1 && indexB === -1)
  314. return a.localeCompare(b);
  315. if (indexA === -1)
  316. return 1;
  317. if (indexB === -1)
  318. return -1;
  319. return indexA - indexB;
  320. }).map((tool) => `'${tool}'`).join(", ");
  321. const lines = [];
  322. lines.push(`---`);
  323. lines.push(`description: ${agent.header.description}.`);
  324. lines.push(`tools: [${tools}]`);
  325. lines.push(`---`);
  326. lines.push("");
  327. lines.push(agent.instructions);
  328. for (const example of agent.examples)
  329. lines.push(`<example>${example}</example>`);
  330. lines.push("");
  331. return lines.join("\n");
  332. }
  333. }
  334. async function writeFile(filePath, content, icon, description) {
  335. console.log(` ${icon} ${import_path.default.relative(process.cwd(), filePath)} ${import_utilsBundle.colors.dim("- " + description)}`);
  336. await (0, import_utils.mkdirIfNeeded)(filePath);
  337. await import_fs.default.promises.writeFile(filePath, content, "utf-8");
  338. }
  339. async function deleteFile(filePath, description) {
  340. try {
  341. if (!import_fs.default.existsSync(filePath))
  342. return;
  343. } catch {
  344. return;
  345. }
  346. console.log(` \u2702\uFE0F ${import_path.default.relative(process.cwd(), filePath)} ${import_utilsBundle.colors.dim("- " + description)}`);
  347. await import_fs.default.promises.unlink(filePath);
  348. }
  349. async function initRepo(config, projectName, options) {
  350. const project = (0, import_seed.seedProject)(config, projectName);
  351. console.log(` \u{1F3AD} Using project "${project.project.name}" as a primary project`);
  352. if (!import_fs.default.existsSync("specs")) {
  353. await import_fs.default.promises.mkdir("specs");
  354. await writeFile(import_path.default.join("specs", "README.md"), `# Specs
  355. This is a directory for test plans.
  356. `, "\u{1F4DD}", "directory for test plans");
  357. }
  358. let seedFile = await (0, import_seed.findSeedFile)(project);
  359. if (!seedFile) {
  360. seedFile = (0, import_seed.defaultSeedFile)(project);
  361. await writeFile(seedFile, import_seed.seedFileContent, "\u{1F331}", "default environment seed file");
  362. }
  363. if (options.promptsFolder) {
  364. await import_fs.default.promises.mkdir(options.promptsFolder, { recursive: true });
  365. for (const promptFile of await import_fs.default.promises.readdir(__dirname)) {
  366. if (!promptFile.endsWith(".prompt.md"))
  367. continue;
  368. const shortName = promptFile.replace(".prompt.md", "");
  369. const fileName = options.promptSuffix ? `${shortName}.${options.promptSuffix}.md` : `${shortName}.md`;
  370. const content = await loadPrompt(promptFile, {
  371. defaultAgentName: "default",
  372. ...options,
  373. seedFile: import_path.default.relative(process.cwd(), seedFile)
  374. });
  375. await writeFile(import_path.default.join(options.promptsFolder, fileName), content, "\u{1F4DD}", "prompt template");
  376. }
  377. }
  378. }
  379. function initRepoDone() {
  380. console.log(" \u2705 Done.");
  381. }
  382. async function loadPrompt(file, params) {
  383. const content = await import_fs.default.promises.readFile(import_path.default.join(__dirname, file), "utf-8");
  384. return Object.entries(params).reduce((acc, [key, value]) => {
  385. return acc.replace(new RegExp(`\\\${${key}}`, "g"), value);
  386. }, content);
  387. }
  388. // Annotate the CommonJS export names for ESM import in node:
  389. 0 && (module.exports = {
  390. ClaudeGenerator,
  391. CopilotGenerator,
  392. OpencodeGenerator,
  393. VSCodeGenerator
  394. });