@codemcp/ade-cli 0.2.0 → 0.2.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/dist/index.d.ts +0 -1
- package/dist/index.js +280 -38
- package/package.json +16 -14
- package/.prettierignore +0 -1
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-format.log +0 -6
- package/.turbo/turbo-lint.log +0 -4
- package/.turbo/turbo-test.log +0 -1264
- package/.turbo/turbo-typecheck.log +0 -4
- package/dist/commands/install.d.ts +0 -1
- package/dist/commands/install.js +0 -39
- package/dist/commands/setup.d.ts +0 -2
- package/dist/commands/setup.js +0 -177
- package/dist/knowledge-installer.d.ts +0 -12
- package/dist/knowledge-installer.js +0 -38
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -1
- package/eslint.config.mjs +0 -40
- package/nodemon.json +0 -7
- package/src/commands/conventions.integration.spec.ts +0 -267
- package/src/commands/install.integration.spec.ts +0 -123
- package/src/commands/install.spec.ts +0 -169
- package/src/commands/install.ts +0 -63
- package/src/commands/knowledge.integration.spec.ts +0 -129
- package/src/commands/setup.integration.spec.ts +0 -148
- package/src/commands/setup.spec.ts +0 -442
- package/src/commands/setup.ts +0 -252
- package/src/index.ts +0 -52
- package/src/knowledge-installer.spec.ts +0 -111
- package/src/knowledge-installer.ts +0 -54
- package/src/version.ts +0 -1
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -10
- package/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.vitest.json +0 -7
- package/vitest.config.ts +0 -5
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import type { Catalog, LogicalConfig } from "@codemcp/ade-core";
|
|
3
|
-
|
|
4
|
-
// ── Mocks ────────────────────────────────────────────────────────────────────
|
|
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
|
-
log: { warn: vi.fn(), info: vi.fn() },
|
|
15
|
-
spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
vi.mock("@codemcp/ade-core", async (importOriginal) => {
|
|
19
|
-
const actual = (await importOriginal()) as typeof import("@codemcp/ade-core");
|
|
20
|
-
return {
|
|
21
|
-
...actual,
|
|
22
|
-
readUserConfig: vi.fn().mockResolvedValue(null),
|
|
23
|
-
writeUserConfig: vi.fn().mockResolvedValue(undefined),
|
|
24
|
-
writeLockFile: vi.fn().mockResolvedValue(undefined),
|
|
25
|
-
resolve: vi.fn().mockResolvedValue({
|
|
26
|
-
mcp_servers: [],
|
|
27
|
-
instructions: [],
|
|
28
|
-
cli_actions: [],
|
|
29
|
-
knowledge_sources: [],
|
|
30
|
-
skills: [],
|
|
31
|
-
git_hooks: [],
|
|
32
|
-
setup_notes: []
|
|
33
|
-
} satisfies LogicalConfig),
|
|
34
|
-
collectDocsets: actual.collectDocsets
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
vi.mock("@codemcp/ade-harnesses", () => ({
|
|
39
|
-
allHarnessWriters: [
|
|
40
|
-
{
|
|
41
|
-
id: "claude-code",
|
|
42
|
-
label: "Claude Code",
|
|
43
|
-
description: "test",
|
|
44
|
-
install: vi.fn().mockResolvedValue(undefined)
|
|
45
|
-
}
|
|
46
|
-
],
|
|
47
|
-
getHarnessWriter: vi.fn().mockReturnValue({
|
|
48
|
-
id: "claude-code",
|
|
49
|
-
label: "Claude Code",
|
|
50
|
-
description: "test",
|
|
51
|
-
install: vi.fn().mockResolvedValue(undefined)
|
|
52
|
-
}),
|
|
53
|
-
getHarnessIds: vi.fn().mockReturnValue(["claude-code"]),
|
|
54
|
-
installSkills: vi.fn().mockResolvedValue(undefined),
|
|
55
|
-
writeInlineSkills: vi.fn().mockResolvedValue([])
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
import * as clack from "@clack/prompts";
|
|
59
|
-
import {
|
|
60
|
-
readUserConfig,
|
|
61
|
-
writeUserConfig,
|
|
62
|
-
writeLockFile,
|
|
63
|
-
resolve
|
|
64
|
-
} from "@codemcp/ade-core";
|
|
65
|
-
import { runSetup } from "./setup.js";
|
|
66
|
-
|
|
67
|
-
// ── Test catalog fixture ─────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
const testCatalog: Catalog = {
|
|
70
|
-
facets: [
|
|
71
|
-
{
|
|
72
|
-
id: "process",
|
|
73
|
-
label: "Process",
|
|
74
|
-
description: "How your agent works",
|
|
75
|
-
required: true,
|
|
76
|
-
options: [
|
|
77
|
-
{
|
|
78
|
-
id: "workflow-a",
|
|
79
|
-
label: "Workflow A",
|
|
80
|
-
description: "First workflow option",
|
|
81
|
-
recipe: []
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
id: "workflow-b",
|
|
85
|
-
label: "Workflow B",
|
|
86
|
-
description: "Second workflow option",
|
|
87
|
-
recipe: []
|
|
88
|
-
}
|
|
89
|
-
]
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
id: "testing",
|
|
93
|
-
label: "Testing",
|
|
94
|
-
description: "Testing strategy",
|
|
95
|
-
required: false,
|
|
96
|
-
options: [
|
|
97
|
-
{
|
|
98
|
-
id: "vitest",
|
|
99
|
-
label: "Vitest",
|
|
100
|
-
description: "Use vitest",
|
|
101
|
-
recipe: []
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
id: "jest",
|
|
105
|
-
label: "Jest",
|
|
106
|
-
description: "Use jest",
|
|
107
|
-
recipe: []
|
|
108
|
-
}
|
|
109
|
-
]
|
|
110
|
-
}
|
|
111
|
-
]
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const docsetCatalog: Catalog = {
|
|
115
|
-
facets: [
|
|
116
|
-
{
|
|
117
|
-
id: "arch",
|
|
118
|
-
label: "Architecture",
|
|
119
|
-
description: "Stack",
|
|
120
|
-
required: true,
|
|
121
|
-
options: [
|
|
122
|
-
{
|
|
123
|
-
id: "react",
|
|
124
|
-
label: "React",
|
|
125
|
-
description: "React framework",
|
|
126
|
-
recipe: [],
|
|
127
|
-
docsets: [
|
|
128
|
-
{
|
|
129
|
-
id: "react-docs",
|
|
130
|
-
label: "React Reference",
|
|
131
|
-
origin: "https://github.com/facebook/react.git",
|
|
132
|
-
description: "Official React docs"
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
id: "react-tutorial",
|
|
136
|
-
label: "React Tutorial",
|
|
137
|
-
origin: "https://github.com/reactjs/react.dev.git",
|
|
138
|
-
description: "React learn guide"
|
|
139
|
-
}
|
|
140
|
-
]
|
|
141
|
-
}
|
|
142
|
-
]
|
|
143
|
-
}
|
|
144
|
-
]
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
148
|
-
|
|
149
|
-
describe("runSetup", () => {
|
|
150
|
-
beforeEach(() => {
|
|
151
|
-
vi.clearAllMocks();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("prompts for each catalog facet and writes user config", async () => {
|
|
155
|
-
// User selects "workflow-a" for process, "vitest" for testing
|
|
156
|
-
vi.mocked(clack.select)
|
|
157
|
-
.mockResolvedValueOnce("workflow-a")
|
|
158
|
-
.mockResolvedValueOnce("vitest");
|
|
159
|
-
// Harness multiselect
|
|
160
|
-
vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]);
|
|
161
|
-
|
|
162
|
-
await runSetup("/tmp/test-project", testCatalog);
|
|
163
|
-
|
|
164
|
-
// select() called once per facet
|
|
165
|
-
expect(clack.select).toHaveBeenCalledTimes(2);
|
|
166
|
-
|
|
167
|
-
// writeUserConfig called with collected choices
|
|
168
|
-
expect(writeUserConfig).toHaveBeenCalledWith(
|
|
169
|
-
"/tmp/test-project",
|
|
170
|
-
expect.objectContaining({
|
|
171
|
-
choices: { process: "workflow-a", testing: "vitest" }
|
|
172
|
-
})
|
|
173
|
-
);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("resolves the config and writes the lock file", async () => {
|
|
177
|
-
const mockLogical: LogicalConfig = {
|
|
178
|
-
mcp_servers: [],
|
|
179
|
-
instructions: ["do stuff"],
|
|
180
|
-
cli_actions: [],
|
|
181
|
-
knowledge_sources: [],
|
|
182
|
-
skills: [],
|
|
183
|
-
git_hooks: [],
|
|
184
|
-
setup_notes: []
|
|
185
|
-
};
|
|
186
|
-
vi.mocked(resolve).mockResolvedValueOnce(mockLogical);
|
|
187
|
-
vi.mocked(clack.select)
|
|
188
|
-
.mockResolvedValueOnce("workflow-a")
|
|
189
|
-
.mockResolvedValueOnce("vitest");
|
|
190
|
-
vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]);
|
|
191
|
-
|
|
192
|
-
await runSetup("/tmp/test-project", testCatalog);
|
|
193
|
-
|
|
194
|
-
// resolve() called with the user config, catalog, and a registry
|
|
195
|
-
expect(resolve).toHaveBeenCalledOnce();
|
|
196
|
-
const resolveArgs = vi.mocked(resolve).mock.calls[0];
|
|
197
|
-
expect(resolveArgs[0]).toMatchObject({
|
|
198
|
-
choices: { process: "workflow-a", testing: "vitest" }
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// writeLockFile called with the resolved logical config
|
|
202
|
-
expect(writeLockFile).toHaveBeenCalledWith(
|
|
203
|
-
"/tmp/test-project",
|
|
204
|
-
expect.objectContaining({
|
|
205
|
-
version: 1,
|
|
206
|
-
logical_config: mockLogical
|
|
207
|
-
})
|
|
208
|
-
);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("excludes skipped facets from choices", async () => {
|
|
212
|
-
// User selects workflow-a for process, skips testing (returns null sentinel)
|
|
213
|
-
vi.mocked(clack.select)
|
|
214
|
-
.mockResolvedValueOnce("workflow-a")
|
|
215
|
-
.mockResolvedValueOnce("__skip__");
|
|
216
|
-
vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]);
|
|
217
|
-
|
|
218
|
-
await runSetup("/tmp/test-project", testCatalog);
|
|
219
|
-
|
|
220
|
-
expect(writeUserConfig).toHaveBeenCalledWith(
|
|
221
|
-
"/tmp/test-project",
|
|
222
|
-
expect.objectContaining({
|
|
223
|
-
choices: { process: "workflow-a" }
|
|
224
|
-
})
|
|
225
|
-
);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("aborts without writing files when user cancels", async () => {
|
|
229
|
-
// First select returns a cancel symbol
|
|
230
|
-
const cancelSymbol = Symbol("cancel");
|
|
231
|
-
vi.mocked(clack.select).mockResolvedValueOnce(cancelSymbol);
|
|
232
|
-
vi.mocked(clack.isCancel).mockReturnValue(true);
|
|
233
|
-
|
|
234
|
-
await runSetup("/tmp/test-project", testCatalog);
|
|
235
|
-
|
|
236
|
-
expect(writeUserConfig).not.toHaveBeenCalled();
|
|
237
|
-
expect(writeLockFile).not.toHaveBeenCalled();
|
|
238
|
-
expect(clack.cancel).toHaveBeenCalled();
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe("docset confirmation step", () => {
|
|
242
|
-
it("presents implied docsets as a multiselect after facet selection", async () => {
|
|
243
|
-
vi.mocked(clack.select).mockResolvedValueOnce("react");
|
|
244
|
-
// User accepts all docsets (returns all ids), then harness selection
|
|
245
|
-
vi.mocked(clack.multiselect)
|
|
246
|
-
.mockResolvedValueOnce(["react-docs", "react-tutorial"])
|
|
247
|
-
.mockResolvedValueOnce(["claude-code"]);
|
|
248
|
-
|
|
249
|
-
await runSetup("/tmp/test-project", docsetCatalog);
|
|
250
|
-
|
|
251
|
-
// multiselect should have been called for docsets
|
|
252
|
-
expect(clack.multiselect).toHaveBeenCalledWith(
|
|
253
|
-
expect.objectContaining({
|
|
254
|
-
message: expect.stringContaining("Documentation")
|
|
255
|
-
})
|
|
256
|
-
);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("stores deselected docsets as excluded_docsets in user config", async () => {
|
|
260
|
-
vi.mocked(clack.select).mockResolvedValueOnce("react");
|
|
261
|
-
// User deselects react-tutorial, keeps only react-docs; then harness
|
|
262
|
-
vi.mocked(clack.multiselect)
|
|
263
|
-
.mockResolvedValueOnce(["react-docs"])
|
|
264
|
-
.mockResolvedValueOnce(["claude-code"]);
|
|
265
|
-
|
|
266
|
-
await runSetup("/tmp/test-project", docsetCatalog);
|
|
267
|
-
|
|
268
|
-
expect(writeUserConfig).toHaveBeenCalledWith(
|
|
269
|
-
"/tmp/test-project",
|
|
270
|
-
expect.objectContaining({
|
|
271
|
-
excluded_docsets: ["react-tutorial"]
|
|
272
|
-
})
|
|
273
|
-
);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("does not set excluded_docsets when all docsets are accepted", async () => {
|
|
277
|
-
vi.mocked(clack.select).mockResolvedValueOnce("react");
|
|
278
|
-
vi.mocked(clack.multiselect)
|
|
279
|
-
.mockResolvedValueOnce(["react-docs", "react-tutorial"])
|
|
280
|
-
.mockResolvedValueOnce(["claude-code"]);
|
|
281
|
-
|
|
282
|
-
await runSetup("/tmp/test-project", docsetCatalog);
|
|
283
|
-
|
|
284
|
-
const configArg = vi.mocked(writeUserConfig).mock.calls[0][1];
|
|
285
|
-
expect(configArg.excluded_docsets).toBeUndefined();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("skips docset prompt when no options have docsets", async () => {
|
|
289
|
-
vi.mocked(clack.select)
|
|
290
|
-
.mockResolvedValueOnce("workflow-a")
|
|
291
|
-
.mockResolvedValueOnce("vitest");
|
|
292
|
-
// Only the harness multiselect should be called (no docsets in testCatalog)
|
|
293
|
-
vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]);
|
|
294
|
-
|
|
295
|
-
await runSetup("/tmp/test-project", testCatalog);
|
|
296
|
-
|
|
297
|
-
// multiselect should have been called exactly once (for harnesses only)
|
|
298
|
-
expect(clack.multiselect).toHaveBeenCalledTimes(1);
|
|
299
|
-
expect(clack.multiselect).toHaveBeenCalledWith(
|
|
300
|
-
expect.objectContaining({
|
|
301
|
-
message: expect.stringContaining("Harnesses")
|
|
302
|
-
})
|
|
303
|
-
);
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it("calls intro and outro from @clack/prompts", async () => {
|
|
308
|
-
vi.mocked(clack.select)
|
|
309
|
-
.mockResolvedValueOnce("workflow-a")
|
|
310
|
-
.mockResolvedValueOnce("vitest");
|
|
311
|
-
vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]);
|
|
312
|
-
|
|
313
|
-
await runSetup("/tmp/test-project", testCatalog);
|
|
314
|
-
|
|
315
|
-
expect(clack.intro).toHaveBeenCalled();
|
|
316
|
-
expect(clack.outro).toHaveBeenCalled();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("displays each setup note via clack.log.info", async () => {
|
|
320
|
-
const mockLogical: LogicalConfig = {
|
|
321
|
-
mcp_servers: [],
|
|
322
|
-
instructions: [],
|
|
323
|
-
cli_actions: [],
|
|
324
|
-
knowledge_sources: [],
|
|
325
|
-
skills: [],
|
|
326
|
-
git_hooks: [],
|
|
327
|
-
setup_notes: ["Add lint script to package.json", "Run npm install"]
|
|
328
|
-
};
|
|
329
|
-
vi.mocked(resolve).mockResolvedValueOnce(mockLogical);
|
|
330
|
-
vi.mocked(clack.select)
|
|
331
|
-
.mockResolvedValueOnce("workflow-a")
|
|
332
|
-
.mockResolvedValueOnce("vitest");
|
|
333
|
-
vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]);
|
|
334
|
-
|
|
335
|
-
await runSetup("/tmp/test-project", testCatalog);
|
|
336
|
-
|
|
337
|
-
expect(clack.log.info).toHaveBeenCalledWith(
|
|
338
|
-
"Add lint script to package.json"
|
|
339
|
-
);
|
|
340
|
-
expect(clack.log.info).toHaveBeenCalledWith("Run npm install");
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
describe("re-run with existing config", () => {
|
|
344
|
-
it("passes existing single-select choice as initialValue", async () => {
|
|
345
|
-
vi.mocked(readUserConfig).mockResolvedValueOnce({
|
|
346
|
-
choices: { process: "workflow-b", testing: "jest" }
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
vi.mocked(clack.select)
|
|
350
|
-
.mockResolvedValueOnce("workflow-b")
|
|
351
|
-
.mockResolvedValueOnce("jest");
|
|
352
|
-
vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]);
|
|
353
|
-
|
|
354
|
-
await runSetup("/tmp/test-project", testCatalog);
|
|
355
|
-
|
|
356
|
-
// First select (process) should receive initialValue "workflow-b"
|
|
357
|
-
expect(clack.select).toHaveBeenCalledWith(
|
|
358
|
-
expect.objectContaining({ initialValue: "workflow-b" })
|
|
359
|
-
);
|
|
360
|
-
// Second select (testing) should receive initialValue "jest"
|
|
361
|
-
expect(clack.select).toHaveBeenCalledWith(
|
|
362
|
-
expect.objectContaining({ initialValue: "jest" })
|
|
363
|
-
);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("passes existing multi-select choices as initialValues", async () => {
|
|
367
|
-
const multiCatalog: Catalog = {
|
|
368
|
-
facets: [
|
|
369
|
-
{
|
|
370
|
-
id: "practices",
|
|
371
|
-
label: "Practices",
|
|
372
|
-
description: "Dev practices",
|
|
373
|
-
required: false,
|
|
374
|
-
multiSelect: true,
|
|
375
|
-
options: [
|
|
376
|
-
{
|
|
377
|
-
id: "tdd",
|
|
378
|
-
label: "TDD",
|
|
379
|
-
description: "Test-driven dev",
|
|
380
|
-
recipe: []
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
id: "adr",
|
|
384
|
-
label: "ADR",
|
|
385
|
-
description: "Architecture decisions",
|
|
386
|
-
recipe: []
|
|
387
|
-
}
|
|
388
|
-
]
|
|
389
|
-
}
|
|
390
|
-
]
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
vi.mocked(readUserConfig).mockResolvedValueOnce({
|
|
394
|
-
choices: { practices: ["tdd", "adr"] }
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
vi.mocked(clack.multiselect)
|
|
398
|
-
.mockResolvedValueOnce(["tdd", "adr"])
|
|
399
|
-
.mockResolvedValueOnce(["claude-code"]);
|
|
400
|
-
|
|
401
|
-
await runSetup("/tmp/test-project", multiCatalog);
|
|
402
|
-
|
|
403
|
-
expect(clack.multiselect).toHaveBeenCalledWith(
|
|
404
|
-
expect.objectContaining({ initialValues: ["tdd", "adr"] })
|
|
405
|
-
);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
it("warns when existing choice references a stale option", async () => {
|
|
409
|
-
vi.mocked(readUserConfig).mockResolvedValueOnce({
|
|
410
|
-
choices: { process: "workflow-a", testing: "mocha" } // "mocha" doesn't exist
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
vi.mocked(clack.select)
|
|
414
|
-
.mockResolvedValueOnce("workflow-a")
|
|
415
|
-
.mockResolvedValueOnce("vitest");
|
|
416
|
-
vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]);
|
|
417
|
-
|
|
418
|
-
await runSetup("/tmp/test-project", testCatalog);
|
|
419
|
-
|
|
420
|
-
expect(clack.log.warn).toHaveBeenCalledWith(
|
|
421
|
-
expect.stringContaining("mocha")
|
|
422
|
-
);
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it("does not set initialValue for stale option", async () => {
|
|
426
|
-
vi.mocked(readUserConfig).mockResolvedValueOnce({
|
|
427
|
-
choices: { process: "deleted-option" }
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
vi.mocked(clack.select)
|
|
431
|
-
.mockResolvedValueOnce("workflow-a")
|
|
432
|
-
.mockResolvedValueOnce("vitest");
|
|
433
|
-
vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]);
|
|
434
|
-
|
|
435
|
-
await runSetup("/tmp/test-project", testCatalog);
|
|
436
|
-
|
|
437
|
-
// First select (process) should NOT have initialValue set
|
|
438
|
-
const firstCall = vi.mocked(clack.select).mock.calls[0][0];
|
|
439
|
-
expect(firstCall).not.toHaveProperty("initialValue");
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
});
|
package/src/commands/setup.ts
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import * as clack from "@clack/prompts";
|
|
2
|
-
import {
|
|
3
|
-
type Catalog,
|
|
4
|
-
type Facet,
|
|
5
|
-
type UserConfig,
|
|
6
|
-
type LockFile,
|
|
7
|
-
readUserConfig,
|
|
8
|
-
writeUserConfig,
|
|
9
|
-
writeLockFile,
|
|
10
|
-
resolve,
|
|
11
|
-
collectDocsets,
|
|
12
|
-
createDefaultRegistry,
|
|
13
|
-
getFacet,
|
|
14
|
-
getOption,
|
|
15
|
-
sortFacets,
|
|
16
|
-
getVisibleOptions
|
|
17
|
-
} from "@codemcp/ade-core";
|
|
18
|
-
import {
|
|
19
|
-
allHarnessWriters,
|
|
20
|
-
getHarnessWriter,
|
|
21
|
-
installSkills,
|
|
22
|
-
writeInlineSkills
|
|
23
|
-
} from "@codemcp/ade-harnesses";
|
|
24
|
-
|
|
25
|
-
export async function runSetup(
|
|
26
|
-
projectRoot: string,
|
|
27
|
-
catalog: Catalog
|
|
28
|
-
): Promise<void> {
|
|
29
|
-
clack.intro("ade setup");
|
|
30
|
-
|
|
31
|
-
const existingConfig = await readUserConfig(projectRoot);
|
|
32
|
-
const existingChoices = existingConfig?.choices ?? {};
|
|
33
|
-
|
|
34
|
-
// Warn about stale choices that reference options no longer in the catalog
|
|
35
|
-
for (const [facetId, value] of Object.entries(existingChoices)) {
|
|
36
|
-
const facet = getFacet(catalog, facetId);
|
|
37
|
-
if (!facet) continue;
|
|
38
|
-
|
|
39
|
-
const ids = Array.isArray(value) ? value : [value];
|
|
40
|
-
for (const optionId of ids) {
|
|
41
|
-
if (!getOption(facet, optionId)) {
|
|
42
|
-
clack.log.warn(
|
|
43
|
-
`Previously selected option "${optionId}" is no longer available in facet "${facet.label}".`
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const choices: Record<string, string | string[]> = {};
|
|
50
|
-
|
|
51
|
-
const sortedFacets = sortFacets(catalog);
|
|
52
|
-
|
|
53
|
-
for (const facet of sortedFacets) {
|
|
54
|
-
const visibleOptions = getVisibleOptions(facet, choices, catalog);
|
|
55
|
-
if (visibleOptions.length === 0) continue;
|
|
56
|
-
|
|
57
|
-
const visibleFacet = { ...facet, options: visibleOptions };
|
|
58
|
-
|
|
59
|
-
if (facet.multiSelect) {
|
|
60
|
-
const selected = await promptMultiSelect(visibleFacet, existingChoices);
|
|
61
|
-
if (typeof selected === "symbol") {
|
|
62
|
-
clack.cancel("Setup cancelled.");
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (selected.length > 0) {
|
|
66
|
-
choices[facet.id] = selected;
|
|
67
|
-
}
|
|
68
|
-
} else {
|
|
69
|
-
const selected = await promptSelect(visibleFacet, existingChoices);
|
|
70
|
-
if (typeof selected === "symbol") {
|
|
71
|
-
clack.cancel("Setup cancelled.");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (typeof selected === "string" && selected !== "__skip__") {
|
|
75
|
-
choices[facet.id] = selected;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Docset confirmation step: collect implied docsets, let user deselect
|
|
81
|
-
const impliedDocsets = collectDocsets(choices, catalog);
|
|
82
|
-
let excludedDocsets: string[] | undefined;
|
|
83
|
-
|
|
84
|
-
if (impliedDocsets.length > 0) {
|
|
85
|
-
const selected = await clack.multiselect({
|
|
86
|
-
message: "Documentation — deselect any you don't need",
|
|
87
|
-
options: impliedDocsets.map((d) => ({
|
|
88
|
-
value: d.id,
|
|
89
|
-
label: d.label,
|
|
90
|
-
hint: d.description
|
|
91
|
-
})),
|
|
92
|
-
initialValues: impliedDocsets.map((d) => d.id),
|
|
93
|
-
required: false
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
if (typeof selected === "symbol") {
|
|
97
|
-
clack.cancel("Setup cancelled.");
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const selectedSet = new Set(selected as string[]);
|
|
102
|
-
const excluded = impliedDocsets
|
|
103
|
-
.filter((d) => !selectedSet.has(d.id))
|
|
104
|
-
.map((d) => d.id);
|
|
105
|
-
if (excluded.length > 0) {
|
|
106
|
-
excludedDocsets = excluded;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Harness selection — multi-select from all available harnesses
|
|
111
|
-
const existingHarnesses = existingConfig?.harnesses;
|
|
112
|
-
const harnessOptions = allHarnessWriters.map((w) => ({
|
|
113
|
-
value: w.id,
|
|
114
|
-
label: w.label,
|
|
115
|
-
hint: w.description
|
|
116
|
-
}));
|
|
117
|
-
|
|
118
|
-
const validExistingHarnesses = existingHarnesses?.filter((h) =>
|
|
119
|
-
allHarnessWriters.some((w) => w.id === h)
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const selectedHarnesses = await clack.multiselect({
|
|
123
|
-
message: "Harnesses — which coding agents should receive config?",
|
|
124
|
-
options: harnessOptions,
|
|
125
|
-
initialValues:
|
|
126
|
-
validExistingHarnesses && validExistingHarnesses.length > 0
|
|
127
|
-
? validExistingHarnesses
|
|
128
|
-
: ["universal"],
|
|
129
|
-
required: false
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
if (typeof selectedHarnesses === "symbol") {
|
|
133
|
-
clack.cancel("Setup cancelled.");
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const harnesses = selectedHarnesses as string[];
|
|
138
|
-
|
|
139
|
-
const userConfig: UserConfig = {
|
|
140
|
-
choices,
|
|
141
|
-
...(excludedDocsets && { excluded_docsets: excludedDocsets }),
|
|
142
|
-
...(harnesses.length > 0 && { harnesses })
|
|
143
|
-
};
|
|
144
|
-
const registry = createDefaultRegistry();
|
|
145
|
-
const logicalConfig = await resolve(userConfig, catalog, registry);
|
|
146
|
-
|
|
147
|
-
await writeUserConfig(projectRoot, userConfig);
|
|
148
|
-
|
|
149
|
-
const lockFile: LockFile = {
|
|
150
|
-
version: 1,
|
|
151
|
-
generated_at: new Date().toISOString(),
|
|
152
|
-
choices: userConfig.choices,
|
|
153
|
-
...(harnesses.length > 0 && { harnesses }),
|
|
154
|
-
logical_config: logicalConfig
|
|
155
|
-
};
|
|
156
|
-
await writeLockFile(projectRoot, lockFile);
|
|
157
|
-
|
|
158
|
-
// Install to all selected harnesses
|
|
159
|
-
for (const harnessId of harnesses) {
|
|
160
|
-
const writer = getHarnessWriter(harnessId);
|
|
161
|
-
if (writer) {
|
|
162
|
-
await writer.install(logicalConfig, projectRoot);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const modifiedSkills = await writeInlineSkills(logicalConfig, projectRoot);
|
|
167
|
-
if (modifiedSkills.length > 0) {
|
|
168
|
-
clack.log.warn(
|
|
169
|
-
`The following skills have been locally modified and will NOT be updated:\n` +
|
|
170
|
-
modifiedSkills.map((s) => ` - ${s}`).join("\n") +
|
|
171
|
-
`\n\nTo use the latest defaults, remove .ade/skills/ and re-run setup.`
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
await installSkills(logicalConfig.skills, projectRoot);
|
|
176
|
-
|
|
177
|
-
if (logicalConfig.knowledge_sources.length > 0) {
|
|
178
|
-
clack.log.info(
|
|
179
|
-
"Knowledge sources selected. Initialize them separately:\n npx @codemcp/knowledge init"
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
for (const note of logicalConfig.setup_notes) {
|
|
184
|
-
clack.log.info(note);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
clack.outro("Setup complete!");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function getValidInitialValue(
|
|
191
|
-
facet: Facet,
|
|
192
|
-
existingChoices: Record<string, string | string[]>
|
|
193
|
-
): string | undefined {
|
|
194
|
-
const value = existingChoices[facet.id];
|
|
195
|
-
if (typeof value !== "string") return undefined;
|
|
196
|
-
// Only set initialValue if the option still exists in the catalog
|
|
197
|
-
return facet.options.some((o) => o.id === value) ? value : undefined;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function getValidInitialValues(
|
|
201
|
-
facet: Facet,
|
|
202
|
-
existingChoices: Record<string, string | string[]>
|
|
203
|
-
): string[] | undefined {
|
|
204
|
-
const value = existingChoices[facet.id];
|
|
205
|
-
if (!Array.isArray(value)) return undefined;
|
|
206
|
-
// Only include options that still exist in the catalog
|
|
207
|
-
const valid = value.filter((v) => facet.options.some((o) => o.id === v));
|
|
208
|
-
return valid.length > 0 ? valid : undefined;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function promptSelect(
|
|
212
|
-
facet: Facet,
|
|
213
|
-
existingChoices: Record<string, string | string[]>
|
|
214
|
-
) {
|
|
215
|
-
const options = facet.options.map((o) => ({
|
|
216
|
-
value: o.id,
|
|
217
|
-
label: o.label,
|
|
218
|
-
hint: o.description
|
|
219
|
-
}));
|
|
220
|
-
|
|
221
|
-
if (!facet.required) {
|
|
222
|
-
options.push({ value: "__skip__", label: "Skip", hint: "" });
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const initialValue = getValidInitialValue(facet, existingChoices);
|
|
226
|
-
|
|
227
|
-
return clack.select({
|
|
228
|
-
message: facet.label,
|
|
229
|
-
options,
|
|
230
|
-
...(initialValue !== undefined && { initialValue })
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function promptMultiSelect(
|
|
235
|
-
facet: Facet,
|
|
236
|
-
existingChoices: Record<string, string | string[]>
|
|
237
|
-
) {
|
|
238
|
-
const options = facet.options.map((o) => ({
|
|
239
|
-
value: o.id,
|
|
240
|
-
label: o.label,
|
|
241
|
-
hint: o.description
|
|
242
|
-
}));
|
|
243
|
-
|
|
244
|
-
const initialValues = getValidInitialValues(facet, existingChoices);
|
|
245
|
-
|
|
246
|
-
return clack.multiselect({
|
|
247
|
-
message: facet.label,
|
|
248
|
-
options,
|
|
249
|
-
required: false,
|
|
250
|
-
...(initialValues !== undefined && { initialValues })
|
|
251
|
-
});
|
|
252
|
-
}
|