@codemcp/ade-cli 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.
- package/.prettierignore +1 -0
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-format.log +6 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +1264 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +21 -0
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.js +33 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +171 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +43 -0
- package/dist/knowledge-installer.d.ts +12 -0
- package/dist/knowledge-installer.js +38 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/eslint.config.mjs +40 -0
- package/nodemon.json +7 -0
- package/package.json +40 -0
- package/src/commands/conventions.integration.spec.ts +267 -0
- package/src/commands/install.integration.spec.ts +123 -0
- package/src/commands/install.spec.ts +168 -0
- package/src/commands/install.ts +53 -0
- package/src/commands/knowledge.integration.spec.ts +129 -0
- package/src/commands/setup.integration.spec.ts +148 -0
- package/src/commands/setup.spec.ts +441 -0
- package/src/commands/setup.ts +242 -0
- package/src/index.ts +52 -0
- package/src/knowledge-installer.spec.ts +111 -0
- package/src/knowledge-installer.ts +54 -0
- package/src/version.ts +1 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.vitest.json +7 -0
- package/vitest.config.ts +5 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, readFile, access } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
vi.mock("@clack/prompts", () => ({
|
|
7
|
+
intro: vi.fn(),
|
|
8
|
+
outro: vi.fn(),
|
|
9
|
+
select: vi.fn(),
|
|
10
|
+
multiselect: vi.fn(),
|
|
11
|
+
confirm: vi.fn(),
|
|
12
|
+
isCancel: vi.fn().mockReturnValue(false),
|
|
13
|
+
cancel: vi.fn(),
|
|
14
|
+
spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import * as clack from "@clack/prompts";
|
|
18
|
+
import { runSetup } from "./setup.js";
|
|
19
|
+
import { readUserConfig, readLockFile } from "@codemcp/ade-core";
|
|
20
|
+
import { getDefaultCatalog } from "../../../core/src/catalog/index.js";
|
|
21
|
+
|
|
22
|
+
describe("architecture and practices facets integration", () => {
|
|
23
|
+
let dir: string;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
dir = await mkdtemp(join(tmpdir(), "ade-conventions-"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await rm(dir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it(
|
|
35
|
+
"writes SKILL.md files and installs inline skills for tanstack architecture",
|
|
36
|
+
{ timeout: 60_000 },
|
|
37
|
+
async () => {
|
|
38
|
+
const catalog = getDefaultCatalog();
|
|
39
|
+
|
|
40
|
+
// Facet order: process (select), architecture (select), practices (multiselect)
|
|
41
|
+
vi.mocked(clack.select)
|
|
42
|
+
.mockResolvedValueOnce("codemcp-workflows") // process
|
|
43
|
+
.mockResolvedValueOnce("tanstack"); // architecture
|
|
44
|
+
vi.mocked(clack.multiselect)
|
|
45
|
+
.mockResolvedValueOnce([]) // practices: none
|
|
46
|
+
.mockResolvedValueOnce([]) // backpressure: none
|
|
47
|
+
.mockResolvedValueOnce([]) // docsets: deselect all
|
|
48
|
+
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
49
|
+
|
|
50
|
+
await runSetup(dir, catalog);
|
|
51
|
+
|
|
52
|
+
// Inline skills should have SKILL.md in .ade/skills/ (staging area)
|
|
53
|
+
for (const skill of [
|
|
54
|
+
"tanstack-architecture",
|
|
55
|
+
"tanstack-design",
|
|
56
|
+
"tanstack-code",
|
|
57
|
+
"tanstack-testing"
|
|
58
|
+
]) {
|
|
59
|
+
const skillMd = await readFile(
|
|
60
|
+
join(dir, ".ade", "skills", skill, "SKILL.md"),
|
|
61
|
+
"utf-8"
|
|
62
|
+
);
|
|
63
|
+
expect(skillMd).toContain(`name: ${skill}`);
|
|
64
|
+
expect(skillMd).toContain("---");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Inline skills should also be installed to .agentskills/skills/ by runAdd
|
|
68
|
+
for (const skill of [
|
|
69
|
+
"tanstack-architecture",
|
|
70
|
+
"tanstack-design",
|
|
71
|
+
"tanstack-code",
|
|
72
|
+
"tanstack-testing"
|
|
73
|
+
]) {
|
|
74
|
+
const installed = await readFile(
|
|
75
|
+
join(dir, ".agentskills", "skills", skill, "SKILL.md"),
|
|
76
|
+
"utf-8"
|
|
77
|
+
);
|
|
78
|
+
expect(installed).toContain(`name: ${skill}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// skills-lock.json should be created by runAdd
|
|
82
|
+
const lockRaw = await readFile(join(dir, "skills-lock.json"), "utf-8");
|
|
83
|
+
const skillsLock = JSON.parse(lockRaw);
|
|
84
|
+
expect(skillsLock.skills).toBeDefined();
|
|
85
|
+
|
|
86
|
+
// skills-server MCP server should be in .mcp.json
|
|
87
|
+
const mcpJson = JSON.parse(
|
|
88
|
+
await readFile(join(dir, ".mcp.json"), "utf-8")
|
|
89
|
+
);
|
|
90
|
+
expect(mcpJson.mcpServers["agentskills"]).toMatchObject({
|
|
91
|
+
command: "npx",
|
|
92
|
+
args: ["-y", "@codemcp/skills-server"]
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
it("writes skills for multiple selected practices", async () => {
|
|
98
|
+
const catalog = getDefaultCatalog();
|
|
99
|
+
|
|
100
|
+
// Facet order: process (select), architecture (select), practices (multiselect)
|
|
101
|
+
vi.mocked(clack.select)
|
|
102
|
+
.mockResolvedValueOnce("native-agents-md") // process
|
|
103
|
+
.mockResolvedValueOnce("__skip__"); // architecture: skip
|
|
104
|
+
vi.mocked(clack.multiselect)
|
|
105
|
+
.mockResolvedValueOnce(["conventional-commits", "tdd-london"]) // practices
|
|
106
|
+
.mockResolvedValueOnce([]) // docsets: deselect all (conventional-commits has docset)
|
|
107
|
+
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
108
|
+
|
|
109
|
+
await runSetup(dir, catalog);
|
|
110
|
+
|
|
111
|
+
// Both inline skills should exist in .ade/skills/ (staging)
|
|
112
|
+
const commits = await readFile(
|
|
113
|
+
join(dir, ".ade", "skills", "conventional-commits", "SKILL.md"),
|
|
114
|
+
"utf-8"
|
|
115
|
+
);
|
|
116
|
+
expect(commits).toContain("name: conventional-commits");
|
|
117
|
+
expect(commits).toContain("Conventional Commits");
|
|
118
|
+
|
|
119
|
+
const tdd = await readFile(
|
|
120
|
+
join(dir, ".ade", "skills", "tdd-london", "SKILL.md"),
|
|
121
|
+
"utf-8"
|
|
122
|
+
);
|
|
123
|
+
expect(tdd).toContain("name: tdd-london");
|
|
124
|
+
expect(tdd).toContain("London");
|
|
125
|
+
|
|
126
|
+
// Both should be installed to .agentskills/skills/
|
|
127
|
+
await expect(
|
|
128
|
+
access(join(dir, ".agentskills", "skills", "conventional-commits"))
|
|
129
|
+
).resolves.toBeUndefined();
|
|
130
|
+
await expect(
|
|
131
|
+
access(join(dir, ".agentskills", "skills", "tdd-london"))
|
|
132
|
+
).resolves.toBeUndefined();
|
|
133
|
+
|
|
134
|
+
// config.yaml should have array of choices under practices
|
|
135
|
+
const config = await readUserConfig(dir);
|
|
136
|
+
expect(config!.choices.practices).toEqual([
|
|
137
|
+
"conventional-commits",
|
|
138
|
+
"tdd-london"
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
// Lock file should reflect both
|
|
142
|
+
const lock = await readLockFile(dir);
|
|
143
|
+
expect(lock!.logical_config.skills.length).toBeGreaterThanOrEqual(2);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("writes ADR skill with template content", async () => {
|
|
147
|
+
const catalog = getDefaultCatalog();
|
|
148
|
+
|
|
149
|
+
// Facet order: process (select), architecture (select), practices (multiselect)
|
|
150
|
+
vi.mocked(clack.select)
|
|
151
|
+
.mockResolvedValueOnce("native-agents-md") // process
|
|
152
|
+
.mockResolvedValueOnce("__skip__"); // architecture: skip
|
|
153
|
+
vi.mocked(clack.multiselect)
|
|
154
|
+
.mockResolvedValueOnce(["adr-nygard"])
|
|
155
|
+
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
156
|
+
|
|
157
|
+
await runSetup(dir, catalog);
|
|
158
|
+
|
|
159
|
+
const adr = await readFile(
|
|
160
|
+
join(dir, ".ade", "skills", "adr-nygard", "SKILL.md"),
|
|
161
|
+
"utf-8"
|
|
162
|
+
);
|
|
163
|
+
expect(adr).toContain("name: adr-nygard");
|
|
164
|
+
expect(adr).toContain("## Context");
|
|
165
|
+
expect(adr).toContain("## Decision");
|
|
166
|
+
expect(adr).toContain("## Consequences");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("skips both architecture and practices when none selected", async () => {
|
|
170
|
+
const catalog = getDefaultCatalog();
|
|
171
|
+
|
|
172
|
+
// Facet order: process (select), architecture (select), practices (multiselect)
|
|
173
|
+
vi.mocked(clack.select)
|
|
174
|
+
.mockResolvedValueOnce("native-agents-md") // process
|
|
175
|
+
.mockResolvedValueOnce("__skip__"); // architecture: skip
|
|
176
|
+
vi.mocked(clack.multiselect)
|
|
177
|
+
.mockResolvedValueOnce([]) // practices: none
|
|
178
|
+
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
179
|
+
|
|
180
|
+
await runSetup(dir, catalog);
|
|
181
|
+
|
|
182
|
+
// No .ade directory should exist
|
|
183
|
+
await expect(access(join(dir, ".ade"))).rejects.toThrow();
|
|
184
|
+
|
|
185
|
+
// config.yaml should not have architecture or practices keys
|
|
186
|
+
const config = await readUserConfig(dir);
|
|
187
|
+
expect(config!.choices).not.toHaveProperty("architecture");
|
|
188
|
+
expect(config!.choices).not.toHaveProperty("practices");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("exposes practices as skills, not instructions", async () => {
|
|
192
|
+
const catalog = getDefaultCatalog();
|
|
193
|
+
|
|
194
|
+
// Facet order: process (select), architecture (select), practices (multiselect)
|
|
195
|
+
vi.mocked(clack.select)
|
|
196
|
+
.mockResolvedValueOnce("native-agents-md") // process
|
|
197
|
+
.mockResolvedValueOnce("__skip__"); // architecture: skip
|
|
198
|
+
vi.mocked(clack.multiselect)
|
|
199
|
+
.mockResolvedValueOnce(["tdd-london"])
|
|
200
|
+
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
201
|
+
|
|
202
|
+
await runSetup(dir, catalog);
|
|
203
|
+
|
|
204
|
+
// Practice produces a skill, not an instruction
|
|
205
|
+
const skillMd = await readFile(
|
|
206
|
+
join(dir, ".ade", "skills", "tdd-london", "SKILL.md"),
|
|
207
|
+
"utf-8"
|
|
208
|
+
);
|
|
209
|
+
expect(skillMd).toContain("name: tdd-london");
|
|
210
|
+
|
|
211
|
+
// Lock file should have skill but no practice-specific instructions
|
|
212
|
+
const lock = await readLockFile(dir);
|
|
213
|
+
expect(lock!.logical_config.skills.length).toBeGreaterThanOrEqual(1);
|
|
214
|
+
// Only process-facet instructions should be present (from native-agents-md)
|
|
215
|
+
for (const instruction of lock!.logical_config.instructions) {
|
|
216
|
+
expect(instruction).not.toContain("tdd-london");
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it(
|
|
221
|
+
"combines architecture and practices selections",
|
|
222
|
+
{ timeout: 60_000 },
|
|
223
|
+
async () => {
|
|
224
|
+
const catalog = getDefaultCatalog();
|
|
225
|
+
|
|
226
|
+
// Facet order: process (select), architecture (select), practices (multiselect)
|
|
227
|
+
vi.mocked(clack.select)
|
|
228
|
+
.mockResolvedValueOnce("codemcp-workflows") // process
|
|
229
|
+
.mockResolvedValueOnce("tanstack"); // architecture
|
|
230
|
+
vi.mocked(clack.multiselect)
|
|
231
|
+
.mockResolvedValueOnce(["tdd-london", "conventional-commits"]) // practices
|
|
232
|
+
.mockResolvedValueOnce([]) // backpressure: none
|
|
233
|
+
.mockResolvedValueOnce([]) // docsets: deselect all
|
|
234
|
+
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
235
|
+
|
|
236
|
+
await runSetup(dir, catalog);
|
|
237
|
+
|
|
238
|
+
// Architecture skills should exist
|
|
239
|
+
const archSkill = await readFile(
|
|
240
|
+
join(dir, ".ade", "skills", "tanstack-architecture", "SKILL.md"),
|
|
241
|
+
"utf-8"
|
|
242
|
+
);
|
|
243
|
+
expect(archSkill).toContain("name: tanstack-architecture");
|
|
244
|
+
|
|
245
|
+
// Practice skills should exist
|
|
246
|
+
const tddSkill = await readFile(
|
|
247
|
+
join(dir, ".ade", "skills", "tdd-london", "SKILL.md"),
|
|
248
|
+
"utf-8"
|
|
249
|
+
);
|
|
250
|
+
expect(tddSkill).toContain("name: tdd-london");
|
|
251
|
+
|
|
252
|
+
const commitsSkill = await readFile(
|
|
253
|
+
join(dir, ".ade", "skills", "conventional-commits", "SKILL.md"),
|
|
254
|
+
"utf-8"
|
|
255
|
+
);
|
|
256
|
+
expect(commitsSkill).toContain("name: conventional-commits");
|
|
257
|
+
|
|
258
|
+
// config.yaml should have both architecture and practices
|
|
259
|
+
const config = await readUserConfig(dir);
|
|
260
|
+
expect(config!.choices.architecture).toBe("tanstack");
|
|
261
|
+
expect(config!.choices.practices).toEqual([
|
|
262
|
+
"tdd-london",
|
|
263
|
+
"conventional-commits"
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
// Mock only the TUI — everything else is real
|
|
7
|
+
vi.mock("@clack/prompts", () => ({
|
|
8
|
+
intro: vi.fn(),
|
|
9
|
+
outro: vi.fn(),
|
|
10
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
11
|
+
select: vi.fn(),
|
|
12
|
+
multiselect: vi.fn(),
|
|
13
|
+
isCancel: vi.fn().mockReturnValue(false),
|
|
14
|
+
cancel: vi.fn(),
|
|
15
|
+
spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import * as clack from "@clack/prompts";
|
|
19
|
+
import { runSetup } from "./setup.js";
|
|
20
|
+
import { runInstall } from "./install.js";
|
|
21
|
+
import { getDefaultCatalog } from "../../../core/src/catalog/index.js";
|
|
22
|
+
|
|
23
|
+
describe("install integration (real temp dir)", () => {
|
|
24
|
+
let dir: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
dir = await mkdtemp(join(tmpdir(), "ade-install-"));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await rm(dir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("applies lock file to regenerate agent files without re-resolving", async () => {
|
|
36
|
+
const catalog = getDefaultCatalog();
|
|
37
|
+
|
|
38
|
+
// Step 1: Run setup to create config.yaml + config.lock.yaml
|
|
39
|
+
vi.mocked(clack.select)
|
|
40
|
+
.mockResolvedValueOnce("codemcp-workflows") // process
|
|
41
|
+
.mockResolvedValueOnce("__skip__"); // architecture
|
|
42
|
+
vi.mocked(clack.multiselect)
|
|
43
|
+
.mockResolvedValueOnce([]) // practices: none
|
|
44
|
+
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
45
|
+
await runSetup(dir, catalog);
|
|
46
|
+
|
|
47
|
+
// Step 2: Delete agent output files to simulate a fresh clone
|
|
48
|
+
await rm(join(dir, ".mcp.json"));
|
|
49
|
+
await rm(join(dir, ".claude"), { recursive: true, force: true });
|
|
50
|
+
|
|
51
|
+
// Step 3: Run install — should regenerate from config.lock.yaml
|
|
52
|
+
await runInstall(dir, ["claude-code"]);
|
|
53
|
+
|
|
54
|
+
// Agent files should be back
|
|
55
|
+
const agentMd = await readFile(
|
|
56
|
+
join(dir, ".claude", "agents", "ade.md"),
|
|
57
|
+
"utf-8"
|
|
58
|
+
);
|
|
59
|
+
expect(agentMd).toContain("Call whats_next()");
|
|
60
|
+
|
|
61
|
+
const mcpJson = JSON.parse(await readFile(join(dir, ".mcp.json"), "utf-8"));
|
|
62
|
+
expect(mcpJson.mcpServers["workflows"]).toMatchObject({
|
|
63
|
+
command: "npx",
|
|
64
|
+
args: ["@codemcp/workflows-server@latest"]
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("does not modify the lock file", async () => {
|
|
69
|
+
const catalog = getDefaultCatalog();
|
|
70
|
+
|
|
71
|
+
// Setup first
|
|
72
|
+
vi.mocked(clack.select)
|
|
73
|
+
.mockResolvedValueOnce("codemcp-workflows") // process
|
|
74
|
+
.mockResolvedValueOnce("__skip__"); // architecture
|
|
75
|
+
vi.mocked(clack.multiselect)
|
|
76
|
+
.mockResolvedValueOnce([]) // practices: none
|
|
77
|
+
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
78
|
+
await runSetup(dir, catalog);
|
|
79
|
+
|
|
80
|
+
const lockRawBefore = await readFile(
|
|
81
|
+
join(dir, "config.lock.yaml"),
|
|
82
|
+
"utf-8"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Re-install
|
|
86
|
+
await runInstall(dir, ["claude-code"]);
|
|
87
|
+
|
|
88
|
+
const lockRawAfter = await readFile(join(dir, "config.lock.yaml"), "utf-8");
|
|
89
|
+
// Lock file should be byte-identical (install doesn't rewrite it)
|
|
90
|
+
expect(lockRawAfter).toBe(lockRawBefore);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("fails when no config.lock.yaml exists", async () => {
|
|
94
|
+
await expect(runInstall(dir, ["claude-code"])).rejects.toThrow(
|
|
95
|
+
/config\.lock\.yaml not found/i
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("works with native-agents-md option", async () => {
|
|
100
|
+
const catalog = getDefaultCatalog();
|
|
101
|
+
|
|
102
|
+
// Setup with native-agents-md
|
|
103
|
+
vi.mocked(clack.select)
|
|
104
|
+
.mockResolvedValueOnce("native-agents-md") // process
|
|
105
|
+
.mockResolvedValueOnce("__skip__"); // architecture
|
|
106
|
+
vi.mocked(clack.multiselect)
|
|
107
|
+
.mockResolvedValueOnce([]) // practices: none
|
|
108
|
+
.mockResolvedValueOnce(["claude-code"]); // harnesses
|
|
109
|
+
await runSetup(dir, catalog);
|
|
110
|
+
|
|
111
|
+
// Delete agent output
|
|
112
|
+
await rm(join(dir, ".claude"), { recursive: true, force: true });
|
|
113
|
+
|
|
114
|
+
// Re-install
|
|
115
|
+
await runInstall(dir, ["claude-code"]);
|
|
116
|
+
|
|
117
|
+
const agentMd = await readFile(
|
|
118
|
+
join(dir, ".claude", "agents", "ade.md"),
|
|
119
|
+
"utf-8"
|
|
120
|
+
);
|
|
121
|
+
expect(agentMd).toContain("AGENTS.md");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type { LogicalConfig } from "@codemcp/ade-core";
|
|
3
|
+
|
|
4
|
+
// ── Mocks ────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
vi.mock("@clack/prompts", () => ({
|
|
7
|
+
intro: vi.fn(),
|
|
8
|
+
outro: vi.fn(),
|
|
9
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const mockLogical: LogicalConfig = {
|
|
13
|
+
mcp_servers: [],
|
|
14
|
+
instructions: ["test instruction"],
|
|
15
|
+
cli_actions: [],
|
|
16
|
+
knowledge_sources: [],
|
|
17
|
+
skills: [],
|
|
18
|
+
git_hooks: [],
|
|
19
|
+
setup_notes: []
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
vi.mock("@codemcp/ade-core", async (importOriginal) => {
|
|
23
|
+
const actual = (await importOriginal()) as typeof import("@codemcp/ade-core");
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
readLockFile: vi.fn()
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const mockInstall = vi.fn().mockResolvedValue(undefined);
|
|
31
|
+
|
|
32
|
+
vi.mock("@codemcp/ade-harnesses", () => ({
|
|
33
|
+
getHarnessWriter: vi.fn().mockImplementation((id: string) => {
|
|
34
|
+
if (id === "universal" || id === "claude-code" || id === "cursor") {
|
|
35
|
+
return { id, install: mockInstall };
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}),
|
|
39
|
+
getHarnessIds: vi
|
|
40
|
+
.fn()
|
|
41
|
+
.mockReturnValue([
|
|
42
|
+
"universal",
|
|
43
|
+
"claude-code",
|
|
44
|
+
"cursor",
|
|
45
|
+
"copilot",
|
|
46
|
+
"windsurf",
|
|
47
|
+
"cline",
|
|
48
|
+
"roo-code",
|
|
49
|
+
"kiro",
|
|
50
|
+
"opencode"
|
|
51
|
+
]),
|
|
52
|
+
installSkills: vi.fn().mockResolvedValue(undefined)
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
import * as clack from "@clack/prompts";
|
|
56
|
+
import { readLockFile } from "@codemcp/ade-core";
|
|
57
|
+
import { runInstall } from "./install.js";
|
|
58
|
+
|
|
59
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
describe("runInstall", () => {
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
// Re-set the default implementation after clearAllMocks
|
|
65
|
+
const { getHarnessWriter } = await import("@codemcp/ade-harnesses");
|
|
66
|
+
vi.mocked(getHarnessWriter).mockImplementation((id: string) => {
|
|
67
|
+
if (id === "universal" || id === "claude-code" || id === "cursor") {
|
|
68
|
+
return {
|
|
69
|
+
id,
|
|
70
|
+
label: id,
|
|
71
|
+
description: "test",
|
|
72
|
+
install: mockInstall
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("reads config.lock.yaml and applies logical config", async () => {
|
|
80
|
+
vi.mocked(readLockFile).mockResolvedValueOnce({
|
|
81
|
+
version: 1,
|
|
82
|
+
generated_at: "2024-01-01T00:00:00.000Z",
|
|
83
|
+
choices: { process: "codemcp-workflows" },
|
|
84
|
+
logical_config: mockLogical
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await runInstall("/tmp/project");
|
|
88
|
+
|
|
89
|
+
expect(readLockFile).toHaveBeenCalledWith("/tmp/project");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("defaults to universal harness when none specified", async () => {
|
|
93
|
+
vi.mocked(readLockFile).mockResolvedValueOnce({
|
|
94
|
+
version: 1,
|
|
95
|
+
generated_at: "2024-01-01T00:00:00.000Z",
|
|
96
|
+
choices: { process: "codemcp-workflows" },
|
|
97
|
+
logical_config: mockLogical
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await runInstall("/tmp/project");
|
|
101
|
+
|
|
102
|
+
expect(mockInstall).toHaveBeenCalledWith(mockLogical, "/tmp/project");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("uses harnesses from lock file when present", async () => {
|
|
106
|
+
vi.mocked(readLockFile).mockResolvedValueOnce({
|
|
107
|
+
version: 1,
|
|
108
|
+
generated_at: "2024-01-01T00:00:00.000Z",
|
|
109
|
+
choices: { process: "codemcp-workflows" },
|
|
110
|
+
harnesses: ["claude-code", "cursor"],
|
|
111
|
+
logical_config: mockLogical
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await runInstall("/tmp/project");
|
|
115
|
+
|
|
116
|
+
expect(mockInstall).toHaveBeenCalledTimes(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("uses explicit harness ids when provided", async () => {
|
|
120
|
+
vi.mocked(readLockFile).mockResolvedValueOnce({
|
|
121
|
+
version: 1,
|
|
122
|
+
generated_at: "2024-01-01T00:00:00.000Z",
|
|
123
|
+
choices: { process: "codemcp-workflows" },
|
|
124
|
+
harnesses: ["claude-code"],
|
|
125
|
+
logical_config: mockLogical
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await runInstall("/tmp/project", ["cursor"]);
|
|
129
|
+
|
|
130
|
+
// Explicit takes priority over lock file
|
|
131
|
+
expect(mockInstall).toHaveBeenCalledTimes(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("throws when config.lock.yaml is missing", async () => {
|
|
135
|
+
vi.mocked(readLockFile).mockResolvedValueOnce(null);
|
|
136
|
+
|
|
137
|
+
await expect(runInstall("/tmp/project")).rejects.toThrow(
|
|
138
|
+
/config\.lock\.yaml not found/i
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("throws when harness id is unknown", async () => {
|
|
143
|
+
vi.mocked(readLockFile).mockResolvedValueOnce({
|
|
144
|
+
version: 1,
|
|
145
|
+
generated_at: "2024-01-01T00:00:00.000Z",
|
|
146
|
+
choices: { process: "codemcp-workflows" },
|
|
147
|
+
logical_config: mockLogical
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await expect(runInstall("/tmp/project", ["unknown-agent"])).rejects.toThrow(
|
|
151
|
+
/unknown harness/i
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("shows intro and outro messages", async () => {
|
|
156
|
+
vi.mocked(readLockFile).mockResolvedValueOnce({
|
|
157
|
+
version: 1,
|
|
158
|
+
generated_at: "2024-01-01T00:00:00.000Z",
|
|
159
|
+
choices: { process: "codemcp-workflows" },
|
|
160
|
+
logical_config: mockLogical
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await runInstall("/tmp/project");
|
|
164
|
+
|
|
165
|
+
expect(clack.intro).toHaveBeenCalled();
|
|
166
|
+
expect(clack.outro).toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as clack from "@clack/prompts";
|
|
2
|
+
import { readLockFile } from "@codemcp/ade-core";
|
|
3
|
+
import {
|
|
4
|
+
getHarnessWriter,
|
|
5
|
+
getHarnessIds,
|
|
6
|
+
installSkills
|
|
7
|
+
} from "@codemcp/ade-harnesses";
|
|
8
|
+
|
|
9
|
+
export async function runInstall(
|
|
10
|
+
projectRoot: string,
|
|
11
|
+
harnessIds?: string[]
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
clack.intro("ade install");
|
|
14
|
+
|
|
15
|
+
const lockFile = await readLockFile(projectRoot);
|
|
16
|
+
if (!lockFile) {
|
|
17
|
+
throw new Error("config.lock.yaml not found. Run `ade setup` first.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Determine which harnesses to install for:
|
|
21
|
+
// 1. --harness flag (comma-separated)
|
|
22
|
+
// 2. harnesses saved in the lock file
|
|
23
|
+
// 3. default: universal
|
|
24
|
+
const ids = harnessIds ?? lockFile.harnesses ?? ["universal"];
|
|
25
|
+
|
|
26
|
+
const validIds = getHarnessIds();
|
|
27
|
+
for (const id of ids) {
|
|
28
|
+
if (!validIds.includes(id)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Unknown harness "${id}". Available: ${validIds.join(", ")}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const logicalConfig = lockFile.logical_config;
|
|
36
|
+
|
|
37
|
+
for (const id of ids) {
|
|
38
|
+
const writer = getHarnessWriter(id);
|
|
39
|
+
if (writer) {
|
|
40
|
+
await writer.install(logicalConfig, projectRoot);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await installSkills(logicalConfig.skills, projectRoot);
|
|
45
|
+
|
|
46
|
+
if (logicalConfig.knowledge_sources.length > 0) {
|
|
47
|
+
clack.log.info(
|
|
48
|
+
"Knowledge sources configured. Initialize them separately:\n npx @codemcp/knowledge init"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
clack.outro("Install complete!");
|
|
53
|
+
}
|