@codemcp/ade-core 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.
Files changed (71) hide show
  1. package/.prettierignore +1 -0
  2. package/.turbo/turbo-build.log +4 -0
  3. package/.turbo/turbo-format.log +6 -0
  4. package/.turbo/turbo-lint.log +4 -0
  5. package/.turbo/turbo-test.log +21 -0
  6. package/.turbo/turbo-typecheck.log +4 -0
  7. package/LICENSE +21 -0
  8. package/dist/catalog/facets/architecture.d.ts +2 -0
  9. package/dist/catalog/facets/architecture.js +424 -0
  10. package/dist/catalog/facets/backpressure.d.ts +2 -0
  11. package/dist/catalog/facets/backpressure.js +123 -0
  12. package/dist/catalog/facets/practices.d.ts +2 -0
  13. package/dist/catalog/facets/practices.js +163 -0
  14. package/dist/catalog/facets/process.d.ts +2 -0
  15. package/dist/catalog/facets/process.js +47 -0
  16. package/dist/catalog/index.d.ts +14 -0
  17. package/dist/catalog/index.js +71 -0
  18. package/dist/config.d.ts +5 -0
  19. package/dist/config.js +29 -0
  20. package/dist/index.d.ts +12 -0
  21. package/dist/index.js +6 -0
  22. package/dist/registry.d.ts +7 -0
  23. package/dist/registry.js +41 -0
  24. package/dist/resolver.d.ts +7 -0
  25. package/dist/resolver.js +142 -0
  26. package/dist/types.d.ts +110 -0
  27. package/dist/types.js +2 -0
  28. package/dist/writers/git-hooks.d.ts +2 -0
  29. package/dist/writers/git-hooks.js +7 -0
  30. package/dist/writers/instruction.d.ts +2 -0
  31. package/dist/writers/instruction.js +6 -0
  32. package/dist/writers/knowledge.d.ts +2 -0
  33. package/dist/writers/knowledge.js +9 -0
  34. package/dist/writers/setup-note.d.ts +2 -0
  35. package/dist/writers/setup-note.js +7 -0
  36. package/dist/writers/skills.d.ts +2 -0
  37. package/dist/writers/skills.js +7 -0
  38. package/dist/writers/workflows.d.ts +2 -0
  39. package/dist/writers/workflows.js +16 -0
  40. package/eslint.config.mjs +40 -0
  41. package/nodemon.json +7 -0
  42. package/package.json +34 -0
  43. package/src/catalog/catalog.spec.ts +531 -0
  44. package/src/catalog/facets/architecture.ts +438 -0
  45. package/src/catalog/facets/backpressure.ts +143 -0
  46. package/src/catalog/facets/practices.ts +173 -0
  47. package/src/catalog/facets/process.ts +50 -0
  48. package/src/catalog/index.ts +86 -0
  49. package/src/config.spec.ts +165 -0
  50. package/src/config.ts +39 -0
  51. package/src/index.ts +49 -0
  52. package/src/registry.spec.ts +144 -0
  53. package/src/registry.ts +68 -0
  54. package/src/resolver.spec.ts +581 -0
  55. package/src/resolver.ts +170 -0
  56. package/src/types.ts +151 -0
  57. package/src/writers/git-hooks.ts +9 -0
  58. package/src/writers/instruction.spec.ts +42 -0
  59. package/src/writers/instruction.ts +8 -0
  60. package/src/writers/knowledge.spec.ts +26 -0
  61. package/src/writers/knowledge.ts +15 -0
  62. package/src/writers/setup-note.ts +9 -0
  63. package/src/writers/skills.spec.ts +109 -0
  64. package/src/writers/skills.ts +9 -0
  65. package/src/writers/workflows.spec.ts +72 -0
  66. package/src/writers/workflows.ts +26 -0
  67. package/tsconfig.build.json +8 -0
  68. package/tsconfig.json +7 -0
  69. package/tsconfig.tsbuildinfo +1 -0
  70. package/tsconfig.vitest.json +7 -0
  71. package/vitest.config.ts +5 -0
@@ -0,0 +1,581 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolve, collectDocsets } from "./resolver.js";
3
+ import { getDefaultCatalog } from "./catalog/index.js";
4
+ import { createRegistry, registerProvisionWriter } from "./registry.js";
5
+ import { instructionWriter } from "./writers/instruction.js";
6
+ import { workflowsWriter } from "./writers/workflows.js";
7
+ import { skillsWriter } from "./writers/skills.js";
8
+ import { setupNoteWriter } from "./writers/setup-note.js";
9
+ import type { UserConfig, WriterRegistry, Catalog } from "./types.js";
10
+
11
+ function buildRegistry(): WriterRegistry {
12
+ const registry = createRegistry();
13
+ registerProvisionWriter(registry, instructionWriter);
14
+ registerProvisionWriter(registry, workflowsWriter);
15
+ registerProvisionWriter(registry, skillsWriter);
16
+ registerProvisionWriter(registry, setupNoteWriter);
17
+ return registry;
18
+ }
19
+
20
+ describe("resolve", () => {
21
+ let catalog: Catalog;
22
+ let registry: WriterRegistry;
23
+
24
+ beforeEach(() => {
25
+ catalog = getDefaultCatalog();
26
+ registry = buildRegistry();
27
+ });
28
+
29
+ describe("single-select resolution", () => {
30
+ it("resolves codemcp-workflows to LogicalConfig with mcp_servers", async () => {
31
+ const userConfig: UserConfig = {
32
+ choices: { process: "codemcp-workflows" }
33
+ };
34
+
35
+ const result = await resolve(userConfig, catalog, registry);
36
+
37
+ // workflows writer produces mcp_servers
38
+ expect(result.mcp_servers).toBeDefined();
39
+ expect(result.mcp_servers.length).toBeGreaterThanOrEqual(1);
40
+ // Should have all LogicalConfig fields
41
+ expect(result).toHaveProperty("instructions");
42
+ expect(result).toHaveProperty("cli_actions");
43
+ expect(result).toHaveProperty("knowledge_sources");
44
+ });
45
+ });
46
+
47
+ describe("different option selection", () => {
48
+ it("resolves native-agents-md to LogicalConfig with instructions but no mcp_servers", async () => {
49
+ const userConfig: UserConfig = {
50
+ choices: { process: "native-agents-md" }
51
+ };
52
+
53
+ const result = await resolve(userConfig, catalog, registry);
54
+
55
+ // instruction writer produces instructions
56
+ expect(result.instructions).toBeDefined();
57
+ // native-agents-md has no workflows provision, so no mcp_servers
58
+ expect(result.mcp_servers).toEqual([]);
59
+ });
60
+ });
61
+
62
+ describe("empty choices", () => {
63
+ it("returns an empty LogicalConfig when no choices are provided", async () => {
64
+ const userConfig: UserConfig = {
65
+ choices: {}
66
+ };
67
+
68
+ const result = await resolve(userConfig, catalog, registry);
69
+
70
+ expect(result).toEqual({
71
+ mcp_servers: [],
72
+ instructions: [],
73
+ cli_actions: [],
74
+ knowledge_sources: [],
75
+ skills: [],
76
+ git_hooks: [],
77
+ setup_notes: []
78
+ });
79
+ });
80
+ });
81
+
82
+ describe("custom section merge", () => {
83
+ it("merges custom instructions and mcp_servers into the output", async () => {
84
+ const userConfig: UserConfig = {
85
+ choices: {},
86
+ custom: {
87
+ instructions: ["Always use TypeScript strict mode"],
88
+ mcp_servers: [
89
+ {
90
+ ref: "my-custom-server",
91
+ command: "node",
92
+ args: ["server.js"],
93
+ env: {}
94
+ }
95
+ ]
96
+ }
97
+ };
98
+
99
+ const result = await resolve(userConfig, catalog, registry);
100
+
101
+ expect(result.instructions).toContain(
102
+ "Always use TypeScript strict mode"
103
+ );
104
+ expect(result.mcp_servers).toContainEqual(
105
+ expect.objectContaining({ ref: "my-custom-server" })
106
+ );
107
+ });
108
+
109
+ it("merges custom section with recipe-produced config", async () => {
110
+ const userConfig: UserConfig = {
111
+ choices: { process: "codemcp-workflows" },
112
+ custom: {
113
+ instructions: ["Extra instruction"]
114
+ }
115
+ };
116
+
117
+ const result = await resolve(userConfig, catalog, registry);
118
+
119
+ // Should have both recipe mcp_servers and custom instructions
120
+ expect(result.mcp_servers.length).toBeGreaterThanOrEqual(1);
121
+ expect(result.instructions).toContain("Extra instruction");
122
+ });
123
+ });
124
+
125
+ describe("unknown facet in choices", () => {
126
+ it("ignores unknown facet ids without throwing", async () => {
127
+ const userConfig: UserConfig = {
128
+ choices: { "nonexistent-facet": "some-option" }
129
+ };
130
+
131
+ // Should not throw
132
+ const result = await resolve(userConfig, catalog, registry);
133
+
134
+ expect(result).toEqual({
135
+ mcp_servers: [],
136
+ instructions: [],
137
+ cli_actions: [],
138
+ knowledge_sources: [],
139
+ skills: [],
140
+ git_hooks: [],
141
+ setup_notes: []
142
+ });
143
+ });
144
+ });
145
+
146
+ describe("unknown option in choices", () => {
147
+ it("throws when facet exists but option id does not", async () => {
148
+ const userConfig: UserConfig = {
149
+ choices: { process: "nonexistent-option" }
150
+ };
151
+
152
+ await expect(resolve(userConfig, catalog, registry)).rejects.toThrow();
153
+ });
154
+ });
155
+
156
+ describe("skills merging", () => {
157
+ it("merges skills from provision writers into the output", async () => {
158
+ // Use a custom catalog with a facet that produces skills
159
+ const skillsCatalog: Catalog = {
160
+ facets: [
161
+ {
162
+ id: "conventions",
163
+ label: "Conventions",
164
+ description: "Team conventions",
165
+ required: false,
166
+ options: [
167
+ {
168
+ id: "test-conv",
169
+ label: "Test Convention",
170
+ description: "A test convention with skills",
171
+ recipe: [
172
+ {
173
+ writer: "skills",
174
+ config: {
175
+ skills: [
176
+ {
177
+ name: "test-skill",
178
+ description: "A test skill",
179
+ body: "Do the thing."
180
+ }
181
+ ]
182
+ }
183
+ }
184
+ ]
185
+ }
186
+ ]
187
+ }
188
+ ]
189
+ };
190
+
191
+ const userConfig: UserConfig = {
192
+ choices: { conventions: "test-conv" }
193
+ };
194
+
195
+ const result = await resolve(userConfig, skillsCatalog, registry);
196
+
197
+ expect(result.skills).toHaveLength(1);
198
+ expect(result.skills[0].name).toBe("test-skill");
199
+
200
+ // Resolver should auto-add agentskills MCP entry when skills are present
201
+ const agentskills = result.mcp_servers.find(
202
+ (s) => s.ref === "agentskills"
203
+ );
204
+ expect(agentskills).toBeDefined();
205
+ expect(agentskills!.command).toBe("npx");
206
+ expect(agentskills!.args).toContain("@codemcp/skills-server");
207
+ });
208
+
209
+ it("does not add agentskills MCP entry when no skills", async () => {
210
+ const userConfig: UserConfig = {
211
+ choices: { process: "native-agents-md" }
212
+ };
213
+ const result = await resolve(userConfig, catalog, registry);
214
+
215
+ const agentskills = result.mcp_servers.find(
216
+ (s) => s.ref === "agentskills"
217
+ );
218
+ expect(agentskills).toBeUndefined();
219
+ });
220
+ });
221
+
222
+ describe("setup_notes merging", () => {
223
+ it("merges setup_notes from provision writers into the output", async () => {
224
+ const notesCatalog: Catalog = {
225
+ facets: [
226
+ {
227
+ id: "quality",
228
+ label: "Quality",
229
+ description: "Quality tools",
230
+ required: false,
231
+ options: [
232
+ {
233
+ id: "test-gate",
234
+ label: "Test Gate",
235
+ description: "A gate with a setup note",
236
+ recipe: [
237
+ {
238
+ writer: "setup-note",
239
+ config: { text: "Run npm install before committing." }
240
+ }
241
+ ]
242
+ }
243
+ ]
244
+ }
245
+ ]
246
+ };
247
+
248
+ const userConfig: UserConfig = { choices: { quality: "test-gate" } };
249
+ const result = await resolve(userConfig, notesCatalog, registry);
250
+
251
+ expect(result.setup_notes).toEqual([
252
+ "Run npm install before committing."
253
+ ]);
254
+ });
255
+ });
256
+
257
+ describe("docset collection", () => {
258
+ it("collects docsets from selected options into knowledge_sources", async () => {
259
+ const docsetCatalog: Catalog = {
260
+ facets: [
261
+ {
262
+ id: "arch",
263
+ label: "Architecture",
264
+ description: "Stack",
265
+ required: false,
266
+ options: [
267
+ {
268
+ id: "react",
269
+ label: "React",
270
+ description: "React framework",
271
+ recipe: [],
272
+ docsets: [
273
+ {
274
+ id: "react-docs",
275
+ label: "React Reference",
276
+ origin: "https://github.com/facebook/react.git",
277
+ description: "Official React documentation"
278
+ }
279
+ ]
280
+ }
281
+ ]
282
+ }
283
+ ]
284
+ };
285
+
286
+ const userConfig: UserConfig = { choices: { arch: "react" } };
287
+ const result = await resolve(userConfig, docsetCatalog, registry);
288
+
289
+ expect(result.knowledge_sources).toHaveLength(1);
290
+ expect(result.knowledge_sources[0]).toEqual({
291
+ name: "react-docs",
292
+ origin: "https://github.com/facebook/react.git",
293
+ description: "Official React documentation"
294
+ });
295
+ });
296
+
297
+ it("deduplicates docsets by id across multiple options", async () => {
298
+ const docsetCatalog: Catalog = {
299
+ facets: [
300
+ {
301
+ id: "stack",
302
+ label: "Stack",
303
+ description: "Tech stack",
304
+ required: false,
305
+ multiSelect: true,
306
+ options: [
307
+ {
308
+ id: "react",
309
+ label: "React",
310
+ description: "React",
311
+ recipe: [],
312
+ docsets: [
313
+ {
314
+ id: "react-docs",
315
+ label: "React Reference",
316
+ origin: "https://github.com/facebook/react.git",
317
+ description: "React docs"
318
+ }
319
+ ]
320
+ },
321
+ {
322
+ id: "nextjs",
323
+ label: "Next.js",
324
+ description: "Next.js",
325
+ recipe: [],
326
+ docsets: [
327
+ {
328
+ id: "react-docs",
329
+ label: "React Reference",
330
+ origin: "https://github.com/facebook/react.git",
331
+ description: "React docs"
332
+ },
333
+ {
334
+ id: "nextjs-docs",
335
+ label: "Next.js Docs",
336
+ origin: "https://nextjs.org/docs",
337
+ description: "Next.js docs"
338
+ }
339
+ ]
340
+ }
341
+ ]
342
+ }
343
+ ]
344
+ };
345
+
346
+ const userConfig: UserConfig = {
347
+ choices: { stack: ["react", "nextjs"] }
348
+ };
349
+ const result = await resolve(userConfig, docsetCatalog, registry);
350
+
351
+ expect(result.knowledge_sources).toHaveLength(2);
352
+ const ids = result.knowledge_sources.map((ks) => ks.name);
353
+ expect(ids).toContain("react-docs");
354
+ expect(ids).toContain("nextjs-docs");
355
+ });
356
+
357
+ it("filters out excluded_docsets", async () => {
358
+ const docsetCatalog: Catalog = {
359
+ facets: [
360
+ {
361
+ id: "arch",
362
+ label: "Architecture",
363
+ description: "Stack",
364
+ required: false,
365
+ options: [
366
+ {
367
+ id: "react",
368
+ label: "React",
369
+ description: "React",
370
+ recipe: [],
371
+ docsets: [
372
+ {
373
+ id: "react-docs",
374
+ label: "React Reference",
375
+ origin: "https://github.com/facebook/react.git",
376
+ description: "React docs"
377
+ },
378
+ {
379
+ id: "react-tutorial",
380
+ label: "React Tutorial",
381
+ origin: "https://github.com/reactjs/react.dev.git",
382
+ description: "React tutorial"
383
+ }
384
+ ]
385
+ }
386
+ ]
387
+ }
388
+ ]
389
+ };
390
+
391
+ const userConfig: UserConfig = {
392
+ choices: { arch: "react" },
393
+ excluded_docsets: ["react-tutorial"]
394
+ };
395
+ const result = await resolve(userConfig, docsetCatalog, registry);
396
+
397
+ expect(result.knowledge_sources).toHaveLength(1);
398
+ expect(result.knowledge_sources[0].name).toBe("react-docs");
399
+ });
400
+
401
+ it("adds knowledge-server MCP entry when knowledge_sources are present", async () => {
402
+ const docsetCatalog: Catalog = {
403
+ facets: [
404
+ {
405
+ id: "arch",
406
+ label: "Architecture",
407
+ description: "Stack",
408
+ required: false,
409
+ options: [
410
+ {
411
+ id: "react",
412
+ label: "React",
413
+ description: "React",
414
+ recipe: [],
415
+ docsets: [
416
+ {
417
+ id: "react-docs",
418
+ label: "React Reference",
419
+ origin: "https://github.com/facebook/react.git",
420
+ description: "React docs"
421
+ }
422
+ ]
423
+ }
424
+ ]
425
+ }
426
+ ]
427
+ };
428
+
429
+ const userConfig: UserConfig = { choices: { arch: "react" } };
430
+ const result = await resolve(userConfig, docsetCatalog, registry);
431
+
432
+ const knowledgeServer = result.mcp_servers.find(
433
+ (s) => s.ref === "knowledge"
434
+ );
435
+ expect(knowledgeServer).toBeDefined();
436
+ expect(knowledgeServer!.command).toBe("npx");
437
+ expect(knowledgeServer!.args).toContain("@codemcp/knowledge-server");
438
+ });
439
+
440
+ it("does not add knowledge-server MCP entry when no knowledge_sources", async () => {
441
+ const userConfig: UserConfig = {
442
+ choices: { process: "native-agents-md" }
443
+ };
444
+ const result = await resolve(userConfig, catalog, registry);
445
+
446
+ const knowledgeServer = result.mcp_servers.find(
447
+ (s) => s.ref === "knowledge"
448
+ );
449
+ expect(knowledgeServer).toBeUndefined();
450
+ });
451
+
452
+ it("produces no knowledge_sources when option has no docsets", async () => {
453
+ const userConfig: UserConfig = {
454
+ choices: { process: "native-agents-md" }
455
+ };
456
+ const result = await resolve(userConfig, catalog, registry);
457
+
458
+ expect(result.knowledge_sources).toEqual([]);
459
+ });
460
+ });
461
+
462
+ describe("collectDocsets", () => {
463
+ it("returns deduplicated docsets for given choices", () => {
464
+ const docsetCatalog: Catalog = {
465
+ facets: [
466
+ {
467
+ id: "stack",
468
+ label: "Stack",
469
+ description: "Stack",
470
+ required: false,
471
+ multiSelect: true,
472
+ options: [
473
+ {
474
+ id: "a",
475
+ label: "A",
476
+ description: "A",
477
+ recipe: [],
478
+ docsets: [
479
+ {
480
+ id: "shared",
481
+ label: "Shared",
482
+ origin: "https://x",
483
+ description: "shared"
484
+ },
485
+ {
486
+ id: "a-only",
487
+ label: "A Only",
488
+ origin: "https://a",
489
+ description: "a"
490
+ }
491
+ ]
492
+ },
493
+ {
494
+ id: "b",
495
+ label: "B",
496
+ description: "B",
497
+ recipe: [],
498
+ docsets: [
499
+ {
500
+ id: "shared",
501
+ label: "Shared",
502
+ origin: "https://x",
503
+ description: "shared"
504
+ },
505
+ {
506
+ id: "b-only",
507
+ label: "B Only",
508
+ origin: "https://b",
509
+ description: "b"
510
+ }
511
+ ]
512
+ }
513
+ ]
514
+ }
515
+ ]
516
+ };
517
+
518
+ const result = collectDocsets({ stack: ["a", "b"] }, docsetCatalog);
519
+
520
+ expect(result).toHaveLength(3);
521
+ const ids = result.map((d) => d.id);
522
+ expect(ids).toContain("shared");
523
+ expect(ids).toContain("a-only");
524
+ expect(ids).toContain("b-only");
525
+ });
526
+
527
+ it("returns empty array when no options have docsets", () => {
528
+ const result = collectDocsets({ process: "native-agents-md" }, catalog);
529
+ expect(result).toEqual([]);
530
+ });
531
+ });
532
+
533
+ describe("MCP server dedup by ref", () => {
534
+ it("deduplicates mcp_servers by ref, keeping the last one", async () => {
535
+ // Create a custom registry with a writer that produces duplicate refs
536
+ const dedupRegistry = createRegistry();
537
+ registerProvisionWriter(dedupRegistry, {
538
+ id: "workflows",
539
+ async write() {
540
+ return {
541
+ mcp_servers: [
542
+ {
543
+ ref: "duplicate-server",
544
+ command: "npx",
545
+ args: ["-y", "pkg-a"],
546
+ env: {}
547
+ }
548
+ ]
549
+ };
550
+ }
551
+ });
552
+ registerProvisionWriter(dedupRegistry, instructionWriter);
553
+
554
+ // Also add a custom mcp_server with the same ref but different args
555
+ const userConfig: UserConfig = {
556
+ choices: { process: "codemcp-workflows" },
557
+ custom: {
558
+ mcp_servers: [
559
+ {
560
+ ref: "duplicate-server",
561
+ command: "node",
562
+ args: ["custom-server.js"],
563
+ env: { CUSTOM: "true" }
564
+ }
565
+ ]
566
+ }
567
+ };
568
+
569
+ const result = await resolve(userConfig, catalog, dedupRegistry);
570
+
571
+ // Should only have one entry with ref "duplicate-server"
572
+ const duplicates = result.mcp_servers.filter(
573
+ (s) => s.ref === "duplicate-server"
574
+ );
575
+ expect(duplicates).toHaveLength(1);
576
+ // Last one wins — the custom one should survive
577
+ expect(duplicates[0].command).toBe("node");
578
+ expect(duplicates[0].env).toEqual({ CUSTOM: "true" });
579
+ });
580
+ });
581
+ });