@codemcp/ade 0.6.1 → 0.8.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/.beads/issues.jsonl +37 -0
- package/.beads/last-touched +1 -1
- package/.kiro/agents/ade.json +10 -2
- package/.kiro/settings/mcp.json +6 -1
- package/.opencode/agents/ade.md +7 -2
- package/.vibe/beads-state-ade-extension-override-skills-d44z9p.json +29 -0
- package/.vibe/beads-state-ade-fix-docset-writing-37fuoj.json +29 -0
- package/.vibe/development-plan-extension-override-skills.md +110 -0
- package/.vibe/development-plan-fix-docset-writing.md +77 -0
- package/.vibe/docs/architecture.md +201 -0
- package/.vibe/docs/design.md +179 -0
- package/.vibe/docs/requirements.md +17 -0
- package/ade.extensions.mjs +13 -15
- package/config.lock.yaml +6 -1
- package/docs/CLI-PRD.md +38 -40
- package/docs/CLI-design.md +47 -57
- package/docs/guide/extensions.md +6 -14
- package/package.json +1 -1
- package/packages/cli/dist/index.js +15213 -5579
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/conventions.integration.spec.ts +29 -4
- package/packages/cli/src/commands/extensions.integration.spec.ts +26 -4
- package/packages/cli/src/commands/install.ts +2 -0
- package/packages/cli/src/commands/knowledge-docset.integration.spec.ts +179 -0
- package/packages/cli/src/commands/knowledge.integration.spec.ts +24 -36
- package/packages/cli/src/commands/setup.spec.ts +1 -101
- package/packages/cli/src/commands/setup.ts +23 -36
- package/packages/cli/src/knowledge-installer.spec.ts +43 -3
- package/packages/cli/src/knowledge-installer.ts +12 -9
- package/packages/core/package.json +1 -1
- package/packages/core/src/catalog/catalog.spec.ts +75 -43
- package/packages/core/src/catalog/facets/architecture.ts +89 -58
- package/packages/core/src/catalog/facets/practices.ts +9 -8
- package/packages/core/src/index.ts +4 -4
- package/packages/core/src/registry.spec.ts +1 -1
- package/packages/core/src/registry.ts +2 -2
- package/packages/core/src/resolver.spec.ts +391 -154
- package/packages/core/src/resolver.ts +16 -54
- package/packages/core/src/types.ts +19 -10
- package/packages/core/src/writers/docset.spec.ts +40 -0
- package/packages/core/src/writers/docset.ts +24 -0
- package/packages/core/src/writers/skills.spec.ts +46 -0
- package/packages/harnesses/package.json +1 -1
- package/packages/harnesses/src/writers/opencode.spec.ts +3 -5
- package/packages/harnesses/src/writers/opencode.ts +3 -4
- package/packages/core/src/writers/knowledge.spec.ts +0 -26
- package/packages/core/src/writers/knowledge.ts +0 -15
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
mkdtemp,
|
|
4
|
+
rm,
|
|
5
|
+
readFile,
|
|
6
|
+
access,
|
|
7
|
+
writeFile,
|
|
8
|
+
mkdir
|
|
9
|
+
} from "node:fs/promises";
|
|
3
10
|
import { tmpdir } from "node:os";
|
|
4
11
|
import { join } from "node:path";
|
|
5
12
|
|
|
@@ -21,6 +28,27 @@ vi.mock("@clack/prompts", () => ({
|
|
|
21
28
|
spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
|
|
22
29
|
}));
|
|
23
30
|
|
|
31
|
+
// Mock the knowledge package to avoid real network I/O
|
|
32
|
+
vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({
|
|
33
|
+
createDocset: vi.fn(
|
|
34
|
+
async (
|
|
35
|
+
params: { id: string; name: string; url?: string },
|
|
36
|
+
options: { cwd?: string }
|
|
37
|
+
) => {
|
|
38
|
+
const dir = join(options?.cwd ?? process.cwd(), ".knowledge");
|
|
39
|
+
await mkdir(dir, { recursive: true });
|
|
40
|
+
const configPath = join(dir, "config.yaml");
|
|
41
|
+
await writeFile(
|
|
42
|
+
configPath,
|
|
43
|
+
`version: "1.0"\ndocsets:\n - id: ${params.id}\n`,
|
|
44
|
+
{ flag: "w" }
|
|
45
|
+
);
|
|
46
|
+
return { docset: {}, configPath, configCreated: true };
|
|
47
|
+
}
|
|
48
|
+
),
|
|
49
|
+
initDocset: vi.fn().mockResolvedValue({ alreadyInitialized: false })
|
|
50
|
+
}));
|
|
51
|
+
|
|
24
52
|
import * as clack from "@clack/prompts";
|
|
25
53
|
import { runSetup } from "./setup.js";
|
|
26
54
|
import { readUserConfig, readLockFile } from "@codemcp/ade-core";
|
|
@@ -51,7 +79,6 @@ describe("architecture and practices facets integration", () => {
|
|
|
51
79
|
vi.mocked(clack.multiselect)
|
|
52
80
|
.mockResolvedValueOnce([]) // practices: none
|
|
53
81
|
.mockResolvedValueOnce([]) // backpressure: none
|
|
54
|
-
.mockResolvedValueOnce([]) // docsets: deselect all
|
|
55
82
|
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
56
83
|
|
|
57
84
|
await runSetup(dir, catalog);
|
|
@@ -110,7 +137,6 @@ describe("architecture and practices facets integration", () => {
|
|
|
110
137
|
.mockResolvedValueOnce("__skip__"); // architecture: skip
|
|
111
138
|
vi.mocked(clack.multiselect)
|
|
112
139
|
.mockResolvedValueOnce(["conventional-commits", "tdd-london"]) // practices
|
|
113
|
-
.mockResolvedValueOnce([]) // docsets: deselect all (conventional-commits has docset)
|
|
114
140
|
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
115
141
|
|
|
116
142
|
await runSetup(dir, catalog);
|
|
@@ -237,7 +263,6 @@ describe("architecture and practices facets integration", () => {
|
|
|
237
263
|
vi.mocked(clack.multiselect)
|
|
238
264
|
.mockResolvedValueOnce(["tdd-london", "conventional-commits"]) // practices
|
|
239
265
|
.mockResolvedValueOnce([]) // backpressure: none
|
|
240
|
-
.mockResolvedValueOnce([]) // docsets: deselect all
|
|
241
266
|
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
242
267
|
|
|
243
268
|
await runSetup(dir, catalog);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, rm, readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
@@ -17,6 +17,27 @@ vi.mock("@clack/prompts", () => ({
|
|
|
17
17
|
spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
|
+
// Mock the knowledge package to avoid real network I/O
|
|
21
|
+
vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({
|
|
22
|
+
createDocset: vi.fn(
|
|
23
|
+
async (
|
|
24
|
+
params: { id: string; name: string; url?: string },
|
|
25
|
+
options: { cwd?: string }
|
|
26
|
+
) => {
|
|
27
|
+
const dir = join(options?.cwd ?? process.cwd(), ".knowledge");
|
|
28
|
+
await mkdir(dir, { recursive: true });
|
|
29
|
+
const configPath = join(dir, "config.yaml");
|
|
30
|
+
await writeFile(
|
|
31
|
+
configPath,
|
|
32
|
+
`version: "1.0"\ndocsets:\n - id: ${params.id}\n`,
|
|
33
|
+
{ flag: "w" }
|
|
34
|
+
);
|
|
35
|
+
return { docset: {}, configPath, configCreated: true };
|
|
36
|
+
}
|
|
37
|
+
),
|
|
38
|
+
initDocset: vi.fn().mockResolvedValue({ alreadyInitialized: false })
|
|
39
|
+
}));
|
|
40
|
+
|
|
20
41
|
import * as clack from "@clack/prompts";
|
|
21
42
|
import { runSetup } from "./setup.js";
|
|
22
43
|
import {
|
|
@@ -43,7 +64,7 @@ describe("extension e2e — option contributes skills and knowledge to setup out
|
|
|
43
64
|
"extension-contributed architecture option writes inline skill and knowledge source",
|
|
44
65
|
{ timeout: 60_000 },
|
|
45
66
|
async () => {
|
|
46
|
-
// Build an extension with a SAP option that has an inline skill +
|
|
67
|
+
// Build an extension with a SAP option that has an inline skill + a docset
|
|
47
68
|
const extensions: AdeExtensions = {
|
|
48
69
|
facetContributions: {
|
|
49
70
|
architecture: [
|
|
@@ -65,9 +86,10 @@ describe("extension e2e — option contributes skills and knowledge to setup out
|
|
|
65
86
|
}
|
|
66
87
|
},
|
|
67
88
|
{
|
|
68
|
-
writer: "
|
|
89
|
+
writer: "docset",
|
|
69
90
|
config: {
|
|
70
|
-
|
|
91
|
+
id: "sap-abap-docs",
|
|
92
|
+
label: "SAP ABAP Cloud",
|
|
71
93
|
origin: "https://help.sap.com/docs/abap-cloud",
|
|
72
94
|
description: "SAP ABAP Cloud documentation"
|
|
73
95
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
installSkills,
|
|
9
9
|
writeInlineSkills
|
|
10
10
|
} from "@codemcp/ade-harnesses";
|
|
11
|
+
import { installKnowledge } from "../knowledge-installer.js";
|
|
11
12
|
|
|
12
13
|
export async function runInstall(
|
|
13
14
|
projectRoot: string,
|
|
@@ -77,6 +78,7 @@ export async function runInstall(
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
if (logicalConfig.knowledge_sources.length > 0) {
|
|
81
|
+
await installKnowledge(logicalConfig.knowledge_sources, projectRoot);
|
|
80
82
|
clack.log.info(
|
|
81
83
|
"Knowledge sources configured. Initialize them separately:\n npx @codemcp/knowledge init"
|
|
82
84
|
);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* E2E regression tests for two issues with docset / knowledge setup:
|
|
8
|
+
*
|
|
9
|
+
* Issue 1 — Design inconsistency (now fixed):
|
|
10
|
+
* The `knowledge` provision writer was a redundant second way to add
|
|
11
|
+
* knowledge sources alongside `option.docsets[]`. It has been removed.
|
|
12
|
+
* Docsets are now declared via `{ writer: "docset", config: {...} }` recipe
|
|
13
|
+
* entries (the `docset` provision writer), consistent with how skills work.
|
|
14
|
+
*
|
|
15
|
+
* Issue 2 — Missing .knowledge/config.yaml (now fixed):
|
|
16
|
+
* `installKnowledge` was never called from `setup` or `install`, so
|
|
17
|
+
* `createDocset` was never invoked and `.knowledge/config.yaml` was
|
|
18
|
+
* never written. Both commands now call `installKnowledge`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Mock the TUI
|
|
22
|
+
vi.mock("@clack/prompts", () => ({
|
|
23
|
+
intro: vi.fn(),
|
|
24
|
+
outro: vi.fn(),
|
|
25
|
+
note: vi.fn(),
|
|
26
|
+
select: vi.fn(),
|
|
27
|
+
multiselect: vi.fn(),
|
|
28
|
+
confirm: vi.fn().mockResolvedValue(false),
|
|
29
|
+
isCancel: vi.fn().mockReturnValue(false),
|
|
30
|
+
cancel: vi.fn(),
|
|
31
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn() },
|
|
32
|
+
spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// Mock the knowledge package to avoid real network I/O while still letting us
|
|
36
|
+
// assert that createDocset is called with the correct arguments.
|
|
37
|
+
// The mock writes a real .knowledge/config.yaml so file-existence assertions work.
|
|
38
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
39
|
+
vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({
|
|
40
|
+
createDocset: vi.fn(
|
|
41
|
+
async (
|
|
42
|
+
params: { id: string; name: string; url?: string },
|
|
43
|
+
options: { cwd?: string }
|
|
44
|
+
) => {
|
|
45
|
+
const dir = join(options?.cwd ?? process.cwd(), ".knowledge");
|
|
46
|
+
await mkdir(dir, { recursive: true });
|
|
47
|
+
const configPath = join(dir, "config.yaml");
|
|
48
|
+
// Append a minimal docset entry so the file is created/updated
|
|
49
|
+
await writeFile(
|
|
50
|
+
configPath,
|
|
51
|
+
`version: "1.0"\ndocsets:\n - id: ${params.id}\n`,
|
|
52
|
+
{ flag: "w" }
|
|
53
|
+
);
|
|
54
|
+
return { docset: {}, configPath, configCreated: true };
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
initDocset: vi.fn().mockResolvedValue({ alreadyInitialized: false })
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
import * as clack from "@clack/prompts";
|
|
61
|
+
import { createDocset } from "@codemcp/knowledge/packages/cli/dist/exports.js";
|
|
62
|
+
import { runSetup } from "./setup.js";
|
|
63
|
+
import { runInstall } from "./install.js";
|
|
64
|
+
import { readLockFile, getDefaultCatalog } from "@codemcp/ade-core";
|
|
65
|
+
|
|
66
|
+
describe("knowledge docset regression tests", () => {
|
|
67
|
+
let dir: string;
|
|
68
|
+
|
|
69
|
+
beforeEach(async () => {
|
|
70
|
+
vi.clearAllMocks();
|
|
71
|
+
vi.mocked(clack.confirm).mockResolvedValue(false);
|
|
72
|
+
dir = await mkdtemp(join(tmpdir(), "ade-knowledge-bug-"));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(async () => {
|
|
76
|
+
await rm(dir, { recursive: true, force: true });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// -------------------------------------------------------------------------
|
|
80
|
+
// Issue 2 fix: setup writes .knowledge/config.yaml
|
|
81
|
+
// -------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
it(
|
|
84
|
+
"setup writes .knowledge/config.yaml when knowledge_sources are configured",
|
|
85
|
+
{ timeout: 30_000 },
|
|
86
|
+
async () => {
|
|
87
|
+
const catalog = getDefaultCatalog();
|
|
88
|
+
|
|
89
|
+
vi.mocked(clack.select)
|
|
90
|
+
.mockResolvedValueOnce("codemcp-workflows") // process
|
|
91
|
+
.mockResolvedValueOnce("tanstack"); // architecture — has 4 docsets
|
|
92
|
+
vi.mocked(clack.multiselect)
|
|
93
|
+
.mockResolvedValueOnce([]) // practices: none
|
|
94
|
+
.mockResolvedValueOnce([]) // backpressure: none
|
|
95
|
+
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
96
|
+
vi.mocked(clack.confirm)
|
|
97
|
+
.mockResolvedValueOnce(false) // skills: skip
|
|
98
|
+
.mockResolvedValueOnce(true); // knowledge: initialize now
|
|
99
|
+
|
|
100
|
+
await runSetup(dir, catalog);
|
|
101
|
+
|
|
102
|
+
// Sanity: knowledge_sources in lock file
|
|
103
|
+
const lock = await readLockFile(dir);
|
|
104
|
+
expect(lock!.logical_config.knowledge_sources).toHaveLength(4);
|
|
105
|
+
|
|
106
|
+
// createDocset must have been called once per source
|
|
107
|
+
expect(createDocset).toHaveBeenCalledTimes(4);
|
|
108
|
+
expect(createDocset).toHaveBeenCalledWith(
|
|
109
|
+
expect.objectContaining({ id: "tanstack-router-docs" }),
|
|
110
|
+
expect.objectContaining({ cwd: dir })
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// .knowledge/config.yaml must exist
|
|
114
|
+
const configYaml = await readFile(
|
|
115
|
+
join(dir, ".knowledge", "config.yaml"),
|
|
116
|
+
"utf-8"
|
|
117
|
+
);
|
|
118
|
+
expect(configYaml).toBeTruthy();
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
it(
|
|
123
|
+
"install writes .knowledge/config.yaml when knowledge_sources exist in lock file",
|
|
124
|
+
{ timeout: 30_000 },
|
|
125
|
+
async () => {
|
|
126
|
+
const catalog = getDefaultCatalog();
|
|
127
|
+
|
|
128
|
+
// First setup to produce a lock file with knowledge_sources
|
|
129
|
+
vi.mocked(clack.select)
|
|
130
|
+
.mockResolvedValueOnce("codemcp-workflows") // process
|
|
131
|
+
.mockResolvedValueOnce("tanstack"); // architecture
|
|
132
|
+
vi.mocked(clack.multiselect)
|
|
133
|
+
.mockResolvedValueOnce([]) // practices: none
|
|
134
|
+
.mockResolvedValueOnce([]) // backpressure: none
|
|
135
|
+
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
136
|
+
// skills: skip, knowledge: skip (install command will handle it)
|
|
137
|
+
vi.mocked(clack.confirm)
|
|
138
|
+
.mockResolvedValueOnce(false)
|
|
139
|
+
.mockResolvedValueOnce(false);
|
|
140
|
+
|
|
141
|
+
await runSetup(dir, catalog);
|
|
142
|
+
vi.clearAllMocks();
|
|
143
|
+
|
|
144
|
+
// Wipe agent files so install has something to regenerate
|
|
145
|
+
await rm(join(dir, ".mcp.json"), { force: true });
|
|
146
|
+
await rm(join(dir, ".knowledge"), { recursive: true, force: true });
|
|
147
|
+
|
|
148
|
+
// Now run install — should also write .knowledge/config.yaml
|
|
149
|
+
await runInstall(dir, ["claude-code"]);
|
|
150
|
+
|
|
151
|
+
// All 4 tanstack docsets are configured via the docset writer (no per-item selection)
|
|
152
|
+
expect(createDocset).toHaveBeenCalledTimes(4);
|
|
153
|
+
expect(createDocset).toHaveBeenCalledWith(
|
|
154
|
+
expect.objectContaining({ id: "tanstack-router-docs" }),
|
|
155
|
+
expect.objectContaining({ cwd: dir })
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const configYaml = await readFile(
|
|
159
|
+
join(dir, ".knowledge", "config.yaml"),
|
|
160
|
+
"utf-8"
|
|
161
|
+
);
|
|
162
|
+
expect(configYaml).toBeTruthy();
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// -------------------------------------------------------------------------
|
|
167
|
+
// Issue 1 fix: knowledge writer removed — docset provision writer is canonical
|
|
168
|
+
// -------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
it("ProvisionWriter type no longer includes 'knowledge'", async () => {
|
|
171
|
+
// Import the type-level check: if 'knowledge' were still in ProvisionWriter,
|
|
172
|
+
// this runtime check would catch the registry accepting it silently.
|
|
173
|
+
const { createDefaultRegistry } = await import("@codemcp/ade-core");
|
|
174
|
+
const registry = createDefaultRegistry();
|
|
175
|
+
// The knowledge writer must not be registered
|
|
176
|
+
const { getProvisionWriter } = await import("@codemcp/ade-core");
|
|
177
|
+
expect(getProvisionWriter(registry, "knowledge")).toBeUndefined();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, rm, readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
@@ -16,6 +16,27 @@ vi.mock("@clack/prompts", () => ({
|
|
|
16
16
|
spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
|
|
17
17
|
}));
|
|
18
18
|
|
|
19
|
+
// Mock the knowledge package to avoid real network I/O
|
|
20
|
+
vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({
|
|
21
|
+
createDocset: vi.fn(
|
|
22
|
+
async (
|
|
23
|
+
params: { id: string; name: string; url?: string },
|
|
24
|
+
options: { cwd?: string }
|
|
25
|
+
) => {
|
|
26
|
+
const dir = join(options?.cwd ?? process.cwd(), ".knowledge");
|
|
27
|
+
await mkdir(dir, { recursive: true });
|
|
28
|
+
const configPath = join(dir, "config.yaml");
|
|
29
|
+
await writeFile(
|
|
30
|
+
configPath,
|
|
31
|
+
`version: "1.0"\ndocsets:\n - id: ${params.id}\n`,
|
|
32
|
+
{ flag: "w" }
|
|
33
|
+
);
|
|
34
|
+
return { docset: {}, configPath, configCreated: true };
|
|
35
|
+
}
|
|
36
|
+
),
|
|
37
|
+
initDocset: vi.fn().mockResolvedValue({ alreadyInitialized: false })
|
|
38
|
+
}));
|
|
39
|
+
|
|
19
40
|
import * as clack from "@clack/prompts";
|
|
20
41
|
import { runSetup } from "./setup.js";
|
|
21
42
|
import { readLockFile } from "@codemcp/ade-core";
|
|
@@ -46,17 +67,11 @@ describe("knowledge integration", () => {
|
|
|
46
67
|
vi.mocked(clack.multiselect)
|
|
47
68
|
.mockResolvedValueOnce([]) // practices: none
|
|
48
69
|
.mockResolvedValueOnce([]) // backpressure: none
|
|
49
|
-
.mockResolvedValueOnce([
|
|
50
|
-
"tanstack-router-docs",
|
|
51
|
-
"tanstack-query-docs",
|
|
52
|
-
"tanstack-form-docs",
|
|
53
|
-
"tanstack-table-docs"
|
|
54
|
-
])
|
|
55
70
|
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
56
71
|
|
|
57
72
|
await runSetup(dir, catalog);
|
|
58
73
|
|
|
59
|
-
// Lock file should contain knowledge_sources
|
|
74
|
+
// Lock file should contain all 4 knowledge_sources from tanstack docset entries
|
|
60
75
|
const lock = await readLockFile(dir);
|
|
61
76
|
expect(lock!.logical_config.knowledge_sources).toHaveLength(4);
|
|
62
77
|
expect(lock!.logical_config.knowledge_sources.map((s) => s.name)).toEqual(
|
|
@@ -84,33 +99,6 @@ describe("knowledge integration", () => {
|
|
|
84
99
|
}
|
|
85
100
|
);
|
|
86
101
|
|
|
87
|
-
it(
|
|
88
|
-
"excludes deselected docsets from lock file",
|
|
89
|
-
{ timeout: 60_000 },
|
|
90
|
-
async () => {
|
|
91
|
-
const catalog = getDefaultCatalog();
|
|
92
|
-
|
|
93
|
-
vi.mocked(clack.select)
|
|
94
|
-
.mockResolvedValueOnce("codemcp-workflows") // process
|
|
95
|
-
.mockResolvedValueOnce("tanstack"); // architecture
|
|
96
|
-
|
|
97
|
-
vi.mocked(clack.multiselect)
|
|
98
|
-
.mockResolvedValueOnce([]) // practices: none
|
|
99
|
-
.mockResolvedValueOnce([]) // backpressure: none
|
|
100
|
-
.mockResolvedValueOnce(["tanstack-router-docs", "tanstack-query-docs"])
|
|
101
|
-
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
102
|
-
|
|
103
|
-
await runSetup(dir, catalog);
|
|
104
|
-
|
|
105
|
-
// Lock file should only have the 2 selected sources
|
|
106
|
-
const lock = await readLockFile(dir);
|
|
107
|
-
expect(lock!.logical_config.knowledge_sources).toHaveLength(2);
|
|
108
|
-
expect(lock!.logical_config.knowledge_sources.map((s) => s.name)).toEqual(
|
|
109
|
-
expect.arrayContaining(["tanstack-router-docs", "tanstack-query-docs"])
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
);
|
|
113
|
-
|
|
114
102
|
it("does not show knowledge hint when no docsets are implied", async () => {
|
|
115
103
|
const catalog = getDefaultCatalog();
|
|
116
104
|
|
|
@@ -118,7 +106,7 @@ describe("knowledge integration", () => {
|
|
|
118
106
|
.mockResolvedValueOnce("native-agents-md") // process
|
|
119
107
|
.mockResolvedValueOnce("__skip__"); // architecture: skip
|
|
120
108
|
vi.mocked(clack.multiselect)
|
|
121
|
-
.mockResolvedValueOnce(["tdd-london"]) // practices: no docsets
|
|
109
|
+
.mockResolvedValueOnce(["tdd-london"]) // practices: tdd-london has no docsets
|
|
122
110
|
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
123
111
|
|
|
124
112
|
await runSetup(dir, catalog);
|
|
@@ -31,8 +31,7 @@ vi.mock("@codemcp/ade-core", async (importOriginal) => {
|
|
|
31
31
|
skills: [],
|
|
32
32
|
git_hooks: [],
|
|
33
33
|
setup_notes: []
|
|
34
|
-
} satisfies LogicalConfig)
|
|
35
|
-
collectDocsets: actual.collectDocsets
|
|
34
|
+
} satisfies LogicalConfig)
|
|
36
35
|
};
|
|
37
36
|
});
|
|
38
37
|
|
|
@@ -112,39 +111,6 @@ const testCatalog: Catalog = {
|
|
|
112
111
|
]
|
|
113
112
|
};
|
|
114
113
|
|
|
115
|
-
const docsetCatalog: Catalog = {
|
|
116
|
-
facets: [
|
|
117
|
-
{
|
|
118
|
-
id: "arch",
|
|
119
|
-
label: "Architecture",
|
|
120
|
-
description: "Stack",
|
|
121
|
-
required: true,
|
|
122
|
-
options: [
|
|
123
|
-
{
|
|
124
|
-
id: "react",
|
|
125
|
-
label: "React",
|
|
126
|
-
description: "React framework",
|
|
127
|
-
recipe: [],
|
|
128
|
-
docsets: [
|
|
129
|
-
{
|
|
130
|
-
id: "react-docs",
|
|
131
|
-
label: "React Reference",
|
|
132
|
-
origin: "https://github.com/facebook/react.git",
|
|
133
|
-
description: "Official React docs"
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
id: "react-tutorial",
|
|
137
|
-
label: "React Tutorial",
|
|
138
|
-
origin: "https://github.com/reactjs/react.dev.git",
|
|
139
|
-
description: "React learn guide"
|
|
140
|
-
}
|
|
141
|
-
]
|
|
142
|
-
}
|
|
143
|
-
]
|
|
144
|
-
}
|
|
145
|
-
]
|
|
146
|
-
};
|
|
147
|
-
|
|
148
114
|
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
149
115
|
|
|
150
116
|
describe("runSetup", () => {
|
|
@@ -239,72 +205,6 @@ describe("runSetup", () => {
|
|
|
239
205
|
expect(clack.cancel).toHaveBeenCalled();
|
|
240
206
|
});
|
|
241
207
|
|
|
242
|
-
describe("docset confirmation step", () => {
|
|
243
|
-
it("presents implied docsets as a multiselect after facet selection", async () => {
|
|
244
|
-
vi.mocked(clack.select).mockResolvedValueOnce("react");
|
|
245
|
-
// User accepts all docsets (returns all ids), then harness selection
|
|
246
|
-
vi.mocked(clack.multiselect)
|
|
247
|
-
.mockResolvedValueOnce(["react-docs", "react-tutorial"])
|
|
248
|
-
.mockResolvedValueOnce(["claude-code"]);
|
|
249
|
-
|
|
250
|
-
await runSetup("/tmp/test-project", docsetCatalog);
|
|
251
|
-
|
|
252
|
-
// multiselect should have been called for docsets
|
|
253
|
-
expect(clack.multiselect).toHaveBeenCalledWith(
|
|
254
|
-
expect.objectContaining({
|
|
255
|
-
message: expect.stringContaining("Documentation")
|
|
256
|
-
})
|
|
257
|
-
);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("stores deselected docsets as excluded_docsets in user config", async () => {
|
|
261
|
-
vi.mocked(clack.select).mockResolvedValueOnce("react");
|
|
262
|
-
// User deselects react-tutorial, keeps only react-docs; then harness
|
|
263
|
-
vi.mocked(clack.multiselect)
|
|
264
|
-
.mockResolvedValueOnce(["react-docs"])
|
|
265
|
-
.mockResolvedValueOnce(["claude-code"]);
|
|
266
|
-
|
|
267
|
-
await runSetup("/tmp/test-project", docsetCatalog);
|
|
268
|
-
|
|
269
|
-
expect(writeUserConfig).toHaveBeenCalledWith(
|
|
270
|
-
"/tmp/test-project",
|
|
271
|
-
expect.objectContaining({
|
|
272
|
-
excluded_docsets: ["react-tutorial"]
|
|
273
|
-
})
|
|
274
|
-
);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("does not set excluded_docsets when all docsets are accepted", async () => {
|
|
278
|
-
vi.mocked(clack.select).mockResolvedValueOnce("react");
|
|
279
|
-
vi.mocked(clack.multiselect)
|
|
280
|
-
.mockResolvedValueOnce(["react-docs", "react-tutorial"])
|
|
281
|
-
.mockResolvedValueOnce(["claude-code"]);
|
|
282
|
-
|
|
283
|
-
await runSetup("/tmp/test-project", docsetCatalog);
|
|
284
|
-
|
|
285
|
-
const configArg = vi.mocked(writeUserConfig).mock.calls[0][1];
|
|
286
|
-
expect(configArg.excluded_docsets).toBeUndefined();
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("skips docset prompt when no options have docsets", async () => {
|
|
290
|
-
vi.mocked(clack.select)
|
|
291
|
-
.mockResolvedValueOnce("workflow-a")
|
|
292
|
-
.mockResolvedValueOnce("vitest");
|
|
293
|
-
// Only the harness multiselect should be called (no docsets in testCatalog)
|
|
294
|
-
vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]);
|
|
295
|
-
|
|
296
|
-
await runSetup("/tmp/test-project", testCatalog);
|
|
297
|
-
|
|
298
|
-
// multiselect should have been called exactly once (for harnesses only)
|
|
299
|
-
expect(clack.multiselect).toHaveBeenCalledTimes(1);
|
|
300
|
-
expect(clack.multiselect).toHaveBeenCalledWith(
|
|
301
|
-
expect.objectContaining({
|
|
302
|
-
message: expect.stringContaining("coding agents")
|
|
303
|
-
})
|
|
304
|
-
);
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
208
|
it("calls intro and outro from @clack/prompts", async () => {
|
|
309
209
|
vi.mocked(clack.select)
|
|
310
210
|
.mockResolvedValueOnce("workflow-a")
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
writeUserConfig,
|
|
9
9
|
writeLockFile,
|
|
10
10
|
resolve,
|
|
11
|
-
collectDocsets,
|
|
12
11
|
createDefaultRegistry,
|
|
13
12
|
getFacet,
|
|
14
13
|
getOption,
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
installSkills,
|
|
23
22
|
writeInlineSkills
|
|
24
23
|
} from "@codemcp/ade-harnesses";
|
|
24
|
+
import { installKnowledge } from "../knowledge-installer.js";
|
|
25
25
|
|
|
26
26
|
export async function runSetup(
|
|
27
27
|
projectRoot: string,
|
|
@@ -107,37 +107,6 @@ export async function runSetup(
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
// Docset confirmation step: collect implied docsets, let user deselect
|
|
111
|
-
const impliedDocsets = collectDocsets(choices, catalog);
|
|
112
|
-
let excludedDocsets: string[] | undefined;
|
|
113
|
-
|
|
114
|
-
if (impliedDocsets.length > 0) {
|
|
115
|
-
const selected = await clack.multiselect({
|
|
116
|
-
message:
|
|
117
|
-
"Documentation sources — Those will be pulled to your local disk for browsing on demand",
|
|
118
|
-
options: impliedDocsets.map((d) => ({
|
|
119
|
-
value: d.id,
|
|
120
|
-
label: d.label,
|
|
121
|
-
hint: d.description
|
|
122
|
-
})),
|
|
123
|
-
initialValues: impliedDocsets.map((d) => d.id),
|
|
124
|
-
required: false
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
if (typeof selected === "symbol") {
|
|
128
|
-
clack.cancel("Setup cancelled.");
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const selectedSet = new Set(selected as string[]);
|
|
133
|
-
const excluded = impliedDocsets
|
|
134
|
-
.filter((d) => !selectedSet.has(d.id))
|
|
135
|
-
.map((d) => d.id);
|
|
136
|
-
if (excluded.length > 0) {
|
|
137
|
-
excludedDocsets = excluded;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
110
|
// Harness selection — multi-select from all available harnesses
|
|
142
111
|
const existingHarnesses = existingConfig?.harnesses;
|
|
143
112
|
const harnessOptions = harnessWriters.map((w) => ({
|
|
@@ -171,7 +140,6 @@ export async function runSetup(
|
|
|
171
140
|
|
|
172
141
|
const userConfig: UserConfig = {
|
|
173
142
|
choices,
|
|
174
|
-
...(excludedDocsets && { excluded_docsets: excludedDocsets }),
|
|
175
143
|
...(harnesses.length > 0 && { harnesses })
|
|
176
144
|
};
|
|
177
145
|
const registry = createDefaultRegistry();
|
|
@@ -234,9 +202,28 @@ export async function runSetup(
|
|
|
234
202
|
}
|
|
235
203
|
|
|
236
204
|
if (logicalConfig.knowledge_sources.length > 0) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
205
|
+
const initCommands = logicalConfig.knowledge_sources
|
|
206
|
+
.map((s) => ` npx @codemcp/knowledge init ${s.name}`)
|
|
207
|
+
.join("\n");
|
|
208
|
+
const confirmInit = await clack.confirm({
|
|
209
|
+
message: `Initialize ${logicalConfig.knowledge_sources.length} knowledge source(s) now?`,
|
|
210
|
+
initialValue: false
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (typeof confirmInit === "symbol") {
|
|
214
|
+
clack.cancel("Setup cancelled.");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (confirmInit) {
|
|
219
|
+
await installKnowledge(logicalConfig.knowledge_sources, projectRoot, {
|
|
220
|
+
force: true
|
|
221
|
+
});
|
|
222
|
+
} else {
|
|
223
|
+
clack.log.info(
|
|
224
|
+
`Knowledge sources configured. Initialize them when ready:\n${initCommands}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
240
227
|
}
|
|
241
228
|
|
|
242
229
|
for (const note of logicalConfig.setup_notes) {
|