@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.
@@ -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
+ }