@mainahq/core 1.0.2 → 1.1.0
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/package.json +1 -1
- package/src/ai/__tests__/availability.test.ts +131 -0
- package/src/ai/__tests__/delegation.test.ts +55 -1
- package/src/ai/availability.ts +23 -0
- package/src/ai/delegation.ts +5 -3
- package/src/context/__tests__/budget.test.ts +29 -6
- package/src/context/__tests__/engine.test.ts +1 -0
- package/src/context/__tests__/selector.test.ts +23 -3
- package/src/context/__tests__/wiki.test.ts +349 -0
- package/src/context/budget.ts +12 -8
- package/src/context/engine.ts +37 -0
- package/src/context/selector.ts +30 -4
- package/src/context/wiki.ts +296 -0
- package/src/db/index.ts +12 -0
- package/src/feedback/__tests__/capture.test.ts +166 -0
- package/src/feedback/__tests__/signals.test.ts +144 -0
- package/src/feedback/__tests__/tmp-capture-1775575256633-lah0etnzlj/feedback.db +0 -0
- package/src/feedback/__tests__/tmp-capture-1775575256640-2xmjme4qraa/feedback.db +0 -0
- package/src/feedback/capture.ts +102 -0
- package/src/feedback/signals.ts +68 -0
- package/src/index.ts +108 -1
- package/src/init/__tests__/init.test.ts +477 -18
- package/src/init/index.ts +419 -13
- package/src/language/__tests__/__fixtures__/detect/composer.lock +1 -0
- package/src/prompts/defaults/index.ts +3 -1
- package/src/prompts/defaults/wiki-compile.md +20 -0
- package/src/prompts/defaults/wiki-query.md +18 -0
- package/src/stats/__tests__/tool-usage.test.ts +133 -0
- package/src/stats/tracker.ts +92 -0
- package/src/verify/__tests__/builtin.test.ts +270 -0
- package/src/verify/__tests__/pipeline.test.ts +11 -8
- package/src/verify/builtin.ts +350 -0
- package/src/verify/pipeline.ts +32 -2
- package/src/verify/tools/__tests__/wiki-lint.test.ts +784 -0
- package/src/verify/tools/wiki-lint-runner.ts +38 -0
- package/src/verify/tools/wiki-lint.ts +898 -0
- package/src/wiki/__tests__/compiler.test.ts +389 -0
- package/src/wiki/__tests__/extractors/code.test.ts +99 -0
- package/src/wiki/__tests__/extractors/decision.test.ts +323 -0
- package/src/wiki/__tests__/extractors/feature.test.ts +186 -0
- package/src/wiki/__tests__/extractors/workflow.test.ts +131 -0
- package/src/wiki/__tests__/graph.test.ts +344 -0
- package/src/wiki/__tests__/hooks.test.ts +119 -0
- package/src/wiki/__tests__/indexer.test.ts +285 -0
- package/src/wiki/__tests__/linker.test.ts +230 -0
- package/src/wiki/__tests__/louvain.test.ts +229 -0
- package/src/wiki/__tests__/query.test.ts +316 -0
- package/src/wiki/__tests__/schema.test.ts +114 -0
- package/src/wiki/__tests__/signals.test.ts +474 -0
- package/src/wiki/__tests__/state.test.ts +168 -0
- package/src/wiki/__tests__/tracking.test.ts +118 -0
- package/src/wiki/__tests__/types.test.ts +387 -0
- package/src/wiki/compiler.ts +1075 -0
- package/src/wiki/extractors/code.ts +90 -0
- package/src/wiki/extractors/decision.ts +217 -0
- package/src/wiki/extractors/feature.ts +206 -0
- package/src/wiki/extractors/workflow.ts +112 -0
- package/src/wiki/graph.ts +445 -0
- package/src/wiki/hooks.ts +49 -0
- package/src/wiki/indexer.ts +105 -0
- package/src/wiki/linker.ts +117 -0
- package/src/wiki/louvain.ts +190 -0
- package/src/wiki/prompts/compile-architecture.md +59 -0
- package/src/wiki/prompts/compile-decision.md +66 -0
- package/src/wiki/prompts/compile-entity.md +56 -0
- package/src/wiki/prompts/compile-feature.md +60 -0
- package/src/wiki/prompts/compile-module.md +42 -0
- package/src/wiki/prompts/wiki-query.md +25 -0
- package/src/wiki/query.ts +338 -0
- package/src/wiki/schema.ts +111 -0
- package/src/wiki/signals.ts +368 -0
- package/src/wiki/state.ts +89 -0
- package/src/wiki/tracking.ts +30 -0
- package/src/wiki/types.ts +169 -0
- package/src/workflow/context.ts +26 -0
|
@@ -131,29 +131,21 @@ describe("bootstrap", () => {
|
|
|
131
131
|
expect(existsSync(hooksDir)).toBe(true);
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
test("skips existing files (no overwrite)", async () => {
|
|
134
|
+
test("skips existing non-agent files (no overwrite)", async () => {
|
|
135
135
|
// Pre-create constitution.md with custom content
|
|
136
136
|
const mainaDir = join(tmpDir, ".maina");
|
|
137
137
|
mkdirSync(mainaDir, { recursive: true });
|
|
138
138
|
const constitutionPath = join(mainaDir, "constitution.md");
|
|
139
139
|
writeFileSync(constitutionPath, "# My Custom Constitution\n");
|
|
140
140
|
|
|
141
|
-
// Pre-create AGENTS.md with custom content
|
|
142
|
-
const agentsPath = join(tmpDir, "AGENTS.md");
|
|
143
|
-
writeFileSync(agentsPath, "# My Custom Agents\n");
|
|
144
|
-
|
|
145
141
|
const result = await bootstrap(tmpDir);
|
|
146
142
|
expect(result.ok).toBe(true);
|
|
147
143
|
if (result.ok) {
|
|
148
144
|
expect(result.value.skipped).toContain(".maina/constitution.md");
|
|
149
|
-
expect(result.value.skipped).toContain("AGENTS.md");
|
|
150
145
|
|
|
151
146
|
// Content should NOT have been overwritten
|
|
152
147
|
const content = readFileSync(constitutionPath, "utf-8");
|
|
153
148
|
expect(content).toBe("# My Custom Constitution\n");
|
|
154
|
-
|
|
155
|
-
const agentsContent = readFileSync(agentsPath, "utf-8");
|
|
156
|
-
expect(agentsContent).toBe("# My Custom Agents\n");
|
|
157
149
|
}
|
|
158
150
|
});
|
|
159
151
|
|
|
@@ -195,8 +187,11 @@ describe("bootstrap", () => {
|
|
|
195
187
|
expect(result.value.created).toContain(".maina/prompts/review.md");
|
|
196
188
|
expect(result.value.created).toContain(".maina/prompts/commit.md");
|
|
197
189
|
|
|
198
|
-
// Total files should add up
|
|
199
|
-
const total =
|
|
190
|
+
// Total files should add up (created + skipped + updated)
|
|
191
|
+
const total =
|
|
192
|
+
result.value.created.length +
|
|
193
|
+
result.value.skipped.length +
|
|
194
|
+
result.value.updated.length;
|
|
200
195
|
expect(total).toBeGreaterThanOrEqual(5);
|
|
201
196
|
}
|
|
202
197
|
});
|
|
@@ -279,7 +274,12 @@ describe("bootstrap", () => {
|
|
|
279
274
|
|
|
280
275
|
// ── .mcp.json generation ──────────────────────────────────────────────
|
|
281
276
|
|
|
282
|
-
test("creates .mcp.json at repo root", async () => {
|
|
277
|
+
test("creates .mcp.json at repo root with npx for node runtime", async () => {
|
|
278
|
+
writeFileSync(
|
|
279
|
+
join(tmpDir, "package.json"),
|
|
280
|
+
JSON.stringify({ dependencies: { express: "^4" } }),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
283
|
const result = await bootstrap(tmpDir);
|
|
284
284
|
expect(result.ok).toBe(true);
|
|
285
285
|
|
|
@@ -289,8 +289,25 @@ describe("bootstrap", () => {
|
|
|
289
289
|
const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
290
290
|
expect(content.mcpServers).toBeDefined();
|
|
291
291
|
expect(content.mcpServers.maina).toBeDefined();
|
|
292
|
-
expect(content.mcpServers.maina.command).toBe("
|
|
293
|
-
expect(content.mcpServers.maina.args).toEqual(["--mcp"]);
|
|
292
|
+
expect(content.mcpServers.maina.command).toBe("npx");
|
|
293
|
+
expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("creates .mcp.json with bunx for bun runtime", async () => {
|
|
297
|
+
writeFileSync(
|
|
298
|
+
join(tmpDir, "package.json"),
|
|
299
|
+
JSON.stringify({ devDependencies: { "@types/bun": "latest" } }),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const result = await bootstrap(tmpDir);
|
|
303
|
+
expect(result.ok).toBe(true);
|
|
304
|
+
|
|
305
|
+
const mcpPath = join(tmpDir, ".mcp.json");
|
|
306
|
+
expect(existsSync(mcpPath)).toBe(true);
|
|
307
|
+
|
|
308
|
+
const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
309
|
+
expect(content.mcpServers.maina.command).toBe("bunx");
|
|
310
|
+
expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
|
|
294
311
|
});
|
|
295
312
|
|
|
296
313
|
test("does not overwrite existing .mcp.json", async () => {
|
|
@@ -351,24 +368,91 @@ describe("bootstrap", () => {
|
|
|
351
368
|
expect(content).toContain("maina verify");
|
|
352
369
|
});
|
|
353
370
|
|
|
354
|
-
test("
|
|
371
|
+
test("merges maina section into existing agent files without ## Maina", async () => {
|
|
355
372
|
writeFileSync(join(tmpDir, "CLAUDE.md"), "# My Custom CLAUDE.md\n");
|
|
356
373
|
writeFileSync(join(tmpDir, "GEMINI.md"), "# My Custom GEMINI.md\n");
|
|
357
374
|
writeFileSync(join(tmpDir, ".cursorrules"), "# My Custom Rules\n");
|
|
358
375
|
|
|
376
|
+
const result = await bootstrap(tmpDir);
|
|
377
|
+
expect(result.ok).toBe(true);
|
|
378
|
+
if (result.ok) {
|
|
379
|
+
expect(result.value.updated).toContain("CLAUDE.md");
|
|
380
|
+
expect(result.value.updated).toContain("GEMINI.md");
|
|
381
|
+
expect(result.value.updated).toContain(".cursorrules");
|
|
382
|
+
expect(result.value.skipped).not.toContain("CLAUDE.md");
|
|
383
|
+
expect(result.value.skipped).not.toContain("GEMINI.md");
|
|
384
|
+
expect(result.value.skipped).not.toContain(".cursorrules");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const claudeContent = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8");
|
|
388
|
+
expect(claudeContent).toContain("# My Custom CLAUDE.md");
|
|
389
|
+
expect(claudeContent).toContain("## Maina");
|
|
390
|
+
expect(claudeContent).toContain("constitution.md");
|
|
391
|
+
expect(claudeContent).toContain("getContext");
|
|
392
|
+
|
|
393
|
+
const geminiContent = readFileSync(join(tmpDir, "GEMINI.md"), "utf-8");
|
|
394
|
+
expect(geminiContent).toContain("# My Custom GEMINI.md");
|
|
395
|
+
expect(geminiContent).toContain("## Maina");
|
|
396
|
+
|
|
397
|
+
const cursorContent = readFileSync(join(tmpDir, ".cursorrules"), "utf-8");
|
|
398
|
+
expect(cursorContent).toContain("# My Custom Rules");
|
|
399
|
+
expect(cursorContent).toContain("## Maina");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("skips agent files that already have ## Maina section", async () => {
|
|
403
|
+
writeFileSync(
|
|
404
|
+
join(tmpDir, "CLAUDE.md"),
|
|
405
|
+
"# My CLAUDE.md\n\n## Maina\n\nAlready configured.\n",
|
|
406
|
+
);
|
|
407
|
+
writeFileSync(
|
|
408
|
+
join(tmpDir, "GEMINI.md"),
|
|
409
|
+
"# My GEMINI.md\n\n## Maina\n\nAlready configured.\n",
|
|
410
|
+
);
|
|
411
|
+
|
|
359
412
|
const result = await bootstrap(tmpDir);
|
|
360
413
|
expect(result.ok).toBe(true);
|
|
361
414
|
if (result.ok) {
|
|
362
415
|
expect(result.value.skipped).toContain("CLAUDE.md");
|
|
363
416
|
expect(result.value.skipped).toContain("GEMINI.md");
|
|
364
|
-
expect(result.value.
|
|
417
|
+
expect(result.value.updated).not.toContain("CLAUDE.md");
|
|
418
|
+
expect(result.value.updated).not.toContain("GEMINI.md");
|
|
365
419
|
}
|
|
366
420
|
|
|
367
|
-
|
|
368
|
-
|
|
421
|
+
// Content should not be modified
|
|
422
|
+
const claudeContent = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8");
|
|
423
|
+
expect(claudeContent).toBe(
|
|
424
|
+
"# My CLAUDE.md\n\n## Maina\n\nAlready configured.\n",
|
|
369
425
|
);
|
|
370
426
|
});
|
|
371
427
|
|
|
428
|
+
test("merges AGENTS.md with maina section", async () => {
|
|
429
|
+
writeFileSync(
|
|
430
|
+
join(tmpDir, "AGENTS.md"),
|
|
431
|
+
"# My Agents File\n\nCustom content here.\n",
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const result = await bootstrap(tmpDir);
|
|
435
|
+
expect(result.ok).toBe(true);
|
|
436
|
+
if (result.ok) {
|
|
437
|
+
expect(result.value.updated).toContain("AGENTS.md");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const content = readFileSync(join(tmpDir, "AGENTS.md"), "utf-8");
|
|
441
|
+
expect(content).toContain("# My Agents File");
|
|
442
|
+
expect(content).toContain("Custom content here.");
|
|
443
|
+
expect(content).toContain("## Maina");
|
|
444
|
+
expect(content).toContain("brainstorm");
|
|
445
|
+
expect(content).toContain("MCP Tools");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("updated array is empty for fresh directory", async () => {
|
|
449
|
+
const result = await bootstrap(tmpDir);
|
|
450
|
+
expect(result.ok).toBe(true);
|
|
451
|
+
if (result.ok) {
|
|
452
|
+
expect(result.value.updated).toEqual([]);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
372
456
|
// ── Workflow order in agent files ─────────────────────────────────────
|
|
373
457
|
|
|
374
458
|
test("AGENTS.md includes workflow order", async () => {
|
|
@@ -451,13 +535,388 @@ describe("bootstrap", () => {
|
|
|
451
535
|
".github/workflows/maina-ci.yml",
|
|
452
536
|
".github/copilot-instructions.md",
|
|
453
537
|
".mcp.json",
|
|
538
|
+
".claude/settings.json",
|
|
454
539
|
"CLAUDE.md",
|
|
455
540
|
"GEMINI.md",
|
|
456
541
|
".cursorrules",
|
|
542
|
+
".windsurfrules",
|
|
543
|
+
".clinerules",
|
|
544
|
+
".continue/config.yaml",
|
|
545
|
+
".continue/mcpServers/maina.json",
|
|
546
|
+
".roo/mcp.json",
|
|
547
|
+
".roo/rules/maina.md",
|
|
548
|
+
".amazonq/mcp.json",
|
|
549
|
+
".aider.conf.yml",
|
|
550
|
+
"CONVENTIONS.md",
|
|
457
551
|
];
|
|
458
552
|
for (const f of expectedFiles) {
|
|
459
553
|
expect(result.value.created).toContain(f);
|
|
460
554
|
}
|
|
461
555
|
}
|
|
462
556
|
});
|
|
557
|
+
|
|
558
|
+
// ── .claude/settings.json generation ──────────────────────────────────
|
|
559
|
+
|
|
560
|
+
test("creates .claude/settings.json for Claude Code MCP config", async () => {
|
|
561
|
+
writeFileSync(
|
|
562
|
+
join(tmpDir, "package.json"),
|
|
563
|
+
JSON.stringify({ devDependencies: { "@types/bun": "latest" } }),
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
const result = await bootstrap(tmpDir);
|
|
567
|
+
expect(result.ok).toBe(true);
|
|
568
|
+
|
|
569
|
+
const settingsPath = join(tmpDir, ".claude", "settings.json");
|
|
570
|
+
expect(existsSync(settingsPath)).toBe(true);
|
|
571
|
+
|
|
572
|
+
const content = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
573
|
+
expect(content.mcpServers).toBeDefined();
|
|
574
|
+
expect(content.mcpServers.maina).toBeDefined();
|
|
575
|
+
expect(content.mcpServers.maina.command).toBe("bunx");
|
|
576
|
+
expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test(".claude/settings.json uses npx for node runtime", async () => {
|
|
580
|
+
writeFileSync(
|
|
581
|
+
join(tmpDir, "package.json"),
|
|
582
|
+
JSON.stringify({ dependencies: { express: "^4" } }),
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
const result = await bootstrap(tmpDir);
|
|
586
|
+
expect(result.ok).toBe(true);
|
|
587
|
+
|
|
588
|
+
const settingsPath = join(tmpDir, ".claude", "settings.json");
|
|
589
|
+
expect(existsSync(settingsPath)).toBe(true);
|
|
590
|
+
|
|
591
|
+
const content = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
592
|
+
expect(content.mcpServers.maina.command).toBe("npx");
|
|
593
|
+
expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// ── CLAUDE.md includes wiki commands ─────────────────────────────────
|
|
597
|
+
|
|
598
|
+
test("CLAUDE.md includes wiki commands", async () => {
|
|
599
|
+
const result = await bootstrap(tmpDir);
|
|
600
|
+
expect(result.ok).toBe(true);
|
|
601
|
+
|
|
602
|
+
const content = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8");
|
|
603
|
+
expect(content).toContain("maina wiki init");
|
|
604
|
+
expect(content).toContain("maina wiki query");
|
|
605
|
+
expect(content).toContain("maina wiki compile");
|
|
606
|
+
expect(content).toContain("maina wiki status");
|
|
607
|
+
expect(content).toContain("maina wiki lint");
|
|
608
|
+
expect(content).toContain("maina brainstorm");
|
|
609
|
+
expect(content).toContain("maina ticket");
|
|
610
|
+
expect(content).toContain("maina design");
|
|
611
|
+
expect(content).toContain("maina spec");
|
|
612
|
+
expect(content).toContain("maina slop");
|
|
613
|
+
expect(content).toContain("maina explain");
|
|
614
|
+
expect(content).toContain("maina status");
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// ── MCP_TOOLS_TABLE includes wiki tools ──────────────────────────────
|
|
618
|
+
|
|
619
|
+
test("MCP tools table includes wikiQuery and wikiStatus", async () => {
|
|
620
|
+
const result = await bootstrap(tmpDir);
|
|
621
|
+
expect(result.ok).toBe(true);
|
|
622
|
+
|
|
623
|
+
// Check multiple agent files that embed the MCP tools table
|
|
624
|
+
const claudeContent = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8");
|
|
625
|
+
expect(claudeContent).toContain("wikiQuery");
|
|
626
|
+
expect(claudeContent).toContain("wikiStatus");
|
|
627
|
+
|
|
628
|
+
const agentsContent = readFileSync(join(tmpDir, "AGENTS.md"), "utf-8");
|
|
629
|
+
expect(agentsContent).toContain("wikiQuery");
|
|
630
|
+
expect(agentsContent).toContain("wikiStatus");
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// ── Wiki section in agent files ──────────────────────────────────────
|
|
634
|
+
|
|
635
|
+
test("agent files include wiki section", async () => {
|
|
636
|
+
const result = await bootstrap(tmpDir);
|
|
637
|
+
expect(result.ok).toBe(true);
|
|
638
|
+
|
|
639
|
+
const claudeContent = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8");
|
|
640
|
+
expect(claudeContent).toContain("## Wiki");
|
|
641
|
+
expect(claudeContent).toContain("wikiQuery");
|
|
642
|
+
|
|
643
|
+
const agentsContent = readFileSync(join(tmpDir, "AGENTS.md"), "utf-8");
|
|
644
|
+
expect(agentsContent).toContain("## Wiki");
|
|
645
|
+
|
|
646
|
+
const geminiContent = readFileSync(join(tmpDir, "GEMINI.md"), "utf-8");
|
|
647
|
+
expect(geminiContent).toContain("## Wiki");
|
|
648
|
+
|
|
649
|
+
const cursorContent = readFileSync(join(tmpDir, ".cursorrules"), "utf-8");
|
|
650
|
+
expect(cursorContent).toContain("## Wiki");
|
|
651
|
+
|
|
652
|
+
const copilotContent = readFileSync(
|
|
653
|
+
join(tmpDir, ".github", "copilot-instructions.md"),
|
|
654
|
+
"utf-8",
|
|
655
|
+
);
|
|
656
|
+
expect(copilotContent).toContain("## Wiki");
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// ── Windsurf ──────────────────────────────────────────────────────────
|
|
660
|
+
|
|
661
|
+
test("creates .windsurfrules at repo root", async () => {
|
|
662
|
+
const result = await bootstrap(tmpDir);
|
|
663
|
+
expect(result.ok).toBe(true);
|
|
664
|
+
|
|
665
|
+
const filePath = join(tmpDir, ".windsurfrules");
|
|
666
|
+
expect(existsSync(filePath)).toBe(true);
|
|
667
|
+
|
|
668
|
+
const content = readFileSync(filePath, "utf-8");
|
|
669
|
+
expect(content).toContain("Windsurf Rules");
|
|
670
|
+
expect(content).toContain("constitution.md");
|
|
671
|
+
expect(content).toContain("brainstorm");
|
|
672
|
+
expect(content).toContain("maina verify");
|
|
673
|
+
expect(content).toContain("getContext");
|
|
674
|
+
expect(content).toContain("checkSlop");
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test(".windsurfrules includes MCP tools table", async () => {
|
|
678
|
+
const result = await bootstrap(tmpDir);
|
|
679
|
+
expect(result.ok).toBe(true);
|
|
680
|
+
|
|
681
|
+
const content = readFileSync(join(tmpDir, ".windsurfrules"), "utf-8");
|
|
682
|
+
expect(content).toContain("MCP Tools");
|
|
683
|
+
expect(content).toContain("getContext");
|
|
684
|
+
expect(content).toContain("reviewCode");
|
|
685
|
+
expect(content).toContain("wikiQuery");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("merges maina section into existing .windsurfrules", async () => {
|
|
689
|
+
writeFileSync(join(tmpDir, ".windsurfrules"), "# My Windsurf Rules\n");
|
|
690
|
+
|
|
691
|
+
const result = await bootstrap(tmpDir);
|
|
692
|
+
expect(result.ok).toBe(true);
|
|
693
|
+
if (result.ok) {
|
|
694
|
+
expect(result.value.updated).toContain(".windsurfrules");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const content = readFileSync(join(tmpDir, ".windsurfrules"), "utf-8");
|
|
698
|
+
expect(content).toContain("# My Windsurf Rules");
|
|
699
|
+
expect(content).toContain("## Maina");
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// ── Cline ─────────────────────────────────────────────────────────────
|
|
703
|
+
|
|
704
|
+
test("creates .clinerules at repo root", async () => {
|
|
705
|
+
const result = await bootstrap(tmpDir);
|
|
706
|
+
expect(result.ok).toBe(true);
|
|
707
|
+
|
|
708
|
+
const filePath = join(tmpDir, ".clinerules");
|
|
709
|
+
expect(existsSync(filePath)).toBe(true);
|
|
710
|
+
|
|
711
|
+
const content = readFileSync(filePath, "utf-8");
|
|
712
|
+
expect(content).toContain("Cline Rules");
|
|
713
|
+
expect(content).toContain("constitution.md");
|
|
714
|
+
expect(content).toContain("brainstorm");
|
|
715
|
+
expect(content).toContain("maina verify");
|
|
716
|
+
expect(content).toContain("getContext");
|
|
717
|
+
expect(content).toContain("checkSlop");
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test("merges maina section into existing .clinerules", async () => {
|
|
721
|
+
writeFileSync(join(tmpDir, ".clinerules"), "# My Cline Rules\n");
|
|
722
|
+
|
|
723
|
+
const result = await bootstrap(tmpDir);
|
|
724
|
+
expect(result.ok).toBe(true);
|
|
725
|
+
if (result.ok) {
|
|
726
|
+
expect(result.value.updated).toContain(".clinerules");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const content = readFileSync(join(tmpDir, ".clinerules"), "utf-8");
|
|
730
|
+
expect(content).toContain("# My Cline Rules");
|
|
731
|
+
expect(content).toContain("## Maina");
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// ── Continue.dev ──────────────────────────────────────────────────────
|
|
735
|
+
|
|
736
|
+
test("creates .continue/mcpServers/maina.json", async () => {
|
|
737
|
+
const result = await bootstrap(tmpDir);
|
|
738
|
+
expect(result.ok).toBe(true);
|
|
739
|
+
|
|
740
|
+
const mcpPath = join(tmpDir, ".continue", "mcpServers", "maina.json");
|
|
741
|
+
expect(existsSync(mcpPath)).toBe(true);
|
|
742
|
+
|
|
743
|
+
const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
744
|
+
expect(content.maina).toBeDefined();
|
|
745
|
+
expect(content.maina.command).toBe("npx");
|
|
746
|
+
expect(content.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test("creates .continue/config.yaml", async () => {
|
|
750
|
+
const result = await bootstrap(tmpDir);
|
|
751
|
+
expect(result.ok).toBe(true);
|
|
752
|
+
|
|
753
|
+
const configPath = join(tmpDir, ".continue", "config.yaml");
|
|
754
|
+
expect(existsSync(configPath)).toBe(true);
|
|
755
|
+
|
|
756
|
+
const content = readFileSync(configPath, "utf-8");
|
|
757
|
+
expect(content).toContain("customInstructions");
|
|
758
|
+
expect(content).toContain("Maina");
|
|
759
|
+
expect(content).toContain("constitution.md");
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test(".continue/mcpServers/maina.json uses bunx for bun runtime", async () => {
|
|
763
|
+
writeFileSync(
|
|
764
|
+
join(tmpDir, "package.json"),
|
|
765
|
+
JSON.stringify({ devDependencies: { "@types/bun": "latest" } }),
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
const result = await bootstrap(tmpDir);
|
|
769
|
+
expect(result.ok).toBe(true);
|
|
770
|
+
|
|
771
|
+
const mcpPath = join(tmpDir, ".continue", "mcpServers", "maina.json");
|
|
772
|
+
const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
773
|
+
expect(content.maina.command).toBe("bunx");
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// ── Roo Code ──────────────────────────────────────────────────────────
|
|
777
|
+
|
|
778
|
+
test("creates .roo/mcp.json", async () => {
|
|
779
|
+
const result = await bootstrap(tmpDir);
|
|
780
|
+
expect(result.ok).toBe(true);
|
|
781
|
+
|
|
782
|
+
const mcpPath = join(tmpDir, ".roo", "mcp.json");
|
|
783
|
+
expect(existsSync(mcpPath)).toBe(true);
|
|
784
|
+
|
|
785
|
+
const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
786
|
+
expect(content.mcpServers).toBeDefined();
|
|
787
|
+
expect(content.mcpServers.maina).toBeDefined();
|
|
788
|
+
expect(content.mcpServers.maina.command).toBe("npx");
|
|
789
|
+
expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test("creates .roo/rules/maina.md with MCP tools", async () => {
|
|
793
|
+
const result = await bootstrap(tmpDir);
|
|
794
|
+
expect(result.ok).toBe(true);
|
|
795
|
+
|
|
796
|
+
const rulesPath = join(tmpDir, ".roo", "rules", "maina.md");
|
|
797
|
+
expect(existsSync(rulesPath)).toBe(true);
|
|
798
|
+
|
|
799
|
+
const content = readFileSync(rulesPath, "utf-8");
|
|
800
|
+
expect(content).toContain("# Maina");
|
|
801
|
+
expect(content).toContain("constitution.md");
|
|
802
|
+
expect(content).toContain("getContext");
|
|
803
|
+
expect(content).toContain("checkSlop");
|
|
804
|
+
expect(content).toContain("reviewCode");
|
|
805
|
+
expect(content).toContain("wikiQuery");
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test(".roo/mcp.json uses bunx for bun runtime", async () => {
|
|
809
|
+
writeFileSync(
|
|
810
|
+
join(tmpDir, "package.json"),
|
|
811
|
+
JSON.stringify({ devDependencies: { "@types/bun": "latest" } }),
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
const result = await bootstrap(tmpDir);
|
|
815
|
+
expect(result.ok).toBe(true);
|
|
816
|
+
|
|
817
|
+
const mcpPath = join(tmpDir, ".roo", "mcp.json");
|
|
818
|
+
const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
819
|
+
expect(content.mcpServers.maina.command).toBe("bunx");
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// ── Amazon Q ──────────────────────────────────────────────────────────
|
|
823
|
+
|
|
824
|
+
test("creates .amazonq/mcp.json", async () => {
|
|
825
|
+
const result = await bootstrap(tmpDir);
|
|
826
|
+
expect(result.ok).toBe(true);
|
|
827
|
+
|
|
828
|
+
const mcpPath = join(tmpDir, ".amazonq", "mcp.json");
|
|
829
|
+
expect(existsSync(mcpPath)).toBe(true);
|
|
830
|
+
|
|
831
|
+
const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
832
|
+
expect(content.mcpServers).toBeDefined();
|
|
833
|
+
expect(content.mcpServers.maina).toBeDefined();
|
|
834
|
+
expect(content.mcpServers.maina.command).toBe("npx");
|
|
835
|
+
expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test(".amazonq/mcp.json uses bunx for bun runtime", async () => {
|
|
839
|
+
writeFileSync(
|
|
840
|
+
join(tmpDir, "package.json"),
|
|
841
|
+
JSON.stringify({ devDependencies: { "@types/bun": "latest" } }),
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
const result = await bootstrap(tmpDir);
|
|
845
|
+
expect(result.ok).toBe(true);
|
|
846
|
+
|
|
847
|
+
const mcpPath = join(tmpDir, ".amazonq", "mcp.json");
|
|
848
|
+
const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
849
|
+
expect(content.mcpServers.maina.command).toBe("bunx");
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// ── Aider ─────────────────────────────────────────────────────────────
|
|
853
|
+
|
|
854
|
+
test("creates .aider.conf.yml", async () => {
|
|
855
|
+
const result = await bootstrap(tmpDir);
|
|
856
|
+
expect(result.ok).toBe(true);
|
|
857
|
+
|
|
858
|
+
const configPath = join(tmpDir, ".aider.conf.yml");
|
|
859
|
+
expect(existsSync(configPath)).toBe(true);
|
|
860
|
+
|
|
861
|
+
const content = readFileSync(configPath, "utf-8");
|
|
862
|
+
expect(content).toContain("CONVENTIONS.md");
|
|
863
|
+
expect(content).toContain("constitution.md");
|
|
864
|
+
expect(content).toContain("auto-commits: false");
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
test("creates CONVENTIONS.md", async () => {
|
|
868
|
+
const result = await bootstrap(tmpDir);
|
|
869
|
+
expect(result.ok).toBe(true);
|
|
870
|
+
|
|
871
|
+
const convPath = join(tmpDir, "CONVENTIONS.md");
|
|
872
|
+
expect(existsSync(convPath)).toBe(true);
|
|
873
|
+
|
|
874
|
+
const content = readFileSync(convPath, "utf-8");
|
|
875
|
+
expect(content).toContain("# Conventions");
|
|
876
|
+
expect(content).toContain("constitution.md");
|
|
877
|
+
expect(content).toContain("brainstorm");
|
|
878
|
+
expect(content).toContain("getContext");
|
|
879
|
+
expect(content).toContain("checkSlop");
|
|
880
|
+
expect(content).toContain("reviewCode");
|
|
881
|
+
expect(content).toContain("maina verify");
|
|
882
|
+
expect(content).toContain("wikiQuery");
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
test("merges maina section into existing CONVENTIONS.md", async () => {
|
|
886
|
+
writeFileSync(join(tmpDir, "CONVENTIONS.md"), "# My Conventions\n");
|
|
887
|
+
|
|
888
|
+
const result = await bootstrap(tmpDir);
|
|
889
|
+
expect(result.ok).toBe(true);
|
|
890
|
+
if (result.ok) {
|
|
891
|
+
expect(result.value.updated).toContain("CONVENTIONS.md");
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const content = readFileSync(join(tmpDir, "CONVENTIONS.md"), "utf-8");
|
|
895
|
+
expect(content).toContain("# My Conventions");
|
|
896
|
+
expect(content).toContain("## Maina");
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
// ── All rules files consistent ───────────────────────────────────────
|
|
900
|
+
|
|
901
|
+
test("all rules files contain MCP tools table", async () => {
|
|
902
|
+
const result = await bootstrap(tmpDir);
|
|
903
|
+
expect(result.ok).toBe(true);
|
|
904
|
+
|
|
905
|
+
const rulesFiles = [
|
|
906
|
+
".cursorrules",
|
|
907
|
+
".windsurfrules",
|
|
908
|
+
".clinerules",
|
|
909
|
+
".roo/rules/maina.md",
|
|
910
|
+
"CONVENTIONS.md",
|
|
911
|
+
];
|
|
912
|
+
|
|
913
|
+
for (const f of rulesFiles) {
|
|
914
|
+
const content = readFileSync(join(tmpDir, f), "utf-8");
|
|
915
|
+
expect(content).toContain("getContext");
|
|
916
|
+
expect(content).toContain("checkSlop");
|
|
917
|
+
expect(content).toContain("reviewCode");
|
|
918
|
+
expect(content).toContain("suggestTests");
|
|
919
|
+
expect(content).toContain("wikiQuery");
|
|
920
|
+
}
|
|
921
|
+
});
|
|
463
922
|
});
|