@clubmatto/ai-kit 0.0.1

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +65 -0
  3. package/dist/scripts/fetch-playwright-skills.js +63 -0
  4. package/dist/src/cmd/sync.js +109 -0
  5. package/dist/src/commands/sync.js +111 -0
  6. package/dist/src/content.js +99 -0
  7. package/dist/src/index.js +19 -0
  8. package/dist/src/logger.js +2 -0
  9. package/dist/src/manifest.js +24 -0
  10. package/dist/src/output.js +46 -0
  11. package/dist/src/reader.js +99 -0
  12. package/dist/src/template.js +10 -0
  13. package/dist/tests/content.test.js +141 -0
  14. package/dist/tests/integration/cli.test.js +43 -0
  15. package/dist/tests/output.js +36 -0
  16. package/dist/tests/reader.test.js +141 -0
  17. package/dist/tests/sync.test.js +90 -0
  18. package/dist/tests/utils.js +20 -0
  19. package/dist/vitest.config.js +9 -0
  20. package/docs/roadmap.md +16 -0
  21. package/eslint.config.mjs +38 -0
  22. package/package.json +78 -0
  23. package/scripts/fetch-playwright-skills.ts +79 -0
  24. package/src/agents/monorepo.md +30 -0
  25. package/src/agents/opencode.json +31 -0
  26. package/src/cmd/sync.ts +158 -0
  27. package/src/commands/commit.md +43 -0
  28. package/src/commands/interview.md +92 -0
  29. package/src/commands/synth.md +45 -0
  30. package/src/index.ts +24 -0
  31. package/src/logger.ts +10 -0
  32. package/src/manifest.ts +29 -0
  33. package/src/output.ts +66 -0
  34. package/src/reader.ts +114 -0
  35. package/src/rules/go.md +306 -0
  36. package/src/rules/kotlin.md +177 -0
  37. package/src/rules/plan-mode.md +7 -0
  38. package/src/rules/spring-boot.md +549 -0
  39. package/src/rules/typescript.md +302 -0
  40. package/src/rules/unsure.md +9 -0
  41. package/src/skills/image-gen/SKILL.md +50 -0
  42. package/src/skills/image-gen/scripts/generate.js +166 -0
  43. package/src/skills/playwright-cli/SKILL.md +279 -0
  44. package/src/skills/playwright-cli/references/request-mocking.md +87 -0
  45. package/src/skills/playwright-cli/references/running-code.md +232 -0
  46. package/src/skills/playwright-cli/references/session-management.md +170 -0
  47. package/src/skills/playwright-cli/references/storage-state.md +275 -0
  48. package/src/skills/playwright-cli/references/test-generation.md +88 -0
  49. package/src/skills/playwright-cli/references/tracing.md +142 -0
  50. package/src/skills/playwright-cli/references/video-recording.md +43 -0
  51. package/src/template.ts +14 -0
  52. package/tests/fixtures/agents/another.json +4 -0
  53. package/tests/fixtures/agents/monorepo.md +5 -0
  54. package/tests/fixtures/agents/opencode.json +4 -0
  55. package/tests/fixtures/commands/another.md +5 -0
  56. package/tests/fixtures/commands/commit.md +7 -0
  57. package/tests/fixtures/commands/test.md +13 -0
  58. package/tests/fixtures/rules/nested/nested-rule.md +3 -0
  59. package/tests/fixtures/rules/test-rule.md +5 -0
  60. package/tests/fixtures/rules/typescript.md +5 -0
  61. package/tests/fixtures/skills/test-skill/SKILL.md +7 -0
  62. package/tests/fixtures/skills/test-skill/nested-refs/doc.md +3 -0
  63. package/tests/fixtures/skills/test-skill/skill-details.md +7 -0
  64. package/tests/integration/cli.test.ts +55 -0
  65. package/tests/output.ts +37 -0
  66. package/tests/reader.test.ts +193 -0
  67. package/tests/sync.test.ts +136 -0
  68. package/tests/utils.ts +17 -0
  69. package/tsconfig.json +23 -0
  70. package/vitest.config.ts +8 -0
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const path_1 = require("path");
5
+ const content_1 = require("../src/content");
6
+ const template_1 = require("../src/template");
7
+ const fixturesDir = (0, path_1.join)(__dirname, "fixtures");
8
+ const commandsDir = (0, path_1.join)(fixturesDir, "commands");
9
+ const rulesDir = (0, path_1.join)(fixturesDir, "rules");
10
+ const skillsDir = (0, path_1.join)(fixturesDir, "skills");
11
+ const agentsDir = (0, path_1.join)(fixturesDir, "agents");
12
+ (0, vitest_1.describe)("getCommandConfig", () => {
13
+ (0, vitest_1.it)("parses command files and returns config object", () => {
14
+ const config = (0, content_1.getCommandConfig)(commandsDir);
15
+ (0, vitest_1.expect)(config).toHaveProperty("test");
16
+ (0, vitest_1.expect)(config.test.description).toBe("A test command for testing purposes.");
17
+ (0, vitest_1.expect)(config.test.template).toContain("This is a test command template.");
18
+ });
19
+ (0, vitest_1.it)("includes all command files", () => {
20
+ const config = (0, content_1.getCommandConfig)(commandsDir);
21
+ (0, vitest_1.expect)(config).toHaveProperty("test");
22
+ (0, vitest_1.expect)(config).toHaveProperty("another");
23
+ });
24
+ (0, vitest_1.it)("has correct structure for opencode.json command format", () => {
25
+ const config = (0, content_1.getCommandConfig)(commandsDir);
26
+ for (const [_, cmd] of Object.entries(config)) {
27
+ (0, vitest_1.expect)(cmd).toHaveProperty("description");
28
+ (0, vitest_1.expect)(cmd).toHaveProperty("template");
29
+ (0, vitest_1.expect)(typeof cmd.description).toBe("string");
30
+ (0, vitest_1.expect)(typeof cmd.template).toBe("string");
31
+ }
32
+ });
33
+ (0, vitest_1.it)("returns empty object for non-existent directory", () => {
34
+ const config = (0, content_1.getCommandConfig)("/non/existent/dir");
35
+ (0, vitest_1.expect)(config).toEqual({});
36
+ });
37
+ });
38
+ (0, vitest_1.describe)("getContentFiles", () => {
39
+ (0, vitest_1.it)("returns files from rules and skills directories", () => {
40
+ const files = (0, content_1.getContentFiles)(rulesDir, skillsDir);
41
+ const ruleFiles = files.filter((f) => f.type === "rules");
42
+ const skillFiles = files.filter((f) => f.type === "skills");
43
+ (0, vitest_1.expect)(ruleFiles.length).toBeGreaterThan(0);
44
+ (0, vitest_1.expect)(skillFiles.length).toBeGreaterThan(0);
45
+ });
46
+ (0, vitest_1.it)("includes nested files recursively", () => {
47
+ const files = (0, content_1.getContentFiles)(rulesDir, skillsDir);
48
+ const nestedRule = files.find((f) => f.name === "nested/nested-rule.md" && f.type === "rules");
49
+ (0, vitest_1.expect)(nestedRule).toBeDefined();
50
+ (0, vitest_1.expect)(nestedRule?.content).toContain("Nested Rule");
51
+ });
52
+ (0, vitest_1.it)("returns correct file structure", () => {
53
+ const files = (0, content_1.getContentFiles)(rulesDir, skillsDir);
54
+ for (const file of files) {
55
+ (0, vitest_1.expect)(file).toHaveProperty("type");
56
+ (0, vitest_1.expect)(file).toHaveProperty("name");
57
+ (0, vitest_1.expect)(file).toHaveProperty("content");
58
+ (0, vitest_1.expect)(["rules", "skills"]).toContain(file.type);
59
+ (0, vitest_1.expect)(file.name.endsWith(".md")).toBe(true);
60
+ }
61
+ });
62
+ (0, vitest_1.it)("includes skill directory name in file path", () => {
63
+ const files = (0, content_1.getContentFiles)(rulesDir, skillsDir);
64
+ const skillFile = files.find((f) => f.type === "skills" && f.name === "test-skill/SKILL.md");
65
+ (0, vitest_1.expect)(skillFile).toBeDefined();
66
+ (0, vitest_1.expect)(skillFile?.content).toContain("Test Skill");
67
+ });
68
+ (0, vitest_1.it)("includes additional files in skill directories", () => {
69
+ const files = (0, content_1.getContentFiles)(rulesDir, skillsDir);
70
+ const detailsFile = files.find((f) => f.type === "skills" && f.name === "test-skill/skill-details.md");
71
+ (0, vitest_1.expect)(detailsFile).toBeDefined();
72
+ (0, vitest_1.expect)(detailsFile?.content).toContain("Skill Details");
73
+ });
74
+ (0, vitest_1.it)("includes nested references in skill directories", () => {
75
+ const files = (0, content_1.getContentFiles)(rulesDir, skillsDir);
76
+ const refFile = files.find((f) => f.type === "skills" && f.name === "test-skill/nested-refs/doc.md");
77
+ (0, vitest_1.expect)(refFile).toBeDefined();
78
+ });
79
+ });
80
+ (0, vitest_1.describe)("getRootFiles", () => {
81
+ (0, vitest_1.it)("returns JSON files from agents directory", () => {
82
+ const files = (0, content_1.getRootFiles)(agentsDir);
83
+ (0, vitest_1.expect)(files.length).toBe(2);
84
+ (0, vitest_1.expect)(files.map((f) => f.name).sort()).toEqual([
85
+ "another.json",
86
+ "opencode.json",
87
+ ]);
88
+ });
89
+ (0, vitest_1.it)("returns file content as string", () => {
90
+ const files = (0, content_1.getRootFiles)(agentsDir);
91
+ const opencodeFile = files.find((f) => f.name === "opencode.json");
92
+ (0, vitest_1.expect)(opencodeFile?.content).toContain("test-agent");
93
+ });
94
+ (0, vitest_1.it)("returns empty array for non-existent directory", () => {
95
+ const files = (0, content_1.getRootFiles)("/non/existent/dir");
96
+ (0, vitest_1.expect)(files).toEqual([]);
97
+ });
98
+ });
99
+ (0, vitest_1.describe)("getAgentsFile", () => {
100
+ (0, vitest_1.it)("returns AGENTS.md file content", () => {
101
+ const file = (0, content_1.getAgentsFile)(agentsDir);
102
+ (0, vitest_1.expect)(file).not.toBeNull();
103
+ (0, vitest_1.expect)(file?.name).toBe("AGENTS.md");
104
+ (0, vitest_1.expect)(file?.content).toContain("AGENTS.md");
105
+ });
106
+ (0, vitest_1.it)("returns null when monorepo.md does not exist", () => {
107
+ const file = (0, content_1.getAgentsFile)(rulesDir);
108
+ (0, vitest_1.expect)(file).toBeNull();
109
+ });
110
+ });
111
+ (0, vitest_1.describe)("processTemplate", () => {
112
+ (0, vitest_1.it)("replaces {{FOOTER}} with full footer text and ISO date", () => {
113
+ const result = (0, template_1.processTemplate)("{{FOOTER}}");
114
+ const today = new Date().toISOString().split("T")[0];
115
+ (0, vitest_1.expect)(result).toBe(`Last updated: ${today}. This file extends the global rules in @AGENTS.md. Always check both files.`);
116
+ });
117
+ (0, vitest_1.it)("replaces {{AGENTS_FOOTER}} with agents footer text and ISO date", () => {
118
+ const result = (0, template_1.processTemplate)("{{AGENTS_FOOTER}}");
119
+ const today = new Date().toISOString().split("T")[0];
120
+ (0, vitest_1.expect)(result).toBe(`This file was last updated: ${today}. Always check the \`.ai/rules/\` directory for the most current language-specific guidelines.`);
121
+ });
122
+ (0, vitest_1.it)("replaces multiple placeholders in same content", () => {
123
+ const result = (0, template_1.processTemplate)("{{FOOTER}} and {{AGENTS_FOOTER}}");
124
+ const today = new Date().toISOString().split("T")[0];
125
+ (0, vitest_1.expect)(result).toContain(`Last updated: ${today}`);
126
+ (0, vitest_1.expect)(result).toContain(`This file was last updated: ${today}`);
127
+ });
128
+ (0, vitest_1.it)("leaves non-matching content unchanged", () => {
129
+ const input = "Some {{OTHER}} content {{NOTREAL}}";
130
+ const result = (0, template_1.processTemplate)(input);
131
+ (0, vitest_1.expect)(result).toBe(input);
132
+ });
133
+ (0, vitest_1.it)("handles empty string", () => {
134
+ const result = (0, template_1.processTemplate)("");
135
+ (0, vitest_1.expect)(result).toBe("");
136
+ });
137
+ (0, vitest_1.it)("handles content without placeholders", () => {
138
+ const result = (0, template_1.processTemplate)("No placeholders here");
139
+ (0, vitest_1.expect)(result).toBe("No placeholders here");
140
+ });
141
+ });
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const child_process_1 = require("child_process");
5
+ const path_1 = require("path");
6
+ const utils_1 = require("../utils");
7
+ const projectRoot = (0, path_1.join)(__dirname, "..", "..");
8
+ (0, vitest_1.describe)("CLI integration", () => {
9
+ let tempDir;
10
+ (0, vitest_1.beforeEach)(() => {
11
+ tempDir = (0, utils_1.createTempDir)();
12
+ });
13
+ (0, vitest_1.it)("runs as CLI and syncs files", () => {
14
+ (0, child_process_1.execSync)(`node ${(0, path_1.join)(projectRoot, "dist", "src", "index.js")} sync`, {
15
+ cwd: tempDir,
16
+ });
17
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, ".agents/.ai-kit")).toBe(true);
18
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, ".agents/rules")).toBe(true);
19
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, "opencode.json")).toBe(true);
20
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, "AGENTS.md")).toBe(true);
21
+ });
22
+ (0, vitest_1.it)("creates rules with templated content", () => {
23
+ (0, child_process_1.execSync)(`node ${(0, path_1.join)(projectRoot, "dist", "src", "index.js")} sync`, {
24
+ cwd: tempDir,
25
+ });
26
+ const ruleContent = (0, utils_1.readFile)(tempDir, ".agents/rules/typescript.md");
27
+ (0, vitest_1.expect)(ruleContent).toContain("Last updated:");
28
+ (0, vitest_1.expect)(ruleContent).not.toContain("{{FOOTER}}");
29
+ (0, vitest_1.expect)(ruleContent).toMatch(/\d{4}-\d{2}-\d{2}/);
30
+ });
31
+ (0, vitest_1.it)("injects commands into opencode.json", () => {
32
+ (0, child_process_1.execSync)(`node ${(0, path_1.join)(projectRoot, "dist", "src", "index.js")} sync`, {
33
+ cwd: tempDir,
34
+ });
35
+ const opencodeJson = JSON.parse((0, utils_1.readFile)(tempDir, "opencode.json"));
36
+ (0, vitest_1.expect)(opencodeJson).toHaveProperty("command");
37
+ });
38
+ (0, vitest_1.it)("skips opencode.json when --skip-opencode is passed", () => {
39
+ (0, child_process_1.execSync)(`node ${(0, path_1.join)(projectRoot, "dist", "src", "index.js")} sync --skip-opencode`, { cwd: tempDir });
40
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, "opencode.json")).toBe(false);
41
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, "AGENTS.md")).toBe(true);
42
+ });
43
+ });
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.testLog = void 0;
4
+ exports.getLastLog = getLastLog;
5
+ exports.findLogs = findLogs;
6
+ const logs = [];
7
+ exports.testLog = {
8
+ clear: () => {
9
+ logs.length = 0;
10
+ },
11
+ get: () => [...logs],
12
+ logo: (version) => {
13
+ logs.push(["logo", version]);
14
+ },
15
+ welcome: () => {
16
+ logs.push(["welcome", ""]);
17
+ },
18
+ section: (msg) => {
19
+ logs.push(["section", msg]);
20
+ },
21
+ success: (msg) => {
22
+ logs.push(["success", msg]);
23
+ },
24
+ final: (msg) => {
25
+ logs.push(["final", msg]);
26
+ },
27
+ summary: (counts) => {
28
+ logs.push(["summary", JSON.stringify(counts)]);
29
+ },
30
+ };
31
+ function getLastLog() {
32
+ return logs[logs.length - 1];
33
+ }
34
+ function findLogs(type) {
35
+ return logs.filter(([t]) => t === type);
36
+ }
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const path_1 = require("path");
5
+ const reader_1 = require("../src/reader");
6
+ const template_1 = require("../src/template");
7
+ const fixturesDir = (0, path_1.join)(__dirname, "fixtures");
8
+ const commandsDir = (0, path_1.join)(fixturesDir, "commands");
9
+ const rulesDir = (0, path_1.join)(fixturesDir, "rules");
10
+ const skillsDir = (0, path_1.join)(fixturesDir, "skills");
11
+ const agentsDir = (0, path_1.join)(fixturesDir, "agents");
12
+ (0, vitest_1.describe)("getCommandConfig", () => {
13
+ (0, vitest_1.it)("parses command files and returns config object", () => {
14
+ const config = (0, reader_1.getCommandConfig)(commandsDir);
15
+ (0, vitest_1.expect)(config).toHaveProperty("test");
16
+ (0, vitest_1.expect)(config.test.description).toBe("A test command for testing purposes.");
17
+ (0, vitest_1.expect)(config.test.template).toContain("This is a test command template.");
18
+ });
19
+ (0, vitest_1.it)("includes all command files", () => {
20
+ const config = (0, reader_1.getCommandConfig)(commandsDir);
21
+ (0, vitest_1.expect)(config).toHaveProperty("test");
22
+ (0, vitest_1.expect)(config).toHaveProperty("another");
23
+ });
24
+ (0, vitest_1.it)("has correct structure for opencode.json command format", () => {
25
+ const config = (0, reader_1.getCommandConfig)(commandsDir);
26
+ for (const [_, cmd] of Object.entries(config)) {
27
+ (0, vitest_1.expect)(cmd).toHaveProperty("description");
28
+ (0, vitest_1.expect)(cmd).toHaveProperty("template");
29
+ (0, vitest_1.expect)(typeof cmd.description).toBe("string");
30
+ (0, vitest_1.expect)(typeof cmd.template).toBe("string");
31
+ }
32
+ });
33
+ (0, vitest_1.it)("returns empty object for non-existent directory", () => {
34
+ const config = (0, reader_1.getCommandConfig)("/non/existent/dir");
35
+ (0, vitest_1.expect)(config).toEqual({});
36
+ });
37
+ });
38
+ (0, vitest_1.describe)("readContent", () => {
39
+ (0, vitest_1.it)("returns files from rules and skills directories", () => {
40
+ const files = (0, reader_1.readContent)(rulesDir, skillsDir);
41
+ const ruleFiles = files.filter((f) => f.type === "rules");
42
+ const skillFiles = files.filter((f) => f.type === "skills");
43
+ (0, vitest_1.expect)(ruleFiles.length).toBeGreaterThan(0);
44
+ (0, vitest_1.expect)(skillFiles.length).toBeGreaterThan(0);
45
+ });
46
+ (0, vitest_1.it)("includes nested files recursively", () => {
47
+ const files = (0, reader_1.readContent)(rulesDir, skillsDir);
48
+ const nestedRule = files.find((f) => f.name === "nested/nested-rule.md" && f.type === "rules");
49
+ (0, vitest_1.expect)(nestedRule).toBeDefined();
50
+ (0, vitest_1.expect)(nestedRule?.content).toContain("Nested Rule");
51
+ });
52
+ (0, vitest_1.it)("returns correct file structure", () => {
53
+ const files = (0, reader_1.readContent)(rulesDir, skillsDir);
54
+ for (const file of files) {
55
+ (0, vitest_1.expect)(file).toHaveProperty("type");
56
+ (0, vitest_1.expect)(file).toHaveProperty("name");
57
+ (0, vitest_1.expect)(file).toHaveProperty("content");
58
+ (0, vitest_1.expect)(["rules", "skills"]).toContain(file.type);
59
+ (0, vitest_1.expect)(file.name.endsWith(".md")).toBe(true);
60
+ }
61
+ });
62
+ (0, vitest_1.it)("includes skill directory name in file path", () => {
63
+ const files = (0, reader_1.readContent)(rulesDir, skillsDir);
64
+ const skillFile = files.find((f) => f.type === "skills" && f.name === "test-skill/SKILL.md");
65
+ (0, vitest_1.expect)(skillFile).toBeDefined();
66
+ (0, vitest_1.expect)(skillFile?.content).toContain("Test Skill");
67
+ });
68
+ (0, vitest_1.it)("includes additional files in skill directories", () => {
69
+ const files = (0, reader_1.readContent)(rulesDir, skillsDir);
70
+ const detailsFile = files.find((f) => f.type === "skills" && f.name === "test-skill/skill-details.md");
71
+ (0, vitest_1.expect)(detailsFile).toBeDefined();
72
+ (0, vitest_1.expect)(detailsFile?.content).toContain("Skill Details");
73
+ });
74
+ (0, vitest_1.it)("includes nested references in skill directories", () => {
75
+ const files = (0, reader_1.readContent)(rulesDir, skillsDir);
76
+ const refFile = files.find((f) => f.type === "skills" && f.name === "test-skill/nested-refs/doc.md");
77
+ (0, vitest_1.expect)(refFile).toBeDefined();
78
+ });
79
+ });
80
+ (0, vitest_1.describe)("readConfigs", () => {
81
+ (0, vitest_1.it)("returns JSON files from agents directory", () => {
82
+ const files = (0, reader_1.readConfigs)(agentsDir);
83
+ (0, vitest_1.expect)(files.length).toBe(2);
84
+ (0, vitest_1.expect)(files.map((f) => f.name).sort()).toEqual([
85
+ "another.json",
86
+ "opencode.json",
87
+ ]);
88
+ });
89
+ (0, vitest_1.it)("returns file content as string", () => {
90
+ const files = (0, reader_1.readConfigs)(agentsDir);
91
+ const opencodeFile = files.find((f) => f.name === "opencode.json");
92
+ (0, vitest_1.expect)(opencodeFile?.content).toContain("test-agent");
93
+ });
94
+ (0, vitest_1.it)("returns empty array for non-existent directory", () => {
95
+ const files = (0, reader_1.readConfigs)("/non/existent/dir");
96
+ (0, vitest_1.expect)(files).toEqual([]);
97
+ });
98
+ });
99
+ (0, vitest_1.describe)("readAgents", () => {
100
+ (0, vitest_1.it)("returns AGENTS.md file content", () => {
101
+ const file = (0, reader_1.readAgents)(agentsDir);
102
+ (0, vitest_1.expect)(file).not.toBeNull();
103
+ (0, vitest_1.expect)(file?.name).toBe("AGENTS.md");
104
+ (0, vitest_1.expect)(file?.content).toContain("AGENTS.md");
105
+ });
106
+ (0, vitest_1.it)("returns null when monorepo.md does not exist", () => {
107
+ const file = (0, reader_1.readAgents)(rulesDir);
108
+ (0, vitest_1.expect)(file).toBeNull();
109
+ });
110
+ });
111
+ (0, vitest_1.describe)("processTemplate", () => {
112
+ (0, vitest_1.it)("replaces {{FOOTER}} with full footer text and ISO date", () => {
113
+ const result = (0, template_1.processTemplate)("{{FOOTER}}");
114
+ const today = new Date().toISOString().split("T")[0];
115
+ (0, vitest_1.expect)(result).toBe(`Last updated: ${today}. This file extends the global rules in @AGENTS.md. Always check both files.`);
116
+ });
117
+ (0, vitest_1.it)("replaces {{AGENTS_FOOTER}} with agents footer text and ISO date", () => {
118
+ const result = (0, template_1.processTemplate)("{{AGENTS_FOOTER}}");
119
+ const today = new Date().toISOString().split("T")[0];
120
+ (0, vitest_1.expect)(result).toBe(`This file was last updated: ${today}. Always check the \`.ai/rules/\` directory for the most current language-specific guidelines.`);
121
+ });
122
+ (0, vitest_1.it)("replaces multiple placeholders in same content", () => {
123
+ const result = (0, template_1.processTemplate)("{{FOOTER}} and {{AGENTS_FOOTER}}");
124
+ const today = new Date().toISOString().split("T")[0];
125
+ (0, vitest_1.expect)(result).toContain(`Last updated: ${today}`);
126
+ (0, vitest_1.expect)(result).toContain(`This file was last updated: ${today}`);
127
+ });
128
+ (0, vitest_1.it)("leaves non-matching content unchanged", () => {
129
+ const input = "Some {{OTHER}} content {{NOTREAL}}";
130
+ const result = (0, template_1.processTemplate)(input);
131
+ (0, vitest_1.expect)(result).toBe(input);
132
+ });
133
+ (0, vitest_1.it)("handles empty string", () => {
134
+ const result = (0, template_1.processTemplate)("");
135
+ (0, vitest_1.expect)(result).toBe("");
136
+ });
137
+ (0, vitest_1.it)("handles content without placeholders", () => {
138
+ const result = (0, template_1.processTemplate)("No placeholders here");
139
+ (0, vitest_1.expect)(result).toBe("No placeholders here");
140
+ });
141
+ });
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const sync_1 = require("../src/cmd/sync");
5
+ const utils_1 = require("./utils");
6
+ const output_1 = require("./output");
7
+ const path_1 = require("path");
8
+ const fixturesDir = (0, path_1.join)(__dirname, "fixtures");
9
+ const testSourceDirs = {
10
+ rules: (0, path_1.join)(fixturesDir, "rules"),
11
+ skills: (0, path_1.join)(fixturesDir, "skills"),
12
+ agents: (0, path_1.join)(fixturesDir, "agents"),
13
+ commands: (0, path_1.join)(fixturesDir, "commands"),
14
+ };
15
+ (0, vitest_1.describe)("sync command", () => {
16
+ let tempDir;
17
+ (0, vitest_1.beforeEach)(() => {
18
+ tempDir = (0, utils_1.createTempDir)();
19
+ output_1.testLog.clear();
20
+ });
21
+ (0, vitest_1.it)("initializes when not already initialized", async () => {
22
+ await (0, sync_1.sync)(tempDir, "0.0.1", {}, output_1.testLog, testSourceDirs);
23
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, ".agents/.ai-kit")).toBe(true);
24
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, ".agents/rules")).toBe(true);
25
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, "opencode.json")).toBe(true);
26
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, "AGENTS.md")).toBe(true);
27
+ });
28
+ (0, vitest_1.it)("skips opencode.json when skipOpencode is true", async () => {
29
+ await (0, sync_1.sync)(tempDir, "0.0.1", { skipOpencode: true }, output_1.testLog, testSourceDirs);
30
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, "opencode.json")).toBe(false);
31
+ (0, vitest_1.expect)((0, utils_1.fileExists)(tempDir, "AGENTS.md")).toBe(true);
32
+ });
33
+ (0, vitest_1.it)("writes correct manifest on init", async () => {
34
+ await (0, sync_1.sync)(tempDir, "0.0.1", {}, output_1.testLog, testSourceDirs);
35
+ const manifest = JSON.parse((0, utils_1.readFile)(tempDir, ".agents/.ai-kit"));
36
+ (0, vitest_1.expect)(manifest.version).toBe("0.0.1");
37
+ (0, vitest_1.expect)(manifest.rootFiles).toContain("opencode.json");
38
+ });
39
+ (0, vitest_1.it)("does not re-initialize if already at latest version", async () => {
40
+ await (0, sync_1.sync)(tempDir, "0.0.1", {}, output_1.testLog, testSourceDirs);
41
+ await (0, sync_1.sync)(tempDir, "0.0.1", {}, output_1.testLog, testSourceDirs);
42
+ const lastLog = (0, output_1.getLastLog)();
43
+ (0, vitest_1.expect)(lastLog).toEqual(["success", "Already at latest version (0.0.1)"]);
44
+ });
45
+ (0, vitest_1.it)("updates when version differs", async () => {
46
+ await (0, sync_1.sync)(tempDir, "0.0.1", {}, output_1.testLog, testSourceDirs);
47
+ await (0, sync_1.sync)(tempDir, "0.0.2", {}, output_1.testLog, testSourceDirs);
48
+ const manifest = JSON.parse((0, utils_1.readFile)(tempDir, ".agents/.ai-kit"));
49
+ (0, vitest_1.expect)(manifest.version).toBe("0.0.2");
50
+ });
51
+ (0, vitest_1.it)("respects skipOpencode option on update", async () => {
52
+ await (0, sync_1.sync)(tempDir, "0.0.1", { skipOpencode: true }, output_1.testLog, testSourceDirs);
53
+ await (0, sync_1.sync)(tempDir, "0.0.2", { skipOpencode: true }, output_1.testLog, testSourceDirs);
54
+ const manifest = JSON.parse((0, utils_1.readFile)(tempDir, ".agents/.ai-kit"));
55
+ (0, vitest_1.expect)(manifest.rootFiles).toEqual([]);
56
+ });
57
+ (0, vitest_1.it)("includes commands in opencode.json", async () => {
58
+ await (0, sync_1.sync)(tempDir, "0.0.1", {}, output_1.testLog, testSourceDirs);
59
+ const opencodeJson = JSON.parse((0, utils_1.readFile)(tempDir, "opencode.json"));
60
+ (0, vitest_1.expect)(opencodeJson).toHaveProperty("command");
61
+ (0, vitest_1.expect)(opencodeJson.command).toHaveProperty("commit");
62
+ (0, vitest_1.expect)(opencodeJson.command.commit.description).toBe("Commit the work done in this session with a structured commit message.");
63
+ });
64
+ (0, vitest_1.it)("processes template variables in content files", async () => {
65
+ await (0, sync_1.sync)(tempDir, "0.0.1", {}, output_1.testLog, testSourceDirs);
66
+ const ruleFiles = (0, utils_1.readFile)(tempDir, ".agents/rules/typescript.md");
67
+ (0, vitest_1.expect)(ruleFiles).toContain("Last updated:");
68
+ (0, vitest_1.expect)(ruleFiles).not.toContain("{{FOOTER}}");
69
+ (0, vitest_1.expect)(ruleFiles).toMatch(/\d{4}-\d{2}-\d{2}/);
70
+ });
71
+ (0, vitest_1.it)("processes template variables in AGENTS.md", async () => {
72
+ await (0, sync_1.sync)(tempDir, "0.0.1", {}, output_1.testLog, testSourceDirs);
73
+ const agentsMd = (0, utils_1.readFile)(tempDir, "AGENTS.md");
74
+ (0, vitest_1.expect)(agentsMd).not.toContain("{{AGENTS_FOOTER}}");
75
+ (0, vitest_1.expect)(agentsMd).toMatch(/\d{4}-\d{2}-\d{2}/);
76
+ });
77
+ (0, vitest_1.it)("logs output correctly", async () => {
78
+ await (0, sync_1.sync)(tempDir, "0.0.1", {}, output_1.testLog, testSourceDirs);
79
+ const logs = output_1.testLog.get();
80
+ (0, vitest_1.expect)(logs[logs.length - 1][0]).toBe("summary");
81
+ });
82
+ (0, vitest_1.it)("logs all synced files", async () => {
83
+ await (0, sync_1.sync)(tempDir, "0.0.1", {}, output_1.testLog, testSourceDirs);
84
+ const successLogs = (0, output_1.findLogs)("success");
85
+ (0, vitest_1.expect)(successLogs.length).toBeGreaterThan(0);
86
+ (0, vitest_1.expect)(successLogs.some(([, msg]) => msg.includes(".md"))).toBe(true);
87
+ (0, vitest_1.expect)(successLogs.some(([, msg]) => msg === "opencode.json")).toBe(true);
88
+ (0, vitest_1.expect)(successLogs.some(([, msg]) => msg === "AGENTS.md")).toBe(true);
89
+ });
90
+ });
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTempDir = createTempDir;
4
+ exports.readFile = readFile;
5
+ exports.fileExists = fileExists;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const os_1 = require("os");
9
+ function createTempDir() {
10
+ return (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), "ai-kit-test-"));
11
+ }
12
+ function readFile(dir, filename) {
13
+ const path = (0, path_1.join)(dir, filename);
14
+ if (!(0, fs_1.existsSync)(path))
15
+ return null;
16
+ return (0, fs_1.readFileSync)(path, "utf-8");
17
+ }
18
+ function fileExists(dir, filename) {
19
+ return (0, fs_1.existsSync)((0, path_1.join)(dir, filename));
20
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_1 = require("vitest/config");
4
+ exports.default = (0, config_1.defineConfig)({
5
+ test: {
6
+ include: ["tests/**/*.test.ts"],
7
+ testTimeout: 10000,
8
+ },
9
+ });
@@ -0,0 +1,16 @@
1
+ # Roadmap
2
+
3
+ ## Near
4
+
5
+ - Publish to npm
6
+
7
+ ## Soon
8
+
9
+ - Add --force flag to sync (reimports everything)
10
+ - Safe copy files (check if file exists before writing)
11
+ - AGENTS.md for single repo (non monorepo)
12
+ - Selective sync (choose which commands/rules to install)
13
+
14
+ ## Long-term
15
+
16
+ - Language detection for lang rules
@@ -0,0 +1,38 @@
1
+ import js from "@eslint/js";
2
+ import tseslint from "typescript-eslint";
3
+
4
+ export default tseslint.config(
5
+ { ignores: ["dist", "node_modules", "src/skills/**/scripts"] },
6
+ js.configs.recommended,
7
+ ...tseslint.configs.recommended,
8
+ {
9
+ files: ["src/**/*.ts"],
10
+ languageOptions: {
11
+ parserOptions: {
12
+ project: "./tsconfig.json",
13
+ },
14
+ },
15
+ rules: {
16
+ "@typescript-eslint/no-unused-vars": [
17
+ "error",
18
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
19
+ ],
20
+ "no-unused-vars": "off",
21
+ },
22
+ },
23
+ {
24
+ files: ["tests/**/*.ts", "vitest.config.ts"],
25
+ languageOptions: {
26
+ parserOptions: {
27
+ project: "./tsconfig.json",
28
+ },
29
+ },
30
+ rules: {
31
+ "@typescript-eslint/no-unused-vars": [
32
+ "error",
33
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
34
+ ],
35
+ "no-unused-vars": "off",
36
+ },
37
+ },
38
+ );
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@clubmatto/ai-kit",
3
+ "version": "0.0.1",
4
+ "description": "The AI configuration CLI from Club Matto",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/clubmatto/vetrina.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/clubmatto/vetrina/issues"
11
+ },
12
+ "homepage": "https://github.com/clubmatto/vetrina#readme",
13
+ "bin": {
14
+ "ai-kit": "./dist/src/index.js"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsx src/index.ts",
19
+ "dev:link": "npm run build && npm link",
20
+ "fetch-playwright-skills": "tsx scripts/fetch-playwright-skills.ts v0.1.1",
21
+ "release": "release-it",
22
+ "test": "vitest run --exclude 'tests/integration/**'",
23
+ "test:integration": "npm run build && vitest run tests/integration",
24
+ "test:watch": "vitest",
25
+ "typecheck": "tsc --noEmit",
26
+ "prettier:write": "prettier --write .",
27
+ "prettier:check": "prettier --check .",
28
+ "format": "prettier --write .",
29
+ "lint": "eslint . && knip",
30
+ "ci": "npm run typecheck && npm run lint && npm run test && npm run test:integration"
31
+ },
32
+ "dependencies": {
33
+ "commander": "^14.0.3",
34
+ "gradient-string": "^3.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@eslint/js": "^10.0.1",
38
+ "@release-it/keep-a-changelog": "^7.0.1",
39
+ "@types/node": "^25.3.0",
40
+ "eslint": "^10.0.1",
41
+ "knip": "^5.85.0",
42
+ "prettier": "^3.4.2",
43
+ "release-it": "^19.2.4",
44
+ "tsx": "latest",
45
+ "typescript": "^5.9.3",
46
+ "typescript-eslint": "^8.56.0",
47
+ "vitest": "^4.0.18"
48
+ },
49
+ "keywords": [
50
+ "ai",
51
+ "opencode",
52
+ "ai-configuration",
53
+ "club-matto",
54
+ "prompt-management",
55
+ "cli"
56
+ ],
57
+ "license": "MIT",
58
+ "release-it": {
59
+ "npm": {
60
+ "publish": true
61
+ },
62
+ "git": {
63
+ "commitMessage": "chore: release v${version}",
64
+ "tagName": "v${version}"
65
+ },
66
+ "version": {
67
+ "bump": false
68
+ },
69
+ "plugins": {
70
+ "@release-it/keep-a-changelog": {}
71
+ }
72
+ },
73
+ "knip": {
74
+ "ignore": [
75
+ "src/skills/**/scripts/*.js"
76
+ ]
77
+ }
78
+ }